Escribir un script en Python que acabará siendo ejecutado como demonio puede ser toda una aventura, te explico paso a paso cómo evitar los problemas más frecuentes utilizando los recursos que ya hay en la red.
El problema a fondo
Cuando demonizamos un script hay dos tareas que debemos realizar:
- Separar el proceso de su tty (mediante dos llamadas a fork)
- Desviar las salidas estándar y error (stdout, stderr) a un fichero (en /var/log/)
- Ejecutarlo o cambiar el propietario a usuario, si se produce un error no debe tener privilegios de super usuario.
- El proceso tiene que ejecutarse fuera de cualquier directorio para evitar conflictos con mount.
Al separar un proceso de su tty se rompen los vínculos de stdio, stdout y stderr, por lo que si se realiza alguna operación sobre estos dispositivos se producirá una excepción que cerrará nuestro proceso y no se nos facilitará el mensaje de error. Así que lo primero que hay que hacer antes de separar el proceso del tty es desviar los dispositivos estándar a un fichero, típicamente situado en /var/log.
Para facilitarnos as tarea aprovecharemos el trabajo hecho por nuestros amigos de ActivateState y Chris, se trata de un pequeño script que se encargará de realizar todo lo necesario para que nuestra aplicación no necesite apenas cambios para funcionar.
La solución
Primero escribiremos un script que diga "Hola mundo" cada 10 segundos, definimos una función llamada "start" que inicialice la clase. Llamaremos al script
hello.pyMi script Hello.py
#!/usr/bin/python
# -*- coding: utf8 -*-
import time
class HolaMundo:
def Hola(self):
print "hola mundo!"
def Ejecutar(self):
while 1:
self.Hola()
time.sleep(10)
def start():
hm = HolaMundo()
hm.Ejecutar()
Ahora ya podemos incluirlo desde el fuente que he llamado oportunamente
hello-daemon.pyEl script Hello-Daemon.py
#!/usr/bin/python
# -*- coding: UTF8 -*-
###########################################################################
# configure these paths:
LOGFILE = '/var/log/hello-daemon.log'
ERRORLOGFILE = '/var/log/hello-daemon.err'
PIDFILE = '/var/run/hello-daemon.pid'
# and let USERPROG be the main function of your project
import hello
USERPROG = hello.start
###########################################################################
#based on Jürgen Hermanns http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
import sys, os, time
class Log:
# file like for writes with auto flush after each write to ensure that everything is logged,
# even during an unexpected exit.
def __init__(self, f):
self.f = f
def write(self, s):
# Gracias a Nubio por este condicional!!
if s.strip() == '': return
self.f.write('%s -- %s\n' % (time.ctime(), s)) self.f.flush()
def main():
#change to data directory if needed
os.chdir("/")
#redirect outputs to a logfile
sys.stdout = Log(open(LOGFILE, 'a+'))
sys.stderr = Log(open(ERRORLOGFILE, 'a+'))
#ensure the that the daemon runs a normal user
#set user and group first "pydaemon"
os.setegid(1000)
os.seteuid(1000)
#start the user program here:
print "Starting daemon"
USERPROG()
print "Endding daemon"
if __name__ == "__main__":
# do the UNIX double-fork magic
# see "Stevens Advanced Programming in the UNIX Environment"
# for details (ISBN 0201563177)
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
sys.exit(1)
# decouple from parent environment and do not prevent unmounting
os.chdir("/") os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent, print eventual PID before
open(PIDFILE,'w').write("%d"%pid) sys.exit(0)
except OSError, e:
print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
sys.exit(1)
# start the daemon main loop
main()
start():
He adaptado un poco el script de Chris para que envíe los mensajes de error a un fichero separado y registrando la hora en la que seceden los mensajes, además, he añadido los correspondientes mensajes de inicio y fin del proceso. En referencias tienes un enlace a la página de Chris donde puedes encontrar el original sin mis modificaciones.
Ahora toca preparar el script de arranque del demonio, yo utilizo gentoo así que el script es una adaptación del script que arranca
gpm, es posible que necesites hacer más de un cambio para adaptarlo a tus distribución.
Script /etc/init.d/hello
#!/sbin/runscript
# Copyright 1999-2004 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
depend() { # El script lo uso para controlar los servicios del servidor, en este caso requiero la ejecución
# de apache.
after apache
}
start() { ebegin "Starting Hello Daemon"
start-stop-daemon --start --pidfile /var/run/hello-dameon.pid --exec /root/daemon/hello-dameon.py
eend ${?}}
stop() { ebegin "Stopping Hello Daemon"
start-stop-daemon --stop --pidfile /var/run/hello-dameon.pid
eend ${?}}
Ahora ya sólo falta probarlo, arranca el servicio y comprueba los logs. ¡Suerte!
Una última aclaración, los scripts que te estás viendo no han sido probados, he copiado los míos y los he adaptado al clásico hola mundo, además es posible que al editarlos haya metido algún error, si lo ves no dudes en ponerte en
contacto conmigo y lo arreglaré lo antes posible.
Agradecimientos
- Gracias a Nubio por su idea para filtrar del log los saltos de línea.
- Gracias a LF por sus correcciones.
Referencias