diff --git a/HISTORY.md b/HISTORY.md index 5d9206857f..78352f95e5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,22 @@ # Keyman Version History +## 18.0.169 alpha 2025-01-17 + +* chore(windows): remove `postinstall` state from mermaid diagram (#12923) + +## 18.0.168 alpha 2025-01-16 + +* chore: update macOS environment-variable shell script (#12878) +* fix(android): use main looper to dispatch key events when OSK is hidden (#12871) +* chore(web): integrates predictive-text builds to top level script, reconnects headless tests (#12866) +* chore(ios): Update crowdin strings for Khmer (#12910) +* chore(windows): merge master epic windows updates (#12904) +* fix(developer): detect invalid key ids in touch layout files (#12895) +* chore: use GitHub PR titles when writing HISTORY.md (#12907) +* fix(developer): filter incorrect fonts out of .keyboard_info (#12909) +* change(web): make 'keep' transform pattern match standard suggestion pattern by including the prefix string (#12906) +* chore: Add docker images for building the different platforms (#11397) + ## 18.0.167 alpha 2025-01-15 * chore: changes to use https and removes anchor in docs (#12838) @@ -213,7 +230,7 @@ ## 18.0.134 alpha 2024-11-04 -* (#12606) +* fix(developer): ldml don't allow a uset as right-hand-side variable (#12606) ## 18.0.133 alpha 2024-11-01 diff --git a/VERSION.md b/VERSION.md index 492b0286be..df7920cb67 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.168 \ No newline at end of file +18.0.170 \ No newline at end of file diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 7d731f2b63..de01e0bcc5 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -31,6 +31,7 @@ import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; @@ -70,6 +71,10 @@ final class KMKeyboard extends WebView { protected KeyboardType keyboardType = KeyboardType.KEYBOARD_TYPE_UNDEFINED; protected ArrayList javascriptAfterLoad = new ArrayList<>(); + // .getMainLooper() returns the looper associated with the main UI thread. + // https://stackoverflow.com/questions/13974661/runonuithread-vs-looper-getmainlooper-post-in-android + private Handler jsQueuer = new Handler(Looper.getMainLooper()); + private static String currentKeyboard = null; /** @@ -368,7 +373,7 @@ final class KMKeyboard extends WebView { if(this.javascriptAfterLoad.size() > 0) { // Don't call this WebView method on just ANY thread - run it on the main UI thread. // https://stackoverflow.com/a/22611010 - this.postDelayed(new Runnable() { + jsQueuer.postDelayed(new Runnable() { @Override public void run() { StringBuilder allCalls = new StringBuilder(); 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 0000000000..33dff40780 --- /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/Keyman.System.UpdateCheckResponse.pas b/common/windows/delphi/general/Keyman.System.UpdateCheckResponse.pas index 918c396411..adc90ab152 100644 --- a/common/windows/delphi/general/Keyman.System.UpdateCheckResponse.pas +++ b/common/windows/delphi/general/Keyman.System.UpdateCheckResponse.pas @@ -35,6 +35,7 @@ type TUpdateCheckResponse = record private + FOriginalData: string; FInstallSize: Int64; FInstallURL: string; FNewVersion: string; @@ -46,9 +47,13 @@ type FFileName: string; function ParseKeyboards(nodes: TJSONObject): Boolean; function ParseLanguages(i: Integer; v: TJSONValue): Boolean; + function DoParse(const message, app, currentVersion: string): Boolean; public function Parse(const message: AnsiString; const app, currentVersion: string): Boolean; + procedure SaveToFile(const Filename: string); + function LoadFromFile(const Filename, app, currentVersion: string): Boolean; + property CurrentVersion: string read FCurrentVersion; property NewVersion: string read FNewVersion; property NewVersionWithTag: string read FNewVersionWithTag; @@ -58,25 +63,32 @@ type property ErrorMessage: string read FErrorMessage; property Status: TUpdateCheckResponseStatus read FStatus; property Packages: TUpdateCheckResponsePackages read FPackages; + property OriginalData: string read FOriginalData; end; implementation uses + System.Classes, System.Generics.Collections, versioninfo; { TUpdateCheckResponse } function TUpdateCheckResponse.Parse(const message: AnsiString; const app, currentVersion: string): Boolean; +begin + Result := DoParse(string(UTF8String(message)), app, currentVersion); +end; + +function TUpdateCheckResponse.DoParse(const message, app, currentVersion: string): Boolean; var node, doc: TJSONObject; begin + FOriginalData := message; FCurrentVersion := currentVersion; FStatus := ucrsNoUpdate; - // TODO: test with UTF8 characters in response - doc := TJSONObject.ParseJSONValue(UTF8String(message)) as TJSONObject; + doc := TJSONObject.ParseJSONValue(UTF8String(FOriginalData)) as TJSONObject; if doc = nil then begin FErrorMessage := Format('Invalid response:'#13#10'%s', [string(message)]); @@ -168,4 +180,29 @@ begin Result := True; end; +function TUpdateCheckResponse.LoadFromFile(const Filename, app, currentVersion: string): Boolean; +var + ss: TStringStream; +begin + ss := TStringStream.Create('', TEncoding.UTF8); + try + ss.LoadFromFile(Filename); + Result := DoParse(ss.DataString, app, currentVersion); + finally + ss.Free; + end; +end; + +procedure TUpdateCheckResponse.SaveToFile(const Filename: string); +var + ss: TStringStream; +begin + ss := TStringStream.Create(FOriginalData, TEncoding.UTF8); + try + ss.SaveToFile(Filename); + finally + ss.Free; + end; +end; + end. diff --git a/common/windows/delphi/general/KeymanPaths.pas b/common/windows/delphi/general/KeymanPaths.pas index fbfa9961d7..08554852cc 100644 --- a/common/windows/delphi/general/KeymanPaths.pas +++ b/common/windows/delphi/general/KeymanPaths.pas @@ -17,6 +17,8 @@ type const S_CEF_SubProcess = 'kmbrowserhost.exe'; const S_CEF_SubProcess_Developer = 'kmdbrowserhost.exe'; const S_CustomisationFilename = 'desktop_pro.pxx'; + + const S_KeymanAppData_UpdateCache = 'Keyman\UpdateCache\'; public const S_KMShell = 'kmshell.exe'; const S_TSysInfoExe = 'tsysinfo.exe'; @@ -27,8 +29,10 @@ type const S_FallbackKeyboardPath = 'Keyboards\'; const S__Package = '_Package\'; const S_MCompileExe = 'mcompile.exe'; + const S_UpdateCache_Metadata = 'cache.json'; class function ErrorLogPath(const app: string = ''): string; static; class function KeymanHelpPath(const HelpFile: string): string; static; + class function KeymanUpdateCachePath(const filename: string = ''): string; static; class function KeymanDesktopInstallPath(const filename: string = ''): string; static; class function KeymanEngineInstallPath(const filename: string = ''): string; static; class function KeymanDesktopInstallDir: string; static; @@ -423,6 +427,11 @@ begin Result := ''; end; +class function TKeymanPaths.KeymanUpdateCachePath(const filename: string): string; +begin + Result := GetFolderPath(CSIDL_LOCAL_APPDATA) + S_KeymanAppData_UpdateCache + filename; +end; + class function TKeymanPaths.RunningFromSource(var keyman_root: string): Boolean; begin // On developer machines, if we are running within the source repo, then use diff --git a/common/windows/delphi/general/RegistryKeys.pas b/common/windows/delphi/general/RegistryKeys.pas index e351b3860f..bacca938c6 100644 --- a/common/windows/delphi/general/RegistryKeys.pas +++ b/common/windows/delphi/general/RegistryKeys.pas @@ -175,8 +175,10 @@ const 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 @@ const 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/core/tests/unit/km_core_keyboard_api.tests.cpp b/core/tests/unit/km_core_keyboard_api.tests.cpp index 2898f9e7cd..06e604c023 100644 --- a/core/tests/unit/km_core_keyboard_api.tests.cpp +++ b/core/tests/unit/km_core_keyboard_api.tests.cpp @@ -56,10 +56,8 @@ TEST_F(KmCoreKeyboardApiTests, LoadFromBlobNull) { // Setup km::core::path kmxfile = ""; - std::unique_ptr data(new uint8_t[0]); - // Execute - auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.get(), 0, &this->keyboard); + auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), nullptr, 0, &this->keyboard); // Verify EXPECT_EQ(status, KM_CORE_STATUS_INVALID_ARGUMENT); diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index 74c06d5d12..ee784429b9 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -499,9 +499,6 @@ export class KeyboardInfoCompiler implements KeymanCompiler { keyboard_info.languages[language] = {}; } - const fontSource = [].concat(...kmpJsonData.keyboards.map(e => e.displayFont ? [e.displayFont] : []), ...kmpJsonData.keyboards.map(e => e.webDisplayFonts ?? [])); - const oskFontSource = [].concat(...kmpJsonData.keyboards.map(e => e.oskFont ? [e.oskFont] : []), ...kmpJsonData.keyboards.map(e => e.webOskFonts ?? [])); - let commonScript = null; for(const bcp47 of Object.keys(keyboard_info.languages)) { @@ -528,6 +525,20 @@ export class KeyboardInfoCompiler implements KeymanCompiler { // do it right now. // + // The code below: + // 1. Only includes fonts associated with keyboards which support the current bcp47 (filter) + // 2. Joins the displayFont and webDisplayFonts data, and removes duplicates (...new Set()) + + const supportedKeyboards = kmpJsonData.keyboards.filter(k => k.languages.find(lang => lang.id == bcp47)); + const fontSource = [...new Set([].concat( + ...supportedKeyboards.map(e => e.displayFont ? [e.displayFont] : []), + ...supportedKeyboards.map(e => e.webDisplayFonts ?? []) + ))]; + const oskFontSource = [...new Set([].concat( + ...supportedKeyboards.map(e => e.oskFont ? [e.oskFont] : []), + ...supportedKeyboards.map(e => e.webOskFonts ?? []) + ))]; + if(fontSource.length) { language.font = await this.fontSourceToKeyboardInfoFont(kpsFilename, kmpJsonData, fontSource); if(language.font == null) { diff --git a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts index a5ee9da293..9bed88f5f8 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts @@ -46,6 +46,16 @@ function GetKeyIdUnicodeType(value: string): TKeyIdType { function KeyIdType(FId: string): TKeyIdType { // I4142 FId = FId.toUpperCase(); + + // Validate key id format: + // K_xxxx -- predefined virtual key - restricted character set + // T_xxxx -- custom 'touch' virtual key - touch key id + // U_ABCD_1234 -- Unicode key id (1+ chars) + // x00 -- "ISO" key identifier (not currently supported in touch layout files) + if(!/^((K_[A-Z0-9_?]+)|(T_\S+)|(U_[0-9A-F_]+))$/.test(FId)) { + // note: |[A-Z][0-9][0-9] -- ISO key identifiers not currently supported + return TKeyIdType.Key_Invalid; + } switch(FId.charAt(0)) { case 'T': return TKeyIdType.Key_Touch; diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.keyman-touch-layout new file mode 100644 index 0000000000..e08e33eedc --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.keyman-touch-layout @@ -0,0 +1,550 @@ +{ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "U_1E6B[_0307]", + "text": "១" + }, + { + "id": "K_2", + "text": "២" + }, + { + "id": "K_3", + "text": "៣" + }, + { + "id": "K_4", + "text": "៤" + }, + { + "id": "K_5", + "text": "៥" + }, + { + "id": "K_6", + "text": "៦" + }, + { + "id": "K_7", + "text": "៧" + }, + { + "id": "K_8", + "text": "៨" + }, + { + "id": "K_9", + "text": "៩" + }, + { + "id": "K_0", + "text": "០" + }, + { + "id": "K_HYPHEN", + "text": "ឥ" + }, + { + "id": "K_EQUAL", + "text": "ឲ" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ឆ", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "" + }, + { + "id": "K_R", + "text": "រ" + }, + { + "id": "K_T", + "text": "ត" + }, + { + "id": "K_Y", + "text": "យ" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "" + }, + { + "id": "K_P", + "text": "ផ" + }, + { + "id": "K_LBRKT", + "text": "" + }, + { + "id": "K_RBRKT", + "text": "ឪ" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "«" + }, + { + "id": "K_A", + "text": "" + }, + { + "id": "K_S", + "text": "ស" + }, + { + "id": "K_D", + "text": "ដ" + }, + { + "id": "K_F", + "text": "ថ" + }, + { + "id": "K_G", + "text": "ង" + }, + { + "id": "K_H", + "text": "ហ" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ក" + }, + { + "id": "K_L", + "text": "ល" + }, + { + "id": "K_COLON", + "text": "" + }, + { + "id": "K_QUOTE", + "text": "" + }, + { + "id": "K_BKSLASH", + "text": "ឮ" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ឋ" + }, + { + "id": "K_X", + "text": "ខ" + }, + { + "id": "K_C", + "text": "ច" + }, + { + "id": "K_V", + "text": "វ" + }, + { + "id": "K_B", + "text": "ប" + }, + { + "id": "K_N", + "text": "ន" + }, + { + "id": "K_M", + "text": "ម" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "។" + }, + { + "id": "K_SLASH", + "text": "" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "*AltGr*", + "width": "160", + "sp": "1" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "160", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "​", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "160", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "ៗ" + }, + { + "id": "K_3", + "text": "\"" + }, + { + "id": "K_4", + "text": "៛" + }, + { + "id": "K_5", + "text": "%" + }, + { + "id": "K_6", + "text": "" + }, + { + "id": "K_7", + "text": "" + }, + { + "id": "K_8", + "text": "" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ឈ", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "" + }, + { + "id": "K_R", + "text": "ឬ" + }, + { + "id": "K_T", + "text": "ទ" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "" + }, + { + "id": "K_P", + "text": "ភ" + }, + { + "id": "K_LBRKT", + "text": "" + }, + { + "id": "K_RBRKT", + "text": "ឧ" + }, + { + "id": "T_new_364", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "»" + }, + { + "id": "K_A", + "text": "" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "ឌ" + }, + { + "id": "K_F", + "text": "ធ" + }, + { + "id": "K_G", + "text": "អ" + }, + { + "id": "K_H", + "text": "ះ" + }, + { + "id": "K_J", + "text": "ញ" + }, + { + "id": "K_K", + "text": "គ" + }, + { + "id": "K_L", + "text": "ឡ" + }, + { + "id": "K_COLON", + "text": "" + }, + { + "id": "K_QUOTE", + "text": "" + }, + { + "id": "K_BKSLASH", + "text": "ឭ" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ឍ" + }, + { + "id": "K_X", + "text": "ឃ" + }, + { + "id": "K_C", + "text": "ជ" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "ព" + }, + { + "id": "K_N", + "text": "ណ" + }, + { + "id": "K_M", + "text": "" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "៕" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_390", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "*AltGr*", + "width": "160", + "sp": "1" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "160", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "160", + "sp": "1" + } + ] + } + ] + } + ], + "font": "Arial" + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.kmn new file mode 100644 index 0000000000..9dfa805bb5 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/error_touch_layout_invalid_identifier.kmn @@ -0,0 +1,10 @@ +store(&VERSION) '15.0' +store(&NAME) "error_touch_layout_invalid_identifier" +store(©RIGHT) '© 2015-2024 SIL Global' +store(&TARGETS) 'any' +store(&LAYOUTFILE) 'error_touch_layout_invalid_identifier.keyman-touch-layout' +store(&KEYBOARDVERSION) '1.3' + +begin Unicode > use(main) + +group(main) using keys \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts index 37a58384b2..d90b33c312 100644 --- a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts +++ b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts @@ -36,7 +36,9 @@ describe('KeymanWeb Compiler', function() { }); this.afterEach(function() { - callbacks.printMessages(); + if(this.currentTest?.isFailed() || debug) { + callbacks.printMessages(); + } callbacks.clear(); }); @@ -203,6 +205,19 @@ describe('KeymanWeb Compiler', function() { assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel)); }); + it('should give error ERROR_TouchLayoutInvalidIdentifier if a virtual key is badly formatted e.g. U_1234[_5678]', async function() { + // #12870 + const filenames = generateTestFilenames('error_touch_layout_invalid_identifier'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNull(result); + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_MinimumCoreEngineVersion)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumWebEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_TouchLayoutInvalidIdentifier)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_InvalidTouchLayoutFile)); + assert.lengthOf(callbacks.messages, 2); + }); + }); async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md index 37b2aafa1a..0f531acb59 100644 --- a/docs/build/linux-ubuntu.md +++ b/docs/build/linux-ubuntu.md @@ -238,83 +238,25 @@ Android projects. `JAVA_HOME_11` is mostly used by CI. ## Docker Builder The Docker builder allows you to perform a build from anywhere Docker is supported. - -To build the docker image: - -```shell -cd linux -docker pull ubuntu:latest # (to make sure you have an up-to-date image) -docker build . -t keymanapp/keyman-linux-builder:latest -``` - -Once the image is built, it may be used to build parts of Keyman. - -**Note** that it's not yet possible to run tests in the Docker container. - -- core - - ```shell - # build 'Keyman Core' in docker - # keep linux build artifacts separate - mkdir -p $(git rev-parse --show-toplevel)/core/build/linux - docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ - -v $(git rev-parse --show-toplevel)/core/build/linux:/home/build/build/core/build \ - keymanapp/keyman-linux-builder:latest \ - core/build.sh --debug - ``` - -- linux - - ```shell - # build 'Keyman for Linux' installation in docker - docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ - --entrypoint /bin/bash keymanapp/keyman-linux-builder:latest \ - -c 'DESTDIR=/home/build /usr/bin/bashwrapper linux/build.sh --debug build install' - ``` - -- Keyman Web - - ```shell - # build 'Keyman Web' in docker - docker run --privileged -it --rm \ - -v $(git rev-parse --show-toplevel):/home/build/build \ - keymanapp/keyman-linux-builder:latest \ - web/build.sh --debug - ``` - -- Keyman for Android - - ```shell - # build 'Keyman for Android' in docker - docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ - keymanapp/keyman-linux-builder:latest \ - android/build.sh --debug - ``` - -### Customizing the builder - -You can use Docker [build args](https://docs.docker.com/build/guide/build-args/) to customize the image build. As an example, the following will build an image explicitly with Ubuntu 23.04 and Node.js 20. Check the [Dockerfile](../../linux/Dockerfile) for `ARG` entries. - -```shell -cd linux -docker pull ubuntu:23.04 # (to make sure you have an up-to-date image) -docker build . -t keymanapp/keyman-linux-builder:u23.04-node20 --build-arg OS_VERSION=23.04 --build-arg NODE_MAJOR=20 -```` +See [this README.md](../../resources/docker-images/README.md) for details. ### Using the builder with VSCode [Dev Containers](https://code.visualstudio.com/docs/devcontainers/tutorial) -1. Save the following as `.devcontainer/devcontainer.json`, updating the `image` to match the Docker image built above. +1. Save the following as `.devcontainer/devcontainer.json`, updating the `image` + to match the Docker image built above. -```json -// file: .devcontainer/devcontainer.json -{ - "name": "Keyman Ubuntu 23.04", - "image": "keymanapp/keyman-linux-builder:u23.04-node18" -} -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu -``` + ```json + // file: .devcontainer/devcontainer.json + { + "name": "Keyman Ubuntu 23.04", + "image": "keymanapp/keyman-linux-builder:u23.04-node18" + } + // For format details, see https://aka.ms/devcontainer.json. For config options, + // see the README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu + ``` -2. in VSCode, use the "Dev Containers: Open Folder In Container…" option and choose the Keyman directory. +2. in VSCode, use the "Dev Containers: Open Folder In Container…" option and + choose the Keyman directory. -3. You will be given a window which is running VSCode inside this builder image, regardless of your host OS. +3. You will be given a window which is running VSCode inside this builder + image, regardless of your host OS. diff --git a/docs/minimum-versions.md b/docs/minimum-versions.md index d30b1e858a..174f4b518e 100644 --- a/docs/minimum-versions.md +++ b/docs/minimum-versions.md @@ -67,6 +67,7 @@ https://help.keyman.com/developer/engine/android/latest-version/ | KEYMAN_MIN_VERSION_NPM | 10.5.1 | | KEYMAN_MIN_VERSION_VISUAL_STUDIO | 2019 | | KEYMAN_VERSION_CLDR | 45 | +| KEYMAN_VERSION_GRADLE | 7.6.4 | | KEYMAN_VERSION_ICU | 73.1 | | KEYMAN_VERSION_ISO639_3 | 2024-05-22 | | KEYMAN_VERSION_JAVA | 11 | diff --git a/ios/engine/KMEI/KeymanEngine/km.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/km.lproj/Localizable.strings index 47b548eea2..d6592e3e98 100644 --- a/ios/engine/KMEI/KeymanEngine/km.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/km.lproj/Localizable.strings @@ -226,6 +226,24 @@ /* Text showing name of spacebar caption - language + keyboard */ "menu-settings-spacebar-item-languageKeyboard" = "ភាសា និងក្ដារចុច"; +/* Label for the "Adjust Keyboard Height" item on the main settings screen */ +"menu-settings-adjust-keyboard-height" = "កែកម្ពស់ក្ដារចុច"; + +/* Title for the "Adjust Keyboard Height" settings child screen */ +"adjust-keyboard-height-title" = "កែក្ដារចុច"; + +/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */ +"button-label-reset-default-keyboard-height" = "ប្ដូរ​កម្ពស់ក្ដារចុចទៅលំនាំដើម"; + +/* Instruction text to drag keyboard to resize */ +"keyboard-drag-instructions" = "រំកិលព្រួញដើម្បីកែកម្ពស់ក្ដារចុច"; + +/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */ +"portrait-keyboard-rotate-instructions" = "បង្វិលឧបករណ៍ដើម្បីប្តូរទៅជាផ្ដេក"; + +/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */ +"landscape-keyboard-rotate-instructions" = "បង្វិលឧបករណ៍ដើម្បីប្តូរទៅជាបញ្ឈរ"; + /* Short text for notification: download failure for keyboard */ "notification-download-failure-keyboard" = "មិន​អាច​ទាញ​យក​ក្ដារចុច​បាន​ទេ"; diff --git a/linux/Dockerfile b/linux/Dockerfile deleted file mode 100644 index fac302b474..0000000000 --- a/linux/Dockerfile +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2022-2023 SIL Global. All rights reserved. -# -# builder image for a linux build -# see ../docs/build/linux-ubuntu.md - -ARG OS_VERSION=latest -ARG OS_PLATFORM=amd64 - -FROM --platform=${OS_PLATFORM} ubuntu:${OS_VERSION} -LABEL org.opencontainers.image.authors="SIL Global." -LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" -LABEL org.opencontainers.image.title="Keyman Linux Build Image" - -# We will switch to a build user after some installation -USER root -ENV HOME /home/build -RUN useradd -c "Build user" --home-dir $HOME --create-home --shell /usr/bin/bashwrapper build -VOLUME /home/build/build -WORKDIR /home/build/build -ENV DEBIAN_FRONTEND noninteractive -ENV DEBIAN_PRIORITY critical -ENV DEBCONF_NOWARNINGS yes - -# Update to the latest -RUN apt-get -q -y update && \ - apt-get -q -y install devscripts equivs meson python3 python3-setuptools software-properties-common curl && \ - add-apt-repository ppa:keymanapp/keyman && \ - add-apt-repository ppa:keymanapp/keyman-alpha -RUN apt-get -q -y update && \ - apt-get -q -y upgrade - -# Install dependencies -ADD debian/control /tmp/control -# Answer 'yes' to install questions -RUN (yes | mk-build-deps --install /tmp/control) || true && \ - rm /tmp/control - -# Install Node -ARG NODE_MAJOR=18 -RUN apt-get install -q -y ca-certificates curl gnupg && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"${NODE_MAJOR}".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y - -ARG EMSCRIPTEN_VERSION=3.1.44 -# Install emscripten -RUN cd /usr/share && \ - git clone https://github.com/emscripten-core/emsdk.git && \ - cd emsdk && \ - ./emsdk install ${EMSCRIPTEN_VERSION} && \ - ./emsdk activate ${EMSCRIPTEN_VERSION} && \ - echo "#!/bin/bash" > /usr/bin/bashwrapper && \ - echo "export EMSCRIPTEN_BASE=/usr/share/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper - -# Keyman Web -RUN curl --output google-chrome-stable_current_amd64.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \ - apt-get -q -y install ./google-chrome-stable_current_amd64.deb && \ - rm google-chrome-stable_current_amd64.deb && \ - echo "export CHROME_BIN=/opt/google/chrome/chrome" >> /usr/bin/bashwrapper - -# Keyman for Android -RUN apt-get -q -y install gradle maven pandoc sdkmanager jq && \ - sdkmanager platform-tools && \ - yes | sdkmanager --licenses && \ - chown -R build:build /opt/android-sdk/ && \ - echo "export ANDROID_HOME=/opt/android-sdk" >> /usr/bin/bashwrapper && \ - echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" >> /usr/bin/bashwrapper - -# Finish bashwrapper script and adjust permissions -RUN echo "\${@:-bash}" >> /usr/bin/bashwrapper && \ - chmod +x /usr/bin/bashwrapper && \ - chown -R build:build $HOME - -# now, switch to build user -USER build - -# Pre-install gradle. This will put files in ~/.gradle which will speed up builds. -RUN mkdir -p $HOME/tmp/gradle/wrapper && \ - # KMEA uses gradle-7.5.1-bin - curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.jar && \ - curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.properties && \ - curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradlew && \ - chmod +x $HOME/tmp/gradlew && \ - $HOME/tmp/gradlew --quiet && \ - # Some projects use gradle-7.5.1-all, so we pre-install that as well - curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.jar && \ - curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties && \ - curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradlew && \ - chmod +x $HOME/tmp/gradlew && \ - $HOME/tmp/gradlew --quiet && \ - rm -rf $HOME/tmp - -ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/oem/firstvoices/windows/src/xml/strings.xml b/oem/firstvoices/windows/src/xml/strings.xml index fbd099c8c7..692a582dd7 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/resources/build/builder.md b/resources/build/builder.md index b1accd5c8a..2c6162dfc0 100644 --- a/resources/build/builder.md +++ b/resources/build/builder.md @@ -86,13 +86,12 @@ This somewhat unwieldy incantation handles all our build environments. The intent is to get a good solid consistent path for the script so that we can safely include the build script, no matter what `pwd` is when the script is run. - The only modification permissible in this block is the `` text which will be a series of `../` paths taking us to the repository root from the location of the script itself. It is essential to make the include relative to the repo root, even for scripts -under the resources/ folder. Doing this gives us significant performance +under the `resources/` folder. Doing this gives us significant performance benefits. Inclusion of other scripts should be kept outside this standard build script @@ -1117,4 +1116,4 @@ Note: it is recommended that you use `$(builder_term text)` instead of [`builder_echo`]: #builderecho-function [`builder_die`]: #builderdie-function [`builder_echo_debug`]: #builderechodebug-function -[`builder_is_debug_build`]: #builderisdebugbuild-function \ No newline at end of file +[`builder_is_debug_build`]: #builderisdebugbuild-function diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index 1a577b480f..da459776eb 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -104,9 +104,9 @@ echo "increment-version.sh: running resources/build/version" pushd "$KEYMAN_ROOT" ABORT=0 if [[ -z "$fromversion" ]]; then - node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE || ABORT=$? + node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr || ABORT=$? else - node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --from "$fromversion" --to "$toversion" || ABORT=$? + node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr --from "$fromversion" --to "$toversion" || ABORT=$? fi if [[ $ABORT = 50 ]]; then @@ -179,7 +179,7 @@ if [ "$action" == "commit" ]; then # In order to avoid potential git conflicts, we run the history collater # again on the master HISTORY.md. Note that the script always exits 1 to # indicate it hasn't updated VERSION.md. We could tweak that in the future. - node resources/build/version/lib/index.js history --no-write-github-comment -t "$GITHUB_TOKEN" -b "$base" || true + node resources/build/version/lib/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true # If HISTORY.md has been updated, then we want to create a branch and push # it for review diff --git a/resources/build/minimum-versions.inc.sh b/resources/build/minimum-versions.inc.sh index 198495f0cc..41a7c3690f 100644 --- a/resources/build/minimum-versions.inc.sh +++ b/resources/build/minimum-versions.inc.sh @@ -31,7 +31,8 @@ KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automati KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019 KEYMAN_MIN_VERSION_MESON=1.0.0 -KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap +KEYMAN_VERSION_GRADLE=7.6.4 # See /android/KMEA/gradle/wrapper/gradle-wrapper.properties +KEYMAN_VERSION_ICU=73.1 # See /core/subprojects/icu-minimal.wrap # Language and runtime versions KEYMAN_VERSION_JAVA=11 # We're using Java/OpenJDK 11 diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts index 2183b11f70..a8ceda3c50 100644 --- a/resources/build/version/src/fixupHistory.ts +++ b/resources/build/version/src/fixupHistory.ts @@ -184,7 +184,7 @@ export const sendCommentToPullRequestAndRelatedIssues = async ( */ export const fixupHistory = async ( - octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, from?: string, to?: string + octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, useGitHubPRInfo: boolean, from?: string, to?: string ): Promise => { // @@ -194,7 +194,7 @@ export const fixupHistory = async ( let pulls: PRInformation[] = []; try { - pulls = await reportHistory(octokit, base, force, false, from, to); + pulls = await reportHistory(octokit, base, force, useGitHubPRInfo, from, to); } catch(e) { logWarning(String(e)); return -1; diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts index 16f1583e39..ea0b12e7e7 100644 --- a/resources/build/version/src/index.ts +++ b/resources/build/version/src/index.ts @@ -108,7 +108,7 @@ const main = async (): Promise => { if(argv._.includes('history')) { logInfo(`# Validating history for ${version}`); - changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv.from, argv.to); + changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv['github-pr'], argv.from, argv.to); logInfo(`# ${changeCount} change(s) found for ${version}\n`); } diff --git a/resources/docker-images/README.md b/resources/docker-images/README.md new file mode 100644 index 0000000000..c36503ad6a --- /dev/null +++ b/resources/docker-images/README.md @@ -0,0 +1,65 @@ +# Container + +Docker containers that can be used to build Keyman on the respective +platforms. They contain everything that a CI build agent needs to +build for the platform. + +## Prerequisites + +You'll need Docker Buildx installed to successfully be able to build the +container images. This is most easily achieved by installing the [official +Docker version](https://docs.docker.com/engine/install/ubuntu/). + +Currently it is not possible to use Podman instead of Docker due to a number +of bugs and incompatibilities in the Podman implementation. + +## Building the images + +To build the docker images: + +```shell +resources/docker-images/build.sh +``` + +By default this will create 64-bit images for building +Keyman Core, Keyman for Android, Keyman for Linux and +Keyman for Web. These images are based on the Ubuntu 24.04 +with Node 20 and Emscripten 3.1.58 (for the exact versions, +see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh)) +and are named e.g. `keyman-core-ci:default`. + +The versions can be changed, e.g. + +```shell +resources/docker-images/build.sh --ubuntu-version jammy --node 20 +``` + +This will create an image named e.g. `keyman-core-ci:jammy-node20`. + +Once the image is built, it may be used to build parts of Keyman. + +## Building locally + +It is possible to build locally with these images: + +```shell +# build 'Keyman Core' in docker +resources/docker-images/run.sh core -- core/build.sh --debug build +``` + +Note: For Core and Linux we put the generated binaries in a +container specific directory because they are platform dependent. + +If you build both with Docker and directly with the build scripts, it is +advisable to run a `git clean -dxf` before switching between the two. The +reason is that the Docker images use a different user, so that paths +will be different. + +## Running tests locally + +To run the tests locally, use the `run.sh` script: + +```shell +# Run common/web tests +resources/docker-images/run.sh web -- common/web/build.sh --debug test +``` diff --git a/resources/docker-images/android/Dockerfile b/resources/docker-images/android/Dockerfile new file mode 100644 index 0000000000..91cd095a69 --- /dev/null +++ b/resources/docker-images/android/Dockerfile @@ -0,0 +1,68 @@ +# Keyman is copyright (C) SIL Global. MIT License. + +ARG BASE_VERSION=default +FROM keymanapp/keyman-base-ci:${BASE_VERSION} +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman Android Build Image" + +# Keyman for Android +SHELL ["/bin/bash", "-c"] + +# Starting with Ubuntu 24.04 sdkmanager is no longer available, instead +# a version dependent package allows to install the cmdline tools +ARG JAVA_VERSION=11 +RUN </dev/null) + echo "OS_VER=${OS_VER}" + if (( ${OS_VER%%.*} > 22 )); then + PKG_SDKMANAGER=google-android-cmdline-tools-13.0-installer + DIR_SDK=/usr/lib/android-sdk + else + PKG_SDKMANAGER=sdkmanager + DIR_SDK=/opt/android-sdk + fi + apt-get -q -y install gradle maven pandoc $PKG_SDKMANAGER jq openjdk-${JAVA_VERSION}-jdk + sdkmanager platform-tools + yes | sdkmanager --licenses + chown -R build:build $DIR_SDK + echo "export ANDROID_HOME=$DIR_SDK" >> /usr/bin/bashwrapper + echo "export JAVA_HOME_${JAVA_VERSION}=/usr/lib/jvm/java-${JAVA_VERSION}-openjdk-amd64" >> /usr/bin/bashwrapper +EOF + +# Finish bashwrapper script and adjust permissions +RUN <> /usr/bin/bashwrapper + +if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then + /usr/bin/run-tests.sh "\${@:-bash}" +else + "\${@:-bash}" +fi +EOF + +# now, switch to build user +USER build + +VOLUME /home/build/build +WORKDIR /home/build/build + +# Pre-install gradle. This will put files in ~/.gradle which will speed up builds. +# Note it would be safer to copy these files directly from our repo rather than +# getting it over the Internet, but Docker doesn't allow us to copy files +# from outside the current directory when building the image. +RUN mkdir -p $HOME/tmp/gradle/wrapper && \ + # KMEA uses gradle-7.6.4-bin + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.jar && \ + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.properties && \ + curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradlew && \ + chmod +x $HOME/tmp/gradlew && \ + $HOME/tmp/gradlew --quiet && \ + # Some projects use gradle-7.6.4-all, so we pre-install that as well + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.jar && \ + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties && \ + curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradlew && \ + chmod +x $HOME/tmp/gradlew && \ + $HOME/tmp/gradlew --quiet && \ + rm -rf $HOME/tmp + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/resources/docker-images/base/Dockerfile b/resources/docker-images/base/Dockerfile new file mode 100644 index 0000000000..87516b3f96 --- /dev/null +++ b/resources/docker-images/base/Dockerfile @@ -0,0 +1,59 @@ +# Keyman is copyright (C) SIL Global. MIT License. + +ARG UBUNTU_VERSION=latest +FROM ubuntu:${UBUNTU_VERSION} + +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman Build Base Image" + +# We will switch to a build user after some installation +USER root +ENV HOME=/home/build +RUN grep ubuntu /etc/passwd && userdel ubuntu || true && \ + rm -rf /home/ubuntu && \ + useradd -c "Build user" --uid 1000 --home-dir $HOME --create-home --shell /usr/bin/bashwrapper build + +ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_PRIORITY=critical +ENV DEBCONF_NOWARNINGS=yes + +# Update to the latest +RUN apt-get -q -y update && \ + apt-get -q -y install ca-certificates curl gnupg meson software-properties-common sudo && \ + add-apt-repository ppa:keymanapp/keyman && \ + add-apt-repository ppa:keymanapp/keyman-alpha +RUN apt-get -q -y update && \ + apt-get -q -y upgrade + +# Allow build user to use `sudo` +RUN echo "build ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +RUN < /usr/bin/bashwrapper +#!/bin/bash +export KEYMAN_USE_NVM=1 +export DOCKER_RUNNING=true +EOF + +# Install NVM +RUN NVM_RELEASE=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep tag_name | cut -d : -f 2 | cut -d '"' -f 2) && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_RELEASE}/install.sh | bash +RUN <> /usr/bin/bashwrapper +PATH=/home/build/.keyman/node:\$PATH +export NVM_DIR="$HOME/.nvm" +. /home/build/.nvm/nvm.sh +EOF + +RUN chmod +x /usr/bin/bashwrapper && \ + chown -R build:build $HOME + +USER build +# Pre-install node +ARG REQUIRED_NODE_VERSION=unset +RUN echo "HOME=\${HOME}; REQUIRED_NODE_VERSION=${REQUIRED_NODE_VERSION}" && \ + export NVM_DIR="/home/build/.nvm" && \ + . /home/build/.nvm/nvm.sh && \ + nvm install "${REQUIRED_NODE_VERSION}" && \ + nvm use "${REQUIRED_NODE_VERSION}" + +USER root diff --git a/resources/docker-images/build.sh b/resources/docker-images/build.sh new file mode 100755 index 0000000000..8100459d47 --- /dev/null +++ b/resources/docker-images/build.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +################################ Main script ################################ + +. "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +builder_describe \ + "Build docker images" \ + ":android" \ + ":base" \ + ":core" \ + ":linux" \ + ":web" \ + "--ubuntu-version=UBUNTU_VERSION The Ubuntu version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})" \ + "--no-cache Force rebuild of docker images" \ + "build Build docker images" \ + "test Test the docker images by running configure,build,test for all or the specified platforms" + +builder_parse "$@" + +_add_build_args() { + local var=$1 + local default_var=$2 + local name=$3 + local value + + if [[ -n "${!var:-}" ]]; then + value="${!var}" + else + value="${!default_var:-}" + fi + + build_args+=(--build-arg="${var}=${value}") + + if [[ -n "${build_version:-}" ]]; then + build_version="${build_version}-${name:-}${value}" + else + build_version="${name}${value}" + fi +} + +_convert_parameters_to_build_args() { + build_args=() + build_version= + local required_node_version + # shellcheck disable=SC2034 + required_node_version="$(_print_expected_node_version)" + + _add_build_args UBUNTU_VERSION KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER "" + _add_build_args JAVA_VERSION KEYMAN_VERSION_JAVA java + _add_build_args REQUIRED_NODE_VERSION required_node_version node + _add_build_args REQUIRED_EMSCRIPTEN_VERSION KEYMAN_MIN_VERSION_EMSCRIPTEN emsdk + + if [[ -n "${BASE_VERSION:-}" ]]; then + build_args+=(--build-arg="BASE_VERSION=${BASE_VERSION}") + fi +} + +_is_default_values() { + [[ -z "${UBUNTU_VERSION:-}" ]] && [[ -z "${JAVA_VERSION:-}" ]] +} + +build_action() { + local platform=$1 + + builder_echo debug "Building image for ${platform}" + + _convert_parameters_to_build_args + + if [[ "${platform}" == "base" ]]; then + docker pull --platform "amd64" "ubuntu:${UBUNTU_VERSION:-${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER}}" + elif [[ "${platform}" == "linux" ]]; then + cp "${KEYMAN_ROOT}/linux/debian/control" "${platform}" + fi + + if builder_has_option --no-cache; then + OPTION_NO_CACHE="--no-cache" + fi + + # shellcheck disable=SC2164 + cd "${platform}" + # shellcheck disable=SC2248,SC2086 + docker build ${OPTION_NO_CACHE:-} --platform amd64 -t "keymanapp/keyman-${platform}-ci:${build_version}" "${build_args[@]}" . + # If the user didn't specify particular versions we will additionaly create an image + # with the tag 'default'. + if _is_default_values; then + builder_echo debug "Setting default tag for ${platform}" + docker build --platform amd64 -t "keymanapp/keyman-${platform}-ci:default" "${build_args[@]}" . + fi + # shellcheck disable=SC2164,SC2103 + cd - + builder_echo success "Docker image 'keymanapp/keyman-${platform}-ci:${build_version}' built" +} + +test_action() { + local platform=$1 + + builder_echo debug "Testing image for ${platform}" + ./run.sh "${platform}" -- ./build.sh configure,build,test:"${platform}" +} + +if builder_has_action build; then + build_action base + BASE_VERSION="${build_version}" + builder_run_action build:android build_action android + builder_run_action build:core build_action core + builder_run_action build:linux build_action linux + builder_run_action build:web build_action web +fi + +builder_run_action test:core test_action core +builder_run_action test:linux test_action linux +builder_run_action test:web test_action web +# Android uses artifacts from web, so it has to come after web +builder_run_action test:android test_action android diff --git a/resources/docker-images/core/Dockerfile b/resources/docker-images/core/Dockerfile new file mode 100644 index 0000000000..924e8f1e39 --- /dev/null +++ b/resources/docker-images/core/Dockerfile @@ -0,0 +1,54 @@ +# Keyman is copyright (C) SIL Global. MIT License. +# +# ARGS used in this file: +# - ARG BASE_VERSION +# - ARG REQUIRED_EMSCRIPTEN_VERSION + +ARG BASE_VERSION=default +FROM keymanapp/keyman-base-ci:${BASE_VERSION} + +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman Core Build Image" + +USER root +RUN apt-get install -qy git jq llvm meson pkgconf \ + xvfb xserver-xephyr metacity + +# Pre-install emscripten +USER build +ARG REQUIRED_EMSCRIPTEN_VERSION=unset +RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \ + export EMSDK_KEEP_DOWNLOADS=1 && \ + cd /home/build/ && \ + git clone https://github.com/emscripten-core/emsdk.git && \ + cd emsdk && \ + ./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \ + ./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION} +USER root +RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \ + echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper + +# Finish bashwrapper script and adjust permissions +RUN <> /usr/bin/bashwrapper + +if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then + /usr/bin/run-tests.sh "\${@:-bash}" +else + "\${@:-bash}" +fi +EOF + +# now, switch to build user +USER build + +# Pre-install node +RUN export NVM_DIR="/home/build/.nvm" && \ + . /home/build/.nvm/nvm.sh && \ + cd /home/build/emsdk/upstream/emscripten && \ + npm install + +VOLUME /home/build/build +WORKDIR /home/build/build + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/resources/docker-images/linux/.gitignore b/resources/docker-images/linux/.gitignore new file mode 100644 index 0000000000..4db28ac495 --- /dev/null +++ b/resources/docker-images/linux/.gitignore @@ -0,0 +1 @@ +control diff --git a/resources/docker-images/linux/Dockerfile b/resources/docker-images/linux/Dockerfile new file mode 100644 index 0000000000..3f680efe3e --- /dev/null +++ b/resources/docker-images/linux/Dockerfile @@ -0,0 +1,52 @@ +# Keyman is copyright (C) SIL Global. MIT License. + +ARG BASE_VERSION=default +FROM keymanapp/keyman-base-ci:${BASE_VERSION} +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman Linux Build Image" + +# Install dependencies +ADD control /tmp/control +# Answer 'yes' to install questions +RUN apt-get install -qy python3 python3-setuptools python3-coverage \ + devscripts equivs libdatetime-perl lcov gcovr xvfb \ + xserver-xephyr metacity mutter dbus-x11 weston xwayland && \ + (yes | mk-build-deps --install /tmp/control) || true && \ + rm /tmp/control + +# Install lcov for code coverage +# Update to the latest and install packages needed for Linux coverage reporting +# and integration tests. We need at least version 2.0 of lcov. However, +# version 2.0-4 from Noble doesn't work either on Jammy. So we use +# version 2.0-1 from Mantic. +RUN LCOV_VERSION=$(dpkg -s lcov | grep Version | cut -d' ' -f2) && \ + if dpkg --compare-versions "${LCOV_VERSION}" lt 2.0; then \ + curl -sS -o /tmp/lcov.deb --location http://mirrors.kernel.org/ubuntu/pool/universe/l/lcov/lcov_2.0-1_all.deb && \ + apt-get -qy install /tmp/lcov.deb && \ + rm /tmp/lcov.deb ; \ + fi + +RUN mkdir -p /var/run/1000 && \ + chown build:build /var/run/1000 && \ + echo "export XDG_RUNTIME_DIR=/var/run/1000" >> /usr/bin/bashwrapper + +COPY run-tests.sh /usr/bin/run-tests.sh + +# Finish bashwrapper script and adjust permissions +RUN <> /usr/bin/bashwrapper + +if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then + /usr/bin/run-tests.sh "\${@:-bash}" +else + "\${@:-bash}" +fi +EOF + +# now, switch to build user +USER build + +VOLUME /home/build/build +WORKDIR /home/build/build + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/resources/docker-images/linux/run-tests.sh b/resources/docker-images/linux/run-tests.sh new file mode 100755 index 0000000000..fbc5fb3b67 --- /dev/null +++ b/resources/docker-images/linux/run-tests.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +if [[ -z "${DOCKER_RUNNING:-}" ]]; then + echo "This script is intended to be run inside a docker container." + exit 0 +fi + +# Start system dbus +sudo dbus-daemon --system --fork + +# Start session dbus +# shellcheck disable=SC2046 # SC2046: quote this to prevent word-splitting +export $(dbus-launch) + +# Start Wayland +weston --no-config --socket=wayland-0 --backend=headless & +export WAYLAND_DISPLAY=wayland-0 + +# Start X11 (on Wayland) +Xwayland & +export DISPLAY=:0 + +"${@:-bash}" diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh new file mode 100755 index 0000000000..c42d6c2a02 --- /dev/null +++ b/resources/docker-images/run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" + +################################ Main script ################################ + +builder_describe \ + "Run build.sh script inside of a docker image. Pass the build script and parameters after --." \ + "android" \ + "core" \ + "linux" \ + "web" \ + "--ubuntu-version=UBUNTU_VERSION The Ubuntu version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})" + +builder_parse "$@" + +run_android() { + docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \ + -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \ + keymanapp/keyman-android-ci:default \ + "${builder_extra_params[@]}" +} + +run_core() { + docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \ + -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \ + keymanapp/keyman-core-ci:default \ + "${builder_extra_params[@]}" +} + +run_linux() { + mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux" + docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \ + -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \ + -v "${KEYMAN_ROOT}/linux/build/docker-linux":/home/build/build/linux/build \ + -e DESTDIR=/tmp \ + keymanapp/keyman-linux-ci:default \ + "${builder_extra_params[@]}" +} + +run_web() { + docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \ + -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \ + keymanapp/keyman-web-ci:default \ + "${builder_extra_params[@]}" +} + +mkdir -p "${KEYMAN_ROOT}/core/build/docker-core" + +builder_run_action android run_android +builder_run_action core run_core +builder_run_action linux run_linux +builder_run_action web run_web diff --git a/resources/docker-images/web/Dockerfile b/resources/docker-images/web/Dockerfile new file mode 100644 index 0000000000..5a27c9724d --- /dev/null +++ b/resources/docker-images/web/Dockerfile @@ -0,0 +1,77 @@ +# Keyman is copyright (C) SIL Global. MIT License. +# +# ARGS used in this file: +# - ARG BASE_VERSION=default +# - ARG REQUIRED_EMSCRIPTEN_VERSION=unset + +ARG BASE_VERSION=default +FROM keymanapp/keyman-base-ci:${BASE_VERSION} + +LABEL org.opencontainers.image.authors="SIL Global." +LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" +LABEL org.opencontainers.image.title="Keyman for Web Build Image" + +USER root +RUN apt-get install -qy git jq xvfb xserver-xephyr metacity +# For playwright: +RUN apt-get install -qy libevent-2.1-7t64 libxslt1.1 libwoff1 libvpx9 \ + libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \ + libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libflite1 \ + gstreamer1.0-libav + +COPY run-tests.sh /usr/bin/run-tests.sh + +# Pre-install emscripten +USER build +ARG REQUIRED_EMSCRIPTEN_VERSION=unset +RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \ + export EMSDK_KEEP_DOWNLOADS=1 && \ + cd /home/build/ && \ + git clone https://github.com/emscripten-core/emsdk.git && \ + cd emsdk && \ + ./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \ + ./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION} +USER root +RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \ + echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper + +# Keyman Web +RUN curl --output google-chrome-stable_current_amd64.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \ + apt-get -qy install ./google-chrome-stable_current_amd64.deb && \ + rm google-chrome-stable_current_amd64.deb && \ + echo "export CHROME_BIN=/opt/google/chrome/chrome" >> /usr/bin/bashwrapper + +RUN < /etc/apt/preferences.d/mozilla +Package: * +Pin: origin packages.mozilla.org +Pin-Priority: 1000 +EOF +RUN curl https://packages.mozilla.org/apt/repo-signing-key.gpg > /etc/apt/keyrings/packages.mozilla.org.asc && \ + echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" >> /etc/apt/sources.list.d/mozilla.list && \ + apt-get update && \ + apt-get -qy install firefox && \ + echo "export FIREFOX_BIN=/usr/bin/firefox" >> /usr/bin/bashwrapper + +# Finish bashwrapper script and adjust permissions +RUN <> /usr/bin/bashwrapper + +if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then + /usr/bin/run-tests.sh "\${@:-bash}" +else + "\${@:-bash}" +fi +EOF + +# now, switch to build user +USER build + +# Pre-install node +RUN export NVM_DIR="/home/build/.nvm" && \ + . /home/build/.nvm/nvm.sh && \ + cd /home/build/emsdk/upstream/emscripten && \ + npm install + +VOLUME /home/build/build +WORKDIR /home/build/build + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/resources/docker-images/web/run-tests.sh b/resources/docker-images/web/run-tests.sh new file mode 100755 index 0000000000..84871d8b19 --- /dev/null +++ b/resources/docker-images/web/run-tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +if [[ -z "${DOCKER_RUNNING:-}" ]]; then + echo "This script is intended to be run inside a docker container." + exit 0 +fi + +set -e +echo "Starting Xvfb..." +Xvfb -screen 0 1024x768x24 :33 &> /dev/null & +sleep 1 +echo "Starting Xephyr..." +DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null & +sleep 1 +echo "Starting metacity" +metacity --display=:32 &> /dev/null & +export DISPLAY=:32 +"${@:-bash}" diff --git a/resources/shellHelperFunctions.sh b/resources/shellHelperFunctions.sh index bcd3da0d85..6f60d4484f 100755 --- a/resources/shellHelperFunctions.sh +++ b/resources/shellHelperFunctions.sh @@ -277,10 +277,14 @@ verify_npm_setup() { popd > /dev/null } +_print_expected_node_version() { +"$JQ" -r '.engines.node' "$KEYMAN_ROOT/package.json" +} + # Use nvm to select a node version according to package.json # see /docs/build/node.md _select_node_version_with_nvm() { - local REQUIRED_NODE_VERSION="$("$JQ" -r '.engines.node' "$KEYMAN_ROOT/package.json")" + local REQUIRED_NODE_VERSION="$(_print_expected_node_version)" local CURRENT_NODE_VERSION if [[ $BUILDER_OS != win ]]; then diff --git a/web/build.sh b/web/build.sh index d01d493517..e6d9a5feb8 100755 --- a/web/build.sh +++ b/web/build.sh @@ -166,7 +166,10 @@ builder_run_child_actions build:engine/attachment # Uses engine/interfaces (due to resource-path config interface) builder_run_child_actions build:engine/keyboard-storage -# Uses engine/interfaces, engine/keyboard-storage, & engine/osk +# Builds the predictive-text components +builder_run_child_actions build:engine/predictive-text + +# Uses engine/interfaces, engine/keyboard-storage, engine/predictive-text, & engine/osk builder_run_child_actions build:engine/main # Uses all but engine/element-wrappers and engine/attachment @@ -190,7 +193,7 @@ builder_run_child_actions build:test-pages builder_run_action build:_all build_action # Run tests -# builder_run_child_actions test +builder_run_child_actions test builder_run_action test:_all test_action function do_test_help() { diff --git a/web/src/engine/predictive-text/build.sh b/web/src/engine/predictive-text/build.sh new file mode 100755 index 0000000000..677d8b2269 --- /dev/null +++ b/web/src/engine/predictive-text/build.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# Compile keymanweb predictive-text components. + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# ################################ Main script ################################ + +builder_describe "Builds predictive-text components used within Keyman Engine for Web (KMW)." \ + "clean" \ + "configure" \ + "build" \ + "test" \ + ":templates Builds the model templates utilized by compiled lexical models" \ + ":wordbreakers Builds the wordbreakers provided for lexical model use" \ + ":worker-main Builds the predictive-text worker interface module" \ + ":worker-thread Builds the predictive-text worker" \ + ":_all (Meta build target used when targets are not specified)" \ + "--ci+ Set to utilize CI-based test configurations & reporting." + +# Possible TODO? +# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ + +builder_parse "$@" + +config=release +if builder_is_debug_build; then + config=debug +fi + +builder_describe_outputs \ + configure "/node_modules" \ + build:templates "/web/src/engine/predictive-text/build/obj/index.js" \ + build:wordbreakers "/web/src/engine/wordbreakers/build/main/obj/index.js" \ + build:worker-main "/web/src/engine/worker-main/build/obj/lmlayer.js" \ + build:worker-thread "/web/src/engine/worker-thread/build/obj/worker-main.wrapped.js" + +BUNDLE_CMD="node ${KEYMAN_ROOT}/web/src/tools/es-bundling/build/common-bundle.mjs" + +#### Build action definitions #### + +# We can run all clean & configure actions at once without much issue. + +builder_run_child_actions clean +builder_run_child_actions configure + +## Build actions + +builder_run_child_actions build:wordbreakers + +builder_run_child_actions build:templates +builder_run_child_actions build:worker-thread +builder_run_child_actions build:worker-main + +# If doing CI testing, the predictive-text child actions have their own build configuration. +# For local testing, though, we can allow them to proceed. +if ! builder_has_option --ci; then + builder_run_child_actions test +fi \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-main/build.sh b/web/src/engine/predictive-text/worker-main/build.sh index f7b788b6ef..f15462d71d 100755 --- a/web/src/engine/predictive-text/worker-main/build.sh +++ b/web/src/engine/predictive-text/worker-main/build.sh @@ -57,12 +57,13 @@ function do_build() { function do_test() { local TEST_OPTIONS= if builder_has_option --ci; then - TEST_OPTIONS=--ci + # We'll test the included libraries here for now. At some point, we may wish + # to establish a ci.sh script for predictive-text to handle this instead. + ./unit_tests/test.sh test:libraries test:headless test:browser --ci + else + # If we're not in --ci mode, then this doesn't need to trigger the sibling projects' tests. + ./unit_tests/test.sh test:headless test:browser fi - - # We'll test the included libraries here for now. At some point, we may wish - # to establish a ci.sh script for predictive-text to handle this instead. - ./unit_tests/test.sh test:libraries test:headless test:browser $TEST_OPTIONS } builder_run_action configure do_configure diff --git a/windows/src/desktop/kmshell/kmshell.dpr b/windows/src/desktop/kmshell/kmshell.dpr index 3726d81cd2..19e22a946c 100644 --- a/windows/src/desktop/kmshell/kmshell.dpr +++ b/windows/src/desktop/kmshell/kmshell.dpr @@ -43,7 +43,6 @@ uses InterfaceHotkeys in '..\..\global\delphi\general\InterfaceHotkeys.pas', utilsystem in '..\..\..\..\common\windows\delphi\general\utilsystem.pas', Upload_Settings in '..\..\..\..\common\windows\delphi\general\Upload_Settings.pas', - UfrmOnlineUpdateNewVersion in 'main\UfrmOnlineUpdateNewVersion.pas' {frmOnlineUpdateNewVersion}, OnlineUpdateCheck in 'main\OnlineUpdateCheck.pas', utilxml in '..\..\..\..\common\windows\delphi\general\utilxml.pas', UfrmInstallKeyboardFromWeb in 'install\UfrmInstallKeyboardFromWeb.pas' {frmInstallKeyboardFromWeb}, @@ -77,7 +76,6 @@ uses UserMessages in '..\..\..\..\common\windows\delphi\general\UserMessages.pas', UILanguages in 'util\UILanguages.pas', UfrmKeyboardOptions in 'main\UfrmKeyboardOptions.pas' {frmKeyboardOptions}, - UfrmOnlineUpdateIcon in 'main\UfrmOnlineUpdateIcon.pas' {frmOnlineUpdateIcon}, KeymanTrayIcon in '..\..\engine\keyman\KeymanTrayIcon.pas', UImportOlderVersionKeyboards10 in 'main\UImportOlderVersionKeyboards10.pas', VisualKeyboard in '..\..\..\..\common\windows\delphi\visualkeyboard\VisualKeyboard.pas', @@ -177,7 +175,15 @@ uses Keyman.Configuration.System.HttpServer.App.TextEditorFonts in 'startup\help\Keyman.Configuration.System.HttpServer.App.TextEditorFonts.pas', Keyman.Configuration.System.HttpServer.App.Locale in 'web\Keyman.Configuration.System.HttpServer.App.Locale.pas', Keyman.System.AndroidStringToKeymanLocaleString in '..\..\..\..\common\windows\delphi\general\Keyman.System.AndroidStringToKeymanLocaleString.pas', - Keyman.Configuration.System.Main in 'main\Keyman.Configuration.System.Main.pas'; + 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.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 d5d3922e2a..2bb3c24512 100644 --- a/windows/src/desktop/kmshell/kmshell.dproj +++ b/windows/src/desktop/kmshell/kmshell.dproj @@ -171,9 +171,6 @@ - -
frmOnlineUpdateNewVersion
-
@@ -224,9 +221,6 @@
frmKeyboardOptions
- -
frmOnlineUpdateIcon
-
@@ -354,6 +348,20 @@ + + + + + + + +
frmInstallNow
+ dfm +
+ +
frmStartInstall
+ dfm +
Cfg_2 @@ -415,13 +423,19 @@ False - + kmshell.rsm true - + + + kmshell.exe + true + + + kmshell.exe true @@ -1228,6 +1242,7 @@ + False 12 diff --git a/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md new file mode 100644 index 0000000000..34383de72e --- /dev/null +++ b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md @@ -0,0 +1,10 @@ +``` mermaid +stateDiagram + [*] --> Idle + Idle --> UpdateAvailable + UpdateAvailable --> Downloading + Downloading --> Installing + Downloading --> WaitingRestart + WaitingRestart --> Installing + Installing --> 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 0000000000..b7766d09bc --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm @@ -0,0 +1,50 @@ +object frmStartInstall: TfrmStartInstall + Left = 0 + Top = 0 + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'Keyman Update' + ClientHeight = 142 + ClientWidth = 322 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + PixelsPerInch = 96 + TextHeight = 13 + object lblInstallUpdate: TLabel + Left = 72 + Top = 48 + Width = 180 + 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 = 140 + Top = 104 + Width = 75 + Height = 25 + Caption = 'Install' + ModalResult = 1 + TabOrder = 0 + end + object cmdLater: TButton + Left = 234 + Top = 104 + 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 0000000000..5d3b11767a --- /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: #12887 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 0000000000..8c0e467033 --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm @@ -0,0 +1,51 @@ +object frmStartInstallNow: TfrmStartInstallNow + Left = 0 + Top = 0 + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'Keyman Update' + ClientHeight = 164 + ClientWidth = 354 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + PixelsPerInch = 96 + TextHeight = 13 + object lblUpdateMessage: TLabel + Left = 32 + Top = 48 + Width = 290 + Height = 41 + Caption = 'Your computer will be restarted if you update now.' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [] + ParentFont = False + WordWrap = True + end + object cmdInstall: TButton + Left = 147 + Top = 120 + Width = 75 + Height = 25 + Caption = 'Update now' + ModalResult = 1 + TabOrder = 0 + end + object cmdLater: TButton + Left = 247 + Top = 120 + 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 0000000000..ce3fd29da0 --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas @@ -0,0 +1,39 @@ +{ + Keyman is copyright (C) SIL Global. MIT License. + + // TODO: #12887 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; + 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 0000000000..40cc86b3b0 --- /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, + Sentry.Client, + httpuploader, + Keyman.System.KeymanSentryClient, + Keyman.System.UpdateCheckResponse, + KeymanPaths; + +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: #12888 verify filesizes match the ucr metadata so we know we don't have partial downloads. + //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; + +procedure ErrorLogMessage(ErrorLogMessage: string); +begin + KL.Log(ErrorLogMessage); + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + ErrorLogMessage); +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 ErrorLogMessage(S_OnlineUpdate_UnableToContact) + else ErrorLogMessage(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 + ErrorLogMessage('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 new file mode 100644 index 0000000000..df892929f0 --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas @@ -0,0 +1,262 @@ +(** + * 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 + +interface + +uses + System.Classes, + System.SysUtils, + KeymanPaths, + httpuploader, + Keyman.System.UpdateCheckResponse; + +const + CheckPeriod: Integer = 7; // Days between checking for updates + +type + ERemoteUpdateCheck = class(Exception); + + TRemoteUpdateCheckResult = (wucUnknown, wucSuccess, wucNoUpdates, wucFailure, wucOffline); + + TRemoteUpdateCheckDownloadParams = record + TotalSize: Integer; + TotalDownloads: Integer; + StartPosition: Integer; + end; + + TRemoteUpdateCheck = class + private + FForce: Boolean; + FRemoteResult: TRemoteUpdateCheckResult; + FErrorMessage: string; + FShowErrors: 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); + destructor Destroy; override; + function Run: TRemoteUpdateCheckResult; + property ShowErrors: Boolean read FShowErrors write FShowErrors; + end; + +(** + * 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, + Sentry.Client, + + GlobalProxySettings, + KLog, + keymanapi_TLB, + KeymanVersion, + Keyman.System.KeymanSentryClient, + Keyman.System.UpdateCheckStorage, + kmint, + ErrorControlledRegistry, + RegistryKeys, + Upload_Settings, + + OnlineUpdateCheckMessages; + +{ TRemoteUpdateCheck } + +constructor TRemoteUpdateCheck.Create(AForce: Boolean); +begin + inherited Create; + + FShowErrors := True; + FRemoteResult := wucUnknown; + + FForce := AForce; + + KL.Log('TRemoteUpdateCheck.Create'); +end; + +destructor TRemoteUpdateCheck.Destroy; +begin + if (FErrorMessage <> '') and FShowErrors then + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + '"+FErrorMessage+"'); + + KL.Log('TRemoteUpdateCheck.Destroy: FErrorMessage = ' + FErrorMessage); + KL.Log('TRemoteUpdateCheck.Destroy: FRemoteResult = ' + + IntToStr(Ord(FRemoteResult))); + + inherited Destroy; +end; + +function TRemoteUpdateCheck.Run: TRemoteUpdateCheckResult; +begin + Result := DoRun; + FRemoteResult := Result; +end; + +function TRemoteUpdateCheck.DoRun: TRemoteUpdateCheckResult; +var + flags: DWord; + i: Integer; + ucr: TUpdateCheckResponse; + pkg: IKeymanPackage; + Registry: TRegistryErrorControlled; + http: THttpUploader; + proceed: Boolean; +begin + { FProxyHost := ''; + FProxyPort := 0; } + + { Check if user is currently online } + if not InternetGetConnectedState(@flags, 0) then + begin + Result := wucOffline; + Exit; + end; + + proceed := ConfigCheckContinue; + if not proceed and not FForce then + begin + Result := wucNoUpdates; + Exit; + end; + + try + 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'); + + for i := 0 to kmcom.Packages.Count - 1 do + begin + pkg := kmcom.Packages[i]; + + // 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)); + pkg := nil; + end; + + http.Proxy.Server := GetProxySettings.Server; + http.Proxy.Port := GetProxySettings.Port; + http.Proxy.Username := GetProxySettings.Username; + http.Proxy.Password := GetProxySettings.Password; + + http.Request.HostName := API_Server; + http.Request.Protocol := API_Protocol; + http.Request.UrlPath := API_Path_UpdateCheck_Windows; + // OnStatus := + http.Upload; + if http.Response.StatusCode = 200 then + begin + if ucr.Parse(http.Response.MessageBodyAsString, 'bundle', CKeymanVersionInfo.Version) then + begin + TUpdateCheckStorage.SaveUpdateCacheData(ucr); + Result := wucSuccess; + end + else + begin + FErrorMessage := ucr.ErrorMessage; + Result := wucFailure; + end; + end + else + raise ERemoteUpdateCheck.Create('Error '+IntToStr(http.Response.StatusCode)); + finally + http.Free; + end; + except + 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]); + Result := wucFailure; + end; + on E: Exception do + begin + FErrorMessage := E.Message; + Result := wucFailure; + end; + end; + + Registry := TRegistryErrorControlled.Create; // I2890 + try + if Registry.OpenKey(SRegKey_KeymanDesktop_CU, True) then + Registry.WriteDateTime(SRegValue_LastUpdateCheckTime, Now); + finally + Registry.Free; + end; +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; + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + E.Message); + end; + end; +end; + +end. diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateCheckStorage.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateCheckStorage.pas new file mode 100644 index 0000000000..a7a822df7c --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateCheckStorage.pas @@ -0,0 +1,82 @@ +unit Keyman.System.UpdateCheckStorage; + +interface + +uses + Keyman.System.UpdateCheckResponse; + +type + TUpdateCheckStorage = class sealed + private + class function MetadataFilename: string; static; + public + class function HasUpdates: Boolean; static; + class function LoadUpdateCacheData(var data: TUpdateCheckResponse): Boolean; static; + class procedure SaveUpdateCacheData(const data: TUpdateCheckResponse); static; + class function HasKeyboardPackages(const data: TUpdateCheckResponse): Boolean; static; + class function HasKeymanInstallFile(const data: TUpdateCheckResponse): Boolean; static; + end; + +implementation + +uses + System.SysUtils, + System.RegularExpressions, + + KeymanPaths, + KeymanVersion; + +{ TUpdateCheckStorage } + +class function TUpdateCheckStorage.MetadataFilename: string; +begin + Result := TKeymanPaths.KeymanUpdateCachePath(TKeymanPaths.S_UpdateCache_Metadata); +end; + +class procedure TUpdateCheckStorage.SaveUpdateCacheData( + const data: TUpdateCheckResponse); +begin + ForceDirectories(TKeymanPaths.KeymanUpdateCachePath); + data.SaveToFile(MetadataFilename); +end; + +class function TUpdateCheckStorage.HasUpdates: Boolean; +begin + Result := FileExists(MetadataFilename); +end; + +class function TUpdateCheckStorage.LoadUpdateCacheData(var data: TUpdateCheckResponse): Boolean; +begin + Result := + HasUpdates and + data.LoadFromFile(MetadataFilename, 'bundle', CKeymanVersionInfo.Version); +end; + +class function TUpdateCheckStorage.HasKeyboardPackages(const data: TUpdateCheckResponse): Boolean; +var + i : Integer; + fileName : string; + KeyboardRegex: TRegEx; +begin + Result := False; + KeyboardRegex := TRegEx.Create('\.k..$'); + for i := 0 to High(data.Packages) do + begin + fileName := data.Packages[i].FileName; + if KeyboardRegex.IsMatch(fileName) then + Result := True; + end; +end; + +class function TUpdateCheckStorage.HasKeymanInstallFile(const data: TUpdateCheckResponse): Boolean; +var + fileExtension: string; +begin + fileExtension := LowerCase(ExtractFileExt(data.FileName)); + if fileExtension = '.exe' then + Result := True + else + Result := False +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 0000000000..80fbf08634 --- /dev/null +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas @@ -0,0 +1,1133 @@ +(* + * 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 Enter; virtual; abstract; + procedure Exit; virtual; abstract; + procedure HandleCheck; virtual; abstract; + function HandleKmShell: Integer; virtual; abstract; + procedure HandleDownload; virtual; abstract; + procedure HandleAbort; virtual; abstract; + procedure HandleInstallNow; virtual; abstract; + procedure HandleInstallPackages; virtual; + procedure HandleFirstRun; virtual; + end; + + { This class also controls the state flow see + ../BackgroundUpdateStateDiagram.md } + TUpdateStateMachine = class + private + FForce: Boolean; + FAutomaticUpdate: Boolean; + FErrorMessage: string; + FShowErrors: Boolean; + + CurrentState: TState; + // State object for performance (could lazy create?) + + FStateInstance: array [TUpdateState] of TState; + + function GetState: TStateClass; + procedure SetState(const Value: TStateClass); + procedure SetStateOnly(const enumState: TUpdateState); + function ConvertStateToEnum(const StateClass: TStateClass): TUpdateState; + function IsCurrentStateAssigned: Boolean; + procedure RemoveCachedFiles; + + function SetRegistryState(Update: TUpdateState): Boolean; + function GetAutomaticUpdates: Boolean; + function SetApplyNow(Value: Boolean): Boolean; + function GetApplyNow: Boolean; + + protected + property State: TStateClass read GetState write SetState; + + public + constructor Create(AForce: Boolean); + destructor Destroy; override; + + procedure HandleCheck; + function HandleKmShell: Integer; + procedure HandleDownload; + procedure HandleAbort; + procedure HandleInstallNow; + procedure HandleInstallPackages; + procedure HandleFirstRun; + function CurrentStateName: string; + (** + * Checks if Keyman is the WaitingRestartState and that + * Keyman has not run in this Windows session. + * The sole purpose is for the calling code then produce + * a UI to confirm the user wants to continue install. + * + * @returns True if the Keyman is ready to install. + *) + function ReadyToInstall: Boolean; + + property ShowErrors: Boolean read FShowErrors write FShowErrors; + function CheckRegistryState: TUpdateState; + + end; + +implementation + +uses + + System.Win.Registry, + Winapi.Windows, + Winapi.WinINet, + ErrorControlledRegistry, + + GlobalProxySettings, + kmint, + keymanapi_TLB, + Keyman.System.KeymanSentryClient, + Keyman.System.DownloadUpdate, + Keyman.System.RemoteUpdateCheck, + Keyman.System.UpdateCheckStorage, + KLog, + RegistryKeys, + utilexecute, + utiluac; + +const + SPackageUpgradeFilename = 'upgrade_packages.inf'; + kmShellContinue = 0; + kmShellExit = 1; + + { State Class Memebers } + +constructor TState.Create(Context: TUpdateStateMachine); +begin + inherited Create; + bucStateContext := Context; +end; + +procedure TState.ChangeState(newState: TStateClass); +begin + bucStateContext.State := newState; +end; + +type + + // Derived classes for each state + IdleState = class(TState) + public + procedure Enter; override; + procedure Exit; override; + procedure HandleCheck; override; + function HandleKmShell: Integer; override; + procedure HandleDownload; override; + procedure HandleAbort; override; + procedure HandleInstallNow; override; + end; + + UpdateAvailableState = class(TState) + private + procedure StartDownloadProcess; + public + procedure Enter; override; + procedure Exit; override; + procedure HandleCheck; override; + function HandleKmShell: Integer; override; + procedure HandleDownload; override; + procedure HandleAbort; override; + procedure HandleInstallNow; override; + end; + + DownloadingState = class(TState) + private + function DownloadUpdatesBackground: Boolean; + procedure Enter; override; + procedure Exit; override; + procedure HandleCheck; override; + function HandleKmShell: Integer; override; + procedure HandleDownload; override; + procedure HandleAbort; override; + procedure HandleInstallNow; override; + end; + + WaitingRestartState = class(TState) + public + procedure Enter; override; + procedure Exit; override; + procedure HandleCheck; override; + function HandleKmShell: Integer; override; + procedure HandleDownload; override; + procedure HandleAbort; override; + procedure HandleInstallNow; override; + end; + + InstallingState = class(TState) + private + + (** + * Installs the Keyman setup file using separate shell. + * + * @params SavePath The path to the downloaded files. + * + * @returns True if the installation is successful, False otherwise. + *) + + function DoInstallKeyman: Boolean; overload; + + (** + * Installs the Keyman Keyboard files using separate shell. + * + * @params SavePath The path to the downloaded files. + * + * @returns True if the installation is successful, False otherwise. + *) + + function DoInstallPackages(Params: TUpdateCheckResponse): Boolean; + function DoInstallPackage(PackageFileName: String): Boolean; + procedure LaunchInstallPackageProcess; + + public + procedure Enter; override; + procedure Exit; override; + procedure HandleCheck; override; + function HandleKmShell: Integer; override; + procedure HandleDownload; override; + procedure HandleAbort; override; + procedure HandleInstallNow; override; + procedure HandleInstallPackages; override; + procedure HandleFirstRun; override; + end; + + { TUpdateStateMachine } + +constructor TUpdateStateMachine.Create(AForce: Boolean); +begin + inherited Create; + FShowErrors := True; + + FForce := AForce; + FAutomaticUpdate := GetAutomaticUpdates; + + FStateInstance[usIdle] := IdleState.Create(Self); + FStateInstance[usUpdateAvailable] := UpdateAvailableState.Create(Self); + FStateInstance[usDownloading] := DownloadingState.Create(Self); + FStateInstance[usWaitingRestart] := WaitingRestartState.Create(Self); + FStateInstance[usInstalling] := InstallingState.Create(Self); + + // Check the Registry setting. + SetStateOnly(CheckRegistryState); +end; + +destructor TUpdateStateMachine.Destroy; +var + lpState: TUpdateState; +begin + if (FErrorMessage <> '') and FShowErrors then + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + '"+FErrorMessage+"'); + + for lpState := Low(TUpdateState) to High(TUpdateState) do + begin + FreeAndNil(FStateInstance[lpState]); + end; + + // TODO: #10210 TODO: epic-windows-update remove debugging comments throughout this Unit. + + inherited Destroy; +end; + +function TUpdateStateMachine.SetRegistryState(Update: TUpdateState): Boolean; +var + UpdateStr: string; + Registry: TRegistryErrorControlled; +begin + Result := False; + Registry := TRegistryErrorControlled.Create; + + try + Registry.RootKey := HKEY_CURRENT_USER; + + if not Registry.OpenKey(SRegKey_KeymanEngine_CU, True) then + begin + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Failed to open registry key: "' + SRegKey_KeymanEngine_CU + '"'); + Exit; + end; + + try + UpdateStr := GetEnumName(TypeInfo(TUpdateState), Ord(Update)); + Registry.WriteString(SRegValue_Update_State, UpdateStr); + Result := True; + except + on E: ERegistryException do + begin + TKeymanSentryClient.ReportHandledException(E, + 'Failed to write install state machine state'); + end; + end; + + finally + Registry.Free; + end; + +end; + +function TUpdateStateMachine.CheckRegistryState: TUpdateState; +var + UpdateState: TUpdateState; + Registry: TRegistryErrorControlled; + StateValue: string; + EnumValue: Integer; +begin + // Default to Idle state if any issues occur + UpdateState := usIdle; + Registry := TRegistryErrorControlled.Create; + + try + Registry.RootKey := HKEY_CURRENT_USER; + if Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) and + Registry.ValueExists(SRegValue_Update_State) then + begin + try + StateValue := Registry.ReadString(SRegValue_Update_State); + EnumValue := GetEnumValue(TypeInfo(TUpdateState), StateValue); + + // Bounds Check EnumValue against TUpdateState + if (EnumValue >= Ord(Low(TUpdateState))) and + (EnumValue <= Ord(High(TUpdateState))) then + UpdateState := TUpdateState(EnumValue) + else + UpdateState := usIdle; // Default if out of bounds + except + on E: ERegistryException do + begin + TKeymanSentryClient.ReportHandledException(E, + 'Failed to read install state machine state'); + UpdateState := usIdle; + end; + end; + end; + finally + Registry.Free; + end; + + Result := UpdateState; +end; + +function TUpdateStateMachine.GetAutomaticUpdates: Boolean; // I2329 +var + Registry: TRegistryErrorControlled; + +begin + // check the registry value + Registry := TRegistryErrorControlled.Create; // I2890 + try + Registry.RootKey := HKEY_CURRENT_USER; + try + Result := not Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) or + not Registry.ValueExists(SRegValue_AutomaticUpdates) or + Registry.ReadBool(SRegValue_AutomaticUpdates); + except + on E: ERegistryException do + begin + TKeymanSentryClient.ReportHandledException(E, + 'Failed to read automatic updates'); + Result := False; + end; + end; + finally + Registry.Free; + end; +end; + +function TUpdateStateMachine.SetApplyNow(Value: Boolean): Boolean; +var + Registry: TRegistryErrorControlled; +begin + Result := False; + Registry := TRegistryErrorControlled.Create; + + try + Registry.RootKey := HKEY_CURRENT_USER; + if not Registry.OpenKey(SRegKey_KeymanEngine_CU, True) then + begin + Exit; + end; + try + Registry.WriteBool(SRegValue_ApplyNow, Value); + Result := True; + except + on E: ERegistryException do + begin + TKeymanSentryClient.ReportHandledException(E, + 'Failed to write "apply now"'); + end; + end; + finally + Registry.Free; + end; +end; + +function TUpdateStateMachine.GetApplyNow: Boolean; +var + Registry: TRegistryErrorControlled; +begin + // check the registry value + Registry := TRegistryErrorControlled.Create; + try + Registry.RootKey := HKEY_CURRENT_USER; + try + Result := Registry.OpenKeyReadOnly(SRegKey_KeymanEngine_CU) and + Registry.ValueExists(SRegValue_ApplyNow) and + Registry.ReadBool(SRegValue_ApplyNow); + except + on E: ERegistryException do + begin + 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.Exit; + end; + + SetStateOnly(ConvertStateToEnum(Value)); + + if Assigned(CurrentState) then + begin + CurrentState.Enter; + 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.ReadyToInstall: Boolean; +begin + if not IsCurrentStateAssigned then + Exit(False); + if (CurrentState.ClassName = 'WaitingRestartState') and not HasKeymanRun then + Result := True + else + Result := False; +end; + +// base implmentation to be overiden + +procedure TState.HandleInstallPackages; +begin + // Do Nothing +end; + +procedure TState.HandleFirstRun; +begin + // If Handle First run hits base implementation + // something is wrong. + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Handle first run called in state:"' + Self.ClassName + '"'); + bucStateContext.RemoveCachedFiles; + ChangeState(IdleState); +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 + + 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 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 = wucSuccess 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; + +{ UpdateAvailableState } + +procedure UpdateAvailableState.StartDownloadProcess; +var + FResult: Boolean; + RootPath: string; +begin + // call separate process + RootPath := ExtractFilePath(ParamStr(0)); + FResult := TUtilExecute.ShellCurrentUser(0, ParamStr(0), + IncludeTrailingPathDelimiter(RootPath), '-bd'); + if not FResult then + begin + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Executing kmshell process to download updated Failed'); + ChangeState(IdleState); + end; +end; + +procedure UpdateAvailableState.Enter; +begin + // Enter UpdateAvailableState + bucStateContext.SetRegistryState(usUpdateAvailable); + if bucStateContext.FAutomaticUpdate then + begin + StartDownloadProcess; + end; +end; + +procedure UpdateAvailableState.Exit; +begin + // Exit UpdateAvailableState +end; + +procedure UpdateAvailableState.HandleCheck; +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 <> wucSuccess 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 + +end; + +procedure UpdateAvailableState.HandleInstallNow; +begin + bucStateContext.SetApplyNow(True); + ChangeState(DownloadingState); +end; + +{ DownloadingState } + +procedure DownloadingState.Enter; +var + DownloadResult: Boolean; + RetryCount: Integer; +begin + // Enter DownloadingState + bucStateContext.SetRegistryState(usDownloading); + + RetryCount := 0; + DownloadResult := False; + + while (not DownloadResult) and (RetryCount < 3) do + begin + DownloadResult := DownloadUpdatesBackground; + if not DownloadResult then + Inc(RetryCount); + end; + + if (not DownloadResult) then + begin + // Failed three times in this process; return to the + // IdleState to wait 'CheckPeriod' before trying again + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Error Updates not downloaded after 3 attempts'); + ChangeState(IdleState); + end + else + begin + if HasKeymanRun then + begin + if bucStateContext.GetApplyNow then + begin + bucStateContext.SetApplyNow(False); + ChangeState(InstallingState); + end + else + ChangeState(WaitingRestartState); + end + else + begin + ChangeState(InstallingState); + end; + end + +end; + +procedure DownloadingState.Exit; +begin + // Exit DownloadingState +end; + +procedure DownloadingState.HandleCheck; +begin + +end; + +function DownloadingState.HandleKmShell; +begin + // Downloading state, in other process, so continue + Result := kmShellContinue; +end; + +procedure DownloadingState.HandleDownload; +begin + // Enter Already Downloading +end; + +procedure DownloadingState.HandleAbort; +begin + // To abort during the downloading +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; +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 } + if Result = wucSuccess then + begin + ChangeState(UpdateAvailableState); + end; +end; + +function WaitingRestartState.HandleKmShell; +var + ucr: TUpdateCheckResponse; + hasPackages, hasKeymanInstall: Boolean; +begin + // Still can't go if keyman has run + if HasKeymanRun then + begin + Result := kmShellContinue; + end + else + begin + hasPackages := False; + hasKeymanInstall := False; + if (TUpdateCheckStorage.LoadUpdateCacheData(ucr)) then + begin + hasPackages := TUpdateCheckStorage.HasKeyboardPackages(ucr); + hasKeymanInstall := TUpdateCheckStorage.HasKeymanInstallFile(ucr); + end; + if not (hasPackages Or hasKeymanInstall) then + begin + // Return to Idle state and check for Updates state + ChangeState(IdleState); + bucStateContext.CurrentState.HandleCheck; + Result := kmShellExit; + end + else + begin + ChangeState(InstallingState); + Result := kmShellExit; + end; + end; +end; + +procedure WaitingRestartState.HandleDownload; +begin + +end; + +procedure WaitingRestartState.HandleAbort; +begin + ChangeState(UpdateAvailableState); +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.Client.MessageEvent + (Sentry.Client.SENTRY_LEVEL_ERROR, + 'Executing kmshell process to install keyboard packages failed:"' + + 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; + fileExt: String; + FileName: String; + FileNames: TStringDynArray; + found: Boolean; +begin + + SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); + GetFileNamesInDirectory(SavePath, FileNames); + found := False; + for FileName in FileNames do + begin + fileExt := LowerCase(ExtractFileExt(FileName)); + if fileExt = '.exe' then + begin + found := True; + break; + end; + end; + + // switch -au for auto update in silent mode. + // We will need to add the pop up that says install update now yes/no + // This will run the setup executable which will ask for elevated permissions + if found then + FResult := TUtilExecute.Shell(0, SavePath + ExtractFileName(FileName), + '', '-au') + else + FResult := False; + + if not FResult then + begin + bucStateContext.RemoveCachedFiles; + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Executing kmshell process to install failed:"' + + IntToStr(Ord(FResult)) + '"'); + ChangeState(IdleState); + end; + + Result := FResult; +end; + +function InstallingState.DoInstallPackage(PackageFileName: String): Boolean; +var + FPackage: IKeymanPackageFile2; +begin + Result := True; + FPackage := kmcom.Packages.GetPackageFromFile(PackageFileName) + as IKeymanPackageFile2; + FPackage.Install2(True); + // Force overwrites existing package and leaves most settings for it intact + FPackage := nil; + + kmcom.Refresh; + kmcom.Apply; + + System.SysUtils.DeleteFile(PackageFileName); + +end; + +function InstallingState.DoInstallPackages + (Params: TUpdateCheckResponse): Boolean; +var + i: Integer; + SavePath: String; + PackageFullPath: String; +begin + SavePath := IncludeTrailingPathDelimiter(TKeymanPaths.KeymanUpdateCachePath); + for i := 0 to High(Params.Packages) do + begin + PackageFullPath := SavePath + Params.Packages[i].FileName; + if not DoInstallPackage(PackageFullPath) then // I2742 + begin + KL.Log('Installing Package failed' + PackageFullPath); + end; + end; + Result := True; +end; + +procedure InstallingState.Enter; +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.HasKeymanInstallFile(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.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 + // 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.HasKeymanInstallFile(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 + DoInstallKeyman; +end; + +procedure InstallingState.HandleFirstRun; +begin + bucStateContext.RemoveCachedFiles; + ChangeState(IdleState); +end; + +end. diff --git a/windows/src/desktop/kmshell/main/OnlineUpdateCheck.pas b/windows/src/desktop/kmshell/main/OnlineUpdateCheck.pas index 15fb06f4e9..ba5d835a17 100644 --- a/windows/src/desktop/kmshell/main/OnlineUpdateCheck.pas +++ b/windows/src/desktop/kmshell/main/OnlineUpdateCheck.pas @@ -98,11 +98,11 @@ type FShowErrors: Boolean; FDownload: TOnlineUpdateCheckDownloadParams; + FCheckOnly: Boolean; function DownloadUpdates: Boolean; procedure DoDownloadUpdates(AOwner: TfrmDownloadProgress; var Result: Boolean); function DoRun: TOnlineUpdateCheckResult; - procedure ShowUpdateForm; procedure ShutDown; procedure DownloadUpdatesHTTPStatus(Sender: THTTPUploader; const Message: string; Position, Total: Int64); // I2855 @@ -112,7 +112,9 @@ type public public - constructor Create(AOwner: TCustomForm; AForce, ASilent: Boolean); + function ResponseToParams(const ucr: TUpdateCheckResponse): TOnlineUpdateCheckParams; + + constructor Create(AOwner: TCustomForm; AForce, ASilent: Boolean; ACheckOnly: Boolean = False); destructor Destroy; override; function Run: TOnlineUpdateCheckResult; property ShowErrors: Boolean read FShowErrors write FShowErrors; @@ -146,6 +148,7 @@ uses KLog, keymanapi_TLB, KeymanVersion, + Keyman.System.UpdateCheckStorage, kmint, ErrorControlledRegistry, RegistryKeys, @@ -153,8 +156,6 @@ uses utildir, utilexecute, OnlineUpdateCheckMessages, - UfrmOnlineUpdateIcon, - UfrmOnlineUpdateNewVersion, utilkmshell, utilsystem, utiluac, @@ -165,7 +166,7 @@ const { TOnlineUpdateCheck } -constructor TOnlineUpdateCheck.Create(AOwner: TCustomForm; AForce, ASilent: Boolean); +constructor TOnlineUpdateCheck.Create(AOwner: TCustomForm; AForce, ASilent: Boolean; ACheckOnly: Boolean); begin inherited Create; @@ -176,6 +177,7 @@ begin FSilent := ASilent; FForce := AForce; + FCheckOnly := ACheckOnly; KL.Log('TOnlineUpdateCheck.Create'); end; @@ -388,86 +390,6 @@ begin end; end; -procedure TOnlineUpdateCheck.ShowUpdateForm; -var - i: Integer; - FRequiresAdmin: Boolean; - FOwnerHandle: THandle; -begin - if Assigned(FOwner) - then FOwnerHandle := FOwner.Handle - else FOwnerHandle := Application.Handle; - - { We have an update available } - with OnlineUpdateNewVersion(FOwner) do - try - Params := Self.FParams; - if ShowModal <> mrYes then - begin - Self.FParams.Result := oucUnknown; - Self.FErrorMessage := ''; - Exit; - end; - - Self.FParams := Params; - finally - Free; - end; - - if not DownloadUpdates then - begin - Self.FParams.Result := oucUnknown; // I2742 - Exit; - end - else - begin - if not kmcom.SystemInfo.IsAdministrator then - begin - FRequiresAdmin := FParams.Keyman.Install; - for i := 0 to High(FParams.Packages) do - if FParams.Packages[i].Install then - begin - FRequiresAdmin := True; - Break; - end; - end - else - FRequiresAdmin := False; - - if FRequiresAdmin then - begin - if CanElevate then - begin - SavePackageUpgradesToDownloadTempPath; - if WaitForElevatedConfiguration(FOwnerHandle, '-ou "'+DownloadTempPath+'"', not FParams.Keyman.Install) <> 0 then // I2513 - FParams.Result := oucFailure - else if FParams.Keyman.Install then - FParams.Result := oucShutDown - else - FParams.Result := oucSuccess; - end - else - begin - ShowMessage('Some of these updates require an Administrator to complete installation. Please login as an Administrator and re-run the update.'); - FParams.Result := oucFailure; - end; - end - else - begin - FParams.Result := oucSuccess; - for i := 0 to High(FParams.Packages) do - if FParams.Packages[i].Install then - if not DoInstallPackage(FParams.Packages[i]) then FParams.Result := oucFailure; - - if FParams.Keyman.Install then - begin - DoInstallKeyman; - FParams.Result := oucShutDown; - end; - end; - end; -end; - procedure TOnlineUpdateCheck.ShutDown; begin if Assigned(Application) then @@ -477,10 +399,9 @@ end; function TOnlineUpdateCheck.DoRun: TOnlineUpdateCheckResult; var flags: DWord; - i, n: Integer; - pkg: IKeymanPackage; - j: Integer; + i: Integer; ucr: TUpdateCheckResponse; + pkg: IKeymanPackage; begin {FProxyHost := ''; FProxyPort := 0;} @@ -567,54 +488,9 @@ begin begin if ucr.Parse(Response.MessageBodyAsString, 'bundle', CKeymanVersionInfo.Version) then begin - SetLength(FParams.Packages,0); - for i := Low(ucr.Packages) to High(ucr.Packages) do - begin - n := kmcom.Packages.IndexOf(ucr.Packages[i].ID); - if n >= 0 then - begin - pkg := kmcom.Packages[n]; - j := Length(FParams.Packages); - SetLength(FParams.Packages, j+1); - FParams.Packages[j].NewID := ucr.Packages[i].NewID; - FParams.Packages[j].ID := ucr.Packages[i].ID; - FParams.Packages[j].Description := ucr.Packages[i].Name; - FParams.Packages[j].OldVersion := pkg.Version; - FParams.Packages[j].NewVersion := ucr.Packages[i].NewVersion; - FParams.Packages[j].DownloadSize := ucr.Packages[i].DownloadSize; - FParams.Packages[j].DownloadURL := ucr.Packages[i].DownloadURL; - FParams.Packages[j].FileName := ucr.Packages[i].FileName; - pkg := nil; - end - else - FErrorMessage := 'Unable to find package '+ucr.Packages[i].ID; - end; - - case ucr.Status of - ucrsNoUpdate: - begin - FErrorMessage := ucr.ErrorMessage; - end; - ucrsUpdateReady: - begin - FParams.Keyman.OldVersion := ucr.CurrentVersion; - FParams.Keyman.NewVersion := ucr.NewVersion; - FParams.Keyman.DownloadURL := ucr.InstallURL; - FParams.Keyman.DownloadSize := ucr.InstallSize; - FParams.Keyman.FileName := ucr.FileName; - end; - end; - - if (Length(FParams.Packages) > 0) or (FParams.Keyman.DownloadURL <> '') then - begin - if not FSilent then - ShowUpdateForm - else - begin - ShowUpdateIcon; - end; - Result := FParams.Result; - end; + ResponseToParams(ucr); + TUpdateCheckStorage.SaveUpdateCacheData(ucr); + Result := FParams.Result; end else begin @@ -651,6 +527,52 @@ begin end; end; +function TOnlineUpdateCheck.ResponseToParams(const ucr: TUpdateCheckResponse): TOnlineUpdateCheckParams; +var + i, j, n: Integer; + pkg: IKeymanPackage; +begin + SetLength(FParams.Packages,0); + for i := Low(ucr.Packages) to High(ucr.Packages) do + begin + n := kmcom.Packages.IndexOf(ucr.Packages[i].ID); + if n >= 0 then + begin + pkg := kmcom.Packages[n]; + j := Length(FParams.Packages); + SetLength(FParams.Packages, j+1); + FParams.Packages[j].NewID := ucr.Packages[i].NewID; + FParams.Packages[j].ID := ucr.Packages[i].ID; + FParams.Packages[j].Description := ucr.Packages[i].Name; + FParams.Packages[j].OldVersion := pkg.Version; + FParams.Packages[j].NewVersion := ucr.Packages[i].NewVersion; + FParams.Packages[j].DownloadSize := ucr.Packages[i].DownloadSize; + FParams.Packages[j].DownloadURL := ucr.Packages[i].DownloadURL; + FParams.Packages[j].FileName := ucr.Packages[i].FileName; + pkg := nil; + end + else + FErrorMessage := 'Unable to find package '+ucr.Packages[i].ID; + end; + + case ucr.Status of + ucrsNoUpdate: + begin + FErrorMessage := ucr.ErrorMessage; + end; + ucrsUpdateReady: + begin + FParams.Keyman.OldVersion := ucr.CurrentVersion; + FParams.Keyman.NewVersion := ucr.NewVersion; + FParams.Keyman.DownloadURL := ucr.InstallURL; + FParams.Keyman.DownloadSize := ucr.InstallSize; + FParams.Keyman.FileName := ucr.FileName; + end; + end; + + Result := FParams; +end; + procedure OnlineUpdateAdmin(OwnerForm: TCustomForm; Path: string); var Package: TOnlineUpdateCheckParamsPackage; diff --git a/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas b/windows/src/desktop/kmshell/main/UImportOlderVersionSettings.pas index 007d854797..48a8293f50 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 @@ unit UImportOlderVersionSettings; 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 @@ -41,14 +41,24 @@ uses keymanapi_TLB, ErrorControlledRegistry, RegistryKeys, + Keyman.System.UpdateStateMachine, 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; p: string; + UpdateSM : TUpdateStateMachine; begin + // send event to statemachine (should result in setting state to idle) + UpdateSM := TUpdateStateMachine.Create(False); + try + UpdateSM.HandleFirstRun; + finally + UpdateSM.Free; + end; + { Copy over all the user settings and set defaults for version 8.0: http://blog.tavultesoft.com/2011/02/keyman-desktop-80-default-options.html } if DoDefaults then // I2753 @@ -136,6 +146,7 @@ begin 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 d9a6f8b441..86df47ec70 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -85,6 +85,7 @@ uses Winapi.Windows, keymanapi_TLB, + Sentry.Client, XMLRenderer, KeyboardListXMLRenderer, UfrmKeymanBase, @@ -141,13 +142,14 @@ type procedure Support_Diagnostics; procedure Support_Online; - procedure Support_UpdateCheck; procedure Support_ProxyConfig; procedure Support_ContactSupport(params: TStringList); // I4390 procedure OpenSite(params: TStringList); procedure DoApply; procedure DoRefresh; + procedure Update_CheckNow; + procedure Update_ApplyNow; protected procedure FireCommand(const command: WideString; params: TStringList); override; @@ -184,12 +186,15 @@ uses LanguagesXMLRenderer, MessageIdentifierConsts, MessageIdentifiers, - OnlineUpdateCheck, + Keyman.System.ExecutionHistory, + Keyman.System.KeymanSentryClient, + Keyman.System.RemoteUpdateCheck, OptionsXMLRenderer, Keyman.Configuration.System.UmodWebHttpServer, Keyman.Configuration.System.HttpServer.App.ConfigMain, Keyman.Configuration.UI.InstallFile, Keyman.Configuration.UI.UfrmSettingsManager, + Keyman.Configuration.UI.UfrmStartInstallNow, RegistryKeys, SupportXMLRenderer, UfrmChangeHotkey, @@ -202,12 +207,14 @@ uses UfrmTextEditor, uninstall, Upload_Settings, + UpdateXMLRenderer, utildir, utilexecute, utilkmshell, utilhttp, utiluac, - utilxml; + utilxml, + KeymanPaths; type PHKL = ^HKL; @@ -301,6 +308,7 @@ begin FXMLRenderers.Add(TOptionsXMLRenderer.Create(FXMLRenderers)); FXMLRenderers.Add(TLanguagesXMLRenderer.Create(FXMLRenderers)); FXMLRenderers.Add(TSupportXMLRenderer.Create(FXMLRenderers)); + FXMLRenderers.Add(TUpdateXMLRenderer.Create(FXMLRenderers)); xml := FXMLRenderers.RenderToString(s); sharedData.Init( @@ -342,9 +350,11 @@ begin else if command = 'support_diagnostics' then Support_Diagnostics else if command = 'support_online' then Support_Online - else if command = 'support_updatecheck' then Support_UpdateCheck 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 else if command = 'opensite' then OpenSite(params) @@ -789,29 +799,49 @@ begin end; end; -procedure TfrmMain.Support_UpdateCheck; +procedure TfrmMain.Update_CheckNow; +// TODO: epic-windows-update +// Get an instance to the state machine and call handle check so the state can change to update +// available. +var UpdateCheck : TRemoteUpdateCheck; begin - with TOnlineUpdateCheck.Create(Self, True, False) do + UpdateCheck := TRemoteUpdateCheck.Create(True); try - case Run of - oucShutDown: - begin - try - if kmcom.Control.IsKeymanRunning then - try - kmcom.Control.StopKeyman; - except - on E:Exception do KL.Log(E.Message); - end; - except - on E:Exception do KL.Log(E.Message); - end; - end; - oucSuccess: - DoRefresh; - end + UpdateCheck.Run; finally - Free; + UpdateCheck.Free; + end; + DoRefresh; +end; + +procedure TfrmMain.Update_ApplyNow; +var + ShellPath : string; + FResult, InstallNow: Boolean; + frmStartInstallNow: TfrmStartInstallNow; +begin + InstallNow := True; + // Confirm User is ok that this will require a reset + 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 + ShellPath := TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell); + FResult := TUtilExecute.Shell(0, ShellPath, '', '-an'); + if not FResult then + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'TrmfMain: Shell Execute Update_ApplyNow Failed'); end; end; diff --git a/windows/src/desktop/kmshell/main/initprog.pas b/windows/src/desktop/kmshell/main/initprog.pas index c22591e79c..651387bc47 100644 --- a/windows/src/desktop/kmshell/main/initprog.pas +++ b/windows/src/desktop/kmshell/main/initprog.pas @@ -80,8 +80,12 @@ type fmUninstallPackage, fmRegistryAdd, fmRegistryRemove, fmMain, fmHelp, fmHelpKMShell, fmMigrate, fmSplash, fmStart, - fmUpgradeKeyboards, fmOnlineUpdateCheck,// I2548 - fmOnlineUpdateAdmin, fmTextEditor, + fmUpgradeKeyboards, // I2548 + fmTextEditor, + fmInstallKeyboardPackageAdmin, + fmBackgroundUpdateCheck, + fmBackgroundDownload, + fmApplyInstallNow, fmFirstRun, // I2562 fmKeyboardWelcome, // I2569 fmKeyboardPrint, // I2329 @@ -113,13 +117,14 @@ uses Keyman.Configuration.System.TIPMaintenance, Keyman.Configuration.System.UImportOlderVersionKeyboards11To13, Keyman.Configuration.UI.UfrmSettingsManager, + Keyman.Configuration.UI.UfrmStartInstall, Keyman.System.KeymanStartTask, KeymanPaths, KLog, kmint, KMShellHints, KeymanMutex, - OnlineUpdateCheck, + Keyman.System.RemoteUpdateCheck, RegistryKeys, UfrmBaseKeyboard, UfrmKeymanBase, @@ -141,6 +146,7 @@ uses UpgradeMnemonicLayout, utilfocusappwnd, utilkmshell, + Keyman.System.UpdateStateMachine, KeyboardTIPCheck, @@ -240,7 +246,7 @@ begin else if s = '-uk' then FMode := fmUninstallKeyboard { I1201 - Fix crash uninstalling admin-installed keyboards and packages } else if s = '-ukl' then FMode := fmUninstallKeyboardLanguage // I3624 else if s = '-up' then FMode := fmUninstallPackage { I1201 - Fix crash uninstalling admin-installed keyboards and packages } - else if s = '-ou' then FMode := fmOnlineUpdateAdmin { I1730 - Check update of keyboards (admin elevation) } + else if s = '-ikp' then FMode := fmInstallKeyboardPackageAdmin else if s = '-a' then FMode := fmAbout else if s = '-ra' then FMode := fmRegistryAdd else if s = '-rr' then FMode := fmRegistryRemove @@ -248,7 +254,9 @@ begin else if s = '-?' then FMode := fmHelpKMShell else if s = '-h' then FMode := fmHelp else if s = '-t' then FMode := fmTextEditor - 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 @@ -381,6 +389,9 @@ var kdl: IKeymanDefaultLanguage; FIcon: string; FMutex: TKeymanMutex; // I2720 + BUpdateSM : TUpdateStateMachine; + frmStartInstall: TfrmStartInstall; + UserCanceled : Boolean; function FirstKeyboardFileName: WideString; begin if KeyboardFileNames.Count = 0 @@ -428,6 +439,55 @@ begin Exit; end; + BUpdateSM := TUpdateStateMachine.Create(False); + try + if (FMode = fmBackgroundUpdateCheck) then + begin + 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 if (FMode = fmInstallKeyboardPackageAdmin) then + begin + BUpdateSM.HandleInstallPackages; + Exit; + end + else + begin + // The following logic around the WaitingRestartState should be + // encapsulated in the state machine however as we want separation of + // UI elements from the state machine we have bring some of logic here. + UserCanceled := False; + if BUpdateSM.ReadyToInstall and + (not FSilent and (FMode in [fmStart, fmSplash, fmMain, fmAbout])) then + begin + frmStartInstall := TfrmStartInstall.Create(nil); + try + if frmStartInstall.ShowModal = mrOk then + UserCanceled := False + else + UserCanceled := True + finally + frmStartInstall.Free; + end; + end; + if not UserCanceled and (BUpdateSM.HandleKmShell = 1) then + Exit; + end; + finally + BUpdateSM.Free; + end; + + if not FSilent or (FMode = fmUpgradeMnemonicLayout) then // I4553 begin // Note: will elevate and re-run if required @@ -469,17 +529,6 @@ begin then ExitCode := 0 else ExitCode := 2; - fmOnlineUpdateAdmin: - OnlineUpdateAdmin(nil, FirstKeyboardFileName); - - fmOnlineUpdateCheck: - with TOnlineUpdateCheck.Create(nil, FForce, FSilent) do - try - Run; - finally - Free; - end; - fmUpgradeKeyboards:// I2548 begin if FQuery='13,backup' then @@ -619,6 +668,7 @@ begin 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/render/UpdateXMLRenderer.pas b/windows/src/desktop/kmshell/render/UpdateXMLRenderer.pas new file mode 100644 index 0000000000..c2f7a2bfc2 --- /dev/null +++ b/windows/src/desktop/kmshell/render/UpdateXMLRenderer.pas @@ -0,0 +1,94 @@ +(* + Name: UpdateXMLRenderer + Copyright: Copyright (C) SIL International. +*) +unit UpdateXMLRenderer; + +interface + +uses + XMLRenderer, + Windows; + +type + TUpdateXMLRenderer = class(TXMLRenderer) + protected + function XMLData: WideString; override; + end; + +implementation + +uses + StrUtils, + SysUtils, + VersionInfo, + kmint, + KeymanVersion, + keymanapi_TLB, + Keyman.System.LocaleStrings, + Keyman.System.UpdateCheckResponse, + Keyman.System.UpdateCheckStorage, + MessageIdentifierConsts, + MessageIdentifiers, + utilxml; + +{ TUpdateXMLRenderer } + +function TUpdateXMLRenderer.XMLData: WideString; +var + xml: string; + ucr: TUpdateCheckResponse; + i, n : Integer; + pkg: IKeymanPackage; +begin + xml := ''; + + if TUpdateCheckStorage.LoadUpdateCacheData(ucr) then + begin + if (ucr.InstallURL <> '') then + begin + xml := xml + + ''+ + '0'+ + IfThen(not kmcom.SystemInfo.IsAdministrator, '')+ + ''+ + ''+xmlencode(TLocaleStrings.MsgFromIdFormat(kmcom, SKUpdate_KeymanText, [ucr.NewVersion]))+''+ + ''+xmlencode(ucr.NewVersion)+''+ + ''+xmlencode(ucr.CurrentVersion)+''+ + ''+xmlencode(Format('%d', [ucr.InstallSize div 1024]))+'KB'+ + ''+xmlencode(ucr.InstallURL)+''+ + ''+ + ''; + end; + + for i := 0 to High(ucr.Packages) do + begin + n := kmcom.Packages.IndexOf(ucr.Packages[i].ID); + if n >= 0 then + begin + pkg := kmcom.Packages[n]; + + xml := xml + + ''+ + ''+IntToStr(i+1)+''+ + IfThen(not kmcom.SystemInfo.IsAdministrator, '')+ + ''+ + ''+xmlencode(TLocaleStrings.MsgFromIdFormat(kmcom, SKUpdate_PackageText, + [ucr.Packages[i].Name, ucr.Packages[i].NewVersion]))+''+ + ''+xmlencode(ucr.Packages[i].NewVersion)+''+ + ''+xmlencode(pkg.version)+''+ + ''+xmlencode(Format('%d', [ucr.Packages[i].DownloadSize div 1024]))+'KB'+ + ''+xmlencode(ucr.Packages[i].DownloadURL)+''+ + ''+ + ''; + pkg := nil; + end; + // else Package not found, skip + end; + end; + + Result := ''+xml+''; +end; + +end. + diff --git a/windows/src/desktop/kmshell/startup/UfrmSplash.pas b/windows/src/desktop/kmshell/startup/UfrmSplash.pas index 1be49479f1..5a6cccd529 100644 --- a/windows/src/desktop/kmshell/startup/UfrmSplash.pas +++ b/windows/src/desktop/kmshell/startup/UfrmSplash.pas @@ -90,7 +90,6 @@ uses MessageIdentifierConsts, MessageIdentifiers, KeymanMutex, - OnlineUpdateCheck, PngImage, ErrorControlledRegistry, RegistryKeys, @@ -268,8 +267,7 @@ begin if kmcom.Options[KeymanOptionName(TUtilKeymanOption.koCheckForUpdates)].Value then begin - if not kmcom.Control.IsOnlineUpdateCheckOpen then - RunConfiguration(0, '-ouc -s'); + RunConfiguration(0, '-buc -s'); end; end; end; diff --git a/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas b/windows/src/desktop/kmshell/util/UfrmDownloadProgress.pas index 2edcfbbcf7..9b5f779c8f 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 7485753283..76eb4929dd 100644 --- a/windows/src/desktop/kmshell/util/utilkmshell.pas +++ b/windows/src/desktop/kmshell/util/utilkmshell.pas @@ -33,7 +33,7 @@ unit utilkmshell; // I3306 // I4181 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 @@ begin 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/config.css b/windows/src/desktop/kmshell/xml/config.css index ac9ddcee0c..e452cca5b8 100644 --- a/windows/src/desktop/kmshell/xml/config.css +++ b/windows/src/desktop/kmshell/xml/config.css @@ -499,6 +499,20 @@ table tr padding: 1px 0 1px 10px; } +.grid_container_update { + display: grid; + position: relative; + grid-template-columns: 50px 1fr 1fr 1fr; + margin: 10px 50px 10px 64px; + /* From https://geary.co/internal-borders-css-grid/ */ + overflow: hidden; + gap: var(--gap); + --gap: 2em; + --line-offset: calc(var(--gap) / 2); + --line-thickness: 1px; + --line-color: black; +} + .grid_container.grid_disabled { opacity: 0.5; @@ -508,11 +522,36 @@ table tr { position: relative; font-size: 12px; - margin: 0px; padding: 1px; margin: 0 0 0 5px; } +.grid_item::before, +.grid_item::after +{ + content: ''; + position: absolute; + background-color: var(--line-color); + z-index: 1; +} + +/* row borders */ +.grid_item::after +{ + inline-size: 100vw; + block-size: var(--line-thickness); + inset-inline-start: 0; + inset-block-start: calc(var(--line-offset) * -1); +} + +/* column borders */ +.grid_item::before +{ + inline-size: var(--line-thickness); + block-size: 100vh; + inset-inline-start: calc(var(--line-offset) * -1); +} + .grid_item_title{ font-weight: bold; } @@ -1099,17 +1138,42 @@ th height: 16px; } -#keepintouch_content { - height: 100%; - overflow: hidden; +.update_title +{ + background: url('keyman-title.png') 96px 13px no-repeat; + height: 60px; } -#keepintouch_frame { - box-sizing: border-box; - width:100%; - height:100%; - border:none; - user-select: none; +.update_title img +{ + margin: 13px 0 0 10px; +} + +.update_edition +{ + float: left; + font-size: 16px; + margin-left: 64px; +} + +#update_status +{ + font-size: 13px; + margin-left: 64px; + margin-top: 30px; +} + +#update_content { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 5px 10px 5px 10px; +} + +.update_controls +{ + margin-left: 64px; } /* QRCodes */ diff --git a/windows/src/desktop/kmshell/xml/config.js b/windows/src/desktop/kmshell/xml/config.js index cf761ce6c2..47b66ca89c 100644 --- a/windows/src/desktop/kmshell/xml/config.js +++ b/windows/src/desktop/kmshell/xml/config.js @@ -35,7 +35,7 @@ function windowResize() e = _$('subcontent_pro'); if(e) e.style.height = h; _$('subcontent_support').style.height = h; - _$('subcontent_keepintouch').style.height = h; + _$('subcontent_update').style.height = h; } } diff --git a/windows/src/desktop/kmshell/xml/keyman.xsl b/windows/src/desktop/kmshell/xml/keyman.xsl index 94c7ed7e95..a774e0aa9c 100644 --- a/windows/src/desktop/kmshell/xml/keyman.xsl +++ b/windows/src/desktop/kmshell/xml/keyman.xsl @@ -12,7 +12,7 @@ - + @@ -50,7 +50,7 @@
-
+
diff --git a/windows/src/desktop/kmshell/xml/keyman_keepintouch.xsl b/windows/src/desktop/kmshell/xml/keyman_keepintouch.xsl deleted file mode 100644 index 780166619f..0000000000 --- a/windows/src/desktop/kmshell/xml/keyman_keepintouch.xsl +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - -
- - -
- -
-
- -
-
-
-
\ No newline at end of file diff --git a/windows/src/desktop/kmshell/xml/keyman_menu.xsl b/windows/src/desktop/kmshell/xml/keyman_menu.xsl index 4fc1b0f225..99d5e05fac 100644 --- a/windows/src/desktop/kmshell/xml/keyman_menu.xsl +++ b/windows/src/desktop/kmshell/xml/keyman_menu.xsl @@ -9,20 +9,20 @@ - + - + diff --git a/windows/src/desktop/kmshell/xml/keyman_support.xsl b/windows/src/desktop/kmshell/xml/keyman_support.xsl index 4bf93f89bd..b36a30a0d8 100644 --- a/windows/src/desktop/kmshell/xml/keyman_support.xsl +++ b/windows/src/desktop/kmshell/xml/keyman_support.xsl @@ -49,7 +49,6 @@
  • keyman:link?url=/keyman.com
  • -
  • keyman:link?url=/go//support
  • diff --git a/windows/src/desktop/kmshell/xml/keyman_update.xsl b/windows/src/desktop/kmshell/xml/keyman_update.xsl new file mode 100644 index 0000000000..0daa6ace3b --- /dev/null +++ b/windows/src/desktop/kmshell/xml/keyman_update.xsl @@ -0,0 +1,136 @@ + + + + + + + +
    + + +
    + +
    +
    + +
    + +
    + +
    +   +
    + +
    + + Updates are available which will be applied when Windows is next restarted: + + + No updates are available. + +
    + +
    +
    Select
    +
    +
    +
    + + + +
    + +
    + + + + + + keyman:update_applynow + 220px + + + + + + + 220px + 1 + + + + + + + keyman:update_checknow + 220px + +
    + + + +
    +
    +
    + + + +
    + + javascript:updateTick(""); + Update_ + checked + Update_ + + + Update__RequiresAdmin + +
    + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    + +
    +
    +
    +
    + +
    diff --git a/windows/src/desktop/kmshell/xml/menuframe_update.png b/windows/src/desktop/kmshell/xml/menuframe_update.png new file mode 100644 index 0000000000..5a7bc9d47b Binary files /dev/null and b/windows/src/desktop/kmshell/xml/menuframe_update.png differ diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index c8910b55b9..24909d3486 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 341f036c6c..0b654cfeec 100644 --- a/windows/src/desktop/setup/RunTools.pas +++ b/windows/src/desktop/setup/RunTools.pas @@ -82,7 +82,7 @@ type 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 @@ type 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 @@ begin 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 @@ begin Exit(False); end; - ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates, + ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates,AutomaticUpdates, StartDisabled,StartWithConfiguration,InstallDefaults,AutomaticallyReportUsage); Result := True; @@ -588,7 +588,7 @@ begin 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 @@ begin 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 293df6a613..ded19ae75c 100644 --- a/windows/src/desktop/setup/UfrmRunDesktop.pas +++ b/windows/src/desktop/setup/UfrmRunDesktop.pas @@ -113,6 +113,7 @@ type FCanUpgrade9: Boolean; FCanUpgrade10: Boolean; FCheckForUpdates: Boolean; + FAutomaticUpdates: Boolean; FStartAfterInstall: Boolean; FStartWithWindows: Boolean; FAutomaticallyReportUsage: Boolean; @@ -523,7 +524,7 @@ begin 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 @@ begin 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/keyman/UfrmKeyman7Main.dfm b/windows/src/engine/keyman/UfrmKeyman7Main.dfm index d04a7f649c..ab0d25d3e5 100644 --- a/windows/src/engine/keyman/UfrmKeyman7Main.dfm +++ b/windows/src/engine/keyman/UfrmKeyman7Main.dfm @@ -27,10 +27,10 @@ object frmKeyman7Main: TfrmKeyman7Main Left = 28 Top = 40 end - object tmrOnlineUpdateCheck: TTimer + object tmrBackgroundUpdateCheck: TTimer Enabled = False Interval = 300000 - OnTimer = tmrOnlineUpdateCheckTimer + OnTimer = tmrBackgroundUpdateCheckTimer Left = 280 Top = 104 end diff --git a/windows/src/engine/keyman/UfrmKeyman7Main.pas b/windows/src/engine/keyman/UfrmKeyman7Main.pas index d8cb3e30e7..c0f6e9d62f 100644 --- a/windows/src/engine/keyman/UfrmKeyman7Main.pas +++ b/windows/src/engine/keyman/UfrmKeyman7Main.pas @@ -198,13 +198,13 @@ type TfrmKeyman7Main = class(TForm) mnu: TPopupMenu; tmrTestKeymanFunctioning: TTimer; - tmrOnlineUpdateCheck: TTimer; + tmrBackgroundUpdateCheck: TTimer; tmrCheckInputPane: TTimer; tmrRefresh: TTimer; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure tmrTestKeymanFunctioningTimer(Sender: TObject); - procedure tmrOnlineUpdateCheckTimer(Sender: TObject); + procedure tmrBackgroundUpdateCheckTimer(Sender: TObject); procedure tmrCheckInputPaneTimer(Sender: TObject); procedure tmrRefreshTimer(Sender: TObject); private @@ -1838,7 +1838,7 @@ begin end; end; -procedure TfrmKeyman7Main.tmrOnlineUpdateCheckTimer(Sender: TObject); +procedure TfrmKeyman7Main.tmrBackgroundUpdateCheckTimer(Sender: TObject); begin with TRegistryErrorControlled.Create do // I2890 try @@ -1846,7 +1846,7 @@ begin begin if ValueExists(SRegValue_CheckForUpdates) and not ReadBool(SRegValue_CheckForUpdates) then Exit; if ValueExists(SRegValue_LastUpdateCheckTime) and (Now - ReadDateTime(SRegValue_LastUpdateCheckTime) < 7) then Exit; - TKeymanDesktopShell.RunKeymanConfiguration('-ouc'); + TKeymanDesktopShell.RunKeymanConfiguration('-buc'); end; finally Free; diff --git a/windows/src/engine/keyman/keyman.dpr b/windows/src/engine/keyman/keyman.dpr index 8685e8f4a6..dadcca01fc 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 44959854d5..aa70b2ab7b 100644 --- a/windows/src/engine/keyman/keyman.dproj +++ b/windows/src/engine/keyman/keyman.dproj @@ -141,7 +141,7 @@
    frmOSKEntryHelper
    - + @@ -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 2c019150d7..79db9c8baa 100644 --- a/windows/src/engine/keyman/main.pas +++ b/windows/src/engine/keyman/main.pas @@ -40,9 +40,11 @@ uses System.Win.Registry, GetOsVersion, + Keyman.System.ExecutionHistory, Keyman.System.Security, Keyman.Winapi.VersionHelpers, KeymanVersion, + Klog, RegistryKeys, UfrmKeyman7Main, UserMessages; @@ -76,9 +78,10 @@ var 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/com/system/keymancontrol.pas b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas index b3f8958596..6b429b985e 100644 --- a/windows/src/engine/kmcomapi/com/system/keymancontrol.pas +++ b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas @@ -429,7 +429,7 @@ end; procedure TKeymanControl.OpenUpdateCheck; begin - RunKeymanConfiguration('-ouc'); + RunKeymanConfiguration('-buc'); end; procedure TKeymanControl.StartKeyman; diff --git a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas index 906e121f07..8a18f3be26 100644 --- a/windows/src/engine/kmcomapi/util/utilkeymanoption.pas +++ b/windows/src/engine/kmcomapi/util/utilkeymanoption.pas @@ -121,7 +121,7 @@ type 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 @@ const KeymanOptionInfo: array[0..16] of TKeymanOptionInfo = ( // I3331 // I36 (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 73356d19cb..2d4be34c0a 100644 --- a/windows/src/global/delphi/general/KeymanOptionNames.pas +++ b/windows/src/global/delphi/general/KeymanOptionNames.pas @@ -10,6 +10,7 @@ type koRightModifierHK, koReleaseShiftKeysAfterKeyPress, koShowHints, // I1256 + koAutomaticUpdate, // Startup options koTestKeymanFunctioning, koStartWithWindows,