【问题标题】:How to redirect large amount of output from command executed by CreateProcess?如何重定向 CreateProcess 执行的命令的大量输出?
【发布时间】:2013-10-13 06:23:14
【问题描述】:

我需要从命令行运行一个 sqlite 备份命令。我不想使用“cmd /c”。命令是:

sqlite3.exe MYDB.db .dump > MYDB.bak

我在 SO 上找不到任何示例来说明如何执行此操作。

到目前为止,我从各种 SO 帖子中收集的代码是这样的,但非常不完整:

function StartProcess(const ACommandLine: string; AShowWindow: boolean = True;
  AWaitForFinish: boolean = False): Integer;
var
  CommandLine: string;
  StartupInfo: TStartupInfo;
  ProcessInformation: TProcessInformation;
  StdOutPipeRead, StdOutPipeWrite: THandle;
  Handle: boolean;
begin
   Result := 0;
   FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
   FillChar(ProcessInformation, SizeOf(TProcessInformation), 0);
   StartupInfo.cb := SizeOf(TStartupInfo);

   StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
   StartupInfo.hStdOutput := StdOutPipeWrite;
   StartupInfo.hStdError := StdOutPipeWrite;

   if not(AShowWindow) then
   begin
   StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
   StartupInfo.wShowWindow := SW_SHOWNORMAL;
   end;

   CommandLine := ACommandLine;
   UniqueString(CommandLine);
   Handle := CreateProcess(nil, PChar(CommandLine), nil, nil, False,
   CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation);

   CloseHandle(StdOutPipeWrite);

   if Handle then


   Result := ProcessInformation.dwProcessId;

   if AWaitForFinish then
   WaitForSingleObject(ProcessInformation.hProcess, INFINITE);

   CloseHandle(ProcessInformation.hProcess);
   CloseHandle(ProcessInformation.hThread);
end;

由于 dump 命令的输出非常大,我不知道如何从 stdout 捕获输出然后重定向它。重定向到什么?复制骗局?还是到 TFileStream.Write?

我见过这个post,但它在实现到输出文件的重定向方面是不完整的。我想我应该问“实现这一点的最有效方法是什么?”

如果有人以前这样做过,请发布一个代码示例来说明我是如何做到的。

TIA。

编辑:

根据 David Heffernan 的回答,这是我修改后确实可以正常工作的代码:

function StartProcessWithRedirectedOutput(const ACommandLine: string; const AOutputFile: string;
  AShowWindow: boolean = True; AWaitForFinish: boolean = False): Integer;
var
  CommandLine: string;
  StartupInfo: TStartupInfo;
  ProcessInformation: TProcessInformation;
  StdOutFileHandle: THandle;
  ProcessResult: boolean;
begin
  Result := 0;

  StdOutFileHandle := CreateFile(PChar(AOutputFile), GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL, 0);
  Win32Check(StdOutFileHandle <> INVALID_HANDLE_VALUE);

  Win32Check(SetHandleInformation(StdOutFileHandle, HANDLE_FLAG_INHERIT, 1));

  try
    FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
    FillChar(ProcessInformation, SizeOf(TProcessInformation), 0);

    StartupInfo.cb := SizeOf(TStartupInfo);
    StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESTDHANDLES;
    StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
    StartupInfo.hStdOutput := StdOutFileHandle;
    StartupInfo.hStdError := StdOutFileHandle;

    if not(AShowWindow) then
    begin
      StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESHOWWINDOW;
      StartupInfo.wShowWindow := SW_HIDE;
    end;

    CommandLine := ACommandLine;
    UniqueString(CommandLine);

    ProcessResult := Win32Check(CreateProcess(nil, PChar(CommandLine), nil, nil, True,
      CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation));

    if ProcessResult then
    begin
      try
        Result := ProcessInformation.dwProcessId;

        if AWaitForFinish then
          WaitForSingleObject(ProcessInformation.hProcess, INFINITE);

      finally
        if ProcessInformation.hProcess <> INVALID_HANDLE_VALUE then
          CloseHandle(ProcessInformation.hProcess);

        if ProcessInformation.hThread <> INVALID_HANDLE_VALUE then
          CloseHandle(ProcessInformation.hThread);
      end;
    end;

  finally
    CloseHandle(StdOutFileHandle);
  end;
end;

procedure TfAdmin.DoDBBackup(ADBBackupFile: String);
var
  b, p, q: String;
begin

  b := ExtractFilePath(ParamStr(0)) + 'PPDB.bak';
  p := ExtractFilePath(ParamStr(0)) + 'sqlite3.exe';
  q := ExtractFilePath(ParamStr(0)) + 'PPDB.db .dump';

  fMain.UniConnection1.Close;
  try
    StartProcessWithRedirectedOutput(p + ' ' + q, b, True, True);
  finally
    fMain.UniConnection1.Open;
  end;

  ZipMaster1.FSpecArgs.Add(b);
  ZipMaster1.ZipFileName := ADBBackupFile;
  ZipMaster1.Add;

  DeleteFile(b);

  ShowMessage('Backup complete!');

end;

【问题讨论】:

  • 另见stackoverflow.com/q/9119999/33732stackoverflow.com/q/19019714/33732。和stackoverflow.com/q/19054789/33732。这三个都包含运行控制台程序并使用您的程序捕获输出的代码。
  • 你抛出一个异常,然后调用exit。这不是异常的工作方式。一旦引发异常,控制就会向上移动异常链,最后是处理程序。此外,使用RaiseLastOSErrorWin32Check 来引发可以通过调用GetLastError 来识别的错误。
  • 现在您添加了对RaiseLastOSError 的同样毫无意义的调用,因为您已经引发了异常,所以它永远无法执行。您只需致电RaiseLastOSError。这就是引发异常的调用。就我个人而言,我会将整个 if 语句替换为 Win32Check(StdOutFileHandle &lt;&gt; INVALID_FILE_HANDLE);
  • 好的,现在对Win32Check 的调用更好,但在错误的地方。应该在try 之前。想想如果Win32Check 失败会发生什么。看看 finally 块。是的,你刚刚打电话给CloseHandle(INVALID_FILE_HANDLE)
  • 好的,还有一些错误处理。为什么CreateProcess的返回值叫Handle。该函数返回一个布尔值!变量被键入为布尔值?你为什么在致电CreateProcess 之前尝试一下。如果CreateProcess 失败,它不会初始化两个句柄,因此它们不应该被关闭。仅当您知道它们已被初始化时才关闭句柄。在检查 CreateProcess 返回 true 后尝试。你可以写Win32Check(CreateProces(...))

标签: delphi winapi


【解决方案1】:

为重定向创建一个文件句柄。这就是你的 cmd 脚本所做的。这将重定向到一个名为 'MYDB.bak' 的文件。

因此,调用CreateFile 以创建具有该名称的文件,并将返回的句柄分配为StartupInfo.hStdOutput。外部进程完成后,在文件句柄上调用CloseHandle 以关闭文件。您需要决定如何处理标准错误句柄。一种常见的选择是将其与标准输出合并。为hStdOutputhStdError 分配相同的句柄。

您的代码正在分配标准句柄,但没有要求外部进程使用它们。您需要在StartupInfo.dwFlags 中包含STARTF_USESTDHANDLES

CreateFile 的调用将如下所示:

StdOutFileHandle := CreateFile(
  'MYDB.bak',
  GENERIC_WRITE,
  FILE_SHARE_READ,
  nil,
  CREATE_ALWAYS,
  FILE_ATTRIBUTE_NORMAL,
  0
);

检查CreateFile返回的值不等于INVALID_HANDLE_VALUE

正如我在上一个问题中提到的,您需要外部进程来继承您传递给它的文件句柄。如果您不允许继承句柄,则外部进程无法使用您传递给它的句柄。所以将True 传递给CreateProcessbInheritHandles 参数。

CreateFile 创建的文件句柄默认情况下不可继承。您可以传递使其可继承的安全属性。或者您可以在创建后显式设置它。后者看起来像这样:

Win32Check(SetHandleInformation(StdOutFileHandle, HANDLE_FLAG_INHERIT, 1));

可以在我的回答中看到前者的示例(在管道的上下文中):How to redirect binary gbak output to a Delphi stream?

提到StdOutPipeWrite的代码都需要删除。它目前无法工作,因为您没有初始化句柄。

您应该充分利用try/finally,以确保即使在遇到异常时也不会泄漏任何句柄。

最后,您的代码包含很多错误,而错误检查很少。我建议您阅读并重新阅读CreateProcess 的文档。还可以在 MSDN 上阅读此示例:http://msdn.microsoft.com/en-us/library/windows/desktop/ms682499.aspx。尽管它使用管道,但原理是相同的。执行完全相同的操作,但使用调用 CreateProcess 返回的句柄代替管道。

【讨论】:

  • 感谢您的回复。我已根据您的回答编辑了问题以包含新代码。现在,它显示了一个输出滚动的窗口。这意味着有些事情仍然是错误的。需要什么改变来修复它?
  • 再次阅读我的所有答案。我知道我更新了它,但同样,你只接受了我提出的一些观点。例如,您不设置STARTF_USESTDHANDLES。另外,请阅读文档,并阅读我最后一段链接中的示例代码。而你的 finally 块是完全错误的。您在可能未初始化的句柄上调用 CloseHandle。当然,编译器会警告你。你启用警告了吗?
  • 代码已更新并建议更正,但仍无法正常工作。正在创建输出文件,但大小为 0 字节。
  • 您设置了StartupInfo.dwFlags := STARTF_USESHOWWINDOW,因此将设置删除为STARTF_USESTDHANDLES。使用逻辑or。但我怀疑代码没有执行。下一步是让您进行一些调试并添加一些错误检查。每次 API 调用后,检查返回值。阅读文档以了解如何解释返回值。例如,使用Win32Check 在失败时引发异常。在您开始出现错误之前了解代码的进展情况。
  • 抱歉,我花了这么长时间才意识到错误。我什至在答案中提到句柄需要是可继承的,然后没能做到!无论如何,这里有一些很好的教训。您在整理代码和添加错误检查方面做得非常好。这帮助我们消除了许多故障模式。这有助于缩小范围。谢谢!
【解决方案2】:

要获得更完整的答案,也可以说明输入重定向,我将发布我的代码。感谢 David Heffernan 的指导,没有他,这是不可能的。

代码涉及 SQLite 数据库的备份和恢复,方法是使用 CreateProcess 调用 Sqlite3.exe 可执行文件。显然输入和输出需要重定向到/从这个命令,下面的代码说明了如何实现这一点:

function StartProcessWithRedirectedOutput(const ACommandLine: string; const AOutputFile: string;
  AShowWindow: boolean = True; AWaitForFinish: boolean = False): Integer;
var
  CommandLine: string;
  StartupInfo: TStartupInfo;
  ProcessInformation: TProcessInformation;
  StdOutFileHandle: THandle;
begin
  Result := 0;

  StdOutFileHandle := CreateFile(PChar(AOutputFile), GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL, 0);
  Win32Check(StdOutFileHandle <> INVALID_HANDLE_VALUE);
  try
    Win32Check(SetHandleInformation(StdOutFileHandle, HANDLE_FLAG_INHERIT, 1));
    FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
    FillChar(ProcessInformation, SizeOf(TProcessInformation), 0);

    StartupInfo.cb := SizeOf(TStartupInfo);
    StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESTDHANDLES;
    StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
    StartupInfo.hStdOutput := StdOutFileHandle;
    StartupInfo.hStdError := StdOutFileHandle;

    if not(AShowWindow) then
    begin
      StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESHOWWINDOW;
      StartupInfo.wShowWindow := SW_HIDE;
    end;

    CommandLine := ACommandLine;
    UniqueString(CommandLine);

    Win32Check(CreateProcess(nil, PChar(CommandLine), nil, nil, True,
      CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation));

    try
      Result := ProcessInformation.dwProcessId;

      if AWaitForFinish then
        WaitForSingleObject(ProcessInformation.hProcess, INFINITE);

    finally
      CloseHandle(ProcessInformation.hProcess);
      CloseHandle(ProcessInformation.hThread);
    end;

  finally
    CloseHandle(StdOutFileHandle);
  end;
end;

function StartProcessWithRedirectedInput(const ACommandLine: string; const AInputFile: string;
  AShowWindow: boolean = True; AWaitForFinish: boolean = False): Integer;
var
  CommandLine: string;
  StartupInfo: TStartupInfo;
  ProcessInformation: TProcessInformation;
  StdInFileHandle: THandle;
begin
  Result := 0;

  StdInFileHandle := CreateFile(PChar(AInputFile), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, 0);
  Win32Check(StdInFileHandle <> INVALID_HANDLE_VALUE);

  try
    Win32Check(SetHandleInformation(StdInFileHandle, HANDLE_FLAG_INHERIT, 1));
    FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
    FillChar(ProcessInformation, SizeOf(TProcessInformation), 0);

    StartupInfo.cb := SizeOf(TStartupInfo);
    StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESTDHANDLES;
    StartupInfo.hStdInput := StdInFileHandle;
    StartupInfo.hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
    StartupInfo.hStdError := GetStdHandle(STD_OUTPUT_HANDLE);

    if not(AShowWindow) then
    begin
      StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESHOWWINDOW;
      StartupInfo.wShowWindow := SW_HIDE;
    end;

    CommandLine := ACommandLine;
    UniqueString(CommandLine);

    Win32Check(CreateProcess(nil, PChar(CommandLine), nil, nil, True,
      CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation));

    try
      Result := ProcessInformation.dwProcessId;

      if AWaitForFinish then
        WaitForSingleObject(ProcessInformation.hProcess, INFINITE);

    finally
      CloseHandle(ProcessInformation.hProcess);
      CloseHandle(ProcessInformation.hThread);
    end;

  finally
    CloseHandle(StdInFileHandle);
  end;
end;

procedure TfAdmin.DoDBBackup(ADBBackupFile: String);
var
  b, p, q: String;
begin
  b := ExtractFilePath(ParamStr(0)) + 'PPDB.bak';
  p := '"' + ExtractFilePath(ParamStr(0)) + 'sqlite3.exe"';
  q := '"' + ExtractFilePath(ParamStr(0)) + 'PPDB.db" .dump';

  fMain.UniConnection1.Close;
  try
    StartProcessWithRedirectedOutput(p + ' ' + q, b, True, True);
  finally
    fMain.UniConnection1.Open;
  end;

  ZipMaster1.FSpecArgs.Add(b);
  ZipMaster1.ZipFileName := ADBBackupFile;
  ZipMaster1.Add;

  DeleteFile(b);

  ShowMessage('Backup complete!');
end;

procedure TfAdmin.DoDBRestore(ADBBackupFile: String);
var
  b, p, q, q2, r: String;
begin
  b := ExtractFilePath(ParamStr(0)) + 'PPDB.bak';
  p := '"' + ExtractFilePath(ParamStr(0)) + 'sqlite3.exe"';
  q := '"' + ExtractFilePath(ParamStr(0)) + 'PPDB.db"';

  ZipMaster1.ExtrBaseDir := ExtractFilePath(ParamStr(0));
  ZipMaster1.ExtrOptions := [ExtrOverWrite];
  ZipMaster1.ZipFileName := ADBBackupFile;
  ZipMaster1.Extract;

  fMain.UniConnection1.Close;
  try
    q2 := StringReplace(q, '"', '', [rfReplaceAll]);
    r := ChangeFileExt(q2, '.db$');
    if FileExists(r) then
      DeleteFile(r);
    if not RenameFile(q2, r) then
      RaiseLastOSError;
    StartProcessWithRedirectedInput(p + ' ' + q, b, True, True);
  finally
    fMain.UniConnection1.Open;
  end;

  DeleteFile(b);

  ShowMessage('Restore complete!');

end;

【讨论】:

  • 我对这个系列和未来访问者的最后说明。不要使用带有输出重定向的 SQLite 命令行来备份 SQLite 数据库。更喜欢使用SQLite Online Backup API。代码可能比这短近 3 倍,最重要的是,可能是原生的!
  • 是的,第二个功能需要更正。我把它留给你!
  • 完成。我还对 DoDBBackup 和 DoDBRestore 中的代码进行了更正,以说明传递文件路径的正确方法。事实证明,如果传递给 CreateProcess 的可执行文件和参数(即 PChar(CommandLine))的路径包含空格,则会出现 Windows 错误。这可以通过将路径括在双引号中来解决。
  • @SteveF 这是意料之中的事。空格用作参数之间的分隔符。您不需要引用 CreateProcess IIRC 的第一个参数,因为它始终是单个路径。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
相关资源
最近更新 更多