Monday, 4 July 2011

Capturing console output with Delphi 2010/XE

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:

12 comments:

  1. Hola Jordi,
    el código se ejecuta correctamente en TurboDelphi.
    Saludos...
    Miguel Angel

    ReplyDelete
  2. Hola Miguel,

    Bueno saber que funciona con TurboDelphi!.

    Un Saludo
    Jordi

    ReplyDelete
  3. Nice! Two change suggestions:
    - Change AMemo:TMemo to AOutput:TStrings - that gives more flexibility
    - Use OEMtoCharBuff instead of OEMtoAnsi - that gives proper support for Unicode output.

    ReplyDelete
  4. Hi Lars,

    Thank you for your comment and for your suggestions. I'm taking into account one of them for the Unicode support.

    Thanks.
    Jordi

    ReplyDelete
  5. Thanks, and you're welcome!

    If 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);

    ReplyDelete
  6. Google ate my brackets :/

    TArg<T> = reference to procedure(const Arg:T);

    procedure CaptureConsoleOutput(const ACommand, AParameters: String; CallBack: TArg<pChar>);

    ReplyDelete
  7. Hi Lars,

    I 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

    ReplyDelete
  8. Hi Jordi,
    Cool 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.

    ReplyDelete
  9. Correction:
    Unicode switch should read: 'cmd /u /c dir c:\testdir'

    ReplyDelete
  10. Hi Lars,

    I 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.

    ReplyDelete
  11. Check the picture, and you'll see the ?.txt file ;)

    ReplyDelete
  12. Sweet! Thank you for mentioning me, as well ;)

    It 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

    ReplyDelete