(* * Keyman is copyright (C) SIL Global. MIT License. * * Notes: For the state diagram in mermaid ../BackgroundUpdateStateDiagram.md *) unit Keyman.System.UpdateStateMachine; interface uses System.SysUtils, System.UITypes, System.IOUtils, System.Types, System.TypInfo, Sentry.Client, httpuploader, KeymanPaths, Keyman.Configuration.UI.UfrmStartInstall, Keyman.Configuration.UI.UfrmStartInstallNow, Keyman.System.ExecutionHistory, Keyman.System.UpdateCheckResponse, utilkmshell; type EUpdateStateMachine = class(Exception); TUpdateState = (usIdle, usUpdateAvailable, usDownloading, usWaitingRestart, usInstalling); // Forward declaration TUpdateStateMachine = class; { State Classes Update } TStateClass = class of TState; TState = class abstract private bucStateContext: TUpdateStateMachine; procedure ChangeState(newState: TStateClass); public constructor Create(Context: TUpdateStateMachine); procedure Enter; virtual; abstract; procedure Exit; virtual; abstract; procedure HandleCheck; virtual; abstract; function HandleKmShell: Integer; virtual; abstract; procedure HandleDownload; virtual; abstract; procedure HandleAbort; virtual; abstract; procedure HandleInstallNow; virtual; abstract; procedure HandleInstallPackages; virtual; procedure HandleFirstRun; virtual; end; { This class also controls the state flow see ../BackgroundUpdateStateDiagram.md } TUpdateStateMachine = class private FForce: Boolean; FAutomaticUpdate: Boolean; FErrorMessage: string; FShowErrors: Boolean; CurrentState: TState; // State object for performance (could lazy create?) FStateInstance: array [TUpdateState] of TState; function GetState: TStateClass; procedure SetState(const Value: TStateClass); procedure SetStateOnly(const enumState: TUpdateState); function ConvertStateToEnum(const StateClass: TStateClass): TUpdateState; function IsCurrentStateAssigned: Boolean; procedure HandleMSIInstallComplete; function SetRegistryState(Update: TUpdateState): Boolean; function GetAutomaticUpdates: Boolean; function SetApplyNow(Value: Boolean): Boolean; function GetApplyNow: Boolean; protected property State: TStateClass read GetState write SetState; public constructor Create(AForce: Boolean); destructor Destroy; override; procedure HandleCheck; function HandleKmShell: Integer; procedure HandleDownload; procedure HandleAbort; procedure HandleInstallNow; procedure HandleInstallPackages; procedure HandleFirstRun; function CurrentStateName: string; property ShowErrors: Boolean read FShowErrors write FShowErrors; function CheckRegistryState: TUpdateState; end; implementation uses System.Win.Registry, Winapi.Windows, Winapi.WinINet, ErrorControlledRegistry, GlobalProxySettings, kmint, keymanapi_TLB, Keyman.System.KeymanSentryClient, Keyman.System.DownloadUpdate, Keyman.System.RemoteUpdateCheck, Keyman.System.UpdateCheckStorage, KLog, RegistryKeys, utilexecute, utiluac; const SPackageUpgradeFilename = 'upgrade_packages.inf'; kmShellContinue = 0; kmShellExit = 1; { State Class Memebers } constructor TState.Create(Context: TUpdateStateMachine); begin inherited Create; bucStateContext := Context; end; procedure TState.ChangeState(newState: TStateClass); begin bucStateContext.State := newState; end; type // Derived classes for each state IdleState = class(TState) public procedure Enter; override; procedure Exit; override; procedure HandleCheck; override; function HandleKmShell: Integer; override; procedure HandleDownload; override; procedure HandleAbort; override; procedure HandleInstallNow; override; end; UpdateAvailableState = class(TState) private procedure StartDownloadProcess; public procedure Enter; override; procedure Exit; override; procedure HandleCheck; override; function HandleKmShell: Integer; override; procedure HandleDownload; override; procedure HandleAbort; override; procedure HandleInstallNow; override; end; DownloadingState = class(TState) private function DownloadUpdatesBackground: Boolean; procedure Enter; override; procedure Exit; override; procedure HandleCheck; override; function HandleKmShell: Integer; override; procedure HandleDownload; override; procedure HandleAbort; override; procedure HandleInstallNow; override; end; WaitingRestartState = class(TState) public procedure Enter; override; procedure Exit; override; procedure HandleCheck; override; function HandleKmShell: Integer; override; procedure HandleDownload; override; procedure HandleAbort; override; procedure HandleInstallNow; override; end; InstallingState = class(TState) private (** * Installs the Keyman setup file using separate shell. * * @params SavePath The path to the downloaded files. * * @returns True if the installation is successful, False otherwise. *) function DoInstallKeyman: Boolean; overload; (** * Installs the Keyman Keyboard files using separate shell. * * @params SavePath The path to the downloaded files. * * @returns True if the installation is successful, False otherwise. *) function DoInstallPackages(Params: TUpdateCheckResponse): Boolean; function DoInstallPackage(PackageFileName: String): Boolean; procedure LaunchInstallPackageProcess; public procedure Enter; override; procedure Exit; override; procedure HandleCheck; override; function HandleKmShell: Integer; override; procedure HandleDownload; override; procedure HandleAbort; override; procedure HandleInstallNow; override; procedure HandleInstallPackages; override; procedure HandleFirstRun; override; end; { TUpdateStateMachine } constructor TUpdateStateMachine.Create(AForce: Boolean); begin inherited Create; FShowErrors := True; FForce := AForce; FAutomaticUpdate := GetAutomaticUpdates; FStateInstance[usIdle] := IdleState.Create(Self); FStateInstance[usUpdateAvailable] := UpdateAvailableState.Create(Self); FStateInstance[usDownloading] := DownloadingState.Create(Self); FStateInstance[usWaitingRestart] := WaitingRestartState.Create(Self); FStateInstance[usInstalling] := InstallingState.Create(Self); // Check the Registry setting. SetStateOnly(CheckRegistryState); end; destructor TUpdateStateMachine.Destroy; var lpState: TUpdateState; begin if (FErrorMessage <> '') and FShowErrors then TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, '"+FErrorMessage+"'); for lpState := Low(TUpdateState) to High(TUpdateState) do begin FreeAndNil(FStateInstance[lpState]); end; // TODO: #10210 TODO: epic-windows-update remove debugging comments throughout this Unit. // KL.Log('TUpdateStateMachine.Destroy: FErrorMessage = '+FErrorMessage); // KL.Log('TUpdateStateMachine.Destroy: FParams.Result = '+IntToStr(Ord(FParams.Result))); inherited Destroy; end; function TUpdateStateMachine.SetRegistryState(Update: TUpdateState): Boolean; var UpdateStr: string; Registry: TRegistryErrorControlled; begin Result := False; Registry := TRegistryErrorControlled.Create; try Registry.RootKey := HKEY_CURRENT_USER; if not Registry.OpenKey(SRegKey_KeymanEngine_CU, True) then begin TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Failed to open registry key: "' + SRegKey_KeymanEngine_CU + '"'); Exit; end; try UpdateStr := GetEnumName(TypeInfo(TUpdateState), Ord(Update)); Registry.WriteString(SRegValue_Update_State, UpdateStr); Result := True; except on E: ERegistryException do begin TKeymanSentryClient.ReportHandledException(E, 'Failed to write install state machine state'); end; end; finally Registry.Free; end; end; function TUpdateStateMachine.CheckRegistryState: TUpdateState; var UpdateState: TUpdateState; Registry: TRegistryErrorControlled; StateValue: string; EnumValue: Integer; begin // Default to Idle state if any issues occur UpdateState := usIdle; Registry := TRegistryErrorControlled.Create; try Registry.RootKey := HKEY_CURRENT_USER; if Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) and Registry.ValueExists(SRegValue_Update_State) then begin try StateValue := Registry.ReadString(SRegValue_Update_State); EnumValue := GetEnumValue(TypeInfo(TUpdateState), StateValue); // Bounds Check EnumValue against TUpdateState if (EnumValue >= Ord(Low(TUpdateState))) and (EnumValue <= Ord(High(TUpdateState))) then UpdateState := TUpdateState(EnumValue) else UpdateState := usIdle; // Default if out of bounds except on E: ERegistryException do begin TKeymanSentryClient.ReportHandledException(E, 'Failed to read install state machine state'); UpdateState := usIdle; end; end; end; finally Registry.Free; end; Result := UpdateState; end; function TUpdateStateMachine.GetAutomaticUpdates: Boolean; // I2329 var Registry: TRegistryErrorControlled; begin // check the registry value Registry := TRegistryErrorControlled.Create; // I2890 try Registry.RootKey := HKEY_CURRENT_USER; try Result := not Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) or not Registry.ValueExists(SRegValue_AutomaticUpdates) or Registry.ReadBool(SRegValue_AutomaticUpdates); except on E: ERegistryException do begin TKeymanSentryClient.ReportHandledException(E, 'Failed to read automatic updates'); Result := False; end; end; finally Registry.Free; end; end; function TUpdateStateMachine.SetApplyNow(Value: Boolean): Boolean; var Registry: TRegistryErrorControlled; begin Result := False; Registry := TRegistryErrorControlled.Create; try Registry.RootKey := HKEY_CURRENT_USER; if not Registry.OpenKey(SRegKey_KeymanEngine_CU, True) then begin Exit; end; try Registry.WriteBool(SRegValue_ApplyNow, Value); Result := True; except on E: ERegistryException do begin TKeymanSentryClient.ReportHandledException(E, 'Failed to write "apply now"'); end; end; finally Registry.Free; end; end; function TUpdateStateMachine.GetApplyNow: Boolean; var Registry: TRegistryErrorControlled; begin // check the registry value Registry := TRegistryErrorControlled.Create; try Registry.RootKey := HKEY_CURRENT_USER; try Result := Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) and Registry.ValueExists(SRegValue_ApplyNow) and Registry.ReadBool(SRegValue_ApplyNow); except on E: ERegistryException do begin KL.Log('Failed to read registry: ' + E.Message); Result := False; end; end; finally Registry.Free; end; end; function TUpdateStateMachine.GetState: TStateClass; begin if Assigned(CurrentState) then Result := TStateClass(CurrentState.ClassType) else begin TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Error CurrentState was uninitiallised'); Result := nil; end; end; procedure TUpdateStateMachine.SetState(const Value: TStateClass); begin if Assigned(CurrentState) then begin CurrentState.Exit; end; SetStateOnly(ConvertStateToEnum(Value)); if Assigned(CurrentState) then begin CurrentState.Enter; end else begin // TODO: #10210 Error log for Unable to set state for Value end; end; procedure TUpdateStateMachine.SetStateOnly(const enumState: TUpdateState); begin CurrentState := FStateInstance[enumState]; end; function TUpdateStateMachine.ConvertStateToEnum(const StateClass: TStateClass) : TUpdateState; begin if StateClass = IdleState then Result := usIdle else if StateClass = UpdateAvailableState then Result := usUpdateAvailable else if StateClass = DownloadingState then Result := usDownloading else if StateClass = WaitingRestartState then Result := usWaitingRestart else if StateClass = InstallingState then Result := usInstalling else begin Result := usIdle; TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Unknown State Machine class'); end; end; function TUpdateStateMachine.IsCurrentStateAssigned: Boolean; begin if Assigned(CurrentState) then Result := True else begin TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Error CurrentState was uninitiallised'); Result := False; end; end; procedure TUpdateStateMachine.HandleMSIInstallComplete; var SavePath: string; FileName: String; FileNames: TStringDynArray; begin SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); KL.Log('TUpdateStateMachine.HandleMSIInstallComplete'); GetFileNamesInDirectory(SavePath, FileNames); for FileName in FileNames do begin System.SysUtils.DeleteFile(FileName); end; CurrentState.ChangeState(IdleState); end; procedure TUpdateStateMachine.HandleCheck; begin if not IsCurrentStateAssigned then Exit; CurrentState.HandleCheck; end; function TUpdateStateMachine.HandleKmShell: Integer; begin if not IsCurrentStateAssigned then Exit(kmShellContinue); Result := CurrentState.HandleKmShell; end; procedure TUpdateStateMachine.HandleDownload; begin if not IsCurrentStateAssigned then Exit; CurrentState.HandleDownload; end; procedure TUpdateStateMachine.HandleAbort; begin if not IsCurrentStateAssigned then Exit; CurrentState.HandleAbort; end; procedure TUpdateStateMachine.HandleInstallNow; begin if not IsCurrentStateAssigned then Exit; CurrentState.HandleInstallNow; end; procedure TUpdateStateMachine.HandleInstallPackages; begin CurrentState.HandleInstallPackages; end; procedure TUpdateStateMachine.HandleFirstRun; begin CurrentState.HandleFirstRun; end; function TUpdateStateMachine.CurrentStateName: string; begin if not IsCurrentStateAssigned then Exit('Undefined'); Result := CurrentState.ClassName; end; // base implmentation to be overiden procedure TState.HandleInstallPackages; begin // Do Nothing end; procedure TState.HandleFirstRun; begin // If Handle First run hits base implementation // something is wrong. TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Handle first run called in state:"' + Self.ClassName + '"'); bucStateContext.HandleMSIInstallComplete; end; { IdleState } procedure IdleState.Enter; begin // Enter UpdateAvailableState bucStateContext.SetRegistryState(usIdle); end; procedure IdleState.Exit; begin end; procedure IdleState.HandleCheck; var CheckForUpdates: TRemoteUpdateCheck; Result: TRemoteUpdateCheckResult; begin { ##### For Testing only just advancing to downloading #### } // ChangeState(UpdateAvailableState); // will keep here as there are more PR's #12621 { #### End of Testing ### }; { // // TODO-WINDOWS-UPDATES Check how long a check takes then determine if it needs to be broken into a seperate state of WaitngCheck RESP } { if Response not OK stay in the idle state and return } // Handle_check event force check CheckForUpdates := TRemoteUpdateCheck.Create(True); try Result := CheckForUpdates.Run; finally CheckForUpdates.Free; end; { Response OK and Update is available } if Result = wucSuccess then begin ChangeState(UpdateAvailableState); end; // else staty in idle state end; function IdleState.HandleKmShell; var CheckForUpdates: TRemoteUpdateCheck; UpdateCheckResult: TRemoteUpdateCheckResult; begin // Remote manages the last check time therfore // we will allow it to return early if it hasn't reached // the configured time between checks. CheckForUpdates := TRemoteUpdateCheck.Create(False); try UpdateCheckResult := CheckForUpdates.Run; finally CheckForUpdates.Free; end; { Response OK and Update is available } if UpdateCheckResult = wucSuccess then begin ChangeState(UpdateAvailableState); end; Result := kmShellContinue; end; procedure IdleState.HandleDownload; begin // Do Nothing end; procedure IdleState.HandleAbort; begin end; procedure IdleState.HandleInstallNow; begin bucStateContext.CurrentState.HandleCheck; // TODO: How do we notify the command line no update available end; { UpdateAvailableState } procedure UpdateAvailableState.StartDownloadProcess; var FResult: Boolean; RootPath: string; begin // call separate process RootPath := ExtractFilePath(ParamStr(0)); FResult := TUtilExecute.ShellCurrentUser(0, ParamStr(0), IncludeTrailingPathDelimiter(RootPath), '-bd'); if not FResult then begin TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Executing kmshell process to download updated Failed'); ChangeState(IdleState); end; end; procedure UpdateAvailableState.Enter; begin // Enter UpdateAvailableState bucStateContext.SetRegistryState(usUpdateAvailable); if bucStateContext.FAutomaticUpdate then begin StartDownloadProcess; end; end; procedure UpdateAvailableState.Exit; begin // Exit UpdateAvailableState end; procedure UpdateAvailableState.HandleCheck; begin end; function UpdateAvailableState.HandleKmShell; begin if bucStateContext.FAutomaticUpdate then begin // we will use a new kmshell process to enable // the download as background process. StartDownloadProcess; end; Result := kmShellContinue; end; procedure UpdateAvailableState.HandleDownload; begin ChangeState(DownloadingState); end; procedure UpdateAvailableState.HandleAbort; begin end; procedure UpdateAvailableState.HandleInstallNow; var frmStartInstallNow: TfrmStartInstallNow; InstallNow: Boolean; begin InstallNow := True; if HasKeymanRun then begin // TODO: epic-update-windows UI and non-UI units should be split // if the unit launches UI then it should be a .UI. unit // https://github.com/keymanapp/keyman/pull/12375/files#r1751041747 frmStartInstallNow := TfrmStartInstallNow.Create(nil); try if frmStartInstallNow.ShowModal = mrOk then InstallNow := True else InstallNow := False; finally frmStartInstallNow.Free; end; end; // If user decides NOT to install now stay in UpdateAvailable State if InstallNow = True then begin bucStateContext.SetApplyNow(True); ChangeState(DownloadingState); end; end; { DownloadingState } procedure DownloadingState.Enter; var DownloadResult: Boolean; RetryCount: Integer; begin // Enter DownloadingState bucStateContext.SetRegistryState(usDownloading); { ## for testing log that we would download } KL.Log('DownloadingState.Enter test code continue'); DownloadResult := True; { End testing } RetryCount := 0; //DownloadResult := False; while (not DownloadResult) and (RetryCount < 3) do begin DownloadResult := DownloadUpdatesBackground; if not DownloadResult then Inc(RetryCount); end; if (not DownloadResult) then begin // Failed three times in this process; return to the // IdleState to wait 'CheckPeriod' before trying again TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Error Updates not downloaded after 3 attempts'); ChangeState(IdleState); end else begin if HasKeymanRun then begin if bucStateContext.GetApplyNow then begin bucStateContext.SetApplyNow(False); ChangeState(InstallingState); end else ChangeState(WaitingRestartState); end else begin ChangeState(InstallingState); end; end end; procedure DownloadingState.Exit; begin // Exit DownloadingState end; procedure DownloadingState.HandleCheck; begin end; function DownloadingState.HandleKmShell; begin // Downloading state, in other process, so continue Result := kmShellContinue; end; procedure DownloadingState.HandleDownload; begin // Enter Already Downloading end; procedure DownloadingState.HandleAbort; begin end; procedure DownloadingState.HandleInstallNow; begin // Already downloading set the registry apply now bucStateContext.SetApplyNow(True); end; function DownloadingState.DownloadUpdatesBackground: Boolean; var DownloadResult: Boolean; DownloadUpdate: TDownloadUpdate; begin DownloadUpdate := TDownloadUpdate.Create; try DownloadResult := DownloadUpdate.DownloadUpdates; Result := DownloadResult; finally DownloadUpdate.Free; end; end; { WaitingRestartState } procedure WaitingRestartState.Enter; begin // Enter WaitingRestartState bucStateContext.SetRegistryState(usWaitingRestart); end; procedure WaitingRestartState.Exit; begin // Exit DownloadingState end; procedure WaitingRestartState.HandleCheck; begin end; function WaitingRestartState.HandleKmShell; var SavedPath: String; FileNames: TStringDynArray; frmStartInstall: TfrmStartInstall; begin // Still can't go if keyman has run if HasKeymanRun then begin Result := kmShellContinue; // Exit; // Exit is not wokring for some reason. // this else is only here because the exit is not working. end else begin // Check downloaded cache if available then SavedPath := IncludeTrailingPathDelimiter (TKeymanPaths.KeymanUpdateCachePath); GetFileNamesInDirectory(SavedPath, FileNames); if Length(FileNames) = 0 then begin // Return to Idle state and check for Updates state ChangeState(IdleState); bucStateContext.CurrentState.HandleCheck; // TODO no event here Result := kmShellExit; // Exit; // again exit was not working end else begin frmStartInstall := TfrmStartInstall.Create(nil); try if frmStartInstall.ShowModal = mrOk then begin ChangeState(InstallingState); Result := kmShellExit; end else Result := kmShellContinue; finally frmStartInstall.Free; end; end; end; end; procedure WaitingRestartState.HandleDownload; begin end; procedure WaitingRestartState.HandleAbort; begin end; procedure WaitingRestartState.HandleInstallNow; // If user decides not to install now stay in WaitingRestart State var frmStartInstallNow: TfrmStartInstallNow; InstallNow: Boolean; begin InstallNow := True; if HasKeymanRun then begin frmStartInstallNow := TfrmStartInstallNow.Create(nil); try if frmStartInstallNow.ShowModal = mrOk then InstallNow := True else InstallNow := False; finally frmStartInstallNow.Free; end; end; if InstallNow = True then begin bucStateContext.SetApplyNow(True); ChangeState(InstallingState); end; end; // Installing packages needs to be elevated procedure InstallingState.LaunchInstallPackageProcess; var executeResult: Cardinal; begin if not kmcom.SystemInfo.IsAdministrator then begin KL.Log('InstallingState.LaunchInstallPackageProcess not IsAdmin'); if CanElevate then begin KL.Log('InstallingState.LaunchInstallPackageProcess CanElevate'); executeResult := WaitForElevatedConfiguration(0, '-ikp'); if (executeResult <> 0) then begin TKeymanSentryClient.Client.MessageEvent (Sentry.Client.SENTRY_LEVEL_ERROR, 'Executing kmshell process to install keyboard packages failed:"' + IntToStr(Ord(executeResult)) + '"'); KL.Log('InstallingState.LaunchInstallPackageProcess Error elevating'); ChangeState(IdleState); end; end else begin KL.Log('InstallingState.LaunchInstallPackageProcess require user with admin'); // TODO: epic-windows-updates How do we alert the user that package requires a user with admin rights // ShowMessage('Some of these updates require an Administrator to complete installation. Please login as an Administrator and re-run the update.'); end; end else begin KL.Log('InstallingState.LaunchInstallPackageProcess HandlePackages straight away'); HandleInstallPackages; // we can install packages straight away end; end; function InstallingState.DoInstallKeyman: Boolean; var FResult: Boolean; SavePath: String; fileExt: String; FileName: String; FileNames: TStringDynArray; found: Boolean; begin SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); GetFileNamesInDirectory(SavePath, FileNames); found := False; for FileName in FileNames do begin fileExt := LowerCase(ExtractFileExt(FileName)); if fileExt = '.exe' then begin found := True; break; end; end; // switch -au for auto update in silent mode. // We will need to add the pop up that says install update now yes/no // This will run the setup executable which will ask for elevated permissions if found then FResult := TUtilExecute.Shell(0, SavePath + ExtractFileName(FileName), '', '-au') else FResult := False; if not FResult then begin bucStateContext.HandleMSIInstallComplete; TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Executing kmshell process to install failed:"' + IntToStr(Ord(FResult)) + '"'); // TODO: epic-windows-updates Check this is correct to return to idle ChangeState(IdleState); end; Result := FResult; end; function InstallingState.DoInstallPackage(PackageFileName: String): Boolean; var FPackage: IKeymanPackageFile2; begin Result := True; KL.Log('InstallingState.DoInstallPackage Entry' + PackageFileName); FPackage := kmcom.Packages.GetPackageFromFile(PackageFileName) as IKeymanPackageFile2; FPackage.Install2(True); // Force overwrites existing package and leaves most settings for it intact FPackage := nil; kmcom.Refresh; kmcom.Apply; KL.Log('InstallingState.DoInstallPackage about to delete'); System.SysUtils.DeleteFile(PackageFileName); end; function InstallingState.DoInstallPackages (Params: TUpdateCheckResponse): Boolean; var i: Integer; SavePath: String; PackageFullPath: String; begin SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); for i := 0 to High(Params.Packages) do begin PackageFullPath := SavePath + Params.Packages[i].FileName; if not DoInstallPackage(PackageFullPath) then // I2742 begin // Package did install log or error KL.Log('Installing Package failed' + PackageFullPath); end; end; Result := True; end; procedure InstallingState.Enter; var SavePath: String; FileNames: TStringDynArray; ucr: TUpdateCheckResponse; hasPackages, hasKeymanInstall: Boolean; begin hasPackages := False; hasKeymanInstall := False; bucStateContext.SetRegistryState(usInstalling); SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); GetFileNamesInDirectory(SavePath, FileNames); // TODO: epic-update-windows // Check if there are also packages to install if so if (TUpdateCheckStorage.LoadUpdateCacheData(ucr)) then begin hasPackages := TUpdateCheckStorage.HasKeyboardPackages(ucr); hasKeymanInstall := TUpdateCheckStorage.HasKeymanInstallFile(ucr); end; KL.Log('InstallingState.Enter before hasPackages'); if hasPackages then begin KL.Log('InstallingState.Enter hasPackages'); LaunchInstallPackageProcess; Exit; end; // only reach here if no has packages otherwise it will if hasKeymanInstall then begin DoInstallKeyman; Exit; end; end; procedure InstallingState.Exit; begin end; procedure InstallingState.HandleCheck; begin end; function InstallingState.HandleKmShell; begin // Result = exit straight away as we are installing (MSI installer) // need to just do a no-op keyman will it maybe using kmshell to install // packages. Result := kmShellContinue; end; procedure InstallingState.HandleDownload; begin end; procedure InstallingState.HandleAbort; begin ChangeState(IdleState); end; procedure InstallingState.HandleInstallNow; begin // Do Nothing. Need the UI to let user know installation in progress OR end; procedure InstallingState.HandleInstallPackages; var ucr: TUpdateCheckResponse; begin KL.Log('InstallingState.HandleInstallPackages'); // This event should only be reached in elevated process if not then // move on to just installing Keyman packages if not kmcom.SystemInfo.IsAdministrator then begin KL.Log('InstallingState.HandleInstallPackages Not Admin'); DoInstallKeyman; Exit; end; if (TUpdateCheckStorage.LoadUpdateCacheData(ucr)) then begin KL.Log('InstallingState.HandleInstallPackages about to call do install packages'); DoInstallPackages(ucr); end; DoInstallKeyman; end; procedure InstallingState.HandleFirstRun; begin bucStateContext.HandleMSIInstallComplete; // Result := kmShellContinue; end; end.