Запуск консольных приложений и перехват потока ввода/вывода в Delphi XE3

Автор: Alex. Опубликовано в Программирование . просмотров: 39516

Рейтинг:  4 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда не активна
 

Предположим вам нужно запустить консольное приложение из оконного приложения и наблюдать за потоком вывода. Вы можете сохранять выводимый текст в файл журнала или отображать текст в окне в многострочном текстовом поле, например, в TRichEdit. Кроме того вы можете отправить информацию приложению, если оно что-то запросило. Давайте разберёмся, как это сделать?

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

Как написать этот код с нуля можно найти в Интернете (например, на этом форуме), здесь же я остановлюсь на компоненте TPipeConsole, который как раз и создан, чтобы перехватывать потоки ввода/вывода в Delphi. Причём он позволяет это делать комфортно.

Компонент входит в состав юнита Pipes.pas, про который я писал в статье «Обмен данными между процессами в Delphi XE3». Там же написано, как лучше установить компоненты. Мой исправленный вариант можно скачать здесь:

Файлы:
Pipes.pas Версия:от 12.01.2010

(Старая версия!!! Лучше использовать версию Pipes.pas (Win32 и Win64), см. ниже). В юните реализация классов TPipeServer (Pipe-сервер), TPipeClient (Pipe-клиент) и TPipeConsole (класс для запуска консольных приложений, управления ими и перехвата потока вывода). Работает только на платформе Win32. Юнит с моими правками для работы с Delphi до версии XE3. Функция TPipeConsole.Execute с моими правками. Источник здесь.

Дата 31.01.2014 Система  Windows Размер файла 135.7 KB Закачек 1527

Есть вариант юнита c поддержкой всех версий Delphi и платформы Win64:

Pipes.pas (Win32 и Win64) Версия:от 04.10.2013

Юнит Pipes.pas с примерами, с runttime и designtime библиотеками и поддержкой платформы Win64. В юните Pipes.pas реализация классов TPipeServer (Pipe-сервер), TPipeClient (Pipe-клиент) и TPipeConsole (класс для запуска консольных приложений, управления ими и перехвата потока вывода). Должна работать с Delphi всех версий, но тестирование не проводилось. Функция TPipeConsole.Execute с моими правками. Источник здесь.

27.04.2017 Добавлены мои правки в функции TPipeConsole.Execute и TPipeConsole.Start, чтобы параметр CommandLine 100%-но не был константой, чтобы избежать AV, см. MSDN.

Дата 08.05.2015 Размер файла 147.99 KB Закачек 1022

Запуск консольного приложения и перехват потока ввода/вывода

Собственно компонент TPipeConsole всё что нам нужно уже делает. Для того, чтобы опробовать как работает запуск консольных приложений с помощью этого компонента создадим консольное приложение и скомпилируем его:

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses System.SysUtils;
 
begin
   //Обычное сообщение
   WriteLn('Здравствуй, мир!');
   //Сообщение об ошибке
   WriteLn(ErrOutput, 'Ошибка!');
   //Результат работы приложения
   ExitCode := 1;
end.

Теперь сделаем проект оконного приложения, на форму положим компонент TRichEdit, растянем его на всю форму (Align -> alClient), включим вертикальную прокрутку (ScrollBars -> ssVertical). Теперь положим на форму компонент TPipeConsole и обработаем все его события:

procedure TForm1.PipeConsole1Output(Sender: TObject; Stream: TStream);
var
   bytes: TBytes;
begin
   //Вывод обычных сообщений
   SetLength(bytes, Stream.Size);
   Stream.Read(bytes, Stream.Size);
   RichEdit1.Text := RichEdit1.Text + TEncoding.GetEncoding('Windows-1251').GetString(bytes);
end;
 
procedure TForm1.PipeConsole1Error(Sender: TObject; Stream: TStream);
var
   bytes: TBytes;
begin
   //Вывод сообщений об ошибке
   SetLength(bytes, Stream.Size);
   Stream.Read(bytes, Stream.Size);
   RichEdit1.Text := RichEdit1.Text + TEncoding.GetEncoding('Windows-1251').GetString(bytes);
end;
 
procedure TForm1.PipeConsole1Stop(Sender: TObject; ExitValue: Cardinal);
begin
   //Приложение отработало, ExitCode находится в переменной ExitValue
   RichEdit1.Text := RichEdit1.Text + 'Приложение завершило работу с кодом ' + IntToStr(ExitValue) + '.';
end;

Теперь по событию формы OnCreate сделаем запуск нашего консольного приложения:

procedure TForm1.FormCreate(Sender: TObject);
begin
   RichEdit1.Text := 'Запуск приложения' + #13#10;
   PipeConsole1.Start('Project1.exe', '');
end;

Выполним наше оконное приложение. Вот результат:

Результат выполнения консольного приложения

Как мы видим, всё отлично отработало, но есть одна ложка дёгтя при работе с компонентом. Если перемешать обычные сообщения с сообщениями об ошибке, то получать мы их будем в произвольном порядке. Вот пример такого консольного приложения:

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses System.SysUtils;
 
begin
   //Ещё одно обычное сообщение
   WriteLn('Сообщение 1!');
   //Сообщение об ошибке
   WriteLn(ErrOutput, 'Ошибка!');
   //Обычное сообщение после ошибки
   WriteLn('Сообщение 2!');
   //Результат работы приложения
   ExitCode := 1;
end.

Здесь сообщение об ошибке записывается между сообщениями 1 и 2. А вот как будет выглядеть результат:

Результат выполнения консольного приложения

Как видите, сначала вывелись все обычные сообщения, а потом сообщение об ошибке. Почему так происходит? Так происходит потому, что компонент TPipeConsole принимает сообщения из двух каналов по очереди: сначала из основного выходного канала, затем из канала для ошибок. Это нужно учитывать при написании консольного приложения, например, после вывода ошибки работа приложения сразу завершается. Или выводить все сообщения в основной канал.

Также здесь я отмечу, что мы использовали асинхронное выполнение консольного приложения. Если вам нужно выполнить консольное приложение синхронно, то нужно воспользоваться функцией Execute.

Теперь попробуем отправить что-нибудь консольному приложению. Для этого слегка перепишем консольное приложение:

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses System.SysUtils;
 
var
   number: integer;
 
begin
   try
      WriteLn('Введите число от 0 до 9:');
      Flush(Output);
      Read(number);
      if (number < 0) or (number > 9) then
         raise Exception.Create('Получено число не попадающее в интервал от 0 до 9.');
      WriteLn('Получено число: ' + IntToStr(number));
      WriteLn('Квадрат числа: ' + IntToStr(number * number));
   except
      on e: Exception do
      begin
         WriteLn(ErrOutput, e.Message);
         ExitCode := 2;
      end;
   end;
end.

Как видите, консольное приложение ждёт число в интервале от 0 до 9, чтобы вычислить квадрат этого числа. Значит нужно передать консольному приложению число:

procedure TForm1.FormCreate(Sender: TObject);
var
   bytes: TBytes;
begin
   RichEdit1.Text := 'Запуск приложения' + #13#10;
   //Запуск приложения
   PipeConsole1.Start('Project1.exe', '');
   //Передаём консольному приложению число 5
   bytes := TEncoding.GetEncoding('Windows-1251').GetBytes('5' + #13#10);
   PipeConsole1.Write(bytes[0], Length(bytes));
end;

Вот результат:

Результат приёма данных консольным приложением

Попробуем теперь передать строку вместо числа:

procedure TForm1.FormCreate(Sender: TObject);
var
   bytes: TBytes;
begin
   RichEdit1.Text := 'Запуск приложения' + #13#10;
   //Запуск приложения
   PipeConsole1.Start('Project1.exe', '');
   //Передаём консольному приложению число 5
   bytes := TEncoding.GetEncoding('Windows-1251').GetBytes('Abc' + #13#10);
   PipeConsole1.Write(bytes[0], Length(bytes));
end;

И, после запуска, увидим ошибку:

Результат приёма данных консольным приложением

Остановка консольного приложения

Теперь пара слов о том, как остановить консольное приложение, которое долго выполняется. Здесь есть 2 способа: прервать выполнение или оповестить приложение, что ему нужно завершить работу. Второй способ предпочтительнее, т.к. в этом случае консольное приложение перед завершением работы может освободить занимаемые ресурсы, удалить временные файлы и т.п.

Прерывание выполнения делается с помощью функции Stop:

PipeConsole1.Stop(0);

Оповещение приложения можно сделать с помощью функций SendCtrlC и SendCtrlBreak. Вот пример вызова:

PipeConsole1.SendCtrlC;

По умолчанию, вызов этой функции тоже прерывает работу приложения, но этот вызов можно обработать внутри консольного приложения по-своему и прерывания работы не произойдёт. Вот пример консольной программы с обработкой сигналов:

program Project1;
 
{$APPTYPE CONSOLE}
 
{$R *.res}
 
uses System.SysUtils, Windows;
 
var
   operation: integer;
   terminated: boolean;
 
   function Ctrl_Handler(Ctrl: DWORD): LongBool; stdcall; far;
   begin
      result := false;
      if Ctrl in [CTRL_C_EVENT, CTRL_BREAK_EVENT] then
      begin
         //Выставляем свой флаг прерывания в true
         terminated := true;
         //Сигнал обработан, поэтому нужно вернуть true
         result := true;
      end;
   end;
 
begin
   //Флаг прерывания в начале устанавливаем в false
   terminated := false;
   //Подписываемся на получение сигналов Ctrl+C, Ctrl+Break и других
   SetConsoleCtrlHandler(@Ctrl_Handler, true);
   //Выполняем операции в цикле
   for operation := 1 to 600 do
   begin
      WriteLn('Обработка ' + IntToStr(operation));
      Flush(Output);
      Sleep(1000);
      //Если получен сигнал Ctrl+C или Ctrl+Break, то выходим из цикла
      if terminated then
      begin
         WriteLn('Прерывание! Выход из цикла.');
         break;
      end;
   end;
end.

Теперь положим на форму нашего оконного приложения кнопку и по событию OnClick вызовем функцию SendCtrlC:

procedure TForm1.Button1Click(Sender: TObject);
begin
   PipeConsole1.SendCtrlC;
end;

Теперь протестируем, как работает отправка сигнала консольному приложению. Запустим нашу программу для тестирования, посмотрим, как приходят сообщения от консольного приложения, и нажмём на кнопку, которую мы сделали. Приложение завершило работу? выдав сообщение «Прерывание! Выход из цикла»:

Результат останова консольного приложения

Как видите, ничего сложного нет. Единственный момент, который нужно учесть, вызов функции Ctrl_Handler в консольном приложении происходит в отдельном потоке, поэтому при работе с глобальными переменными нужно использовать критические секции, чего в примере не сделано, чтобы не замусоривать код примера.

Итак, как вы убедились, с помощью компонента TPipeConsole можно легко контролировать консольное приложение, не углубляясь в тонкости этого процесса.

Tags: Обзоры инструментов для программирования Учебники по программированию Консольное приложение Delphi

Комментарии   

iMic
0 #11 iMic 26.04.2017 11:45
Цитата:
С помощью функции PipeConsole.Write(); нужно передавать строки завершённые символами перевода каретки #13#10. Программа html2xhtml.exe наверняка ждёт их. Может быть вы забыли передать эти символы?
Пробовал, а также пытался дописывать '\0' в конец строки, чтобы эмитировать стиль Си-строк - не помогло.
Полагаю, дело в том, что html2xhtml не поддерживает интерактивный режим (если я правильно выражаюсь), т.е. тот же код нормально работает, например с nslookup.exe, в то же время, если в отдельном окне cmd.exe передать вывод по цепочке, то всё работает:
Цитата:
echo "" | html2xhtml.exe
www.dropbox.com/.../cmdPipeLine.png?dl=0
Цитировать
Alex
0 #12 Alex 26.04.2017 23:01
Цитирую iMic:
Предлагаю добавить пометки в статью о том, что это более старая версия модуля, сейчас это не очевидно.

Пометку сделал.
Цитирую iMic:
Во-вторых, предлагаю следующую правку, чтобы не передавать имя программы в обоих параметрах метода Execute(), а также защитить путь к программе от пробелов

Я поправил код, но так, чтобы можно было использовать и первый и второй параметр. И так, чтобы избежать ошибки access violation. Т.е. в соответствии с описанием функции CreateProcess в MSDN. Теперь если вы ставите пустую строку первым или вторым параметром, то в CreateProcess будет передан nil. Сейчас функцию Execute можно вызвать так:

pipeConsole.Execute(
    '',
    'C:\test\html2xhtml.exe --help',
    outputStream, errorStream, processExitCode, processId, nil);
Цитировать
iMic
0 #13 iMic 27.04.2017 11:33
Соответствие MSDN это может и хорошо, но не являлось ли целью создания этого класса сокрытие сложности WinAPI (кроме того Вы внесли правки только для синхронного запуска)?
Можете ли Вы привести пример программы, которая, работает, если ей передавать параметры не вместе с именем, т.е., вроде того:
Цитата:
pipeConsole.Execute( 'C:\test\html2xhtml.exe', '--help', outputStream, errorStream, processExitCode, processId, nil);
Ведь согласно MSDN склейка имени программы и аргументов в один параметр нужна только для Win16 программ?!
Цитировать
Alex
0 #14 Alex 27.04.2017 13:18
Цитирую iMic:
кроме того Вы внесли правки только для синхронного запуска

Поправил и для TPipeConsole.Start.
Цитирую iMic:
Можете ли Вы привести пример программы, которая, работает, если ей передавать параметры не вместе с именем

Если в параметре CommandLine вы передадите только '--help', то консольная программа так CommandLine и прочитает. А, поскольку вначале CommandLine обычно стоит имя файла программы, то она этот параметр может просто не заметить. Вы можете написать простое консольное приложение и оконное, которое его вызывает, и посмотреть, что вы будете получать в консольном приложении в разных ситуациях.
Цитирую iMic:
Ведь согласно MSDN склейка имени программы и аргументов в один параметр нужна только для Win16 программ?!

Для остальных версий такая склейка тоже нужна.
Цитировать
iMic
0 #15 iMic 28.04.2017 11:57
Что-то Execute совсем перeстал работать,
Код SetLength(CommandLine, MAX_PATH) - портит строку параметров, что логично т.к.: SetLength() - Existing characters in the string or elements in the array are preserved, but the content of newly allocated space is undefined.
Цитировать
Alex
0 #16 Alex 28.04.2017 16:50
Цитирую iMic:
Что-то Execute совсем перeстал работать,
Код SetLength(CommandLine, MAX_PATH) - портит строку параметров, что логично т.к.: SetLength() - Existing characters in the string or elements in the array are preserved, but the content of newly allocated space is undefined.

Я уже поменял код в тот же день на другой. Скачайте ещё раз.
Цитировать

Добавить комментарий


Защитный код
Обновить