Un Python demoniaco alla fermata del bus!

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.

Angeli e diavoli

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:

Monitor di sistema

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!

Facebook Twitter Linkedin Plusone Pinterest Email

15 pensieri su “Un Python demoniaco alla fermata del bus!

  1. hai proprio ragione, sono agli inizi, ma python è un linguaggio con delle incredibili potenzialità, e si può unire anche ad altri linguaggi

  2. 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

  3. 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.

  4. 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

  5. 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.

  6. 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


  7. Michele:

    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

    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 ^_^

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *