我想捕获一个长期运行的输出,但冗长的脚本(目前是最快的),并打印在网页上的进展.我在这里面临着两个问题

首先:我需要异步运行该脚本,并在它仍在运行时捕获输出.我发现PYTEST上有--capture=tee-sys的旗帜,我想是为了这个目的,但我找不到它是如何工作的.

一种更通用的方法是使用subprocess.Popen,它似乎已经处理了异步部分,并且输出可以通过管道传输.然而,如果我用Popen.communicat()捕获,我要么阻塞线程,直到进程结束,我不能实时更新,要么我必须不断地捕获TimeoutExpired异常并重新启动通信,这感觉像是一种解决办法,但可能会起作用.但有没有更简单的方法来实现这一点呢?

第二:我需要在没有收到请求的情况下更新前端.起初,我甚至认为有一个终端风格的输出会很酷,并找到了类似ttyd的东西,但这似乎有点夸张,如果我没有弄错的话,将允许用户向终端输入我不想要的东西.现在我还找到了this answer,这建议使用iframe和flask documentation中的stream_template_context(),然而,如果我在这两种情况下都没有弄错,我似乎需要使用子进程收集的数据在运行中构建一个有效的html页面,这感觉很容易出错.我还找到了flask-socketio,这是我想要避免的,因为Falsk服务器应该运行在定制的Linux上,我需要首先添加这个模块.这最终应该是可行的,但如果有更简单的方法,我更愿意这样做.我曾考虑过从前端的Java脚本轮询,但这看起来又像是一种变通办法.在这里最好的做法是什么?

对于这两个部分,我都很高兴有更多的资源可以阅读最佳实践等内容.

经过更多的研究,我得到了一个如下所示的最小工作示例:

from flask import Flask, Response, render_template
from subprocess import Popen, PIPE

app = Flask(__name__)

@app.route('/content')
def content():
    # start subprocess
    def inner():
        # proc = Popen(['/usr/local/bin/pytest', 'test1.py'], stdout=PIPE)
        with Popen(['/usr/local/bin/pytest', 'test1.py'], stdout=PIPE) as proc:
            while proc.poll() is None:
                line = proc.stdout.readline()
                # yield line.decode() + '<br/>\n'
                yield str(line) + '<br/>\n'

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

@app.route('/')
def index():
    return render_template('test.html')

if __name__ == '__main__':
    app.run(debug=True)

其中test.py只定义了一系列测试函数,每个Hibernate 秒和assert True只是为了获得所需的样本输出, 和test.html分:

<!doctype html>
<head>
    <title>Title</title>
</head>
<body>
    <div>
        <iframe frameborder="1" 
                width="52%"
                height="500px"
                style='background: transparent;' 
                src="{{ url_for('content')}}">
        </iframe>
    </div>
</body>

然而,像这样使用proc.stdout.readline()subprocess documentation个州是不受欢迎的,因为它会导致子进程死锁. 此外,在子进程终止后,我有时会收到几行,有时会有很多空行.我使用str(line)使它们可见,因为字节字符串中的b''就在那里. 你知道接下来该怎么做吗?

推荐答案

我找到了两个解决方案,其中一个我真的很喜欢,所以我要和大家分享.

但首先:我想我误解了关于不要使用subprocess documentation中的Popen.stdout.read()的警告.我只用readline我应该没问题,因为电话无论如何都应该返回.(如果我在这里错了,请纠正我,因为整个答案都是基于这样的假设,即readline不会在read会死锁的地方陷入僵局.)

第一个解决方案:修改this answer. 实际上并不需要<iframe>,Flask可以将数据串流到<div>中.该html如下所示:

<p>Test Output</p>

<div id="output">
    {% if data %}
        {% for item in data %}
            {{ item }}<br />
        {% endfor %}
    {% endif %}
</div>

{% if data %}是可选的.我使用它来加载页面的空白<div>,并在按钮上按下重新加载站点,并提供data来填充<div>

Python部分如下所示:

from flask import Response, stream_template
from subproces import Popen, PIPE

@app.route('/')
def stream() -> Response:
    """stream data to template"""
    def update(task: str | list[str]):
        """update the stream data"""
        with Popen(task, stdout=PIPE, stderr=PIPE) as proc:
            while proc.poll() is None:
                line = proc.stdout.readline()
                log.info(line.decode())
                yield line.decode()
            while (line := proc.stdout.readline()) != b'':
                log.info(line.decode())
                yield line.decode()
            err = proc.stderr.read() # risk of deadlock
            # could be replaced by while readline as above
            if err != b'':
                log.warning("additionional stderr produced while running tests: %s", err.decode())
               
    return Response(
        stream_template('index.html.jinja',
                        data=update(['/usr/local/bin/pytest', 'dummytest.py'])))

我不得不在返回proc.poll()之后添加第二个While循环,因为我总是遗漏最后几行数据.但我不愿单独使用第二个While循环,因为如果一行为空(不应该发生,因为换行符应该总是在那里),就会有中断的风险.

使用EventSource,第二个解决方案基本上是this answer.我对此犹豫不决,因为我的Java脚本不是很流利,但最终它起到了护身符的作用.我在一个按钮上添加了一个事件处理程序,它阻止表单post连接到服务器,但手动打开了事件源.该html如下所示:

<div id="output"></div>


<p><form action="/action" method="post" role="form" id="testsForm">
    <button class="btn btn-primary" type="submit">Run Test</button>
</form></p>
    
<script type="text/javascript">
    (() => {
        'use strict'
        const form = document.getElementById("testsForm")
        form.addEventListener('submit', event => {
            event.preventDefault()
            var target = document.getElementById("output")
            var update = new EventSource("/action/stream")

            update.onmessage = function(e) {
                if (e.data == "close") {
                    update.close();
                } else {
                    target.innerHTML += (e.data + '<br/>');
                }
            };
        }, false)
    })()
</script>

表单中的actionmethod可能是完全可选的,因为表单从不传播到后端.(这是使用bootstrap个类,如果有人想要重新创建它的话)

用于此操作的python如下所示:

from flask import Response
from subproces import Popen, PIPE

@app.route("/action/stream")
def stream() -> Response:
    """open event stream to client"""
    def update(task: str | list[str]):
        """update the event stream data"""
        with Popen(task, stdout=PIPE, stderr=PIPE) as proc:
            log.info("opened subprocess for async task communication")
            while proc.poll() is None:
                line = proc.stdout.readline()
                log.info(line.decode().rstrip())
                # the '\n\n' is needed for termination in th frontend 
                # the stream handling (especially closing) will break without it
                yield 'data: ' + line.decode() + "\n\n"
            while (line := proc.stdout.readline()) != b'':
                log.info(line.decode())
                yield 'data: ' + line.decode() + "\n\n"
            err = proc.stderr.read() # risk of deadlock
            if err != b'':
                log.warning("additionional stderr produced while running script: %s", err.decode())
                
        yield "data: close\n\n"

    return Response(update(['/usr/local/bin/pytest', 'dummytest.py']), mimetype="text/event-stream")

这个解决方案的一大优点是,网站不需要重新加载就可以流式传输数据.我有一个空框,当按下按钮时,脚本的输出将出现在屏幕上.(I even found some styling to make it look like an old school monitor)

在更长的通信方面:我在pytest脚本中的一个测试中测试了两个版本,超时10分钟,两个版本都没有问题.

甚至可能有another solution to this个,但我从来没有try 过,因为我不太理解它.如果第二种解决方案由于某些原因不够充分,我可能会在future .

Html相关问答推荐

html中背景色的全单元格R中的Rmarkdown表

CSS复选框并排

在Apps Script中连接CSS与HTML

Select 包含iframe的图形<><>

将网格包装在css中

Angular 滑块问题

Angular 分量被循环

react :事件和转发器在特定代码段中不起作用

如何更改计时器S输入的Angular 字体大小?

风格规则应该适用于响应图像的哪些方面?哪种规则适用于<;图片和gt;,哪种规则适用于<;img>;?

在Vue 3中使用v-Bind将计算(computed)属性传递给css url()

如何生成随机字符串的字母数字字符集长度到html跨度?

SVG';COLOR&39;属性不优先于通用css';COLOR&39;属性

rmarkdown HTML数字不适用于针织衫_1.44

网格项未在同一行上对齐

将组件移动到页面底部 Angular

网页设计不适合移动设备

如何并排放置部分?

涉及短代码时如何在页面上定位元素?

水平滚动框不显示所有元素