Как запустить Perl `` ver`` TAP-жгут в небуферизованном режиме?

Как часть тестового набора, написанного на Python 3 [. 4-.6] в Linux, мне нужно запустить ряд сторонних тестов. Тесты сторонних разработчиков - это bash скрипты. Они предназначены для работы с Perl prove TAP harness. Один bash script может содержать до нескольких тысяч отдельных тестов - и некоторые из них могут зависать бесконечно. После таймаута я хочу убить тест script и собрать некоторую информацию о том, где он застрял.

Поскольку сценарии bash создают собственные процессы, я пытаюсь изолировать все дерево процессов prove в новой группе процессов, поэтому я могу в конечном итоге убить всю группу процессов в целом, если все пойдет не так. Поскольку тесты должны выполняться с привилегиями root, я использую sudo -b для создания новой группы процессов с привилегиями root. Эта стратегия (в отличие от использования setsid в той или иной форме) является результатом комментариев, полученных мной по этому вопросу в SE Unix & Linux

Проблема в том, что я теряю весь вывод из жгута prove TAP, если я его преждевременно убиваю, когда запускается с помощью sudo -b через Python subprocess.Popen.

Я выделил его в простой тестовый пример. Ниже приведен тест bash script с именем job.t:

#!/bin/bash

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

Просто для сравнения, я также написал Python script с именем 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 script job.py, весь вывод этого script полностью до точки, когда он был убит, захватывается и печатается успешно. Когда run_demo выполняется во второй раз с демонстрационным bash тестом script job.t поверх prove, вывод не записывается и печатаются только пустые строки.

[email protected]:~> 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:  
[email protected]:~>

Что здесь происходит и как я могу это исправить?

т.е. как я могу прерывать/завершать тест bash script, работающий с prove (и всей его группой процессов) таким образом, чтобы я мог записать его вывод?

EDIT: предложил в ответ, что наблюдаемое поведение происходит из-за того, что Perl выполняет буферизацию своего вывода. В пределах индивидуального Perl script это можно отключить. Однако нет очевидной опции, позволяющей отключить буферизацию для 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, но я могу сгенерировать их самостоятельно.

Ответ 1

По умолчанию STDOUT для многих программ (включая perl) буферизируется по строке (сбрасывается в новой строке), когда STDOUT подключается к терминалу и блокируется буфером (сбрасывается при заполнении файлового буфера) в противном случае ( например, когда он подключен к трубе).

Вы можете обмануть такие программы, используя буферизацию строк, используя псевдо-tty (ptty) вместо канала. С этой целью unbuffer является вашим другом. В Ubuntu это часть пакета expect (sudo apt install expect).

Из docs:

unbuffer отключает буферизацию вывода это происходит, когда выход программы перенаправляется от неинтерактивных программ. Например, предположим, что вы смотрите вывод из fifo, запустив его через od и затем больше.

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

Ответ 2

Это начало ответа, у него больше информации, чем я мог бы вдавить в комментарий.

Проблема, которую вы выложили, на самом деле не связана с bash, она связана с Perl. В моей системе which prove указывает на /usr/bin/prove, который является perl script. Реальный вопрос здесь в основном о 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 script faile для вывода результата при его уничтожении.

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

Итак, это означает, что ваша проблема действительно является конкретным примером того, как захватить вывод из убитой программы perl, работающей в фоновом режиме, управляемой программой Python.

В качестве следующего шага я установил job.pl в unbuffer stdout:

$ 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
$ 

Итак, возможно, если вы взломаете доказательство script и установите все, чтобы запустить небуферизованный, который будет делать то, что вы хотите. В любом случае, я думаю, ваш вопрос теперь "как я могу запустить prove -v в небуферизованном режиме".

Надеюсь, это поможет.