Как перенаправить двоичный вывод gbak в поток Delphi?

Я хочу, чтобы средство резервного копирования Firebird gbak записывало свои выходные данные в поток Delphi (без промежуточного файла). Существует параметр командной строки для записи в стандартный вывод, а не в файл. Затем я использую метод Execute в JEDI JclSysUtils для запуска gbak и обработки этого вывода.

Это выглядит так:

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;

Проблема в том, что выходной файл слишком мал, чтобы содержать реальную резервную копию. Тем не менее я вижу элементы содержимого файла. Я пробовал разные типы потоков, но, похоже, это не имеет значения. Что здесь может быть не так?

Обновлять

Чтобы было ясно: приветствуются и другие решения. Больше всего мне нужно что-то надежное. Вот почему я в первую очередь пошел с JEDI, чтобы не изобретать такие вещи. Тогда, было бы неплохо, если бы это не было слишком сложно.


person Thijs van Dien    schedule 27.09.2013    source источник
comment
Может быть, вы просто записываете вывод в LBackupStream, но на самом деле сохраняете на диск поток BackupStream? Это две разные переменные и, следовательно, вероятно, два разных объекта потока. Если вы хотите сохранить поток на диск, то почему бы просто не использовать TFileStream? Или скажите gbak, чтобы он писал прямо на диск и не использовал вашу программу?   -  person Rob Kennedy    schedule 27.09.2013
comment
@RobKennedy Нет, это была просто ошибка в примере, так как мне пришлось упростить его, прежде чем размещать в Интернете. Я хочу обрабатывать данные, например, при сжатии, шифровании и т. д. «на лету», прежде чем отправлять их в другое место.   -  person Thijs van Dien    schedule 27.09.2013
comment
Что содержит поток и как он соотносится с тем, что вы получаете, пропуская поток и заставляя gbak записывать резервную копию прямо на диск? Если вы поместите точку останова в WriteString, увидите ли вы все данные, которые ожидаете увидеть?   -  person Rob Kennedy    schedule 27.09.2013
comment
@RobKennedy Не знаю, как это описать. Это, например, только 136566 из 10127872 байт. В первой части файла (выглядит как определения столбцов) есть пробелы, затем фактические данные отсутствуют, а затем снова пробелы (больше определений).   -  person Thijs van Dien    schedule 27.09.2013
comment
Я могу ответить на вопрос в теле, но я не могу легко ответить на вопрос в заголовке. На какой из них вы хотели получить ответ?   -  person Rob Kennedy    schedule 28.09.2013
comment
@RobKennedy Ну, мне любопытно, почему вышеизложенное не работает, но другие решения приемлемы, если они достигают того же самого таким же простым способом. Я только что сам нашел PJConsoleApp, который, кажется, работает, но все же должен завоевать мое доверие - он действительно чувствителен к значению TimeSlice, и как только у меня сложилось впечатление, что он пропустил самые последние байты.   -  person Thijs van Dien    schedule 28.09.2013
comment
@ThijsvanDien: Вы уверены, что потоковый метод для записи строки подходит для записи двоичных данных (как вы говорите в заголовке вопроса)?   -  person mghie    schedule 28.09.2013


Ответы (2)


Мой первый ответ эффективен, когда вы хотите объединить stdout и stderr. Однако, если вам нужно хранить их отдельно, этот подход бесполезен. И теперь я вижу, из более внимательного прочтения вашего вопроса и ваших комментариев, что вы действительно хотите разделить два потока вывода.

Теперь не совсем просто расширить мой первый ответ, чтобы охватить это. Проблема в том, что в коде используется блокирующий ввод-вывод. А если нужно обслуживать две трубы, тут явный конфликт. Широко используемым решением в Windows является асинхронный ввод-вывод, известный в мире Windows как перекрывающийся ввод-вывод. Однако реализовать асинхронный ввод-вывод намного сложнее, чем блокирующий ввод-вывод.

Итак, я собираюсь предложить альтернативный подход, который по-прежнему использует блокирующий ввод-вывод. Если мы хотим обслуживать несколько каналов и использовать блокирующий ввод-вывод, то очевидный вывод состоит в том, что нам нужен один поток для каждого канала. Это легко реализовать — намного проще, чем асинхронный вариант. Мы можем использовать почти идентичный код, но переместить блокирующие циклы чтения в потоки. Мой пример, переработанный таким образом, теперь выглядит так:

{$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.

Если вы хотите добавить поддержку отмены, вам нужно просто добавить вызов TerminateProcess при отмене пользователем. Это остановит все, и функция вернет код выхода, который вы предоставили TerminateProcess. Сейчас я не решаюсь предложить вам структуру отмены, но я думаю, что код в этом ответе теперь довольно близок к удовлетворению ваших требований.

person David Heffernan    schedule 28.09.2013
comment
Превосходно! Это работает очень хорошо. Две небольшие проблемы с кодом: inherited Create в конструкторе TReadPipeThread отсутствует значение для CreateSuspended — держу пари, что должно быть False — и Code Insight жалуется на несколько рекурсивное определение PipeSecurityAttributes — должно ли быть нормально, если я превращу его в SizeOf(TSecurityAttributes)? Я приму ответ, как только реализую прекращение. - person Thijs van Dien; 29.09.2013
comment
Код отлично работает в моей версии Delphi. Я думаю, что ваш старше. Конструктор перегрузки, который я вызываю, устанавливает CreateSuspended в False. Очевидно, иначе поток никогда бы не начался. Найдите, чтобы изменить атрибут безопасности, как вы описываете. - person David Heffernan; 29.09.2013
comment
В настоящее время я переделываю ваш пример, чтобы разрешить ввод данных (для восстановления резервной копии), поэтому я пытаюсь лучше понять, что происходит. Еще несколько вопросов о вашем решении: во многих сопоставимых фрагментах используется PeekNamedPipe. Это было сознательное решение? Может ли что-то сломаться, если я добавлю его? А что это за HANDLE_FLAG_INHERIT? MSDN не очень полезен, говоря, что он определяет, может ли дочерний процесс наследовать дескриптор, если я не знаю, что это означает/приводит к. - person Thijs van Dien; 30.09.2013
comment
Если внешний процесс наследует дескриптор чтения канала, то канал никогда не закрывается, потому что у внешнего процесса есть дескриптор, который он никогда не закрывает. PeekNamedPipe будет использоваться в асинхронном решении. Поскольку мы использовали блокировку ввода-вывода, она нам не нужна. - person David Heffernan; 30.09.2013
comment
Возвращаясь к этому (да, это было давно)... Моя интуиция подсказывает, что самый простой, но приемлемый способ заставить эту функцию вернуться (отменить) - это убить процесс, запущенный из другого потока. В противном случае мне пришлось бы заменить все Wait на WaitForMultipleObject и использовать SetEvent или что-то в этом роде? Если вам не нравится это предложение, я собираюсь открыть еще один вопрос об этом... - person Thijs van Dien; 12.11.2013
comment
Ваш единственный вариант - убить процесс. Не нужно еще один вопрос! - person David Heffernan; 12.11.2013
comment
Ну, это работает так. :) Просто немного боли, чтобы действительно убедиться, что внешний процесс завершен при любых обстоятельствах. Мне было интересно, зачем нам вообще нужны stdoutThread.WaitFor; stderrThread.WaitFor;, если за ними следует WaitForSingleObject в процессе. Если бы я мог оставить WaitFor вне, это немного уменьшило бы логику завершения. - person Thijs van Dien; 16.11.2013
comment
Если вы оставите их, вы заблокируете IIRC. - person David Heffernan; 16.11.2013

Я ожидаю, что ваш код не работает, потому что он пытается передать двоичные данные через текстовый поток. В любом случае достаточно просто решить вашу проблему с помощью пары вызовов Win32 API. Я не вижу веских причин использовать сторонние компоненты только для этой задачи.

Вот что вам нужно сделать:

  1. Создайте канал, который вы будете использовать в качестве канала связи между двумя процессами.
  2. Создайте процесс gbak и сделайте так, чтобы его стандартный вывод был концом канала для записи.
  3. Читать с читаемого конца канала.

Вот простая демонстрационная программа:

{$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.
person David Heffernan    schedule 28.09.2013
comment
Что мне нравится в вашем решении, так это то, что оно быстрое по сравнению с PJConsoleApp. К сожалению, мне не хватает некоторых вещей, таких как возможность тайм-аута или прерывания вручную (убедившись, что процесс завершен), отдельный вывод для stderr и код ошибки. Я не хочу пытаться вписать это в себя - я искал разные библиотеки, потому что я не очень хорошо знаком с WinAPI, и кажется, что легко ошибиться в деталях. Есть ли какой-то ограниченный набор ресурсов, которые вы можете порекомендовать, чтобы помочь понять, что происходит выше? - person Thijs van Dien; 28.09.2013
comment
Код ошибки, который вы получите, позвонив по номеру GetExitCodeProcess. Отдельный stdout/stderr просто включает создание двух каналов вместо одного. Для тайм-аута/отмены вы можете вставить этот код в поток и прервать с помощью TerminateProcess. Если вы не хотите переходить в другой поток и хотите сделать это, проверив флаг, как вы это делаете в коде в Q, выполните эту проверку в цикле while. - person David Heffernan; 28.09.2013
comment
Я попытался изменить это, как вы предложили. Не был уверен, как должна выглядеть петля. Код выше. Что-то не так, учитывая, что он зависает. - person Thijs van Dien; 28.09.2013
comment
Извините, у меня сейчас нет времени отлаживать это. - person David Heffernan; 28.09.2013
comment
Что произойдет, если вы добавите вызов GetFileSize(hstderrr) перед попыткой чтения из канала stderr? В противном случае, я думаю, произойдет то, что ReadFile для блоков stderr, потому что в этот канал ничего не записывается. И тогда вы никогда не сможете вернуться к очистке канала stdout, который необходим для того, чтобы все закрылось. - person David Heffernan; 28.09.2013
comment
ничего не меняет; попробовал это и для hstdoutr тоже. Должен ли я переписать это во что-то вроде while ProcessRunning do begin if PeekOut then WriteOut; if PeekErr then WriteErr; end? - person Thijs van Dien; 28.09.2013
comment
То, что я предложил, работает здесь. Но это довольно хромой подход. Правильный способ сделать это — использовать асинхронный ввод-вывод. Но это пугающе сложно. Взгляните на код JEDI. Код JEDI предполагает, что вам нужен текст, который, очевидно, является проблемой. Вам было бы интересно простое решение, основанное на потоках? Одна резьба на трубу. - person David Heffernan; 28.09.2013
comment
Средства на самом деле не имеют значения, пока они дают мне все упомянутое, надежны и не требуют повторного изобретения. JPConsoleApp подходит к этому довольно близко. Меня просто не устраивает тот факт, что он настолько чувствителен к значению TimeSlice, и однажды у меня сложилось впечатление, что он пропустил последние фрагменты данных. Я не хочу чувствовать себя счастливым, что это работает. Ваше первое решение кажется довольно надежным, за исключением того, что я не могу проверить, все ли в порядке с выводом, потому что он записан в stderr. Затем я мог бы перенаправить это в файл (для этого есть параметр), но я пытался вообще избегать файлов. - person Thijs van Dien; 28.09.2013