如何根据 HTTP 请求使用 Python 和 Flask 执行 shell 命令并流输出?

use*_*759 2 python linux subprocess flask

在这篇文章之后,我可以将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()
Run Code Online (Sandbox Code Playgroud)

这种方法有两个问题。

  1. tail -f log关闭网页后,该过程将持续存在。访问http://localhost:5000/yield n 次后会有 n 个 tail 进程
  2. 一次只能有1个客户端访问http://localhost:5000/yield

我的问题是,是否可以让flask在有人访问页面时执行shell命令,并在客户端关闭页面时终止该命令?就像Ctrl+C之后tail -f log。如果没有,还有哪些替代方案?为什么我一次只能有 1 个客户端访问该页面?

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

Mat*_*ipp 6

这是一些应该完成这项工作的代码。一些注意事项:

  1. 您需要检测请求何时断开连接,然后终止进程。下面的 try/ except 代码可以做到这一点。然而,在inner()到达末尾后,Python将尝试正常关闭套接字,这将引发异常(我认为这是socket.error,按照How to handle a Broken Pipe (SIGPIPE) in python?)。我找不到一种方法来干净地捕获这个异常;例如,如果我在inner()末尾显式引发StopIteration,并用try/ except socket.error块包围它,它就不起作用。这可能是 Python 异常处理的限制。您可能可以在生成器函数中执行其他操作来告诉 Flask 中止流式传输,而无需尝试正常关闭套接字,但我还没有找到它。

  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. 网络服务器似乎会缓冲发送到客户端的前几 kB 数据,因此在将许多新行添加到 ./log 之前,您可能不会在网络浏览器中看到任何内容。之后,似乎立即发送新数据。不确定如何立即发送请求的第一部分,但这可能是流服务器的一个非常常见的问题,所以应该有一个解决方案。对于自行快速终止的命令来说这不是问题,因为它们的完整输出将在终止后发送。

您还可以在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 /sf/ask/1295778361/
        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()
Run Code Online (Sandbox Code Playgroud)