Гость
Форумы / Delphi [игнор отключен] [закрыт для гостей] / Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки / 19 сообщений из 19, страница 1 из 1
04.10.2021, 11:58
    #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
04.10.2021, 12:03
    #40101751
Кроик Семён
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
мой код в отличие от многих примеров с перехватом вывода не виснет при убитии вызванного процесса, стоило мне дня напряжённого дебага
...
Рейтинг: 0 / 0
04.10.2021, 12:43
    #40101764
Гаджимурадов Рустам
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
Спасибо.

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

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

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


какая версия Delphi?
...
Рейтинг: 0 / 0
04.10.2021, 13:59
    #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
04.10.2021, 14:04
    #40101798
Кроик Семён
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Делюсь: запуск внешней программы + перехватывает её вывода + вкусняшки
_Vasilisk_И какой смысл в callback передавать ProcessId?


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

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

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

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


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

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


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


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

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

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

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


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