5

I want the Firebird backup tool gbak to write its output to a Delphi stream (with no intermediate file). There is a command line parameter to write to stdout rather than a file. I then use the Execute method in JEDI's JclSysUtils to launch gbak and process that output.

It looks like this:

procedure DoBackup;
var
  LBackupAbortFlag: Boolean;
  LBackupStream: TStringStream;
begin
  LBackupAbortFlag := False;
  LBackupStream := TStringStream.Create;
  try
    Execute('"C:\path to\gbak.exe" -b -t -v -user SYSDBA -pas "pw" <db> stdout',
      LBackupStream.WriteString, // Should process stdout (backup)
      SomeMemo.Lines.Append, // Should process stderr (log)
      True, // Backup is "raw"
      False, // Log is not
      @LBackupAbortFlag);
    LBackupStream.SaveToFile('C:\path to\output.fbk');
  finally
    LBackupStream.Free;
  end;
end;

The problem is that the output file is way too small to contain that actual backup. Still I see elements of the file's content. I tried different stream types, but that doesn't seem to make a difference. What could be going wrong here?

Update

To be clear: other solutions are welcome as well. Most of all, I need something reliable. That's why I went with JEDI in the first place, not to reinvent such a thing. Then, it would be nice, if it would be not too complicated.

David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389
Thijs van Dien
  • 6,123
  • 1
  • 26
  • 44
  • Could it simply be that you're writing the output to `LBackupStream`, but the stream you actually save to disk is `BackupStream`? Those are two different variables, and thus probably two different stream objects. If you want to save the stream to disk, then why not just use a `TFileStream`? Or tell gbak to write directly to disk and leave your program out of it? – Rob Kennedy Sep 27 '13 at 15:55
  • @RobKennedy No, that was just a mistake in the example, since I had to simplify it before putting it on the internet. I want to process the data as in compress, encrypt, et cetera on the fly before sending it elsewhere. – Thijs van Dien Sep 27 '13 at 16:01
  • What does the stream contain, and how does it compare to what you get when you skip the stream and have gbak write the backup directly to disk? If you put a breakpoint in `WriteString`, do you see all the data you expect to see? – Rob Kennedy Sep 27 '13 at 16:14
  • @RobKennedy Not sure how to describe this. It's for example only 136566 out of 10127872 bytes. The first part of the file (looks like column definitions) has gaps, then the actual data is missing and then again gaps (more definitions). – Thijs van Dien Sep 27 '13 at 16:29
  • I can answer the question in the body, but I cannot easily answer the one in the title. Which one did you intend to get an answer for? – Rob Kennedy Sep 27 '13 at 21:47
  • @RobKennedy Well, I'm curious why the above doesn't work, but other solutions are acceptable if they achieve the same thing in an equally straightforward way. I just found `PJConsoleApp` myself, which seems to work, but still needs to gain my trust - it's really sensitive for the `TimeSlice` value and once I got the impression it skipped the very last bytes. – Thijs van Dien Sep 27 '13 at 22:01
  • @ThijsvanDien: Are you sure the stream method to write a string is the proper one to write binary data (as you say in the question title)? – mghie Sep 28 '13 at 05:35

2 Answers2

8

My first answer is effective when you wish to merge stdout and stderr. However, if you need to keep these separate, that approach is no use. And I can now see, from a closer reading of your question, and your comments, that you do wish to keep the two output streams separate.

Now, it is not completely straightforward to extend my first answer to cover this. The problem is that the code there uses blocking I/O. And if you need to service two pipes, there is an obvious conflict. A commonly used solution in Windows is asynchronous I/O, known in the Windows world as overlapped I/O. However, asynchronous I/O is much more complex to implement than blocking I/O.

So, I'm going to propose an alternative approach that still uses blocking I/O. If we want to service multiple pipes, and we want to use blocking I/O then the obvious conclusion is that we need one thread for each pipe. This is easy to implement – much easier than the asynchronous option. We can use almost identical code but move the blocking read loops into threads. My example, re-worked in this way, now looks like this:

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Windows;

type
  TProcessOutputPipe = class
  private
    Frd: THandle;
    Fwr: THandle;
  public
    constructor Create;
    destructor Destroy; override;
    property rd: THandle read Frd;
    property wr: THandle read Fwr;
    procedure CloseWritePipe;
  end;

constructor TProcessOutputPipe.Create;
const
  PipeSecurityAttributes: TSecurityAttributes = (
    nLength: SizeOf(TSecurityAttributes);
    bInheritHandle: True
  );
begin
  inherited;
  Win32Check(CreatePipe(Frd, Fwr, @PipeSecurityAttributes, 0));
  Win32Check(SetHandleInformation(Frd, HANDLE_FLAG_INHERIT, 0));//don't inherit read handle of pipe
end;

destructor TProcessOutputPipe.Destroy;
begin
  CloseHandle(Frd);
  if Fwr<>0 then
    CloseHandle(Fwr);
  inherited;
end;

procedure TProcessOutputPipe.CloseWritePipe;
begin
  CloseHandle(Fwr);
  Fwr := 0;
end;

type
  TReadPipeThread = class(TThread)
  private
    FPipeHandle: THandle;
    FStream: TStream;
  protected
    procedure Execute; override;
  public
    constructor Create(PipeHandle: THandle; Stream: TStream);
  end;

constructor TReadPipeThread.Create(PipeHandle: THandle; Stream: TStream);
begin
  inherited Create(False);
  FPipeHandle := PipeHandle;
  FStream := Stream;
end;

procedure TReadPipeThread.Execute;
var
  Buffer: array [0..4096-1] of Byte;
  BytesRead: DWORD;
begin
  while ReadFile(FPipeHandle, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead<>0) do begin
    FStream.WriteBuffer(Buffer, BytesRead);
  end;
end;

function ReadOutputFromExternalProcess(const ApplicationName, CommandLine: string; stdout, stderr: TStream): DWORD;
var
  stdoutPipe, stderrPipe: TProcessOutputPipe;
  stdoutThread, stderrThread: TReadPipeThread;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
  lpApplicationName: PChar;
  ModfiableCommandLine: string;
begin
  if ApplicationName='' then
    lpApplicationName := nil
  else
    lpApplicationName := PChar(ApplicationName);
  ModfiableCommandLine := CommandLine;
  UniqueString(ModfiableCommandLine);

  stdoutPipe := nil;
  stderrPipe := nil;
  stdoutThread := nil;
  stderrThread := nil;
  try
    stdoutPipe := TProcessOutputPipe.Create;
    stderrPipe := TProcessOutputPipe.Create;

    ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
    StartupInfo.cb := SizeOf(StartupInfo);
    StartupInfo.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
    StartupInfo.wShowWindow := SW_HIDE;
    StartupInfo.hStdOutput := stdoutPipe.wr;
    StartupInfo.hStdError := stderrPipe.wr;
    Win32Check(CreateProcess(lpApplicationName, PChar(ModfiableCommandLine), nil, nil, True,
      CREATE_NO_WINDOW or NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInfo));

    stdoutPipe.CloseWritePipe;//so that the process is able to terminate
    stderrPipe.CloseWritePipe;//so that the process is able to terminate

    stdoutThread := TReadPipeThread.Create(stdoutPipe.rd, stdout);
    stderrThread := TReadPipeThread.Create(stderrPipe.rd, stderr);
    stdoutThread.WaitFor;
    stderrThread.WaitFor;

    Win32Check(WaitForSingleObject(ProcessInfo.hProcess, INFINITE)=WAIT_OBJECT_0);
    Win32Check(GetExitCodeProcess(ProcessInfo.hProcess, Result));
  finally
    stderrThread.Free;
    stdoutThread.Free;
    stderrPipe.Free;
    stdoutPipe.Free;
  end;
end;

procedure Test;
var
  stdout, stderr: TFileStream;
  ExitCode: DWORD;
begin
  stdout := TFileStream.Create('C:\Desktop\stdout.txt', fmCreate);
  try
    stderr := TFileStream.Create('C:\Desktop\stderr.txt', fmCreate);
    try
      ExitCode := ReadOutputFromExternalProcess('', 'cmd /c dir /s C:\Windows\system32', stdout, stderr);
    finally
      stderr.Free;
    end;
  finally
    stdout.Free;
  end;
end;

begin
  Test;
end.

If you wish to add support for cancelling, then you would simply add in a call to TerminateProcess when the user cancelled. This would bring everything to a halt, and the function would return the exit code that you supplied to TerminateProcess. I'm hesitant right now to suggest a cancellation framework for you, but I think that the code in this answer is now pretty close to meeting your requirements.

David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389
  • Excellent! It works very well. Two minor issues with the code: `inherited Create` in the constructor of `TReadPipeThread` is missing a value for `CreateSuspended` - I bet that should be `False` - and Code Insight complains about the somewhat recursive definition of `PipeSecurityAttributes` - should it be fine if I turn it into `SizeOf(TSecurityAttributes)`? I'll accept the answer once I implemented the termination thing. – Thijs van Dien Sep 29 '13 at 10:37
  • Code fine in my version of Delphi. I guess yours is older. The constructor of overload I call sets CreateSuspended to False. Obvious really otherwise the thread would never start. Find to change security attr as you describe. – David Heffernan Sep 29 '13 at 10:42
  • Currently I'm reworking your example to allow for input (to restore the backup), so I'm trying to develop a better understanding of what's going on. Few more questions about your solution: in many comparable snippets, `PeekNamedPipe` is used. Was that a conscious decision? May things break if I add it? And what is this `HANDLE_FLAG_INHERIT` about? MSDN isn't very helpful by saying it defines whether the child process may inherit the handle if I don't know what that means/results in. – Thijs van Dien Sep 30 '13 at 13:41
  • If the external process inherits the pipe read handle, then the pipe never closes because the external process has a handle to it which it never closes. `PeekNamedPipe` would be used in an ansync solution. Because we used blocking I/O, we don't have any need for it. – David Heffernan Sep 30 '13 at 14:28
  • Coming back to this (yup, it's been a while)... It is my intuition that the easiest yet acceptable way to force this function to return (canceling) would be to kill the process it launched from another thread. Otherwise I'd have to change all the `Wait`s into `WaitForMultipleObject` and use `SetEvent` or something? If you dislike this proposal, I'm going to open another question just about that... – Thijs van Dien Nov 11 '13 at 22:09
  • Your only option is to kill the process. No need for another question! – David Heffernan Nov 11 '13 at 23:20
  • Well, it works like this. :) Just a little pain to make really sure the external process is terminated under all circumstances. I was wondering why we need the `stdoutThread.WaitFor; stderrThread.WaitFor;` at all if they're followed by a `WaitForSingleObject` on the process. If I can leave the `WaitFor` out, that would reduce termination logic quite a bit. – Thijs van Dien Nov 16 '13 at 17:30
  • If you leave them out then you deadlock IIRC – David Heffernan Nov 16 '13 at 18:00
3

I expect that your code is failing because it tries to put binary data through a text oriented stream. In any case, it's simple enough to solve your problem with a couple of Win32 API calls. I don't see any compelling reason to use third party components for just this task.

Here's what you need to do:

  1. Create a pipe that you will use as a communication channel between the two processes.
  2. Create the gbak process and arrange for its stdout to be the write end of the pipe.
  3. Read from the read end of the pipe.

Here's a simple demonstration program:

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Windows;

procedure ReadOutputFromExternalProcess(const ApplicationName, CommandLine: string; Stream: TStream);
const
  PipeSecurityAttributes: TSecurityAttributes = (
    nLength: SizeOf(PipeSecurityAttributes);
    bInheritHandle: True
  );
var
  hstdoutr, hstdoutw: THandle;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
  lpApplicationName: PChar;
  ModfiableCommandLine: string;
  Buffer: array [0..4096-1] of Byte;
  BytesRead: DWORD;
begin
  if ApplicationName='' then begin
    lpApplicationName := nil;
  end else begin
    lpApplicationName := PChar(ApplicationName);
  end;

  ModfiableCommandLine := CommandLine;
  UniqueString(ModfiableCommandLine);

  Win32Check(CreatePipe(hstdoutr, hstdoutw, @PipeSecurityAttributes, 0));
  Try
    Win32Check(SetHandleInformation(hstdoutr, HANDLE_FLAG_INHERIT, 0));//don't inherit read handle of pipe
    ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
    StartupInfo.cb := SizeOf(StartupInfo);
    StartupInfo.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
    StartupInfo.wShowWindow := SW_HIDE;
    StartupInfo.hStdOutput := hstdoutw;
    StartupInfo.hStdError := hstdoutw;
    if not CreateProcess(
      lpApplicationName,
      PChar(ModfiableCommandLine),
      nil,
      nil,
      True,
      CREATE_NO_WINDOW or NORMAL_PRIORITY_CLASS,
      nil,
      nil,
      StartupInfo,
      ProcessInfo
    ) then begin
      RaiseLastOSError;
    end;
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);
    CloseHandle(hstdoutw);//close the write end of the pipe so that the process is able to terminate
    hstdoutw := 0;
    while ReadFile(hstdoutr, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead<>0) do begin
      Stream.WriteBuffer(Buffer, BytesRead);
    end;
  Finally
    CloseHandle(hstdoutr);
    if hstdoutw<>0 then begin
      CloseHandle(hstdoutw);
    end;
  End;
end;

procedure Test;
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create('C:\Desktop\out.txt', fmCreate);
  Try
    ReadOutputFromExternalProcess('', 'cmd /c dir /s C:\Windows\system32', Stream);
  Finally
    Stream.Free;
  End;
end;

begin
  Test;
end.
David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389
  • What I like about your solution is that it's fast compared to `PJConsoleApp`. Unfortunately I'm missing some things like the ability to timeout or abort manually (making sure the process is terminated), separate output for stderr and the error code. I'm reluctant to try fit this in myself - I looked for different libraries because I'm not very familiar with WinAPI and it seems easy to get a detail wrong. Is there some limited set of resources you can recommend to help understand what's going on above? – Thijs van Dien Sep 28 '13 at 13:53
  • Error code you obtain by calling `GetExitCodeProcess`. Separate stdout/stderr just involves creating two pipes instead of one. For timeout/abort, you could pop this code in a thread and abort with `TerminateProcess`. If you don't want to move to a different thread, and want to do it by checking a flag, as you do in the code in the Q, perform that check in the while loop. – David Heffernan Sep 28 '13 at 13:57
  • I tried to modify it was you suggested. Wasn't sure what the loop should look like. Code above. Something is wrong, given that it hangs. – Thijs van Dien Sep 28 '13 at 14:35
  • Sorry, I haven't got the time to debug this now. – David Heffernan Sep 28 '13 at 14:40
  • What happens if you add in a call to `GetFileSize(hstderrr)` before you attempt to read from the stderr pipe? Otherwise what I guess happens is that the `ReadFile` for stderr blocks because nothing is written to that pipe. And then you can never get back to empty the stdout pipe which is needed in order for everything to close down. – David Heffernan Sep 28 '13 at 14:51
  • Doesn't change anything; tried it for `hstdoutr` too. Should I rewrite this thing to something like `while ProcessRunning do begin if PeekOut then WriteOut; if PeekErr then WriteErr; end`? – Thijs van Dien Sep 28 '13 at 15:01
  • What I suggested works here. But it's a rather lame approach. The right way to do this is to use async I/O. But it's fearsomely complex. Have a look at the JEDI code. The JEDI code assumes you want text which is obviously the problem. Would you be interested in a simple solution based on threads? One thread per pipe. – David Heffernan Sep 28 '13 at 15:26
  • The means don't really matter as long as it gives me everything mentioned, is reliable and doesn't require reinvention. `JPConsoleApp` comes rather close to that. I'm just uncomfortable with the fact that it's so sensitive for the `TimeSlice` value and I once got the impression it missed the last chunk(s) of data. I don't want to feel lucky that it works. Your first solution seems rather solid, except for that I can't check if the output is all fine, because that's written to stderr. I could then redirect that to a file (there's a parameter for that) but I was trying to avoid files altogether. – Thijs van Dien Sep 28 '13 at 15:42