Рейтинг@Mail.ru

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

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

Рейтинг:  5 / 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 Закачек 3098

Есть вариант юнита 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 Закачек 3466

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

Собственно компонент 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

Комментарии   

Alex
0 #21 Alex 01.07.2017 18:42
Цитирую Bellic:
...Или же все команды можно обработать не применяя Компоненту?
(пардон - я еще мало опыта имею...)

Компонент TPipeConsole нужен для перехвата потока вывода от консольного приложения. Если вы хотите просто запустить приложение и поток вывода вам не нужен, то и этот компонент вам тоже не нужен. Достаточно просто запустить процесс с помощью функции CreateProcess. Параметры, которые вы перечислили выше, можно использовать в обоих случаях.
Цитировать
Bellic
0 #22 Bellic 01.07.2017 19:26
Alex, большое спасибо!!!
Пока ждал ответа - практически полностью написал программу...
Использовал CreateProcess и выводил сообщения из консоли в Мемо через поток в памяти...
За основу взял код от сюда:
sql.ru/.../...
Цитировать

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