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".

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


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