Criando um daemon com Python

Fala, pessoal, tranquilidade? Como de costume, fiquei muito tempo sem postar. Pre voltar, vamos ver como criar um daemon usando Python. Mas primeiro...

O que é um daemon?

Um daemon é um processo que fica sendo executado em segundo plano, sem contato interativo com um usuário e desassociado de um tty. O nome daemon vem do Demônio de Maxuel, e foi usado pela primeira vez (em computação, claro) pelo pessoal do projeto MAC[1]. Os daemons estão presentes no Unix desde os primórios. Aquele monte de *d que você vê, como sshd, httpd, crond e etc, são todos daemons.

Como criar um daemon?

Bom, a explicação rápida pra isso é: com o bom e velho fork-off-and-die. Cria-se um uma cópia de um processo, mata-se o processo pai e faz-se o trabalho no processo filho. A explicação longa é a seguinte:

  1. Cria-se um fork e sai do processo pai. Com isso, libera-se o controle de shell se o daemon foi invocado de um. Também atribui-se outro id para o processo filho, fazendo com que ele não seja session leader.
  2. Criar uma nova sessão sem um terminal de controle associado.
  3. Criar outro fork e sair novamente do pai - aquele que foi filho antes para garantir novamente que o processo não será session leader.
  4. Alterar a máscara de arquivos (umask).
  5. Alterar o diretório de trabalho.
  6. Fechar todos os descritores de arquivos descecessários.
  7. E, agora sim executar o seu trabalho.

Por que o segundo fork()?

Bom, essa é uma discussão grande. Geralmente é dito que o segundo fork é necessário para evitar que o seu daemon obtenha um terminal de controle nos SystemV R4. Como esse já é um sistema em desuso, diz-se que o segundo fork é desnecessário. Mas a especificação POSIX diz o seguinte[2]:

The controlling terminal for a session is allocated by the session leader in an implementation-defined manner. If a session leader has no controlling terminal, and opens a terminal device file that is not already associated with a session without using the O_NOCTTY option (see open()), it is implementation-defined whether the terminal becomes the controlling terminal of the session leader

Então, como é uma questão de implementação, prefiro continuar usando um segundo fork. :) Agora, vamos parar de falação e ir pro que importa.

O código

#-*- coding: utf-8 -*-

import os
import sys
import resource

def create_daemon(stdout, stderr, working_dir):
    """
    cria um daemon
    """
    # faz o primeiro fork
    _fork_off_and_die()
    # cria uma nova sessão 
    os.setsid()
    # faz o segundo fork
    _fork_off_and_die()
    # altera a máscara de arquivos
    os.umask(0)
    # altera o diretório de trabalho
    os.chdir(working_dir)
    # fecha todos os descritores de arquivos
    _close_file_descriptors()
    # redireciona stdout e stderr
    _redirect_file_descriptors(stdout, stderr)

def _fork_off_and_die():    
    """
    cria um fork e sai do processo pai
    """
    pid = os.fork()
    # se o pid == 0, é o processo filho
    # se o pid > é o processo pai
    if pid != 0:
        sys.exit(0)

def _close_file_descriptors():
    # Fechando todos os file descriptors para evitar algum
    # lock

    # RLIMIT_NOFILE é o número de descritores de arquivo que
    # um processo pode manter aberto
    limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
    for fd in range(limit):
        try:
            os.close(fd)
        except OSError:
            pass

def _redirect_file_descriptors(stdout, stderr):
    """
    redireciona stdout e stderr 
    """
    # redirecionando stdout e stderr
    for fd in sys.stdout, sys.stderr:
        fd.flush()

    sys.stdout= open(stdout, 'a', 1)
    sys.stderr = open(stderr, 'a', 1)

def daemonize_func(func, stdout, stderr, working_dir, *args, **kwargs):
    """
    executa uma função como um daemon
    """
    create_daemon(stdout, stderr, working_dir)
    func(*args, **kwargs)

class daemonize(object):
    """
    decorator para executar uma função como daemon
    """

    def __init__(self, stdout='/dev/null/', stderr='/dev/null/',
                 working_dir='.'):

        # stdout e stderr são os lugares para onde serão redirecionados
        # sys.stdout e sys.stderr
        self.stdout = stdout
        self.stderr = stderr
        # working_dir é o diretório onde o daemon trabalhará
        self.working_dir = working_dir

    def __call__(self, func):
        def decorated_function(*args, **kwargs):
            daemonize_func(func, self.stdout, self.stderr, self.working_dir,
                      *args, **kwargs)
        return decorated_function

E agora você pode usar esse código assim:

#-*- coding: utf-8 -*-

import time
from daemonize import daemonize

@daemonize(stdout='log_b.txt', stderr='log_b.txt')
def b():
    print('comecando')
    time.sleep(10)
    print('fim')

if __name__ == '__main__':
    b()

Ou assim:

#-*- coding: utf-8 -*-

import time
from daemonize import daemonize_func

def a():
    print(time.time())
    time.sleep(50)
    print(time.time())

if __name__ == '__main__':
    daemonize_func(a, stdout='log_a.txt', stderr='log_a.txt', working_dir='.')

É isso, pessoal. Valeu. :) [

1] http://www.takeourword.com/TOW146/page4.html 

[2] http://pubs.opengroup.org/onlinepubs/007904975/basedefs/xbd_chap11.html#tag_11_01_03

Comentários

Faça seu comentário