scanner.Scan() 阻塞直到 Linux 退出,但 Windows 不退出

scanner.Scan() blocking until exit on Linux but not Windows

我正在开发一个 devtool,该工具的功能之一涉及生成子进程并读取该进程的标准输出流。我需要将每一行输出读入内存以便以某种方式处理它(该工具的未来功能之一将涉及处理日志并将它们发送到外部位置,例如日志管理器和仪表板等)所以这就是我不这样做的原因不要简单地做 cmd.Stdout = os.Stdout)

它运行良好,已经运行了一段时间,但显然只在 Windows 上运行。我最近遇到一个相当混乱的问题 bug report,用户报告输出不是 "real time",在 Linux 上测试后,我发现这是真的,并且输出仅在以下情况下转储到控制台进程退出。

这是扫描 reader 的代码,在 Windows 上按预期工作,但在 Linux 上或在 Windows/MacOS 上的 Linux 容器中不工作(两者都测试了)

https://github.com/Southclaws/sampctl/blob/f639c941dd8f9ca7c7c819a152909044ad63be08/runtime/run.go#L133-L137

如果您查看代码,您会发现 reader 是使用 io.Pipe() 创建并绑定到 cmd 的 Stdout/Stderr 输出的位置。

第 134 行是程序阻塞的地方,直到下面 goroutine 中的 cmd 在第 161 行停止 运行。

我假设这与缓冲区和刷新有关,但我对 Go 的内部结构了解不够,无法真正查明问题所在。 Windows 和 Linux 上的 scanner.Scan() 到底有什么不同?为什么它在一个平台上阻塞而在另一个平台上不阻塞?是否与 threads/goroutines 的安排不同有关? (两台测试机都有多个核心,即使 Docker 容器也有 4 个 vCPU)

这里的问题供参考:https://github.com/Southclaws/sampctl/issues/100

我真的被这个问题难住了,希望得到一些帮助来解决这个问题!

编辑:

于是又折腾了一番,还是没有解决。我尝试使用 Python 脚本并得到相同的结果,stdout 在定向到 tty 时工作正常,但当它被进程读取时它只是挂起:

from subprocess import Popen, PIPE
from time import sleep

p = Popen(
    ['/root/.samp/runtime/0.3.7/samp03svr'],
    stdin=PIPE,
    stdout=PIPE,
    stderr=PIPE,
    shell=False,
    cwd="/root/.samp/runtime/0.3.7/")

while True:
    print "attempting to read a line"
    output = p.stdout.read()
    print "read a line"
    if not output:
        print '[No more data]'
        break
    print output

attempting to read a line 挂在那里。

默认情况下,Linux 在不处于交互模式时(即不在终端中)缓冲输出,因此只有在缓冲区已满时才会刷新输出(例如,每 4096 字节,但这是实现定义的);当程序显式调用 flush 时(这里显然没有发生);或者当进程结束时(如您所见)。

您可以通过调整缓冲区大小来更改此默认行为。例如,通过 stdbuf:

启动程序
stdbuf -oO /root/.samp/runtime/0.3.7/samp03svr

-o 对于 stdout(还有 -e-i),O 对于 "Off"(对于 L "line buffered" 或显式缓冲区大小的大小)。

或者有一个 unbuffer 命令或 script 命令:

https://unix.stackexchange.com/questions/25372/turn-off-buffering-in-pipe/61833#61833

继续 Y_Less 的回答,一般的解决方案是使用 pseudo-terminal。我想避免使用 stdbuf 或 unbuffer,因为这需要取决于存在的外部命令。

所以我的最终解决方案是 https://github.com/kr/pty 这是 pseudo-terminal.

的 Go 实现

只是想 self-answer 帮助任何其他 Go 用户通过搜索找到这个问题。