如果我调用ShellExecuteEx,为什么GetExitCodeProcess表示该程序已终止,即使它仍在运行?

时间:2013-10-31 17:45:39

标签: windows delphi winapi shellexecute

我想打开一个最初保存到SQL表的文件,但在调用ShellExecuteEx之前保存到磁盘。一旦保存,我现在有一个有效的文件路径,理论上应该能够使用此功能来实现我的目标。我需要程序在适当的程序中打开文件,并等到该程序关闭后再继续。目前代码将启动正确的应用程序并打开传递的文件,但它没有等待(我知道,因为我显示一条消息来指示应用程序何时终止)和我写的应该启动正确的程序关闭的应用程序。它显示消息,然后启动程序。我承认我并不完全理解ShellExecuteEx的工作原理,并且使用我在网上找到的代码和我的代码来实现所需的结果。您将在下面找到代码。任何帮助将不胜感激。

procedure Fire(FileStr: String);
var
  SEInfo: TShellExecuteInfo;
  ExitCode: DWORD;
  ExecuteFile, ParamString, StartInString: string;
begin
  ExecuteFile:= FileStr;
  FillChar(SEInfo, SizeOf(SEInfo), 0) ;
  SEInfo.cbSize := SizeOf(TShellExecuteInfo);
  with SEInfo do 
  begin
    fMask := SEE_MASK_NOCLOSEPROCESS;
    Wnd := Application.Handle;
    lpFile := PChar(ExecuteFile) ;
    nShow := SW_SHOWNORMAL;
  end;

  if ShellExecuteEx(@SEInfo) then
  begin
    repeat
      Application.ProcessMessages;
      GetExitCodeProcess(SEInfo.hProcess, ExitCode) ;
    until (ExitCode <> STILL_ACTIVE) or Application.Terminated;
    ShowMessage('App terminated') ;
  end
    else ShowMessage('Error starting App') ;
end;

我意识到我写入磁盘的文件还没有完成,这就是为什么消息出现在程序之前。在调用将文件写入磁盘后添加另一个application.Processmessages解决了这个问题。但是,当被叫应用程序打开时,GetExitCodeProcess仍然不会返回STILL_ACTIVE的值。

在做了更多研究之后,我决定开一个更加突出我想要完成的新问题。如果有人有兴趣关注它,你可以在这里找到它

How do I wait to delete a file until after the program I started has finished using it?

2 个答案:

答案 0 :(得分:4)

就目前而言,问题中的信息不足以为您提供一个答案,可以准确解释您的方案中发生的情况。你还没有完成足够的调试。所以这个答案将具有更多的教学性质。我将尝试教你如何诊断这种性质的问题。

首先,让我们清除代码中明显的问题,这些问题很重要,但却是实际问题的外围。

  • 您正在运行繁忙的等待循环。这总是一个坏主意。您正在等待主GUI线程中的100%CPU。有两种明显的方法可以避免这种情况。使用线程执行对ShellExecuteEx的调用以及随后的等待,如Marko所建议的那样。或者使用MsgWaitForMultipleObjects执行可以中断的等待以处理输入事件。
  • 如果ShellExecuteEx确实向您返回了一个处理句柄,则会泄漏它。当API函数返回句柄时,通常负责在完成后关闭它。这需要拨打CloseHandledocumentation将其调出:调用应用程序负责在不再需要时关闭句柄。
  • 您没有执行全面的错误检查。您调用Win32 API函数并且无法检查其返回值是否有错误。稍后会详细介绍,但执行正确的错误检查至关重要。

现在,让我们仔细看看您在问题中报告的问题。问题是对ShellExecuteEx的调用成功,文档被打开,但繁忙的等待在显示文档的进程关闭之前返回。繁忙的循环如下所示:

repeat
  Application.ProcessMessages;
  GetExitCodeProcess(SEInfo.hProcess, ExitCode);
until (ExitCode <> STILL_ACTIVE) or Application.Terminated;

这里真的没有很多代码。如果此循环返回的时间早于您的预期,那怎么会发生?循环在ExitCode <> STILL_ACTIVEApplication.TerminatedTrue时终止。首先要做的是隔离哪些条件导致循环终止。一些简单的调试将产生该信息。我发现很难相信您不小心终止了您的申请,因此我将继续Application.TerminatedFalse并且ExitCode测试终止循环。

那么,让我们再看一下GetExitCodeProcess的电话。这对我来说很可疑,因为您没有执行任何错误检查。现在,我碰巧有额外的信息,你可能缺乏。特别是SEInfo.hProcess可能不包含进程句柄。这使我更容易预测问题,但您可以在调试器下学习所有这些。再次从documentation开始:

  

新启动的应用程序的句柄。此成员已开启   除非fMask设置为,否则返回并始终为NULL   SEE_MASK_NOCLOSEPROCESS。即使fMask设置为   SEE_MASK_NOCLOSEPROCESS,如果没有进程,则hProcess将为NULL   推出。例如,如果要启动的文档是URL和   Internet Explorer的实例已在运行,它将显示   文献。没有启动新进程,并且hProcess将为NULL。

     

注意 ShellExecuteEx并不总是返回hProcess,即使作为调用结果启动了进程。例如,一个   使用SEE_MASK_INVOKEIDLIST调用时,hProcess不返回   IContextMenu。

所以,也许正在发生的事情是shell正在处理您的文档,使hProcess返回给您NULL的值为0,即GetExitCodeProcess。发生这种情况时,对False的调用将失败并返回GetExitCodeProcess。这是你没有检查的错误情况。您只需调用ExitCode并忽略它是否成功。如果该调用未成功,则ExitCode将不会被赋予有意义的值,并且根本不会尝试阅读GetExitCodeProcess。它仅在True返回ShellExecuteEx时才有意义。

还有另一种失败模式更加微妙。对ShellExecuteEx的调用可能会返回有效的进程句柄。但是,启动的进程通过将请求传递给另一个进程然后终止来处理请求。从您的角度来看,显示文档的过程仍在运行,但Fire给您的过程已终止。当显示文档的进程已在运行的实例时,通常会发生这种情况。例如,尝试在Delphi IDE打开时调用.pas函数传递ShellExecuteEx文件。启动了一个新进程,但它会立即将文件移交给正在运行的Delphi IDE并终止。

好的,这就是我现在所能想到的。我希望这可以帮助您找出您的方案中真正发生的事情。它可能不是您希望听到的新闻,因为我怀疑您会发现使用{{1}}并等待它返回的流程句柄的方法将无法满足您的需求。不幸的是,使用shell打开文档比最初看起来要复杂得多。

答案 1 :(得分:0)

此处repeat..until循环错误。避免使用Application.ProcessMessages()使您的应用程序不被阻止,因为它的不良做法会导致无法预料的问题和奇怪的错误。

相反,使用线程等待应用终止:

unit uSEWaitThread;

interface

uses
  Winapi.Windows, System.Classes;

type
  TSEWaitThread = class(TThread)
  private
    FFile     : String;
    FSESuccess: Boolean;
    FExitCode : DWORD;
    FParentWnd: HWND;

  protected
    procedure Execute; override;

  public
    constructor Create(const AFile: String; const AParentWindowHandle: HWND; const AOnDone: TNotifyEvent);

    property ExitCode           : DWORD read FExitCode;
    property ShellExecuteSuccess: Boolean read FSESuccess;
  end;

implementation

uses
  Winapi.ShellApi;


constructor TSEWaitThread.Create(const AFile: String; const AParentWindowHandle: HWND; const AOnDone: TNotifyEvent);
begin
  FreeOnTerminate := TRUE;

  FFile := AFile;
  FParentWND := AParentWindowHandle;
  FSESuccess := FALSE;
  FExitCode := 0;
  OnTerminate := AOnDone;

  inherited Create;
end;

procedure TSEWaitThread.Execute;
var
  exec_info: TShellExecuteInfo;
begin
  FillChar(exec_info, SizeOf(TShellExecuteInfo), 0);
  exec_info.cbSize := SizeOf(TShellExecuteInfo);
  exec_info.fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_FLAG_NO_UI;
  exec_info.Wnd := FParentWnd;
  exec_info.lpFile := PChar(FFile);
  exec_info.nShow := SW_SHOWNORMAL;

  if ShellExecuteEx(@exec_info) then
  begin
    FSESuccess := TRUE;
    WaitForSingleObject(exec_info.hProcess, INFINITE);
    GetExitCodeProcess(exec_info.hProcess, FExitCode);
  end;
end;

end.

以下行将启动线程:

TSEWaitThread.Create('C:\test.txt', Handle, SEWaitThreadDone);

将处理线程结果的回调函数示例:

procedure TForm1.SEWaitThreadDone(Sender: TObject);
var
  se_wait_thread: TSEWaitThread;
begin
  se_wait_thread := Sender as TSEWaitThread;

  if se_wait_thread.ShellExecuteSuccess then
  begin
    ShowMessage(Format('App exit code: %d', [se_wait_thread.ExitCode]));
  end
  else
    ShowMessage('Error Starting App');
end;