If I call ShellExecuteEx, why does GetExitCodeProcess say the program terminated even though it's still running?

StackOverflow https://stackoverflow.com/questions/19713326

Question

I want to open a file which is initially saved to SQL table, but is saved to disk before the call to ShellExecuteEx. Once it is saved I now have a valid file path and theoretically should be able to use this function to accomplish my goal. I need the program to open the file in its appropriate program and wait until that program closes before continuing. Currently the code will launch the correct application and open the passed file, but it does this without waiting (which I know because I display a message to indicate when the application terminates) and the app I wrote which is supposed to launch the correct program closes. It displays the message, then launches the program. I will admit that I do not fully understand how ShellExecuteEx works and have used code I found on the web in conjunction with my code to achieve the desired result. Below you will find the code. Any help would be greatly appreciated.

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;

I realized that the the file I was writing to disk hadn't finished which is why the message appeared before the program. Adding another application.Processmessages after the call to write the file to disk resolved this. GetExitCodeProcess still does not return the value of STILL_ACTIVE while the called app is open, though.

After doing more research I have decided to open a new question that is more pointed at what I am trying to accomplish. If anyone is interested in following it, you can find it here

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

Was it helpful?

Solution

As it stands, there's not enough information in the question to give you an answer that explains exactly what is happening in your scenario. You have not done enough debugging yet. So this answer will be of a more didactic nature. I'm going to attempt to teach you how to diagnose a problem of this nature.

First of all, let's clear up the obvious problems with your code, that are important but peripheral to the actual problem.

  • You are running a busy wait loop. That's always a bad idea. You are burning 100% CPU in the main GUI thread just waiting. There are two obvious ways to avoid that. Use a thread to perform the call to ShellExecuteEx and the subsequent wait, as suggested by Marko. Or use MsgWaitForMultipleObjects to perform a wait that can be interrupted in order to process input events.
  • If ShellExecuteEx does return a process handle to you, you leak it. When an API function returns a handle, you typically are responsible for closing it when you are done with it. That takes a call to CloseHandle. The documentation calls this out: The calling application is responsible for closing the handle when it is no longer needed.
  • You are not performing comprehensive error checking. You call a Win32 API function and fail to check its return value for an error. More on this later, but it's critical that you perform correct error checking.

Now, let's look closely at the problem you report in the question. The problem is that the call to ShellExecuteEx succeeds, the document is opened, but the busy wait returns before the process that displays the document closes. Your busy loop looks like this:

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

There's really not a lot of code here. If this loop returns earlier than you expect, how can that happen? The loop terminates when ExitCode <> STILL_ACTIVE or when Application.Terminated is True. The first thing to do is to isolate which of those conditions leads to termination of the loop. Some straightforward debugging would yield that information. I find it hard to believe that you accidentally terminated your application so I am going to proceed on the basis that Application.Terminated is False and the ExitCode test terminates the loop.

So, let's look again at the call to GetExitCodeProcess. This is immediately suspicious to me because you don't perform any error checking. Now, I happen to have extra information that you are perhaps lacking. Specifically that SEInfo.hProcess may not contain a process handle. That makes it easier for me to anticipate the problems, but you can learn all this under the debugger. From the documentation again:

A handle to the newly started application. This member is set on return and is always NULL unless fMask is set to SEE_MASK_NOCLOSEPROCESS. Even if fMask is set to SEE_MASK_NOCLOSEPROCESS, hProcess will be NULL if no process was launched. For example, if a document to be launched is a URL and an instance of Internet Explorer is already running, it will display the document. No new process is launched, and hProcess will be NULL.

Note ShellExecuteEx does not always return an hProcess, even if a process is launched as the result of the call. For example, an hProcess does not return when you use SEE_MASK_INVOKEIDLIST to invoke IContextMenu.

So, perhaps what is happening is that your document is being processed by the shell in a way such that the value of hProcess returned to you is NULL, i.e. 0. When that happens, the call to GetExitCodeProcess fails and returns False. That's the error condition that you did not check. You simply called GetExitCodeProcess and ignored whether or not it succeeded. If that call did not succeed, then ExitCode will not have been assigned a meaningful value and it is simply a mistake to attempt to read ExitCode at all. It only has meaning when GetExitCodeProcess returns True.

There's yet another failure mode that is even more subtle. It's possible that the call to ShellExecuteEx returns a valid process handle. But the process that is started deals with the request by passing it on to a different process and then terminating. From your perspective the process that displays the document is still running, but the process that ShellExecuteEx gives you has terminated. This commonly happens when an instance the process that displays the document is already running. For example, try calling your Fire function passing a .pas file when your Delphi IDE is open. A new process is started, but it immediately hands the file off to the running Delphi IDE and terminates.

OK, that's all I can think of for now. I hope that this helps you track down what's really happening in your scenario. It may not be the news you wish to hear though because I suspect that you will find that the approach of using ShellExecuteEx and waiting on the process handle that it returns will not meet your needs. Unfortunately opening a document using the shell is much more complicated than it might initially seem.

OTHER TIPS

Your repeat..until loop is wrong here. Avoid using Application.ProcessMessages() to make your application not block because it's bad practice and can/will cause unforeseen problems and weird bugs.

Instead, use thread to wait for app termination:

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.

Following line will launch thread:

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

And sample of callback function that would process thread result:

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;
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top