This new method supersedes the previous one using TDosCommand. It's been tested and it works with Delphi 2010 and Delphi XE, so it's worth to give it a try. It's really easy to use and I'm preparing a little tool with it.
//Anonymous procedure approach by Lars Fosdal
type
TArg<T> = reference to procedure(const Arg: T);
procedure TForm1.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);
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;
usage:
CaptureConsoleOutput('java -version', '',
procedure(const Line: PAnsiChar)
begin
Memo1.Lines.Add(String(Line));
end
);
With all the help I got from Lars, I've released a version of the function for test purposes. You can download the app from here (Thundax Output).
Example:
Related links:

Hola Jordi,
ReplyDeleteel código se ejecuta correctamente en TurboDelphi.
Saludos...
Miguel Angel
Hola Miguel,
ReplyDeleteBueno saber que funciona con TurboDelphi!.
Un Saludo
Jordi
Nice! Two change suggestions:
ReplyDelete- Change AMemo:TMemo to AOutput:TStrings - that gives more flexibility
- Use OEMtoCharBuff instead of OEMtoAnsi - that gives proper support for Unicode output.
Hi Lars,
ReplyDeleteThank you for your comment and for your suggestions. I'm taking into account one of them for the Unicode support.
Thanks.
Jordi
Thanks, and you're welcome!
ReplyDeleteIf you type dBuffer as Char, it is compatible with pre-unicode Delphi.
var
dBuffer : array [0 .. CReadBuffer] of Char;
If you use TStrings as output, you can still call it with AMemo:
CaptureConsoleOutput('java -version', '', Memo1.Lines);
Or - you could be even more radical and use an anonymous procedure for each line - then the user has complete control of how to use the output.
type
TArg = reference to procedure(const Arg:T);
procedure CaptureConsoleOutput(const ACommand, AParameters: String; CallBack: TArg);
...
CaptureConsoleOutput('java -version', '', procedure (const Line:pChar)
begin
Memo1.Lines.Add(String(Line));
end);
Google ate my brackets :/
ReplyDeleteTArg<T> = reference to procedure(const Arg:T);
procedure CaptureConsoleOutput(const ACommand, AParameters: String; CallBack: TArg<pChar>);
Hi Lars,
ReplyDeleteI love your radical approach using anonymous methods. I have gone for it!, but using PAnsiChar and OemToCharA(ANSI). After several trials with OEMtoCharBuff it turns out that the output was not displayed correctly.
Regards,
Jordi
Hi Jordi,
ReplyDeleteCool that you liked the anon.method approach. Anon.methods are incredibly potent, even if they have some limitations with regards to scope.
On the Unicode side of things:
I created a directory with a file named €.txt, and called your function with 'cmd /c dir c:\testdir'.
€.txt showed up as ?.txt in the raw data from the pipe, so I figured there was something fishy.
It turns out that cmd.exe have two output modes, and the default is to output AnsiChar. If I used the /u for Unicode switch: 'cmd /c dir c:\testdir' - all the pipe data were Unicode.
This means that CaptureConsoleOutput should either always use 'cmd /u /c command arg1 arg 2' and a WideChar buffer, or you need to move the conversion out into the anon.method, and let the user figure out what kind of conversion to do.
For security reasons, MS recommend using the OEMto...Buff/A/W which has a fixed length reference to avoid buffer overruns.
Correction:
ReplyDeleteUnicode switch should read: 'cmd /u /c dir c:\testdir'
Hi Lars,
ReplyDeleteI agree with you. After generating the €.txt file, if you try to do a 'dir c:\testfile' you'll see a ?.txt file from the console and that's what is picking up the pipe. I think that we should leave the user decide what are they going to use and modify the function according to that.
Cheers,
Jordi.
Check the picture, and you'll see the ?.txt file ;)
ReplyDeleteSweet! Thank you for mentioning me, as well ;)
ReplyDeleteIt was a surprise to me that cmd actually is ANSI by default.
Looking forward to see more godd stuff from you in the future!
If you use Google+, you hook up with me here: http://plus.lars.fosdal.com