Decorate \ delegate a File object to add functionality

后端 未结 5 901
忘掉有多难
忘掉有多难 2020-12-03 01:57

I\'ve been writing a small Python script that executes some shell commands using the subprocess module and a helper function:

import subprocess          


        
5条回答
  •  清歌不尽
    2020-12-03 02:23

    1 and 2 are reasonable solutions, but overriding write() won't be enough.

    The problem is that Popen needs file handles to attach to the process, so Python file objects doesn't work, they have to be OS level. To solve that you have to have a Python object that has a os level file handle. The only way I can think of solving that is to use pipes, so you have an os level file handle to write to. But then you need another thread that sits and polls that pipe for things to read in so it can log it. (So this is more strictly an implementation of 2, as it delegates to logging).

    Said and done:

    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])
    

    So that class starts a os level pipe that Popen can attach the stdin/out/error to for the subprocess. It also starts a thread that polls the other end of that pipe once a second for things to log, which it then logs with the logging module.

    Possibly this class should implement more things for completeness, but it works in this case anyway.

    Example code:

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

    output.log ends up like so:

    INFO:root:output.log
    INFO:root:streamlogger.py
    INFO:root:and
    INFO:root:so
    INFO:root:on
    

    Tested with Python 2.6, 2.7 and 3.1.

    I would think any implementation of 1 and 3 would need to use similar techniques. It is a bit involved, but unless you can make the Popen command log correctly itself, I don't have a better idea).

提交回复
热议问题