如何在HTTP请求时使用Python和Flask执行shell命令和流输出?

时间:2016-07-28 08:01:47

标签: python linux flask subprocess

关注this post,我可以tail -f将日志文件发送到网页:

from gevent import sleep
from gevent.wsgi import WSGIServer
import flask
import subprocess

app = flask.Flask(__name__)

@app.route('/yield')
def index():
    def inner():
        proc = subprocess.Popen(
                ['tail -f ./log'],
                shell=True,
                stdout=subprocess.PIPE
                )
        for line in iter(proc.stdout.readline,''):
            sleep(0.1)
            yield line.rstrip() + '<br/>\n'

    return flask.Response(inner(), mimetype='text/html')

http_server = WSGIServer(('', 5000), app)
http_server.serve_forever()

这种方法有两个问题。

  1. 关闭网页后tail -f log进程将停留。访问http://localhost:5000/yield n时间
  2. 后会有n个尾部流程
  3. 一次只能有1个客户端访问http://localhost:5000/yield
  4. 我的问题是,当有人访问页面并在客户端关闭页面时终止命令时,是否可以让flask执行shell命令?喜欢tail -f log之后的 Ctrl + C 。如果没有,有哪些替代方案? 为什么我一次只能让一个客户端访问该页面?

    注意:我正在研究启动/停止任意shell命令的一般方法,而不是特别拖尾文件

1 个答案:

答案 0 :(得分:3)

这是一些应该完成这项工作的代码。一些说明:

  1. 您需要检测请求何时断开连接,然后终止proc。下面的try / except代码会这样做。但是,在inner()到达终点后,Python将尝试正常关闭套接字,这将引发异常(我认为它是socket.error,每How to handle a broken pipe (SIGPIPE) in python?)。我找不到干净利落地抓住这个例外的方法;例如,如果我在inner()的末尾显式提出StopIteration,并使用try / except socket.error块包围它,则它不起作用。这可能是Python异常处理的限制。在生成器函数中可能还有其他的东西可以告诉烧瓶中止流而不试图正常关闭套接字,但我还没有找到它。

  2. 您的主线程在proc.stdout.readline()期间阻塞,并且gevent.sleep()来得太晚无法提供帮助。原则上gevent.monkey.patch_all()可以修补标准库,这样通常会阻塞线程的函数将产生对gevent的控制(参见http://www.gevent.org/gevent.monkey.html)。但是,这似乎不会修补proc.stdout.readline()。下面的代码使用gevent.select.select()来等待数据在产生新数据之前在proc.stdout或proc.stderr上可用。这允许gevent在等待时运行其他greenlet(例如,服务于其他Web客户端)。

  3. 网络服务器似乎缓冲了发送到客户端的前几个数据库,因此在./log中添加了许多新行之前,您可能无法在Web浏览器中看到任何内容。之后,它似乎立即发送新数据。不知道如何立即发送请求的第一部分,但它可能是流服务器的一个相当普遍的问题,所以应该有一个解决方案。对于自行终止的命令,这不是问题,因为一旦它们终止,它们的完整输出将被发送。

  4. 您还可以在https://mortoray.com/2014/03/04/http-streaming-of-command-output-in-python-flask/找到有用的内容。

    以下是代码:

    from gevent.select import select
    from gevent.wsgi import WSGIServer
    import flask
    import subprocess
    
    app = flask.Flask(__name__)
    
    @app.route('/yield')
    def index():
        def inner():
            proc = subprocess.Popen(
                    ['tail -f ./log'],
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                    )
            # pass data until client disconnects, then terminate
            # see https://stackoverflow.com/questions/18511119/stop-processing-flask-route-if-request-aborted
            try:
                awaiting = [proc.stdout, proc.stderr]
                while awaiting:
                    # wait for output on one or more pipes, or for proc to close a pipe
                    ready, _, _ = select(awaiting, [], [])
                    for pipe in ready:
                        line = pipe.readline()
                        if line:
                            # some output to report
                            print "sending line:", line.replace('\n', '\\n')
                            yield line.rstrip() + '<br/>\n'
                        else:
                            # EOF, pipe was closed by proc
                            awaiting.remove(pipe)
                if proc.poll() is None:
                    print "process closed stdout and stderr but didn't terminate; terminating now."
                    proc.terminate()
    
            except GeneratorExit:
                # occurs when new output is yielded to a disconnected client
                print 'client disconnected, killing process'
                proc.terminate()
    
            # wait for proc to finish and get return code
            ret_code = proc.wait()
            print "process return code:", ret_code
    
        return flask.Response(inner(), mimetype='text/html')
    
    http_server = WSGIServer(('', 5000), app)
    http_server.serve_forever()