跳转到主要内容

一个用于创建超级炫酷Unix守护进程的Python库

项目描述

daemonocle是一个Python库,用于编写自己的Unix风格的守护进程。它解决了其他守护进程库中的许多问题,并提供了一些在其他守护进程中很少见到的非常有用的功能。

安装

通过pip安装

pip install daemonocle

或下载源代码并手动安装

git clone https://github.com/jnrbsn/daemonocle.git
cd daemonocle/
python setup.py install

基本用法

这里有一个 非常非常 基本示例

import sys
import time

import daemonocle

# This is your daemon. It sleeps, and then sleeps again.
def main():
    while True:
        time.sleep(10)

if __name__ == '__main__':
    daemon = daemonocle.Daemon(
        worker=main,
        pidfile='/var/run/daemonocle_example.pid',
    )
    daemon.do_action(sys.argv[1])

这里有一个带有日志和 关闭回调 的相同示例

import logging
import sys
import time

import daemonocle

def cb_shutdown(message, code):
    logging.info('Daemon is stopping')
    logging.debug(message)

def main():
    logging.basicConfig(
        filename='/var/log/daemonocle_example.log',
        level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s',
    )
    logging.info('Daemon is starting')
    while True:
        logging.debug('Still running')
        time.sleep(10)

if __name__ == '__main__':
    daemon = daemonocle.Daemon(
        worker=main,
        shutdown_callback=cb_shutdown,
        pidfile='/var/run/daemonocle_example.pid',
    )
    daemon.do_action(sys.argv[1])

运行时的样子如下

user@host:~$ python example.py start
Starting example.py ... OK
user@host:~$ python example.py status
example.py -- pid: 1234, status: running, uptime: 1m, %cpu: 0.0, %mem: 0.0
user@host:~$ python example.py stop
Stopping example.py ... OK
user@host:~$ cat /var/log/daemonocle_example.log
2014-05-04 12:39:21,090 [INFO] Daemon is starting
2014-05-04 12:39:21,091 [DEBUG] Still running
2014-05-04 12:39:31,091 [DEBUG] Still running
2014-05-04 12:39:41,091 [DEBUG] Still running
2014-05-04 12:39:51,093 [DEBUG] Still running
2014-05-04 12:40:01,094 [DEBUG] Still running
2014-05-04 12:40:07,113 [INFO] Daemon is stopping
2014-05-04 12:40:07,114 [DEBUG] Terminated by SIGTERM (15)

有关更多详细信息,请参阅下面的 详细用法 部分。

原因

如果你仔细想想,很多Unix守护进程其实根本不知道自己在做什么。你是否曾经遇到过类似这种情况?

user@host:~$ sudo example start
starting example ... ok
user@host:~$ ps aux | grep example
user      1234  0.0  0.0   1234  1234 pts/1    S+   12:34   0:00 grep example
user@host:~$ sudo example start
starting example ... ok
user@host:~$ echo $?
0
user@host:~$ tail -f /var/log/example.log
...

或者类似这种情况?

user@host:~$ sudo example stop
stopping example ... ok
user@host:~$ ps aux | grep example
user       123  0.0  0.0   1234  1234 ?        Ss   00:00   0:00 /usr/local/bin/example
user      1234  0.0  0.0   1234  1234 pts/1    S+   12:34   0:00 grep example
user@host:~$ sudo example stop
stopping example ... ok
user@host:~$ ps aux | grep example
user       123  0.0  0.0   1234  1234 ?        Ss   00:00   0:00 /usr/local/bin/example
user      1240  0.0  0.0   1234  1234 pts/1    S+   12:34   0:00 grep example
user@host:~$ sudo kill -9 123
...

或者类似这种情况?

user@host:~$ sudo example status
Usage: example {start|stop|restart}
user@host:~$ ps aux | grep example
...

这些都是一些常见问题的例子。其实不必如此。

注意:你可能正在想,“为什么不写一个更智能的启动/停止shell脚本包装器,检查守护进程是否真的启动了,真的停止了等等?” 真的吗? 其实不必如此。 我认为守护进程应该更有自我意识。它们应该大部分时间自己处理问题,你的启动/停止脚本应该只围绕你的守护进程是一个非常薄的包装器,或者简单地指向你的守护进程的符号链接。

问题

如果你曾经深入研究过守护进程化工作的细节,你可能会熟悉W. Richard Stevens在其著作《UNIX环境高级编程》中首次提出的标准“双fork”范式。这种标准实现方式的问题之一是,如果最终子进程在开始实际工作时就立即死亡,那么最初父进程(即控制你的终端的那个进程)早已消失。所以你所知道的就是进程被fork了,但你不知道它是否真正持续运行了超过一秒钟。而且,说实话,守护进程死亡最可能的时间是在启动后(由于配置错误、权限等)。

上面提到的下一个问题是,当你尝试停止守护进程时,它实际上并没有停止,但你不知道它实际上没有停止。这种情况发生在进程没有正确响应SIGTERM信号时。这种情况比应有的情况更常见。问题不一定是它没有停止。问题是你没有意识到它没有停止。启动/停止脚本知道它成功发送了信号,因此它假设成功了。这也成为了一个问题,因为当你的restart命令盲目地调用stop然后start时,因为它会在前一个守护进程退出之前尝试启动一个新的守护进程实例。

在我看来,这些是大多数守护进程最大的问题。daemonocle解决了这些问题,并提供了许多其他“花哨”的功能。

解决方案

守护进程在启动后立即死亡且你不知道的问题是通过第一个子进程(最终子进程的直接父进程)暂停一秒钟,然后调用os.waitpid(pid, os.WNOHANG)来检查进程是否仍在运行来解决的。这就是daemonocle所做的工作。所以如果你的守护进程在启动后一秒内死亡,你就会知道。

守护进程没有停止且你不知道的问题是通过简单地等待进程完成(带有超时)来解决的。这也是daemonocle所做的工作。(注意:当发生超时时,它不会尝试发送SIGKILL。这并不总是你想要的,而且通常不是一个好主意。)

其他有用功能

以下是daemonocle提供的一些其他有用的功能,你可能在其他地方找不到。

状态 动作

有一个status操作,不仅显示守护进程是否在运行及其PID,而且还显示守护进程的运行时间和同一进程组中所有进程的%CPU和%内存使用率(这些进程可能是其子进程)。所以如果你有一个启动多个工作进程的守护进程,status操作将显示所有工作进程的总%CPU和%内存使用率。

它可能看起来像这样

user@host:~$ python example.py status
example.py -- pid: 1234, status: running, uptime: 12d 3h 4m, %cpu: 12.4, %mem: 4.5

稍微智能一点的 重启 动作

您是否曾经尝试重启一个守护进程,但发现自己实际上并没有运行?让我猜猜:它只是给出错误信息而没有启动守护进程。很多时候这并不是问题,但如果您正在尝试以自动化的方式重启守护进程,那么检查它是否正在运行并相应地进行启动重启就变得很烦人。使用daemonocle,如果您尝试重启一个未运行的守护进程,它将给出警告说它没有运行,然后启动守护进程。这通常是人们所期望的。

自我重新加载

使用daemonocle的守护进程具有通过简单地调用daemon.reload()来自我重新加载的能力,其中daemon是您的daemonocle.Daemon实例。当前守护进程在daemon.reload()被调用的地方停止执行,并启动一个新的守护进程来替换当前的守护进程。从您的代码角度来看,这几乎和重启一样,只不过它是从守护进程内部启动的,并且没有涉及信号处理。以下是一个基本的守护进程示例,它监视配置文件,并在配置文件更改时自行重新加载:

import os
import sys
import time

import daemonocle

class FileWatcher(object):

    def __init__(self, filename, daemon):
        self._filename = filename
        self._daemon = daemon
        self._file_mtime = os.stat(self._filename).st_mtime

    def file_has_changed(self):
        current_mtime = os.stat(self._filename).st_mtime
        if current_mtime != self._file_mtime:
            self._file_mtime = current_mtime
            return True
        return False

    def watch(self):
        while True:
            if self.file_has_changed():
                self._daemon.reload()
            time.sleep(1)

if __name__ == '__main__':
    daemon = daemonocle.Daemon(pidfile='/var/run/daemonocle_example.pid')
    fw = FileWatcher(filename='/etc/daemonocle_example.conf', daemon=daemon)
    daemon.worker = fw.watch
    daemon.do_action(sys.argv[1])

关闭回调

您可能已经在上面的基本用法部分注意到定义了shutdown_callback。此函数会在守护进程以可捕获的方式关闭时被调用,这应该是在大多数情况下,除了SIGKILL或如果您的服务器意外崩溃或失去电源等情况。此函数可以用于执行守护进程需要的任何清理工作。此外,如果您想记录(到您选择的记录器)关闭的原因和预期的退出代码,可以使用传递给您的回调(您的回调必须接受这两个参数)的messagecode参数。

非分离模式

这本身并不特别有趣,但值得一提的是,在非分离模式下,您的守护进程将执行您配置的所有其他操作(即setuidsetgidchroot等),但实际上不会从您的终端分离。因此,在测试期间,您可以非常准确地了解您的守护进程在野外的行为。还有一点值得注意,自我重新加载在非分离模式下也有效,这最初有点难以理解。

文件描述符处理

守护进程通常执行的事情之一是关闭所有打开的文件描述符,并为STDINSTDOUTSTDERR建立新的描述符,这些描述符仅指向/dev/null。这大多数时候是可以的,但如果您的工作进程是类的一个实例方法,该实例方法在其__init__()方法中打开文件,那么如果您不小心,就会遇到问题。如果您导入一个留下打开文件的模块,这也会是一个问题。例如,在Python 3中导入random标准库模块会导致/dev/urandom的打开文件描述符。

由于守护进程的这个“功能”往往会产生比解决的问题更多的问题,并且它引起的问题有时会有奇怪的副作用,这使得调试非常困难,因此这个功能是可选的,在daemonocle中默认禁用,通过close_open_files选项。

详细用法

daemonocle.Daemon类是使用daemonocle创建守护进程的主要类。以下是类的构造函数签名:

class daemonocle.Daemon(
    worker=None, shutdown_callback=None, prog=None, pidfile=None, detach=True,
    uid=None, gid=None, workdir='/', chrootdir=None, umask=022, stop_timeout=10,
    close_open_files=False)

以下是所有参数的描述:

worker

执行您守护进程所有工作的函数。

shutdown_callback

该函数将在守护进程关闭时被调用。它应接受一个 message 和一个 code 参数。消息是一个可读的消息,解释了守护进程为何关闭。记录此消息可能很有用。代码是它打算退出的退出码。有关更多详细信息,请参阅 关闭回调

prog

用于输出消息的程序名称。默认值:os.path.basename(sys.argv[0])

pidfile

要使用的 PID 文件路径。不使用 PID 文件不是必需的,但如果您不这样做,您将无法使用您可能期望的所有功能。请确保运行守护进程的用户有权写入该文件的目录。

detach

是否从终端分离并进入后台。有关更多详细信息,请参阅 非分离模式。默认值:True

uid

守护进程启动时要切换到的用户 ID。默认情况下不切换用户。

gid

守护进程启动时要切换到的组 ID。默认情况下不切换组。

workdir

守护进程启动时要更改到的目录路径。请注意,如果进程的工作目录在该文件系统上,则无法卸载文件系统。因此,如果您更改了默认值,请务必小心更改。默认值:"/"

chrootdir

守护进程启动时要设置为有效根目录的目录路径。默认情况下不执行任何操作。

umask

进程的文件创建掩码(“umask”)。默认值:022

stop_timeout

在抛出错误之前等待守护进程停止的秒数。默认值:10

close_open_files

守护进程分离时是否关闭所有打开的文件。默认值:False

动作

默认操作是 startstoprestartstatus。您可以使用 daemonocle.Daemon.list_actions() 方法获取可用操作的列表。调用操作的建议方法是使用 daemonocle.Daemon.do_action(action) 方法。操作字符串名称与方法名称相同,只是下划线被短横线替换。

如果您想创建自己的操作,只需继承 daemonocle.Daemon 并将 @daemonocle.expose_action 装饰器添加到您的操作方法中,然后就可以了。

以下是一个示例

import daemonocle

class MyDaemon(daemonocle.Daemon):

    @daemonocle.expose_action
    def full_status(self):
        """Get more detailed status of the daemon."""
        pass

然后,如果您像上面所有示例中那样进行了基本的 daemon.do_action(sys.argv[1]) 操作,您可以使用类似 python example.py full-status 的命令调用您的操作。

与mitsuhiko的click集成

daemonocle 还提供了与 click 的集成,click 是“可组合的命令行工具”。集成形式为自定义命令类 daemonocle.cli.DaemonCLI,您可以使用它与 @click.command() 装饰器一起使用,以自动生成具有子命令的命令行界面,这些子命令适用于您所有的操作。它还自动将装饰函数转换为守护进程。装饰函数成为工作者,操作自动从 click 映射到 daemonocle。

以下是一个示例

import time

import click
from daemonocle.cli import DaemonCLI

@click.command(cls=DaemonCLI, daemon_params={'pidfile': '/var/run/example.pid'})
def main():
    """This is my awesome daemon. It pretends to do work in the background."""
    while True:
        time.sleep(10)

if __name__ == '__main__':
    main()

运行此示例可能看起来像这样

user@host:~$ python example.py --help
Usage: example.py [<options>] <command> [<args>]...

  This is my awesome daemon. It pretends to do work in the background.

Options:
  --help  Show this message and exit.

Commands:
  start    Start the daemon.
  stop     Stop the daemon.
  restart  Stop then start the daemon.
  status   Get the status of the daemon.
user@host:~$ python example.py start --help
Usage: example.py start [<options>]

  Start the daemon.

Options:
  --debug  Do NOT detach and run in the background.
  --help   Show this message and exit.

daemonocle.cli.DaemonCLI 类还接受一个 daemon_class 参数,该参数可以是 daemonocle.Daemon 的子类。它将使用您的自定义类,自动为任何您定义的自定义操作创建子命令,并使用操作方法的文档字符串作为帮助文本,就像 click 通常所做的那样。

此集成完全可选。daemonocle不强制进行任何类型的参数解析。如果你想使用,可以使用argparse、optparse或直接使用sys.argv

错误、请求、问题等。

请在GitHub上创建一个问题

支持