tuberias - ¿Cómo redirigir la salida binaria de gbak a una transmisión Delphi?
tuberias y filtros en linux (2)
Espero que su código esté fallando porque intenta poner datos binarios a través de una secuencia orientada al texto. En cualquier caso, es bastante simple resolver su problema con un par de llamadas a la API de Win32. No veo ningún motivo convincente para utilizar componentes de terceros solo para esta tarea.
Esto es lo que necesita hacer:
- Cree una tubería que usará como un canal de comunicación entre los dos procesos.
- Cree el proceso gbak y organice que su stdout sea el final de escritura de la tubería.
- Lea desde el final de lectura de la tubería.
Aquí hay un programa de demostración simple:
{$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.
Quiero que la herramienta de copia de seguridad de Firebird, gbak, escriba su salida en una secuencia Delphi (sin ningún archivo intermedio). Hay un parámetro de línea de comando para escribir en stdout en lugar de en un archivo. Luego uso el método Execute
en JclSysUtils
de JEDI para lanzar gbak y procesar ese resultado.
Se parece a esto:
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;
El problema es que el archivo de salida es demasiado pequeño para contener esa copia de seguridad real. Todavía veo elementos del contenido del archivo. Intenté diferentes tipos de transmisión, pero eso no parece marcar la diferencia. ¿Qué podría estar yendo mal aquí?
Actualizar
Para ser claros: también son bienvenidas otras soluciones. Más que nada, necesito algo confiable. Es por eso que fui con JEDI en primer lugar, no para reinventar tal cosa. Entonces, sería bueno, si no fuera demasiado complicado.
Mi primera respuesta es efectiva cuando desea fusionar stdout y stderr. Sin embargo, si necesita mantenerlos separados, ese enfoque no sirve de nada. Y ahora puedo ver, a partir de una lectura más cercana de su pregunta y sus comentarios, que desea mantener las dos secuencias de salida separadas.
Ahora, no es completamente sencillo extender mi primera respuesta para cubrir esto. El problema es que el código allí usa bloqueo de E / S. Y si necesita dar servicio a dos tuberías, hay un conflicto obvio. Una solución comúnmente utilizada en Windows es la E / S asíncrona, conocida en el mundo de Windows como E / S superpuesta. Sin embargo, la E / S asíncrona es mucho más compleja de implementar que el bloqueo de E / S.
Por lo tanto, voy a proponer un enfoque alternativo que todavía utilice bloqueo de E / S. Si queremos dar servicio a varios conductos, y queremos usar E / S de bloqueo, la conclusión obvia es que necesitamos un hilo para cada tubería. Esto es fácil de implementar, mucho más fácil que la opción asincrónica. Podemos usar código casi idéntico pero mover los bucles de lectura de bloqueo en hilos. Mi ejemplo, re-trabajado de esta manera, ahora se ve así:
{$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.
Si desea agregar soporte para cancelar, simplemente debe agregar una llamada a TerminateProcess
cuando el usuario cancela. Esto detendría todo, y la función devolvería el código de salida que proporcionó a TerminateProcess
. En este momento, no estoy seguro de sugerirle un marco de cancelación, pero creo que el código en esta respuesta está ahora muy cerca de cumplir con sus requisitos.