装饰\委托File对象以添加功能

Ada*_*tan 14 python logging subprocess design-patterns

我一直在编写一个小的Python脚本,它使用subprocess模块和辅助函数执行一些shell命令:

import subprocess as sp
def run(command, description):
    """Runs a command in a formatted manner. Returns its return code."""
    start=datetime.datetime.now()
    sys.stderr.write('%-65s' % description)
    s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE)
    out,err=s.communicate()
    end=datetime.datetime.now()
    duration=end-start
    status='Done' if s.returncode==0 else 'Failed'
    print '%s (%d seconds)' % (status, duration.seconds)
Run Code Online (Sandbox Code Playgroud)

以下行读取标准输出和错误:

    s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE)
    out,err=s.communicate()
Run Code Online (Sandbox Code Playgroud)

如您所见,未使用stdout和stderr.假设我想以格式化的方式将输出和错误消息写入日志文件,例如:

[STDOUT: 2011-01-17 14:53:55] <message>
[STDERR: 2011-01-17 14:53:56] <message>
Run Code Online (Sandbox Code Playgroud)

我的问题是,最恐怖的方式是什么?我想到了三个选择:

  1. 继承文件对象并覆盖该write方法.
  2. 使用实现的Delegate类write.
  3. PIPE某种方式连接到自身.

更新:参考测试脚本

我正在使用此脚本检查结果,保存为test.py:

#!/usr/bin/python
import sys

sys.stdout.write('OUT\n')
sys.stdout.flush()
sys.stderr.write('ERR\n')
sys.stderr.flush()
Run Code Online (Sandbox Code Playgroud)

有任何想法吗?

Len*_*bro 14

1和2是合理的解决方案,但重写write()是不够的.

问题是Popen需要文件句柄来附加到进程,因此Python文件对象不起作用,它们必须是操作系统级别.要解决这个问题,你必须拥有一个具有os级文件句柄的Python对象.我能想到解决的唯一方法是使用管道,所以你有一个os级文件句柄来写.但是接下来你需要另一个线程来坐管并轮询该管道以便读取内容以便它可以记录它.(因此,它更严格地是2的实现,因为它委托给日志记录).

说完了:

import io
import logging
import os
import select
import subprocess
import time
import threading

LOG_FILENAME = 'output.log'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

class StreamLogger(io.IOBase):
    def __init__(self, level):
        self.level = level
        self.pipe = os.pipe()
        self.thread = threading.Thread(target=self._flusher)
        self.thread.start()

    def _flusher(self):
        self._run = True
        buf = b''
        while self._run:
            for fh in select.select([self.pipe[0]], [], [], 0)[0]:
                buf += os.read(fh, 1024)
                while b'\n' in buf:
                    data, buf = buf.split(b'\n', 1)
                    self.write(data.decode())
            time.sleep(1)
        self._run = None

    def write(self, data):
        return logging.log(self.level, data)

    def fileno(self):
        return self.pipe[1]

    def close(self):
        if self._run:
            self._run = False
            while self._run is not None:
                time.sleep(1)
            os.close(self.pipe[0])
            os.close(self.pipe[1])
Run Code Online (Sandbox Code Playgroud)

因此,该类启动一个os级别管道,Popen可以将stdin/out/error附加到子进程.它还会启动一个线程,每秒一次轮询该管道的另一端以记录事物,然后使用日志记录模块记录该线程.

可能这个类应该为完整性实现更多的东西,但无论如何它都适用于这种情况.

示例代码:

with StreamLogger(logging.INFO) as out:
    with StreamLogger(logging.ERROR) as err:
        subprocess.Popen("ls", stdout=out, stderr=err, shell=True)
Run Code Online (Sandbox Code Playgroud)

output.log最终如下:

INFO:root:output.log
INFO:root:streamlogger.py
INFO:root:and
INFO:root:so
INFO:root:on
Run Code Online (Sandbox Code Playgroud)

使用Python 2.6,2.7和3.1进行测试.

我认为1和3的任何实现都需要使用类似的技术.它有点涉及,但除非你能让Popen命令自己正确记录,否则我没有更好的主意).