mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-07 01:15:33 +00:00
1166 lines
29 KiB
ObjectPascal
1166 lines
29 KiB
ObjectPascal
(*
|
|
* 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.
|