daemon.py


#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2014-2015 clowwindy
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import absolute_import, division, print_function, \
    with_statement

import os
import sys
import logging
import signal
import time
from shadowsocks import common, shell

# this module is ported from ShadowVPN daemon.c

def daemon_exec(config):#->start->write_pid_file
    if 'daemon' in config:
        if os.name != 'posix':
            raise Exception('daemon mode is only supported on Unix')
        command = config['daemon'] #-d后的start
        if not command:
            command = 'start'
        pid_file = config['pid-file']
        log_file = config['log-file']
        if command == 'start':
            daemon_start(pid_file, log_file)
        elif command == 'stop':
            daemon_stop(pid_file)
            # always exit after daemon_stop
            sys.exit(0)
        elif command == 'restart':
            daemon_stop(pid_file)
            daemon_start(pid_file, log_file)
        else:
            raise Exception('unsupported daemon command %s' % command)#start或者stop或者restart
        #下面两个是shell.py里默认的赋值
        #  config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid')
        #config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log')
        #PID 文件(进程标识文件)通常是一个包含着运行中进程的进程ID(PID)的文件。在很多情况下,当一个进程启动时,它会把自己的PID写入一个特定的文件中,这个文件通常被放置在预定义的位置上,比如 /var/run/ 目录下

def write_pid_file(pid_file, pid):#打开文件 修改描述符 加锁 并进行异常处理
    import fcntl
    import stat

    try:
        fd = os.open(pid_file, os.O_RDWR | os.O_CREAT,
                     stat.S_IRUSR | stat.S_IWUSR)#os.O_RDWR 表示以读写模式打开文件,允许读取和写入文件内容。os.O_CREAT 表示如果文件不存在,则创建文件。这个标志告诉操作系统,如果指定的文件不存在,则尝试创建它。stat.S_IRUSR | stat.S_IWUSR 是用于设置文件权限的参数。这里的 stat.S_IRUSR 表示用户(文件所有者)具有读取权限,stat.S_IWUSR 表示用户具有写入权限。
    except OSError as e:
        shell.print_exception(e)
        return -1
    flags = fcntl.fcntl(fd, fcntl.F_GETFD)#获取文件描述符 fd 的标志位。
    assert flags != -1
    flags |= fcntl.FD_CLOEXEC # 将 flags 变量中的标志位与 FD_CLOEXEC 进行按位或操作,这会将 FD_CLOEXEC 标志位添加到 flags 中,即设置了文件描述符在执行 exec 调用时自动关闭。
    r = fcntl.fcntl(fd, fcntl.F_SETFD, flags) # flags 变量中存储的标志位设置到文件描述符 fd 上 fcntl.F_SETFD 是一个常量,表示对文件描述符标志位的设置操作
    assert r != -1 #失败则r=-1
    # There is no platform independent way to implement fcntl(fd, F_SETLK, &fl)#设置锁
    # via fcntl.fcntl. So use lockf instead
    try:
        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET) #fd 是文件描述符,表示对哪个文件进行加锁操作。
        #fcntl.LOCK_EX | fcntl.LOCK_NB 是锁定的类型,LOCK_EX 表示独占锁(排他锁),LOCK_NB 表示非阻塞模式,如果无法获得锁则立即返回而不是等待。 没获得则抛出异常
        #0 和 0 是指锁定的起始位置和长度。在这种情况下,它们被设置为0,表示从文件的开始位置锁定整个文件。
        #os.SEEK_SET 表示设置锁的起始位置是相对于文件开头的位置。
    except IOError:
        r = os.read(fd, 32)
        if r:
            logging.error('already started at pid %s' % common.to_str(r))
        else:
            logging.error('already started')
        os.close(fd)
        return -1
    os.ftruncate(fd, 0)#从0截断
    os.write(fd, common.to_bytes(str(pid))) #清空重写
    return 0

def freopen(f, mode, stream):#文件描述符重新指向
    oldf = open(f, mode) #a是追加
    oldfd = oldf.fileno() #文件描述符
    newfd = stream.fileno()
    os.close(newfd)
    os.dup2(oldfd, newfd) #它会将 oldf 的文件描述符复制到 stream 的文件描述符,这样就实现了对 stream 的文件描述符重定向到 oldf 所代表的文件或者 I/O 资源。

def daemon_start(pid_file, log_file):#分支出来 描述符上锁 忽视终端的信息

    def handle_exit(signum, _):
        if signum == signal.SIGTERM:
            sys.exit(0)
        sys.exit(1)

    signal.signal(signal.SIGINT, handle_exit)#使用 signal.signal() 给 SIGINT 和 SIGTERM 信号注册了相应的处理函数。
    signal.signal(signal.SIGTERM, handle_exit) #SIGTERM 允许程序在收到信号后执行清理和终止前的操作。同样,程序可以捕获这个信号,并根据需要进行处理,然后优雅地退出

    # fork only once because we are sure parent will exit
    pid = os.fork()
    #os.fork() 是一个在 Unix 系统下创建新进程的系统调用。它会创建一个与调用进程(父进程)几乎完全相同的新进程(子进程),并在两个进程中都返回。在父进程中,os.fork() 返回子进程的PID(进程ID),而在子进程中返回 0。
    assert pid != -1

    if pid > 0:
        # parent waits for its child 等待的时间可能是为了确保子进程已经完全启动
        time.sleep(5)
        sys.exit(0)

    # child signals its parent to exit
    ppid = os.getppid()
    pid = os.getpid()#更新为自己的pid
    if write_pid_file(pid_file, pid) != 0: #应该只有子进程 在过程中会清空pidfile,加锁并完成覆写 在这之前pid没写,所以父进程没啥事
        os.kill(ppid, signal.SIGINT)#关闭父进程
        sys.exit(1)#关闭自己
    #将自己的PID写入到PID文件中,如果写入失败,子进程会向父进程发送 SIGINT 信号,然后退出。

    os.setsid() #调用 os.setsid() 创建一个新的会话,脱离父进程的控制,成为新的进程组的组长。
    signal.signal(signal.SIGHUP, signal.SIG_IGN) #signal.SIG_IGN 将 SIGHUP 信号的处理方式设置为忽略,即使收到 SIGHUP 信号,进程也会无视它,不做任何响应。 防止终端影响

    print('started')
    os.kill(ppid, signal.SIGTERM)

    sys.stdin.close() #子进程关闭标准输入,
    try:
        freopen(log_file, 'a', sys.stdout)#重定向
        freopen(log_file, 'a', sys.stderr)
    except IOError as e:
        shell.print_exception(e)
        sys.exit(1)

def daemon_stop(pid_file):#新的进程杀了老的进程
    import errno
    try:
        with open(pid_file) as f:
            buf = f.read()
            pid = common.to_str(buf)
            if not buf:
                logging.error('not running')
    except IOError as e:
        shell.print_exception(e)
        if e.errno == errno.ENOENT:#文件或目录不存在”
            # always exit 0 if we are sure daemon is not running pid没有或者目录没有
            logging.error('not running')
            return
        sys.exit(1)
    pid = int(pid)
    if pid > 0:
        try:
            os.kill(pid, signal.SIGTERM)
        except OSError as e:
            if e.errno == errno.ESRCH: #ESRCH 表示没有这样的进程。
                logging.error('not running')
                # always exit 0 if we are sure daemon is not running
                return
            shell.print_exception(e)
            sys.exit(1)
    else:
        logging.error('pid is not positive: %d', pid)

    # sleep for maximum 10s
    for i in range(0, 200):
        try:
            # query for the pid
            os.kill(pid, 0)#非阻塞 就是发信号
        except OSError as e:
            if e.errno == errno.ESRCH:#已经杀了
                break
        time.sleep(0.05)
    else:
        logging.error('timed out when stopping pid %d', pid)
        sys.exit(1)
    print('stopped')
    os.unlink(pid_file) #是要删除的文件路径或文件名。

#这个py文件只有这里一次 没有嵌套调用
def set_user(username): #管理员需要
    if username is None:
        return

    import pwd
    import grp

    try:
        pwrec = pwd.getpwnam(username) #如果找到了匹配的用户记录,pwrec 将会是一个包含有关该用户的信息的数据结构,其中可能包括用户名、用户ID、组ID、home目录等信息。
    except KeyError:
        logging.error('user not found: %s' % username)
        raise
    user = pwrec[0]
    uid = pwrec[2]
    gid = pwrec[3]#用户名 用户标识 组标识

    cur_uid = os.getuid() #当前的
    if uid == cur_uid:
        return
    if cur_uid != 0:
        logging.error('can not set user as nonroot user')
        # will raise later

    # inspired by supervisor
    if hasattr(os, 'setgroups'):
        groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] #常是组的数字标识(Group ID)。因此,这段代码创建了一个列表,其中包含了用户所属的所有附加组的组ID。
        groups.insert(0, gid)
        os.setgroups(groups) #将程序的附加组设置为这个列表中包含的组
    os.setgid(gid)
    os.setuid(uid) # 是一个用于在 Unix/Linux 系统中切换当前进程的有效用户ID


发表评论