Setup.SpawnServer.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. unit Setup.SpawnServer;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2024 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: Integer;
  19. FNotifyRestartRequested: Boolean;
  20. FNotifyNewLanguage: Integer;
  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 NotifyNewLanguage: Integer read FNotifyNewLanguage;
  28. property NotifyRestartRequested: Boolean read FNotifyRestartRequested;
  29. property Wnd: HWND read FWnd;
  30. end;
  31. procedure EnterSpawnServerDebugMode;
  32. function NeedToRespawnSelfElevated(const ARequireAdministrator,
  33. AEmulateHighestAvailable: Boolean): Boolean;
  34. procedure RespawnSelfElevated(const AExeFilename, AParams: String;
  35. var AExitCode: DWORD);
  36. implementation
  37. { For debugging only; remove 'x' to enable the define: }
  38. {x$DEFINE SPAWNSERVER_RESPAWN_ALWAYS}
  39. uses
  40. Classes, Forms, ShellApi, Shared.Int64Em, PathFunc, Shared.CommonFunc, Setup.InstFunc, Setup.SpawnCommon;
  41. type
  42. TPtrAndSize = record
  43. Ptr: ^Byte;
  44. Size: Cardinal;
  45. end;
  46. procedure ProcessMessagesProc;
  47. begin
  48. Application.ProcessMessages;
  49. end;
  50. function ExtractBytes(var Data: TPtrAndSize; const Bytes: Cardinal;
  51. var Value: Pointer): Boolean;
  52. begin
  53. if Data.Size < Bytes then
  54. Result := False
  55. else begin
  56. Value := Data.Ptr;
  57. Dec(Data.Size, Bytes);
  58. Inc(Data.Ptr, Bytes);
  59. Result := True;
  60. end;
  61. end;
  62. function ExtractLongint(var Data: TPtrAndSize; var Value: Longint): Boolean;
  63. var
  64. P: Pointer;
  65. begin
  66. Result := ExtractBytes(Data, SizeOf(Longint), P);
  67. if Result then
  68. Value := Longint(P^);
  69. end;
  70. function ExtractString(var Data: TPtrAndSize; var Value: String): Boolean;
  71. var
  72. Len: Longint;
  73. P: Pointer;
  74. begin
  75. Result := ExtractLongint(Data, Len);
  76. if Result then begin
  77. if (Len < 0) or (Len > $FFFF) then
  78. Result := False
  79. else begin
  80. Result := ExtractBytes(Data, Len * SizeOf(Value[1]), P);
  81. if Result then
  82. SetString(Value, PChar(P), Len);
  83. end;
  84. end;
  85. end;
  86. const
  87. TokenElevationTypeDefault = 1; { User does not have a split token (they're
  88. not an admin, or UAC is turned off) }
  89. TokenElevationTypeFull = 2; { Has split token, process running elevated }
  90. TokenElevationTypeLimited = 3; { Has split token, process not running
  91. elevated }
  92. function GetTokenElevationType: DWORD;
  93. { Returns token elevation type (TokenElevationType* constant). In case of
  94. failure, 0 is returned. }
  95. const
  96. TokenElevationType = 18;
  97. var
  98. Token: THandle;
  99. ElevationType: DWORD;
  100. ReturnLength: DWORD;
  101. begin
  102. Result := 0;
  103. if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then begin
  104. ElevationType := 0;
  105. if GetTokenInformation(Token, TTokenInformationClass(TokenElevationType),
  106. @ElevationType, SizeOf(ElevationType), ReturnLength) then
  107. Result := ElevationType;
  108. CloseHandle(Token);
  109. end;
  110. end;
  111. function NeedToRespawnSelfElevated(const ARequireAdministrator,
  112. AEmulateHighestAvailable: Boolean): Boolean;
  113. {$IFNDEF SPAWNSERVER_RESPAWN_ALWAYS}
  114. var
  115. ElevationType: DWORD;
  116. begin
  117. Result := False;
  118. if not IsAdminLoggedOn then begin
  119. if ARequireAdministrator then
  120. Result := True
  121. else if AEmulateHighestAvailable then begin
  122. { Emulate the "highestAvailable" requestedExecutionLevel: respawn if
  123. the user has a split token and the process isn't running elevated.
  124. (An inverted test for TokenElevationTypeLimited is used, so that if
  125. GetTokenElevationType unexpectedly fails or returns some value we
  126. don't recognize, we default to respawning.) }
  127. ElevationType := GetTokenElevationType;
  128. if (ElevationType <> TokenElevationTypeDefault) and
  129. (ElevationType <> TokenElevationTypeFull) then
  130. Result := True;
  131. end;
  132. end;
  133. end;
  134. {$ELSE}
  135. begin
  136. { For debugging/testing only: }
  137. Result := True;
  138. end;
  139. {$ENDIF}
  140. function GetFinalFileName(const Filename: String): String;
  141. { Calls GetFinalPathNameByHandle to expand any SUBST'ed drives, network drives,
  142. and symbolic links in Filename. This is needed for elevation to succeed when
  143. Setup is started from a SUBST'ed drive letter. }
  144. function ConvertToNormalPath(P: PChar): String;
  145. begin
  146. Result := P;
  147. if StrLComp(P, '\\?\', 4) = 0 then begin
  148. Inc(P, 4);
  149. if (PathStrNextChar(P) = P + 1) and (P[1] = ':') and PathCharIsSlash(P[2]) then
  150. Result := P
  151. else if StrLIComp(P, 'UNC\', 4) = 0 then begin
  152. Inc(P, 4);
  153. Result := '\\' + P;
  154. end;
  155. end;
  156. end;
  157. const
  158. FILE_SHARE_DELETE = $00000004;
  159. var
  160. GetFinalPathNameByHandleFunc: function(hFile: THandle; lpszFilePath: PWideChar;
  161. cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
  162. Attr, FlagsAndAttributes: DWORD;
  163. H: THandle;
  164. Res: Integer;
  165. Buf: array[0..4095] of Char;
  166. begin
  167. GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
  168. 'GetFinalPathNameByHandleW');
  169. if Assigned(GetFinalPathNameByHandleFunc) then begin
  170. Attr := GetFileAttributes(PChar(Filename));
  171. if Attr <> INVALID_FILE_ATTRIBUTES then begin
  172. { Backup semantics must be requested in order to open a directory }
  173. if Attr and FILE_ATTRIBUTE_DIRECTORY <> 0 then
  174. FlagsAndAttributes := FILE_FLAG_BACKUP_SEMANTICS
  175. else
  176. FlagsAndAttributes := 0;
  177. { Use zero access mask and liberal sharing mode to ensure success }
  178. H := CreateFile(PChar(Filename), 0, FILE_SHARE_READ or FILE_SHARE_WRITE or
  179. FILE_SHARE_DELETE, nil, OPEN_EXISTING, FlagsAndAttributes, 0);
  180. if H <> INVALID_HANDLE_VALUE then begin
  181. Res := GetFinalPathNameByHandleFunc(H, Buf, SizeOf(Buf) div SizeOf(Buf[0]), 0);
  182. CloseHandle(H);
  183. if (Res > 0) and (Res < (SizeOf(Buf) div SizeOf(Buf[0])) - 16) then begin
  184. { ShellExecuteEx fails with error 3 on \\?\UNC\ paths, so try to
  185. convert the returned path from \\?\ form }
  186. Result := ConvertToNormalPath(Buf);
  187. Exit;
  188. end;
  189. end;
  190. end;
  191. end;
  192. Result := Filename;
  193. end;
  194. function GetFinalCurrentDir: String;
  195. var
  196. Res: Integer;
  197. Buf: array[0..MAX_PATH-1] of Char;
  198. begin
  199. DWORD(Res) := GetCurrentDirectory(SizeOf(Buf) div SizeOf(Buf[0]), Buf);
  200. if (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0])) then
  201. Result := GetFinalFileName(Buf)
  202. else begin
  203. RaiseFunctionFailedError('GetCurrentDirectory');
  204. Result := '';
  205. end;
  206. end;
  207. procedure RespawnSelfElevated(const AExeFilename, AParams: String;
  208. var AExitCode: DWORD);
  209. { Spawns a new process using the "runas" verb.
  210. Notes:
  211. 1. Despite the function's name, the spawned process may not actually be
  212. elevated / running as administrator. If UAC is disabled, "runas"
  213. behaves like "open". Also, if a non-admin user is a member of a special
  214. system group like Backup Operators, they can select their own user account
  215. at a UAC dialog. Therefore, it is critical that the caller include some
  216. kind of protection against respawning more than once.
  217. 2. If AExeFilename is on a network drive, the ShellExecuteEx function is
  218. smart enough to substitute it with a UNC path. }
  219. const
  220. SEE_MASK_NOZONECHECKS = $00800000;
  221. var
  222. ExpandedExeFilename, WorkingDir: String;
  223. Info: TShellExecuteInfo;
  224. WaitResult: DWORD;
  225. begin
  226. if not SameText(PathExtractExt(AExeFilename), '.exe') then
  227. InternalError('Cannot respawn self, not named .exe');
  228. ExpandedExeFilename := GetFinalFileName(AExeFilename);
  229. WorkingDir := GetFinalCurrentDir;
  230. FillChar(Info, SizeOf(Info), 0);
  231. Info.cbSize := SizeOf(Info);
  232. Info.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_FLAG_DDEWAIT or
  233. SEE_MASK_NOCLOSEPROCESS or SEE_MASK_NOZONECHECKS;
  234. Info.lpVerb := 'runas';
  235. Info.lpFile := PChar(ExpandedExeFilename);
  236. Info.lpParameters := PChar(AParams);
  237. Info.lpDirectory := PChar(WorkingDir);
  238. Info.nShow := SW_SHOWNORMAL;
  239. if not ShellExecuteEx(@Info) then begin
  240. { Don't display error message if user clicked Cancel at UAC dialog }
  241. if GetLastError = ERROR_CANCELLED then
  242. Abort;
  243. Win32ErrorMsg('ShellExecuteEx');
  244. end;
  245. if Info.hProcess = 0 then
  246. InternalError('ShellExecuteEx returned hProcess=0');
  247. { Wait for the process to terminate, processing messages in the meantime }
  248. try
  249. repeat
  250. ProcessMessagesProc;
  251. WaitResult := MsgWaitForMultipleObjects(1, Info.hProcess, False,
  252. INFINITE, QS_ALLINPUT);
  253. until WaitResult <> WAIT_OBJECT_0+1;
  254. if WaitResult = WAIT_FAILED then
  255. Win32ErrorMsg('MsgWaitForMultipleObjects');
  256. { Now that the process has exited, process any remaining messages.
  257. (If our window is handling notify messages (ANotifyWndPresent=False)
  258. then there may be an asynchronously-sent "restart request" message
  259. still queued if MWFMO saw the process terminate before checking for
  260. new messages.) }
  261. ProcessMessagesProc;
  262. if not GetExitCodeProcess(Info.hProcess, AExitCode) then
  263. Win32ErrorMsg('GetExitCodeProcess');
  264. finally
  265. CloseHandle(Info.hProcess);
  266. end;
  267. end;
  268. procedure EnterSpawnServerDebugMode;
  269. { For debugging purposes only: Creates a spawn server window, but does not
  270. start a new process. Displays the server window handle in the taskbar.
  271. Terminates when F11 is pressed. }
  272. var
  273. Server: TSpawnServer;
  274. begin
  275. Server := TSpawnServer.Create;
  276. try
  277. Application.Title := Format('Wnd=$%x', [Server.FWnd]);
  278. while True do begin
  279. ProcessMessagesProc;
  280. if (GetFocus = Application.Handle) and (GetKeyState(VK_F11) < 0) then
  281. Break;
  282. WaitMessage;
  283. end;
  284. finally
  285. Server.Free;
  286. end;
  287. Halt(1);
  288. end;
  289. { TSpawnServer }
  290. constructor TSpawnServer.Create;
  291. begin
  292. inherited;
  293. FNotifyNewLanguage := -1;
  294. FWnd := AllocateHWnd(WndProc);
  295. if FWnd = 0 then
  296. RaiseFunctionFailedError('AllocateHWnd');
  297. end;
  298. destructor TSpawnServer.Destroy;
  299. begin
  300. if FWnd <> 0 then
  301. DeallocateHWnd(FWnd);
  302. inherited;
  303. end;
  304. function TSpawnServer.HandleExec(const IsShellExec: Boolean;
  305. const ADataPtr: Pointer; const ADataSize: Cardinal): LRESULT;
  306. var
  307. Data: TPtrAndSize;
  308. EDisableFsRedir: Longint;
  309. EVerb, EFilename, EParams, EWorkingDir: String;
  310. EWait, EShowCmd: Longint;
  311. ClientCurrentDir, SaveCurrentDir: String;
  312. ExecResult: Boolean;
  313. begin
  314. { Recursive calls aren't supported }
  315. if FCallStatus = SPAWN_STATUS_RUNNING then begin
  316. Result := SPAWN_MSGRESULT_ALREADY_IN_CALL;
  317. Exit;
  318. end;
  319. Result := SPAWN_MSGRESULT_INVALID_DATA;
  320. Data.Ptr := ADataPtr;
  321. Data.Size := ADataSize;
  322. if IsShellExec then begin
  323. if not ExtractString(Data, EVerb) then Exit;
  324. end
  325. else begin
  326. if not ExtractLongint(Data, EDisableFsRedir) then Exit;
  327. end;
  328. if not ExtractString(Data, EFilename) then Exit;
  329. if not ExtractString(Data, EParams) then Exit;
  330. if not ExtractString(Data, EWorkingDir) then Exit;
  331. if not ExtractLongint(Data, EWait) then Exit;
  332. if not ExtractLongint(Data, EShowCmd) then Exit;
  333. if not ExtractString(Data, ClientCurrentDir) then Exit;
  334. if Data.Size <> 0 then Exit;
  335. Inc(FSequenceNumber);
  336. FResultCode := -1;
  337. FCallStatus := SPAWN_STATUS_RUNNING;
  338. try
  339. SaveCurrentDir := GetCurrentDir;
  340. try
  341. SetCurrentDir(ClientCurrentDir);
  342. Result := SPAWN_MSGRESULT_SUCCESS_BITS or FSequenceNumber;
  343. { Send back the result code now to unblock the client }
  344. ReplyMessage(Result);
  345. if IsShellExec then begin
  346. ExecResult := InstShellExec(EVerb, EFilename, EParams, EWorkingDir,
  347. TExecWait(EWait), EShowCmd, ProcessMessagesProc, FResultCode);
  348. end
  349. else begin
  350. ExecResult := InstExec(EDisableFsRedir <> 0, EFilename, EParams, EWorkingDir,
  351. TExecWait(EWait), EShowCmd, ProcessMessagesProc, nil, FResultCode);
  352. end;
  353. if ExecResult then
  354. FCallStatus := SPAWN_STATUS_RETURNED_TRUE
  355. else
  356. FCallStatus := SPAWN_STATUS_RETURNED_FALSE;
  357. finally
  358. SetCurrentDir(SaveCurrentDir);
  359. end;
  360. finally
  361. { If the status is still SPAWN_STATUS_RUNNING here, then an unexpected
  362. exception must've occurred }
  363. if FCallStatus = SPAWN_STATUS_RUNNING then
  364. FCallStatus := SPAWN_STATUS_EXCEPTION;
  365. end;
  366. end;
  367. procedure TSpawnServer.WndProc(var Message: TMessage);
  368. var
  369. Res: LRESULT;
  370. begin
  371. case Message.Msg of
  372. WM_COPYDATA:
  373. begin
  374. try
  375. case TWMCopyData(Message).CopyDataStruct.dwData of
  376. CD_SpawnServer_Exec,
  377. CD_SpawnServer_ShellExec:
  378. begin
  379. Message.Result := HandleExec(
  380. TWMCopyData(Message).CopyDataStruct.dwData = CD_SpawnServer_ShellExec,
  381. TWMCopyData(Message).CopyDataStruct.lpData,
  382. TWMCopyData(Message).CopyDataStruct.cbData);
  383. end;
  384. end;
  385. except
  386. if ExceptObject is EOutOfMemory then
  387. Message.Result := SPAWN_MSGRESULT_OUT_OF_MEMORY
  388. else
  389. { Shouldn't get here; we don't explicitly raise any exceptions }
  390. Message.Result := SPAWN_MSGRESULT_UNEXPECTED_EXCEPTION;
  391. end;
  392. end;
  393. WM_SpawnServer_Query:
  394. begin
  395. Res := SPAWN_MSGRESULT_INVALID_SEQUENCE_NUMBER;
  396. if Message.LParam = FSequenceNumber then begin
  397. Res := SPAWN_MSGRESULT_INVALID_QUERY_OPERATION;
  398. case Message.WParam of
  399. SPAWN_QUERY_STATUS:
  400. Res := SPAWN_MSGRESULT_SUCCESS_BITS or FCallStatus;
  401. SPAWN_QUERY_RESULTCODE_LO:
  402. Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Lo;
  403. SPAWN_QUERY_RESULTCODE_HI:
  404. Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Hi;
  405. end;
  406. end;
  407. Message.Result := Res;
  408. end;
  409. WM_USER + 150: begin
  410. { Got a SetupNotifyWnd message. (See similar handling in SetupLdr.dpr) }
  411. if Message.WParam = 10000 then
  412. FNotifyRestartRequested := True
  413. else if Message.WParam = 10001 then
  414. FNotifyNewLanguage := Message.LParam;
  415. end;
  416. else
  417. Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam,
  418. Message.LParam);
  419. end;
  420. end;
  421. end.