unelevated program starts an elevated updater, updater should wait for finishing of program

498 views Asked by At

I have 2 apps, program.exe and updater.exe, both written in Delphi5. Program runs without admin-rights (and without manifest), updater has a manifest with "requireAdministrator" because he must be able to write at Program-Folder to update program.exe.

The problem is to launch updater and let him wait until program is closed. I've found different ways at the web, but none works (in most cases the 1st app starts 2nd app and wait for ending of 2nd app, in my case 2nd app should wait for ending of 1nd app).

Updater should wait, thats easy
updater.exe

{$R manifest.res}
label.caption:='Wait for program.exe closing';
repeat
    sleep(1000);
until File is not open
ProgramHandle := Read Handle from File
WaitForSingleObject(ProgramHandle,INFINITE);
label.caption:='program.exe CLOSED';
Do updates

Way 1
Starting updater with CreateProcess:
program.exe

FillChar(siInfo, SizeOf(siInfo), 0);
siInfo.cb := SizeOf(siInfo);

saProcessAttributes.nLength := SizeOf(saProcessAttributes);
saProcessAttributes.lpSecurityDescriptor := nil;
saProcessAttributes.bInheritHandle := TRUE;

saThreadAttributes.nLength := SizeOf(saThreadAttributes);
saThreadAttributes.lpSecurityDescriptor := nil;
saThreadAttributes.bInheritHandle := True;

if CreateProcess(nil,
           PChar('updater.exe'),
           @saProcessAttributes,
           @saThreadAttributes,
           TRUE, NORMAL_PRIORITY_CLASS, nil,
           PChar(ExtractFilePath(Application.ExeName)),
           siInfo, piInfo) then
begin
DuplicateHandle(GetCurrentProcess, GetCurrentProcess,
                piInfo.hProcess, @MyHandle,
                PROCESS_QUERY_INFORMATION, TRUE,
                DUPLICATE_SAME_ACCESS) then
Write MyHandle in a File
end;
Close program

Doesn't do anything, works only when updater has no manifest with requireAdministrator into. If i run program with explizit admin-rights, it works too.

Way 2 Starting updater with ShellExecuteEx:
program.exe

  FillChar(Info, SizeOf(Info), Chr(0));
  Info.cbSize := SizeOf(Info);
  Info.fMask := SEE_MASK_NOCLOSEPROCESS;
  Info.lpVerb := PChar('runas');
  Info.lpFile := PChar('update.exe');
  Info.lpDirectory := nil;
  Info.nShow := SW_RESTORE;
  ShellExecuteEx(@Info);

  MyHandle:=OpenProcess(PROCESS_ALL_ACCESS, False, GetCurrentProcessId())));
  Write MyHandle in a File
  Close program

Doesnt' work, MyHandle has a different value each time i run this procedure (without restarting the program), so updater can't work with it.

So i have no idea how to start updater.exe and write the handle of program.exe in the file.

Im not very familiar with these parts of programing ... does somebody has an idea for my proplem?

3

There are 3 answers

3
EProgrammerNotFound On BEST ANSWER

Your code is not working because the handle table is per process, which means that the second process could have the same handle pointing to another kernel object. Below, there is one of many possible solutions:

When creating the process 2, pass the PID of the process 1 as parameter:

procedure CreateUpdater;
var
  Info: TShellExecuteInfo;
begin  
  FillChar(Info, SizeOf(TShellExecuteInfo), 0);
  Info.cbSize := SizeOf(TShellExecuteInfo);
  Info.fMask  := SEE_MASK_NOCLOSEPROCESS;
  Info.lpVerb := PChar('runas');
  Info.lpFile := PChar('Update.exe');
  Info.lpParameters := PChar(IntToStr(GetCurrentProcessId));
  Info.lpDirectory := nil;
  Info.nShow := SW_RESTORE;
  ShellExecuteEx(@Info);
  //NOTE: MISSING ERROR CHECKING!
end;

Inside the Updater, wait for the process1 to terminate:

procedure WaitForAndClose;
var
  PID: String;
  AHandle: Cardinal;
  Ret: longbool;
  ExitNumber: DWORD;
begin
  PID:= ParamStr(1);
  if PID <> '' then
  begin
    AHandle:= OpenProcess(PROCESS_QUERY_INFORMATION, False, StrToInt(PID));  
    //NOTE: MISSING ERROR CHECKING!
    try
      repeat
        Ret:= GetExitCodeProcess(AHandle, ExitNumber);
        //NOTE: MISSING ERROR CHECKING!
        Sleep(1000); //define a time to poolling
      until (ExitNumber <> STILL_ACTIVE);
    finally
      CloseHandle(AHandle);
    end;
    //Terminate the process;
    Application.Terminate;        
  end;
end;

You can also use WaitForSingleObject to avoid polling:

WaitForSingleObject(AHandle, INFINITE);
//NOTE: MISSING ERROR CHECKING!

But you need the SYNCHRONIZE access to open the process:

AHandle:= OpenProcess(SYNCHRONIZE, False, StrToInt(PID));
//NOTE: MISSING ERROR CHECKING!

Note: There is no error checking here. You should read the docs and properly check for errors.

Note 2: I would like to get your attention to the fact you are leaking a handle. When you use SEE_MASK_NOCLOSEPROCESS the caller is responsible to close the handle of the calee. In your case I think you don't need that mask at all. I would remove it.

0
Arioch 'The On

I see the main problem there in indeterminate order of two events: closing of the program and starting of the updater main code.

One possible way to fix it would be using Events - https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms686670(v=vs.85).aspx

The program creates the Event (with an option for the children to inherit it), then launches the updater (passing handles of both the Event and the program's process as integers via the command line to it !), then freezes in WaitForSingleObject on the Event.

This ensures the program would not exit before the updater would be ready to monitor it, so PID would not get invalid.

The updater then calls OpenProcess on the program's PID gained from the command line, then calls SignalAndWait both knocking the Event (gained from the command line) and freezing upon the handle (gained from OpenProcess) - https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms686293(v=vs.85).aspx

The program, now being released from waiting upon Event, terminates. The termination of the process is signalling it, so now the updater gets released in turn and can start doing the main work.


Another approach suggested at C++, How to determine if a Windows Process is running? is querying the exit code of the program ProcessID - it is said that while the program is still running there would be a specific error code and you can Sleep(100) then try again. Any other result means the program already had finished.

The program exits immediately after launching the updater without waiting for it to starting monitoring.

This seems nice approach except that I do not now any warranty that PID values would not be reused. Chances are infinitesimal, but still not zero.


Personally I would probably use a flag file. The CreateFile API has a very interesting flag - the temporary-file mode. It means, Windows would automatically delete the file after process ends. So then

  1. The program creates a new GUID using Windows API (or a new Random Value using Crypto API).
  2. The program creates in the temporary files folder the temporary-mode file with the name based upon the GUID or the Random value. If by any wild luck such a file already exist - you just obtain a new GUID or Random value.
  3. The program launches the updater and passes the filename to it via the command line
  4. The program exits immediately after launching the updater without waiting for it to starting monitoring.
  5. The updater keeps checking if the file exists (making pauses between attempts). When the file does not more exist - that means the program had finished and Windows auto-deleted it.

Again, there is an infinitesimal chance that some other process would create the flag file with exactly the same name in-between the program terminates and the updater checks again, but that is next to impossible in practice

0
kobik On

Here is a basic example of how to achieve this using events:

program.exe:

  // manual-reset event, non-signaled 
  Event := CreateEvent(nil, True, False, 'MyUniqueName');
  ExecuteUpdater; // via ShellExecuteEx with runas
  // synchronize - wait for the event to be signaled
  WaitForSingleObject(Event, INFINITE);
  // WAIT_OBJECT_0 = The state of the specified object is signaled.  
  CloseHandle(Event);

updater.exe:

  Event := CreateEvent(nil, True, False, 'MyUniqueName');
  if Event = 0 then RaiseLastWin32Error;
  SetEvent(Event); // sets the event object to the signaled state
  CloseHandle(Event);

You should also add a manifest to program.exe (requestedExecutionLevel should be level="asInvoker") to avoid virtualization.