10

作为在 Linux 上用 Python 3 [.4-.6] 编写的测试套件的一部分,我必须运行一些第 3 方测试。第 3 方测试是 bash 脚本。它们被设计为与Perl 的proveTAP 工具一起运行。一个 bash 脚本最多可以包含数千个单独的测试——其中一些可以无限期挂起。超时后,我想杀死测试脚本并收集一些关于它卡在哪里的信息。

因为 bash 脚本会创建自己的进程,所以我尝试将整个prove进程树隔离到一个新的进程组中,因此如果出现问题,我最终可以将整个进程组作为一个整体杀死。因为测试必须以 root 权限运行,所以我sudo -b用于创建一个具有 root 权限的新进程组。这种策略(而不setsid是以一种或另一种方式使用)是我在 SE Unix&Linux 上收到的关于这个问题的评论的结果

问题是,prove如果我在sudo -b通过 Python 的subprocess.Popen.

我把它隔离成一个简单的测试用例。以下是一个名为 的 bash 测试脚本job.t

#!/bin/bash

MAXCOUNT=20
echo "1..$MAXCOUNT"
for (( i=1; i<=$MAXCOUNT; i++ ))
do
   echo "ok $i"
   sleep 1
done

只是为了比较,我还编写了一个 Python 脚本,名为job.py产生或多或少相同的输出并表现出相同的行为:

import sys
import time
if __name__ == '__main__':
    maxcount = 20
    print('1..%d' % maxcount)
    for i in range(1, maxcount + 1):
        sys.stdout.write('ok %d\n' % i)
        time.sleep(1)

最后但同样重要的是,以下是我的精简版“Python 测试基础设施”,名为demo.py

import psutil # get it with "pip install psutil"
import os
import signal
import subprocess

def run_demo(cmd, timeout_after_seconds, signal_code):
    print('DEMO: %s' % ' '.join(cmd))
    proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
    try:
        outs, errs = proc.communicate(timeout = timeout_after_seconds)
    except subprocess.TimeoutExpired:
        print('KILLED!')
        kill_pid = _get_pid(cmd)
        subprocess.Popen(['sudo', 'kill', '-%d' % signal_code, '--', '-%d' % os.getpgid(kill_pid)]).wait()
        outs, errs = proc.communicate()
    print('Got our/err:', outs.decode('utf-8'), errs.decode('utf-8'))

def _get_pid(cmd_line_list):
    for pid in psutil.pids():
        proc = psutil.Process(pid)
        if cmd_line_list == proc.cmdline():
            return proc.pid
    raise # TODO some error ...

if __name__ == '__main__':
    timeout_sec = 5
    # Works, output is captured and eventually printed
    run_demo(['sudo', '-b', 'python', 'job.py'], timeout_sec, signal.SIGINT)
    # Failes, output is NOT captured (i.e. printed) and therefore lost
    run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

启动时demo.py,它会运行run_demo两次例程 - 使用不同的配置。这两次,都会启动一个具有 root 权限的新进程组。两次,“测试作业”ok [line number]每秒打印一次新行 ( ) - 理论上为 20 秒 / 20 行。但是,两个脚本都有 5 秒的超时时间,并且整个进程组在此超时时间后被终止。

run_demo第一次使用我的小 Python 脚本运行时,该脚本job.py的所有输出一直到它被杀死时都被捕获并成功打印。当run_demo第二次在 上运行演示 bash 测试脚本job.tprove,不会捕获任何输出,并且只打印空字符串。

user@computer:~> python demo.py 
DEMO: sudo -b python job.py
KILLED!
Got our/err: 1..20
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
 Traceback (most recent call last):
  File "job.py", line 11, in <module>
    time.sleep(1)
KeyboardInterrupt

DEMO: sudo -b prove -v /full/path/to/job.t
KILLED!
Got our/err:  
user@computer:~>

这里发生了什么,我该如何解决?

即,我如何中断/终止与prove(及其整个进程组)一起运行的 bash 测试脚本,以便我可以捕获其输出?

编辑:在答案中建议观察到的行为是由于 Perl 缓冲其输出而发生的。在单独的 Perl 脚本中,可以将其关闭。但是,没有明显的选项允许关闭prove[-v] 的缓冲。我怎样才能做到这一点?


我可以通过直接运行我的测试作业来解决这个问题bash。以下命令必须从

run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

run_demo(['sudo', '-b', 'bash', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

这样,我不会得到由 打印的测试统计信息prove,但我可以自己生成它们。

4

2 回答 2

5

默认情况下,perl当 STDOUT 连接到终端时,许多程序(包括管道)。

您可以通过使用伪 tty (ptty) 而不是管道来欺骗此类程序使用行缓冲。为此,unbuffer是你的朋友。在 Ubuntu 上,这是expect包 ( sudo apt install expect) 的一部分。

文档

unbuffer禁用从非交互式程序重定向程序输出时发生的输出缓冲。例如,假设您正在通过 od 运行它来查看 fifo 的输出,然后再运行更多。

od -c /tmp/fifo | more

在生成一整页输出之前,您将看不到任何内容。

您可以按如下方式禁用此自动缓冲:

unbuffer od -c /tmp/fifo | more

我尝试了您的示例代码并得到了与您描述的相同的结果(感谢您的最小、完整和可验证的示例!)。

然后我改变了

run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

run_demo(['sudo', '-b', 'unbuffer', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

unbuffer那就是:我只是在prove命令前面加上。然后的输出是:

DEMO: sudo -b python job.py
KILLED!
Got our/err: 1..20
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
 Traceback (most recent call last):
  File "job.py", line 8, in <module>
    time.sleep(1)
KeyboardInterrupt

DEMO: sudo -b unbuffer prove -v /home/dirk/w/sam/p/job.t
KILLED!
Got our/err: /home/dirk/w/sam/p/job.t .. 
1..20
ok 1
ok 2
ok 3
ok 4
ok 5
于 2017-12-31T12:27:03.077 回答
3

这是一个答案的开始,它包含的信息比我可以挤进评论的要多。

您提出的问题与 bash 无关,它与 Perl 有关。在我的系统上,which prove指向/usr/bin/prove,这是一个 perl 脚本。这里真正的问题通常是关于 perl 脚本,甚至不是特定于prove. 我在上面复制了您的文件并测试了我可以重现您所看到的内容,然后我创建了第三个测试:

$ cat job.pl
#!/usr/bin/perl
foreach (1..20){
  print "$_\n";   
  sleep 1;
}

很酷,然后我将其添加到演示程序中:

(也导入后shlex`):

cmdargs = shlex.split('sudo -b '+os.path.join(os.getcwd(), 'job.pl'))
run_demo(cmdargs, timeout_sec, signal.SIGINT)

而且,可以肯定的是,这个简单的 perl 脚本在被杀死时无法产生输出。

$ python3 demo.py
...(output as you wrote above followed by)... 
DEMO: sudo -b /home/jawguychooser/job.pl
KILLED!
Got our/err:  
$

因此,这意味着您的问题实际上是一个特定实例,即如何从 Python 程序控制的后台运行的已终止 perl 程序中捕获输出。

作为下一步,我设置job.pl为取消缓冲标准输出:

$ cat job.pl
#!/usr/bin/perl
$| = 1;
foreach (1..20){
  print "$_\n"; 
  sleep 1;
}

然后,我重新运行 demo.py,瞧!

$ python3 demo.py 
DEMO: sudo -b /home/jawguychooser/job.pl
KILLED!
Got our/err: 1
2
3
4
5
6
$ 

因此,也许如果您侵入证明脚本并将其设置为无缓冲运行,这将满足您的要求。无论如何,我认为您现在的问题是“我如何prove -v在无缓冲模式下运行”。

我希望这有帮助。

于 2017-12-31T07:58:44.193 回答