mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 08:25:32 +00:00
1248 lines
31 KiB
ObjectPascal
1248 lines
31 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.Types,
|
|
System.TypInfo,
|
|
Sentry.Client,
|
|
|
|
KeymanPaths,
|
|
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 EnterState; virtual; abstract;
|
|
procedure ExitState; 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;
|
|
|
|
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 RemoveCachedFiles;
|
|
|
|
function SetRegistryState(Update: TUpdateState): 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;
|
|
(**
|
|
* Checks if Keyman is the WaitingRestartState, the cache is valid
|
|
* and that Keyman has not run in this Windows session. It also confirms
|
|
* that automatic updates are still enabled #14295.
|
|
* The purpose is for the calling code can choose to produce
|
|
* a UI to confirm the user wants to install an update,
|
|
* if everything is ready to start a update.
|
|
*
|
|
* NOTE: If it finds the cache is invalid it will abort causing
|
|
* the State Machine to change state, returning to idle.
|
|
*
|
|
* @returns True if the Keyman is ready to install.
|
|
*)
|
|
function ValidateReadyToInstall: Boolean;
|
|
function IsInstallingState: Boolean;
|
|
|
|
function GetAutomaticUpdates: Boolean;
|
|
function CheckRegistryState: TUpdateState;
|
|
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
|
|
System.Win.Registry,
|
|
Winapi.Windows,
|
|
Winapi.WinINet,
|
|
ErrorControlledRegistry,
|
|
|
|
GlobalProxySettings,
|
|
kmint,
|
|
keymanapi_TLB,
|
|
KeymanMutex,
|
|
Keyman.System.KeymanSentryClient,
|
|
Keyman.System.DownloadUpdate,
|
|
Keyman.System.RemoteUpdateCheck,
|
|
Keyman.System.UpdateCheckStorage,
|
|
KLog,
|
|
RegistryKeys,
|
|
utilexecute,
|
|
UtilCheckOnline,
|
|
utiluac;
|
|
|
|
const
|
|
SPackageUpgradeFilename = 'upgrade_packages.inf';
|
|
kmShellContinue = 0;
|
|
kmShellExit = 1;
|
|
KeymanDownloadMutexName = 'KeymanDownloading';
|
|
|
|
{ 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 EnterState; override;
|
|
procedure ExitState; override;
|
|
procedure HandleCheck; override;
|
|
function HandleKmShell: Integer; override;
|
|
procedure HandleDownload; override;
|
|
procedure HandleAbort; override;
|
|
procedure HandleInstallNow; override;
|
|
procedure HandleFirstRun; override;
|
|
end;
|
|
|
|
UpdateAvailableState = class(TState)
|
|
private
|
|
procedure StartDownloadProcess;
|
|
public
|
|
procedure EnterState; override;
|
|
procedure ExitState; 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 EnterState; override;
|
|
procedure ExitState; override;
|
|
procedure HandleCheck; override;
|
|
function HandleKmShell: Integer; override;
|
|
procedure HandleDownload; override;
|
|
procedure HandleAbort; override;
|
|
procedure HandleInstallNow; override;
|
|
end;
|
|
|
|
WaitingRestartState = class(TState)
|
|
public
|
|
procedure EnterState; override;
|
|
procedure ExitState; 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 EnterState; override;
|
|
procedure ExitState; 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;
|
|
|
|
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
|
|
for lpState := Low(TUpdateState) to High(TUpdateState) do
|
|
begin
|
|
FreeAndNil(FStateInstance[lpState]);
|
|
end;
|
|
|
|
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_CheckForUpdates) or
|
|
Registry.ReadBool(SRegValue_CheckForUpdates);
|
|
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
|
|
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
|
|
'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.ExitState;
|
|
end;
|
|
|
|
SetStateOnly(ConvertStateToEnum(Value));
|
|
|
|
if Assigned(CurrentState) then
|
|
begin
|
|
CurrentState.EnterState;
|
|
end
|
|
else
|
|
begin
|
|
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
|
|
'Set CurrentState was failed');
|
|
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.RemoveCachedFiles;
|
|
var
|
|
SavePath: string;
|
|
FileName: String;
|
|
FileNames: TStringDynArray;
|
|
begin
|
|
SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath);
|
|
GetFileNamesInDirectory(SavePath, FileNames);
|
|
for FileName in FileNames do
|
|
begin
|
|
System.SysUtils.DeleteFile(FileName);
|
|
end;
|
|
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;
|
|
|
|
function TUpdateStateMachine.ValidateReadyToInstall: Boolean;
|
|
begin
|
|
if not IsCurrentStateAssigned then
|
|
Exit(False);
|
|
|
|
if not (CurrentState is WaitingRestartState) then
|
|
Exit(False);
|
|
|
|
// Verify conditions since entering WaitingRestartState:
|
|
// If the cache is not valid or automatic updates have
|
|
// been disabled clear the cache and return to the idle state.
|
|
if not TUpdateCheckStorage.CheckMetaDataForUpdate or not Self.GetAutomaticUpdates then
|
|
begin
|
|
HandleAbort;
|
|
Exit(False);
|
|
end;
|
|
|
|
if not HasKeymanRun then
|
|
Exit(True);
|
|
|
|
Result := False;
|
|
end;
|
|
|
|
function TUpdateStateMachine.IsInstallingState: Boolean;
|
|
begin
|
|
Result := (CurrentState is InstallingState);
|
|
end;
|
|
|
|
// base implmentation to be overiden
|
|
|
|
procedure TState.HandleInstallPackages;
|
|
begin
|
|
// Do Nothing
|
|
end;
|
|
|
|
procedure TState.HandleFirstRun;
|
|
begin
|
|
// A Keyman install file can be downloaded and installed directly therefore
|
|
// the state machine could be in a "unexpected" state such as UpdateAvailable.
|
|
// It will still be good to record the breadcrumb of the state incase there is a error
|
|
// during the first run.
|
|
TKeymanSentryClient.Breadcrumb('info',
|
|
'TState.HandleFirstRun first run called in state:"' + Self.ClassName + '"', 'update');
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
{ IdleState }
|
|
|
|
procedure IdleState.EnterState;
|
|
begin
|
|
// Enter UpdateAvailableState
|
|
bucStateContext.SetRegistryState(usIdle);
|
|
end;
|
|
|
|
procedure IdleState.ExitState;
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure IdleState.HandleCheck;
|
|
var
|
|
CheckForUpdates: TRemoteUpdateCheck;
|
|
Result: TRemoteUpdateCheckResult;
|
|
begin
|
|
|
|
CheckForUpdates := TRemoteUpdateCheck.Create(True);
|
|
try
|
|
Result := CheckForUpdates.Run;
|
|
finally
|
|
CheckForUpdates.Free;
|
|
end;
|
|
|
|
if (Result = wucUpdateAvailable) then
|
|
begin
|
|
ChangeState(UpdateAvailableState);
|
|
end;
|
|
// else stay in idle state
|
|
end;
|
|
|
|
function IdleState.HandleKmShell;
|
|
var
|
|
CheckForUpdates: TRemoteUpdateCheck;
|
|
UpdateCheckResult: TRemoteUpdateCheckResult;
|
|
begin
|
|
// ensure the cache is empty
|
|
bucStateContext.RemoveCachedFiles;
|
|
// Remote manages the last check time therefore
|
|
// 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 = wucUpdateAvailable then
|
|
begin
|
|
ChangeState(UpdateAvailableState);
|
|
end;
|
|
Result := kmShellContinue;
|
|
end;
|
|
|
|
procedure IdleState.HandleDownload;
|
|
begin
|
|
// Do Nothing
|
|
end;
|
|
|
|
procedure IdleState.HandleAbort;
|
|
begin
|
|
// Do Nothing
|
|
end;
|
|
|
|
procedure IdleState.HandleInstallNow;
|
|
begin
|
|
// Do Nothing
|
|
end;
|
|
|
|
procedure IdleState.HandleFirstRun;
|
|
begin
|
|
// This would be the case if it was a clean first time install of Keyman
|
|
// and not an update. However, incase it was some sort of failed update
|
|
// clean any cached files.
|
|
bucStateContext.RemoveCachedFiles;
|
|
end;
|
|
|
|
{ UpdateAvailableState }
|
|
|
|
procedure UpdateAvailableState.StartDownloadProcess;
|
|
var
|
|
FResult: Boolean;
|
|
RootPath: string;
|
|
begin
|
|
// check if online
|
|
if not IsOnline then
|
|
begin
|
|
TKeymanSentryClient.Breadcrumb('default', 'Not Online, cant execute download process.', 'update');
|
|
Exit;
|
|
end;
|
|
// 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.EnterState;
|
|
begin
|
|
// Enter UpdateAvailableState
|
|
bucStateContext.SetRegistryState(usUpdateAvailable);
|
|
if bucStateContext.FAutomaticUpdate then
|
|
begin
|
|
StartDownloadProcess;
|
|
end;
|
|
end;
|
|
|
|
procedure UpdateAvailableState.ExitState;
|
|
begin
|
|
// Exit UpdateAvailableState
|
|
end;
|
|
|
|
procedure UpdateAvailableState.HandleCheck;
|
|
var
|
|
CheckForUpdates: TRemoteUpdateCheck;
|
|
Result: TRemoteUpdateCheckResult;
|
|
begin
|
|
// Check if new updates while in this state
|
|
CheckForUpdates := TRemoteUpdateCheck.Create(True);
|
|
try
|
|
Result := CheckForUpdates.Run;
|
|
finally
|
|
CheckForUpdates.Free;
|
|
end;
|
|
if Result = wucFailure then
|
|
begin
|
|
KL.Log('UpdateAvailableState.HandleCheck CheckForUpdates not successful: '+
|
|
GetEnumName(TypeInfo(TUpdateState), Ord(Result)));
|
|
end;
|
|
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
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
procedure UpdateAvailableState.HandleInstallNow;
|
|
begin
|
|
bucStateContext.SetApplyNow(True);
|
|
// A new kmshell process will be used to download
|
|
StartDownloadProcess;
|
|
end;
|
|
|
|
{ DownloadingState }
|
|
|
|
procedure DownloadingState.EnterState;
|
|
var
|
|
DownloadResult: Boolean;
|
|
FMutex: TKeymanMutex;
|
|
begin
|
|
// Enter DownloadingState
|
|
bucStateContext.SetRegistryState(usDownloading);
|
|
|
|
FMutex := TKeymanMutex.Create(KeymanDownloadMutexName);
|
|
|
|
try
|
|
// Should be impossible but just exit anyway and let the process current
|
|
// downloading process finish.
|
|
if not FMutex.TakeOwnership then
|
|
begin
|
|
TKeymanSentryClient.Breadcrumb('default', 'DownloadingState.EnterState: Unable to get Mutex download process exists', 'update');
|
|
Exit;
|
|
end;
|
|
// verify there is an update available
|
|
if not TUpdateCheckStorage.CheckMetaDataForUpdate then
|
|
begin
|
|
// Return to Idle state
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
Exit;
|
|
end;
|
|
|
|
DownloadResult := DownloadUpdatesBackground;
|
|
FMutex.ReleaseOwnership;
|
|
finally
|
|
FreeAndNil(FMutex);
|
|
end;
|
|
|
|
if (not DownloadResult) then
|
|
begin
|
|
// Failed download; return to the
|
|
// IdleState to wait 'CheckPeriod' before trying again
|
|
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
|
|
'Error Updates not downloaded');
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end
|
|
else
|
|
begin
|
|
// TODO: #8993 for stage 2 we can got to the InstallingState
|
|
// or even have a call back to notify the user to installation
|
|
// is about to begin.
|
|
if bucStateContext.GetApplyNow then
|
|
begin
|
|
bucStateContext.SetApplyNow(False);
|
|
ChangeState(InstallingState);
|
|
end
|
|
else
|
|
begin
|
|
ChangeState(WaitingRestartState);
|
|
end;
|
|
end
|
|
|
|
end;
|
|
|
|
procedure DownloadingState.ExitState;
|
|
begin
|
|
// Exit DownloadingState
|
|
end;
|
|
|
|
procedure DownloadingState.HandleCheck;
|
|
begin
|
|
|
|
end;
|
|
|
|
function DownloadingState.HandleKmShell;
|
|
var
|
|
FMutex: TKeymanMutex;
|
|
begin
|
|
// Whether already downloading in another process or download has failed and
|
|
// this function has clean up and force a restart, kmshell should continue processing
|
|
Result := kmShellContinue;
|
|
// Check to ensure a download process is running if not
|
|
// clean up return to the the idle state and check for updates
|
|
FMutex := TKeymanMutex.Create(KeymanDownloadMutexName);
|
|
try
|
|
if FMutex.TakeOwnership then
|
|
begin
|
|
bucStateContext.RemoveCachedFiles;
|
|
FMutex.ReleaseOwnership; // Mutex must be freed before changing state
|
|
ChangeState(IdleState);
|
|
bucStateContext.CurrentState.HandleCheck;
|
|
end;
|
|
finally
|
|
FreeAndNil(FMutex);
|
|
end;
|
|
end;
|
|
|
|
procedure DownloadingState.HandleDownload;
|
|
var
|
|
FMutex: TKeymanMutex;
|
|
begin
|
|
// If downloading process is not running clean files and return to idle
|
|
FMutex := TKeymanMutex.Create(KeymanDownloadMutexName);
|
|
try
|
|
if FMutex.TakeOwnership then
|
|
begin
|
|
bucStateContext.RemoveCachedFiles;
|
|
FMutex.ReleaseOwnership; // Mutex must be freed before changing state
|
|
ChangeState(IdleState);
|
|
bucStateContext.CurrentState.HandleCheck;
|
|
end;
|
|
finally
|
|
FreeAndNil(FMutex);
|
|
end;
|
|
end;
|
|
|
|
procedure DownloadingState.HandleAbort;
|
|
begin
|
|
// don't abort during the downloading which is executing in a separate process
|
|
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.EnterState;
|
|
begin
|
|
// Entering this state means that it was not an 'apply now' update
|
|
// It should be then a automatic update. However, the automatic update
|
|
// may have been set to disable after the idle state. Is so abort
|
|
if not bucStateContext.FAutomaticUpdate then
|
|
begin
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
Exit;
|
|
end;
|
|
|
|
// Enter WaitingRestartState
|
|
bucStateContext.SetRegistryState(usWaitingRestart);
|
|
end;
|
|
|
|
procedure WaitingRestartState.ExitState;
|
|
begin
|
|
// Exit DownloadingState
|
|
end;
|
|
|
|
procedure WaitingRestartState.HandleCheck;
|
|
var
|
|
CheckForUpdates: TRemoteUpdateCheck;
|
|
Result: TRemoteUpdateCheckResult;
|
|
begin
|
|
// Check if new updates while in this state
|
|
CheckForUpdates := TRemoteUpdateCheck.Create(True);
|
|
try
|
|
Result := CheckForUpdates.Run;
|
|
finally
|
|
CheckForUpdates.Free;
|
|
end;
|
|
{ Response OK and go back to update available so files can be downloaded }
|
|
// TODO: This actually needs to check if the updates available are newer than the already downloaded updates
|
|
|
|
if Result = wucUpdateAvailable then
|
|
begin
|
|
ChangeState(UpdateAvailableState);
|
|
end;
|
|
end;
|
|
|
|
function WaitingRestartState.HandleKmShell;
|
|
begin
|
|
// Still can't go if keyman has run
|
|
if HasKeymanRun then
|
|
begin
|
|
Result := kmShellContinue;
|
|
end
|
|
else
|
|
begin
|
|
if not (TUpdateCheckStorage.CheckMetaDataForUpdate) or
|
|
not bucStateContext.FAutomaticUpdate then // set by user after idle state
|
|
begin
|
|
// Return to Idle state and check for Updates state
|
|
ChangeState(IdleState);
|
|
bucStateContext.CurrentState.HandleCheck;
|
|
Result := kmShellContinue;
|
|
end
|
|
else
|
|
begin
|
|
ChangeState(InstallingState);
|
|
Result := kmShellExit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure WaitingRestartState.HandleDownload;
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure WaitingRestartState.HandleAbort;
|
|
begin
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
procedure WaitingRestartState.HandleInstallNow;
|
|
begin
|
|
bucStateContext.SetApplyNow(True);
|
|
ChangeState(InstallingState);
|
|
end;
|
|
|
|
// Installing packages needs to be elevated
|
|
procedure InstallingState.LaunchInstallPackageProcess;
|
|
var
|
|
executeResult: Cardinal;
|
|
begin
|
|
if not kmcom.SystemInfo.IsAdministrator then
|
|
begin
|
|
if CanElevate then
|
|
begin
|
|
executeResult := WaitForElevatedConfiguration(0, '-ikp');
|
|
if (executeResult <> 0) then
|
|
begin
|
|
TKeymanSentryClient.Breadcrumb('error',
|
|
'Executing kmshell process to install keyboard packages failed"' +
|
|
IntToStr(Ord(executeResult)) + '"', 'update');
|
|
KL.Log('InstallingState.LaunchInstallPackageProcess failed executing kmshell ' +
|
|
'process to install keyboard packages: "' + IntToStr(Ord(executeResult)) + '"');
|
|
ChangeState(IdleState);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
// 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
|
|
HandleInstallPackages; // can install packages straight away
|
|
end;
|
|
end;
|
|
|
|
function InstallingState.DoInstallKeyman: Boolean;
|
|
var
|
|
FResult: Boolean;
|
|
SavePath: String;
|
|
FileName: String;
|
|
FileNames: TStringDynArray;
|
|
ucr: TUpdateCheckResponse;
|
|
ucrFileName: String;
|
|
found: Boolean;
|
|
begin
|
|
TUpdateCheckStorage.LoadUpdateCacheData(ucr);
|
|
ucrFileName := ucr.FileName;
|
|
SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath);
|
|
GetFileNamesInDirectory(SavePath, FileNames);
|
|
found := False;
|
|
for FileName in FileNames do
|
|
begin
|
|
if SameText(ucrFileName, ExtractFileName(FileName)) 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.RemoveCachedFiles;
|
|
TKeymanSentryClient.Breadcrumb('error',
|
|
'InstallingState.DoInstallKeyman: failed executing kmshell or file not found', 'update');
|
|
KL.Log('InstallingState.DoInstallKeyman failed executing kmshell ' +
|
|
'or the update was not found in the cache');
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
Result := FResult;
|
|
end;
|
|
|
|
function InstallingState.DoInstallPackage(PackageFileName: String): Boolean;
|
|
var
|
|
FPackage: IKeymanPackageFile2;
|
|
begin
|
|
Result := False;
|
|
try
|
|
FPackage := kmcom.Packages.GetPackageFromFile(PackageFileName)
|
|
as IKeymanPackageFile2;
|
|
// Force overwrites existing package and leaves most settings for it intact
|
|
FPackage.Install2(True);
|
|
Result := True;
|
|
except
|
|
on E:Exception do
|
|
begin
|
|
TKeymanSentryClient.ReportHandledException(E,
|
|
'Failed to install keyboard package');
|
|
end;
|
|
end;
|
|
|
|
FPackage := nil;
|
|
if Result then
|
|
begin
|
|
kmcom.Refresh;
|
|
kmcom.Apply;
|
|
System.SysUtils.DeleteFile(PackageFileName);
|
|
end;
|
|
|
|
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 FileExists(PackageFullPath) then
|
|
begin
|
|
Continue;
|
|
end;
|
|
|
|
if not DoInstallPackage(PackageFullPath) then // I2742
|
|
begin
|
|
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
|
|
'Installing Package failed"' + PackageFullPath + '"');
|
|
end;
|
|
end;
|
|
Result := True;
|
|
end;
|
|
|
|
procedure InstallingState.EnterState;
|
|
var
|
|
ucr: TUpdateCheckResponse;
|
|
hasPackages, hasKeymanInstall: Boolean;
|
|
begin
|
|
|
|
hasPackages := False;
|
|
hasKeymanInstall := False;
|
|
bucStateContext.SetRegistryState(usInstalling);
|
|
|
|
if (TUpdateCheckStorage.LoadUpdateCacheData(ucr)) then
|
|
begin
|
|
hasPackages := TUpdateCheckStorage.HasKeyboardPackages(ucr);
|
|
hasKeymanInstall := TUpdateCheckStorage.HasKeymanInstallFileUpdate(ucr);
|
|
end;
|
|
{ Notes: The reason packages (keyboards) is installed first is
|
|
because we are trying to reduce the number of times the user has
|
|
to be asked to elevate to admin or restart. Keyboard installation always
|
|
needs elevation, when we do that and execute kmshell as an elevated process
|
|
we can then launch the Keyman installer and it will not need
|
|
to ask for elevation. }
|
|
if hasPackages then
|
|
begin
|
|
LaunchInstallPackageProcess;
|
|
Exit;
|
|
end;
|
|
// If no packages then install Keyman now
|
|
if hasKeymanInstall then
|
|
begin
|
|
DoInstallKeyman;
|
|
Exit;
|
|
end;
|
|
// unexpected: should have had either packages or a keyman file
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
procedure InstallingState.ExitState;
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure InstallingState.HandleCheck;
|
|
begin
|
|
|
|
end;
|
|
|
|
function InstallingState.HandleKmShell;
|
|
begin
|
|
// Should not be possible while called in InstallingState. The MSI installer may have
|
|
// failed. Clean Up and return to Idle
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
Result := kmShellContinue;
|
|
end;
|
|
|
|
procedure InstallingState.HandleDownload;
|
|
begin
|
|
|
|
end;
|
|
|
|
procedure InstallingState.HandleAbort;
|
|
begin
|
|
// To late as MSI is installing
|
|
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;
|
|
hasKeymanInstall : Boolean;
|
|
begin
|
|
TUpdateCheckStorage.LoadUpdateCacheData(ucr);
|
|
hasKeymanInstall := TUpdateCheckStorage.HasKeymanInstallFileUpdate(ucr);
|
|
// This event should only be reached in elevated process if not then
|
|
// move on to just installing Keyman
|
|
if not kmcom.SystemInfo.IsAdministrator then
|
|
begin
|
|
if hasKeymanInstall then
|
|
DoInstallKeyman;
|
|
Exit;
|
|
end;
|
|
|
|
if (TUpdateCheckStorage.LoadUpdateCacheData(ucr)) then
|
|
begin
|
|
DoInstallPackages(ucr);
|
|
end;
|
|
|
|
if hasKeymanInstall then
|
|
begin
|
|
DoInstallKeyman;
|
|
end
|
|
else
|
|
begin
|
|
// There is no Keyman installer we can return to the IdleState
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
end;
|
|
|
|
procedure InstallingState.HandleFirstRun;
|
|
begin
|
|
bucStateContext.RemoveCachedFiles;
|
|
ChangeState(IdleState);
|
|
end;
|
|
|
|
end.
|