1054 lines
38 KiB
ObjectPascal
1054 lines
38 KiB
ObjectPascal
{}{
|
||
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.
|