Logo universita'

Università degli Studi di Cagliari

Facoltà di Ingegneria - Corso di laurea in Ingegneria Elettronica


TESINA

di Ingegneria del Software

Anno accademico 1997/98

 



ICARO

Studio della installazione dell'Aglet Software Development Kit in ambiente Linux e sviluppo di un sistema di monitoraggio di server attraverso agenti mobili

Docente:

Realizzatori:


Sommario

* Introduzione e presentazione del progetto
* Introduzione agli agenti mobili e all'architettura Aglets
* Installazione dell'ASDK in ambiente Linux
* Uso dell'ASDK
* ICARO: il sorgente Java
* Conclusioni
* Strumenti utilizzati
* Bibliografia
* Ringraziamenti

Introduzione

Presentazione

Nell'ambito dello studio di applicazioni ad oggetti distribuiti una delle tecnologie proposte si basa sul concetto di agente mobile. Tra i vari prodotti commerciali disponibili ha riscosso un buon successo quello sviluppato da I.B.M. Japan, basato sul linguaggio Java. Questo sistema ad agenti, denominati Aglets, offre alta portabilità, una architettura robusta e sicura, unite ad un supporto completo per il programmatore.

L'ambiente di sviluppo degli Aglets (ASDK - Aglets Software Development Kit) è interamente realizzato in Java ed è liberamente scaricabile dal sito I.B.M.; questo particolare, unito al fatto che anche il JDK è distribuito gratuitamente dalla Sun Microsystems, rende gli Aglets una soluzione economica per implementare oggetti distribuiti.

Benchè scritto in 100% Pure Java, l'ASDK è stato testato (ed è in un certo senso "garantito") solo sulle piattaforme commerciali Microsoft Windows 95/NT, Sun Solaris e SunOS, I.B.M. OS/2 e AIX. Partendo dal presupposto di portabilità di codice Java in diverse piattaforme, scopo del nostro progetto è

Il progetto è stato denominato ICARO, le cui vocali individuano l'acronimo Intelligent Agent-based Observer.


Introduzione agli agenti mobili e all'architettura Aglets

Gli agenti mobili

Negli ultimi anni si è sviluppato il paradigma ad oggetti distribuiti che, sfruttando le moderne architteture di rete, si prefigge l'obiettivo di rendere possibile la realizzazione di sistemi di calcolo potenti ed estremamente flessibili. Locale e remoto perdono di importanza e diversi oggetti, appunto "distribuiti", cooperano per ottenere il risultato richiesto dall'utente. In una visione di questo tipo rientrano esempi di macchine parallele virtuali, database eterogenei e soprattutto la possibilità di ottenere grosse capacità di calcolo combinando assieme diverse componenti semplici. Accanto alle soluzioni "statiche" (in cui gli oggetti, pur essendo distribuiti risiedono permanentemente su una macchina) come CORBA del consorzio OMG, DCOM di Microsoft e RMI della JavaSoft, esiste la cosìddetta tecnologia ad agenti mobili. La loro caratteristica principale consiste nell'essere oggetti distribuiti dotati appunto di mobilità, cioè della possibilità di spostarsi dinamicamente da una macchina ad un'altra a seconda del compito da svolgere.

Oltre a questa caratteristica peculiare, gli agenti mobili possono essere sviluppati in modo che siano autonomi e reattivi rispetto all'ambiente di esecuzione in cui si trovano (per esempio in grado di ricevere dal sistema la notifica di un imminente shutdown e di conseguenza decidere di migrare verso un'altra macchina), dotati della possibilità di comunicare ed eventualmente di "imparare" durante il loro ciclo di vita.

Vi sono diverse buone ragioni per utilizzare la tecnologia ad agenti mobili:

Tra le diverse soluzioni proposte una delle piu' sofisticate e' quella degli Aglets sviluppata da I.B.M. e basata sul linguaggio Java.

L'architettura Aglets

Gli Aglets sono una architettura ad agenti mobili interamente sviluppata in Java; questa prima caratteristica li rende intrinsecamente multipiattaforma. Per poter utilizzare gli Aglets è sufficente disporre di una Java Virtual Machine e di uno stack TCP/IP nel proprio sistema operativo.

Sostanzialmente l'architettura degli Aglets è basata su:

L'uso di Java fornisce in partenza agli Aglets caratteristiche di multithreading, sicurezza nonchè le potenzialità offerte dalla programmazione ad oggetti. I programmatori I.B.M. hanno ulteriormente potenziato queste caratteristiche di Java con delle soluzioni dedicate quali ad esempio i proxy (utilizzati per aumentare la sicurezza degli aglet all'interno di un contesto) e un sistema di messaggistica.

Tutti gli elementi qui descritti costituiscono l'ASDK (Aglet Software Development Kit).


Installazione ASDK in ambiente Linux

Preliminari

I requisiti di sistema:

Per poter installare l'ASDK (Aglets Software Development Kit) sulla propria macchina è necessario disporre del JDK (Java Development Kit) versione 1.1 o successiva. Per l'ambiente Linux non è disponibile il JDK ufficiale sviluppato dalla Sun, mentre esiste un porting indipendente (aggiornato all'API 1.1) sviluppato appositamente per Linux e liberamente scaricabile dalla rete.

Pur avendo utilizzato l'ultima versione disponibile della distribuzione Debian (1.3.1) si aveva a disposizione una versione di tale JDK che non implementava tutte le API 1.1 più recenti, in particolare la libreria di classi e la relativa interfaccia sulla JVM (Java Virtual Machine) risultava carente nel supporto al sistema grafico (X11 - Motif) e ad alcune nuove estensioni del linguaggio. La versione in questione è la chapman: 10/12/12-23:12. Tali errori si manifestavano già in fase di installazione, a causa del fatto che il pacchetto ASDK disponibile era in formato .class autoinstallante (realizzata attraverso InstallShield).

gulch$ java Agletsl_0
InstallShield JavaTM Edition
Extracting installation code.............................done

File not found (/usr/lib/jdk/i586/libawt.so)
java.lang.UnsatisfiedLinkError: no awt in shared library path
at java.lang.Runtime.loadLibrary(Runtime.java)
at java.lang.System.loadLibrary(System.java)
at sun.awt.motif.MToolkit.(MToolkit.java:37)
at java.awt.Toolkit.getDefaultToolkit(Toolkit.java:199)
at java.awt.Component.getToolkit(Component.java:170)
at java.awt.Component.getFontMetrics(component.java:574)

SIGSEGV 11* segmentation violation

Full thread dump:
"Finalizer thread" (TID:0x403d03b0, syc_thread_t:0x411daf2c) prio=l
"Async Garbage Collector" (TID:0x403d0368, sys_thread_t:0x411b8f2c) prio=l
"Idle thread" (TID:0x403d0320, sys_thread_t:0x41196f2c) prio=0
"clock handler" (TID:0x403d01f8, sys_thread_t:0x41174f2c) prio=11
"main" (TID:0x403d0OaO, sys_thread t:0x804ce88) prio=5 *current thread*
java.lang.Throwable.printStackTrace(Throwable.java)
java.awt.Toolkit.getDefaultToolkit(Toolkit.java:199)
java.awt.Component.getToolkit(Component.java:170)
java.awt.Component.getFontMetrics(Component.java:574)

Killed

In pratica il file classes.zip fornito con la versione chapman conteneva le tutte dichiarazioni dell'API 1.1, che però non trovavano implementazione sulla JVM (da qui il messaggio no awt in shared library path in /usr/lib/jdk/i586/libawt.so).

È stato necessario installare una versione del JDK sempre compatibile con l'API 1.1 ma dotata di pieno supporto a tutta la libreria. La versione risultata pienamente rispondente alle esigenze è la 1.1.3 (versione sbb:08/16/97-14:45). Il file "incriminato" (jdk/i586/libawt.so) è passato da 211018 byte a ben 1877689 byte!

Installazione JDK

Una volta scaricato e decompresso l'archivio (LINUX.JDK.1.1.3-v2.tar.gz) la procedura di installazione si riduce a settare alcune variabili d'ambiente e alcuni link simbolici. Supponendo d'aver installato il JDK nella directory /usr/local/jdk sarà necessario includere nel PATH la posizione degli eseguibili java e javac

Nota: si presuppone 1'uso di BASH come SHELL di sistema

inserendo

PATH=$PATH:/usr/1oca1/jdk/bin

nel file /etc/profile (nella configurazione globale della macchina) o nel file ~/.profile (nella configurazione di un singolo utente). Nello stesso file sarà inoltre necessario specificare il percorso delle librerie standard di Java:

CLASSPATH=/usr/local/jdk/lib/classes.zip

È necessario infine aggiornare alcuni link simbolici presenti nella distribuzione Debian in modo tale che puntino agli eseguibili del JDK appena installato (l'effettiva procedura dipende, oltre che dalla distribuzione installata, anche dal tipo di pacchetti installati, per cui non si riportano dettagliatamente i passi effettuati).

Installazione ASDK

L'installazione dell'ASDK è quasi totalmente automatica: eseguendo il file di classe Aglets1_0.class (java Aglets1_0) il sistema, dopo aver chiesto di specificare la directory di destinazione, decomprime le classi e le relative directory.

L'ASDK necessita di 7 variabili d'ambiente, dedicate all'aglet server e all'ambiente di sviluppo.

AGLET_HOME=/home/g10/java/Agletsl.0
ASDK_CLASSPATH=/usr/local/jdk/lib/classes.zip
ASDK_JAVA=/usr/local/jdk/bin/java
PATH=$PATH:$AGLET_HOME/bin
CLASSPATH=$AGLET_HOME/lib:$CLASSPATH
AGLET_PATH=/home/g10/tesina/aglet
AGLET_EXPORT_PATH=$AGLET_PATH
export AGLET_HOME
export ASDK_CLASSPATH
export ASDK_JAVA
export PATH
export CLASSPATH
export AGLET_PATH
export AGLET_EXPORT_PATH

Vediamone in dettaglio la funzione:

AGLET_HOME
Indica la directory ove è installato l'ASDK (è quella specificata in fase di installazione nella finestra di dialogo mostrata sopra);
ASDK_CLASSPATH
Indica all'aglet server dove risiede la libreria standard del JDK installato sulla macchina (come ogni CLASSPATH è necessario specificare, oltre al nome della directory, anche il nome del file - nel nostro caso classes.zip);
ASDK_JAVA
Indica all'ASDK dove trovare l'interprete Java (tutta l'infrastruttura degli aglet, essendo scritta interamente in Java, necessita di una Java Virtual Machine per poter essere eseguito);
PATH
Include nel PATH corrente anche la directory contenente gli eseguibili dell'aglet-server;
CLASSPATH
Aggiorna la variabile d'ambiente definita nell'ambito del JDK includendo la libreria di classi propria dell'Aglet Software Development Kit;
AGLET_PATH
Indica il percorso standard dove l'aglet viewer deve andare a recuperare gli aglet locali;
AGLET_EXPORT_PATH
Individua una o più regioni del filesystem ove sono collocati gli aglet che potranno essere inviati ad altri host.

È importante notare che i file di classe vengono trasferiti da un server ad un altro solo se essi sono presenti all'interno dei percorsi specificati da AGLET_EXPORT_PATH del server di origine.

Nota:


Uso dell'ASDK

Una volta installato l'ASDK, per avviare l'aglet server è sufficiente eseguire lo shell script $AGLET_HOME/bin/agletsd (che, avendo settato le variabili d'ambiente, dovrebbe essere raggiungibile senza specificarne il percorso). Al primo avvio il sistema crea automaticamente alcuni file nella home directory dell'utente che esegue l'aglet server. Tali file, contenuti nella directory .aglets, contengono la configurazione dell'aglet server stesso: parametri di sicurezza (accesso a risorse locali e remote), parametri dell'interfaccia grafica, personalizzazioni dell'utente.

Il fatto che i file di configurazione siano contenuti all'interno della home directory dell'utente che esegue di volta in volta l'aglet server permette, in un ambito multiutente come quello di Linux, di separare l'installazione dell'ASDK dalla sua gestione: la prima sarà compito dell'amministratore di sistema (che potrà ad esempio stabilire la porta TCP/IP d'appoggio), la seconda sarà compito degli utenti (benchè l'ASDK non ponga vincoli di questo tipo).

ALCUNE NOTE SULLA SICUREZZA
  L'ASDK può essere installato ed eseguito da qualsiasi utente della macchina; chiunque può attivare il proprio aglet server e dunque creare potenzialmente un buco di sicurezza nella macchina ospite. Di più, ciascun utente può stabilire il tipo di risorse a cui aglet locali e remoti possono accedere (il loro accesso avrà le stesse restrizioni dell'utente che esegue il server). In uno scenario di questo tipo utenti malintenzionati potrebbero installare il proprio aglet server con permessi di lettura sul filesystem aperti verso aglet remoti.
A questo punto un aglet di semplicissima realizzazione potrebbe leggere il file delle password (/etc/passwd, aperto in lettura a tutti gli utenti), decriptare la password di root e tornare all'host di partenza con la preziosa informazione senza che questa attività sia in alcun modo tracciata sul sistema. Una situazione come quella appena descritta può essere facilmente risolta utilizzando il sistema delle Shadow-Password, ma un programmatore più esperto potrebbe sfruttare bug della JVM per raggiungere i suoi scopi.

Gli sviluppatori dell'ASDK ribadiscono che spedire un agente da una macchina all'altra equivale ad inviare un messaggio di posta elettronica: per questo motivo al primo avvio è necessario compilare un apposito modulo di registrazione con proprio nome, ente di appartenenza e indirizzo di posta elettronica. Tali informazioni accompagneranno ogni agente che partirà dalla nostra macchina.

L'interfaccia dell'aglet server può considerarsi divisa in due parti: una consolle (Java consolle) e l'aglet viewer (nel nostro caso Tahiti):

La prima (in modalità testo) compare all'interno della shell da cui è stato avviato il server e notifica informazioni di servizio: accesso ai file di configurazione, errori di sistema - del server e della JVM -, eccezioni non raccolte. La seconda (in modalita grafica) è chiamata Tahiti (detta anche aglet viewer) e costituisce per cosi dire il pannello di controllo del server stesso. Attraverso Tahiti è possibile maneggiare aglet locali e remoti, settare protezioni e permessi, monitorare l'attivita del sistema (per dettagli si rimanda alla documentazione ufficiale).

Nota:

Dai sette pulsanti della finestra di Tahiti è possibile creare, clonare e distruggere gli aglet, recuperarli o spedirli attraverso la retela rete, visualizzare la finestra di dialogo e avere informazioni su quelli attivi. Le stesse funzionalita sono attivabili da menù: attraverso i quali à possibile selezionare tutte le altre funzioni disponibili (parametri di sicurezza e utente).

Per creare un aglet (non per svilupparlo ma per eseguirlo) è necessario selezionare la funzione Create dell'aglet viewer, che presenta una finestra di dialogo da cui è possibile selezionare aglet locali o remoti.

Per individuare un aglet è necessario fornire l'URL preciso da cui scaricarlo: oltre a specificare il nome della classe (senza l'estensione .class) è necessario specificare protocollo, nome dell'host, porta, percorso dell'interno dell'host ove risiede l'aglet. Per aglet locali è possibile settare un'apposita variabile d'ambiente (AGLET_PATH) per mezzo della quale quale è possibile richiamare degli aglet specificando solamente il nome del file .class. In tutti gli altri casi sarà necessario specificare il percorso esatto:

agenti locali
file:/percorso/del/file/
file:///percorso/del/file/
agenti remoti
protocollo://nomeHost:porta/percorso/del/file/

Nota:

Tramite un apposito file di registro (~home_directory_utente/.aglets/aglet.properties) è possibile conservare liste di host e aglet a cui si accede di frequente. Esistono due modalita per la creazione degli aglet. La prima (Create) carica se necessario ed esegue il codice di classe dell'Aglet specificato; la seconda (Reload and Create) forza la Garbage Collection della JVM e ricarica il file di classe. Questa seconda modalità è utile in fase di sviluppo e debugging perchè forza il sistema a caricare sempre e comunque l'ultima versione del file di classe.

Un altro metodo per rendere attivi degli aglet nel contesto corrente è quello di recuperarli da una macchina remota; perchè ciò sia possibile è necessario che l'aglet sia attivo nel contesto della macchina remota e che il codice delle classi che lo costituiscono siano all'interno del percorso individuato dalla variabile d'ambiente AGLET_EXPORT_PATH. Senza questo presupposto il class loader della macchina locale non è in grado di recuperare il codice.

Per tutte le altre caratteristiche dell'aglet viewer si rimanda alla documentazione ufficiale.


Il codice sorgente

Note introduttive

Una volta installato l'ASDK in ambiente Linux se ne voleva verificare il funzionamento sviluppando un piccolo agente mobile di test. Abbiamo così creato ICARO, l'Intelligent Agent-based Observer, un agente in grado, una volta giunto su una macchina Linux dotato di aglet server, di leggere lo stato della macchina e tornare all'host di partenza. Altre caratteristiche del programma sono l'interfaccia grafica completa e la gestione di un ampio numero di eccezioni. Il suo funzionamento si basa sulla lettura di un file presente nel pseudo-filesystem /proc/ contenente l'immagine sotto forma di file di diverse variabili d'esecuzione del kernel. Quella d'interesse per il progetto era /proc/loadavg, contenente in un'unica riga di testo il carico medio della macchina negli intervalli uno, cinque e quindici minuti appena trascorsi, numero di processi attivi e in esecuzioni, numero di processi terminati dall'avvio della macchina.

Nota:

ICARO: il codice dell'aglet

Vengono inclusi package e classi indispensabili per il programma:

com.ibm.aglet, com.ibm.event, com.ibm.util
Sono i package che contengono le classi necessarie alla costruzione degli Aglet: agenti, messaggi, interfacciamento al contesto...
com.ibm.agletx.SimpleItinerary
È la classe utilizzata per consentire la mobilità di ICARO. Per un errore nella procedura di installazione il package com.ibm.agletx non viene inserito nella gerarchia delle classi standard dell'ASDK (presenti su $AGLET_HOME/lib/com/ibm/) ma viene inserito in un'altra directory (in $AGLET_HOME/public/com/ibm/). Tale errore si manifesta sia in ambiente Linux che in ambiente Windows NT (e non potrebbe essere altrimenti, visto che la procedura di installazione è interamente realizzata in Java, dunque indipendentemente dalla piattaforma di destinazione). La presenza di questa anomalia rende inutilizzabili anche gli esempi forniti con l'ASDK, dato che in fase di compilazione il compilatore non trova SimpleItinerary nel CLASSPATH. Per risolvere il problema è sufficiente spostare il package nella posizione corretta oppure creare un opportuno link simbolico.

/*
* ICARO - Intelligent Aglet-based Observer
*
* Alessandro Spanu - Stefano Sanna 1998
*
* Tesina di Ingegneria del Software - Docente: Prof. Giuliano Armano
*/

package isw.g10;

import com.ibm.aglet.*;
import com.ibm.aglet.event.*;
import com.ibm.aglet.util.*;
import com.ibm.agletx.util.SimpleItinerary;
import java.lang.InterruptedException;
import java.lang.reflect.*;
import java.lang.Class;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.util.Enumeration;

La classe principale del progetto è Icaro, che estende la classe astratta Aglet da cui eredita tutte le caratteristiche principali degli aglet: mobilità, accesso al contesto, protezioni, interattività (Tahiti e scambio di messaggi).

public class Icaro extends Aglet {

Come abbiamo visto, il compito di Icaro è spostarsi tra diverse macchine Linux, leggerne lo stato, tornare all'host di partenza (per la precisione all'host di creazione); per questo motivo l'insieme dei dati (lo stato) che l'aglet deve portare con se è limitato all'URL dell'host di partenza, porta e protocollo di default, lista degli host da visitare, lista dello stato di ciascun host visitato, alcuni contatori necessari a tenere un riferimento durante l'esecuzione del suo ciclo di vita:

String home = null;
String protocol = "atp://";
String port = "4434";
int counter;
int hostNumber;
String itineraryArray[];
String statusArray[];

La mobilità degli aglet è ottenuta attraverso un'istanza della classe SimpleItinerary. I metodi di questa classe permettono di spostare gli agenti da un host all'altro (ove sia presente un aglet server attivo) e di controllarne il comportamento alla partenza (attraverso la gestione delle eccezioni) e all'arrivo ( attraverso lo scambio di messaggi). Anche l'oggetto itinerary (istanza di SimpleItinerary) viene serializzato, cioè trasportato insieme allo stato durante lo spostamento dell'aglet. Questo è necessario perchè il costruttore di SimpleItinerary riceve come parametro il riferimento all'aglet a cui si riferisce:

SimpleItinerary itinerary = null;

L'aglet dichiara un oggeto della classe SimpleItinerary necessario per il suo spostamento. Tale oggetto, in fase di istanziazione (vedi codice del metodo onCreation()) contiene un riferimento all'aglet che l'ha creato e del quale dovrà gestire la mobilità.

L'interazione tra utente e agente avviene attraverso una interfaccia grafica apposita (realizzata da noi attraverso la libreria standard AWT), dalla quale è possibile controllare tutti i parametri dell'agente stesso.

Non appena l'agente comincia a muoversi l'interfaccia non è più necessaria, e a maggior ragione è del tutto inutile in quelle macchine ove l'unica operazione compiuta è la lettura di un file. La finestra di dialogo è perciò definita transient in modo tale che il suo stato non venga serializzato (la classe è comunque serializzata, altrimenti non sarebbe possibile creare Icaro da un server remoto).

transient Frame mainDialog = new MainDialog(this);

Alla finestra appena istanziata viene passato attraverso il costruttore un riferimento (this) all'aglet di cui costituirà l'interfaccia. Tale riferimento verrà utilizzato da tutte le finestre per accedere ai parametri dell'agente e per controllarne il comportamento.

All'atto della creazione il contesto ove l'aglet viene istanziato richiama il metodo pubblico onCreation() per mezzo del quale avvengono tutte le inizializzazioni:

public void onCreation(Object init) {
   itinerary = new SimpleItinerary(this);
   hostNumber = 0;
   mainDialog.show();
   home = getAgletContext().getHostingURL().toString();
   setText(codeBase + "unilogo.gif" );
   waitMessage(2000);
}

viene istanziato l'oggetto della classe SimpleItinerary (al quale viene passato il riferimento all'aglet corrente), viene mostrata la finestra di dialogo, viene inizializzato il contatore degli host da visitare. Viene poi salvato l'indirizzo dell'host dove l'aglet viene creato e che costituisce la destinazione a fine ciclo:

home = getAgletContext().getHostingURL().toString();

Attraverso il metodo getAgletContext() si ottiene il contesto corrente dell'aglet. Il contesto a sua volta definisce un metodo (getHostingURL()) che restituisce l'URL dell'host ove l'aglet server è in esecuzione. L'oggetto URL infine possiede il metodo toString() che restituisce l'URL sotto forma di stringa.

L'interazione tra contesto e aglet avviene attraverso lo scambio di messaggi e la chiamata di metodi noti. I messaggi vengono gestiti attraverso una chiamata al metodo pubblico (necessario perchè sia accessibile al contesto) handleMessage(), ereditato dalla classe Aglet.

public boolean handleMessage(Message msg) {
   if (msg.sameKind("atHome")) {
      atHome(msg);
   }
   else if (msg.sameKind("startCheck")) {
      startCheck(msg);
   }
   else if (msg.sameKind("getStatus")) {
      getStatus(msg); // attenzione al passaggio di messaggi!!!
   }
   else if (msg.sameKind("dialog")) {
      dialog(msg);
   }
   else {
      return false;
   }
   return true;
}

L'architettura degli Aglet prevede messaggi composti da due parti: la prima, di tipo String individua il kind (tipo) del messaggio. In genere è sufficiente questa porzione per stabilire la comunicazione (ed è questa che viene usata dal nostro agente mobile). Il messaggio può avere anche una seconda parte, chiamata arg, contenente uno dei tipi primitivi o un oggetto qualsiasi (specificato nel costruttore). Tale porzione può essere usata per fornire informazioni complesse a chi riceve il messaggio. Si potrebbe ad esempio pensare di inviare un messaggio "go" (kind) che contiene come argomento l'indirizzo di destinazione costituito da un'istanza della classe SimpleItinerary.

Durante il suo ciclo di vita ICARO può ricevere quattro tipi di messaggio:

"atHome"
viene notificato appena l'aglet ritorna all'host di partenza;
"startCheck"
viene notificato ogni volta che ICARO deve spostarsi all'host successivo;
"getStatus"
viene notificato all'arrivo in un host da controllare;
"dialog"
è un messaggio standard dell'architettura degli aglet. Viene notificato dall' aglet viewer (Tahiti o Fiji) quando l'utente richiede la visualizzazione della finestra di dialogo.

A seconda del messaggio ricevuto handleMessage() richiama il metodo opportuno, restituendo true se il messaggio è stato riconosciuto (e dunque maneggiato) o false in caso contrario.

Il metodo atHome(),

public void atHome(Message msg) {
   // avvisa l'utente sull'aglet viewer
   setText("Controllo completato");

   // lascia il messaggio per due secondi sullo schermo
   waitMessage(2 * 1000);

   handleMessage(new Message("dialog")); // mostra la finestra di dialogo
}

richiamato da handleMessage in corrispondenza del messaggio "atHome", si limita a visualizzare (attraverso la notifica del messaggio "dialog") la finestra con i risultati. Questa potrebbe essere visualizzata con una semplice chiamata al metodo mainDialog.show() (e appositi controlli di consistenza), ma si è lo scambio di messaggi per avere un livello di astrazione maggiore.

Il metodo startCheck

public synchronized void startCheck(Message msg) {
   nextDestination(msg);
}

costituisce una regione critica (da qui il modificatore sincronized) e richiama il metodo nextDestination() che si occupa di inviare l'aglet all'host successivo.

Il metodo nextDestination() sovraintende alla mobilità dell'agente. Esso, richiamato dall'utente a inizio ciclo e dopo ogni check locale, stabilisce a quale host inviare l'aglet. Dovendo eseguire uno spostamento all'host successivo a quello corrente, viene prima di tutto incrementato il contatore relativo alla posizione corrente. Come già detto la spedizione dell'aglet avviene attraverso un istanza della classe SimpleItinerary che definisce il metodo go() che può sollevare tutto un insieme di eccezioni legate alla connessione di rete. Per questo motivo l'invocazione del metodo itinerary.go() è fatta all'interno di un blocco try..catch. Se si è esaurito l'elenco degli host da visitare si inizializza il contatore e l'aglet viene rispedito all'host di partenza. In caso contrario viene inviato all'host successivo indicato dall'utente. Il metodo go() della classe SimpleItinerary permette di specificare, oltre all'host di destinazione, anche un messaggio che verra notificato all'arrivo dell'aglet. E' attraverso questo messaggio che si controlla il comportamento di Icaro in un determinato host.

/*
 * Sposta l'aglet nella destinazione successiva gestendo le eccezioni del caso...
 */

public void nextDestination() {
   counter++;
   try {
      if (counter == hostNumber) {
         setText("Back home: " + home);
         counter = -1; // reimposta il contatore
         itinerary.go(home, "atHome");
      }
      else {
         setText("Next host: " + itineraryArray[counter]);
         itinerary.go(itineraryArray[counter], "getStatus");
      }
   } // gestiamo le eccezioni del caso...
   catch (MalformedURLException malformed) {
      setText("URL malformed! ");
      statusArray[counter] = "URL malformed! ";
      nextDestination();
   }
   catch (UnknownHostException unknown) {
      setText("Host unknown! ");
      statusArray[counter] = "Host unknown! ";
      nextDestination();
   }
   catch (UnknownServiceException service) {
      setText("No AgletServer! ");
      statusArray[counter] = "No AgletServer! ";
      nextDestination();
   }
   catch (ServerNotFoundException server) {
      setText("Server not found! ");
      statusArray[counter] = "Server not found! ";
      nextDestination();
   }
   catch (ConnectException connect) {
      setText("Connect error! ");
      statusArray[counter] = "Connect error! ";
      nextDestination();
   }
   catch (RequestRefusedException refused) {
      setText("Request refused! ");
      statusArray[counter] = "Request refused! ";
      nextDestination();
   }
   catch (AgletException ex) {
      // dobbiamo comunque intercettare tutte le altre eccezioni
      setText("Aglet!");
      ex.printStackTrace();
   }
   catch (IOException io) {
      io.printStackTrace();
   }
}

nextDestination() è in grado di raccogliere cinque tipi di eccezioni relative a errori di connessione:

MalformedURLException
sollevata nel caso un cui l'URL dell'host di destinazione sia errata;
UnknownHostException
sollevata quando l' host specificato è inesistente;
UnknownServiceException
sollevata quando non è stato trovato un aglet server attivo nell'host di destinazione;
ServerNotFoundException
segnala che il server non è attualmente disponibile;
ConnectException
sollevata quando non è possibile stabilire la connessione (per esempio a causa di una porzione di rete isolata);
RequestRefusedException
sollevata quando la macchina di destinazione rifiuta la connessione.

Per ognuna di queste viene salvata nel vettore di stato una stringa che notifica all'utente il tipo di errore riscontrato e viene richiamato ricorsivamente il metodo nextDestination() che sposta il focus sull'host successivo. Per altri tipi di eccezioni viene semplicemente fatto un output dello stack sulla console Java.

Il metodo getStatus(), richiamato all' arrivo in un host, si occupa di leggere lo stato della macchina (richiamando il metodo readStatus())e di richiamare il metodo nextDestination() per proseguire il ciclo.

/*
 * Recupera lo stato della macchina corrente....
 */

public void getStatus(Message msg) {
   setText("ICARO: checking status...");
   waitMessage(2 * 1000); // tiene il messaggio sullo schermo...
   // Leggiamo lo stato della macchina
   readStatus();
   setText("Load: " + statusArray[counter]);
   waitMessage(2 * 1000);
   nextDestination();
}

Il metodo readStatus() è quello che effettivamente rileva il carico della macchina e lo salva nella posizione del vettore di stato relativo all'host corrente. Il carico medio di una macchina Linux è riportato da diverse utilità di sistema (top, uptime) e può essere ricavato leggendo il file loadavg presente nel pseudofilesystem proc (si rimanda alla manualistica in linea per maggiori dettagli). Il file /proc/loadavg è costituito da una sola linea nella quale è registrato il carico medio della macchina nei tre intervalli più significativi (uno, cinque e quindici minuti appena trascorsi), oltre al rapporto processi_attivi/processi_in_esecuzione e il numero di processi eseguiti dall'avvio della macchina. Per prima cosa viene dichiarato e istanziato un oggetto della classe File, a cui viene passato nel costruttore il nome del file interessato (/proc/loadvg). La lettura del file, all'interno di un blocco try..catch, avviene attraverso un'istanza della classe FileReader una comoda sottoclasse di InputStreamReader dedicata alla lettura dei file di testo l'assegnazione della stringa di stato al vettore di stato avviene attraverso una chiamata del metodo readLine() di un istanza della classe BufferedReader che funge da buffer per lo stream individuato da FileReader.

/*
 * Legge i dati dal file e li mette nel vettore di stato
 */

private void readStatus() {
   // crea l'oggetto file che punta ai dati di interesse...
   File loadavgFile = new File("/proc/loadavg");

   try {
      // Creiamo lo streamer per la lettura di caratteri anziche' byte...
      FileReader fr = new FileReader(loadavgFile);
      BufferedReader inputLine = new BufferedReader(fr);
      statusArray[counter] = inputLine.readLine();
   }
   catch (IOException e) {
      e.printStackTrace();
   }
}

La clausola catch ci permette di raccogliere eventuali eccezioni sollevate durante l'accesso al file.

Nota:

L'ultimo metodo della classe Icaro è invocato alla notifica del messaggio "dialog" e si occupa della visualizzazione della finestra principale mainDialog.

/*
 * Crea (se necessario) e mostra la finestra di dialogo.
 * Viene invocato in corrispondenza del messaggio "dialog"
 */

public void dialog(Message msg) {
   if (mainDialog == null) {
      mainDialog = new MainDialog(this);
   }
   mainDialog.show();
}

ICARO: il codice della finestra principale

L'interazione tra utente e ICARO avviene attraverso una finestra che contiene gli elementi standard delle interfacce grafiche : bordo di ridimensionamento, pulsanti di chiusura e attivazione, barra del titolo. Tutte queste caratteristiche sono ereditate dalla classe Frame.

Il contenuto della finestra puè essere considerato suddiviso in due parti complementari: una barra dei menù e un insieme di componenti organizzati attraverso una griglia. Tutti gli elementi necessari alla creazione dell'interfaccia (compreso il riferimento all'agente controllato) sono dichiarati come attributi privati dell' oggetto.

class MainDialog extends Frame {

   // contiene il riferimento all'aglet "comandato" da questa finestra
   private Icaro aglet = null;

   /*
    * Componenti dell'interfaccia
    */
   private Label titleLabel = new Label("ICARO 1.0", Label.CENTER);
   private Button addHostButton = new Button("Add host");
   private Button removeHostButton = new Button("Remove host");
   private TextField hostTextField = new TextField(21);
   private Button detailsButton = new Button("Details");
   private Button startButton = new Button("Start");
   private Button aboutButton = new Button("About");
   private Button closeButton = new Button("Close");
   private List hostList = new List ();
   /* Creiamo i menu' */
   private MenuBar menuBar = new MenuBar();
   private Menu fileMenu = new Menu("File");
   private MenuItem closeMenuItem = new MenuItem("Close");
   private MenuItem disposeMenuItem = new MenuItem("Dispose");
   private Menu preferencesMenu = new Menu("Preferences");
   private MenuItem protocolMenuItem = new MenuItem("Default protocol...");
   private MenuItem portMenuItem = new MenuItem("Default port...");
   private Menu helpMenu = new Menu("Help");
   private MenuItem aboutMenuItem = new MenuItem("About ICARO...");
   private TemplateDialog childDialog; // scelta protocollo e porta...
   private DetailsDialog detailsDialog = null; // dettagli sul lavoro svolto
   private InfoDialog infoDialog = null; // about box (!)

   /*
    * Salviamo il riferimento all'aglet controllato, in modo che sia visibile
    * da tutti i metodi. Viene assemblata l'interfaccia grafica.
    */
   MainDialog(Icaro aglet) {
      this.aglet = aglet;
      layoutComponents();
   }

La barra dei menù è assemblata usando le istanze delle classi MenuBar, Menu e MenuItem. MenuBar costituisce la barra vera e propria dei menù a cui vengono addizionate delle etichette di menù attraverso il metodo add(). A ciascun menù vengono poi aggiunte le diverse voci attraverso il metodo add().

Nella seconda parte della finestra sono presenti alcuni componenti (pulsanti, caselle di testo, liste) organizzati e distribuiti attraverso una griglia che costituisce l'oggetto Layout, cioe lo stile della finestra stessa. La creazione di tale griglia avviene attraverso un'istanza della classe GridBagLayout e della classe GridBagConstraints. La prima costituisce il layout vero e proprio (che verrà assegnato al frame attraverso una chiamata del metodo setLayout), mentre la seconda si occupa della collocazione di ciascun componente all'interno della griglia.

Per ciascun componente verrà dunque settata una apposita configurazione del GridBagConstraints: dimensione dei bordi, fattore di riempimento, posizione all'interno della griglia (coordinate colonna - riga), occupazione all'interno della griglia (numero di colonne e righe occupate), fattore di ridimensionamento della cella. Nel listato vi sarà dunque una successione di configurazione/assegnazione/riempimento dei vari elementi che costituiscono l'interfaccia.

/*
 * Costruiamo la griglia con i componenti...
 */
private void layoutComponents() {
   setTitle("ICARO 1.0");

   // organizziamo i menu'...
   menuBar.add(fileMenu);
   fileMenu.add(closeMenuItem);
   fileMenu.addSeparator();
   fileMenu.add(disposeMenuItem);
   menuBar.add(preferencesMenu);
   preferencesMenu.add(protocolMenuItem);
   preferencesMenu.add(portMenuItem);
   menuBar.add(helpMenu);
   helpMenu.add(aboutMenuItem);
   setMenuBar(menuBar);

   // inizializziamo la griglia...
   GridBagLayout mainGrid = new GridBagLayout();
   GridBagConstraints mGBC = new GridBagConstraints();
   setLayout (mainGrid);
   mGBC.fill = GridBagConstraints.BOTH;
   mGBC.insets = new Insets(5,5,5,5);

   // aggiungiamo il pulsante con Add Host
   buildConstraints(mGBC, 0, 0, 1, 1, 0, 0);
   mainGrid.setConstraints(addHostButton, mGBC);
   add(addHostButton);

   // aggiungiamo il pulsante con Remove Host
   buildConstraints(mGBC, 0, 1, 1, 1, 0, 0);
   mainGrid.setConstraints(removeHostButton, mGBC);
   add(removeHostButton);

   // aggiungiamo il textfield dove inserire un nuovo host
   // modifichiamo le dimensioni del bordo
   mGBC.insets = new Insets(17,5,17,5);
   buildConstraints(mGBC, 1, 0, 3, 2, 0, 0);
   mainGrid.setConstraints(hostTextField, mGBC);
   add(hostTextField);

   mGBC.insets = new Insets(5,5,5,5);
   // aggiungiamo la lista degli host da visitare
   buildConstraints(mGBC, 0, 2, 4, 3, 1, 1);
   mainGrid.setConstraints(hostList, mGBC);
   add(hostList);

   // aggiungiamo il pulsante che mostra la finestra dei dettagli...
   buildConstraints(mGBC, 0, 5, 1, 1, 0, 0);
   mainGrid.setConstraints(detailsButton, mGBC);
   add(detailsButton);

   // aggiungiamo il pulsante che avvia la ricerca
   buildConstraints(mGBC, 1, 5, 1, 1, 1, 0);
   mainGrid.setConstraints(startButton, mGBC);
   add(startButton);

   // aggiungiamo il pulsante che mostra informazioni sugli autori...
   buildConstraints(mGBC, 2, 5, 1, 1, 1, 0);
   mainGrid.setConstraints(aboutButton, mGBC);
   add(aboutButton);

   // aggiungiamo il pulsante che chiude l'aglet
   buildConstraints(mGBC, 3, 5, 1, 1, 1, 0);
   mainGrid.setConstraints(closeButton, mGBC);
   add(closeButton);

   // riempiamo la lista se necessario...
   loadHostList();

   setSize(400,300);

}

Per rendere più leggibile il codice si è pensato di raggruppare le assegnazioni del GridBagConstraints (ben sei) nella chiamata ad un metodo che ricevesse la configurazione come parametro; tale metodo, chiamato buildConstraints(), è definito di classe perchè utilizzato da più finestre. Vi è poi un metodo che si occupa di mostrare la lista degli host già inseriti dall'utente.

public static void buildConstraints(GridBagConstraints gbc, int x, int y, int w, int h, int w100, int h100) {
   // utilizzato per assemblare i componenti nella griglia...
   gbc.gridx = x;
   gbc.gridy = y;
   gbc.gridwidth = w;
   gbc.gridheight = h;
   gbc.weightx = w100;
   gbc.weighty = h100;
}

/*
 * Carica (se esiste) il vettore indirizzi
 */
private void loadHostList() {
   for (int i = 0 ; i < aglet.hostNumber ; i++) {
      hostList.add(aglet.itineraryArray[i]);
   }
}

La gestione degli eventi generati dall'utente avviene attraverso il metodo action() ereditato dalla classe Frame (che a sua volta lo eredita dalla classe Component). Tale metodo, deprecato nell'API 1.1 viene sovrascritto opportunamente per controllare il comportamento del programma; esso riceve come parametri un oggetto Event, che contiene l'evento vero e proprio, e un riferimento all'oggetto che ha generato quell'evento. Attraverso una sequenza di if..else si controlla l'attributo Event.target per stabilire quale azione eseguire. Un primo blocco if..else gestisce gli eventi relativi alle voci di menù: chiusura delle finestre, distruzione dell'agente, scelta del protocollo e della porta di default, visualizzazione della finestra informazione. La scelta di porta e protocollo avviene attraverso un istanza della classe TemplateDialog.

public boolean handleEvent(Event ev) {
   if (ev.id == Event.WINDOW_DESTROY) {
      hide();
      return true;
   }
   return super.handleEvent(ev);
}

/*
 * gestione degli eventi dell'interfaccia
 */
public boolean action(Event ev, Object obj) {
   if (ev.target == closeMenuItem) {
      if ((detailsDialog != null) && (detailsDialog.isShowing())) {
         // chiude anche la finestra dei dettagli se aperta...
         detailsDialog.hide();
      }
      hide();
   }
   else if (ev.target == disposeMenuItem) {
      // distrugge l'aglet
      if ((detailsDialog != null)) detailsDialog.dispose();
         aglet.dispose();
   }
   else if (ev.target == protocolMenuItem) {
      // permette la scelta del protocollo
      childDialog = new TemplateDialog("protocol", aglet, this);
   }
   else if (ev.target == portMenuItem) {
      // permette la scelta della porta
      childDialog = new TemplateDialog("port", aglet, this);
   }
   else if (ev.target == aboutMenuItem) {
      // mostra informazioni sul programma
      if (infoDialog == null) infoDialog = new InfoDialog(this);
         infoDialog.show();
   }

Un secondo blocco si occupa della gestione dei pulsanti, che è del tutto analoga a quella vista per le voci di menù:

else if (ev.target == closeButton) {
   // nasconde la finestra (senza distruggere l'aglet)
   if ((detailsDialog != null) && (detailsDialog.isShowing())) {
      detailsDialog.hide();
   }
   hide();
}
else if (ev.target == addHostButton) {
   addHost();
}
else if (ev.target == removeHostButton) {
   removeHost();
}
else if (ev.target == detailsButton) {
   // mostra la finestra con i dettagli (ed eventualmente la crea)
   if (aglet.statusArray != null) {
      if (detailsDialog == null) {
         detailsDialog = new DetailsDialog(aglet);
      }
      else detailsDialog.show();
   }
}
else if (ev.target == startButton) {
   // inizia il ciclo...
   if (aglet.hostNumber > 0) {
      // ... solo se il vettore non e' vuoto...
      if ((detailsDialog != null)) {
         detailsDialog.hide();
         detailsDialog = null;
      }
      // prendiamo la lista degli host
      aglet.itineraryArray = hostList.getItems();
      // creiamo un vettore di stato di dimensione opportuna
      aglet.statusArray = new String[aglet.itineraryArray.length];
      aglet.counter = -1;
      // notifichiamo il messaggio per far iniziare il ciclo
      aglet.handleMessage(new Message("startCheck"));
   }
}
else if (ev.target == hostTextField) {
   // intercettiamo la pressione del tasto invio sul textField
   if (ev.id == 1001) addHost();
}
else if (ev.target == aboutButton) {
   if (infoDialog == null) infoDialog = new InfoDialog(this);
   infoDialog.show();
}
else return false;
return true;
}

Alla pressione del pulsante addHostButton viene chiamato il metodo addHost() che copia il contenuto della casella di testo hostTextField:

public void addHost() {
   if (!(hostTextField.getText().equals(""))) {
      if ((detailsDialog != null)) {
         // i dati di stato non sono piu' consistenti con il vettore indirizzi...
         // distruggiamo la finestra e inizializziamo i vettori...
         detailsDialog.hide();
         detailsDialog = null;
         aglet.statusArray = null;
         aglet.itineraryArray = null;
      }
      // aggiungiamo alla lista
      hostList.addItem(aglet.protocol + hostTextField.getText() + ":" + aglet.port);
      aglet.hostNumber++;
      hostTextField.setText("");
   }
}

Se la casella di testo non è vuota la stringa in essa contenuta viene aggiunta all'oggetto lista hostList, concatenando protocollo di default, nome dell'host, porta di default; infine viene incrementato il contatore hostNumber dell'aglet che contiene il numero degli host da visitare e viene eliminata la stringa dal TextField. Il metodo addHost() è richiamato anche alla pressione del tasto invio quando il focus è sul TextField: questo permette di inserire rapidamente diversi indirizzi senza dover togliere le mani dalla tastiera.

In maniera analoga premendo il pulsante removeHostButton viene richiamato il metodo removeHost() che elimina dalla hostList un host precedentemente selezionato. Se nessun host è selezionato (hostList.getSelectedIndex == -1) il metodo termina senza compiere nessuna azione.

public void removeHost() {
   if (hostList.getSelectedIndex() > -1) {
      if ((detailsDialog != null)) {
         // inizializziamo vettori e finestra...
         detailsDialog.hide();
         detailsDialog = null;
         aglet.statusArray = null;
         aglet.itineraryArray = null;
      }
      hostList.delItem(hostList.getSelectedIndex());
      aglet.hostNumber--;
   }
}

La pressione del tasto startButton comanda l'inizio del ciclo di controllo. Se la lista degli host non è vuota il suo contenuto viene copiato nel vettore itineraryArray dell'aglet. In realtà il metodo hostList.getItems() crea un vettore di stringhe contenenti gli elementi della lista, e tale vettore viene poi assegnato all'aggetto itineraryArray. Successivamente viene istanziato l'oggetto statusArray come vettore di stringa di dimensione pari al numero di host da visitare. Dopo aver inizializzato il contatore aglet.counter viene notificato all'agente il messaggio "startCheck" che comanderà effettivamente il movimento dell'aglet.


ICARO: il codice della finestra dei risultati

La classe DetailsDialog, sottoclasse di Frame, costruisce la finestra con i risultati del controllo effetuato. La costruzione dell'interfaccia avviene in maniera del tutto analoga a quanto visto per la classe MainDialog: viene definita una griglia e al suo interno vengono assemblati i vari componenti. I risultati sono presentati all'interno di una lista le cui righe (elementi) contengono, per ciascun host, l'indirizzo (preso dal vettore itineraryArray) e lo stato (preso dal vettore statusArray). Come gia' visto per la classe MainDialog, anche DetailsDialog riceve attraverso il proprio costruttore un riferimento all'aglet corrente.

class DetailsDialog extends Frame {
   private Button closeButton = new Button("Close");
   private List detailsList = new List();
   private Icaro aglet;
   private String[] detailsData = null;

   DetailsDialog (Icaro aglet) {
   /*
    * costruisce l'interfaccia in maniera del tutto analoga a MainDialog
    */
   this.aglet = aglet;
   GridBagLayout mainGrid = new GridBagLayout();
   GridBagConstraints mGBC = new GridBagConstraints();
   setLayout (mainGrid);
   mGBC.fill = GridBagConstraints.BOTH;
   mGBC.insets = new Insets(5,5,5,5);
   setTitle("Details...");
   setSize(400, 300);

   // aggiungiamo la listacon i risultati
   MainDialog.buildConstraints(mGBC, 0, 0, 2, 1, 1, 1);
   mainGrid.setConstraints(detailsList, mGBC);
   add(detailsList);

   // aggiungiamo il pulsante di chiusura
   MainDialog.buildConstraints(mGBC, 1, 1, 1, 1, 0, 0);
   mainGrid.setConstraints(closeButton, mGBC);
   add(closeButton);
   updateData();

   show();
}

Non essendo disponibile un oggetto "tabella" nella libreria AWT di Java 1.1 (ma sara' presente nella libreria swing presente in Java 1.2 di prossimo rilascio) si è pensato di "simularne" una all'interno dell'oggetto lista formattando opportunamente il testo delle righe.

Tale formattazione è possibile solo se i caratteri all'interno della lista sono a spaziatura fissa. In ambiente XWindows questa condizione è sempre verificata, mentre sotto Windows NT l'impostazione di default prevede dei caratteri a spaziatura variabile (volendo rendere ICARO multipiattaforma sarà sufficiente impostare un carattere a spaziatura fissa attraverso il metodo setFont() dell'oggetto List). Il codice del metodo updateData() assembla le varie righe utilizzando istanze della classe StringBuffer: questa rispetto alla classe String permette la modifiica della stringa memorizzata.

public void updateData() {
/*
 * Attraverso l'uso delle stringhe modificabili StringBuffer, i risultati
 * contenuti nel vettore di stato (statusArray) vengono formattati
 * opportunamente in una lista
 */
   int maxHostLength = 0;
   int i,j;
   StringBuffer buffer;
   detailsData = new String[aglet.hostNumber];

   // cerchiamo l'indirizzo piu' lungo
   for (i=0; i < aglet.hostNumber; i++) {
      if (aglet.itineraryArray[i].length() > maxHostLength) {
         maxHostLength = aglet.itineraryArray[i].length();
      }
   }
   maxHostLength++;

   // prima riga
   buffer = new StringBuffer("Host");
   if (maxHostLength == 1) {
      for(i =0; i < 20; i++)
         buffer.append(' ');
   }
   else {
      for (i=0; i < (maxHostLength - 4); i++) {
         buffer.append(' ');
      }
   }
   buffer.append("| 1min | 5min | 15min|");
   detailsList.add(buffer.toString());

   // riga di separazione
   buffer = new StringBuffer("");
   if (maxHostLength == 1) {
      for(i =0; i < 24; i++)
         buffer.append('-');
   }
   else {
      for (i=0; i < maxHostLength ; i++) {
         buffer.append('-');
      }
   }
   buffer.append("+------+------+------+");
   detailsList.add(buffer.toString());

   // aggiungiamo gli host...
   for (i=0; i < aglet.hostNumber; i++) {
      buffer = new StringBuffer(aglet.itineraryArray[i]);
      for (j=0; j < (maxHostLength - aglet.itineraryArray[i].length()); j++) {
         buffer.append(" ");
      }
      buffer.append("| ");
      if (aglet.statusArray != null) {
         if (Character.isDigit(aglet.statusArray[i].charAt(0))) {
            buffer.append(aglet.statusArray[i].substring(0,4));
            buffer.append(" | ");
            buffer.append(aglet.statusArray[i].substring(5,9));
            buffer.append(" | ");
            buffer.append(aglet.statusArray[i].substring(10,14));
            buffer.append(" | ");
            detailsList.add(buffer.toString());
         }
         else {
            buffer.append(aglet.statusArray[i] + " | ");
            detailsList.add(buffer.toString());
         }
      }
   }
}

Per ogni macchina controllata vi saranno quattro colonne contenenti l'URL e il carico medio nei tre intervalli 1, 5, 15 minuti (oppure l'eventuale messaggio d'errore). Attravesro spazi e caratteri aggiuntivi verranno incolonnate le varie voci.


ICARO: il codice della finestra delle opzioni

La classe TemplateDialog è richiamata dal menù dell'aglet ogni volta che viene richiesta una variazione del protocollo o della porta di default.

TemplateDialog estende la classe Dialog: è dunque una finestra senza barra del titolo e non ridimensionabile. Il suo costruttore richiama quello della superclasse, a cui passa il riferimento al frame da cui è stata creata e una costante booleana (true) che specifica la finestra di dialogo di tipo modale.

Nota:

/*
 * E' il template usato per inserire protocollo e porta di default...
 */

class TemplateDialog extends Dialog {
   private Icaro aglet; // l'aglet con cui interagisce questa dialog...
   private Button okButton = new Button("OK");
   private Button cancelButton = new Button("Cancel");
   private TextField templateTextField = new TextField(10);
   private String oldValue;
   private Label typeLabel;
   private String dialogType;
   private Class c;

   TemplateDialog(String type, Icaro aglet, Frame parent) {
      super(parent, true);
      this.aglet = aglet;
      dialogType = type;

Dovendo implementare la stessa funzionalità per entrambi i parametri, si è pensato di sfruttare un'unica finestra modificata in fase di creazione. Il costruttore riceve come parametro una stringa che specifica il tipo di finestra richiesta. Piuttosto che utilizzare un blocco if..else per gestire il parametro in ingresso (e dunque per stabilire quale attributo dell'aglet modificare, port o protocol), abbiamo preferito, per esercizio, utilizzare una potenzialità dell'API 1.1 chiamata reflection. Questa caratteristica, detta anche introspezione, permette di recuperare a tempo di esecuzione informazioni sulla classe di appartenenza di un oggetto, sui suoi metodi e i suoi attributi. Attraverso il metodo getClass(), ereditato da Object, si ottiene un'istanza di Class che individua la classe dell'oggetto corrente. Possiamo dire in un certo senso che come l'istanziazione di un oggetto costituisce la "concretizzazione" di una classe, così la reflection permette di "astrarre" la classe da un oggetto concreto.

Una volta ottenuto la classe dell'aglet

c = aglet.getClass();

possiamo ottenere un riferimento ad un certo campo (o metodo) semplicemente fornendone il nome come parametro al metodo getDeclaredField() (questo metodo a differenza di getField() può riferirsi a qualsiasi attributo dell'oggetto indipendentemente dal fatto che sia pubblico o meno). A tale metodo verrà passato come argomento il parametro dialogType ricevuto nel costruttore della finestra, che è una stringa avente i due valori protocol o port (nomi che coincidono con gli omonimi attributi dell'aglet).

c.getDeclaredField(dialogType)

Il metodo qui sopra restituirà un oggetto della classe Field specifico del campo individuato da dialogType. Per ottenere il valore di un campo di un determinato oggetto sarà sufficiente richiamare il metodo get() sull'oggetto Field, specificando come argomento l'oggetto (istanza della classe a cui il campo individuato da Field appartiene) a cui si riferisce. Tale metodo restituisce un'istanza di Object, che attraverso il casting viene trasformato in una istanza della classe di appartenenza del campo stesso.

oldValue = (String) c.getDeclaredField(dialogType).get(aglet);

Un meccanismo simile si utilizza per scrivere su un oggetto Field: una volta individuato all'interno della classe attraverso getDeclaredField(), è sufficiente richiamarne il metodo set() fornendo come argomento l'oggetto su cui si vuole agire e il valore da registrare. L'accesso ai metodi e ai campi di un oggetto Class può sollevare delle eccezioni (ad esempio, campo inesistente o non accessibile) che vanno raccolte.

   c = aglet.getClass(); // otteniamo l'oggetto "classe" di Icaro...
   // salviamo i dati!!!
   try {
      oldValue = (String) c.getDeclaredField(dialogType).get(aglet);
      /* con c = aglet.getClass() otteniamo la classe di apparteneza di aglet.
       * con c.getDeclaredField(dialogType) otteniamo la variabile individuata
       * dal contenuto di dialogType all'interno della classe. Finora abbiamo
       * lavorato su una astrazione dell'oggetto, cioe' sulla classe, non sull'
       * oggetto "aglet" attuale. Per prendere il campo dell'oggetto attuale
       * invochiamo c.getDeclaredField(dialogType).get(aglet) che tramite il
       * metodo get e l'oggetto di interesse e' in grado di recuperare il dato
       * richiesto.
       */
   }
   catch (Exception e) {
      System.out.println(c.getName());
      e.printStackTrace();
   }
   // sistemiamo l'interfaccia
   templateTextField.setText(oldValue);
   setSize(230,100);
   GridBagLayout mainGrid = new GridBagLayout();
   GridBagConstraints mGBC = new GridBagConstraints();
   setLayout (mainGrid);
   mGBC.fill = GridBagConstraints.BOTH;
   mGBC.insets = new Insets(5,5,5,5);

   // aggiungiamo l'etichetta
   MainDialog.buildConstraints(mGBC, 0, 0, 1, 1, 0, 0);
   typeLabel = new Label("Default " + type + ": ");
   mainGrid.setConstraints(typeLabel, mGBC);
   add(typeLabel);
   // aggiungiamo il textfield
   MainDialog.buildConstraints(mGBC, 1, 0, 1, 1, 0, 0);
   mainGrid.setConstraints(templateTextField, mGBC);
   add(templateTextField);

   // aggiungiamo il pulsante OK
   MainDialog.buildConstraints(mGBC, 0, 1, 1, 1, 0, 0);
   mainGrid.setConstraints(okButton, mGBC);
   add(okButton);

   // aggiungiamo il pulsante CANCEL
   MainDialog.buildConstraints(mGBC, 1, 1, 1, 1, 0, 0);
   mainGrid.setConstraints(cancelButton, mGBC);
   add(cancelButton);
   setLocation(new Point(parent.getLocation().x + 100, parent.getLocation().y + 100));
   show();
}

public void setValue() {
   // recuperiamo il valore inserito dall'utente
   try {
      c.getDeclaredField(dialogType).set(aglet, templateTextField.getText());
      hide(); // mettiamo declared perche' non e' un campo public
   }
   catch (Exception e) {
      System.out.println(templateTextField.getText());
      e.printStackTrace();
   }
}

public boolean action(Event ev, Object obj) {
   if (ev.target == cancelButton) {
      hide();
   }
   else if (ev.target == okButton) {
      setValue();
   }
   else if (ev.target == templateTextField) {
      if (ev.id == 1001) {
         setValue();
      }
    }
   else return false;
   return true;
}

ICARO: il codice dell'about box

L'ultima classe InfoDialog si occupa di visualizzare una semplice dialog box con le informazioni sul programma:

Il codice riprende lo stile utilizzato per la creazione delle altre finestre:

class InfoDialog extends Dialog {
   private Button closeButton = new Button("Close"); // pulsante di chiusura
   private Label titleLabel = new Label("ICARO 1.0", Label.CENTER);
   private Label informationLabel = new Label("Tesina di Ingegneria del Software", Label.CENTER);
   private Label authorsLabel = new Label("Alessandro Spanu e Stefano Sanna", Label.CENTER);

   public InfoDialog(Frame parent) {
      super(parent, "About authors...", true); // chiamo il Frame sovrastante...
      GridBagLayout mainGrid = new GridBagLayout();
      GridBagConstraints mGBC = new GridBagConstraints();
      setLayout (mainGrid);
      mGBC.fill = GridBagConstraints.BOTH;
      mGBC.insets = new Insets(5,5,5,5);

      // etichetta programma
      titleLabel.setFont(new Font("Helvetica", Font.BOLD + Font.ITALIC, 18));
      MainDialog.buildConstraints(mGBC, 1, 0, 1, 1, 0, 0);
      mainGrid.setConstraints(titleLabel, mGBC);
      add(titleLabel);

      // etichetta esame
      MainDialog.buildConstraints(mGBC, 0, 1, 3, 1, 0, 0);
      mainGrid.setConstraints(informationLabel, mGBC);
      add(informationLabel);

      // etichetta
      MainDialog.buildConstraints(mGBC, 0, 2, 3, 1, 0, 0);
      mainGrid.setConstraints(authorsLabel, mGBC);
      add(authorsLabel);

      // aggiungiamo il pulsante di chiusura
      MainDialog.buildConstraints(mGBC, 1, 3, 1, 1, 0, 0);
      mainGrid.setConstraints(closeButton, mGBC);
      add(closeButton);

      setLocation(new Point(parent.getLocation().x + 100, parent.getLocation().y + 100));
      setSize(270,160); // sistema la dimensione della finestra
   }

   public boolean action(Event ev, Object arg) {
      if (ev.target == closeButton) {
         this.hide();
         this.dispose();
         return true;
      }
      else return false;
   }
}

Conclusioni

Il nostro progetto si è basato su tre tecnologie ancora in fase di maturazione: Java come linguaggio, gli Aglets come sistema ad agenti mobili, Linux come sistema operativo. Il primo è tuttora in continua crescita, ma non può ancora considerarsi una piattaforma collaudata per lo sviluppo di applicazioni professionali. Questo costituisce una ulteriore sfida per l'ASDK: proporre un'architettura innovativa - gli agenti mobili - basandosi su un linguaggio ancora giovane. Infine Linux costituisce probabilmente l'incognita più grande, dato che è completamente basato sul free software, e dunque sul contributo volontario dei suoi sostenitori. Con la nostra tesina abbiamo potuto verificare che è possibile combinare, non senza qualche difficoltà, queste tre tecnologie per ottenere - almeno in potenza - un prodotto valido. In particolare:

Java
Ha mantenuto la promessa di essere un linguaggio realmente multipiattaforma. Il nostro piccolo agente mobile ha funzionato correttamente su diverse JVM installate su diversi sistemi operativi. Inoltre, benchè conosciuto e apprezzato per sviluppare piccoli programmi per il web, si è rivelato un linguaggio potente per sviluppare applicazioni complete (e nel caso dell'ASDK persino un'intera architettura).
ASDK
Presentato in vari articoli di programmazione come un "semplice" sistema ad agenti mobili, l'ASDK si è rivelato un'architettura potente e molto sofisticata per l'implementazione di oggetti distribuiti. Sfogliando la libreria delle classi ci si rende conto che i creatori degli aglet non hanno lasciato niente al caso, offrendo al programmatore un ambiente di sviluppo estremamente ricco e complesso. Gli aglet costituiscono, per così dire, un'architettura dentro l'archittetura: come la JVM offre l'ambiente di esecuzione completo e sistemi di sicurezza per le applicazioni Java, allo stesso modo l'aglet server offre agli aglet un ambiente "privilegiato" di esecuzione (il contesto), un sistema di sicurezza dedicato, sistemi per la comunicazione e l'interazione con gli altri agenti. Nonostante l'apparente semplicità, per sfruttare appieno tutte le potenzialità offerte dall'ASDK è necessario conoscere a fondo tutta l'architettura (libreria delle classi, protocolli, problemi specifici legati al linguaggio) cosa non fattibile in questa sede. La scelta di Java si è dimostrata vincente perchè ha reso di fatto l'ASDK un sistema realmente multipiattaforma. Malgrado alcuni errori, dovuti alla complessita intrinseca del sistema e alla modifica delle API nel passaggio dalla versione Alpha5 alla 1.0, l'ASDK sta conoscendo una grossa diffusione fra i programmatori, ed è continuamente oggetto di continui miglioramenti (basti pensare che nel corso dello sviluppo di questo progetto si è passati dalla versione Alpha5 alla 1.0 sino alla attuale 1.0.2).
Linux
Dover lavorare su un sistema completamente gratuito non offriva nessuna garanzia di successo, anche parziale. La disponibilità di una buona implementazione della JVM e di una vasta base di documentazione scritta dagli utenti del sistema operativo, unite alle caratteristiche di robustezza e affidabilità di Unix, ci ha permesso di lavorare con Linux senza sentire la necessità di passare a sistemi alternativi commerciali come Windows o MacOS (per quest'ultimo, tra l'altro non è disponibile il JDK 1.1). Ribadendo quanto detto nell'introduzione, Linux costituisce senza dubbio un ottima base, peraltro gratuita, per sviluppare sistemi ad agenti distribuiti, in particolare in Java.

Ulteriori sviluppi di ICARO potrebbero essere:


Strumenti utilizzati

Computer e sistemi operativi

Le macchine indicate in grasseto sono state utilizzate per il test in rete di ICARO.

Compilatori e tools Java

Tools per lo sviluppo degli Aglets

Altri strumenti


Bibliografia

Documenti online e riviste

Libri

Indirizzi utili

Linux
Java
Agenti mobili

Ringraziamenti

Vogliamo ringraziare alcune persone che ci hanno aiutato nello studio di Java, nello sviluppo di ICARO... e dunque nella preparazione di questo esame:


Alessandro Spanu g10@tiffany.diee.unica.it
Stefano Sanna stefano@mcweb.unica.it
ICARO homepage http://gulch.unica.it/~gerda/tesina/