I miei studi sul Python proseguono a gonfie vele e oggi vi presento un piccolo HOWTO su come scrivere un demone nel linguaggio di programmazione più portabile del mondo (e a chi, in questo momento, viene in mente solo la parola Java dico: “Seee! Te piacerebbe!“).
Andiamo per gradi, iniziamo a rispondere alla prima domanda: cos’è un demone? Cito Wikipedia:
Nei sistemi Unix, e più in generale nei sistemi operativi multitasking, un demone (daemon in inglese) è un programma eseguito in background, senza che sia sotto il controllo diretto dell’utente. Di solito i demoni hanno nomi che finiscono per “d”: per esempio, syslogd è il demone che gestisce i log di sistema.
In ambiente Windows un tale programma è noto con il nome di Servizio.
Insomma, un demone è un applicazione residente in memoria che può avere, come compiti principali, quello di rispondere a delle richieste da parte di altri programmi e/o eseguire periodicamente delle procedure; questo tipo di soluzione è molto utile nel caso si intenda sviluppare un’applicazione distribuita secondo un modello client/server e nel seguente esempio verrà implementato un piccolo demone, il quale avrà come unico scopo quello di rispondere alle richieste ricevute tramite DBus, il bus di sistema di GNOME. Se pensate che possa essere un compito difficile, ve ne farò ricredere!
Ok, siamo pronti, cominciamo: innanzitutto entriamo nella nostra Home e creiamoci una cartella dove andremo a posizionare i sorgenti:
cd ~
mkdir test
Dunque creiamo il file che conterrà il codice del nostro infernale componente software:
gedit ~/test/daemon.py
Ecco fatto! Ora non resta che copiare e incollare all’interno dell’editor il seguente codice, al cui interno ho già inserito numerosi commenti, i quali, grazie anche all’estrema semplicità del linguaggio, saranno sicuramente più che sufficienti per comprendere il funzionamento del demone:
#!/usr/bin/env python # File: daemon.py # Spostiamoci nella directory corrente import os,sys if __name__ == '__main__': os.chdir(os.path.dirname(os.path.normpath(os.path.join(os.getcwd(),sys.argv[0])))) # Importiamo i moduli necessari import gobject import dbus import dbus.service if getattr(dbus, 'version', (0,0,0)) >= (0,41,0): import dbus.glib LOG_DIR = '/tmp' # Questa ci serve per memorizzare il file .pid # Il nostro oggetto condiviso class Demone(dbus.service.Object): def __init__(self, bus_name, object_path='/org/test/daemon'): # è necessario richiamare il costruttore della classe base dbus.service.Object.__init__(self, bus_name, object_path) ## Eccoci ad un punto saliente finalmente! ## Questo "stupido" metodo, che verrà richiama dalla nostra ## applicazione, fa solo una cosa: stampa a schermo un ## simpatico saluto (strano per un *demone*, vi pare?). @dbus.service.method('org.test.daemon.prova') def Hello(self): sys.stdout.write('Hello world from a dbus daemon!\n') sys.stdout.flush() return 0 #end function Hello ############## SIGNAL HANDLERS ################# ## Qui raccogliamo tutti i metodi che gestiranno i segnali ricevuti. ## In questo caso ci interessa gestire solo SIGTERM. def terminate(signal, param): try: # Esegue tutte le operazioni necessarie alla corretta # chiusura dell'applicazione # Cancella il file .pid os.remove(os.path.join(LOG_DIR, 'run.pid')) except: pass # Ignora tutti gli errori... ## ...e stampa un messaggio di addio! sys.stdout.write("........terminating\n") sys.exit(0) ############## END SIGNAL HANDLERS ############## ## Analizza e restituisce lo stato attuale del demone (se è attivo ## o meno). Restituisce il process id del demone se è attualmente ## presente in memoria, -1 in caso contrario. def status(): if os.path.isfile(os.path.join(LOG_DIR, 'run.pid')): f = open(os.path.join(LOG_DIR, 'run.pid'), 'r') pid = string.atoi(string.strip(f.readline())) f.close() try: os.kill(pid, 0) except os.error, args: if args[0] != errno.ESRCH: # Nessun processo raise os.error, args else: # il demone è avviato return pid # il demone non è presente in memoria return -1 ## Quelle che seguono sono le istruzioni relative alla gestione ## dei processi del demone: verranno creati due fork, in questo modo ## sarà possibile "staccare" il programma dal terminale e farne ## continuare l'esecuzione in background. def start(): try: # procediamo con il primo fork... pid = os.fork() if pid > 0: # chiudiamo il processo padre... sys.exit(0) except OSError, e: print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror) sys.exit(1) # "stacchiamolo" dal processo principale... os.setsid() os.umask(0) # ...e diamogli i giusti permessi! # Ora procediamo con il secondo fork... try: pid = os.fork() if pid > 0: print "test daemon: pid " + str(pid) sys.exit(0) except OSError, e: print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror) sys.exit(1) # Controlla l'esistenza della directory del file di log. Se non esiste, la crea. if not os.path.isdir(LOG_DIR): os.mkdir(LOG_DIR, 0755) # Infine scrive l'id del processo nel file .pid. f = open(os.path.join(LOG_DIR, 'run.pid'), 'w') f.write("%d" % os.getpid()) f.close() return 0 ## Se start() serve per avviarlo, immaginate un po' a che serve ## questo... Se il demone è attivo lo termina e restituisce 0, altrimenti ## stampa un avviso e restituisce -1. def stop(): ## Controlliamo la presenza del file .pid... if os.path.isfile(os.path.join(LOG_DIR, 'run.pid')): ## apre il file e legge l'id del processo... f = open(os.path.join(LOG_DIR, 'run.pid'), 'r') pid = string.atoi(string.strip(f.readline())) f.close() try: os.kill(pid, 0) ## ...dunque prova a killarlo... except os.error, args: if args[0] != errno.ESRCH: ## ...se non trova il processo raise os.error, args ## lancia un'eccezione... else: os.kill(pid, signal.SIGTERM) ##....altrimenti invia un SIGTERM return 0 ## al demone return -1 # Memorizza il nome del programma. call_name = os.path.split(sys.argv[0])[1] ## Riceve gli argomenti passati sulla riga di comando. Stabiliamo di ## processarne 3, "start", "status" e "stop", uno per ogni metodo di ## quelli definiti in precedenza. if len(sys.argv) != 2: sys.stderr.write("Usage: %s [start|stop|status]\n" % call_name) sys.exit(1) if len(sys.argv) == 2: if sys.argv[1] == 'stop': # "Stoppa" il demone s = stop() if s == 1: ## se il demone non è attivo stampa un avviso... sys.stdout.write("%s: daemon is not running\n" % call_name) sys.exit(1) ## ...e esci. sys.stdout.write("%s: stopped.\n" % call_name) sys.exit(0) elif sys.argv[1] == 'status': # Stampa lo stato del demone.... beh, questa è facile, no? s = status() if s == -1: sys.stdout.write("%s: daemon is not running\n" % call_name) else: sys.stdout.write("%s: daemon [%d] is running\n" % (call_name, s)) sys.exit(0) elif sys.argv[1] == 'start': # Oh ecco, qui avviamo il demone, verificando prima che non sia già # già stato caricato in memoria. s = status() if s != -1: sys.stdout.write("%s: daemon [%d] is running\n" % (call_name, s)) sys.exit(1) start() else: ## e qui ricordiamo all'utente come funziona il nostro demone.... sys.stderr.write("Usage: %s [stop|status]\n" % call_name) sys.exit(1) ## Terminata la parte "infernale", ovvero quella relativa al demone, ## passiamo ad occuparci del lato server. # Apriamo la connessione con DBus e "attacchiamoci" al bus della sessione. session_bus = dbus.SessionBus() # Per un demone di sistema va sostituito con dbus.SystemBus() try: # Creiamo, inizializziamo e "attacchiamo" l'oggetto a DBus bus_name = dbus.service.BusName('org.test.daemon', bus=session_bus) object = Demone(bus_name) except Exception, e: print "Errore ", e # Ora non resta che avviare il loop principale mainloop = gobject.MainLoop() mainloop.run() # Il demone è pronto e già freme nell'attesa di # rispondere alle nostre richieste!
Ok, salvato il file apriamo un terminale e diamogli i giusti permessi con
chmod +x ~/test/daemon.py
Finito! Per avviarlo basta digitare il nome del comando seguito dall’opzione start:
~/test/daemon.py start
Successivamente, per stopparlo, basterà sostituire start con stop.
Se si vuole essere sicuri che tutto sia andato a buon fine, su Ubuntu è possibile andare su Sistema -> Amministrazione -> Monitor di sistema e verificare con i propri occhi la presenza del demone fra i processi residenti in memoria:
Soddisfatti? Io molto e sì, sono uno che si diverte con davvero poco! Ma soprattutto mi diverte farmi stupire da Python, un linguaggio davvero sbalorditivo per come riesce a unire potenza, semplicità e grandi performance (e qui mi viene da pensare a Java: “oh! come rido!” :D)
Alla prossima puntata, dove scriveremo una piccola applicazione capace di interagire con il nostro piccolo diavolo!
hai proprio ragione, sono agli inizi, ma python è un linguaggio con delle incredibili potenzialità, e si può unire anche ad altri linguaggi
ma la libreria gobject dove la prendi? =)
Anche io vorrei fare una specie di demone, o servizio in windows, un programma che resti in esecuzione e svolga il suo compito in background ogni 15 minuti.
mi saresti una mano?
Saluti
Cioa,
ho provato a fare il copia ed incolla del tuo codice, ma mi esce sempre tutto sfasato e quando cerco di eseguirlo ho errori vari sull’indentazione.
Ho provato anche a copiarlo riga per riga, ma nulla.
Ho anche degli errori sulle vocali accentate nei commenti.
potresto mettere un link ad un file da scaricare dirtettamente con il sorgente, o in alternativa, visto che hai la mia mail, spedirmelo?
Sto iniziando anche io a programmare in python, e so che questo non e’ il modo migliore, iniziare direttamente dai demoni, ma mi servirebbe capire come funziona (sperimentarlo) per un progettino che vorrei realizzare.
In COBOL (linguaggio che conosco) non si puo’ demonizzare nulla.
Aggiornamento: sono riuscito a farglielo eseguire ma avviandolo con start ricevo solo:
test daemon: pid 3549
Se invece gli do status, ricevo:
paride@imladris:~/work$ ./daemon.py status
Traceback (most recent call last):
File “./daemon.py”, line 150, in
s = status()
File “./daemon.py”, line 57, in status
pid = string.atoi(string.strip(f.readline()))
NameError: global name ‘string’ is not defined
Cosa sto sbagliando?
La versione di python che uso e’ la 2.5.2 di Debian Lenny
prova con
import string
Ciao Paride devi importare il modulo string aggiungendo sotto gli altri “import string”. Infatti nell’errore lo script non riesce a trovare le funzioni strip e atoi del modulo string.
uhm
Avevo scritto un bel commento ma non me l’ha pubblicato…va beh.
In sintesi, aggiungere:
import string, errno, signal
Poi, dopo: #!/usr/bin/env python
aggiungere: # -*- coding: cp1252 -*-
per non aver problemi con le vocali accentate.
Forse è finito nello spam, non saprei :/
Manca anche l’invocazione del metodo Hello. Verso la fine dello script,
try:
bus_name = dbus.service.BusName(‘org.test.daemon’, bus=session_bus)
object = Demone(bus_name)
object.Hello() #<—Aggiungere
except Exception, e:
print "Errore ", e
A parte queste sviste, ottimo articolo 😉
Grazie, sistemerò appena possibile! 🙂
Hemm, ma poi l’altro articolo non l’hai scritto, o non lo trovo io?
Chiedo venia, non ho avuto più il tempo di scriverlo.
Spero di farlo presto :-/
Complimenti per l’articolo.
Volevo anch’io realizzare una cosa simile a quella di Michele (per Windows), sto muovendo qualche passo in Python e lo scopo è leggere (solo leggere) dati da un mdb in rete intranet ogni tot minuti. Il database è sfruttato dai diversi client locali come backend e mi serve essere informato se qualcuno aggiorna le diverse tabelle tramite il proprio client.
Ma non so come iniziare ^_^