MokaByte 101 - 9mbre 2005
 
MokaByte 101 - 9mbre 2005 Prima pagina Cerca Home Page

 

 

 

Controllo a distanza

Immaginate di avere un'applicazione java stand-alone, ad esempio un blocco note. Come è possibile governare l'applicazione, non attraverso il mouse, ma usando comandi batch? L'esempio sembra forzato, ma non fatevi ingannare, un mecanismo simile entra in gioco ogni volta che fate lo shutdown della macchina. Il sistema operativo chiude tutte le applicazioni che sono rimaste, eventualmente, aperte. L'astrazione del problema è questa: ho un'applicazione java che ha una gui e, quindi, prevede una normale interazione con l'utente, ma voglio anche che sia possibile inviare dei comandi all'appicazione stessa da una procedura batch.

Il canale
Il meccanismo più semplice è quello di comunicare con l'applicazione attraverso il canale dei socket. Naturalmente questo implica che l'applicazione "guidata", o meglio un suo modulo, stia ad ascoltare una porta e, ricevuto un comando, lo esegua.
Ecco allora già ben definito il nostro primo punto di arrivo: costruire un piccolo modulo che sia in grado di ascoltare su di un canale e trasmettere gli eventuali comandi ricevuti ad un'applicazione.
Naturalmente, l'ascoltare un canale non deve interferire con la normale interazione che l'applicazione ha con l'utente finale. Questo ci costringe a pensare il modulo di ascolto come un thread che assolva questo compito specifico.
Per quanto riguarda la parte di comunicazione tra il modulo socket e l'applicazione gui, la cosa più naturale è quella di estendere il meccanismo dei listener. In pratica così come avviene per i normali componenti grafici, sui quali è possibile aggiungere degli ascoltatori per un componente specifico,così una qualsiasi classe può essere in grado di ricevere comandi via socket implementando i metodi della corrispettiva interfaccia.

 

E l'applicazione cosa fa?
Naturalmente l'applicazione socket-comandata è libera di fare quello che più vi piace, ma per il nostro esempio qualcosa si doveva pur scegliere e quindi abbiamo deciso di costruire un'applicazione che fosse in grado di eseguire comandi batch associati ad un nome logico. In pratica da una lista sarà possibile selezionare un elemento al quale corrisponde l'esecuzione di un processo. Con la pressione di un tasto la procedura verrà eseguita.

 

L'unione fa la forza!
Unendo queste due caratteristiche riusciremo ad ottenere qualcosa di veramente carino. Non va dimenticato, infatti, che l'aver scelto il socket come canale di comunicazione ci permette di pilotare l'applicazione target da un punto della rete qualsiasi. In pratica ciò significa, che se riusciremo nel nostro intento, avremo costruito un'applicazione in grado di eseguire processi batch su un server a partire da un qualsivoglia client; costruiremo un "remote Commander"!

 

Il fascino della TeleInterazione
Alzi la mano chi non è rimasto affascinato da strumenti quali il "Terminal Server", oppure non è rimasto a bocca aperta davanti allo schermo della sua macchina, mentre un assistente in remoto, configurava qualche parte del sistema. E' il fascino della tele interazione che può raggiungere i livelli più alti quando i tecnici della Nasa riescono a guidare il robottino su marte direttamente dalla terra.

 

Attuatori ed informazione veloce
Anche se internet ha permesso, rispetto alla tele presenza ed alla tele interazione, di sviluppare applicazioni stupefacenti ed ora di uso molto comune come ad esempio l'istant messagging o i giochi in rete, nell'era pre-internettautica lo strumento più tele interattivo in assoluto è stato senza dubbio il telefono, mentre un'altra applicazione diffusissima che costituisce in pratica, una sorta di tele trasporto cartaceo è il fax.
Pensando un po' in astratto a queste applicazioni, possiamo identificare due meta elementi che le caratterizzano:

  • l'attuatore;
  • il canale.

Iniziamo dal secondo elemento, il canale, che è la via fisica attraverso la quale una sorta di informazione corre molto velocemente da un punto ad un altro e viceversa. Agli estremi di questa via si trovano due attuatori che sono, generalmente, una qualsiasi sorta di apparecchiaitura tecnologica in grado di interpretare l'informazione che corre nel canale e trasformarla in informazioni che una persona può capire e con la quale può interagire. L'attuatore deve essere in grado di compiere anche il processo inverso, cioè riimodulare il segnale che l'utente genera per immetterlo nuovamente nel canale e spedirlo verso l'altro punto del sistema.

 

Attuatori sistemati in posti strani...
La cosa più interessante di questi attuatori è che possono essere sistemati nei posti più strani, ho sentito di un paio di pianoforti, uno sistemato a New York, l'altro in Giappone. Un pianista suonava in America ed il pubblico giapponese ascoltava la musica come se il pianista stesse utilizzando la tastiera del pianoforte giapponese. Attuatori un po' più alla portata di tutti sono gli ultimi modelli di WebCam con scheda telefonica incorporata che possono spedire suoni ed immagini ad un telefonino. Ancora più interessanti sono qquegli attuatori che non sono posti in mondi reali, ma in mondi virtuali.
Ed ora è come se la storia volesse farci uno scherzetto! Se tutto è partito dal telefono al telefono sembra essere ritornato, infatti una delle applicazioni più diffuse e promettenti in quanto a velocità di espanzione ed utilizzo è proprio il VOIP (Voice Over IP).

 

Il disegno
Dopo queste brevi riflessioni torniamo a quello che era il nostro problema tecnico di partenza: costruire una semplice architettura in grado di inviare comandi da un PC all'altro all'interno di una rete.
Per i più distratti occorre sottolineare che quella che cercheremo di costruire è un'applicazione Client/Server. Questo significa che nel disegno dell'applicazione dovremmo distinguere le due parti: il Client ed il Server appunto...

 

Il server
Iniziamo dalla parte più complessa, il lato server. L'applicazione lato server è costituita da due modoli di facile identificazione: il modulo esecutore che ha il compito di lanciare una procedura batch che l'utente è in grado di scegliere attraverso un nome logico; ed il modulo Socket il cui compito è ascoltare un determinato canale in attesa che arrivino dei comandi da eseguire. Naturalmente, una volta arrivato un comando valido, il modulo stesso deve essere in grado di trasmetterlo all'applicazione che lo eseguirà come si trattasse di un normale comando fornito dall'utente attraverso l'uso del mouse o della tastiera.

 

Il client
Molto più semplice è il compito della parte client: si tratta di un programmino che deve essere in grado, aprendo una comunicazione socket, di inviare un comando all'applicazione bersaglio.
Eseguire processi Batch in java

Per eseguire un processo batch in java, la prima cosa da effettuare è quella di recuperare l'ambiente di runtime con il metodo getRuntime() della classe Runtime. Il metodo exec() è, infine, quello che esegue il comando batch. Questo metodo prevede tre parametri:

  • il comando da lanciare;
  • un array di stringhe che nella forma chiave=valore, rappresentano variabili d'ambiente utilizzate dal processo che si intende lanciare;
  • la directory di lavoro che è la directory nella quale il processo lanciato lavorerà.

Per semplificare la gestione di questi parametri, abbiamo costruito un piccolo oggetto che li contiene tutti.

package org.illeva.remcom;

import java.util.*;
import java.io.*;
/**
* E' la classe che esegue i processi batch.
*/
public class OsRunner
{
// Data members
/** Nome logico del processo da eseguire */
private String dName;
/**
* Nome fisico del processo da eseguire. Si tratta di un vettore di stringhe perchè il nome fisico del processo
* può essere la composizione di più stringhe
*/
private Vector dCmd=new Vector();
/**
* E' la collezione di coppie chiave/valore che costituiscono
* le variabili d'ambiente con le quali
* il processo in eseguzione può interagire.
*/
private HashMap dEnv=new HashMap();
/** E' la directory di lavoro del processo. */
private File dWorkDir;
[...]
} // end class

Di seguito, sempre dalla medesima classe, riportiamo il metodo che esegue il processo batch:

public void run()
{
String[] vCmd=null;
String[] vEnv=null;
/* Trasforma il vettore di stringhe che rappresenta il comando da lanciare
in un array di stringhe. */
if(dCmd.size()>0)
{
vCmd=new String[dCmd.size()];
for(int i=0; i0)
{
vEnv=new String[dEnv.size()];
Iterator i=dEnv.keySet().iterator();
int j=0;
while(i.hasNext())
{
String vTemp=(String)(i.next());
vEnv[j++]=vTemp+"="+((String)(dEnv.get(vTemp)));
} // end while
} // end if
try
{
Process vChild=Runtime.getRuntime().exec(vCmd,vEnv,dWorkDir);
}
catch(IOException anEx)
{
anEx.printStackTrace(System.err);
} // end catch
} // end method

 

La GUI del server

L'interfaccia dell'applicazione server è molto semplice: una lista di comandi batch che possono essere eseguiti, e due bottoni; uno per l'esecuzione del comando scelto dalla lista e l'altro bottone per abilitare o disabilitare il modulo di ascolto remoto che permette di eseguire gli stessi comandi ma da un altro PC.

Per quanto riguarda il funzionamento di questa parte di codice non vi è nulla di sostanziale da sottolineare o su cui valga la pena porre la nostra attenzione. Rimandiamo i nostri lettori alla lettura diretta del codice sorgenteper chi volesse approfondire questa parte il cui funzionamento è quello classico delle "vecchie" interfacce grafiche implementate con AWT.

 

Il file di configurazione
Spendiamo invece due parole sulla struttura del file di configurazione, per tutte quelle persone che volessero utilizzare realmente l'applicazione. Nella directory "config" è presente un file nominato "process.txt", dove vengono destritti i processi batch che è possibile lanciare:

#
# File process definition
#

#
# e' il nome logico del processo. Quello che compare nella lista dei processi che
# l'utente può scegliere di eseguire.
#
ProcName=Test
#
# e' la stringa di comando che costituisce l'esecuzione vera e propria del
# processo batch.
#
CmdList=cmd.exe,/C,start,funny.bat
#
# E' la directory di lavoro nella quale il batch viene eseguito
#
WorkDir=D:/Batch
ProcName=Prova
CmdList=cmd.exe,/C,start,prova.bat
#
# E' la lista di variabili d'ambiente che il processo batch può utilizzare durante la sua esecuzione.
#
EnvList=JAVA_HOME=D:/src,CLASSPATH=D:/classes
WorkDir=D:/Batch

In un secondo file denominato "MainSocket.properties" possiamo impostare la porta sulla quale il socket server ascolta le richieste che possono pervenire da vari client ed il tempo in millisecondi nel quale il thread resta in attesa dei comandi prima di dare un po' di ascolto anche all'interfaccia GUI che lo ha generato perchè da questa potrebbero arrivare ordini del tipo: "Mi sto chiudendo, chiudi tutto anche tu..." oppure "Per il momento smetti di ascoltare la porta socket..."

# MainSocket configuration files
# Porta sulla quale ascolta il socket
port=1304
# E' il numero massimo di richieste che possono essere accodate prima di essere servite.
backlog=2
# Tempo di attesa, in millisecondi, per una richiesta sul canale socket
sleep=3000

Il parametro backlog è quello che permette di difinire quante richieste, al massimo, possono essere accodate in attesa di essere servite.

 

Il modulo d'ascolto
Cuore del modulo d'ascolto che abbiamo racchiuso nel package cmdsock per rendere più agevole il lavoro a chi volesse estrapolarlo da questa applicazione e usarlo in altri contesti, e la classe MainSocket le cui caratteristiche principali sono due: è un thread e ha al suo interno un serverSocket che ha il compito di smistare le richieste in arrivo dai vari client:

package org.illeva.remcom.cmdsock;

import java.io.*;
import java.util.*;
import java.net.*;

/**
* E' il thread che rimane in ascolto su di una porta per ricevere i comandi che verranno trasmessi
* all'applicazione target.
*/
public class MainSocket extends Thread
{
// Data members
/** E' il flag per gestire l'esistenza del thread. */
private boolean dExists=true;
/** E' il flag che attiva il thread oppure ne sospende le attività. */
private boolean dActive=true;
/** E' il socket che ascolta una porta in attesa di comandi. */
private ServerSocket dSock=null;
/** File di configurazione per la configurazione dell'applicazione. */
private Properties dProp=new Properties();
/** E' il tempo di sleep tra l'attesa di un comando ed il successivo. */
long dSleep=3000;
/** E' l'applicazione target a cui i comandi andranno inviati. */
private SocketListener dApp=null;
[...]
} // end class

Molto importante è la dichiarazione dell'ultimo dato membro, che altro non è che l'applicazione GUI alla quale andranno inviati i comandi ricevuti dal canale socket. Abbiamo scelto, per una migliore libertà di progettazione, di usare una interfaccia che stabilisca un canale di comunicazione con il modulo socket.
In effetti l'interfaccia SocketListener è molto semplice e definisce due metodi che permettono di inviare all'applicazione in ascolto i comandi che vengono ricevuti via socket:

package org.illeva.remcom.cmdsock;

/**
* E' l'interfaccia che l'applicazione target deve implementare per eseguire i comandi ricevuti via socket.
*/
public interface SocketListener
{
/** E' il metodo per l'esecuzione dei comandi GUI, cioè quelli che in qualche modo
riguardano l'aspetto grafico. */
public void processWinCommand(SocketEvent anEvent);
/** Esegue i comandi di tipo applicativo. */
public String processApplicationCommand(SocketEvent anEvent);
} // end interface

Abbiamo costruito una classe specifica per gestire gli eventi che possono arrivare via socket:

package org.illeva.remcom.cmdsock;

import java.util.EventObject;

/**
* E' l'evento che lapplicazione target deve gestire.
*/
public class SocketEvent extends EventObject
{
/** E' un evento di tipo Window che in qualche modo riguarda la GUI dell'applicazione target */
public static final int WIN_CMD=1;
/** E' un evento di tipo applicativo. */
public static final int APP_CMD=2;
public static final int WIN_CLOSE=10;
public static final int WIN_HIDE=11;
public static final int WIN_SHOW=12;
public static final int APP_PARAM=100;
public static final int APP_CMDLIST=101;
// Data members
/** E' il tipo di evento da gestire */
private int dType=0;
/** E' il parametro dell'evento */
private String dParam;
/** E' il comando che va eseguito */
private int dCommand=0;
/** Ritorna il tipo di comando. */
public int getType() {return dType;}
/** Ritorna il comando da eseguire. */
public int getCommand() {return dCommand;}
/** Ritorna il parametro del comando. */
public String getParam() {return dParam;}
/**
* Costruttore dell'evento.
*/
public SocketEvent(Object aSource,int aType,int aCommand,String aParam)
{
super(aSource);
dType=aType;
dCommand=aCommand;
dParam=aParam;
} // end constructor
/**
* Costruttore dell'evento con parametro nullo.
*/
public SocketEvent(Object aSource,int aType,int aCommand)
{
this(aSource,aType,aCommand,null);
} // end constructor
} // end class

Torniamo per un attimo alla classe MainSocket per dare un'occhiata al metodo principale della classe stessa, il metodo run il cui compito è quello di smistare le chiamate in arrivo facendo in modo che ogni chiamata continui il suo dialogo su di un canale privato per liberare il più presto possibile il canale principale in attesa di nuove richieste:

/**
* E' il metodo vero e proprio del thread. Non appena viene ricevuto un comando, un socket viene associato
* alla richiesta ed inizia il colloquio vero e proprio fra Client e Server.
*/
public void run()
{
/* Ciclo che regola l'sistenza del thread */
while(dExists)
{
/* Ciclo che regola l'ascolto o meno sul canale del thread stesso */
while(dActive)
{
Socket vSock=null;
try
{
// System.out.println("Prima della accept...");
vSock=dSock.accept();
new ChildSocket(vSock,dApp).start();
System.out.println("Ho creato un Child...");
} // end try
catch(SocketTimeoutException anEx)
{
// System.out.print(".");
} // end catch
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end while
/* Se l'ascolto sul canale è disattivato il thread dorme per un po' di tempo */
try
{
// System.out.println("Sto dormendo...");
sleep(dSleep);
} // end try
catch(InterruptedException anEx)
{
System.out.println("MainSocket wake up...");
} // end catch
} // end while
} // end method

Una volta che la richiesta arriva al ServerSocket il dialogo vero e proprio tra client e server viene gestito dalla classe ChildSocket. Essendo praticamente possibile che più richieste si sovrappongano, ciascuna classe che gestisce il dialogo tra server e client deve essere un thread in modo da riuscire a parallelizzare il lavoro.

package org.illeva.remcom.cmdsock;

import java.net.*;
import java.io.*;
import java.awt.event.*;

/**
* E' il socket lato server che viene creato ad ogni richiesta che il ServerSocket riceve.
*/
public class ChildSocket extends Thread
{
// Data members
/** E' il canale di comunicazione che si apre fra i due socket. */
private Socket dSock=null;
/** E' l'applicazione target. */
private SocketListener dApp=null;
/** E' la Stringa da spedire. */
private String dSend;
[...]
} // end class

Anche per questa classe il cuore di tutto è il metodo run dove, con una semplicissima macchina a stati, si gestisce il dialogo tra client e server:

/**
* Gestisce il protocollo di comunicazione, lato server, con il client.
*/
public void run()
{
try
{
PrintWriter vOs=new PrintWriter(dSock.getOutputStream(),true);
BufferedReader vIs=new BufferedReader(new InputStreamReader(dSock.getInputStream()));
int vStatus=1;
dSend="Hy rcClient";
String vReceive="";
String vCmd=null;
while(vStatus>0)
{
vOs.println(dSend);
vReceive=vIs.readLine();
if(vReceive.equals("Bye"))
vStatus=0;
else
switch(vStatus)
{
case 1:
if(vReceive.startsWith("Hy rcServer, i'm"))
{
vStatus=2;
dSend="Sends command";
} // end if
else
vStatus=0;
break;
case 2:
if(vReceive.equals("WinCmd(Close)"))
vCmd=vReceive;
else
executeCommand(vReceive);
// vSend="Executed command "+vReceive;
vStatus=3;
break;
default:
vStatus=0;
} // end switch
} // end while
vOs.close();
dSock.close();
/* Se alla fine del dialogo è stato ricevuto il comando, allora
lo trasmette all'applicazione.
Questo perchè c'è almeno un comando che non può essere eseguito immediatamente,
quello di chiusura dell'applicazione. */
if(vCmd!=null)
executeCommand(vCmd);
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end method

In fase di testing si è scoperto che esiste almeno un comando che non può essere inviato immediatamente all'applicazione, cioè il comando che chiude l'applicazione stessa. Se questo viene eseguito immediatamente, il dialogo fra client e server si interrompe ed il client riceve un'eccezzione di IO. Questo è il motivo per il quale, una volta chiuso il canale di comunicazione controlliamo se c'è un comando che deve essere eseguito.

 

Il programma client
Paassiamo ora ad analizzare la parte client di tutto il progetto, e cominciamo subito dalla comunicazione via socket che avevamo appena lasciato dal lato server.

 

Il dialogo lato client
Il dialogo sul lato del fiume che è abitato dai clients è gestito dalla semplice classe che con un enorme sforzo di fantasia creativa abbiamo chiamato "Client". Glissando su questo fatto che da solo basterebbe a farci radiare dall'albo dei programmatori, diamo un'occhiata alla sua struttura:

package org.illeva.remcom.cmdsock;

import java.net.*;
import java.io.*;

/**
* E' la classe che gestisce il protocollo di comunicazione, lato client. In pratica invia i comandi all'applicazione target.
*/
public class Client
{
// Data members
/** E' il canale di comunicazione */
private Socket dSock=null;
/** E' il comando da inviare */
String dCmd;
[...]
} // end class

Al costruttore di questa classe viene passato un IP e la porta da usare per la chiamata al server:

/**
* E' il costruttore della classe.
*/
public Client(String aHost,int aPort,String aCmd)
{
dCmd=aCmd;
try
{
dSock=new Socket(aHost,aPort);
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
} // end method

Come al solito, comunque, il metodo run è quello che racchiude il lavoro vero e proprio che la classe esegue:

public String run()
{
String vRc="";
try
{
PrintWriter vOs=new PrintWriter(dSock.getOutputStream(),true);
BufferedReader vIs=new BufferedReader(new InputStreamReader(dSock.getInputStream()));
int vStatus=1;
String vReceive="";
String vSend="";
while(vStatus>0)
{
vReceive=vIs.readLine();
System.out.println("[Rec] "+vReceive);
switch(vStatus)
{
case 1:
if(vReceive.startsWith("Hy rcClient"))
{
vSend="Hy rcServer, i'm user,password";
vStatus=2;
} // end if
else
{
vSend="Bye";
vStatus=0;
} // end else
break;
case 2:
if(vReceive.equals("Sends command"))
{
vSend=dCmd;
vStatus=3;
} // end if
else
{
vSend="Bye";
vStatus=0;
} // end else
break;
case 3:
if(vReceive.startsWith("Executed command"))
{
vSend="Bye";
vStatus=0;
} // end if
else
{
vRc=vReceive;
vSend="Bye";
vStatus=0;
} // end else
break;
default:
vSend="Bye";
vStatus=0;
} // end switch
vOs.println(vSend);
System.out.println("[Send] "+vSend);
} // end while
vOs.close();
dSock.close();
} // end try
catch(IOException anEx)
{
System.err.println(anEx);
} // end catch
return vRc;
} // end method

La stringa che il metodo restituisce è la risposta che il server da alla chiamata effettuata. Nella maggior parte dei casi questa risposta conferma l'avvenuta esecuzione del comando richiesto. Vi è però una richiesta particolare alla quale il server risponde con la lista di tutti i comandi che possono essere eseguiti su quella macchina.

 

Un dialogo di esempio
A titolo di esempio riportiamo un dialogo tra client e server per cercare di capire quali sono le informazioni che vengono scambiate.

[Rec] Hy rcClient
[Send] Hy rcServer, i'm user,password
[Rec] Sends command
[Send] App(CmdList)
[Rec] CmdList(Win(Close),Win(Hide),Win(Show),App(Batch1),App(Batch2))
[Send] Bye

Il dialogo sopra riportato è quello che si instaura quando un client con interfaccia grafica, all'atto della partenza chiede al server quali sono i comandi che possono essere eseguiti.

La GUI del client
L'interfaccia del programma client è molto semplice: una combobox dove sono presenti tutti gli hosts con relativa porta ai quali è è possibile inviare un comando. Una volta scelto l'host, la lista sotto la combo viene riempita con i comandi che è possibile eseguire sull'ost selezionato. Scelto un comando dalla lista, la pressione del bottone di esecuzione invia il comando al PC target.

Come per la parte server, anche il programma client per funzionare correttamente ha bisogno di un file di configurazione che riporta la lista degli host che possono essere contatttati con la rispettiva porta alla quale rispondono. Il file che si trova nella directory config si chiama hosts.txt:

#
# Lista dei Server
#
localhost(1304)
# test(1305)

Un'occhiata al sorgente che esegue il programma client ci permette di scoprire una caratteristica interessante:

package org.illeva.remcom;

import org.illeva.remcom.cmdsock.Client;
import org.illeva.remcom.gui.ClientWin;

public class MainClient
{
public static void main(String[] argv)
{
if(argv.length==3)
{
Client vObj=new Client(argv[0],new Integer(argv[1]).intValue(),argv[2]);
System.out.println("Return: "+vObj.run());
} // end if
else
{
ClientWin vObj=new ClientWin();
vObj.show();
vObj.pack();
} // end else
} // end main
} // end class

Il programma lanciato con i parametri che rappresentano un host, una porta sulla quale operare ed un comando da lanciare, invia direttamente il comando senza aprire la GUI. Questo è utile per chi volesse costruirsi un comando batch da inserire direttamente sul desktop del computer. Richiamato senza parametri, il programma parte mostrando la GUI attraverso la quale l'utente può operare normalmente.

 

Implemetazioni possibili
Ci preme sottolineare che pur trattandosi di un programma dimostrativo, può essere utilizzato in un contesto reale. Se si dovesse comunque utilizzare l'architettura in un ambiente professionale, personalmente riorganizzerei tutto il disegno per rendere possibile il poter pilotare più applicazioni diverse utilizzando un solo modulo di comando socket. Così com'è strutturato, se si devono pilotare due applicazioni diverse sullo stesso server, occorrono due istanze del programma che utilizzino due porte diverse.
Un'altra pecca del progetto è che, una volta lanciato il comando, il server non rinvia al client nessun tipo di informazione sul batch in esecuzione (potrebbe ad esempio rinviare l'output del processo lanciato). Personalmente ho, in parte, ovviato a questo problema utilizzando il comando net send di windows, comando che permette di visualizzare messaggi su di una determinata macchina.
Per chi volesse mettere le mani in pasta, questi sono i due principali filoni di sviluppo, oltre, magari, a riscrivere la GUI utilizzando le swing, che risultano essere un po' più eleganti della AWT che abbiamo utilizzato per semplicità.
Per chi volesse dare un'occhiata alla documentazione generata con javadocs, abbiamo cercato di commentare tutte le parti del codice sorgente in modo da rendere più chiaro possibile quale sia il disegno di questa piccola applicazione. L'applicazione completa può essere scaricata da questo stesso link.