Setup.Install.HelperFunc.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. unit Setup.Install.HelperFunc;
  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. Installation helper functions which don't need install state such as UninstLog and RegisterFileList
  8. Only to be called by Setup.Install: if you want to reuse any of these functione from another unit
  9. you should move the function so somewhere else, like Setup.InstFunc
  10. }
  11. interface
  12. uses
  13. Windows, SHA256, Shared.FileClass, Shared.Struct, Setup.UninstallLog;
  14. type
  15. TSetupUninstallLog = class(TUninstallLog)
  16. protected
  17. procedure HandleException; override;
  18. end;
  19. TRegErrorFunc = (reRegSetValueEx, reRegCreateKeyEx, reRegOpenKeyEx);
  20. procedure SetFilenameLabelText(const S: String; const CallUpdate: Boolean);
  21. procedure SetStatusLabelText(const S: String;
  22. const ClearFilenameLabelText: Boolean = True);
  23. procedure InstallMessageBoxCallback(const Flags: LongInt; const After: Boolean;
  24. const Param: LongInt);
  25. procedure CalcFilesSize(var InstallFilesSize, AfterInstallFilesSize: Int64);
  26. procedure InitProgressGauge(const InstallFilesSize: Int64);
  27. procedure UpdateProgressGauge;
  28. procedure FinishProgressGauge(const HideGauge: Boolean);
  29. procedure SetProgress(const AProgress: Int64);
  30. procedure IncProgress(const N: Int64);
  31. function CurProgress: Int64;
  32. procedure ProcessEvents;
  33. procedure InternalProgressProc(const Bytes: Cardinal);
  34. procedure ExternalProgressProc64(const Bytes, MaxProgress: Int64);
  35. procedure JustProcessEventsProc64(const Bytes, Param: Int64);
  36. function AbortRetryIgnoreTaskDialogMsgBox(const Text: String;
  37. const RetryIgnoreAbortButtonLabels: array of String): Boolean;
  38. function FileTimeToStr(const AFileTime: TFileTime): String;
  39. function TryToGetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String;
  40. var Sum: TSHA256Digest): Boolean;
  41. procedure CopySourceFileToDestFile(const SourceF, DestF: TFile;
  42. [ref] const Verification: TSetupFileVerification; const ISSigSourceFilename: String;
  43. const AExpectedSize: Int64);
  44. function ShortenOrExpandFontFilename(const Filename: String): String;
  45. function GetLocalTimeAsStr: String;
  46. procedure PackCustomMessagesIntoString(var S: String);
  47. function PackCompiledCodeTextIntoString(const CompiledCodeText: AnsiString): String;
  48. procedure RegError(const Func: TRegErrorFunc; const RootKey: HKEY;
  49. const KeyName: String; const ErrorCode: Longint);
  50. procedure WriteMsgData(const F: TFile);
  51. procedure MarkExeHeader(const F: TFile; const ModeID: Longint);
  52. procedure ProcessInstallDeleteEntries;
  53. procedure ProcessNeedRestartEvent;
  54. procedure ProcessComponentEntries;
  55. procedure ProcessTasksEntries;
  56. procedure ShutdownApplications;
  57. implementation
  58. uses
  59. Classes, SysUtils, Forms,
  60. NewProgressBar, PathFunc, RestartManager, TaskbarProgressFunc,
  61. Shared.CommonFunc, Shared.CommonFunc.Vcl, Shared.SetupMessageIDs, Shared.SetupTypes,
  62. SetupLdrAndSetup.Messages,
  63. Setup.InstFunc, Setup.ISSigVerifyFunc, Setup.LoggingFunc, Setup.MainFunc, Setup.ScriptRunner,
  64. Setup.WizardForm;
  65. procedure TSetupUninstallLog.HandleException;
  66. begin
  67. Application.HandleException(Self);
  68. end;
  69. procedure SetFilenameLabelText(const S: String; const CallUpdate: Boolean);
  70. begin
  71. WizardForm.FilenameLabel.Caption := MinimizePathName(S, WizardForm.FilenameLabel.Font, WizardForm.FileNameLabel.Width);
  72. if CallUpdate then
  73. WizardForm.FilenameLabel.Update;
  74. end;
  75. procedure SetStatusLabelText(const S: String;
  76. const ClearFilenameLabelText: Boolean = True);
  77. begin
  78. if WizardForm.StatusLabel.Caption <> S then begin
  79. WizardForm.StatusLabel.Caption := S;
  80. WizardForm.StatusLabel.Update;
  81. end;
  82. if ClearFilenameLabelText then
  83. SetFilenameLabelText('', True);
  84. end;
  85. procedure InstallMessageBoxCallback(const Flags: LongInt; const After: Boolean;
  86. const Param: LongInt);
  87. const
  88. States: array [TNewProgressBarState] of TTaskbarProgressState =
  89. (tpsNormal, tpsError, tpsPaused);
  90. var
  91. NewState: TNewProgressBarState;
  92. begin
  93. if After then
  94. NewState := npbsNormal
  95. else if (Flags and MB_ICONSTOP) <> 0 then
  96. NewState := npbsError
  97. else
  98. NewState := npbsPaused;
  99. with WizardForm.ProgressGauge do begin
  100. State := NewState;
  101. Invalidate;
  102. end;
  103. SetAppTaskbarProgressState(States[NewState]);
  104. end;
  105. procedure CalcFilesSize(var InstallFilesSize, AfterInstallFilesSize: Int64);
  106. var
  107. N: Integer;
  108. CurFile: PSetupFileEntry;
  109. begin
  110. InstallFilesSize := 0;
  111. AfterInstallFilesSize := InstallFilesSize;
  112. for N := 0 to Entries[seFile].Count-1 do begin
  113. CurFile := PSetupFileEntry(Entries[seFile][N]);
  114. if ShouldProcessFileEntry(WizardComponents, WizardTasks, CurFile, False) then begin
  115. with CurFile^ do begin
  116. var FileSize: Int64;
  117. if LocationEntry <> -1 then { not an "external" file }
  118. FileSize := PSetupFileLocationEntry(Entries[seFileLocation][
  119. LocationEntry])^.OriginalSize
  120. else
  121. FileSize := ExternalSize;
  122. Inc(InstallFilesSize, FileSize);
  123. if not (foDeleteAfterInstall in Options) then
  124. Inc(AfterInstallFilesSize, FileSize);
  125. end;
  126. end;
  127. end;
  128. end;
  129. var
  130. CurProgressValue: Int64;
  131. ProgressShiftCount: Cardinal;
  132. procedure InitProgressGauge(const InstallFilesSize: Int64);
  133. begin
  134. { Calculate the MaxValue for the progress meter }
  135. var NewMaxValue: Int64 := 1000 * Entries[seIcon].Count;
  136. if Entries[seIni].Count <> 0 then Inc(NewMaxValue, 1000);
  137. if Entries[seRegistry].Count <> 0 then Inc(NewMaxValue, 1000);
  138. Inc(NewMaxValue, InstallFilesSize);
  139. { To avoid progress updates that are too small to result in any visible
  140. change, divide the Max value by 2 until it's under 1500 }
  141. ProgressShiftCount := 0;
  142. while NewMaxValue >= 1500 do begin
  143. NewMaxValue := NewMaxValue shr 1;
  144. Inc(ProgressShiftCount);
  145. end;
  146. WizardForm.ProgressGauge.Max := NewMaxValue;
  147. SetMessageBoxCallbackFunc(InstallMessageBoxCallback, 0);
  148. end;
  149. procedure UpdateProgressGauge;
  150. begin
  151. var NewPosition := CurProgressValue;
  152. NewPosition := NewPosition shr ProgressShiftCount;
  153. if WizardForm.ProgressGauge.Position <> NewPosition then begin
  154. WizardForm.ProgressGauge.Position := NewPosition;
  155. WizardForm.ProgressGauge.Update;
  156. end;
  157. SetAppTaskbarProgressValue(NewPosition, WizardForm.ProgressGauge.Max);
  158. if (CodeRunner <> nil) and CodeRunner.FunctionExists('CurInstallProgressChanged', True) then begin
  159. try
  160. CodeRunner.RunProcedures('CurInstallProgressChanged', [NewPosition,
  161. WizardForm.ProgressGauge.Max], False);
  162. except
  163. Log('CurInstallProgressChanged raised an exception.');
  164. Application.HandleException(nil);
  165. end;
  166. end;
  167. end;
  168. procedure FinishProgressGauge(const HideGauge: Boolean);
  169. begin
  170. SetMessageBoxCallbackFunc(nil, 0);
  171. if HideGauge then
  172. WizardForm.ProgressGauge.Visible := False;
  173. SetAppTaskbarProgressState(tpsNoProgress);
  174. end;
  175. procedure SetProgress(const AProgress: Int64);
  176. begin
  177. CurProgressValue := AProgress;
  178. UpdateProgressGauge;
  179. end;
  180. procedure IncProgress(const N: Int64);
  181. begin
  182. Inc(CurProgressValue, N);
  183. UpdateProgressGauge;
  184. end;
  185. function CurProgress: Int64;
  186. begin
  187. Result := CurProgressValue;
  188. end;
  189. procedure ProcessEvents;
  190. { Processes any waiting events. Must call this this periodically or else
  191. events like clicking the Cancel button won't be processed.
  192. Calls Abort if NeedToAbortInstall is True, which is usually the result of
  193. the user clicking Cancel and the form closing. }
  194. begin
  195. if NeedToAbortInstall then Abort;
  196. Application.ProcessMessages;
  197. if NeedToAbortInstall then Abort;
  198. end;
  199. procedure InternalProgressProc(const Bytes: Cardinal);
  200. begin
  201. IncProgress(Bytes);
  202. ProcessEvents;
  203. end;
  204. procedure ExternalProgressProc64(const Bytes, MaxProgress: Int64);
  205. begin
  206. var NewProgress := CurProgress;
  207. Inc(NewProgress, Bytes);
  208. { In case the source file was larger than we thought it was, stop the
  209. progress bar at the maximum amount. Also see CopySourceFileToDestFile. }
  210. if NewProgress > MaxProgress then
  211. NewProgress := MaxProgress;
  212. SetProgress(NewProgress);
  213. ProcessEvents;
  214. end;
  215. procedure JustProcessEventsProc64(const Bytes, Param: Int64);
  216. begin
  217. ProcessEvents;
  218. end;
  219. function AbortRetryIgnoreTaskDialogMsgBox(const Text: String;
  220. const RetryIgnoreAbortButtonLabels: array of String): Boolean;
  221. { Returns True if Ignore was selected, False if Retry was selected, or
  222. calls Abort if Abort was selected. }
  223. begin
  224. Result := False;
  225. case LoggedTaskDialogMsgBox('', SetupMessages[msgAbortRetryIgnoreSelectAction], Text, '',
  226. mbError, MB_ABORTRETRYIGNORE, RetryIgnoreAbortButtonLabels, 0, True, IDABORT) of
  227. IDABORT: Abort;
  228. IDRETRY: ;
  229. IDIGNORE: Result := True;
  230. else
  231. Log('LoggedTaskDialogMsgBox returned an unexpected value. Assuming Abort.');
  232. Abort;
  233. end;
  234. end;
  235. function FileTimeToStr(const AFileTime: TFileTime): String;
  236. { Converts a TFileTime into a string for log purposes. }
  237. var
  238. FT: TFileTime;
  239. ST: TSystemTime;
  240. begin
  241. FileTimeToLocalFileTime(AFileTime, FT);
  242. if FileTimeToSystemTime(FT, ST) then
  243. Result := Format('%.4u-%.2u-%.2u %.2u:%.2u:%.2u.%.3u',
  244. [ST.wYear, ST.wMonth, ST.wDay, ST.wHour, ST.wMinute, ST.wSecond,
  245. ST.wMilliseconds])
  246. else
  247. Result := '(invalid)';
  248. end;
  249. function TryToGetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String;
  250. var Sum: TSHA256Digest): Boolean;
  251. { Like GetSHA256OfFile but traps exceptions locally. Returns True if successful. }
  252. begin
  253. try
  254. Sum := GetSHA256OfFile(DisableFsRedir, Filename);
  255. Result := True;
  256. except
  257. Result := False;
  258. end;
  259. end;
  260. procedure CopySourceFileToDestFile(const SourceF, DestF: TFile;
  261. [ref] const Verification: TSetupFileVerification; const ISSigSourceFilename: String;
  262. const AExpectedSize: Int64);
  263. { Copies all bytes from SourceF to DestF, incrementing process meter as it
  264. goes. Assumes file pointers of both are 0. }
  265. var
  266. Buf: array[0..16383] of Byte;
  267. Context: TSHA256Context;
  268. begin
  269. var ExpectedFileHash: TSHA256Digest;
  270. if Verification.Typ <> fvNone then begin
  271. if Verification.Typ = fvHash then
  272. ExpectedFileHash := Verification.Hash
  273. else
  274. DoISSigVerify(SourceF, nil, ISSigSourceFilename, True, Verification.ISSigAllowedKeys, ExpectedFileHash);
  275. { ExpectedFileHash checked below after copy }
  276. SHA256Init(Context);
  277. end;
  278. var MaxProgress := CurProgress;
  279. Inc(MaxProgress, AExpectedSize);
  280. var BytesLeft := SourceF.Size;
  281. { To avoid file system fragmentation, preallocate all of the bytes in the
  282. destination file }
  283. DestF.Seek(BytesLeft);
  284. DestF.Truncate;
  285. DestF.Seek(0);
  286. while BytesLeft > 0 do begin
  287. var BufSize: Cardinal := SizeOf(Buf);
  288. if BytesLeft < BufSize then
  289. BufSize := Cardinal(BytesLeft);
  290. SourceF.ReadBuffer(Buf, BufSize);
  291. DestF.WriteBuffer(Buf, BufSize);
  292. Dec(BytesLeft, BufSize);
  293. if Verification.Typ <> fvNone then
  294. SHA256Update(Context, Buf, BufSize);
  295. ExternalProgressProc64(BufSize, MaxProgress);
  296. end;
  297. if Verification.Typ <> fvNone then begin
  298. if not SHA256DigestsEqual(SHA256Final(Context), ExpectedFileHash) then
  299. VerificationError(veFileHashIncorrect);
  300. Log(VerificationSuccessfulLogMessage);
  301. end;
  302. { In case the source file was shorter than we thought it was, bump the
  303. progress bar to the maximum amount }
  304. SetProgress(MaxProgress);
  305. end;
  306. function ShortenOrExpandFontFilename(const Filename: String): String;
  307. { Expands Filename, except if it's in the Fonts directory, in which case it
  308. removes the path }
  309. var
  310. FontDir: String;
  311. begin
  312. Result := PathExpand(Filename);
  313. FontDir := GetShellFolder(False, sfFonts);
  314. if FontDir <> '' then
  315. if PathCompare(PathExtractDir(Result), FontDir) = 0 then
  316. Result := PathExtractName(Result);
  317. end;
  318. function GetLocalTimeAsStr: String;
  319. var
  320. SysTime: TSystemTime;
  321. begin
  322. GetLocalTime(SysTime);
  323. SetString(Result, PChar(@SysTime), SizeOf(SysTime) div SizeOf(Char));
  324. end;
  325. procedure PackCustomMessagesIntoString(var S: String);
  326. var
  327. M: TMemoryStream;
  328. Count, I, N: Integer;
  329. begin
  330. M := TMemoryStream.Create;
  331. try
  332. Count := 0;
  333. M.WriteBuffer(Count, SizeOf(Count)); { overwritten later }
  334. for I := 0 to Entries[seCustomMessage].Count-1 do begin
  335. with PSetupCustomMessageEntry(Entries[seCustomMessage][I])^ do begin
  336. if (LangIndex = -1) or (LangIndex = ActiveLanguage) then begin
  337. N := Length(Name);
  338. M.WriteBuffer(N, SizeOf(N));
  339. M.WriteBuffer(Name[1], N*SizeOf(Name[1]));
  340. N := Length(Value);
  341. M.WriteBuffer(N, SizeOf(N));
  342. M.WriteBuffer(Value[1], N*SizeOf(Value[1]));
  343. Inc(Count);
  344. end;
  345. end;
  346. end;
  347. M.Seek(0, soFromBeginning);
  348. M.WriteBuffer(Count, SizeOf(Count));
  349. SetString(S, PChar(M.Memory), M.Size div SizeOf(Char));
  350. finally
  351. M.Free;
  352. end;
  353. end;
  354. function PackCompiledCodeTextIntoString(const CompiledCodeText: AnsiString): String;
  355. var
  356. N: Integer;
  357. begin
  358. N := Length(CompiledCodeText);
  359. if N mod 2 = 1 then
  360. Inc(N); { This will lead to 1 extra byte being moved but that's ok since it is the #0 }
  361. N := N div 2;
  362. SetString(Result, PChar(Pointer(CompiledCodeText)), N);
  363. end;
  364. procedure RegError(const Func: TRegErrorFunc; const RootKey: HKEY;
  365. const KeyName: String; const ErrorCode: Longint);
  366. const
  367. ErrorMsgs: array[TRegErrorFunc] of TSetupMessageID =
  368. (msgErrorRegWriteKey, msgErrorRegCreateKey, msgErrorRegOpenKey);
  369. FuncNames: array[TRegErrorFunc] of String =
  370. ('RegSetValueEx', 'RegCreateKeyEx', 'RegOpenKeyEx');
  371. begin
  372. raise Exception.Create(FmtSetupMessage(ErrorMsgs[Func],
  373. [GetRegRootKeyName(RootKey), KeyName]) + SNewLine2 +
  374. FmtSetupMessage(msgErrorFunctionFailedWithMessage,
  375. [FuncNames[Func], IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
  376. end;
  377. procedure WriteMsgData(const F: TFile);
  378. var
  379. MsgLangOpts: TMessagesLangOptions;
  380. LangEntry: PSetupLanguageEntry;
  381. begin
  382. FillChar(MsgLangOpts, SizeOf(MsgLangOpts), 0);
  383. MsgLangOpts.ID := MessagesLangOptionsID;
  384. StrPLCopy(MsgLangOpts.DialogFontName, LangOptions.DialogFontName,
  385. (SizeOf(MsgLangOpts.DialogFontName) div SizeOf(MsgLangOpts.DialogFontName[0])) - 1);
  386. MsgLangOpts.DialogFontSize := LangOptions.DialogFontSize;
  387. if LangOptions.RightToLeft then
  388. Include(MsgLangOpts.Flags, lfRightToLeft);
  389. LangEntry := Entries[seLanguage][ActiveLanguage];
  390. F.WriteBuffer(LangEntry.Data[1], Length(LangEntry.Data));
  391. F.WriteBuffer(MsgLangOpts, SizeOf(MsgLangOpts));
  392. end;
  393. procedure MarkExeHeader(const F: TFile; const ModeID: Longint);
  394. begin
  395. F.Seek(SetupExeModeOffset);
  396. F.WriteBuffer(ModeID, SizeOf(ModeID));
  397. end;
  398. procedure ProcessInstallDeleteEntries;
  399. var
  400. I: Integer;
  401. begin
  402. for I := 0 to Entries[seInstallDelete].Count-1 do
  403. with PSetupDeleteEntry(Entries[seInstallDelete][I])^ do
  404. if ShouldProcessEntry(WizardComponents, WizardTasks, Components, Tasks, Languages, Check) then begin
  405. DebugNotifyEntry(seInstallDelete, I);
  406. NotifyBeforeInstallEntry(BeforeInstall);
  407. case DeleteType of
  408. dfFiles, dfFilesAndOrSubdirs:
  409. DelTree(InstallDefaultDisableFsRedir, ExpandConst(Name), False, True, DeleteType = dfFilesAndOrSubdirs, False,
  410. nil, nil, nil);
  411. dfDirIfEmpty:
  412. DelTree(InstallDefaultDisableFsRedir, ExpandConst(Name), True, False, False, False, nil, nil, nil);
  413. end;
  414. NotifyAfterInstallEntry(AfterInstall);
  415. end;
  416. end;
  417. procedure ProcessNeedRestartEvent;
  418. begin
  419. if (CodeRunner <> nil) and CodeRunner.FunctionExists('NeedRestart', True) then begin
  420. if not NeedsRestart then begin
  421. try
  422. if CodeRunner.RunBooleanFunctions('NeedRestart', [''], bcTrue, False, False) then begin
  423. NeedsRestart := True;
  424. Log('Will restart because NeedRestart returned True.');
  425. end;
  426. except
  427. Log('NeedRestart raised an exception.');
  428. Application.HandleException(nil);
  429. end;
  430. end
  431. else
  432. Log('Not calling NeedRestart because a restart has already been deemed necessary.');
  433. end;
  434. end;
  435. procedure ProcessComponentEntries;
  436. var
  437. I: Integer;
  438. begin
  439. for I := 0 to Entries[seComponent].Count-1 do begin
  440. with PSetupComponentEntry(Entries[seComponent][I])^ do begin
  441. if ShouldProcessEntry(WizardComponents, nil, Name, '', Languages, '') and (coRestart in Options) then begin
  442. NeedsRestart := True;
  443. Break;
  444. end;
  445. end;
  446. end;
  447. end;
  448. procedure ProcessTasksEntries;
  449. var
  450. I: Integer;
  451. begin
  452. for I := 0 to Entries[seTask].Count-1 do begin
  453. with PSetupTaskEntry(Entries[seTask][I])^ do begin
  454. if ShouldProcessEntry(nil, WizardTasks, '', Name, Languages, '') and (toRestart in Options) then begin
  455. NeedsRestart := True;
  456. Break;
  457. end;
  458. end;
  459. end;
  460. end;
  461. procedure ShutdownApplications;
  462. const
  463. ERROR_FAIL_SHUTDOWN = 351;
  464. ForcedStrings: array [Boolean] of String = ('', ' (forced)');
  465. ForcedActionFlag: array [Boolean] of ULONG = (0, RmForceShutdown);
  466. var
  467. Forced: Boolean;
  468. Error: DWORD;
  469. begin
  470. Forced := InitForceCloseApplications or
  471. ((shForceCloseApplications in SetupHeader.Options) and not InitNoForceCloseApplications);
  472. Log('Shutting down applications using our files.' + ForcedStrings[Forced]);
  473. RmDoRestart := True;
  474. Error := RmShutdown(RmSessionHandle, ForcedActionFlag[Forced], nil);
  475. while Error = ERROR_FAIL_SHUTDOWN do begin
  476. Log('Some applications could not be shut down.');
  477. if AbortRetryIgnoreTaskDialogMsgBox(
  478. SetupMessages[msgErrorCloseApplications],
  479. [SetupMessages[msgAbortRetryIgnoreRetry], SetupMessages[msgAbortRetryIgnoreIgnore], SetupMessages[msgAbortRetryIgnoreCancel]]) then
  480. Break;
  481. Log('Retrying to shut down applications using our files.' + ForcedStrings[Forced]);
  482. Error := RmShutdown(RmSessionHandle, ForcedActionFlag[Forced], nil);
  483. end;
  484. { Close session on all errors except for ERROR_FAIL_SHUTDOWN, should still call RmRestart in that case. }
  485. if (Error <> ERROR_SUCCESS) and (Error <> ERROR_FAIL_SHUTDOWN) then begin
  486. RmEndSession(RmSessionHandle);
  487. LogFmt('RmShutdown returned an error: %d', [Error]);
  488. RmDoRestart := False;
  489. end;
  490. end;
  491. end.