Getting output from a shell/dos app into a Delphi app
Asked Answered
A

3

45

I have a commandline application coded in delphi that I need to call from a normal desktop application (also coded in delphi). In short, I want to call the commandline app and display the text it outputs "live" in a listbox.

It's been ages since I have played around with the shell, but I distinctly remember that in order to grab the text from a commandline app - I have to use the pipe symbol ">". Like this:

C:/mycmdapp.exe >c:/result.txt

This will take any text printed to the shell (using writeLn) and dump it to a textfile called "result.txt".

But.. (and here comes the pickle), I want a live result rather than a backlog file. A typical example is the Delphi compiler itself - which manages to report back to the IDE what is going on. If my memory serves me correctly, I seem to recall that I must create a "pipe" channel (?), and then assign the pipe-name to the shell call.

I have tried to google this but I honestly was unsure of how to formulate it. Hopefully someone from the community can point me in the right direction.

Updated: This question might be identical to How do I run a command-line program in Delphi?. Some of the answers fit what I'm looking for, although the title and question itself is not identical.

Aviles answered 2/2, 2012 at 21:6 Comment(6)
Console Application Runner Classes - looks promisingApriorism
Im using Delphi XE2 (although i have most Delphi versions from Delphi 4 and up). Wasnt one of the big news earlier that Delphi 2010+ are binary compatible with XE and XE2? In that case the classes should work. Thank you so much for you feedback!Aviles
@Jon: No, that's incorrect. There is no binary compatibility between Delphi compiler versions (with the single exception being between D6 and D7). You can't use .dcu files compiled by D2010 with any other version of Delphi without recompiling the source.Diehard
Thanks for clearing it up! Dont learn if we dont ask. I could have sworn i read that somewhere - but it could be wrong. Thanks for pointing it out :)Aviles
@KenWhite, wasn't it D2006 to D2007?Bolinger
@Uwe, I believe you're right. It was that single version, and I got it wrong. Mea culpa. Thanks for the correction. :)Diehard
B
58

As ever so often Zarco Gajic has a solution: Capture the output from a DOS (command/console) Window. This is a copy from his article for future reference:

The example runs 'chkdsk.exe c:\' and displays the output to Memo1. Put a TMemo (Memo1) and a TButton (Button1) on your form. Put this code in the OnCLick event procedure for Button1:

procedure RunDosInMemo(DosApp: string; AMemo:TMemo);
const
    READ_BUFFER_SIZE = 2400;
var
    Security: TSecurityAttributes;
    readableEndOfPipe, writeableEndOfPipe: THandle;
    start: TStartUpInfo;
    ProcessInfo: TProcessInformation;
    Buffer: PAnsiChar;
    BytesRead: DWORD;
    AppRunning: DWORD;
begin
    Security.nLength := SizeOf(TSecurityAttributes);
    Security.bInheritHandle := True;
    Security.lpSecurityDescriptor := nil;

    if CreatePipe({var}readableEndOfPipe, {var}writeableEndOfPipe, @Security, 0) then
    begin
        Buffer := AllocMem(READ_BUFFER_SIZE+1);
        FillChar(Start, Sizeof(Start), #0);
        start.cb := SizeOf(start);

        // Set up members of the STARTUPINFO structure.
        // This structure specifies the STDIN and STDOUT handles for redirection.
        // - Redirect the output and error to the writeable end of our pipe.
        // - We must still supply a valid StdInput handle (because we used STARTF_USESTDHANDLES to swear that all three handles will be valid)
        start.dwFlags := start.dwFlags or STARTF_USESTDHANDLES;
        start.hStdInput := GetStdHandle(STD_INPUT_HANDLE); //we're not redirecting stdInput; but we still have to give it a valid handle
        start.hStdOutput := writeableEndOfPipe; //we give the writeable end of the pipe to the child process; we read from the readable end
        start.hStdError := writeableEndOfPipe;

        //We can also choose to say that the wShowWindow member contains a value.
        //In our case we want to force the console window to be hidden.
        start.dwFlags := start.dwFlags + STARTF_USESHOWWINDOW;
        start.wShowWindow := SW_HIDE;

        // Don't forget to set up members of the PROCESS_INFORMATION structure.
        ProcessInfo := Default(TProcessInformation);

        //WARNING: The unicode version of CreateProcess (CreateProcessW) can modify the command-line "DosApp" string. 
        //Therefore "DosApp" cannot be a pointer to read-only memory, or an ACCESS_VIOLATION will occur.
        //We can ensure it's not read-only with the RTL function: UniqueString
        UniqueString({var}DosApp);

        if CreateProcess(nil, PChar(DosApp), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, start, {var}ProcessInfo) then
        begin
            //Wait for the application to terminate, as it writes it's output to the pipe.
            //WARNING: If the console app outputs more than 2400 bytes (ReadBuffer),
            //it will block on writing to the pipe and *never* close.
            repeat
                Apprunning := WaitForSingleObject(ProcessInfo.hProcess, 100);
                Application.ProcessMessages;
            until (Apprunning <> WAIT_TIMEOUT);

            //Read the contents of the pipe out of the readable end
            //WARNING: if the console app never writes anything to the StdOutput, then ReadFile will block and never return
            repeat
                BytesRead := 0;
                ReadFile(readableEndOfPipe, Buffer[0], READ_BUFFER_SIZE, {var}BytesRead, nil);
                Buffer[BytesRead]:= #0;
                OemToAnsi(Buffer,Buffer);
                AMemo.Text := AMemo.text + String(Buffer);
            until (BytesRead < READ_BUFFER_SIZE);
        end;
        FreeMem(Buffer);
        CloseHandle(ProcessInfo.hProcess);
        CloseHandle(ProcessInfo.hThread);
        CloseHandle(readableEndOfPipe);
        CloseHandle(writeableEndOfPipe);
    end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin {button 1 code}
   RunDosInMemo('chkdsk.exe c:\',Memo1);
end;

Update: The above example reads the output in one step. Here is another example from DelphiDabbler showing how the output can be read while the process is still running:

function GetDosOutput(CommandLine: string; Work: string = 'C:\'): string;
var
  SA: TSecurityAttributes;
  SI: TStartupInfo;
  PI: TProcessInformation;
  StdOutPipeRead, StdOutPipeWrite: THandle;
  WasOK: Boolean;
  Buffer: array[0..255] of AnsiChar;
  BytesRead: Cardinal;
  WorkDir: string;
  Handle: Boolean;
begin
  Result := '';
  with SA do begin
    nLength := SizeOf(SA);
    bInheritHandle := True;
    lpSecurityDescriptor := nil;
  end;
  CreatePipe(StdOutPipeRead, StdOutPipeWrite, @SA, 0);
  try
    with SI do
    begin
      FillChar(SI, SizeOf(SI), 0);
      cb := SizeOf(SI);
      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), SI, PI);
    CloseHandle(StdOutPipeWrite);
    if Handle then
      try
        repeat
          WasOK := ReadFile(StdOutPipeRead, Buffer, 255, BytesRead, nil);
          if BytesRead > 0 then
          begin
            Buffer[BytesRead] := #0;
            Result := Result + Buffer;
          end;
        until not WasOK or (BytesRead = 0);
        WaitForSingleObject(PI.hProcess, INFINITE);
      finally
        CloseHandle(PI.hThread);
        CloseHandle(PI.hProcess);
      end;
  finally
    CloseHandle(StdOutPipeRead);
  end;
end;
Bolinger answered 2/2, 2012 at 21:15 Comment(16)
That is perfect! Thank you so much! I didnt think of checking delphi.about.com, exellent!Aviles
@JonLennartAasenden, I added a second example showing a better approach.Bolinger
Note that it is also possible to THandleStream rather than using ReadFile directly. This can be useful if you want more flexiblity, e.g. to copy to TMemoryStream or any other TStream descendent. Just use THandleStream.Create(StdOutPipeRead) to create the stream.Gambol
The code above will cause problems when using unicode versionof CreateProcess: The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation (msdn.microsoft.com/en-us/library/windows/desktop/…)Ahoufe
Doesn't Zarco's code lead to deadlock when the program produces more output than will fit in the pipe buffer?Limoges
@RobKennedy, probably yes. The second approach should address this better.Bolinger
Note that with GetDosOutput the parent program will freeze while the DOS program is running.Tippet
I also added stderr in RunDosInMemo: start.hStdError := WritePipe;Barden
Holy Intersimone, that's a lot of complicated code for what should be a one-function operation. 10 variables, filling buffers, pipes, handles, flags.... Delphi really needs to catch up to Free Pascal in this area. To put the output of a command in a string "s" in the newest version is just "RunCommand('/bin/bash',['-c','echo $PATH'],s)" and it's cross-platform too.Accede
A very useful function for many reasons but it doesn't compile on XE8 or 10.. CreatePipe(StdOutPipeRead, StdOutPipeWrite, @SA, 0); Gives the error "Types of actual and formal var parameters must be identical" Darned if I can see what's wrong with that, though!Contrapuntal
@MitchellV, must be something in your scope. A simple unit containing only uses Winapi.Windows; and this function compiles flawlessly under Seattle and Berlin.Bolinger
@Uwe - thanks for responding.. I'm confused as to why it won't compile for me in my project but apparently something else I'm including is interfering with the declaration of CreatePipe.. On a fresh project it does compile.. Apologies for the confusing note!Contrapuntal
@MitchellV, try using Winapi.Windows last. If the error persists, the offending code is in the same unit. Otherwise move Winapi.Windows stepwise one unit back until you find the culprit.Bolinger
I moved the function out to its own Unit and all works well (well, except that apparently Signtool from MS is fussy about being run like that -- but that's a whole different ball of wax!). I'll take the win! Thanks again for the responses.Contrapuntal
This work but somehow if call it a second time, the program get stucks on this : repeat Apprunning := WaitForSingleObject(ProcessInfo.hProcess, 100); Application.ProcessMessages; until (Apprunning <> WAIT_TIMEOUT);Prut
Ok to use in Lazarus, add uses Windows. Compile nicely and works for normal dos program.Deangelis
A
25

You probably have the code on your harddisk already: the Execute function in the JclSysUtils unit of the JCL (JEDI Code Library) does what you need:

function Execute(const CommandLine: string; OutputLineCallback: TTextHandler; 
  RawOutput: Boolean = False; AbortPtr: PBoolean = nil): Cardinal;

You can supply it with a callback procedure:
TTextHandler = procedure(const Text: string) of object;

Aileen answered 3/4, 2013 at 8:40 Comment(1)
writeln(itoa(JExecute('cmd /C dir .',@TTextHandlerQ, true, false))); callback in maXbox4: {type TTextHandler =} procedure TTextHandlerQ(const aText: string); begin memo2.lines.add(atext); end;Rustcolored
R
2

Did an answer too for better understanding:

{type TTextHandler =} procedure TTextHandlerQ(const aText: string);
 begin
   memo2.lines.add(atext);
 end;  

writeln(itoa(JExecute('cmd /C dir *.*',@TTextHandlerQ, true, false))); 

You have to use /C then cmd /c is used to run commands in MS-DOS and terminate after command or process completion, otherwise it blocks output to memo.

Rustcolored answered 29/8, 2022 at 9:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.