1054 lines
38 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{}{
FlushFileBuffers перед tryread (хэндл у нас остался), попробовать с named pipe
PeekNamedPipe(FHandles[ehStdOut], nil, 0, nil, @ToRead, nil);
// если ничего не прочитано и процесс завершился, закрываем трубу
if ToRead = 0 then
begin
if FProcessId = 0 then CloseAndZeroHandle(FHandles[ehStdOut]);
end
else
//
piped command : Launch( [ 'cmd /c dir', 'gzip -9', 'gzip --decompress' ] )
with checking status & exit codes
}
{*******************************************************************************
Эмулятор консоли для запуска консольных приложений
с перенаправлением ввода/вывода.
© Fr0sT, fr0st.brutal@gmail.com
Возможности:
* Как визуальный, так и невизуальный (без Forms) вариант. Для
использования визуального варианта надо объявить дефайн ConEm_VCL
* Запуск любых консольных программ, batch файлов и т.д.
* Получение вывода
* Передача переменных окружения
* Ввод команд после запуска программы
* Запись данных в STDIN запущенной программы
* Отслеживание таймаута неактивности
* Стандартизованный вывод сообщений об ошибках (должна быть поддержка в
запускаемых программах/скриптах)
* Запись выводимых данных в лог файл
* Неблокирующие операции чтения-записи
*******************************************************************************}
unit ConsoleEmulator;
{$mode delphi}
{$codepage UTF8}
interface
uses Classes, Windows, SysUtils, Messages, StrUtils
{$IFDEF ConEm_VCL}
, ExtCtrls, Forms, Graphics, Controls, StdCtrls
{$ENDIF}
{Utils};
{$REGION 'Notes'}
{========= IMPORTANT NOTES ON PIPES =========
SCHEME OF CONSOLE EMULATOR
Usual console app:
STDIN =====> [APP] =====> STDOUT
=====> STDERR
Console app launched with console emulator:
CONS_EM.InputStream =====> [APP] =====> CONS_EM.OuputStream
CONS_EM.DataInput() CONS_EM.OnDataOutput()
DELAYED OUTPUT / PIPE BUFFERING
If you encounter delays in launched command's output, that's the case.
Windows buffers pipe output to internal memory and writes data when the
buffer fills. Compilator-specific IO libs may have their own buffering as
well. Alas, there's nothing we can do with it.
WAIT FOR INPUT / PIPE EOF
Console apps that read input from STDIN continue reading until they encounter
EOF (IOW, ReadFile returns -1). This happens only when the input channel
closes. You can control this manually with CloseInput method or automatically
by setting AutoCloseInput parameter in Launch() method to True. In this case
STDIN will be closed when InputStream reaches its end. There's no means to
reopen input channel after closing, that's why InputStream setting is available
only in Launch().
=============================================}
{$ENDREGION}
type
THandles = (ehStdIn, // STDIN pipe handle
ehStdOut, // STDOUT pipe handle
ehProcess); // Process handle for checking state
TConsEmulState = (cesWaiting, cesRunning, cesFinished, cesTerminated, cesTerminatedByTimeout);
TIODir = (ioInput, ioOutput);
// Class which implements command line execution
TConsoleEmulator = class
strict private
FHWnd: HWND; // owns timer for reading data and tracking timeout
FInputBuf, FOutputBuf: TBytes;
FProcExitCode: Cardinal;
FTimeout: Cardinal; // [sec]
FHandles: array[THandles] of THandle;
FProcessId: THandle;
FState: TConsEmulState;
FLaunchTick, FLastActiveTick: Cardinal;
FCmdLine: string;
FInputStm, FOutputStm: TStream;
FAutoCloseInput: Boolean;
const
BufSize = 16*1024;
EventIDs: array[TIODir] of UINT_PTR = (100, 101);
TimerIntervals: array[TIODir] of UINT = (500, 300);
function WndProc(wnd: HWND; msg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT;
procedure SetState(NewState: TConsEmulState);
procedure ClearHandles;
procedure SetTimer(IODir: TIODir; Enabled: Boolean);
procedure TryWrite;
procedure TryRead;
public
OnDataOutput : procedure(Sender: TConsoleEmulator; Data: PByte; DataLen: Integer) of object;
OnStateChange : procedure(Sender: TConsoleEmulator; State: TConsEmulState; ExitCode: Cardinal) of object;
OnIOError : procedure(Sender: TConsoleEmulator; IODirection: TIODir; ErrCode: Cardinal) of object;
constructor Create(ProcTimeout: Cardinal); reintroduce;
destructor Destroy; override;
procedure Launch(CmdLine: string; CurrDir: string = ''; EnvVars: string = ''; InputStm: TStream = nil; AutoCloseInput: Boolean = False);
procedure Terminate(ByTimeout: Boolean = False);
function DataInput(const Data; DataLen: Integer): Integer;
procedure SendCmd(const Cmd: string);
procedure CloseInput;
property State: TConsEmulState read FState;
property ExitCode: Cardinal read FProcExitCode;
property CmdLine: string read FCmdLine;
property OutputStm: TStream read FOutputStm write FOutputStm;
property InputStm: TStream read FInputStm;
end;
{$IFDEF ConEm_VCL}
// Console form with logging of output, command input, status labels and so on
TfrmConsole = class(TCustomForm)
private
// components
mConsole: TMemo;
eCommand: TEdit;
lblState, lblEmulCallback: TLabel;
// fields
FMemoLineCompl, FThisLineCompl: Boolean;
FConsEmul: TConsoleEmulator;
FLogFile: TFileStream;
FExitCode: Cardinal;
FErrorMsg: string;
// property g/setters
function GetLogFileName: string;
// events
procedure btnSendCmdClick(Sender: TObject);
procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure btnAbortClick(Sender: TObject);
// event handers
procedure ConsEmulOnDataOutput(Sender: TConsoleEmulator; Data: PByte; DataLen: Integer);
procedure ConsEmulOnStateChange(Sender: TConsoleEmulator; State: TConsEmulState; ExitCode: Cardinal);
public
OnProcessFinished: procedure(Sender: TfrmConsole; State: TConsEmulState) of object;
property ExitCode: Cardinal read FExitCode;
property ErrorMsg: string read FErrorMsg;
property LogFileName: string read GetLogFileName;
constructor Create(AOwner: TComponent; ATimeout: Cardinal); reintroduce;
destructor Destroy; override;
procedure Log(Msg: string);
function Launch(const CmdLine: string; LogFN: string = ''; CurrDir: string = ''; EnvVars: string = ''): Boolean;
function IsRunning: Boolean;
end;
{$ENDIF}
const
ErrSignName = 'ConEm_Err'; // имя переменной окружения, содержащей сигнатуру ошибки
ErrSign = '*ERROR*'; // сигнатура ошибки. Если запускаемая программа выводит сообщение
// с этой сигнатурой, это сообщение будет распознано эмулятором
// и занесено в поле ErrorMsg
function Execute(CmdLine: string; CurrDir: string; EnvVars: string;
InputStm: TStream; OutputStm: TStream; Timeout: Cardinal): DWORD;
implementation
uses TlHelp32;
const // localizable
S_AlreadyLaunched = 'Launch: процесс уже запущен!';
S_NotLaunched = 'SendCmd: Процесс не запущен!';
{$IFDEF ConEm_VCL}
S_FormCaption = 'Эмулятор консоли';
S_BtnSendCaption = 'Отправить';
S_BtnAbortCaption = 'Прервать';
S_ConsEmulStateLabels: array[TConsEmulState] of string =
('ожидает', 'запущен', 'завершён', 'остановлен', 'остановлен по таймауту');
S_LblStatePatt = 'Состояние: %s Командная строка: %s';
S_ProcessStatePatt = '<=== %s Процесс %s%s ===>';
S_ExitCodePatt = ' (Код %d%s)';
S_ErrMsgPatt = ', сообщение "%s"';
S_ErrLaunching = '! Ошибка запуска "%s": %s';
{$ENDIF}
// Функция завершает процесс вместе со всеми дочерними
function KillProcessTree(ProcId: DWORD): Integer;
var Snapshot: Cardinal;
PrEntry: PROCESSENTRY32;
hProc: Cardinal;
begin
// получаем слепок
Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if Snapshot = INVALID_HANDLE_VALUE then
begin Result := GetLastError; Exit; end;
// получаем первый процесс
PrEntry.dwSize := SizeOf(PrEntry);
if not Process32First(Snapshot, PrEntry) then
begin Result := GetLastError; CloseHandle(Snapshot); Exit; end;
// убиваем все процессы, порождённые текущим
repeat
if PrEntry.th32ParentProcessID = ProcId then
KillProcessTree(PrEntry.th32ProcessID);
until not Process32Next(Snapshot, PrEntry);
CloseHandle(Snapshot);
// и завершаем исходный процесс
hProc := OpenProcess(PROCESS_TERMINATE, False, ProcId);
if hProc <> 0 then TerminateProcess(hProc, High(DWORD)-1);
Result := GetLastError;
CloseHandle(hProc);
end;
// Execute console application or script
// CmdLine - command to execute
// CurrDir [opt] - current dir for command
// EnvVars [opt] - custom environment variables, "v1=val1;v2=val2;..."
// InputStm [opt] - stream to write to STDIN
// OutputStm [opt] - stream to read from STDOUT
// ? очень странно. если неблок на вывод - ООООчень долго. чтение по мелким кусочкам.
// но если блок и без внутреннего цикла - также чтение по мелким кусочкам
function Execute(CmdLine: string; CurrDir: string; EnvVars: string;
InputStm: TStream; OutputStm: TStream; Timeout: Cardinal): DWORD;
const
BufSize = 16*1024;
MaxLoopTime = 100; // [ms] максимальное время
var // общие для всех процедур переменные
ProcessId: THandle;
InputBuf, OutputBuf: TBytes;
Handles: array[THandles] of THandle;
LastActiveTick: Cardinal;
procedure Launch(CmdLine: string; CurrDir: string; EnvVars: string);
var
si: TStartupInfo;
pi: TProcessInformation;
sa: TSecurityAttributes;
pOldEnv, tmp: PChar;
OldEnvLen, NewEnvLen, err: Integer;
hStdOut, hStdIn: THandle;
mode: DWORD;
IntCmdLine: string;
begin
ZeroMem(si, SizeOf(si));
ZeroMem(pi, SizeOf(pi));
try
// TSecurityAttributes для процесса и труб
ZeroMem(sa, SizeOf(sa));
sa.nLength := SizeOf(sa);
sa.lpSecurityDescriptor := nil;
sa.bInheritHandle := True;
// create pipes
mode := PIPE_READMODE_BYTE or PIPE_NOWAIT;
// STDOUT
if not CreatePipe(Handles[ehStdOut], hStdOut, @sa, 1) then
Error('CreatePipe: '+LastErrMsg);
// Ensure the read handle to the pipe for STDOUT is not inherited (from MSDN example)
SetHandleInformation(Handles[ehStdOut], HANDLE_FLAG_INHERIT, 0);
// Set non-blocking R/W mode for the pipe (!)
// SetNamedPipeHandleState(Handles[ehStdOut], mode, nil, nil);
// STDIN
if not CreatePipe(hStdIn, Handles[ehStdIn], @sa, 1) then
Error('CreatePipe: '+LastErrMsg);
// Ensure the write handle to the pipe for STDIN is not inherited (from MSDN example)
SetHandleInformation(Handles[ehStdIn], HANDLE_FLAG_INHERIT, 0);
// Set non-blocking R/W mode for the pipe (!)
SetNamedPipeHandleState(Handles[ehStdIn], mode, nil, nil);
// заполняем структуры для создания процесса
si.cb := SizeOf(si);
si.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
si.wShowWindow := SW_HIDE;
si.hStdInput := hStdIn;
si.hStdOutput := hStdOut;
si.hStdError := hStdOut;
if CurrDir = '' then CurrDir := GetCurrentDir;
// Конструируем новое окружение из переданной в параметре строки, сразу добавляя
// и сигнатуру сообщения об ошибке. Соответственно EnvVars у нас всегда непуст,
// и проверки if (EnvVars <> '') можно убрать
EnvVars := ErrSignName + '=' + ErrSign + ';' + EnvVars;
if EnvVars[Length(EnvVars)] <> ';' then EnvVars := EnvVars + ';';
EnvVars := StringReplace(EnvVars, ';', #0, [rfReplaceAll]);
NewEnvLen := Length(EnvVars);
// Получаем старые переменные окружения, определяем их длину
// (НЕ включая завершающий второй ноль, чтобы обработать случай пустой строки)
pOldEnv := GetEnvironmentStrings;
if pOldEnv <> nil then
begin
tmp := pOldEnv;
while not ( (tmp^ = #0) and (CharNext(tmp)^ = #0) ) do Inc(tmp, StrLen(tmp)+1);
OldEnvLen := tmp - pOldEnv;
end
else
OldEnvLen := 0;
// Собираем новый список переменных окружения, в конец вручную добавляем второй ноль
SetLength(EnvVars, NewEnvLen + OldEnvLen + 1);
Move(pOldEnv^, EnvVars[NewEnvLen + 1], OldEnvLen*SizeOf(Char));
EnvVars[Length(EnvVars)] := #0;
FreeEnvironmentStrings(pOldEnv);
IntCmdLine := CmdLine; // обеспечиваем изменяемость комстроки - особенности CreateProcessW
UniqueString(IntCmdLine);
if not CreateProcess(nil, PChar(IntCmdLine), @sa, nil, True,
CREATE_NEW_CONSOLE{$IFDEF UNICODE} or CREATE_UNICODE_ENVIRONMENT{$ENDIF},
PChar(EnvVars), PChar(CurrDir), si, pi) then
begin
err := GetLastError;
Error('CreateProcess: '+SysErrorMessage(err)+' ['+IntToStr(err)+'], "'+IntCmdLine+'"');
end;
ProcessId := pi.dwProcessId;
Handles[ehProcess] := pi.hProcess;
finally
// освобождаем хэндлы потока и уже унаследованных концов труб
CloseAndZeroHandle(hStdIn);
CloseAndZeroHandle(hStdOut);
CloseAndZeroHandle(pi.hThread);
end;
end;
procedure TryWrite(InputStm: TStream);
var
InputPtr: PByte;
ToWrite: Integer;
bytes, StartTick: Cardinal;
res: Boolean;
begin
if Handles[ehStdIn] <> 0 then
begin
// copy input data from stream to buffer
ToWrite := InputStm.Read(InputBuf[0], Length(InputBuf));
if ToWrite <= 0 then // nothing to write
begin
CloseAndZeroHandle(Handles[ehStdIn]);
Exit;
end;
InputPtr := @InputBuf[0];
StartTick := GetTickCount;
// write data in a loop
repeat
res := WriteFile(Handles[ehStdIn], InputPtr^, ToWrite, bytes, nil);
if not res then
case GetLastError of
ERROR_NO_DATA, // here: pipe closed
ERROR_BROKEN_PIPE: // pipe closed on the other end
CloseAndZeroHandle(Handles[ehStdIn]);
else // other error - report & close
begin
CloseAndZeroHandle(Handles[ehStdIn]);
Error('TryWrite: '+LastErrMsg);
end;
end; // case
if bytes = 0 then Break;
// if something was read, regardless the error, process it
LastActiveTick := GetTickCount;
Inc(InputPtr, bytes);
Dec(ToWrite, bytes);
if not res then Break; // if WriteFile failed, break
// control loop execution time
if TicksSince(StartTick) > MaxLoopTime then Break;
until False;
// ToWrite is amount of data unwritten so rewind the stream (hoping it supports that!)
if ToWrite > 0 then
InputStm.Seek(-ToWrite, soCurrent);
end; // if
end;
procedure TryRead(OutputStm: TStream);
var
bytes, StartTick: Cardinal;
res: Boolean;
begin
//WriteLnToFile('console.log', FormatDateTime('hh:mm:ss.zzz', now)+ ' << TryRead');
// read data from pipe in a loop
if Handles[ehStdOut] <> 0 then
begin
StartTick := GetTickCount;
repeat
// read data, on error close the {}handle and break the loop
res := ReadFile(Handles[ehStdOut], OutputBuf[0], Length(OutputBuf), bytes, nil);
if not res then
case GetLastError of
ERROR_NO_DATA: // here: pipe is currently empty, that's OK
;
ERROR_BROKEN_PIPE: // pipe closed on the other end
CloseAndZeroHandle(Handles[ehStdOut]);
else // other error - report & close
begin
CloseAndZeroHandle(Handles[ehStdOut]);
Error('TryRead: '+LastErrMsg);
end;
end; // case
if bytes = 0 then Break;
//WriteLnToFile('console.log', FormatDateTime('hh:mm:ss.zzz', now)+ ' read '+itos(bytes));
// if something was read, regardless the error, process it
LastActiveTick := GetTickCount;
OutputStm.Write(OutputBuf[0], bytes);
if not res then Break; // if ReadFile failed, break
// control loop execution time
if TicksSince(StartTick) > MaxLoopTime then Break;
until False;
end; // if
//WriteLnToFile('console.log', FormatDateTime('hh:mm:ss.zzz', now)+ ' TryRead >>');
end;
var h: THandles;
begin
Result := 0;
Launch(CmdLine, CurrDir, EnvVars);
LastActiveTick := GetTickCount;
if InputStm <> nil then
SetLength(InputBuf, BufSize);
SetLength(OutputBuf, BufSize);
// основной цикл
Sleep(200);
repeat
if InputStm <> nil then
TryWrite(InputStm);
TryRead(OutputStm);
if (not GetExitCodeProcess(Handles[ehProcess], Result)) or (Result <> STILL_ACTIVE) then
Break
// если нет - проверяем, не истёк ли таймаут неактивности
else if Timeout <> 0 then
if TicksSince(LastActiveTick) >= Timeout*MSecsPerSec then
begin
KillProcessTree(ProcessId);
Result := 1;
end;
Sleep(200);
until False;
for h := Low(THandles) to High(THandles) do
CloseAndZeroHandle(Handles[h]);
end;
{$REGION 'TConsoleEmulator'}
constructor TConsoleEmulator.Create(ProcTimeout: Cardinal);
begin
inherited Create;
FHWnd := AllocateMsgWnd(WndProc);
if FHWnd = 0 then
Error('AllocateMsgWnd: '+LastErrMsg);
SetTimer(ioOutput, True);
SetLength(FOutputBuf, BufSize);
FTimeout := ProcTimeout;
FState := cesWaiting;
end;
destructor TConsoleEmulator.Destroy;
begin
Terminate;
DestroyWindow(FHWnd);
inherited;
end;
// Меняем состояние и вызываем обработчик этого события, если он присвоен
procedure TConsoleEmulator.SetState(NewState: TConsEmulState);
begin
if FState = NewState then Exit;
FState := NewState;
if Assigned(OnStateChange) then
OnStateChange(Self, FState, FProcExitCode);
// если процесс так или иначе завершился - отключаем таймеры, меняем состояние на ожидающее
if FState in [cesFinished, cesTerminated, cesTerminatedByTimeout] then
begin
SetTimer(ioInput, False);
SetTimer(ioOutput, False);
SetState(cesWaiting);
end;
end;
procedure TConsoleEmulator.SetTimer(IODir: TIODir; Enabled: Boolean);
begin
if not Enabled then
KillTimer(FHWnd, EventIDs[IODir])
else
if Windows.SetTimer(FHWnd, EventIDs[IODir], TimerIntervals[IODir], nil) = 0 then
Error('SetTimer: '+LastErrMsg);
end;
// закрываем и обнуляем все хэндлы, за исключением Id процесса (Id просто обнуляем - это не хэндл)
procedure TConsoleEmulator.ClearHandles;
var h: THandles;
begin
for h := Low(THandles) to High(THandles) do
CloseAndZeroHandle(FHandles[h]);
FProcessId := 0;
end;
// завершаем процесс и обнуляем все хэндлы
procedure TConsoleEmulator.Terminate(ByTimeout: Boolean);
begin
if FProcessId <> 0 then
begin
// if the input pipe is open, try closing it, child process will probably flush its STDOUT
if ByTimeout then
if FHandles[ehStdIn] <> 0 then
begin
CloseInput;
Sleep(100); // wait some time to flush
TryRead; // read the data
end;
KillProcessTree(FProcessId);
FProcExitCode := 0;
if ByTimeout
then SetState(cesTerminatedByTimeout)
else SetState(cesTerminated);
end;
ClearHandles;
end;
// Execute console application or script
// CmdLine - command to execute
// CurrDir [opt] - current dir for command
// EnvVars [opt] - custom environment variables, "v1=val1;v2=val2;..."
// InputStm [opt] - stream to write to STDIN
// AutoCloseInput [opt] - close STDIN pipe when InputStm reaches the end. See Notes#WAIT FOR INPUT
procedure TConsoleEmulator.Launch(CmdLine: string; CurrDir: string; EnvVars: string;
InputStm: TStream; AutoCloseInput: Boolean);
var si: TStartupInfo;
pi: TProcessInformation;
sa: TSecurityAttributes;
pOldEnv, tmp: PChar;
OldEnvLen, NewEnvLen, err: Integer;
hStdOut, hStdIn: THandle;
mode: DWORD;
begin
if FState = cesRunning then Error(S_AlreadyLaunched);
FProcExitCode := 0;
// обнуляем все переменные, чтобы в finally их все скопом закрыть
ClearHandles;
hStdOut := 0; hStdIn := 0;
ZeroMem(si, SizeOf(si));
ZeroMem(pi, SizeOf(pi));
try try
// TSecurityAttributes для процесса и труб
ZeroMem(sa, SizeOf(sa));
sa.nLength := SizeOf(sa);
sa.lpSecurityDescriptor := nil;
sa.bInheritHandle := True;
// create pipes
mode := PIPE_READMODE_BYTE or PIPE_NOWAIT;
// STDOUT
if not CreatePipe(FHandles[ehStdOut], hStdOut, @sa, 1) then
Error('CreatePipe: '+LastErrMsg);
// Ensure the read handle to the pipe for STDOUT is not inherited (from MSDN example)
SetHandleInformation(FHandles[ehStdOut], HANDLE_FLAG_INHERIT, 0);
// Set non-blocking R/W mode for the pipe (!)
SetNamedPipeHandleState(FHandles[ehStdOut], mode, nil, nil);
// STDIN
if not CreatePipe(hStdIn, FHandles[ehStdIn], @sa, 1) then
Error('CreatePipe: '+LastErrMsg);
// Ensure the write handle to the pipe for STDIN is not inherited (from MSDN example)
SetHandleInformation(FHandles[ehStdIn], HANDLE_FLAG_INHERIT, 0);
// Set non-blocking R/W mode for the pipe (!)
SetNamedPipeHandleState(FHandles[ehStdIn], mode, nil, nil);
// заполняем структуры для создания процесса
si.cb := SizeOf(si);
si.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
si.wShowWindow := SW_HIDE;
si.hStdInput := hStdIn;
si.hStdOutput := hStdOut;
si.hStdError := hStdOut;
if CurrDir = '' then CurrDir := GetCurrentDir;
// Конструируем новое окружение из переданной в параметре строки, сразу добавляя
// и сигнатуру сообщения об ошибке. Соответственно EnvVars у нас всегда непуст,
// и проверки if (EnvVars <> '') можно убрать
EnvVars := ErrSignName + '=' + ErrSign + ';' + EnvVars;
if EnvVars[Length(EnvVars)] <> ';' then EnvVars := EnvVars + ';';
EnvVars := StringReplace(EnvVars, ';', #0, [rfReplaceAll]);
NewEnvLen := Length(EnvVars);
// Получаем старые переменные окружения, определяем их длину
// (НЕ включая завершающий второй ноль, чтобы обработать случай пустой строки)
pOldEnv := GetEnvironmentStrings;
if pOldEnv <> nil then
begin
tmp := pOldEnv;
while not ( (tmp^ = #0) and (CharNext(tmp)^ = #0) ) do Inc(tmp, StrLen(tmp)+1);
OldEnvLen := tmp - pOldEnv;
end
else
OldEnvLen := 0;
// Собираем новый список переменных окружения, в конец вручную добавляем второй ноль
SetLength(EnvVars, NewEnvLen + OldEnvLen + 1);
Move(pOldEnv^, EnvVars[NewEnvLen + 1], OldEnvLen*SizeOf(Char));
EnvVars[Length(EnvVars)] := #0;
FreeEnvironmentStrings(pOldEnv);
FCmdLine := CmdLine; // обеспечиваем изменяемость комстроки - особенности CreateProcessW
UniqueString(FCmdLine);
if not CreateProcess(nil, PChar(FCmdLine), @sa, nil, True,
CREATE_NEW_CONSOLE{$IFDEF UNICODE} or CREATE_UNICODE_ENVIRONMENT{$ENDIF},
PChar(EnvVars), PChar(CurrDir), si, pi) then
begin
err := GetLastError;
Error('CreateProcess: '+SysErrorMessage(err)+' ['+IntToStr(err)+'], "'+CmdLine+'"');
end;
FProcessId := pi.dwProcessId;
FHandles[ehProcess] := pi.hProcess;
// присваиваем различные внутренние поля, запускаем таймеры и меняем состояние
FLaunchTick := GetTickCount; FLastActiveTick := GetTickCount;
SetTimer(ioOutput, True);
// set input stream and activate input timer
FInputStm := InputStm;
fAutoCloseInput := AutoCloseInput;
if FInputStm <> nil then
begin
SetTimer(ioInput, True);
SetLength(FInputBuf, BufSize);
end
else
SetLength(FInputBuf, 0);
SetState(cesRunning);
except
on E: Exception do
begin
Terminate;
raise;
end;
end;
finally
// освобождаем хэндлы потока и уже унаследованных концов труб
CloseAndZeroHandle(hStdIn);
CloseAndZeroHandle(hStdOut);
CloseAndZeroHandle(pi.hThread);
end;
end;
procedure TConsoleEmulator.CloseInput;
begin
CloseAndZeroHandle(FHandles[ehStdIn]);
end;
procedure TConsoleEmulator.TryRead;
var
bytes, StartTick: Cardinal;
res: Boolean;
begin
// read data from pipe in a loop
if FHandles[ehStdOut] <> 0 then
begin
StartTick := GetTickCount;
repeat
// read data, on error close the {}handle and break the loop
res := ReadFile(FHandles[ehStdOut], FOutputBuf[0], Length(FOutputBuf), bytes, nil);
if not res then
case GetLastError of
ERROR_NO_DATA: // here: pipe is currently empty, that's OK
;
ERROR_BROKEN_PIPE: // pipe closed on the other end
CloseAndZeroHandle(FHandles[ehStdOut]);
else // other error - report & close
begin
if Assigned(OnIOError) then
OnIOError(Self, ioOutput, GetLastError);
CloseAndZeroHandle(FHandles[ehStdOut]);
end;
end; // case
if bytes = 0 then Break;
// if something was read, regardless the error, process it
FLastActiveTick := GetTickCount;
if Assigned(OnDataOutput) then OnDataOutput(Self, @FOutputBuf[0], bytes);
if Assigned(FOutputStm) then FOutputStm.Write(FOutputBuf[0], bytes);
//debug(timetostr(now)+' '+'read '+itos(bytes));
if not res then Break; // if ReadFile failed, break
// control loop execution time
if TicksSince(StartTick) > TimerIntervals[ioOutput] div 2 then Break;
until False;
end;
end;
procedure TConsoleEmulator.TryWrite;
var
InputPtr: PByte;
ToWrite: Integer;
bytes, StartTick: Cardinal;
res: Boolean;
begin
if FProcessId = 0 then Exit;
if FInputStm = nil then Exit;
if FHandles[ehStdIn] <> 0 then
begin
// copy input data from stream to buffer
ToWrite := FInputStm.Read(FInputBuf[0], Length(FInputBuf));
if ToWrite <= 0 then // nothing to write
begin
if FAutoCloseInput then
begin
CloseInput;
SetTimer(ioInput, False);
end;
Exit;
end;
InputPtr := @FInputBuf[0];
StartTick := GetTickCount;
// write data in a loop
repeat
res := WriteFile(FHandles[ehStdIn], InputPtr^, ToWrite, bytes, nil);
if not res then
case GetLastError of
ERROR_NO_DATA, // here: pipe closed
ERROR_BROKEN_PIPE: // pipe closed on the other end
CloseAndZeroHandle(FHandles[ehStdIn]);
else // other error - report & close
begin
if Assigned(OnIOError) then
OnIOError(Self, ioInput, GetLastError);
CloseAndZeroHandle(FHandles[ehStdIn]);
end;
end; // case
if bytes = 0 then Break;
// if something was read, regardless the error, process it
FLastActiveTick := GetTickCount;
Inc(InputPtr, bytes);
Dec(ToWrite, bytes);
if not res then Break; // if WriteFile failed, break
// control loop execution time
if TicksSince(StartTick) > TimerIntervals[ioInput] div 2 then Break;
until False;
// ToWrite is amount of data unwritten so rewind the stream (hoping it supports that!)
if ToWrite > 0 then
FInputStm.Seek(-ToWrite, soCurrent);
end;
//debug(timetostr(now)+' '+itos(FInputStm.Position));
end;
// по таймеру периодически проверять состояние запущенного процесса
// и считывать выводимую в трубу информацию
function TConsoleEmulator.WndProc(wnd: HWND; msg: UINT; wPar: WPARAM; lPar: LPARAM): LRESULT;
begin
Result := 0;
if (msg = WM_TIMER) and (FState = cesRunning) then
begin
if wPar = EventIDs[ioInput] then // write data from stream (if assigned) to the ehProcess input pipe
begin
TryWrite;
end
else if wPar = EventIDs[ioOutput] then // read data from ehProcess
begin
TryRead;
// проверяем, не завершился ли ещё процесс, и если так, то меняем состояние
if FProcessId = 0 then Exit;
if (not GetExitCodeProcess(FHandles[ehProcess], FProcExitCode)) or (FProcExitCode <> STILL_ACTIVE) then
begin ClearHandles; SetState(cesFinished); end
// если нет - проверяем, не истёк ли таймаут неактивности
else if FTimeout <> 0 then
if TicksSince(FLastActiveTick) >= FTimeout*MSecsPerSec then
Terminate(True);
end // Output
else Exit;
end // msg = WM_TIMER
else
Result := DefWindowProc(wnd, msg, wPar, lPar);
end;
// Write the data to input pipe
function TConsoleEmulator.DataInput(const Data; DataLen: Integer): Integer;
var bytes: Cardinal;
begin
if not WriteFile(FHandles[ehStdIn], Data, DataLen, bytes, nil) then
Result := -1
else
Result := bytes;
end;
// посылка команды запущенному процессу через перехваченную трубу
procedure TConsoleEmulator.SendCmd(const Cmd: string);
var bytes : Integer;
curr : Integer;
CmdToSend: AnsiString;
begin
// выполнять, только если все нужные хэндлы ненулевые
if (FProcessId = 0) or (FHandles[ehStdIn] = 0) or (FHandles[ehStdOut] = 0) or
(FState <> cesRunning) then Error(S_NotLaunched);
CmdToSend := AnsiString(Cmd) + NL; // признак конца команды, иначе будет ждать
curr := 1;
while curr <= Length(CmdToSend) do
begin
bytes := DataInput(CmdToSend[curr], StrSize(CmdToSend)-curr+1);
if bytes = -1 then Error('WriteFile: '+LastErrMsg);
if bytes > 0 then Inc(curr, bytes) else Break; {}// repeat
end;
end;
{$ENDREGION}
{$REGION 'TfrmConsole'}
{$IFDEF ConEm_VCL}
const MaxMemoLines = 500;
constructor TfrmConsole.Create(AOwner: TComponent; ATimeout: Cardinal);
begin
CreateNew(AOwner);
// init внутренние поля
FMemoLineCompl := True;
FThisLineCompl := False;
FConsEmul := TConsoleEmulator.Create(ATimeout);
FConsEmul.OnDataOutput := ConsEmulOnDataOutput;
FConsEmul.OnStateChange := ConsEmulOnStateChange;
// init себя
Caption := S_FormCaption;
ClientHeight := 370;
ClientWidth := 730;
Font.Size := 9;
Font.Name := 'Tahoma';
KeyPreview := True;
Padding.Left := 5;
Padding.Top := 5;
Padding.Right := 5;
Padding.Bottom := 5;
Position := poDesktopCenter;
OnKeyUp := FormKeyUp;
// init контролы
with TLabel.Create(Self) do
begin;
Parent := Self;
SetBounds(8, 361, 17, 25);
Anchors := [akLeft, akBottom];
Caption := '>';
Font.Size := 16;
Font.Name := 'Tahoma';
Font.Style := [fsBold];
end;
lblState := TLabel.Create(Self);
with lblState do
begin
Parent := Self;
SetBounds(8, 340, 232, 14);
Anchors := [akLeft, akBottom];
AutoSize := True;
end;
lblEmulCallback := TLabel.Create(Self);
with lblEmulCallback do
begin
Parent := Self;
top:=0;
Align := alTop;
AlignWithMargins := True;
Margins.SetBounds(5,5,5,5);
Caption := '';
end;
eCommand := TEdit.Create(Self);
with eCommand do
begin
Parent := Self;
SetBounds(31, 366, 500, 22);
Anchors := [akLeft, akRight, akBottom];
end;
with TButton.Create(Self) do
begin
Parent := Self;
SetBounds(545, 361, 95, 32);
Anchors := [akRight, akBottom];
Caption := S_BtnSendCaption;
Default := True;
OnClick := btnSendCmdClick;
end;
with TButton.Create(Self) do
begin
Parent := Self;
SetBounds(645, 361, 90, 32);
Anchors := [akRight, akBottom];
Caption := S_BtnAbortCaption;
OnClick := btnAbortClick;
end;
mConsole := TMemo.Create(Self);
with mConsole do
begin
Parent := Self;
Height := 305;
Align := alTop;
Anchors := [akLeft, akTop, akRight, akBottom];
Color := clCream;
Font.Size := 10;
Font.Name := 'Courier';
ReadOnly := True;
ScrollBars := ssVertical;
WordWrap := False;
end;
end;
destructor TfrmConsole.Destroy;
begin
FreeAndNil(FConsEmul);
FreeAndNil(FLogFile);
inherited;
end;
function TfrmConsole.GetLogFileName: string;
begin
if FLogFile = nil then Result := '' else Result := FLogFile.FileName;
end;
procedure TfrmConsole.btnAbortClick(Sender: TObject);
begin
FConsEmul.Terminate;
end;
procedure TfrmConsole.btnSendCmdClick(Sender: TObject);
begin
FConsEmul.SendCmd(eCommand.Text);
eCommand.SelectAll;
end;
// Поступили данные в консоль - пишем их в мемо и в лог-файл
procedure TfrmConsole.ConsEmulOnDataOutput(Sender: TConsoleEmulator; Data: PByte; DataLen: Integer);
var tmp: String;
pData, pNl: PAnsiChar;
begin
try
pData := PAnsiChar(Data);
OemToAnsiBuff(pData, pData, DataLen);
if FLogFile <> nil then
FLogFile.WriteBuffer(pData^, DataLen);
mConsole.Lines.BeginUpdate;
// если строк в мемо больше максимума, удаляем их
if mConsole.Lines.Count > MaxMemoLines + 50 then
while mConsole.Lines.Count > MaxMemoLines do mConsole.Lines.Delete(0);
// цикл, пока не исчерпаем все данные
while DataLen > 0 do
begin
// пропускаем все #10
while (DataLen > 0) and (pData^ = #10) do
begin Inc(pData); Dec(DataLen); end;
// ищем #13, если не нашли - присваиваем ему указатель на конец строки
pNl := AnsiStrScan(pData, #13);
FThisLineCompl := pNl <> nil;
if pNl = nil then pNl := PAnsiChar(pData+DataLen);
// получаем строку и в зависимости от флага прибавляем к существующей строке
// в мемо либо добавляем новую строку
tmp := string(Copy(pData, 1, pNl-pData));
with mConsole.Lines do
if FMemoLineCompl
then Add(tmp)
else Strings[Count-1] := Strings[Count-1] + tmp;
FMemoLineCompl := FThisLineCompl;
Dec(DataLen, pNl-pData+1);
Inc(pData, pNl-pData+1);
if StrIsStartingFrom(tmp,ErrSign) then
FErrorMsg := tmp;
end;
finally
mConsole.Lines.EndUpdate;
SendMessage(mConsole.{}handle, WM_VSCROLL, SB_BOTTOM, 0);
end;
end;
// Процесс завершён - сообщаем об этом
procedure TfrmConsole.ConsEmulOnStateChange(Sender: TConsoleEmulator; State: TConsEmulState; ExitCode: Cardinal);
var ExitStr: string;
begin
lblState.Caption := Format(S_LblStatePatt, [S_ConsEmulStateLabels[State], Sender.CmdLine]);
case State of
cesWaiting:
begin
FExitCode := 0; FErrorMsg := '';
end;
cesRunning:
begin
Log(Format(S_ProcessStatePatt, [DateTimeToStr(Now), Sender.CmdLine, ' ' + S_ConsEmulStateLabels[State]]));
FExitCode := 0; FErrorMsg := '';
end;
cesFinished, cesTerminated, cesTerminatedByTimeout:
begin
FExitCode := ExitCode;
if FErrorMsg <> ''
then ExitStr := Format(S_ErrMsgPatt, [FErrorMsg])
else ExitStr := '';
// код процесса имеет смысл только при нормальном завершении
if State = cesFinished then
ExitStr := Format(S_ExitCodePatt, [FExitCode, ExitStr]);
Log(Format(S_ProcessStatePatt, [DateTimeToStr(Now), '', S_ConsEmulStateLabels[State] + ExitStr]));
lblState.Caption := lblState.Caption + ExitStr;
if Assigned(OnProcessFinished) then OnProcessFinished(Self, State);
end;
end;
end;
// Скрытие формы по Escape
procedure TfrmConsole.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
if Key = VK_ESCAPE then Close;
end;
// Запуск приложения в консоли
function TfrmConsole.Launch(const CmdLine: string; LogFN: string; CurrDir: string; EnvVars: string): Boolean;
begin
Result := False;
try
// если лог-файл другой, закрываем его и создаем заново
if LogFN = ''
then FLogFile := nil
else
begin
FLogFile := TFileStream.Create(LogFN, fmCreate or fmOpenWrite);
FLogFile.Seek( 0, soEnd);
end;
FConsEmul.Launch(CmdLine, CurrDir, EnvVars);
Result := True;
except
on E: Exception do begin
Log(Format(S_ErrLaunching, [CmdLine, E.Message]));
lblState.Caption := Format(S_LblStatePatt, [S_ConsEmulStateLabels[FConsEmul.State], E.Message]);
FErrorMsg := E.Message;
end;
end;
end;
// Запись строки в мемо и в файл
procedure TfrmConsole.Log(Msg: string);
var MsgAnsi: AnsiString;
begin
// добавляем перевод строки, чтобы не склеивались
if LeftStr(Msg, 2) <> NL then
Msg := Msg + NL;
mConsole.Lines.Add(Msg);
MsgAnsi := AnsiString(NL+Msg);
if FLogFile <> nil then
FLogFile.WriteBuffer(MsgAnsi[ 1], Length(MsgAnsi));
end;
function TfrmConsole.IsRunning: Boolean;
begin
Result := (FConsEmul.State=cesRunning);
end;
{$ENDIF}
{$ENDREGION}
end.