June 5, 2008

О перенаправлении вывода

Я почти уверен, что каждый программист хотя бы раз в жизни, но сталкивается с проблемой перенаправления потока вывода процесса куда-то наружу. Например, при написании GUI обёртки для консольного приложения, которое "кто-то написал давным давно, да к тому же под *nix, а потом добрые люди спортировали это чудо в Windows и так и оставили". Я долго избегал этой участи, может быть подсазнательно, но вот и моё время пришло.

Напомню, что дело имею в основном с C# и .Net, так что разговаривать будем с использованием этих слов. Хотя, наверное, теория, она одна, и на чём ты её реализовываешь - дело десятое.

Начну, пожалуй, издалека. Если обратить внимание на правый side-bar данного блога можно запросто заметить иконку замечательного текстового редактора Notepad++, которым я иногда пользуюсь, когда это удобнее Vim'а. Так вот, некоторое время назад я столкнулся с ним вплотную с точки зрения использования его в качестве некого подобия IDE. И существенной частью этого использования должна была стать возможность запускать некие скрипты, набираемые в поле редактирования, прямо из редактора, используя внешний интерпретатор. Сейчас это реализовано через plug-in, кому интересно, могут посмотреть в документацию и узнать подробности.

Внимательному читателю уже стало ясно, что именно вызов внешнего интерпретатора из оконного приложения и есть то самое место, где просто необходимо перенаправление вывода дочернего процесса, так как никому не охота смотреть на чёрное окно, закрывающее вид на редактор. Вот тогда я, пожалуй, впервые столкнулся с проблемой. Как оказалось не зря столкнулся, потому что слегка корявая реализация (или плохая настройка) этой возможности, заставила меня задуматься над сутью вещей, и именно в тот самый момент я заинтересовался как же это работает.

Позволю себе углубиться ещё дальше в историю вопроса. Многим известно, что перенаправление потока вывода консольного приложения можно осуществить, так сказать, голыми руками, просто набрав в консоли "program.exe > out". Известно это было и мне, более того, я всегда активно пользовался этой возможностью, за что однажды в институте получил упрёк от преподавателя за "недружелюбность интерфейса". Вобщем, знание о магическом операторе ">" было мне ведомо и хранилось до поры до времени внутри головного мозга.

Ну что ж. Задача понятна. Будем искать решение. Куда идём? Правильно! Для начала MSDN Library.

ProcessStartInfo info = new ProcessStartInfo(executable_path, command_line_parameters);
info.UseShellExecute = false; // это важно
info.RedirectStandardOutput = true; // и это тоже не менее важно
Process proc = Process.Start(info);
string output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();

Поход ясен как день, но проблемы не решает. Хотелось бы чего-то более интерактивного. "ReadToEnd", конечно, гарантирует, что весь вывод, который когда-то предполагался для консоли, попадёт в мои руки через "output", но произойдёт это только после того, как процесс завершится, а в моём случае произойти это может сколь угодно нескоро. А значит, пользователь, который имел неосторожность нажать кнопку "Старт" в моём приложении, будет недоумевать, почему всё встало и ничто не булькает и не моргает, чтобы хоть каким-то способом заявить о кипящей где-то глубоко внутри работе.

Стоит отметить, что в МСДНе есть ещё много иностранных слов о том, что чтение перенаправленного потока вывода можно осуществлять как синхронно, так и не очень. Но все мои потуги вызвать "proc.StandardOutput.BeginReadToEnd()" или даже "proc.BeginOutputReadLine()" привели к тому же печальному результату, даже более того, в последнем случае по завершении процесса я получал только первую строку его вывода.

Собственно, настало время перестать слушать маму читать что написали другие, и написать что-то самому. Вот здесь как раз вспомнился опыт общения с Notepad++, где одна из переменных для настройки называлась как-то вроде "OutputPingTimeout", или по-другому но с тем же смыслом.

Идея в том, чтобы периодически тыкать поток вывода дочернего процесса на момент наличия в нём "чего почитать". Ну и конечно, эти тычки должны происходить с некоторой периодичностью в процессе работы процесса. (Да, я знаю, что масло масляное.) В этом нам помогут старые добрые треды (будем называть их так, чтобы не путать с потоками ввода/вывода). Дабы не городить огород из "public static void Main", возьмём в руки IronPython и забацаем, так сказать, по-быстрому.

import clr
from System.Diagnostics import ProcessStartInfo, Process
from System.IO import StreamReader
from System.Threading import Thread, ThreadStart
from System import Exception

def create_start_info():
   info = ProcessStartInfo(r"...")
   info.Arguments = "..."
   info.UseShellExecute = False
   info.RedirectStandardOutput = True
   return info

def run_proc():
   proc = Process.Start(create_start_info())
   return proc

reader = None

def thread_proc():
   while True:
      s = reader.ReadLine()
      if not s:
         break
      print 'read from reader:', s

if __name__ == "__main__":  
   try:
      try:
         proc = run_proc()
         reader = proc.StandardOutput
         Thread.CurrentThread.Join(100)
         thread = Thread(ThreadStart(thread_proc))
         thread.Start()
         proc.WaitForExit()
         print 'Process finished'
         thread.Join()
         print 'Thread finished'
      except Exception, ex:
         print ex.ToString()
   finally:
      proc.Close()

Ну вот как-то так. И что самое удивительное - этот код заработал! Вроде бы всё должно быть понятно.

Один только момент, который хотелось бы уточнить, это окончание треда. Тред крутится до тех пор, пока "reader.ReadLine" не вернёт null (или None в случае с Python), а это должно произойти только когда поток закончится, а значит, ккогда закочнится выполнение дочернего процесса. Следовательно, если исходить из предположения, что процесс рано или поздно закончит своё существование, можно надеяться, что и "thread.Join()" без таймаута не приведёт к "мёртвому висяку", известному так же под именем "dead lock".

Вот на этой оптимистичной ноте, позвольте закончить и откланяться.


Comments:
open ( F, 'yourprocessname --arguments|' ) || die "error to run: $!";

while ( my $line = <F> )
{
print $line;
}

exit ( 0 );

и примерно так же в любом инструменте, который не ставит себе целью загнать все в странные рамки ...

 


Я так понимаю, это Perl?
Удобненько, конечно. Позволю себе предположить, что в этом случае ключевым моментом передачи вывода является символ "|" после аргументов процесса?
То есть перенаправлением занимается сама операционная система, занятно.
К сожалению я вот так с разгону не могу сказать можно ли каким-то образом открыть такой "файл" с использованием .Net, но то, что использовать стандартный файловый ввод-вывод здесь не получится - однозначно, ибо "недопустимые символы в пути к файлу".
А кстати, как перенаправить ещё и поток ошибок? Ну и ввод за компанию :-)

 


да, это perl, но оно везде примерно так. поэтому и удивала сложность примера из статьи: "плождение" дополнительного thread и тд - зачем, если стартовали задачу по определению в соседнем процессе еще и какие-то сложности с паралелльностью вычитывания в синхронном чтении в основном процессе.

если перенаравить stderr туда же, куда stdout, то это делается очень просто:

yourprocessname 2>&1

загнать второй handle туда же, куда и первый.

с и in и out можно сделать так:

'|yourprocessname|'

но будет гадость с буферизацией. поэтому есть отдельный IPC::Open2 вида:

my $in = new IO::Handle(...);
my $out = new IO::Handle(...);
IPC::Open2::open2 ( $in, $out, 'yourprocessname --parameters' );
while ( my $line = <$out> )
{
$in->print ( "qwe" ) if $line eq "aaaa\n";
}

реально это все обертки к fork, exec, dup/dup2, waitpid.

концепции "потоков" на unix/windows вроде не отличаются.

реально ли много проще (подобно примером выше) написать примеры cущественно компактнее/"инкапсулированне", чем в начальном примере ?

так как c# мне по сравнению с java нравился/нравится существенно более компактным api библиотер, где следование oop/d не ставится в любую главу угла, а только в необходимые ... притом что oop/d я люблю :), но не люблю излишней нормализации моделей и подобного.

Michael

 


Интересное дело...
Мне совершенно непонятно, как я смог упустить тот факт, что тред здесь вовсе не нужен. То есть вот в этом самом простом примере, где мы просто переправляем вывод одного процесса в вывод другого. (Что касается непосредственно того приложения, над которым я работаю это ещё предстоит обдумать.)
Но, действительно, упростить можно почти до уровня примера на Perl.

proc = run_proc()
reader = proc.StandardOutput
while True:
line = reader.ReadLine()
if not line:
break
print "read from reader:", line

При этом, функция run_proc никуда не девается, правда.
Это всё очень странно. Ведь я ж неспроста тред создал. Я точно помню что ReadLine блокировал мне решительно всё до тех пор, пока процесс не завершался, кода к сожалению не сохранилось, но я честное слово пробовал, сам не люблю треды плодить.
Перенаправление потока ошибок в тот же поток вывода в моём случае, наверное, подошло бы, если бы "2>&1" нотация работала в классе Process. Кстати, возможность использования ">" в командной строке я тоже проверял, не вышло. Возможно, придётся перепроверить, такое дело...

Ну и напоследок. Немного поразмышляю на тему использования без-тредного варианта перенаправления консольного потока дочернего процесса куда-нибудь в GUI. В теории можно было бы написать цикл а-ля:
while True:
out = proc.StandardOutput.ReadLine()
err = proc.StandardError.ReadLine()
if not (out or err):
break
Тут вроде бы ясно. Когда обоим потокам нечего сказать -- прекращаем ожидать вывода. Но, ReadLine операция блокирующая и если у нас из процесса сыплются только ошибки, мы будем ждать бесконечно долго.
С другой стороны, чтобы этого избежать, можно попробовать асинхронные операции чтения типа Process.BeginOutputReadLine() в паре с событием Process.OutputDataReceived. Но тут сразу возникают проблемы с синхронизацией, потому что Begin...ReadLine можно вызывать повторно только после того, как сработало событие или предыдущая операция была отменена. И тогда я снова возвращаюсь к своему первоначальному мнению, что организация отдельного треда для чтения из каждого потока - есть не самый плохой вариант. По крайней мере никакой возни с синхронизацией. Сиди себе и жди пока процесс не закончится. Кроме того в Windows Forms приложении ещё и GUI Thread не блокируется (если не вызывать Process.WaitForExit без параметров) и может делать всё что душе угодно.

 


После нескольких проверок, могу сказать почему я отказался от использования ReadLine в пользу отдельных тредов.
Я проверял на двух разных дочерних процессах. Первый -- python скрипт, второй -- IronPython скрипт. Оба они делали одно и то же, выводили в консоль последовательность чисел от 0 до 100 по одному в строке с небольшой задержкой.
Результаты удивили. Оказалось, что IronPython процесс отдавал строки по одной как и задумывалось, а вот python почему-то отдавал всё скопом.
Так что не всё тут ясно... Версия с тредами, кстати, тоже не работает как надо.

 


Оказалось, дело в buffered output. И ведь два дня назад имел схожую проблему только внутри .Net Stream IO, а не признал с первого взгляда. Шестой рабочий день подряд, что ни говори...

 


1 не блокируемое чтение :) но придется "полить", и не readline, а read()

2 именно асинхронное чтение (а вот тут я posix/unix не скажу - ни разу не пробовал)

3 в gui приложениях действительно имеет смысл пложить отдельный thread, чтобы не связываться с async io и не блокировать gui - так просто проще. я "возбудился" на начальный пример простого чтения, который получился излишне сложным

4 2>&1 - это shell. если запускать непосредственно процесс без shell через exec() etc, то подобное надо сделать руками через dup2. вида

if ( my $pid = fork() )
{
# we are in parent process
waitpid ( $pid ); # и тут бы ошибки обработать :)
exit();
}
elsif ( defined $pid )
{
dup2 ( 2, 1 );
exec ( 'yourprocessname', 'arg1', 'arg2' );
}
else
{
die "error to fork: $!";
}

5 '>' и тд - это тоже shell. просто dup2 делается не на ( 2, 1 ), на ( 1 /* stdout */, IO::File->new ( 'path', 'w' )->fileno() ); # с точностью до того, что лучше писать sysopen :)

 


про python vs ironpython: просто один делает flush() сам при "println" или autoflush ( 1 ) ставит, второй - не делает подобного по умолчанию и надо заставить его делать подобное "руками".

 


Нда... Про autoflush я дошёл.
Спасибо за дельные комментарии.

 


Кстати, столкнулся с проблемой. А можно ли как-то извне заставить Perl делать autoflush?
Для Python я нашёл простое решение - установку переменной окружения PYTHONUNBUFFERED в любое значение, а вот для Perl, не обнаружил ничего кроме модификации самого скрипта, что в моём случае мягко говоря нежелательно.

 


rss reexport :)

не правля скрипт - есть вариант через LD_PRELOAD перегрузить write() syscall и после каждого write() делать flush();

но это

1 не про perl
2 как-то грубо

иного в голову пока не приходит.

Michael

 


Нда... РСС чего-то сегодня обновился ни с того ни с сего.

 


Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?