Program hangs/Issue with WaitForSingleObject/CreateProcess in Delphi - delphi

I am executing an executable written in Go from Delphi (which downloads files from a URL list) and am capturing its console output in a TMemo on a Delphi form.
The last two lines in Go's main function are:
fmt.Println(fmt.Sprintf("Requested %d URLs in %f seconds", uc-1, duration))
os.Exit(0)
This line does appear in Delphi's memo, so I assume that the Go executable cleanly exits with a code of 0. I need to resolve two issues with my code:
After Go has issued a few thousand HTTP GETs (it outputs requested URLs one by one to the console) it has to 'wait for some stragglers'. During that time, my Delphi app displays the infamous 'Not responding' in the caption bar and Task Manager.
Even though the Go executable seems to cleanly exit, my Done() procedure never gets reached - it appears that Delphi never leaves the loop..? What am I doing wrong?
As always, any form of help is greatly appreciated!
procedure Tform_Main.CaptureConsoleOutput(const ACommand, AParameters: String; AMemo: TMemo);
const
CReadBuffer = 65536;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: Array[0..CReadBuffer] of AnsiChar;
dRead: DWord;
dRunning: DWord;
begin
(*
ACommand: ex. {GoGetter.exe}
AParameters: ex. {C:\temp\downloads\ c:\temp\urls.txt 1}
*)
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
try
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
begin
Screen.Cursor := crHourglass;
Application.ProcessMessages;
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
//
if CreateProcess(nil, PChar(ACommand + ' ' + AParameters), #saSecurity, #saSecurity, True, NORMAL_PRIORITY_CLASS,
nil, nil, suiStartup, piProcess) then
begin
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
Application.ProcessMessages();
repeat
dRead := 0;
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
pBuffer[dRead] := #0;
OemToAnsi(pBuffer, pBuffer);
AMemo.Lines.Text := AMemo.Lines.Text + String(pBuffer);
SendMessage(AMemo.Handle, WM_VSCROLL, SB_BOTTOM, 0);
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
end;
end;
Done(); // writes a 'finished' message to the memo, resets the screen cursor & re-enables the start button
finally
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
CloseHandle(hRead);
CloseHandle(hWrite);
end;
end;

Don't assign your hRead handle to the child process's hStdInput. You are not sending any data to the child process. Don't let the child process inherit your hRead handle at all. Use SetHandleInformation() to remove the HANDLE_FLAG_INHERIT flag from it.
And, you need to close your hWrite handle after the child process has inherited it, otherwise the pipe will remain open after the child process has terminated. You are not writing anything to the child process, so you don't need to leave your original hWrite handle open. When the child process terminates, its inherited hWrite handle will be closed, thus breaking the pipe, allowing ReadFile() to stop waiting for further data.
See How to read output from cmd.exe using CreateProcess() and CreatePipe() for more details about the use of pipe handles during I/O redirection.
Then, you can remove the outer repeat loop altogether. Just loop on ReadFile() until it fails, then call WaitForSingleObject() on the hProcess handle before cleaning up.
And just an FYI, using AMemo.Lines.Text := AMemo.Lines.Text + String(pBuffer); is a very inefficient way to append strings to a TMemo, especially over a long time.
Try something more like this instead:
procedure Tform_Main.CaptureConsoleOutput(const ACommand, AParameters: String; AMemo: TMemo);
const
CReadBuffer = 65536;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array[0..CReadBuffer] of AnsiChar;
dRead: DWord;
begin
(*
ACommand: ex. {GoGetter.exe}
AParameters: ex. {C:\temp\downloads\ c:\temp\urls.txt 1}
*)
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
try
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
ZeroMemory(#suiStartup, SizeOf(suiStartup));
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES;
Screen.Cursor := crHourglass;
Application.ProcessMessages;
try
if CreateProcess(nil, PChar(ACommand + ' ' + AParameters), #saSecurity, #saSecurity, True, CREATE_NO_WINDOW or NORMAL_PRIORITY_CLASS,
nil, nil, suiStartup, piProcess) then
try
CloseHandle(piProcess.hThread);
CloseHandle(hWrite);
hWrite := INVALID_HANDLE_VALUE;
repeat
Application.ProcessMessages();
if (not ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil)) or (dRead = 0) then
Break;
pBuffer[dRead] := #0;
OemToAnsi(pBuffer, pBuffer);
AMemo.SelStart := AMemo.GetTextLen();
AMemo.SelLength := 0;
AMemo.SelText := String(pBuffer);
SendMessage(AMemo.Handle, WM_VSCROLL, SB_BOTTOM, 0);
until False;
WaitForSingleObject(piProcess.hProcess, INFINITE);
finally
CloseHandle(piProcess.hProcess);
end;
finally
Done();
end;
finally
CloseHandle(hRead);
if hWrite <> INVALID_HANDLE_VALUE then
CloseHandle(hWrite);
end;
end;
Then, consider moving this code into a separate worker thread so you don't block your main UI thread anymore. Then you won't need ProcessMessages() anymore. Otherwise, if you really want to call ProcessMessages() inside the loop, use a named pipe instead of an anonymous pipe, then you can read asynchronously using OVERLAPPED I/O (see Overlapped I/O on anonymous pipe).

Related

Moving full Console information to TMemo

I'm using the following procedure to put the result of DOS commands in a TMemo:
procedure RunDosInMemo(DosApp: String; AMemo: TMemo);
const
ReadBuffer = 2400;
var
Security : TSecurityAttributes;
ReadPipe, WritePipe : THandle;
start : TStartUpInfo;
ProcessInfo : TProcessInformation;
Buffer : Pchar;
BytesRead : DWord;
Apprunning : DWord;
begin
With Security do begin
nlength := SizeOf(TSecurityAttributes) ;
binherithandle := true;
lpsecuritydescriptor := nil;
end;
if Createpipe (ReadPipe, WritePipe,#Security, 0) then begin
Buffer := AllocMem(ReadBuffer + 1);
FillChar(Start, Sizeof(Start), #0);
start.cb := SizeOf(start) ;
start.hStdOutput := WritePipe;
start.hStdInput := ReadPipe;
start.dwFlags := STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW;
start.wShowWindow := SW_HIDE;
if CreateProcess(nil,
PChar(DosApp),
#Security,
#Security,
true,
NORMAL_PRIORITY_CLASS,
nil,
nil,
start,
ProcessInfo)
then
begin
repeat
Apprunning := WaitForSingleObject(ProcessInfo.hProcess, 200);
Application.ProcessMessages;
until (Apprunning <> WAIT_TIMEOUT);
repeat
BytesRead := 0;
ReadFile(ReadPipe, Buffer[0], ReadBuffer, BytesRead, nil);
Buffer[BytesRead] := #0;
OemToAnsi(Buffer, Buffer);
AMemo.Text := AMemo.text + String(Buffer);
until (BytesRead < ReadBuffer);
end;
FreeMem(Buffer);
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
CloseHandle(ReadPipe);
CloseHandle(WritePipe);
end;
end;
Then I use it with a copy of netsh.exe to get a list of wireless signals and MAC addresses like this:
RunDosInMemo('C:\Edge LR\netsh.exe wlan show networks mode=Bssid', Memo3);
But it shows only the first 9 wireless signals from the list. When I run it on the console directly, it shows the full list with all wireless signals and specs.
Does anyone have an idea how to solve this?
If you read the following MSDN documentation, you will see that you are missing some very important steps:
Creating a Child Process with Redirected Input and Output
Most notably, you are using the parent process's read end of the pipe for the child process's STDIN, which is wrong. And you are letting the child process inherit that read end of the pipe, which is also wrong.
You also need to close the write end of the pipe after the child process has inherited it, before you then start reading from the pipe. Otherwise, the child process will not fully exit and signal the handle returned by CreateProcess(). By closing the write end, you ensure the process can fully terminate, and that ReadFile() will fail with an ERROR_BROKEN_PIPE error when the child process closes its end of the pipe and there is no more data to be read.
Try something more like this:
procedure RunDosInMemo(DosApp: String; AMemo: TMemo);
const
ReadBuffer = 2400;
var
Security : TSecurityAttributes;
ReadPipe, WritePipe : THandle;
start : TStartUpInfo;
ProcessInfo : TProcessInformation;
Buffer : array of AnsiChar;
Str: AnsiString;
BytesRead : DWord;
AppRunning : DWord;
begin
with Security do begin
nLength := SizeOf(TSecurityAttributes);
bInherithandle := true;
lpSecurityDescriptor := nil;
end;
if not CreatePipe(ReadPipe, WritePipe, #Security, 0) then RaiseLastOSError;
try
if not SetHandleInformation(ReadPipe, HANDLE_FLAG_INHERIT, 0) then RaiseLastOSError;
SetLength(Buffer, ReadBuffer);
FillChar(Start, Sizeof(Start), 0);
start.cb := SizeOf(start);
start.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
start.hStdOutput := WritePipe;
start.hStdError := WritePipe;
start.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
start.wShowWindow := SW_HIDE;
if not CreateProcess(nil,
PChar(DosApp),
#Security,
#Security,
true,
NORMAL_PRIORITY_CLASS,
nil,
nil,
start,
ProcessInfo) then RaiseLastOSError;
try
CloseHandle(WritePipe);
WritePipe := 0;
repeat
AppRunning := MsgWaitForMultipleObjects(1, ProcessInfo.hProcess, False, 200, QS_ALLINPUT);
if AppRunning = (WAIT_OBJECT_0 + 1) then Application.ProcessMessages;
until (AppRunning <> WAIT_TIMEOUT);
repeat
BytesRead := 0;
if not ReadFile(ReadPipe, Buffer[0], ReadBuffer, BytesRead, nil) then
begin
if GetLastError() <> ERROR_BROKEN_PIPE then RaiseLastOSError;
Break;
end;
if BytesRead = 0 then Break;
SetString(Str, #Buffer[0], BytesRead);
AMemo.SelStart := AMemo.GetTextLen;
AMemo.SelLength := 0;
AMemo.SelText := String(Str);
until False;
finally
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
end;
finally
CloseHandle(ReadPipe);
if WritePipe <> 0 then CloseHandle(WritePipe);
end;
end;
You could do:
uses
JCLSysUtils;
procedure TForm1.HandleOutput( const Text: string );
begin
AMemo.Lines.Add( Text );
end;
procedure TForm1.Button1Click( Sender: TObject );
begin
AMemo.Clear;
JCLSysUtils.Execute( 'C:\Edge LR\netsh.exe wlan show networks mode=Bssid',
HandleOutput );
end;

ShellExecuteEx 7z Delphi

So I'm trying to do a archive using delphi and ShellExecuteEx my code is :
Result := False;
DecodeDate(now,y,m,d);
NumeFisier := dir+'\Export_'+IntToStr(y)+'.'+IntToStr(m)+'.'+IntToStr(d)+'.zip';
FillChar(exInfo, SizeOf(exInfo), 0);
with exInfo do
begin
cbSize := SizeOf(exInfo);
fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_FLAG_DDEWAIT;
Wnd := GetActiveWindow();
exInfo.lpVerb := nil;
exInfo.lpFile := PAnsiChar('C:\Windows\System32\cmd.exe');
exInfo.lpParameters := PAnsiChar('C:\Program Files\7-Zip\7z.exe ' +'a ' + NumeFisier + ' ' + dir);
nShow := SW_SHOWNORMAL;
end;
if ShellExecuteEx(#exInfo) then
Ph := exInfo.hProcess
else
begin
ShowMessage(SysErrorMessage(GetLastError));
Result := true;
exit;
end;
while WaitForSingleObject(exInfo.hProcess, 50) <> WAIT_OBJECT_0 do
Application.ProcessMessages;
CloseHandle(Ph);
Result := true;
For some reason this only opens the Command Prompt and doesn't execute the archiving. How can I make it execute the 7z.exe file.
I tried with ShellExecute and it works great, but I have to check then the process is finished, so I'm stuck with ShellExecuteEx
There's no need to involve cmd.exe. That's the command interpreter. You want to execute a different executable so do that directly.
You don't want to use ShellExecuteEx since that has far more generality than you need. All that ShellExecuteEx is doing here is calling CreateProcess. You should do that directly and avoid the middle man. What's more, calling CreateProcess allows you to hide the console window easily. Pass CREATE_NO_WINDOW to achieve that.
Finally, there are better ways to wait than your code. Using MsgWaitForMultipleObjects allows you to avoid polling. And putting this code into a thread would allow you to avoid calls to Application.ProcessMessages.
procedure WaitUntilSignaled(Handle: THandle; ProcessMessages: Boolean);
var
retval: DWORD;
begin
if ProcessMessages then begin
Application.ProcessMessages;//in case there are messages already in the queue
while True do begin
retval := MsgWaitForMultipleObjects(1, Handle, False, INFINITE, QS_ALLEVENTS);
case retval of
WAIT_OBJECT_0,WAIT_ABANDONED_0:
break;
WAIT_OBJECT_0+1:
Application.ProcessMessages;
WAIT_FAILED:
RaiseLastOSError;
end;
end;
end else begin
Win32Check(WaitForSingleObject(Handle, INFINITE)<>WAIT_FAILED);
end;
end;
procedure ExecuteProcess(
const ExecutablePath: string;
const Arguments: string;
const CurrentDirectory: string;
const Wait: Boolean;
const CreationFlags: DWORD
);
var
si: TStartupInfo;
pi: TProcessInformation;
MyCurrentDirectory: PChar;
begin
ZeroMemory(#si, SizeOf(si));
si.cb := SizeOf(si);
if CurrentDirectory <> '' then begin
MyCurrentDirectory := PChar(CurrentDirectory);
end else begin
MyCurrentDirectory := nil;
end;
Win32Check(CreateProcess(
nil,
PChar('"' + ExecutablePath + '" ' + Arguments),
nil,
nil,
False,
CreationFlags,
nil,
MyCurrentDirectory,
si,
pi
));
try
if Wait then begin
WaitUntilSignaled(pi.hProcess, True);
end;
finally
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
end;
end;

Execute background program and continue execution of code?

I want to run a background application from my Delphi code. The application that opens is a DOS based EXE which outputs stuff to the DOS window. The program will be open indefinitely until it is closed from task manager. The current code I use to open the application is;
procedure CaptureConsoleOutput(const ACommand, AParameters: String; CallBack: TArg<PAnsiChar>);
const
CReadBuffer = 2400;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array [0 .. CReadBuffer] of AnsiChar;
dBuffer: array [0 .. CReadBuffer] of AnsiChar;
dRead: DWord;
dRunning: DWord;
begin
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
begin
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, pChar(ACommand + ' ' + AParameters), #saSecurity, #saSecurity, True, NORMAL_PRIORITY_CLASS, nil, nil, suiStartup, piProcess) then
begin
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
//Application.ProcessMessages();
repeat
dRead := 0;
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
if(dRead > 0) then
begin
pBuffer[dRead] := #0;
//ShowMessage(pBuffer);
//OemToAnsi(pBuffer, pBuffer);
//Unicode support by Lars Fosdal
OemToCharA(pBuffer, dBuffer);
CallBack(dBuffer);
end;
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end;
CloseHandle(hRead);
CloseHandle(hWrite);
end;
end;
This is good, however because the program doesn't 'quit' and stays open forever, my application hangs, and the code never moves along.
Any help would be appreciated
If you don't need to do anything with the spawned process, you can simply close the handles that CreateProcess() returned and move on, the process will keep running. But since you appear to need to read continuously from the output of the spawned process, you can simply move that logic into a worker thread so your main code is not blocked anymore.
//You have to put your conde inside a thread. For example:
//Let's suppose you want to ping Google Server. You'll have to create a thread just like //below in your interface section inside Delphi. You'll get this done by choosing File-New-//Other-Thread Object, Then name your thread as you want.
type
TPing = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;
//Copy your CaptureConsoleOutput procedure onto Thread unit.
//inside Execute procedure, call your CaptureConsoleOutput procedure.
//In my case, I just capture the output directly in the Memo component.
//That worked just fine for me !
procedure TPing.Execute;
begin
Priority := tpLower;
CaptureConsoleOutput('ping www.yahoo.com', '-t', Form1.Memo2);
end;

CreateProcess using netsh hangs/freezes the application [Delphi]

I'm using the code bellow to run a few "netsh wlan" commands in order to check wifi status, connect to a wifi profile, etc.
The problem that I'm having is that every now and then the app will hang on any of the commands, it's just a random thing, plus, sometimes the output returned get overwritten with "nothing", when I debugged it seemed like a timing issue.
I tried the conventional approach to run a command with Pascal but it didn't work with netsh, the approach is "cmd.exe /C netsh wlan....".
I appreciate any advise on getting this freezing procedure working better or another approach.
I'm running DelphiXE5.
Thanks
Sample commands: netsh wlan show profiles, netsh wlan show interfaces, etc.
procedure GetDosOutput(const ACommand, AParameters: String; CallBack: TArg<PAnsiChar>);
const
CReadBuffer = 2400;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array [0 .. CReadBuffer] of AnsiChar;
dBuffer: array [0 .. CReadBuffer] of AnsiChar;
dRead: DWord;
dRunning: DWord;
begin
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
begin
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, pChar(ACommand + ' ' + AParameters), #saSecurity, #saSecurity, True, NORMAL_PRIORITY_CLASS, nil, nil, suiStartup, piProcess) then
begin
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
Application.ProcessMessages();
repeat
dRead := 0;
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
pBuffer[dRead] := #0;
//OemToAnsi(pBuffer, pBuffer);
//Unicode support by Lars Fosdal
OemToCharA(pBuffer, dBuffer);
CallBack(dBuffer);
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end;
CloseHandle(hRead);
CloseHandle(hWrite);
end;
end;
After following all the advises I got this portion of code changed and so far the app hasn't hanged anymore.
Thanks a lot!
procedure GetDosOutput(const ACommand, AParameters: String; CallBack: TArg<PAnsiChar>);
const
CReadBuffer = 2400;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array [0 .. CReadBuffer] of AnsiChar;
dBuffer: array [0 .. CReadBuffer] of AnsiChar;
dRead: DWord;
dRunning: DWord;
begin
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
begin
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, pChar(ACommand + ' ' + AParameters), #saSecurity, #saSecurity, True, NORMAL_PRIORITY_CLASS, nil, nil, suiStartup, piProcess) then
begin
Application.ProcessMessages();
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
repeat
dRead := 0;
try
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
except on E: Exception do
Exit;
end;
pBuffer[dRead] := #0;
//OemToAnsi(pBuffer, pBuffer);
//Unicode support by Lars Fosdal
OemToCharA(pBuffer, dBuffer);
CallBack(dBuffer);
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end;
CloseHandle(hRead);
CloseHandle(hWrite);
end;
end;
I created this wrapper to simplify the process:
function GetDosOutputSimple(const ACommand, AParameters: String) : String;
var
Tmp, S : String;
begin
GetDosOutput(ACommand, AParameters, procedure (const Line: PAnsiChar)
begin
Tmp := Line;
S := S + Tmp;
end);
GetDosOutputSimple := S;
end;
If for any reason by the time you call ReadFile, the process have not completed a write operation, or your buffer is not filled, ReadFile will block. Normally it should fail, but it can't since you're holding a handle to the write end. See documentation:
... It is important for the parent process to close its handle to the
write end of the pipe before calling ReadFile. If this is not done,
the ReadFile operation cannot return zero because the parent process
has an open handle to the write end of the pipe.
So close 'hWrite' before reading from the pipe.
Note that, in this case - if the process have not been able to write anything to the pipe yet, instead of blocking, ReadFile will properly fail - and GetLastError will report ERROR_BROKEN_PIPE. Under this condition, you'd probably gracefully fail too. So better check return of ReadFile.
Alternatively, wait until the process terminates. Then you won't risk ReadFile blocking waiting for writing since the handles on child's side will have been closed.
...
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
Application.ProcessMessages();
until (dRunning <> WAIT_TIMEOUT);
repeat
dRead := 0;
...
If there's a chance that you'll have some sizeable output, read from the pipe when the application is running:
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then begin
try
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, pChar(ACommand + ' ' + AParameters), #saSecurity,
#saSecurity, True, NORMAL_PRIORITY_CLASS, nil, nil,
suiStartup, piProcess) then begin
CloseHandle(hWrite);
try
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
Application.ProcessMessages();
repeat
dRead := 0;
if ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil) then begin
pBuffer[dRead] := #0;
OemToCharA(pBuffer, dBuffer);
CallBack(dBuffer);
end;
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
finally
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end;
end;
finally
CloseHandle(hRead);
if GetHandleInformation(hWrite, flags) then
CloseHandle(hWrite);
end;
end;

How do I run a command-line program in Delphi?

I need to execute a Windows "find" command from a Delphi software. I've tried to use the ShellExecute command, but it doesn't seem to work. In C, I'd use the system procedure, but here... I don't know. I'd like to do something like this:
System('find "320" in.txt > out.txt');
Edit : Thanks for the answer :)
I was trying to run 'Find' as an executable, not as argument for cmd.exe.
An example using ShellExecute():
procedure TForm1.Button1Click(Sender: TObject);
begin
ShellExecute(0, nil, 'cmd.exe', '/C find "320" in.txt > out.txt', nil, SW_HIDE);
Sleep(1000);
Memo1.Lines.LoadFromFile('out.txt');
end;
Note that using CreateProcess() instead of ShellExecute() allows for much better control of the process.
Ideally you would also call this in a secondary thread, and call WaitForSingleObject() on the process handle to wait for the process to complete. The Sleep() in the example is just a hack to wait some time for the program started by ShellExecute() to finish - ShellExecute() will not do that. If it did you couldn't for example simply open a notepad instance for editing a file, ShellExecute() would block your parent app until the editor was closed.
Variant 1 (using the "advanced" CreateProcess):
This will run a 'DOS' program and retrieve its output:
function GetDosOutput(CommandLine: string; Work: string = 'C:\'): string; { Run a DOS program and retrieve its output dynamically while it is running. }
var
SecAtrrs: TSecurityAttributes;
StartupInfo: TStartupInfo;
ProcessInfo: TProcessInformation;
StdOutPipeRead, StdOutPipeWrite: THandle;
WasOK: Boolean;
pCommandLine: array[0..255] of AnsiChar;
BytesRead: Cardinal;
WorkDir: string;
Handle: Boolean;
begin
Result := '';
with SecAtrrs do begin
nLength := SizeOf(SecAtrrs);
bInheritHandle := True;
lpSecurityDescriptor := nil;
end;
CreatePipe(StdOutPipeRead, StdOutPipeWrite, #SecAtrrs, 0);
try
with StartupInfo do
begin
FillChar(StartupInfo, SizeOf(StartupInfo), 0);
cb := SizeOf(StartupInfo);
dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
wShowWindow := SW_HIDE;
hStdInput := GetStdHandle(STD_INPUT_HANDLE); // don't redirect stdin
hStdOutput := StdOutPipeWrite;
hStdError := StdOutPipeWrite;
end;
WorkDir := Work;
Handle := CreateProcess(nil, PChar('cmd.exe /C ' + CommandLine),
nil, nil, True, 0, nil,
PChar(WorkDir), StartupInfo, ProcessInfo);
CloseHandle(StdOutPipeWrite);
if Handle then
try
repeat
WasOK := windows.ReadFile(StdOutPipeRead, pCommandLine, 255, BytesRead, nil);
if BytesRead > 0 then
begin
pCommandLine[BytesRead] := #0;
Result := Result + pCommandLine;
end;
until not WasOK or (BytesRead = 0);
WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
finally
CloseHandle(ProcessInfo.hThread);
CloseHandle(ProcessInfo.hProcess);
end;
finally
CloseHandle(StdOutPipeRead);
end;
end;
Variant 2:
Capture console output in [Realtime] and how it in a TMemo:
procedure CaptureConsoleOutput(const ACommand, AParameters: String; AMemo: TMemo);
const
CReadBuffer = 2400;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array[0..CReadBuffer] of AnsiChar; <----- update
dRead: DWord;
dRunning: DWord;
begin
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, #saSecurity, 0) then
begin
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, PChar(ACommand + ' ' + AParameters), #saSecurity,
#saSecurity, True, NORMAL_PRIORITY_CLASS, nil, nil, suiStartup, piProcess)
then
begin
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
Application.ProcessMessages();
repeat
dRead := 0;
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
pBuffer[dRead] := #0;
OemToAnsi(pBuffer, pBuffer);
AMemo.Lines.Add(String(pBuffer));
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end;
CloseHandle(hRead);
CloseHandle(hWrite);
end;
end;
Source: delphi.wikia.com

Resources