Commits

Can Xue committed fd7d837

daemonkit

Comments (0)

Files changed (1)

kahgean/daemonkit.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2012 Xue Can <xuecan@gmail.com>
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license
+
+u"""守护程序工具包
+
+这个模块提供了编写守护程序所需的工具。包括:
+
+  - daemonize: 检查并创建 pid 文件,将当前进程转变成在后台运行的守护进程
+  - change_rlimit_nofile: 修改一个进程可同时打开的文件描述符数量
+  - change_group: 修改用户组
+  - change_user: 修改用户
+
+这个模块仅适用于类 Unix 的操作系统,在 Windows 下不发生作用,但并不抛出错误。
+"""
+
+import os
+import sys
+import signal
+import errno
+import logging
+
+try:
+    import resource, grp, pwd
+except ImportError:
+    resource = grp = pwd = None
+
+
+__all__ = ['MAXFD', 'DEVNULL', 'daemonize',
+           'change_group', 'change_user', 'change_rlimit_nofile']
+
+
+# 默认情况下一个进程最大允许打开的文件描述符是 1024 个
+MAXFD = 1024
+
+# 默认的将会把标准输入输出重定向到 /dev/null
+DEVNULL = os.devnull if hasattr(os, "devnull") else "/dev/null"
+
+
+def _daemonize(umask, stdout, stderr):
+    # 将当前进程从控制台中剥离出来,作为守护进程在后台运行。
+    #
+    # 根据 Chad J. Schroeder 发布于 http://code.activestate.com/recipes/278731/
+    # 的脚本改写。
+    #
+    # Copyright (C) 2012 Xue Can <xuecan@gmail.com>
+    # Copyright (C) 2005 Chad J. Schroeder
+    try:
+        # Fork 第一个子进程,这样父进程就可以退出了。这个操作用于交出对
+        # 终端的控制权。这个操作还保证了子进程不会成为进程组的 leader,
+        # 因为这个子进程拥有新的进程 ID 并继承了父进程的进程组 ID。这个
+        # 步骤对于保证后续的 os.setsid() 得以成功调用来说是必需的。
+        pid = os.fork()
+    except OSError, e:
+        raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+    if pid > 0:
+        # 结束最初的进程
+        #
+        # exit() 还是 _exit()?
+        # _exit() 和 exit() 类似,但是并不调用 atexit 以及 on_exit 注册的函数
+        # 以及信号处理器。它还关闭所有开启的文件描述符。使用 exit() 将会造成
+        # 所有 stdio 流 flush 两次并可能意外删除临时文件。因此建议 fork() 子
+        # 进程成为守护程序时父进程使用 _exit() 退出。
+        os._exit(0)
+        
+    # 现在我们在第一个子进程中 :-)
+    #
+    # 调用 os.setsid() 用于成为新的 session 和进程组的 leader。这个步骤
+    # 再次确保没有控制终端。
+    os.setsid()
+
+    # 忽略 SIGHUP 信号是必需的吗?
+    #
+    # It's often suggested that the SIGHUP signal should be ignored before
+    # the second fork to avoid premature termination of the process.  The
+    # reason is that when the first child terminates, all processes, e.g.
+    # the second child, in the orphaned group will be sent a SIGHUP.
+    #
+    # "However, as part of the session management system, there are exactly
+    # two cases where SIGHUP is sent on the death of a process:
+    #
+    #   1) When the process that dies is the session leader of a session that
+    #      is attached to a terminal device, SIGHUP is sent to all processes
+    #      in the foreground process group of that terminal device.
+    #   2) When the death of a process causes a process group to become
+    #      orphaned, and one or more processes in the orphaned group are
+    #      stopped, then SIGHUP and SIGCONT are sent to all members of the
+    #      orphaned group." [2]
+    #
+    # The first case can be ignored since the child is guaranteed not to have
+    # a controlling terminal.  The second case isn't so easy to dismiss.
+    # The process group is orphaned when the first child terminates and
+    # POSIX.1 requires that every STOPPED process in an orphaned process
+    # group be sent a SIGHUP signal followed by a SIGCONT signal.  Since the
+    # second child is not STOPPED though, we can safely forego ignoring the
+    # SIGHUP signal.  In any case, there are no ill-effects if it is ignored.
+    #
+    # 总之结论是:忽略 SIGHUP 信号并没有任何副作用。
+    signal.signal(signal.SIGHUP, signal.SIG_IGN)
+
+    try:
+        # Fork 第二个子进程并立即退出第一个子进程以避免僵尸进程。这让第二个
+        # 子进程成为孤儿,让 init 进程负责对该进程进行清理工作。此外,由于
+        # 第一个子进程是一个没有可控制终端的 session leader,后续还是可能
+        # 从打开的终端中获得该进程的(基于 SysV 的系统)。第二次 fork 确保了
+        # 这个进程不再是 session leader,从而杜绝了从终端获得该进程的可能。
+        pid = os.fork()
+    except OSError, e:
+        raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+    if pid > 0:
+        # 结束第一个子进程
+        os._exit(0)	     
+
+    # 终于到了第二个子进程,也就是守护进程中了 :-)
+
+    # 我们可能不希望继承父进程的 umask,因此可以在这里设置
+    os.umask(umask)
+
+    # 关闭所有的文件描述符。用于避免子进程继续保有从父进程继承而来的文件描述符。
+    # 有几种不同的方法实现这一目的。
+    #
+    # 尝试获得系统配置变量 SC_OPEN_MAX 来获得开启的文件描述符的最大值。如果不
+    # 存在,则使用可配置的默认值。
+    #
+    # try:
+    #     maxfd = os.sysconf("SC_OPEN_MAX")
+    # except (AttributeError, ValueError):
+    #     maxfd = MAXFD
+    #
+    # 或者
+    #
+    # if (os.sysconf_names.has_key("SC_OPEN_MAX")):
+    #     maxfd = os.sysconf("SC_OPEN_MAX")
+    # else:
+    #     maxfd = MAXFD
+    #
+    # 或者
+    #
+    # 使用 getrlimit() 来获取一个进程可打开的最大的文件描述符。如果没有限制
+    # 则使用默认值。
+    #
+    import resource
+    maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+    if (maxfd == resource.RLIM_INFINITY):
+       maxfd = MAXFD
+   
+    # 遍历所有文件描述符并关闭,如果失败(因为并未打开)简单忽略即可。
+    for fd in range(0, maxfd):
+       try:
+          os.close(fd)
+       except OSError:
+          pass
+ 
+    # 重定向标准输入/输出文件描述符到特殊的文件。由于守护程序不再拥有可控制
+    # 的终端,许多守护程序重定向 stdin、stdout 和 stderr 到 /dev/null。这样
+    # 可以避免读写标准输入/输出时产生的副作用。
+ 
+    # 这个 open() 调用可以确保获得最小的文件描述符(0),这是因为之前已经关闭
+    # 了所有的文件描述符。
+    os.open(DEVNULL, os.O_RDWR)	        # stdin  (0)
+ 
+    # 将 stdin 复制到 stdout 和 stderr
+    if stdout == DEVNULL:
+        os.dup2(0, 1)                   # stdout (1)
+    else:
+        os.open(stdout, os.O_CREAT|os.O_APPEND|os.O_RDWR)
+    if stderr == DEVNULL:
+        os.dup2(0, 2)                   # stderr (2)
+    else:
+        os.open(stderr, os.O_CREAT|os.O_APPEND|os.O_RDWR)
+    
+    # 上面的代码有些“魔术”。为什么这么设置一下 stdin,stdout 和 stderr 就自动
+    # 指向 /dev/null 或者指定的文件了呢?首先,之前我们清空了从 0 到 maxfd
+    # 之间所有文件描述符。这并非必须,但是被认为是最佳实践。随后执行的 open()
+    # 操作确保返回的是最小的文件描述符 0。而内部的 stdin、stdout 和 stderr 对应
+    # 的文件描述符恰恰是 0, 1, 2,这就可以理解上述如此简洁的代码为什么是有效
+    # 的了。
+    #
+    # 详情请参考 http://code.activestate.com/recipes/278731/ 下原作者的答疑。
+    #
+    # 当然,如果之前没有进行关闭所有文件描述符的操作,这就不能工作了。一个
+    # 可行的替代方案如下:
+    #
+    # class NullDevice:
+    #     def write(self, s):
+    #         pass
+    # sys.stdin.close()
+    # sys.stdout = NullDevice()
+    # sys.stderr = NullDevice()
+
+
+def _check_pidfile(pidfile):
+    """检查 pid 文件"""
+    if not pidfile: return
+    if not os.path.exists(pidfile): return
+    try:
+        pid = int(open(pidfile).read())
+    except ValueError:
+        message = u'PID 文件 %s 中包含的信息不是进程号。' % pidfile
+        logging.getLogger().error(message)
+        sys.exit(message)
+    try:
+        # kill -0 用于检查进程是否存在
+        os.kill(pid, 0)
+    except OSError, e:
+        if e.args[0] == errno.ESRCH:
+            # PID 不存在
+            logging.getLogger().info(u'删除过期的 PID 文件。')
+            os.remove(pidfile)
+        else:
+            message = (u'无法根据 PID 文件 %s 中 PID %d 检查进程状态,'
+                       u'失败原因是:%s' % (pidfile, pid, e.args[1]))
+            logging.getLogger().error(message)
+            sys.exit(message)
+    else:
+        message = u"另一个进程正在运行中,PID 为:%d\n" % pid
+        logging.getLogger().error(message)
+        sys.exit(message)
+
+
+def _write_pidfile(pidfile):
+    u"""将当前进程号写入指定的 PID 文件"""
+    if not pidfile: return
+    with open(pidfile, 'wb') as f:
+        f.write(str(os.getpid()))
+    if not os.path.exists(pidfile):
+        raise Exception(u"PID 文件 %s 丢失。" % pidfile)
+
+
+def _remove_pidfile(pidfile):
+    u"""删除 PID 文件并退出程序"""
+    if pidfile and os.path.exists(pidfile):
+        os.remove(pidfile)
+    sys.exit(0)
+
+
+def daemonize(pidfile, umask=0o000, stdout=DEVNULL, stderr=DEVNULL):
+    u"""让程序成为守护进程
+    
+    首先将会检查 PID 文件判断是否有服务进程正在运行。若没有,则将当前进程
+    从控制台中剥离出来,作为守护进程在后台运行。最后,将守护进程的进程号
+    写入 PID 文件。
+    
+    这个函数还会注册 SIGTERM 信号处理器,一旦收到 kill TERM 信号,将会
+    删除 PID 文件并退出系统。
+    
+    若参数 pidfile 为 None,则不会进行和 PID 文件有关的操作。
+    """
+    if not resource: return
+    _check_pidfile(pidfile)
+    _daemonize(umask, stdout, stderr)
+    _write_pidfile(pidfile)
+    signal.signal(signal.SIGTERM, lambda s, f: _remove_pidfile(pidfile))
+
+
+def change_group(groupname):
+    u"""修改用户组"""
+    if not grp: return
+    os.setgid(grp.getgrnam(groupname).gr_gid)
+
+
+def change_user(username):
+    u"""修改用户"""
+    if not pwd: return
+    os.setgid(pwd.getpwnam(username).pw_uid)
+
+
+def change_rlimit_nofile(limit):
+    u"""修改进程同时能打开的最大文件描述符数量"""
+    if not resource: return
+    resource.setrlimit(resource.RLIMIT_NOFILE, (limit, limit))