Setup.Install.HelperFunc.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  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 := Integer(CurProgressValue shr ProgressShiftCount);
  152. if WizardForm.ProgressGauge.Position <> NewPosition then begin
  153. WizardForm.ProgressGauge.Position := NewPosition;
  154. WizardForm.ProgressGauge.Update;
  155. end;
  156. SetAppTaskbarProgressValue(NewPosition, WizardForm.ProgressGauge.Max);
  157. if (CodeRunner <> nil) and CodeRunner.FunctionExists('CurInstallProgressChanged', True) then begin
  158. try
  159. CodeRunner.RunProcedures('CurInstallProgressChanged', [NewPosition,
  160. WizardForm.ProgressGauge.Max], False);
  161. except
  162. Log('CurInstallProgressChanged raised an exception.');
  163. Application.HandleException(nil);
  164. end;
  165. end;
  166. end;
  167. procedure FinishProgressGauge(const HideGauge: Boolean);
  168. begin
  169. SetMessageBoxCallbackFunc(nil, 0);
  170. if HideGauge then
  171. WizardForm.ProgressGauge.Visible := False;
  172. SetAppTaskbarProgressState(tpsNoProgress);
  173. end;
  174. procedure SetProgress(const AProgress: Int64);
  175. begin
  176. CurProgressValue := AProgress;
  177. UpdateProgressGauge;
  178. end;
  179. procedure IncProgress(const N: Int64);
  180. begin
  181. Inc(CurProgressValue, N);
  182. UpdateProgressGauge;
  183. end;
  184. function CurProgress: Int64;
  185. begin
  186. Result := CurProgressValue;
  187. end;
  188. procedure ProcessEvents;
  189. { Processes any waiting events. Must call this this periodically or else
  190. events like clicking the Cancel button won't be processed.
  191. Calls Abort if NeedToAbortInstall is True, which is usually the result of
  192. the user clicking Cancel and the form closing. }
  193. begin
  194. if NeedToAbortInstall then Abort;
  195. Application.ProcessMessages;
  196. if NeedToAbortInstall then Abort;
  197. end;
  198. procedure InternalProgressProc(const Bytes: Cardinal);
  199. begin
  200. IncProgress(Bytes);
  201. ProcessEvents;
  202. end;
  203. procedure ExternalProgressProc64(const Bytes, MaxProgress: Int64);
  204. begin
  205. var NewProgress := CurProgress;
  206. Inc(NewProgress, Bytes);
  207. { In case the source file was larger than we thought it was, stop the
  208. progress bar at the maximum amount. Also see CopySourceFileToDestFile. }
  209. if NewProgress > MaxProgress then
  210. NewProgress := MaxProgress;
  211. SetProgress(NewProgress);
  212. ProcessEvents;
  213. end;
  214. procedure JustProcessEventsProc64(const Bytes, Param: Int64);
  215. begin
  216. ProcessEvents;
  217. end;
  218. function AbortRetryIgnoreTaskDialogMsgBox(const Text: String;
  219. const RetryIgnoreAbortButtonLabels: array of String): Boolean;
  220. { Returns True if Ignore was selected, False if Retry was selected, or
  221. calls Abort if Abort was selected. }
  222. begin
  223. Result := False;
  224. case LoggedTaskDialogMsgBox('', SetupMessages[msgAbortRetryIgnoreSelectAction], Text, '',
  225. mbError, MB_ABORTRETRYIGNORE, RetryIgnoreAbortButtonLabels, 0, True, IDABORT) of
  226. IDABORT: Abort;
  227. IDRETRY: ;
  228. IDIGNORE: Result := True;
  229. else
  230. Log('LoggedTaskDialogMsgBox returned an unexpected value. Assuming Abort.');
  231. Abort;
  232. end;
  233. end;
  234. function FileTimeToStr(const AFileTime: TFileTime): String;
  235. { Converts a TFileTime into a string for log purposes. }
  236. var
  237. FT: TFileTime;
  238. ST: TSystemTime;
  239. begin
  240. FileTimeToLocalFileTime(AFileTime, FT);
  241. if FileTimeToSystemTime(FT, ST) then
  242. Result := Format('%.4u-%.2u-%.2u %.2u:%.2u:%.2u.%.3u',
  243. [ST.wYear, ST.wMonth, ST.wDay, ST.wHour, ST.wMinute, ST.wSecond,
  244. ST.wMilliseconds])
  245. else
  246. Result := '(invalid)';
  247. end;
  248. function TryToGetSHA256OfFile(const DisableFsRedir: Boolean; const Filename: String;
  249. var Sum: TSHA256Digest): Boolean;
  250. { Like GetSHA256OfFile but traps exceptions locally. Returns True if successful. }
  251. begin
  252. try
  253. Sum := GetSHA256OfFile(DisableFsRedir, Filename);
  254. Result := True;
  255. except
  256. Result := False;
  257. end;
  258. end;
  259. procedure CopySourceFileToDestFile(const SourceF, DestF: TFile;
  260. [ref] const Verification: TSetupFileVerification; const ISSigSourceFilename: String;
  261. const AExpectedSize: Int64);
  262. { Copies all bytes from SourceF to DestF, incrementing process meter as it
  263. goes. Assumes file pointers of both are 0. }
  264. var
  265. Buf: array[0..16383] of Byte;
  266. Context: TSHA256Context;
  267. begin
  268. var ExpectedFileHash: TSHA256Digest;
  269. if Verification.Typ <> fvNone then begin
  270. if Verification.Typ = fvHash then
  271. ExpectedFileHash := Verification.Hash
  272. else
  273. DoISSigVerify(SourceF, nil, ISSigSourceFilename, True, Verification.ISSigAllowedKeys, ExpectedFileHash);
  274. { ExpectedFileHash checked below after copy }
  275. SHA256Init(Context);
  276. end;
  277. var MaxProgress := CurProgress;
  278. Inc(MaxProgress, AExpectedSize);
  279. var BytesLeft := SourceF.Size;
  280. { To avoid file system fragmentation, preallocate all of the bytes in the
  281. destination file }
  282. DestF.Seek(BytesLeft);
  283. DestF.Truncate;
  284. DestF.Seek(0);
  285. while BytesLeft > 0 do begin
  286. var BufSize: Cardinal := SizeOf(Buf);
  287. if BytesLeft < BufSize then
  288. BufSize := Cardinal(BytesLeft);
  289. SourceF.ReadBuffer(Buf, BufSize);
  290. DestF.WriteBuffer(Buf, BufSize);
  291. Dec(BytesLeft, BufSize);
  292. if Verification.Typ <> fvNone then
  293. SHA256Update(Context, Buf, BufSize);
  294. ExternalProgressProc64(BufSize, MaxProgress);
  295. end;
  296. if Verification.Typ <> fvNone then begin
  297. if not SHA256DigestsEqual(SHA256Final(Context), ExpectedFileHash) then
  298. VerificationError(veFileHashIncorrect);
  299. Log(VerificationSuccessfulLogMessage);
  300. end;
  301. { In case the source file was shorter than we thought it was, bump the
  302. progress bar to the maximum amount }
  303. SetProgress(MaxProgress);
  304. end;
  305. function ShortenOrExpandFontFilename(const Filename: String): String;
  306. { Expands Filename, except if it's in the Fonts directory, in which case it
  307. removes the path }
  308. var
  309. FontDir: String;
  310. begin
  311. Result := PathExpand(Filename);
  312. FontDir := GetShellFolder(False, sfFonts);
  313. if FontDir <> '' then
  314. if PathCompare(PathExtractDir(Result), FontDir) = 0 then
  315. Result := PathExtractName(Result);
  316. end;
  317. function GetLocalTimeAsStr: String;
  318. var
  319. SysTime: TSystemTime;
  320. begin
  321. GetLocalTime(SysTime);
  322. SetString(Result, PChar(@SysTime), SizeOf(SysTime) div SizeOf(Char));
  323. end;
  324. procedure PackCustomMessagesIntoString(var S: String);
  325. var
  326. M: TMemoryStream;
  327. Count, I, N: Integer;
  328. begin
  329. M := TMemoryStream.Create;
  330. try
  331. Count := 0;
  332. M.WriteBuffer(Count, SizeOf(Count)); { overwritten later }
  333. for I := 0 to Entries[seCustomMessage].Count-1 do begin
  334. with PSetupCustomMessageEntry(Entries[seCustomMessage][I])^ do begin
  335. if (LangIndex = -1) or (LangIndex = ActiveLanguage) then begin
  336. N := Length(Name);
  337. M.WriteBuffer(N, SizeOf(N));
  338. M.WriteBuffer(Name[1], N*SizeOf(Name[1]));
  339. N := Length(Value);
  340. M.WriteBuffer(N, SizeOf(N));
  341. M.WriteBuffer(Value[1], N*SizeOf(Value[1]));
  342. Inc(Count);
  343. end;
  344. end;
  345. end;
  346. M.Seek(0, soFromBeginning);
  347. M.WriteBuffer(Count, SizeOf(Count));
  348. SetString(S, PChar(M.Memory), M.Size div SizeOf(Char));
  349. finally
  350. M.Free;
  351. end;
  352. end;
  353. function PackCompiledCodeTextIntoString(const CompiledCodeText: AnsiString): String;
  354. var
  355. N: Integer;
  356. begin
  357. N := Length(CompiledCodeText);
  358. if N mod 2 = 1 then
  359. Inc(N); { This will lead to 1 extra byte being moved but that's ok since it is the #0 }
  360. N := N div 2;
  361. SetString(Result, PChar(Pointer(CompiledCodeText)), N);
  362. end;
  363. procedure RegError(const Func: TRegErrorFunc; const RootKey: HKEY;
  364. const KeyName: String; const ErrorCode: Longint);
  365. const
  366. ErrorMsgs: array[TRegErrorFunc] of TSetupMessageID =
  367. (msgErrorRegWriteKey, msgErrorRegCreateKey, msgErrorRegOpenKey);
  368. FuncNames: array[TRegErrorFunc] of String =
  369. ('RegSetValueEx', 'RegCreateKeyEx', 'RegOpenKeyEx');
  370. begin
  371. raise Exception.Create(FmtSetupMessage(ErrorMsgs[Func],
  372. [GetRegRootKeyName(RootKey), KeyName]) + SNewLine2 +
  373. FmtSetupMessage(msgErrorFunctionFailedWithMessage,
  374. [FuncNames[Func], IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
  375. end;
  376. procedure WriteMsgData(const F: TFile);
  377. var
  378. MsgLangOpts: TMessagesLangOptions;
  379. LangEntry: PSetupLanguageEntry;
  380. begin
  381. FillChar(MsgLangOpts, SizeOf(MsgLangOpts), 0);
  382. MsgLangOpts.ID := MessagesLangOptionsID;
  383. StrPLCopy(MsgLangOpts.DialogFontName, LangOptions.DialogFontName,
  384. (SizeOf(MsgLangOpts.DialogFontName) div SizeOf(MsgLangOpts.DialogFontName[0])) - 1);
  385. MsgLangOpts.DialogFontSize := LangOptions.DialogFontSize;
  386. if LangOptions.RightToLeft then
  387. Include(MsgLangOpts.Flags, lfRightToLeft);
  388. LangEntry := Entries[seLanguage][ActiveLanguage];
  389. F.WriteBuffer(LangEntry.Data[1], Length(LangEntry.Data));
  390. F.WriteBuffer(MsgLangOpts, SizeOf(MsgLangOpts));
  391. end;
  392. procedure MarkExeHeader(const F: TFile; const ModeID: Longint);
  393. begin
  394. F.Seek(SetupExeModeOffset);
  395. F.WriteBuffer(ModeID, SizeOf(ModeID));
  396. end;
  397. procedure ProcessInstallDeleteEntries;
  398. var
  399. I: Integer;
  400. begin
  401. for I := 0 to Entries[seInstallDelete].Count-1 do
  402. with PSetupDeleteEntry(Entries[seInstallDelete][I])^ do
  403. if ShouldProcessEntry(WizardComponents, WizardTasks, Components, Tasks, Languages, Check) then begin
  404. DebugNotifyEntry(seInstallDelete, I);
  405. NotifyBeforeInstallEntry(BeforeInstall);
  406. case DeleteType of
  407. dfFiles, dfFilesAndOrSubdirs:
  408. DelTree(InstallDefaultDisableFsRedir, ExpandConst(Name), False, True, DeleteType = dfFilesAndOrSubdirs, False,
  409. nil, nil, nil);
  410. dfDirIfEmpty:
  411. DelTree(InstallDefaultDisableFsRedir, ExpandConst(Name), True, False, False, False, nil, nil, nil);
  412. end;
  413. NotifyAfterInstallEntry(AfterInstall);
  414. end;
  415. end;
  416. procedure ProcessNeedRestartEvent;
  417. begin
  418. if (CodeRunner <> nil) and CodeRunner.FunctionExists('NeedRestart', True) then begin
  419. if not NeedsRestart then begin
  420. try
  421. if CodeRunner.RunBooleanFunctions('NeedRestart', [''], bcTrue, False, False) then begin
  422. NeedsRestart := True;
  423. Log('Will restart because NeedRestart returned True.');
  424. end;
  425. except
  426. Log('NeedRestart raised an exception.');
  427. Application.HandleException(nil);
  428. end;
  429. end
  430. else
  431. Log('Not calling NeedRestart because a restart has already been deemed necessary.');
  432. end;
  433. end;
  434. procedure ProcessComponentEntries;
  435. var
  436. I: Integer;
  437. begin
  438. for I := 0 to Entries[seComponent].Count-1 do begin
  439. with PSetupComponentEntry(Entries[seComponent][I])^ do begin
  440. if ShouldProcessEntry(WizardComponents, nil, Name, '', Languages, '') and (coRestart in Options) then begin
  441. NeedsRestart := True;
  442. Break;
  443. end;
  444. end;
  445. end;
  446. end;
  447. procedure ProcessTasksEntries;
  448. var
  449. I: Integer;
  450. begin
  451. for I := 0 to Entries[seTask].Count-1 do begin
  452. with PSetupTaskEntry(Entries[seTask][I])^ do begin
  453. if ShouldProcessEntry(nil, WizardTasks, '', Name, Languages, '') and (toRestart in Options) then begin
  454. NeedsRestart := True;
  455. Break;
  456. end;
  457. end;
  458. end;
  459. end;
  460. procedure ShutdownApplications;
  461. const
  462. ERROR_FAIL_SHUTDOWN = 351;
  463. ForcedStrings: array [Boolean] of String = ('', ' (forced)');
  464. ForcedActionFlag: array [Boolean] of ULONG = (0, RmForceShutdown);
  465. var
  466. Forced: Boolean;
  467. Error: DWORD;
  468. begin
  469. Forced := InitForceCloseApplications or
  470. ((shForceCloseApplications in SetupHeader.Options) and not InitNoForceCloseApplications);
  471. Log('Shutting down applications using our files.' + ForcedStrings[Forced]);
  472. RmDoRestart := True;
  473. Error := RmShutdown(RmSessionHandle, ForcedActionFlag[Forced], nil);
  474. while Error = ERROR_FAIL_SHUTDOWN do begin
  475. Log('Some applications could not be shut down.');
  476. if AbortRetryIgnoreTaskDialogMsgBox(
  477. SetupMessages[msgErrorCloseApplications],
  478. [SetupMessages[msgAbortRetryIgnoreRetry], SetupMessages[msgAbortRetryIgnoreIgnore], SetupMessages[msgAbortRetryIgnoreCancel]]) then
  479. Break;
  480. Log('Retrying to shut down applications using our files.' + ForcedStrings[Forced]);
  481. Error := RmShutdown(RmSessionHandle, ForcedActionFlag[Forced], nil);
  482. end;
  483. { Close session on all errors except for ERROR_FAIL_SHUTDOWN, should still call RmRestart in that case. }
  484. if (Error <> ERROR_SUCCESS) and (Error <> ERROR_FAIL_SHUTDOWN) then begin
  485. RmEndSession(RmSessionHandle);
  486. LogFmt('RmShutdown returned an error: %d', [Error]);
  487. RmDoRestart := False;
  488. end;
  489. end;
  490. end.