SpawnServer.pas 14 KB

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