带有超时设置的subprocess

背景

在使用subprocess模块时,总是遇到调用调用系统命令后,子进程执行完后没有退出,造成卡死在系统中。虽然遇到的几率不是很频繁,但是对于频繁调用的定时任务来说,会产生很多卡死的进程(实际中遇到过很多子进程卡死,导致系统资源耗尽,宕机)。为解决这个问题,重新对subprocess封装,加入对调用命令执行超时的处理。

程序说明

重新封装SubWork类

对subprocess重新封装,定义一些默认属性。

class SubWork(Object):    
def __init__(self):                                                                                                                                           """
    Default None                                                           
    """                                                                   
    self._Popen = None                                                     
    self._pid = None           #子进程PID                                                   
    self._return_code = None   #执行命令返回值                                           
    self._cwd = None           #执行目录                                           
    self._start_time = None    #子进程开始执行的时间戳

调用命令的内部方法

这里定义了一个内部方法,用于使用subprocess调用系统命令,并做超时处理。

  1. subprocess接收命令格式为一个list,所以使用shlex.split()进行命令切分。此方法接收4个参数:命令,标准输出,标准错误和执行目录。
  2. 超时处理,在调用subprocess.Popen()方法后,先获取当前时间戳,然后循环判断,是否程序正常退出(poll()方法获取子进程状态)并且是在超时时间之内,这里用这个循环来代替wait()方法来阻塞进程,并判断超时。如果超过超时时间,方法继续执行,获取进程执行返回的状态(此时可能子进程并未执行完毕),再次判断子进程是否退出,如在此处还未退出,执行terminate()方法,给子进程发送信号,退出子进程,等待1秒,再次判断进程是否退出,如还未退出证明进程未正常相应terminate()发出的信号,此时使用kill()方法,发出SIGKILL信号,强制退出子进程。
  3. 最终获取子进程退出的状态。

代码如下:

def _run(self):
    #Run cmd.
    #Split command string.
    cmd = shlex.split(self._cmd)
    self._Popen = subprocess.Popen(args=cmd,
                                   stdout=self._stdout_fd,
                                   stderr=self._stderr_fd,
                                   cwd=self._cwd)
    self._pid = self._Popen.pid
    self._start_time = time.time()

    while (self._Popen.poll() == None and
            (time.time() - self._start_time) < self._timeout):
        time.sleep(1)

    _r_code = self._Popen.poll()

    # If child process has not exited yet, terminate it.
    if self._Popen.poll() == None:
        self._Popen.terminate()
        _r_code = 254

    # Wait for the child process to exit.
    time.sleep(1)

    # If child process has not been terminated yet, kill it.
    if self._Popen.poll() == None:
        self._Popen.kill()
        _r_code = 255

    self._return_code = _r_code

对外的方法

外部调用的方法start(),使用此方法进行执行命令的调用。接收命令,超时时间,标准输入,标准输出,标准错误,是否使用tty(是否将结果输出到终端),是否使用时间戳(返回结果中打印开始的时间)。此方法主要是调用内部执行命令的方法(_run()),然后对输出进行格式化。

def start(self,
          cmd,
          timeout=5*60*60,
          stdin=None,
          stdout=None,
          stderr=None,
          tty=False,
          timestamp=False):

    self._cmd = cmd
    self._stdin = stdin
    self._stdout = stdout
    self._stderr = stderr
    self._timeout = timeout
    self._is_tty = tty
    self._timestamp = timestamp

    #Init output.
    info = None
    err = None

    if self._timestamp:
        start_time = time.strftime("%Y-%m-%d %X", time.localtime())
        file_start = "Start Time: " + start_time + "\n"
    else:
        file_start = ""
        file_end = ""

    try:
        #Init the file handle of output.
        if self._is_tty:
            self._stdout_fd = None
            self._stderr_fd = None

        elif (self._stdout is None or
            self._stderr is None or
            self._stdout == self._stderr):

            self._stdout_fd = tempfile.TemporaryFile()
            self._stderr_fd = tempfile.TemporaryFile()

        else:
            self._stdout_fd = self._create_handler(self._stdout)
            self._stderr_fd = self._create_handler(self._stderr)
            self._stdout_fd.write(file_start)
            self._stdout_fd.flush()
            self._stderr_fd.write(file_start)
            self._stderr_fd.flush()

        self._run()

        if self._timestamp:
            end_time = time.strftime("%Y-%m-%d %X", time.localtime())
            file_end = "End Time: " + end_time + "\n"

        #Write and Read output content.
        if not self._is_tty:
            self._stdout_fd.write(file_end)
            self._stderr_fd.write(file_end)

            self._stdout_fd.flush()
            self._stderr_fd.flush()

            self._stdout_fd.seek(0)
            self._stderr_fd.seek(0)
            info = file_start + self._stdout_fd.read() + file_end
            err = file_start + self._stderr_fd.read() + file_end
    finally:
        #Close file handle.
        if not self._is_tty:
            self._stdout_fd.close()
            self._stderr_fd.close()

    return {"code":self._return_code,
            "stdout":info,
            "stderr":err
            }

这里输出分为3种类型:

  1. 直接输出到终端,设置tty=True即可,就是直接打印到终端。
  2. 输出到临时文件中,如果tty=False,并且没有输入指定的log文件(stdout和stderr),某块会自动创建一个临时文件来记录日志。
  3. 输出到指定日志中,设置了输出日志,会将命令执行的标准输出和标准错误输出到指定日志文件中(实时输出)。

*注意: 如果设置使用输出到tty,最终start()方法只会返回命令执行的状态码,不会返回执行结果(已经输出到终端),使用临时文件作为日志或指定输出日志文件,最终start()方法会最终将日志返回给调用的程序。

对日志处理的方法

创建日志文件,初始化日志句柄。

#Create file handle.
def _create_handler(self, filename):
    if isinstance(filename, file):
        return filename
    elif isinstance(filename, basestring):
        path = os.path.dirname(filename)
        timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())

        if not os.path.exists(path):
            os.makedirs(path)
        elif os.path.exists(filename) and not os.path.isfile(filename):
            backup_name = filename + timestamp
            os.rename(filename, backup_name)

        fd = open(filename, 'a+b')
        return fd
    else:
        raise "The type of \'filename\' must be \'file\' or \'basestring\'"

使用方法

SubWork使用方法如下:

import SubWork

cmd = "/bin/ls /tmp"
worker = SubWork()
res = worker.start(cmd, 300, "/tmp/stdout.log", "/tmp/stderr.log")
print res

这里会执行/bin/ls /tmp命令,超时时间300秒,将执行的标准输出实时输出到/tmp/stdout.log中,将输出的标准错误实时输出到/tmp/stderr.log 中,最终res变量中会有命令执行的状态码和命令执行的输出结果。