diff --git a/common/windows/delphi/general/Keyman.System.ExecutionHistory.pas b/common/windows/delphi/general/Keyman.System.ExecutionHistory.pas
new file mode 100644
index 00000000000..33dff407802
--- /dev/null
+++ b/common/windows/delphi/general/Keyman.System.ExecutionHistory.pas
@@ -0,0 +1,52 @@
+{
+ Keyman is copyright (C) SIL Global. MIT License.
+
+ This module provides functionality to track the execution state of the Keyman
+ engine. It uses a global atom to record whether Keyman has started during the
+ current session and checks if it has previously run.
+}
+unit Keyman.System.ExecutionHistory;
+
+
+interface
+
+const
+ AtomName = 'KeymanSessionFlag';
+
+function RecordKeymanStarted : Boolean;
+function HasKeymanRun : Boolean;
+
+implementation
+
+uses
+ System.SysUtils,
+ Winapi.Windows,
+ KLog;
+
+function RecordKeymanStarted : Boolean;
+var
+ atom: WORD;
+begin
+ atom := GlobalAddAtom(AtomName);
+ if atom = 0 then
+ begin
+ // TODO-WINDOWS-UPDATES: #10210 log to sentry
+ Result := False;
+ end
+ else
+ Result := True;
+end;
+
+function HasKeymanRun : Boolean;
+begin
+ Result := GlobalFindAtom(AtomName) <> 0;
+ if not Result then
+ begin
+ if GetLastError <> ERROR_FILE_NOT_FOUND then
+ begin
+ // TODO-WINDOWS-UPDATES: log to Sentry
+ end;
+ end;
+end;
+
+end.
diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas
index e351b3860ff..bacca938c66 100644
--- a/common/windows/delphi/general/RegistryKeys.pas
+++ b/common/windows/delphi/general/RegistryKeys.pas
@@ -175,8 +175,10 @@ interface
SRegValue_CharMapSourceData = 'charmap source data'; // LM
- SRegValue_AvailableLanguages = 'available languages'; //CU
- SRegValue_CurrentLanguage = 'current language'; //CU
+ SRegValue_AvailableLanguages = 'available languages'; // CU
+ SRegValue_CurrentLanguage = 'current language'; // CU
+
+ SRegValue_Update_State = 'update state'; // CU
{ Privacy }
@@ -312,8 +314,10 @@ interface
SRegValue_ActiveProject_Filename = 'project filename';
SRegValue_ActiveProject_SourcePath = 'source path';
+ SRegValue_AutomaticUpdates = 'automatic updates'; //CU
SRegValue_CheckForUpdates = 'check for updates'; // CU
SRegValue_LastUpdateCheckTime = 'last update check time'; // CU
+ SRegValue_ApplyNow = 'apply now'; // CU Start the install now even though it will require an restart
SRegValue_UpdateCheck_UseProxy = 'update check use proxy'; // CU
SRegValue_UpdateCheck_ProxyHost = 'update check proxy host'; // CU
diff --git a/oem/firstvoices/windows/src/xml/strings.xml b/oem/firstvoices/windows/src/xml/strings.xml
index fbd099c8c74..692a582dd72 100644
--- a/oem/firstvoices/windows/src/xml/strings.xml
+++ b/oem/firstvoices/windows/src/xml/strings.xml
@@ -388,6 +388,11 @@
Show welcome screen
+
+
+
+ Automatically download updates in the background, for installation later
+
diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr
index 829006912a4..87f9fbf6bfc 100644
--- a/windows/src/desktop/kmshell/kmshell.dpr
+++ b/windows/src/desktop/kmshell/kmshell.dpr
@@ -180,7 +180,12 @@ uses
Keyman.Configuration.System.Main in 'main\Keyman.Configuration.System.Main.pas',
UpdateXMLRenderer in 'render\UpdateXMLRenderer.pas',
Keyman.System.UpdateCheckStorage in 'main\Keyman.System.UpdateCheckStorage.pas',
- Keyman.System.RemoteUpdateCheck in 'main\Keyman.System.RemoteUpdateCheck.pas';
+ Keyman.System.RemoteUpdateCheck in 'main\Keyman.System.RemoteUpdateCheck.pas',
+ Keyman.System.UpdateStateMachine in 'main\Keyman.System.UpdateStateMachine.pas',
+ Keyman.System.DownloadUpdate in 'main\Keyman.System.DownloadUpdate.pas',
+ Keyman.System.ExecutionHistory in '..\..\..\..\common\windows\delphi\general\Keyman.System.ExecutionHistory.pas',
+ Keyman.Configuration.UI.UfrmStartInstallNow in 'main\Keyman.Configuration.UI.UfrmStartInstallNow.pas' {frmInstallNow},
+ Keyman.Configuration.UI.UfrmStartInstall in 'main\Keyman.Configuration.UI.UfrmStartInstall.pas' {frmStartInstall};
{$R VERSION.RES}
{$R manifest.res}
diff --git a/windows/src/desktop/kmshell/kmshell.dproj b/windows/src/desktop/kmshell/kmshell.dproj
index 45c71ffb03f..5be47294005 100644
--- a/windows/src/desktop/kmshell/kmshell.dproj
+++ b/windows/src/desktop/kmshell/kmshell.dproj
@@ -357,6 +357,17 @@
+
+
+
+
+
+ dfm
+
+
+
+ dfm
+
Cfg_2
@@ -418,21 +429,21 @@
False
-
+
kmshell.exe
true
-
+
kmshell.exe
true
-
+
- kmshell.rsm
+ kmshell.exe
true
@@ -1231,6 +1242,7 @@
+ False
12
diff --git a/windows/src/desktop/kmshell/kmshell.res b/windows/src/desktop/kmshell/kmshell.res
new file mode 100644
index 00000000000..80494425e9c
Binary files /dev/null and b/windows/src/desktop/kmshell/kmshell.res differ
diff --git a/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md
new file mode 100644
index 00000000000..160d54b2c10
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md
@@ -0,0 +1,11 @@
+``` mermaid
+stateDiagram
+ [*] --> Idle
+ Idle --> UpdateAvailable
+ UpdateAvailable --> Downloading
+ Downloading --> Installing
+ Downloading --> WaitingRestart
+ WaitingRestart --> Installing
+ Installing --> PostInstall
+ PostInstall --> Idle
+```
diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm
new file mode 100644
index 00000000000..9084afe1b69
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm
@@ -0,0 +1,47 @@
+object frmStartInstall: TfrmStartInstall
+ Left = 0
+ Top = 0
+ Caption = 'Keyman Update'
+ ClientHeight = 225
+ ClientWidth = 425
+ Color = clBtnFace
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -11
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ OldCreateOrder = False
+ PixelsPerInch = 96
+ TextHeight = 13
+ object lblInstallUpdate: TLabel
+ Left = 128
+ Top = 96
+ Width = 175
+ Height = 19
+ Caption = 'Keyman update available'
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -16
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ ParentFont = False
+ end
+ object cmdInstall: TButton
+ Left = 228
+ Top = 184
+ Width = 75
+ Height = 25
+ Caption = 'Install'
+ ModalResult = 1
+ TabOrder = 0
+ end
+ object cmdLater: TButton
+ Left = 336
+ Top = 184
+ Width = 75
+ Height = 25
+ Caption = 'Close'
+ ModalResult = 8
+ TabOrder = 1
+ end
+end
diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas
new file mode 100644
index 00000000000..8571c0b5158
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas
@@ -0,0 +1,39 @@
+{
+ Keyman is copyright (C) SIL Global. MIT License.
+
+ // TODO-WINDOWS-UPDATES: Localise all the labels and captions.
+}
+unit Keyman.Configuration.UI.UfrmStartInstall;
+interface
+
+uses
+ System.Classes,
+ System.SysUtils,
+ System.Variants,
+ Vcl.Controls,
+ Vcl.Dialogs,
+ Vcl.ExtCtrls,
+ Vcl.Forms,
+ Vcl.Graphics,
+ Vcl.StdCtrls,
+ Winapi.Messages,
+ Winapi.Windows,
+ UfrmKeymanBase,
+ UserMessages;
+
+type
+ TfrmStartInstall = class(TfrmKeymanBase)
+ cmdInstall: TButton;
+ cmdLater: TButton;
+ lblInstallUpdate: TLabel;
+ private
+ public
+ end;
+
+
+implementation
+
+{$R *.dfm}
+
+
+end.
diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm
new file mode 100644
index 00000000000..eae54930403
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm
@@ -0,0 +1,61 @@
+object frmStartInstallNow: TfrmStartInstallNow
+ Left = 0
+ Top = 0
+ Caption = 'Keyman Update'
+ ClientHeight = 225
+ ClientWidth = 425
+ Color = clBtnFace
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -11
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ OldCreateOrder = False
+ PixelsPerInch = 96
+ TextHeight = 13
+ object lblUpdateMessage: TLabel
+ Left = 56
+ Top = 88
+ Width = 274
+ Height = 19
+ Caption = 'Keyman and Windows will be restarted'
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -16
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ ParentFont = False
+ WordWrap = True
+ end
+ object lblUpdateNow: TLabel
+ Left = 56
+ Top = 40
+ Width = 115
+ Height = 25
+ Caption = 'Update Now'
+ Font.Charset = DEFAULT_CHARSET
+ Font.Color = clWindowText
+ Font.Height = -21
+ Font.Name = 'Tahoma'
+ Font.Style = []
+ ParentFont = False
+ end
+ object cmdInstall: TButton
+ Left = 228
+ Top = 184
+ Width = 75
+ Height = 25
+ Caption = 'Update'
+ ModalResult = 1
+ TabOrder = 0
+ end
+ object cmdLater: TButton
+ Left = 336
+ Top = 184
+ Width = 75
+ Height = 25
+ Caption = 'Close'
+ ModalResult = 8
+ TabOrder = 1
+ end
+end
diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas
new file mode 100644
index 00000000000..5f9c9caefc7
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas
@@ -0,0 +1,40 @@
+{
+ Keyman is copyright (C) SIL Global. MIT License.
+
+ // TODO-WINDOWS-UPDATES: Localise all the labels and captions.
+}
+unit Keyman.Configuration.UI.UfrmStartInstallNow;
+interface
+
+uses
+ System.Classes,
+ System.SysUtils,
+ System.Variants,
+ Vcl.Controls,
+ Vcl.Dialogs,
+ Vcl.ExtCtrls,
+ Vcl.Forms,
+ Vcl.Graphics,
+ Vcl.StdCtrls,
+ Winapi.Messages,
+ Winapi.Windows,
+ UfrmKeymanBase,
+ UserMessages;
+
+type
+ TfrmStartInstallNow = class(TfrmKeymanBase)
+ cmdInstall: TButton;
+ cmdLater: TButton;
+ lblUpdateMessage: TLabel;
+ lblUpdateNow: TLabel;
+ private
+ public
+ end;
+
+implementation
+
+{$R *.dfm}
+
+
+
+end.
diff --git a/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas b/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas
new file mode 100644
index 00000000000..f91c8f6ea4d
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.System.DownloadUpdate.pas
@@ -0,0 +1,186 @@
+(*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *)
+unit Keyman.System.DownloadUpdate;
+
+interface
+uses
+ System.Classes,
+ System.SysUtils,
+ httpuploader,
+ Keyman.System.UpdateCheckResponse,
+ KeymanPaths,
+ OnlineUpdateCheck;
+
+type
+ TDownloadUpdateParams = record
+ TotalSize: Integer;
+ TotalDownloads: Integer;
+ StartPosition: Integer;
+ end;
+
+ TDownloadUpdate = class
+ private
+ FShowErrors: Boolean;
+ FDownload: TDownloadUpdateParams;
+ (**
+ *
+ * Performs updates download in the background.
+ * @params SavePath The path where the downloaded files will be saved.
+ *
+ *@returns A Boolean value indicating the overall result of the
+ * download process.
+ *)
+ function DoDownloadUpdates(SavePath: string; Params: TUpdateCheckResponse): Boolean;
+
+ public
+
+ constructor Create;
+ destructor Destroy; override;
+
+
+ function DownloadUpdates : Boolean;
+ // TODO-WINDOWS-UPDATES: verify filesizes match the ucr metadata so we know we don't have partial downloades.
+ //function VerifyAllFilesDownloaded : Boolean;
+ property ShowErrors: Boolean read FShowErrors write FShowErrors;
+
+ end;
+
+implementation
+
+
+uses
+ System.StrUtils,
+ System.Types,
+ ErrorControlledRegistry,
+ GlobalProxySettings,
+ keymanapi_TLB,
+ KeymanVersion,
+ Keyman.System.UpdateCheckStorage,
+ KLog,
+ kmint,
+ OnlineUpdateCheckMessages,
+ RegistryKeys,
+ Upload_Settings,
+ utilkmshell;
+
+ // TODO-WINDOWS-UPDATES: temp wrapper for converting showmessage to logs don't know where
+ // if not using klog
+ procedure LogMessage(LogMessage: string);
+ begin
+ KL.Log(LogMessage);
+ end;
+
+constructor TDownloadUpdate.Create;
+begin
+ inherited Create;
+
+ FShowErrors := True;
+ KL.Log('TDownloadUpdate.Create');
+end;
+
+destructor TDownloadUpdate.Destroy;
+begin
+ inherited Destroy;
+end;
+
+function TDownloadUpdate.DoDownloadUpdates(SavePath: string; Params: TUpdateCheckResponse): Boolean;
+var
+ i : Integer;
+ http: THttpUploader;
+ fs: TFileStream;
+
+ function DownloadFile(const url, savepath: string): Boolean;
+ begin
+ try
+ http := THttpUploader.Create(nil);
+ try
+ http.Proxy.Server := GetProxySettings.Server;
+ http.Proxy.Port := GetProxySettings.Port;
+ http.Proxy.Username := GetProxySettings.Username;
+ http.Proxy.Password := GetProxySettings.Password;
+ http.Request.Agent := API_UserAgent;
+
+ http.Request.SetURL(url);
+ http.Upload;
+ if http.Response.StatusCode = 200 then
+ begin
+ fs := TFileStream.Create(savepath, fmCreate);
+ try
+ fs.Write(http.Response.PMessageBody^, http.Response.MessageBodyLength);
+ finally
+ fs.Free;
+ end;
+ Result := True;
+ end
+ else // I2742
+ begin
+ // If it fails we set to false but will try the other files
+ Exit(False);
+ end;
+ finally
+ http.Free;
+ end;
+ except
+ on E:EHTTPUploader do
+ begin
+ if (E.ErrorCode = 12007) or (E.ErrorCode = 12029)
+ then LogMessage(S_OnlineUpdate_UnableToContact)
+ else LogMessage(WideFormat(S_OnlineUpdate_UnableToContact_Error, [E.Message]));
+ Result := False;
+ end;
+ end;
+ end;
+
+begin
+ Result := False;
+
+ FDownload.TotalSize := 0;
+ FDownload.TotalDownloads := 0;
+
+ // Keyboard Packages
+ FDownload.StartPosition := 0;
+ for i := 0 to High(Params.Packages) do
+ begin
+ Inc(FDownload.TotalDownloads);
+ Inc(FDownload.TotalSize, Params.Packages[i].DownloadSize);
+ Params.Packages[i].SavePath := SavePath + Params.Packages[i].FileName;
+ if not DownloadFile(Params.Packages[i].DownloadURL, Params.Packages[i].SavePath) then // I2742
+ begin
+ Params.Packages[i].Install := False; // Download failed but install other files
+ end;
+ FDownload.StartPosition := FDownload.StartPosition + Params.Packages[i].DownloadSize;
+ end;
+
+ // Keyman Installer
+ Inc(FDownload.TotalDownloads);
+ Inc(FDownload.TotalSize, Params.InstallSize);
+ if not DownloadFile(Params.InstallURL, SavePath + Params.FileName) then // I2742
+ begin
+ // TODO-WINDOWS-UPDATES: #10210 convert to error log.
+ LogMessage('DoDownloadUpdates Failed to download' + Params.InstallURL);
+ end
+ else
+ begin
+ // If installer has downloaded that is success even
+ // if zero packages were downloaded.
+ Result := True;
+ end;
+end;
+
+function TDownloadUpdate.DownloadUpdates: Boolean;
+var
+ DownloadBackGroundSavePath : String;
+ ucr: TUpdateCheckResponse;
+begin
+ DownloadBackGroundSavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath);
+ if TUpdateCheckStorage.LoadUpdateCacheData(ucr) then
+ begin
+ Result := DoDownloadUpdates(DownloadBackGroundSavePath, ucr);
+ KL.Log('DownloadUpdates.DownloadUpdates: DownloadResult = '+IntToStr(Ord(Result)));
+ end
+ else
+ Result := False;
+end;
+
+end.
diff --git a/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas b/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas
index 0eebd52ff93..9e4b0b007e2 100644
--- a/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas
+++ b/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas
@@ -1,10 +1,10 @@
-{
+(**
* Keyman is copyright (C) SIL International. MIT License.
*
* Keyman.System.RemoteUpdateCheck: Checks for keyboard package and Keyman
- for Windows updates.
-}
-unit Keyman.System.RemoteUpdateCheck; // I3306
+ * for Windows updates.
+*)
+unit Keyman.System.RemoteUpdateCheck; // I3306
interface
@@ -16,6 +16,9 @@ interface
Keyman.System.UpdateCheckResponse,
OnlineUpdateCheck;
+const
+ CheckPeriod: Integer = 7; // Days between checking for updates
+
type
ERemoteUpdateCheck = class(Exception);
@@ -31,19 +34,23 @@ TRemoteUpdateCheck = class
private
FForce: Boolean;
FRemoteResult: TRemoteUpdateCheckResult;
-
FErrorMessage: string;
-
FShowErrors: Boolean;
- FDownload: TRemoteUpdateCheckDownloadParams;
- FCheckOnly: Boolean;
-
- function DownloadUpdates(Params: TUpdateCheckResponse) : Boolean;
- procedure DoDownloadUpdates(SavePath: string; Params: TUpdateCheckResponse; var Result: Boolean);
+ (**
+ * Performs an online query of both the main keyman package and
+ * the keyboard packages. It utilizes the kmcom API to retrieve the current
+ * packages. The function then performs an HTTP request to query the remote
+ * versions of these packages.
+ * The resulting information is stored in the TUpdateCheckResponse
+ * variable and seralized to disk.
+ *
+ * @returns A TBackgroundUpdateResult indicating the result of the update
+ * check.
+ *)
function DoRun: TRemoteUpdateCheckResult;
public
- constructor Create(AForce : Boolean; ACheckOnly: Boolean = False);
+ constructor Create(AForce: Boolean);
destructor Destroy; override;
function Run: TRemoteUpdateCheckResult;
property ShowErrors: Boolean read FShowErrors write FShowErrors;
@@ -51,10 +58,19 @@ TRemoteUpdateCheck = class
procedure LogMessage(LogMessage: string);
+(**
+ * This function checks if a week or CheckPeriod time has passed since the last
+ * update check.
+ *
+ * @returns True if it has been longer then CheckPeriod time since last update
+*)
+function ConfigCheckContinue: Boolean;
+
implementation
uses
System.WideStrUtils,
+ System.Win.Registry,
Winapi.Windows,
Winapi.WinINet,
@@ -72,7 +88,7 @@ implementation
{ TRemoteUpdateCheck }
-constructor TRemoteUpdateCheck.Create(AForce, ACheckOnly: Boolean);
+constructor TRemoteUpdateCheck.Create(AForce: Boolean);
begin
inherited Create;
@@ -80,7 +96,6 @@ constructor TRemoteUpdateCheck.Create(AForce, ACheckOnly: Boolean);
FRemoteResult := wucUnknown;
FForce := AForce;
- FCheckOnly := ACheckOnly;
KL.Log('TRemoteUpdateCheck.Create');
end;
@@ -90,8 +105,9 @@ destructor TRemoteUpdateCheck.Destroy;
if (FErrorMessage <> '') and FShowErrors then
LogMessage(FErrorMessage);
- KL.Log('TRemoteUpdateCheck.Destroy: FErrorMessage = '+FErrorMessage);
- KL.Log('TRemoteUpdateCheck.Destroy: FRemoteResult = '+IntToStr(Ord(FRemoteResult)));
+ KL.Log('TRemoteUpdateCheck.Destroy: FErrorMessage = ' + FErrorMessage);
+ KL.Log('TRemoteUpdateCheck.Destroy: FRemoteResult = ' +
+ IntToStr(Ord(FRemoteResult)));
inherited Destroy;
end;
@@ -99,138 +115,21 @@ destructor TRemoteUpdateCheck.Destroy;
function TRemoteUpdateCheck.Run: TRemoteUpdateCheckResult;
begin
Result := DoRun;
-
- if Result in [ wucSuccess] then
- begin
- kmcom.Keyboards.Refresh;
- kmcom.Keyboards.Apply;
- kmcom.Packages.Refresh;
- end;
-
FRemoteResult := Result;
end;
-
-procedure TRemoteUpdateCheck.DoDownloadUpdates(SavePath: string; Params: TUpdateCheckResponse; var Result: Boolean);
-var
- i, downloadCount: Integer;
- http: THttpUploader;
- fs: TFileStream;
-
- function DownloadFile(const url, savepath: string): Boolean;
- begin
- try
- http := THttpUploader.Create(nil);
- try
- http.Proxy.Server := GetProxySettings.Server;
- http.Proxy.Port := GetProxySettings.Port;
- http.Proxy.Username := GetProxySettings.Username;
- http.Proxy.Password := GetProxySettings.Password;
- http.Request.Agent := API_UserAgent;
-
- http.Request.SetURL(url);
- http.Upload;
- if http.Response.StatusCode = 200 then
- begin
- fs := TFileStream.Create(savepath, fmCreate);
- try
- fs.Write(http.Response.PMessageBody^, http.Response.MessageBodyLength);
- finally
- fs.Free;
- end;
- Result := True;
- end
- else // I2742
- // If it fails we set to false but will try the other files
- Result := False;
- Exit;
- finally
- http.Free;
- end;
- except
- on E:EHTTPUploader do
- begin
- if (E.ErrorCode = 12007) or (E.ErrorCode = 12029)
- then LogMessage(S_OnlineUpdate_UnableToContact)
- else LogMessage(WideFormat(S_OnlineUpdate_UnableToContact_Error, [E.Message]));
- Result := False;
- end;
- end;
- end;
-
-begin
- Result := False;
-
- FDownload.TotalSize := 0;
- FDownload.TotalDownloads := 0;
- downloadCount := 0;
-
- // Keyboard Packages
- for i := 0 to High(Params.Packages) do
- begin
- Inc(FDownload.TotalDownloads);
- Inc(FDownload.TotalSize, Params.Packages[i].DownloadSize);
- Params.Packages[i].SavePath := SavePath + Params.Packages[i].FileName;
- end;
-
- // Add the Keyman installer
- Inc(FDownload.TotalDownloads);
- Inc(FDownload.TotalSize, Params.InstallSize);
-
- // Keyboard Packages
- FDownload.StartPosition := 0;
- for i := 0 to High(Params.Packages) do
- begin
- if not DownloadFile(Params.Packages[i].DownloadURL, Params.Packages[i].SavePath) then // I2742
- begin
- Params.Packages[i].Install := False; // Download failed but install other files
- end
- else
- Inc(downloadCount);
- FDownload.StartPosition := FDownload.StartPosition + Params.Packages[i].DownloadSize;
- end;
-
- // Keyman Installer
- if not DownloadFile(Params.InstallURL, SavePath + Params.FileName) then // I2742
- begin
- // TODO: #10210record fail? and log // Download failed but user wants to install other files
- end
- else
- begin
- Inc(downloadCount)
- end;
-
- // There needs to be at least one file successfully downloaded to return
- // True that files were downloaded
- if downloadCount > 0 then
- Result := True;
-end;
-
-function TRemoteUpdateCheck.DownloadUpdates(Params: TUpdateCheckResponse): Boolean;
-var
- DownloadBackGroundSavePath : String;
- DownloadResult : Boolean;
-begin
- DownloadBackGroundSavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath);
-
- DoDownloadUpdates(DownloadBackGroundSavePath, Params, DownloadResult);
- KL.Log('TRemoteUpdateCheck.DownloadUpdatesBackground: DownloadResult = '+IntToStr(Ord(DownloadResult)));
- Result := DownloadResult;
-
-end;
-
function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult;
var
flags: DWord;
i: Integer;
ucr: TUpdateCheckResponse;
pkg: IKeymanPackage;
- downloadResult: boolean;
- registry: TRegistryErrorControlled;
+ Registry: TRegistryErrorControlled;
http: THttpUploader;
+ proceed: Boolean;
begin
- {FProxyHost := '';
- FProxyPort := 0;}
+ { FProxyHost := '';
+ FProxyPort := 0; }
{ Check if user is currently online }
if not InternetGetConnectedState(@flags, 0) then
@@ -239,53 +138,22 @@ function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult;
Exit;
end;
- { Verify that it has been at least 7 days since last update check }
- try
- registry := TRegistryErrorControlled.Create; // I2890
- try
- if registry.OpenKeyReadOnly(SRegKey_KeymanDesktop_CU) then
- begin
- if registry.ValueExists(SRegValue_CheckForUpdates) and not registry.ReadBool(SRegValue_CheckForUpdates) and not FForce then
- begin
- Result := wucNoUpdates;
- Exit;
- end;
- if registry.ValueExists(SRegValue_LastUpdateCheckTime) and (Now - registry.ReadDateTime(SRegValue_LastUpdateCheckTime) < 7) and not FForce then
- begin
- Result := wucNoUpdates;
- // TODO: #10210 This exit is just to remove the time check for testing.
- //Exit;
- end;
-
- {if ValueExists(SRegValue_UpdateCheck_UseProxy) and ReadBool(SRegValue_UpdateCheck_UseProxy) then
- begin
- FProxyHost := ReadString(SRegValue_UpdateCheck_ProxyHost);
- FProxyPort := StrToIntDef(ReadString(SRegValue_UpdateCheck_ProxyPort), 80);
- end;}
- end;
- finally
- registry.Free;
- end;
- except
- { we will not run the check if an error occurs reading the settings }
- on E:Exception do
- begin
- Result := wucFailure;
- FErrorMessage := E.Message;
- Exit;
- end;
+ proceed := ConfigCheckContinue;
+ if not proceed and not FForce then
+ begin
+ Result := wucNoUpdates;
+ Exit;
end;
- Result := wucNoUpdates;
-
try
- http := THTTPUploader.Create(nil);
+ http := THttpUploader.Create(nil);
try
http.Fields.Add('version', ansistring(CKeymanVersionInfo.Version));
http.Fields.Add('tier', ansistring(CKeymanVersionInfo.Tier));
- if FForce
- then http.Fields.Add('manual', '1')
- else http.Fields.Add('manual', '0');
+ if FForce then
+ http.Fields.Add('manual', '1')
+ else
+ http.Fields.Add('manual', '0');
for i := 0 to kmcom.Packages.Count - 1 do
begin
@@ -294,8 +162,10 @@ function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult;
// Due to limitations in PHP parsing of query string parameters names with
// space or period, we need to split the parameters up. The legacy pattern
// is still supported on the server side. Relates to #4886.
- http.Fields.Add(AnsiString('packageid_'+IntToStr(i)), AnsiString(pkg.ID));
- http.Fields.Add(AnsiString('packageversion_'+IntToStr(i)), AnsiString(pkg.Version));
+ http.Fields.Add(ansistring('packageid_' + IntToStr(i)),
+ ansistring(pkg.ID));
+ http.Fields.Add(ansistring('packageversion_' + IntToStr(i)),
+ ansistring(pkg.Version));
pkg := nil;
end;
@@ -307,31 +177,14 @@ function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult;
http.Request.HostName := API_Server;
http.Request.Protocol := API_Protocol;
http.Request.UrlPath := API_Path_UpdateCheck_Windows;
- //OnStatus :=
+ // OnStatus :=
http.Upload;
if http.Response.StatusCode = 200 then
begin
if ucr.Parse(http.Response.MessageBodyAsString, 'bundle', CKeymanVersionInfo.Version) then
begin
- //ResponseToParams(ucr);
-
- if FCheckOnly then
- begin
- TUpdateCheckStorage.SaveUpdateCacheData(ucr);
- Result := FRemoteResult;
- end
- // TODO: ##10210
- // Integerate into state machine. in the download state
- // the process can call LoadUpdateCacheData if needed to get the
- // response result.
- else if (Length(ucr.Packages) > 0) or (ucr.InstallURL <> '') then
- begin
- downloadResult := DownloadUpdates(ucr);
- if DownloadResult then
- begin
- Result := wucSuccess;
- end;
- end;
+ TUpdateCheckStorage.SaveUpdateCacheData(ucr);
+ Result := wucSuccess;
end
else
begin
@@ -345,34 +198,71 @@ function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult;
http.Free;
end;
except
- on E:EHTTPUploader do
+ on E: EHTTPUploader do
begin
- if (E.ErrorCode = 12007) or (E.ErrorCode = 12029)
- then FErrorMessage := S_OnlineUpdate_UnableToContact
- else FErrorMessage := WideFormat(S_OnlineUpdate_UnableToContact_Error, [E.Message]);
+ if (E.ErrorCode = 12007) or (E.ErrorCode = 12029) then
+ FErrorMessage := S_OnlineUpdate_UnableToContact
+ else
+ FErrorMessage := WideFormat(S_OnlineUpdate_UnableToContact_Error,
+ [E.Message]);
Result := wucFailure;
end;
- on E:Exception do
+ on E: Exception do
begin
FErrorMessage := E.Message;
Result := wucFailure;
end;
end;
- registry := TRegistryErrorControlled.Create; // I2890
+ Registry := TRegistryErrorControlled.Create; // I2890
try
- if registry.OpenKey(SRegKey_KeymanDesktop_CU, True) then
- registry.WriteDateTime(SRegValue_LastUpdateCheckTime, Now);
+ if Registry.OpenKey(SRegKey_KeymanDesktop_CU, True) then
+ Registry.WriteDateTime(SRegValue_LastUpdateCheckTime, Now);
finally
- registry.Free;
+ Registry.Free;
end;
end;
- // temp wrapper for converting showmessage to logs don't know where
- // if nt using klog
- procedure LogMessage(LogMessage: string);
- begin
- KL.Log(LogMessage);
- end;
+// temp wrapper for converting showmessage to logs don't know where
+// if nt using klog
+procedure LogMessage(LogMessage: string);
+begin
+ KL.Log(LogMessage);
+end;
+
+function ConfigCheckContinue: Boolean;
+var
+ Registry: TRegistryErrorControlled;
+begin
+ { Verify that it has been at least CheckPeriod days since last update check }
+ Result := False;
+ try
+ Registry := TRegistryErrorControlled.Create; // I2890
+ try
+ if Registry.OpenKeyReadOnly(SRegKey_KeymanDesktop_CU) then
+ begin
+ if Registry.ValueExists(SRegValue_CheckForUpdates) and
+ not Registry.ReadBool(SRegValue_CheckForUpdates) then
+ begin
+ Exit;
+ end;
+ if Registry.ValueExists(SRegValue_LastUpdateCheckTime) and
+ (Now - Registry.ReadDateTime(SRegValue_LastUpdateCheckTime) >
+ CheckPeriod) then
+ begin
+ Result := True;
+ end;
+ end;
+ finally
+ Registry.Free;
+ end;
+ except
+ on E: ERegistryException do
+ begin
+ Result := False;
+ LogMessage(E.Message);
+ end;
+ end;
+end;
end.
diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas
new file mode 100644
index 00000000000..a3c262d2bc1
--- /dev/null
+++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas
@@ -0,0 +1,1085 @@
+(*
+ * 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,
+
+ 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, usRetry, usPostInstall);
+
+ // 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;
+ 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;
+
+ 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;
+ 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,
+ Keyman.System.DownloadUpdate,
+ Keyman.System.RemoteUpdateCheck,
+ KLog,
+ RegistryKeys,
+ utilexecute;
+
+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(SavePath: string): Boolean; overload;
+
+ public
+ procedure Enter; override;
+ procedure Exit; override;
+ procedure HandleCheck; override;
+ function HandleKmShell: Integer; override;
+ procedure HandleDownload; override;
+ procedure HandleAbort; override;
+ procedure HandleInstallNow; override;
+ end;
+
+ RetryState = 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;
+
+ PostInstallState = class(TState)
+ private
+ procedure HandleMSIInstallComplete;
+ public
+ procedure Enter; override;
+ procedure Exit; override;
+ procedure HandleCheck; override;
+ function HandleKmShell: Integer; override;
+ procedure HandleDownload; override;
+ procedure HandleAbort; override;
+ procedure HandleInstallNow; 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);
+ FStateInstance[usRetry] := RetryState.Create(Self);
+ FStateInstance[usPostInstall] := PostInstallState.Create(Self);
+
+ // Check the Registry setting.
+ SetStateOnly(CheckRegistryState);
+end;
+
+destructor TUpdateStateMachine.Destroy;
+var
+ lpState: TUpdateState;
+begin
+ if (FErrorMessage <> '') and FShowErrors then
+ KL.Log(FErrorMessage); // TODO: #10210 Log to Sentry
+
+ for lpState := Low(TUpdateState) to High(TUpdateState) do
+ begin
+ FreeAndNil(FStateInstance[lpState]);
+ end;
+
+ // TODO: #10210 remove debugging comments
+ // 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
+ // TODO: #10210 Log to Sentry
+ KL.Log('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
+ // TODO: #10210 Log to Sentry
+ KL.Log('Failed to write to registry: ' + E.Message);
+ 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
+ // TODO: #10210 Log to Sentry
+ KL.Log('Failed to write to registry: ' + E.Message);
+ 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
+ // TODO: #10210 Log to Sentry
+ KL.Log('Failed to read registery: ' + E.Message);
+ 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
+ // TODO: #10210 Log to Sentry 'Failed to write '+SRegValue_ApplyNow+' to registry: ' + E.Message
+ KL.Log('Failed to write to registry: ' + E.Message);
+ 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
+ // TODO: #10210 Log to Sentry
+ KL.Log('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 if StateClass = RetryState then
+ Result := usRetry
+ else if StateClass = PostInstallState then
+ Result := usPostInstall
+ else
+ begin
+ // TODO: #10210 Log to Sentry
+ Result := usIdle;
+ KL.Log('Unknown StateClass'); // TODO-WINDOWS-UPDATES
+ end;
+end;
+
+function TUpdateStateMachine.IsCurrentStateAssigned: Boolean;
+begin
+ if Assigned(CurrentState) then
+ Result := True
+ else
+ begin
+ // TODO: #10210 Log to Sentry
+ KL.Log('Unexpected Error: Current state is not assigned.');
+ Result := False;
+ 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;
+
+function TUpdateStateMachine.CurrentStateName: string;
+begin
+ if not IsCurrentStateAssigned then
+ Exit('Undefined');
+ Result := CurrentState.ClassName;
+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 seperate process
+ RootPath := ExtractFilePath(ParamStr(0));
+ FResult := TUtilExecute.ShellCurrentUser(0, ParamStr(0), IncludeTrailingPathDelimiter(RootPath), '-bd');
+ if not FResult then
+ // TODO: #10210 Log to Sentry
+ KL.Log('TrmfMain: Executing KMshell for download updated Failed');
+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: 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(InstallingState)
+ end;
+
+end;
+
+{ DownloadingState }
+
+procedure DownloadingState.Enter;
+var
+ DownloadResult: Boolean;
+begin
+ // Enter DownloadingState
+ bucStateContext.SetRegistryState(usDownloading);
+ { ## for testing log that we would download }
+ KL.Log('DownloadingState.HandleKmshell test code continue');
+ DownloadResult := True;
+ { End testing }
+ DownloadResult := DownloadUpdatesBackground;
+ // TODO check if keyman is running then send to Waiting Restart
+ if DownloadResult then
+ begin
+ if HasKeymanRun then
+ begin
+ if bucStateContext.GetApplyNow then
+ begin
+ bucStateContext.SetApplyNow(False);
+ ChangeState(InstallingState);
+ end
+ else
+ ChangeState(WaitingRestartState);
+ end
+ else
+ begin
+ bucStateContext.SetApplyNow(False);
+ ChangeState(InstallingState);
+ end;
+ end
+ else
+ begin
+ ChangeState(RetryState);
+ 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;
+
+function InstallingState.DoInstallKeyman(SavePath: string): Boolean;
+var
+ s: string;
+ FResult: Boolean;
+begin
+ s := LowerCase(ExtractFileExt(SavePath));
+ if s = '.msi' then
+ FResult := TUtilExecute.Shell(0, 'msiexec.exe', '', '/qb /i "' + SavePath +
+ '" AUTOLAUNCHPRODUCT=1') // I3349
+ else if s = '.exe' then
+ begin
+ // 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
+ FResult := TUtilExecute.Shell(0, SavePath, '', '-au') // I3349
+ end
+ else
+ FResult := False;
+
+ if not FResult then
+ begin
+ // TODO: #10210 Log to Sentry
+ KL.Log('TUpdateStateMachine.InstallingState.DoInstall: Result = ' +
+ IntToStr(Ord(FResult)));
+ // Log message ShowMessage(SysErrorMessage(GetLastError));
+ end;
+
+ Result := FResult;
+end;
+
+procedure InstallingState.Enter;
+var
+ SavePath: String;
+ fileExt: String;
+ fileName: String;
+ Filenames: TStringDynArray;
+begin
+
+ bucStateContext.SetRegistryState(usInstalling);
+ SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath);
+
+ GetFileNamesInDirectory(SavePath, Filenames);
+ // for now we only want the exe although excute install can
+ // handle msi
+ for fileName in Filenames do
+ begin
+ fileExt := LowerCase(ExtractFileExt(fileName));
+ if fileExt = '.exe' then
+ break;
+ end;
+
+ if DoInstallKeyman(SavePath + ExtractFileName(fileName)) then
+ begin
+ KL.Log('TUpdateStateMachine.InstallingState.Enter: DoInstall OK');
+ end
+ else
+ begin
+ // TODO: #10210 clean failed download
+ // TODO: #10210 Do we do a retry on install? probably not
+ KL.Log('TUpdateStateMachine.InstallingState.Enter: DoInstall fail');
+ ChangeState(IdleState);
+ 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;
+
+{ RetryState }
+
+procedure RetryState.Enter;
+begin
+ bucStateContext.SetRegistryState(usRetry);
+end;
+
+procedure RetryState.Exit;
+begin
+
+end;
+
+procedure RetryState.HandleCheck;
+begin
+
+end;
+
+function RetryState.HandleKmShell;
+begin
+ // #TODO: #10210 Implement retry
+ Result := kmShellContinue
+end;
+
+procedure RetryState.HandleDownload;
+begin
+
+end;
+
+procedure RetryState.HandleAbort;
+begin
+
+end;
+
+procedure RetryState.HandleInstallNow;
+begin
+ // TODO: #10038 handle retry counts
+ ChangeState(InstallingState);
+end;
+
+{ PostInstallState }
+
+procedure PostInstallState.Enter;
+begin
+ // Enter downloading state
+ bucStateContext.SetRegistryState(usPostInstall);
+end;
+
+procedure PostInstallState.Exit;
+begin
+
+end;
+
+procedure PostInstallState.HandleCheck;
+begin
+ // Handle Check
+end;
+
+function PostInstallState.HandleKmShell;
+begin
+ HandleMSIInstallComplete;
+ Result := kmShellContinue;
+end;
+
+procedure PostInstallState.HandleDownload;
+begin
+ // Do Nothing
+end;
+
+procedure PostInstallState.HandleMSIInstallComplete;
+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;
+ ChangeState(IdleState);
+end;
+
+procedure PostInstallState.HandleAbort;
+begin
+ // Handle Abort
+end;
+
+procedure PostInstallState.HandleInstallNow;
+begin
+ // Do nothing as files will be cleaned via HandleKmShell
+end;
+
+end.
diff --git a/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas b/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas
index 007d854797b..e4cdff54bb3 100644
--- a/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas
+++ b/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas
@@ -1,18 +1,18 @@
(*
Name: UImportOlderVersionSettings
Copyright: Copyright (C) 2003-2017 SIL International.
- Documentation:
- Description:
+ Documentation:
+ Description:
Create Date: 22 Feb 2011
Modified Date: 3 Jun 2014
Authors: mcdurdin
- Related Files:
- Dependencies:
+ Related Files:
+ Dependencies:
- Bugs:
- Todo:
- Notes:
+ Bugs:
+ Todo:
+ Notes:
History: 22 Feb 2011 - mcdurdin - I2651 - Install does not set desired default options
22 Feb 2011 - mcdurdin - I2753 - Firstrun crashes because start with windows and auto update check options are set in Engine instead of Desktop
03 May 2011 - mcdurdin - I2890 - Record diagnostic data when encountering registry errors
@@ -24,7 +24,7 @@
interface
-function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753
+function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates,DoAutomaticUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753
implementation
@@ -43,7 +43,7 @@ implementation
RegistryKeys,
UImportOlderKeyboardUtils;
-function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753
+function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates,DoAutomaticUpdates: Boolean; FDisablePackages, FDefaultUILanguage: string; DoAutomaticallyReportUsage: Boolean): Boolean; // I2753
var
n, I: Integer;
v: Integer;
@@ -136,6 +136,7 @@ function FirstRunInstallDefaults(DoDefaults,DoStartWithWindows,DoCheckForUpdates
if DoStartWithWindows then kmcom.Options['koStartWithWindows'].Value := True; // I2753
if DoCheckForUpdates then kmcom.Options['koCheckForUpdates'].Value := True; // I2753
+ if DoAutomaticUpdates then kmcom.Options['koAutomaticUpdate'].Value := True;
if DoAutomaticallyReportUsage then kmcom.Options['koAutomaticallyReportUsage'].Value := True;
diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas
index a779fdcdd2b..cb206e0291b 100644
--- a/windows/src/desktop/kmshell/main/UfrmMain.pas
+++ b/windows/src/desktop/kmshell/main/UfrmMain.pas
@@ -149,6 +149,7 @@ TfrmMain = class(TfrmWebContainer)
procedure DoApply;
procedure DoRefresh;
procedure Update_CheckNow;
+ procedure Update_ApplyNow;
protected
procedure FireCommand(const command: WideString; params: TStringList); override;
@@ -185,6 +186,7 @@ implementation
LanguagesXMLRenderer,
MessageIdentifierConsts,
MessageIdentifiers,
+ Keyman.System.RemoteUpdateCheck,
OnlineUpdateCheck,
OptionsXMLRenderer,
Keyman.Configuration.System.UmodWebHttpServer,
@@ -209,7 +211,8 @@ implementation
utilkmshell,
utilhttp,
utiluac,
- utilxml;
+ utilxml,
+ KeymanPaths;
type
PHKL = ^HKL;
@@ -349,6 +352,7 @@ procedure TfrmMain.FireCommand(const command: WideString; params: TStringList);
else if command = 'support_proxyconfig' then Support_ProxyConfig
else if command = 'update_checknow' then Update_CheckNow
+ else if command = 'update_applynow' then Update_ApplyNow
else if command = 'contact_support' then Support_ContactSupport(params) // I4390
@@ -793,7 +797,7 @@ procedure TfrmMain.Support_ProxyConfig;
Free;
end;
end;
-
+// TODO-WINDOWS-UPDATES: #10210 Remove Update
procedure TfrmMain.Support_UpdateCheck;
begin
with TOnlineUpdateCheck.Create(Self, True, False) do
@@ -821,16 +825,28 @@ procedure TfrmMain.Support_UpdateCheck;
end;
procedure TfrmMain.Update_CheckNow;
+var UpdateCheck : TRemoteUpdateCheck;
begin
- with TOnlineUpdateCheck.Create(Self, True, True, True) do
+ UpdateCheck := TRemoteUpdateCheck.Create(True);
try
- Run;
+ UpdateCheck.Run;
finally
- Free;
+ UpdateCheck.Free;
end;
DoRefresh;
end;
+procedure TfrmMain.Update_ApplyNow;
+var
+ ShellPath, s: string;
+ FResult: Boolean;
+begin
+ ShellPath := TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell);
+ FResult := TUtilExecute.Shell(0, ShellPath, '', '-an');
+ if not FResult then
+ KL.Log('TrmfMain: Executing Update_ApplyNow Failed'); // TODO: Make error log
+end;
+
procedure TfrmMain.TntFormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
inherited;
diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas
index 4076ab21015..b5a84792ce6 100644
--- a/windows/src/desktop/kmshell/main/initprog.pas
+++ b/windows/src/desktop/kmshell/main/initprog.pas
@@ -83,6 +83,8 @@ procedure Main(Owner: TComponent = nil);
fmUpgradeKeyboards, fmOnlineUpdateCheck,// I2548
fmOnlineUpdateAdmin, fmTextEditor,
fmBackgroundUpdateCheck,
+ fmBackgroundDownload,
+ fmApplyInstallNow,
fmFirstRun, // I2562
fmKeyboardWelcome, // I2569
fmKeyboardPrint, // I2329
@@ -143,6 +145,7 @@ implementation
UpgradeMnemonicLayout,
utilfocusappwnd,
utilkmshell,
+ Keyman.System.UpdateStateMachine,
KeyboardTIPCheck,
@@ -250,8 +253,12 @@ function Init(var FMode: TKMShellMode; KeyboardFileNames: TStrings; var FSilent,
else if s = '-?' then FMode := fmHelpKMShell
else if s = '-h' then FMode := fmHelp
else if s = '-t' then FMode := fmTextEditor
+ //TODO-WINDOWS-UPDATES: will remove -ouc not used
+ // -buc uses the Statemachine can be used for external scripts to force a check
else if s = '-ouc' then FMode := fmOnlineUpdateCheck
else if s = '-buc' then FMode := fmBackgroundUpdateCheck
+ else if s = '-bd' then FMode := fmBackgroundDownload
+ else if s = '-an' then FMode := fmApplyInstallNow
else if s = '-basekeyboard' then FMode := fmBaseKeyboard // I4169
else if s = '-nowelcome' then FNoWelcome := True
else if s = '-kw' then FMode := fmKeyboardWelcome // I2569
@@ -384,7 +391,7 @@ procedure RunKMCOM(FMode: TKMShellMode; KeyboardFileNames: TStrings; FSilent, FF
kdl: IKeymanDefaultLanguage;
FIcon: string;
FMutex: TKeymanMutex; // I2720
- RemoteUpdateCheck: TRemoteUpdateCheck;
+ BUpdateSM : TUpdateStateMachine;
function FirstKeyboardFileName: WideString;
begin
if KeyboardFileNames.Count = 0
@@ -431,17 +438,31 @@ procedure RunKMCOM(FMode: TKMShellMode; KeyboardFileNames: TStrings; FSilent, FF
ShowMessage(MsgFromId(SKOSNotSupported));
Exit;
end;
- // TODO: #10038 Will add this as part of the background update state machine
- // for now just verifing the download happens via -buc switch.
- RemoteUpdateCheck := TRemoteUpdateCheck.Create(False, False);
+
+ BUpdateSM := TUpdateStateMachine.Create(False);
try
if (FMode = fmBackgroundUpdateCheck) then
begin
- RemoteUpdateCheck.Run;
+ BUpdateSM.HandleCheck;
+ Exit;
+ end
+ else if (FMode = fmBackgroundDownload) then
+ begin
+ BUpdateSM.HandleDownload;
+ Exit;
+ end
+ else if (FMode = fmApplyInstallNow) then
+ begin
+ BUpdateSM.HandleInstallNow;
Exit;
end
+ else
+ begin
+ if BUpdateSM.HandleKmShell = 1 then
+ Exit;
+ end;
finally
- RemoteUpdateCheck.Free;
+ BUpdateSM.Free;
end;
@@ -636,6 +657,7 @@ function FirstRun(FQuery, FDisablePackages, FDefaultUILanguage: string): Boolean
Pos('installdefaults', FQuery) > 0,
Pos('startwithwindows', FQuery) > 0,
Pos('checkforupdates', FQuery) > 0,
+ Pos('automaticupdates', FQuery) > 0,
FDisablePackages,
FDefaultUILanguage,
Pos('automaticallyreportusage', FQuery) > 0); // I2651, I2753
diff --git a/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas b/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas
index 2edcfbbcf70..9b5f779c8f1 100644
--- a/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas
+++ b/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas
@@ -1,18 +1,18 @@
(*
Name: UfrmDownloadProgress
Copyright: Copyright (C) SIL International.
- Documentation:
- Description:
+ Documentation:
+ Description:
Create Date: 4 Dec 2006
Modified Date: 18 May 2012
Authors: mcdurdin
- Related Files:
- Dependencies:
+ Related Files:
+ Dependencies:
- Bugs:
- Todo:
- Notes:
+ Bugs:
+ Todo:
+ Notes:
History: 04 Dec 2006 - mcdurdin - Initial version
05 Dec 2006 - mcdurdin - Localize caption
15 Jan 2007 - mcdurdin - Use font from locale.xml
diff --git a/windows/src/desktop/kmshell/util/utilkmshell.pas b/windows/src/desktop/kmshell/util/utilkmshell.pas
index 74857532839..76eb4929dd1 100644
--- a/windows/src/desktop/kmshell/util/utilkmshell.pas
+++ b/windows/src/desktop/kmshell/util/utilkmshell.pas
@@ -33,7 +33,7 @@
interface
uses
- System.UITypes,
+ System.UITypes, System.IOUtils, System.Types,
Dialogs, Windows, ComObj, shlobj, controls, sysutils, classes;
const
@@ -96,6 +96,7 @@ procedure SplitString(const instr: string; var outstr1, outstr2: string; const s
function ValidDirectory(const dir: string): string;
function GetLongFile(APath:String):String;
+procedure GetFileNamesInDirectory(const directoryPath: string; var fileNames: TStringDynArray);
function TSFInstalled: Boolean;
@@ -540,6 +541,19 @@ function GetLongFile(APath:String):String;
until Length(APath)=0;
end; {Peter Haas}
+procedure GetFileNamesInDirectory(const directoryPath: string; var fileNames: TStringDynArray);
+
+begin
+ // Check if the directory exists
+ if TDirectory.Exists(directoryPath) then
+ begin
+ // Retrieve file names within the directory
+ fileNames := TDirectory.GetFiles(directoryPath);
+ end
+ else
+ KL.Log('Directory does not exist.');
+end;
+
{ TString }
constructor TString.Create(const AString: string);
diff --git a/windows/src/desktop/kmshell/xml/menuframe_update.jpg b/windows/src/desktop/kmshell/xml/menuframe_update.jpg
new file mode 100644
index 00000000000..6eeb76bfd6f
Binary files /dev/null and b/windows/src/desktop/kmshell/xml/menuframe_update.jpg differ
diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml
index c8910b55b9d..24909d34864 100644
--- a/windows/src/desktop/kmshell/xml/strings.xml
+++ b/windows/src/desktop/kmshell/xml/strings.xml
@@ -417,6 +417,11 @@
Show welcome screen
+
+
+
+ Automatically check for updates and download
+
@@ -586,8 +591,6 @@
Diagnostics
-
-
@@ -712,6 +715,10 @@ keyboard that you use in Windows. Keyman keyboards will adapt automatically to
+
+
+
+ Update
@@ -803,6 +810,16 @@ keyboard that you use in Windows. Keyman keyboards will adapt automatically to
+
+
+
+ Apply update now
+
+
+
+
+ Check for new updates
+
diff --git a/windows/src/desktop/setup/RunTools.pas b/windows/src/desktop/setup/RunTools.pas
index 341f036c6cd..0b654cfeec5 100644
--- a/windows/src/desktop/setup/RunTools.pas
+++ b/windows/src/desktop/setup/RunTools.pas
@@ -82,7 +82,7 @@ TRunTools = class
InstallSuccess: Boolean);
function InstallMSI(msiLocation: TInstallInfoFileLocation; var InstallDefaults: Boolean; ContinueSetup: Boolean): Boolean;
procedure ConfigFirstRun(StartKeyman,StartWithWindows,
- CheckForUpdates,StartDisabled,StartWithConfiguration,InstallDefaults,
+ CheckForUpdates,AutomaticUpdates,StartDisabled,StartWithConfiguration,InstallDefaults,
AutomaticallyReportUsage: Boolean);
procedure PrepareForReboot(res: Cardinal; InstallDefaults: Boolean);
function RestartWindows: Boolean;
@@ -101,7 +101,7 @@ TRunTools = class
destructor Destroy; override;
procedure CheckInternetConnectedState;
function DoInstall(Handle: THandle;
- StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled,
+ StartAfterInstall, StartWithWindows, CheckForUpdates, AutomaticUpdates, StartDisabled,
StartWithConfiguration, InstallDefaults, AutomaticallyReportUsage, ContinueSetup: Boolean): Boolean;
procedure LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean = True);
procedure LogInfo(const msg: string; ShowDialogIfNotSilent: Boolean = False);
@@ -194,7 +194,7 @@ destructor TRunTools.Destroy;
end;
function TRunTools.DoInstall(Handle: THandle;
- StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled,
+ StartAfterInstall, StartWithWindows, CheckForUpdates, AutomaticUpdates, StartDisabled,
StartWithConfiguration, InstallDefaults, AutomaticallyReportUsage, ContinueSetup: Boolean): Boolean;
var
msiLocation: TInstallInfoFileLocation;
@@ -224,7 +224,7 @@ function TRunTools.DoInstall(Handle: THandle;
Exit(False);
end;
- ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates,
+ ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates,AutomaticUpdates,
StartDisabled,StartWithConfiguration,InstallDefaults,AutomaticallyReportUsage);
Result := True;
@@ -588,7 +588,7 @@ procedure TRunTools.WaitFor(hProcess: THandle; var Waiting, Cancelled: Boolean);
end;
end;
-procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates,
+procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates,AutomaticUpdates,
StartDisabled,StartWithConfiguration,InstallDefaults,AutomaticallyReportUsage: Boolean);
var
i: Integer;
@@ -686,6 +686,7 @@ procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates,
if StartWithWindows then s := s + 'StartWithWindows,';
if CheckForUpdates then s := s + 'CheckForUpdates,';
+ if AutomaticUpdates then s := s + 'AutomaticUpdates,';
if AutomaticallyReportUsage then s := s + 'AutomaticallyReportUsage,';
if InstallDefaults then
diff --git a/windows/src/desktop/setup/UfrmRunDesktop.pas b/windows/src/desktop/setup/UfrmRunDesktop.pas
index 293df6a6138..ded19ae75cd 100644
--- a/windows/src/desktop/setup/UfrmRunDesktop.pas
+++ b/windows/src/desktop/setup/UfrmRunDesktop.pas
@@ -113,6 +113,7 @@ TfrmRunDesktop = class(TForm)
FCanUpgrade9: Boolean;
FCanUpgrade10: Boolean;
FCheckForUpdates: Boolean;
+ FAutomaticUpdates: Boolean;
FStartAfterInstall: Boolean;
FStartWithWindows: Boolean;
FAutomaticallyReportUsage: Boolean;
@@ -523,7 +524,7 @@ procedure TfrmRunDesktop.DoInstall(Silent, PromptForReboot: Boolean); // I3355
SetupMSI; // I2644
if GetRunTools.DoInstall(Handle, FStartAfterInstall, FStartWithWindows, FCheckForUpdates,
- FInstallInfo.StartDisabled, FInstallInfo.StartWithConfiguration, FInstallDefaults,
+ FAutomaticUpdates, FInstallInfo.StartDisabled, FInstallInfo.StartWithConfiguration, FInstallDefaults,
FAutomaticallyReportUsage, FContinueSetup) then
begin
if not Silent and not FStartAfterInstall then // I2610
@@ -1032,6 +1033,7 @@ procedure TfrmRunDesktop.GetDefaultSettings; // I2651
begin
FStartWithWindows := True; // I2607
FCheckForUpdates := True; // I2609
+ FAutomaticUpdates := True;
try
with CreateHKCURegistry do // I2749
@@ -1041,6 +1043,8 @@ procedure TfrmRunDesktop.GetDefaultSettings; // I2651
FCheckForUpdates := ValueExists(SRegValue_CheckForUpdates) and ReadBool(SRegValue_CheckForUpdates);
FStartWithWindows := ValueExists(SRegValue_UpgradeRunKeyman) or
(OpenKeyReadOnly('\' + SRegKey_WindowsRun_CU) and ValueExists(SRegValue_WindowsRun_Keyman));
+ FAutomaticUpdates := not ValueExists(SRegValue_AutomaticUpdates) or ReadBool(SRegValue_AutomaticUpdates);
+
end
else if FCanUpgrade10 and OpenKeyReadOnly(SRegKey_KeymanEngine100_ProductOptions_Desktop_CU) then // I4293
begin
diff --git a/windows/src/engine/insthelper/Keyman.System.Install.EnginePostInstall.pas b/windows/src/engine/insthelper/Keyman.System.Install.EnginePostInstall.pas
index bdf602ae056..6c756bbbac2 100644
--- a/windows/src/engine/insthelper/Keyman.System.Install.EnginePostInstall.pas
+++ b/windows/src/engine/insthelper/Keyman.System.Install.EnginePostInstall.pas
@@ -25,6 +25,38 @@ function ReportFailure(hInstall: MSIHANDLE; const func: string; code: UINT): UIN
Result := code;
end;
+
+function UpdateState: Boolean;
+var
+ UpdateStr : UnicodeString;
+ hk: Winapi.Windows.HKEY;
+begin
+
+ Result := False;
+ UpdateStr := 'usPostInstall';
+
+ if RegCreateKeyEx(HKEY_CURRENT_USER, PChar(SRegKey_KeymanEngine_CU), 0, nil, 0, KEY_ALL_ACCESS, nil, &hk, nil) = ERROR_SUCCESS then
+ begin
+ try
+ if RegSetValueEx(hk, PChar(SRegValue_Update_State), 0, REG_SZ, PChar(UpdateStr), (Length(UpdateStr)+1) * SizeOf(Char)) = ERROR_SUCCESS then
+ begin
+ Result := True;
+ end
+ else
+ begin
+ // TODO-WINDOWS-UPDATES: error log
+ end;
+ finally
+ RegCloseKey(hk);
+ end;
+ end
+ else
+ begin
+ //TODO-WINDOWS-UPDATES: error log creating key
+ end;
+end;
+
+
{
Add permission for ALL APPLICATION PACKAGES to read %ProgramData%\Keyman folder
}
@@ -61,6 +93,8 @@ function EnginePostInstall(hInstall: MSIHANDLE): UINT;
end;
Result := ERROR_SUCCESS;
+ // TODO-WINDOWS-UPDATES: better error checking on the registry key update
+ UpdateState;
finally
if not CloseHandle(hFile) then
diff --git a/windows/src/engine/keyman/keyman.dpr b/windows/src/engine/keyman/keyman.dpr
index 8685e8f4a62..dadcca01fc5 100644
--- a/windows/src/engine/keyman/keyman.dpr
+++ b/windows/src/engine/keyman/keyman.dpr
@@ -32,7 +32,7 @@ uses
UfrmOSKPlugInBase in 'viskbd\UfrmOSKPlugInBase.pas' {frmOSKPlugInBase},
UfrmOSKCharacterMap in 'viskbd\UfrmOSKCharacterMap.pas' {frmOSKCharacterMap},
UfrmOSKEntryHelper in 'viskbd\UfrmOSKEntryHelper.pas' {frmOSKEntryHelper},
- TTInfo in '..\..\..\..\common\windows\delphi\general\TTInfo.pas',
+ ttinfo in '..\..\..\..\common\windows\delphi\general\ttinfo.pas',
UnicodeData in '..\..\..\..\common\windows\delphi\charmap\UnicodeData.pas',
CharacterMapSettings in '..\..\..\..\common\windows\delphi\charmap\CharacterMapSettings.pas',
CharacterRanges in '..\..\..\..\common\windows\delphi\charmap\CharacterRanges.pas',
@@ -112,7 +112,8 @@ uses
Sentry.Client.Vcl in '..\..\..\..\common\windows\delphi\ext\sentry\Sentry.Client.Vcl.pas',
sentry in '..\..\..\..\common\windows\delphi\ext\sentry\sentry.pas',
Keyman.System.KeymanSentryClient in '..\..\..\..\common\windows\delphi\general\Keyman.System.KeymanSentryClient.pas',
- Keyman.System.LocaleStrings in '..\..\global\delphi\cust\Keyman.System.LocaleStrings.pas';
+ Keyman.System.LocaleStrings in '..\..\global\delphi\cust\Keyman.System.LocaleStrings.pas',
+ Keyman.System.ExecutionHistory in '..\..\..\..\common\windows\delphi\general\Keyman.System.ExecutionHistory.pas';
{$R ICONS.RES}
{$R VERSION.RES}
diff --git a/windows/src/engine/keyman/keyman.dproj b/windows/src/engine/keyman/keyman.dproj
index 44959854d5a..aa70b2ab7bd 100644
--- a/windows/src/engine/keyman/keyman.dproj
+++ b/windows/src/engine/keyman/keyman.dproj
@@ -141,7 +141,7 @@
-
+
@@ -238,6 +238,7 @@
+
Cfg_2
@@ -299,21 +300,21 @@
False
-
+
- keyman.exe
+ .\
true
-
+
keyman.rsm
true
-
+
- .\
+ keyman.exe
true
diff --git a/windows/src/engine/keyman/main.pas b/windows/src/engine/keyman/main.pas
index 2c019150d7c..79db9c8baa2 100644
--- a/windows/src/engine/keyman/main.pas
+++ b/windows/src/engine/keyman/main.pas
@@ -40,9 +40,11 @@ implementation
System.Win.Registry,
GetOsVersion,
+ Keyman.System.ExecutionHistory,
Keyman.System.Security,
Keyman.Winapi.VersionHelpers,
KeymanVersion,
+ Klog,
RegistryKeys,
UfrmKeyman7Main,
UserMessages;
@@ -76,9 +78,10 @@ procedure RunProgram;
hMutex: Cardinal;
begin
-
if not ValidateParameters(FCommand) then Exit;
+ RecordKeymanStarted;
+
hProgramMutex := CreateMutex(nil, False, 'KeymanEXE70');
if hProgramMutex = 0 then
begin
diff --git a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas
index 906e121f077..8a18f3be262 100644
--- a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas
+++ b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas
@@ -121,7 +121,7 @@ TKeymanOptionInfo = record
GroupName: string;
end;
-const KeymanOptionInfo: array[0..16] of TKeymanOptionInfo = ( // I3331 // I3620 // I4552
+const KeymanOptionInfo: array[0..17] of TKeymanOptionInfo = ( // I3331 // I3620 // I4552
// Global options
(opt: koKeyboardHotkeysAreToggle; RegistryName: SRegValue_KeyboardHotkeysAreToggle; OptionType: kotBool; BoolValue: False; GroupName: 'kogGeneral'),
@@ -129,7 +129,8 @@ TKeymanOptionInfo = record
(opt: koAltGrCtrlAlt; RegistryName: SRegValue_AltGrCtrlAlt; OptionType: kotBool; BoolValue: False; GroupName: 'kogGeneral'),
(opt: koRightModifierHK; RegistryName: SRegValue_AllowRightModifierHotKey; OptionType: kotBool; BoolValue: False; GroupName: 'kogGeneral'),
(opt: koShowHints; RegistryName: SRegValue_EnableHints; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'),
- (opt: koBaseLayout; RegistryName: SRegValue_UnderlyingLayout; OptionType: kotLong; IntValue: 0; GroupName: 'kogGeneral'),
+ (opt: koBaseLayout; RegistryName: SRegValue_UnderlyingLayout; OptionType: kotLong; IntValue: 0; GroupName: 'kogGeneral'),
+ (opt: koAutomaticUpdate; RegistryName: SRegValue_AutomaticUpdates; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'),
(opt: koAutomaticallyReportErrors; RegistryName: SRegValue_AutomaticallyReportErrors; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), // I4393
(opt: koAutomaticallyReportUsage; RegistryName: SRegValue_AutomaticallyReportUsage; OptionType: kotBool; BoolValue: True; GroupName: 'kogGeneral'), // I4393
diff --git a/windows/src/global/delphi/general/KeymanOptionNames.pas b/windows/src/global/delphi/general/KeymanOptionNames.pas
index 73356d19cb3..2d4be34c0a7 100644
--- a/windows/src/global/delphi/general/KeymanOptionNames.pas
+++ b/windows/src/global/delphi/general/KeymanOptionNames.pas
@@ -10,6 +10,7 @@ interface
koRightModifierHK,
koReleaseShiftKeysAfterKeyPress,
koShowHints, // I1256
+ koAutomaticUpdate,
// Startup options
koTestKeymanFunctioning,
koStartWithWindows,