Setup.SpawnServer.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. unit Setup.SpawnServer;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. Spawn server
  8. }
  9. interface
  10. uses
  11. Windows, SysUtils, Messages;
  12. type
  13. TSpawnServer = class
  14. private
  15. FWnd: HWND;
  16. FSequenceNumber: Word;
  17. FCallStatus: Word;
  18. FResultCode: DWORD;
  19. FExitNowRequested: Boolean;
  20. FExitNowExitCode: DWORD;
  21. function HandleExec(const IsShellExec: Boolean; const ADataPtr: Pointer;
  22. const ADataSize: Cardinal): LRESULT;
  23. procedure WndProc(var Message: TMessage);
  24. public
  25. constructor Create;
  26. destructor Destroy; override;
  27. property Wnd: HWND read FWnd;
  28. end;
  29. procedure EnterSpawnServerDebugMode;
  30. function NeedToRespawnSelfElevated(const ARequireAdministrator,
  31. AEmulateHighestAvailable: Boolean): Boolean;
  32. procedure RespawnSelfElevated(const AExeFilename, AParams: String;
  33. const ASpawnServer: TSpawnServer; var AExitCode: Integer);
  34. implementation
  35. { For debugging only; remove 'x' to enable the define: }
  36. {x$DEFINE SPAWNSERVER_RESPAWN_ALWAYS}
  37. uses
  38. Classes, Forms, ShellApi, PathFunc, Shared.CommonFunc,
  39. SetupLdrAndSetup.InstFunc, Setup.InstFunc, Setup.SpawnCommon;
  40. type
  41. TPtrAndSize = record
  42. Ptr: ^Byte;
  43. Size: Cardinal;
  44. end;
  45. procedure ProcessMessagesProc;
  46. begin
  47. Application.ProcessMessages;
  48. end;
  49. function ExtractBytes(var Data: TPtrAndSize; const Bytes: Cardinal;
  50. var Value: Pointer): Boolean;
  51. begin
  52. if Data.Size < Bytes then
  53. Result := False
  54. else begin
  55. Value := Data.Ptr;
  56. Dec(Data.Size, Bytes);
  57. Inc(Data.Ptr, Bytes);
  58. Result := True;
  59. end;
  60. end;
  61. function ExtractInteger(var Data: TPtrAndSize; var Value: Integer): Boolean;
  62. var
  63. P: Pointer;
  64. begin
  65. Result := ExtractBytes(Data, SizeOf(Integer), P);
  66. if Result then
  67. Value := Longint(P^);
  68. end;
  69. function ExtractString(var Data: TPtrAndSize; var Value: String): Boolean;
  70. var
  71. Len: Longint;
  72. P: Pointer;
  73. begin
  74. Result := ExtractInteger(Data, Len);
  75. if Result then begin
  76. if (Len < 0) or (Len > $FFFF) then
  77. Result := False
  78. else begin
  79. Result := ExtractBytes(Data, Cardinal(Len) * SizeOf(Value[1]), P);
  80. if Result then
  81. SetString(Value, PChar(P), Len);
  82. end;
  83. end;
  84. end;
  85. const
  86. TokenElevationTypeDefault = 1; { User does not have a split token (they're
  87. not an admin, or UAC is turned off) }
  88. TokenElevationTypeFull = 2; { Has split token, process running elevated }
  89. TokenElevationTypeLimited = 3; { Has split token, process not running
  90. elevated }
  91. function GetTokenElevationType: DWORD;
  92. { Returns token elevation type (TokenElevationType* constant). In case of
  93. failure, 0 is returned. }
  94. const
  95. TokenElevationType = 18;
  96. var
  97. Token: THandle;
  98. ElevationType: DWORD;
  99. ReturnLength: DWORD;
  100. begin
  101. Result := 0;
  102. if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then begin
  103. ElevationType := 0;
  104. if GetTokenInformation(Token, TTokenInformationClass(TokenElevationType),
  105. @ElevationType, SizeOf(ElevationType), ReturnLength) then
  106. Result := ElevationType;
  107. CloseHandle(Token);
  108. end;
  109. end;
  110. function NeedToRespawnSelfElevated(const ARequireAdministrator,
  111. AEmulateHighestAvailable: Boolean): Boolean;
  112. {$IFNDEF SPAWNSERVER_RESPAWN_ALWAYS}
  113. var
  114. ElevationType: DWORD;
  115. begin
  116. Result := False;
  117. if not IsAdminLoggedOn then begin
  118. if ARequireAdministrator then
  119. Result := True
  120. else if AEmulateHighestAvailable then begin
  121. { Emulate the "highestAvailable" requestedExecutionLevel: respawn if
  122. the user has a split token and the process isn't running elevated.
  123. (An inverted test for TokenElevationTypeLimited is used, so that if
  124. GetTokenElevationType unexpectedly fails or returns some value we
  125. don't recognize, we default to respawning.) }
  126. ElevationType := GetTokenElevationType;
  127. if (ElevationType <> TokenElevationTypeDefault) and
  128. (ElevationType <> TokenElevationTypeFull) then
  129. Result := True;
  130. end;
  131. end;
  132. end;
  133. {$ELSE}
  134. begin
  135. { For debugging/testing only: }
  136. Result := True;
  137. end;
  138. {$ENDIF}
  139. procedure RespawnSelfElevated(const AExeFilename, AParams: String;
  140. const ASpawnServer: TSpawnServer; var AExitCode: Integer);
  141. { Spawns a new process using the "runas" verb.
  142. Notes:
  143. 1. Despite the function's name, the spawned process may not actually be
  144. elevated / running as administrator. If UAC is disabled, "runas"
  145. behaves like "open". Also, if a non-admin user is a member of a special
  146. system group like Backup Operators, they can select their own user account
  147. at a UAC dialog. Therefore, it is critical that the caller include some
  148. kind of protection against respawning more than once.
  149. 2. If AExeFilename is on a network drive, the ShellExecuteEx function is
  150. smart enough to substitute it with a UNC path. }
  151. const
  152. SEE_MASK_NOZONECHECKS = $00800000;
  153. var
  154. ExpandedExeFilename, WorkingDir: String;
  155. Info: TShellExecuteInfo;
  156. WaitResult: DWORD;
  157. begin
  158. if not SameText(PathExtractExt(AExeFilename), '.exe') then
  159. InternalError('Cannot respawn self, not named .exe');
  160. ExpandedExeFilename := GetFinalFileName(AExeFilename);
  161. WorkingDir := GetFinalCurrentDir;
  162. FillChar(Info, SizeOf(Info), 0);
  163. Info.cbSize := SizeOf(Info);
  164. Info.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_FLAG_DDEWAIT or
  165. SEE_MASK_NOCLOSEPROCESS or SEE_MASK_NOZONECHECKS;
  166. Info.lpVerb := 'runas';
  167. Info.lpFile := PChar(ExpandedExeFilename);
  168. Info.lpParameters := PChar(AParams);
  169. Info.lpDirectory := PChar(WorkingDir);
  170. Info.nShow := SW_SHOWNORMAL;
  171. if not ShellExecuteEx(@Info) then begin
  172. { Don't display error message if user clicked Cancel at UAC dialog }
  173. if GetLastError = ERROR_CANCELLED then
  174. Abort;
  175. Win32ErrorMsg('ShellExecuteEx');
  176. end;
  177. if Info.hProcess = 0 then
  178. InternalError('ShellExecuteEx returned hProcess=0');
  179. { Wait for the process to terminate, processing messages in the meantime }
  180. try
  181. repeat
  182. ProcessMessagesProc;
  183. if Assigned(ASpawnServer) and ASpawnServer.FExitNowRequested then begin
  184. DWORD(AExitCode) := ASpawnServer.FExitNowExitCode;
  185. Exit;
  186. end;
  187. WaitResult := MsgWaitForMultipleObjects(1, Info.hProcess, False,
  188. INFINITE, QS_ALLINPUT);
  189. until WaitResult <> WAIT_OBJECT_0+1;
  190. if WaitResult = WAIT_FAILED then
  191. Win32ErrorMsg('MsgWaitForMultipleObjects');
  192. { Now that the process has exited, process any remaining messages.
  193. (If our window is handling notify messages (ANotifyWndPresent=False)
  194. then there may be an asynchronously-sent "restart request" message
  195. still queued if MWFMO saw the process terminate before checking for
  196. new messages.) }
  197. ProcessMessagesProc;
  198. if not GetExitCodeProcess(Info.hProcess, DWORD(AExitCode)) then
  199. Win32ErrorMsg('GetExitCodeProcess');
  200. finally
  201. CloseHandle(Info.hProcess);
  202. end;
  203. end;
  204. procedure EnterSpawnServerDebugMode;
  205. { For debugging purposes only: Creates a spawn server window, but does not
  206. start a new process. Displays the server window handle in the taskbar.
  207. Terminates when F11 is pressed. }
  208. var
  209. Server: TSpawnServer;
  210. begin
  211. Server := TSpawnServer.Create;
  212. try
  213. { The UInt32 cast prevents sign extension }
  214. Application.Title := Format('Wnd=$%x', [UInt32(Server.FWnd)]);
  215. while True do begin
  216. ProcessMessagesProc;
  217. if (GetFocus = Application.Handle) and (GetKeyState(VK_F11) < 0) then
  218. Break;
  219. WaitMessage;
  220. end;
  221. finally
  222. Server.Free;
  223. end;
  224. Halt(1);
  225. end;
  226. { TSpawnServer }
  227. constructor TSpawnServer.Create;
  228. begin
  229. inherited;
  230. FWnd := AllocateHWnd(WndProc);
  231. if FWnd = 0 then
  232. RaiseFunctionFailedError('AllocateHWnd');
  233. end;
  234. destructor TSpawnServer.Destroy;
  235. begin
  236. if FWnd <> 0 then
  237. DeallocateHWnd(FWnd);
  238. inherited;
  239. end;
  240. function TSpawnServer.HandleExec(const IsShellExec: Boolean;
  241. const ADataPtr: Pointer; const ADataSize: Cardinal): LRESULT;
  242. var
  243. Data: TPtrAndSize;
  244. EDisableFsRedir: Integer;
  245. EVerb, EFilename, EParams, EWorkingDir: String;
  246. EWait, EShowCmd: Integer;
  247. ClientCurrentDir, SaveCurrentDir: String;
  248. ExecResult: Boolean;
  249. begin
  250. { Recursive calls aren't supported }
  251. if FCallStatus = SPAWN_STATUS_RUNNING then begin
  252. Result := SPAWN_MSGRESULT_ALREADY_IN_CALL;
  253. Exit;
  254. end;
  255. Result := SPAWN_MSGRESULT_INVALID_DATA;
  256. Data.Ptr := ADataPtr;
  257. Data.Size := ADataSize;
  258. if IsShellExec then begin
  259. if not ExtractString(Data, EVerb) then Exit;
  260. end
  261. else begin
  262. if not ExtractInteger(Data, EDisableFsRedir) then Exit;
  263. end;
  264. if not ExtractString(Data, EFilename) then Exit;
  265. if not ExtractString(Data, EParams) then Exit;
  266. if not ExtractString(Data, EWorkingDir) then Exit;
  267. if not ExtractInteger(Data, EWait) then Exit;
  268. if not ExtractInteger(Data, EShowCmd) then Exit;
  269. if not ExtractString(Data, ClientCurrentDir) then Exit;
  270. if Data.Size <> 0 then Exit;
  271. Inc(FSequenceNumber);
  272. FResultCode := DWORD(-1);
  273. FCallStatus := SPAWN_STATUS_RUNNING;
  274. try
  275. SaveCurrentDir := GetCurrentDir;
  276. try
  277. SetCurrentDir(ClientCurrentDir);
  278. Result := SPAWN_MSGRESULT_SUCCESS_BITS or FSequenceNumber;
  279. { Send back the result code now to unblock the client }
  280. ReplyMessage(Result);
  281. if IsShellExec then begin
  282. ExecResult := InstShellExec(EVerb, EFilename, EParams, EWorkingDir,
  283. TExecWait(EWait), EShowCmd, ProcessMessagesProc, FResultCode);
  284. end
  285. else begin
  286. ExecResult := InstExec(EDisableFsRedir <> 0, EFilename, EParams, EWorkingDir,
  287. TExecWait(EWait), EShowCmd, ProcessMessagesProc, nil, FResultCode);
  288. end;
  289. if ExecResult then
  290. FCallStatus := SPAWN_STATUS_RETURNED_TRUE
  291. else
  292. FCallStatus := SPAWN_STATUS_RETURNED_FALSE;
  293. finally
  294. SetCurrentDir(SaveCurrentDir);
  295. end;
  296. finally
  297. { If the status is still SPAWN_STATUS_RUNNING here, then an unexpected
  298. exception must've occurred }
  299. if FCallStatus = SPAWN_STATUS_RUNNING then
  300. FCallStatus := SPAWN_STATUS_EXCEPTION;
  301. end;
  302. end;
  303. procedure TSpawnServer.WndProc(var Message: TMessage);
  304. var
  305. Res: LRESULT;
  306. begin
  307. case Message.Msg of
  308. WM_COPYDATA:
  309. begin
  310. try
  311. const CopyDataMsg = DWORD(TWMCopyData(Message).CopyDataStruct.dwData);
  312. case CopyDataMsg of
  313. CD_SpawnServer_Exec,
  314. CD_SpawnServer_ShellExec:
  315. begin
  316. Message.Result := HandleExec(
  317. TWMCopyData(Message).CopyDataStruct.dwData = CD_SpawnServer_ShellExec,
  318. TWMCopyData(Message).CopyDataStruct.lpData,
  319. TWMCopyData(Message).CopyDataStruct.cbData);
  320. end;
  321. end;
  322. except
  323. if ExceptObject is EOutOfMemory then
  324. Message.Result := SPAWN_MSGRESULT_OUT_OF_MEMORY
  325. else
  326. { Shouldn't get here; we don't explicitly raise any exceptions }
  327. Message.Result := SPAWN_MSGRESULT_UNEXPECTED_EXCEPTION;
  328. end;
  329. end;
  330. WM_SpawnServer_Query:
  331. begin
  332. Res := SPAWN_MSGRESULT_INVALID_SEQUENCE_NUMBER;
  333. if Message.LParam = FSequenceNumber then begin
  334. Res := SPAWN_MSGRESULT_INVALID_QUERY_OPERATION;
  335. const Operation = Integer(Message.WParam);
  336. case Operation of
  337. SPAWN_QUERY_STATUS:
  338. Res := SPAWN_MSGRESULT_SUCCESS_BITS or FCallStatus;
  339. SPAWN_QUERY_RESULTCODE_LO:
  340. Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Lo;
  341. SPAWN_QUERY_RESULTCODE_HI:
  342. Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Hi;
  343. end;
  344. end;
  345. Message.Result := Res;
  346. end;
  347. WM_SpawnServer_ExitNow:
  348. begin
  349. { Because this message is posted (not sent), RespawnSelfElevated's
  350. message loop will have to break out of a wait state to process it.
  351. After we return, the message loop checks FExitNowRequested. }
  352. if Message.LParam = SPAWN_EXITNOW_LPARAM_MAGIC then begin
  353. FExitNowExitCode := DWORD(Message.WParam);
  354. FExitNowRequested := True;
  355. end;
  356. end;
  357. else
  358. Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam,
  359. Message.LParam);
  360. end;
  361. end;
  362. end.