powered by simpleCommunicator - 2.0.37     © 2025 Programmizd 02
Форумы / Delphi [игнор отключен] [закрыт для гостей] / Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
19 сообщений из 19, страница 1 из 1
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101745
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Приветствую!

Хочу поделиться процедурой для выполнения внешних программ с ожиданием их завершения (или прерыванием выполнения из callback-функции) и получением вывода, что полезно для логирования

юнит
писал и тестировал на Delphi 6
UNIT AppExecUtils
Код: pascal
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
//Delphi 6

UNIT AppExecUtils;

INTERFACE
uses Windows, Classes;

type
   TAppExecutionOutputEvent = procedure(const ACmdStr: string;
                                        AProcessID: DWORD;
                                        const AOutputTextPart: string;
                                        const AAnyID: string;
                                        AActDurationMSec: DWORD;
                                        var ADoTerminating: boolean) of object;


procedure ExecAppAndGetOutput(const ACmdStr: string;  
                              AOutput: TStrings;  //опционально: сразу вывод в TStrings
                              AOutputEvent: TAppExecutionOutputEvent; //опционально: обработчик события вывода с вкусняшками
                              const AAnyID: string); //опционально: строка, передаваемая в AOutputEvent 

IMPLEMENTATION
uses SysUtils;

//совет нашёл здесь  https://stackoverflow.com/questions/13816962
//для предотвращения зависания ReadFile(PipeHandle) 
//это происходит, например, при убитии процесса
function CheckIfPipeHasData(APipeHandle: THandle; AOnlyUnreadData: boolean): boolean;
const
   READ_BUFFER_SIZE  = 10;
var
   arrBuffer         : array[0..READ_BUFFER_SIZE-1] of AnsiChar;
   iBytesRead        : DWORD;
   iBytesAvailable   : DWORD;
   iBytesUnread      : DWORD;
begin
   //эта функция не зависнет, это важно. В отличие от ReadFile
   if PeekNamedPipe(APipeHandle,          // HANDLE hNamedPipe = handle to pipe to copy from
                    @arrBuffer,           // LPVOID lpBuffer = pointer to data buffer
                    READ_BUFFER_SIZE,     // DWORD nBufferSize = size, in bytes, of data buffer
                    @iBytesRead,          // LPDWORD lpBytesRead = pointer to number of bytes read
                    @iBytesAvailable,     // LPDWORD lpTotalBytesAvail = pointer to total number of bytes available
                    @iBytesUnread         // LPDWORD lpBytesLeftThisMessage = pointer to unread bytes in this message
                   ) then
   begin
      if AOnlyUnreadData then
         Result := (iBytesUnread > 0)
      else
         Result := (iBytesAvailable > 0);
   end
   else
   begin
      Result := false;
   end;
end;
//------------------------------------------------------------------------------
//базируется на RunDosInMemo() с https://stackoverflow.com/questions/9119999
//с моей доработкой
procedure ExecAppAndGetOutput(const ACmdStr: string; AOutput: TStrings;
  AOutputEvent: TAppExecutionOutputEvent; const AAnyID: string);
const
   READ_BUFFER_SIZE     = 2400;
var
   iExecutionStartMSec  : DWORD;
   sCmdStr              : string;
   Security             : TSecurityAttributes;
   readableEndOfPipe    : THandle;
   writeableEndOfPipe   : THandle;
   start                : TStartUpInfo;
   Buffer               : PAnsiChar;
   ProcessInfo          : TProcessInformation;
   iAppRunningFlag      : DWORD;
   bDoTerminating       : boolean;

   (**) procedure PushOutput(out ADoTerminating: boolean);
   (**) var
   (**)    iBytesRead : DWORD;
   (**) begin
   (**)    ADoTerminating := false;
   (**)    //Read the contents of the pipe out of the readable end
   (**)    //WARNING: if the console app never writes anything to the StdOutput, then ReadFile will block and never return
   (**)    repeat
   (**)       iBytesRead := 0;
   (**)       if CheckIfPipeHasData(readableEndOfPipe, true) then //<<< эта проверка предотвращает зависание (моя находка)
   (**)       begin
   (**)          ReadFile(readableEndOfPipe, Buffer[0], READ_BUFFER_SIZE, {var}iBytesRead, nil);
   (**)          Buffer[iBytesRead]:= #0;
   (**)          OemToAnsi(Buffer,Buffer);
   (**)       end
   (**)       else
   (**)       begin
   (**)          iBytesRead := 0;
   (**)          Buffer[iBytesRead]:= #0;
   (**)       end;
   (**)
   (**)       if Assigned(AOutput) then
   (**)          AOutput.Text := AOutput.Text + String(Buffer);
   (**)
   (**)       if Assigned(AOutputEvent) then
   (**)          AOutputEvent(ACmdStr,
   (**)                       ProcessInfo.dwProcessId,
   (**)                       String(Buffer),
   (**)                       AAnyID,
   (**)                       GetTickCount()-iExecutionStartMSec,
   (**)                       ADoTerminating);
   (**)    until ADoTerminating or (iBytesRead < READ_BUFFER_SIZE);
   (**) end;
begin
   iExecutionStartMSec := GetTickCount(); 

   sCmdStr := ACmdStr;

   Security.nLength := SizeOf(TSecurityAttributes);
   Security.bInheritHandle := True;
   Security.lpSecurityDescriptor := nil;

   if CreatePipe({var}readableEndOfPipe, {var}writeableEndOfPipe, @Security, 0) then
   begin
      Buffer := AllocMem(READ_BUFFER_SIZE+1);
      try
         FillChar(Start, SizeOf(start), #0);
         start.cb := SizeOf(start);

         // Set up members of the STARTUPINFO structure.
         // This structure specifies the STDIN and STDOUT handles for redirection.
         // - Redirect the output and error to the writeable end of our pipe.
         // - We must still supply a valid StdInput handle (because we used STARTF_USESTDHANDLES to swear that all three handles will be valid)
         start.dwFlags := start.dwFlags or STARTF_USESTDHANDLES;
         start.hStdInput := GetStdHandle(STD_INPUT_HANDLE); //we're not redirecting stdInput; but we still have to give it a valid handle
         start.hStdOutput := writeableEndOfPipe; //we give the writeable end of the pipe to the child process; we read from the readable end
         start.hStdError := writeableEndOfPipe;

         //We can also choose to say that the wShowWindow member contains a value.
         //In our case we want to force the console window to be hidden.
         start.dwFlags := start.dwFlags + STARTF_USESHOWWINDOW;
         start.wShowWindow := SW_HIDE;

         // Don't forget to set up members of the PROCESS_INFORMATION structure.
         //--- ProcessInfo := Default(TProcessInformation);
         FillChar(ProcessInfo, SizeOf(ProcessInfo), 0);


         //WARNING: The unicode version of CreateProcess (CreateProcessW) can modify the command-line "DosApp" string.
         //Therefore "DosApp" cannot be a pointer to read-only memory, or an ACCESS_VIOLATION will occur.
         //We can ensure it's not read-only with the RTL function: UniqueString
         UniqueString({var}sCmdStr);

         try
            if CreateProcess(nil, PChar(sCmdStr), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, start, {var}ProcessInfo) then
            begin
               bDoTerminating := false;

               //Wait for the application to terminate, as it writes it's output to the pipe.
               //WARNING: If the console app outputs more than 2400 bytes (ReadBuffer),
               //it will block on writing to the pipe and *never* close.
               repeat
                  iAppRunningFlag := WaitForSingleObject(ProcessInfo.hProcess, 100);

                  if iAppRunningFlag = WAIT_TIMEOUT then
                     PushOutput(bDoTerminating);

               until bDoTerminating or (iAppRunningFlag <> WAIT_TIMEOUT);

               //под конец считываем что там ещё осталось в трубке
               if not bDoTerminating then
                  PushOutput(bDoTerminating);
            end;
         finally
            if bDoTerminating then
               TerminateProcess(ProcessInfo.hProcess, 0)
            else
            begin
               CloseHandle(ProcessInfo.hProcess);
               CloseHandle(ProcessInfo.hThread);
            end;

            CloseHandle(readableEndOfPipe);
            CloseHandle(writeableEndOfPipe);
         end;
      finally
         FreeMem(Buffer);
      end;
   end; //if CreatePipe
end;
//------------------------------------------------------------------------------
END.




пример использования

Код: pascal
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
    procedure OutputMsgToMemo(const AMsg: string);

    procedure OnExecutionOutput(const ACmdStr: string;
                                AProcessID: DWORD;
                                const AOutputTextPart: string;
                                const AAnyID: string;
                                AActDurationMSec: DWORD;
                                var ADoTerminating: boolean);
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

IMPLEMENTATION
uses AppExecUtils;
{$R *.dfm}


//------------------------------------------------------------------------------
procedure TForm1.OutputMsgToMemo(const AMsg: string);
var
   sTrimedMsg  : string;
   sLine       : string;
begin
   sTrimedMsg := TrimRight(AMsg);
   sLine := FormatDateTime('dd.mm.yyyy hh:nn:ss',Now())+'  '+sTrimedMsg;

   Memo1.Lines.Add(sLine);
end;
//------------------------------------------------------------------------------
procedure TForm1.OnExecutionOutput(const ACmdStr: string; AProcessID: DWORD;
  const AOutputTextPart, AAnyID: string; AActDurationMSec: DWORD;
  var ADoTerminating: boolean);
begin
   //вывод в мемку
   if AOutputTextPart<>'' then
      OutputMsgToMemo(AOutputTextPart);

   Application.ProcessMessages();  // <<< хотя и без него почему-то работает

   //прерывание через 20 секунд
   if (AAnyID='PING_TO_TERMINATE') and (AActDurationMSec>20*1000) then
   begin
      ADoTerminating := true;
      OutputMsgToMemo('TERMINATING AFTER '+IntToStr(Round(AActDurationMSec/1000))+' Sec.');
   end;
end;
//------------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
begin
   //пример прерывания через 20 секунд
   //Сигнал о прерывании выполнения исходит из события OnExecutionOutput
   ExecAppAndGetOutput('ping google.de /t',  // ACmdStr: string
                       nil,                  // AOutput: TStrings
                       OnExecutionOutput,    // AOutputEvent: TAppExecutionOutputEvent;
                       'PING_TO_TERMINATE'); // AAnyID: string);

   //пример ожидания самостоятельного завершения
   ExecAppAndGetOutput('ping google.de',     // ACmdStr: string
                       nil,                  // AOutput: TStrings
                       OnExecutionOutput,    // AOutputEvent: TAppExecutionOutputEvent;
                       'SIMPLE_PING');       // AAnyID: string);
end;
//------------------------------------------------------------------------------
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101751
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
мой код в отличие от многих примеров с перехватом вывода не виснет при убитии вызванного процесса, стоило мне дня напряжённого дебага
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101764
Гаджимурадов Рустам
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Спасибо.

Надо было ещё многопоточность добавить. :)
Posted via ActualForum NNTP Server 1.5
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101768
Softologic
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Гаджимурадов Рустам
Спасибо.

Надо было ещё многопоточность добавить. :)

+1
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101775
Softologic
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Кроик Семён, проверил - все работает как надо, респект!
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101792
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Softologic
Кроик Семён, проверил - все работает как надо, респект!


какая версия Delphi?
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101795
Фотография _Vasilisk_
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Кроик Семён
Код: pascal
1.
2.
3.
4.
5.
6.
7.
            if bDoTerminating then
               TerminateProcess(ProcessInfo.hProcess, 0)
            else
            begin
               CloseHandle(ProcessInfo.hProcess);
               CloseHandle(ProcessInfo.hThread);
            end;

else здесь лишнее. И какой смысл в callback передавать ProcessId?
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101798
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
_Vasilisk_И какой смысл в callback передавать ProcessId?


по ProcessID можно узнать, например, путь к EXE-файлу, если был запущен без указания абсолютного пути и всякое другое, мало ли
Изначально думал организовать "прибитие" процесса в callback-функции по таймаут, но потом передумал и добавил "var ADoTerminating: boolean"
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101805
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
P.S.

насчёт "else" - сижу и не знаю, зачем это было написано. В пятницу работал "в потоке", торопясь успеть до выходных
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101810
rgreat
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
А вдруг TerminateProcess внезапно не сможет отработать?!!
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101813
Фотография ну я
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
rgreat
А вдруг TerminateProcess внезапно не сможет отработать?!!

На этот случай ставят второй TerminateProcess.
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101814
Мимопроходящий
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
04.10.2021 14:48, ну я пишет:
> На этот случай ставят второй TerminateProcess.

не.
делают цикл.
бесконечный.
Posted via ActualForum NNTP Server 1.5
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101825
DmSer
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Жалко было на CreateProcess отдельный try..finally ?
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101845
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
DmSer
Жалко было на CreateProcess отдельный try..finally ?


А разве Windows-API - функции могут вываливаться с Exception?
Они же для Си-кода спроектированы , возвращают статус / и код ошибки отдельной функцией
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101918
Фотография _Vasilisk_
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Кроик Семён
А разве Windows-API - функции могут вываливаться с Exception?
Не могут. Но вот AOutputEvent исключение может поднять легко
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101925
shalamyansky
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Вы StdOutput и StdError в один поток выводите? ИМХО, не очень эстетично. Хотя, конечно, StdError мало кто использует, но вдруг.

Полезно еще код возврата отдавать. GetExitCodeProcess();
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101957
DmSer
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
Кроик Семён
DmSer
Жалко было на CreateProcess отдельный try..finally ?


А разве Windows-API - функции могут вываливаться с Exception?
Они же для Си-кода спроектированы , возвращают статус / и код ошибки отдельной функцией


Я не про exception. Просто немного режут глаз вызовы CloseHandle(0)
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40101964
Фотография Кроик Семён
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Участник
shalamyansky,

код возврата ... может и нужен, я подумаю. За подсказку с GetExitCodeProcess отдельное спасибо
...
Рейтинг: 0 / 0
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
    #40102008
Fr0sT-Brutal
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Гость
Кроик Семён
shalamyansky,

код возврата ... может и нужен, я подумаю. За подсказку с GetExitCodeProcess отдельное спасибо

Конечно нужен, без него мало смысла.
...
Рейтинг: 0 / 0
19 сообщений из 19, страница 1 из 1
Форумы / Delphi [игнор отключен] [закрыт для гостей] / Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
Целевая тема:
Создать новую тему:
Автор:
Закрыть
Цитировать
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]