带有超时设置的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调用系统命令,并做超时处理。
- subprocess接收命令格式为一个list,所以使用shlex.split()进行命令切分。此方法接收4个参数:命令,标准输出,标准错误和执行目录。
- 超时处理,在调用
subprocess.Popen()
方法后,先获取当前时间戳,然后循环判断,是否程序正常退出(poll()
方法获取子进程状态)并且是在超时时间之内,这里用这个循环来代替wait()
方法来阻塞进程,并判断超时。如果超过超时时间,方法继续执行,获取进程执行返回的状态(此时可能子进程并未执行完毕),再次判断子进程是否退出,如在此处还未退出,执行terminate()
方法,给子进程发送信号,退出子进程,等待1秒,再次判断进程是否退出,如还未退出证明进程未正常相应terminate()
发出的信号,此时使用kill()
方法,发出SIGKILL
信号,强制退出子进程。 - 最终获取子进程退出的状态。
代码如下:
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种类型:
- 直接输出到终端,设置tty=True即可,就是直接打印到终端。
- 输出到临时文件中,如果tty=False,并且没有输入指定的log文件(stdout和stderr),某块会自动创建一个临时文件来记录日志。
- 输出到指定日志中,设置了输出日志,会将命令执行的标准输出和标准错误输出到指定日志文件中(实时输出)。
*注意: 如果设置使用输出到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
变量中会有命令执行的状态码和命令执行的输出结果。