14

I'm trying to start another application from my program with elevated rights, and wait for it to terminate before continuing.

I've tried several different solutions on the web, but I can't find one that works exactly right.

The code below is the closest I have to working right. It runs the app with elevated privileges and waits for it to terminate, but it freezes once the external app is terminated. In other words, it doesn't keep processing once the launched app is closed.

How can I accomplish what I'm after here?

procedure TfMain.RunFileAsAdminWait(hWnd: HWND; aFile, aParameters: string);
var
  sei: TShellExecuteInfo;
begin
  FillChar(sei, SizeOf(sei), 0);
  sei.cbSize := SizeOf(sei);
  sei.Wnd := hWnd;
  sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
  sei.lpVerb := 'runas';
  sei.lpFile := PChar(aFile);
  sei.lpParameters := PChar(aParameters);
  sei.nShow := SW_SHOWNORMAL;

  if not ShellExecuteEx(@sei) then
    RaiseLastOSError
  else
    while WaitForSingleObject(sei.hProcess, 50) <> WAIT_OBJECT_0 do
      Application.ProcessMessages;

  CloseHandle(sei.hProcess);
end;

Update:

I've come up with the following function, but it only works if I have a ShowMessage statement after calling it. So, I have to have:

RunFileAsAdminWait(Handle, ExtractFilePath(Application.Exename) + 'AutoUpdate.exe', '/auto');
ShowMessage('test');

in order to make the function work. How can I make it work without the ShowMessage call?

Here's the updated function:

procedure TfMain.RunFileAsAdminWait(hWnd: HWND; aFile, aParameters: string);
var
  sei: TShellExecuteInfo;
begin
  FillChar(sei, SizeOf(sei), 0);
  sei.cbSize := SizeOf(sei);
  sei.Wnd := hWnd;
  sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
  sei.lpVerb := 'runas';
  sei.lpFile := PChar(aFile);
  sei.lpParameters := PChar(aParameters);
  sei.nShow := SW_SHOWNORMAL;

  if not ShellExecuteEx(@sei) then
    RaiseLastOSError
  else
    if sei.hProcess <> 0 then
      WaitForSingleObject(sei.hProcess, 50)
    else
      Exit;

  CloseHandle(sei.hProcess);
end;
croceldon
  • 4,511
  • 11
  • 57
  • 92

3 Answers3

15

The following code works for me:

procedure RunFileAsAdminWait(hWnd: HWND; aFile, aParameters: string);
var
  sei: TShellExecuteInfo;
begin
  FillChar(sei, SizeOf(sei), 0);
  sei.cbSize := SizeOf(sei);
  sei.Wnd := hWnd;
  sei.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_NOCLOSEPROCESS;
  sei.lpVerb := 'runas';
  sei.lpFile := PChar(aFile);
  sei.lpParameters := PChar(aParameters);
  sei.nShow := SW_SHOWNORMAL;

  if not ShellExecuteEx(@sei) then
    RaiseLastOSError;
  if sei.hProcess <> 0 then begin
    while WaitForSingleObject(sei.hProcess, 50) = WAIT_TIMEOUT do
      Application.ProcessMessages;
    CloseHandle(sei.hProcess);
  end;
end;

You have to pass the SEE_MASK_NOCLOSEPROCESS flag to get the process handle to wait for. I also changed the code to loop as long as WaitForSingleObject() returns with timeout.

For more information on the flags see the MSDN page for the SHELLEXECUTEINFO structure.

mghie
  • 32,028
  • 6
  • 87
  • 129
  • 1
    It would be more efficient to use `MsgWaitForMultipleObjects()` instead of `WaitForSingleObject()`, so you can call `Application.ProcessMessages()` only when it tells you that messages are waiting to be processed. Don't blindly call `ProcessMessages()` needlessly. – Remy Lebeau Dec 21 '14 at 20:59
5

The code in @mghie's answer has the right idea in general, but the code to process messages while waiting on the process handle could be better. Try this:

procedure RunFileAsAdminWait(hWnd: HWND; aFile, aParameters: string);
var
  sei: TShellExecuteInfo;
  Ret: DWORD;
begin
  FillChar(sei, SizeOf(sei), 0);
  sei.cbSize := SizeOf(sei);
  sei.Wnd := hWnd;
  sei.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_NOCLOSEPROCESS;
  sei.lpVerb := 'runas';
  sei.lpFile := PChar(aFile);
  sei.lpParameters := PChar(aParameters);
  sei.nShow := SW_SHOWNORMAL;

  if not ShellExecuteEx(@sei) then
    RaiseLastOSError;
  if sei.hProcess <> 0 then
  try
    repeat
      Ret := MsgWaitForMultipleObjects(1, sei.hProcess, False, INFINITE, QS_ALLINPUT);
      if Ret = (WAIT_OBJECT_0+1) then Application.ProcessMessages
      else if Ret = WAIT_FAILED then RaiseLastOSError;
    until Ret = WAIT_OBJECT_0;
  finally
    CloseHandle(sei.hProcess);
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
0

Your wait (50 ms is too short), try

WaitForSingleObject(sei.hProcess, INFINITE)

The check for valid process handle (sei.hProcess <> 0) can be left out.

Corrected Answer:

  while MsgWaitForMultipleObjects(1, sei.hProcess, False, INFINITE, QS_ALLINPUT)
    <> WAIT_OBJECT_0 do
  begin
    while PeekMessage(msg, 0, 0, 0, PM_REMOVE) do
    begin
      DispatchMessage(Msg);
    end;
  end;
Remko
  • 7,214
  • 2
  • 32
  • 52
  • 1
    That will cause the calling application to appear hung, as it will no longer process messages. – mghie Nov 11 '09 at 20:11
  • Sorry I overlooked the part about freezing, I corrected my answer – Remko Nov 11 '09 at 21:05
  • You should be processing messages only when `MsgWaitForMultipleObjects()` specifically returns `WAIT_OBJECT_0+1` only. Handle other return vales accordingly. – Remy Lebeau Dec 21 '14 at 21:01
  • @RemyLebeau Hi Remy, what is the correct answer for this, then? – Steve F Jan 24 '16 at 09:18
  • @Remko: the check for `hProcess <> 0` is needed, the documentation even says so: "*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... ShellExecuteEx does not always return an hProcess, even if a process is launched as the result of the call.*". – Remy Lebeau Jan 24 '16 at 22:27
  • @SteveF: see my answer. – Remy Lebeau Jan 24 '16 at 22:29