diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java index cb13cc967e..1ce55bf0ce 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/KMLog.java @@ -4,15 +4,19 @@ package com.keyman.engine.util; +import static com.keyman.engine.KMManager.KMKey_LexicalModelID; + import android.util.Log; import android.widget.Toast; import com.keyman.engine.BaseActivity; import com.keyman.engine.BuildConfig; import com.keyman.engine.KMManager; -import com.keyman.engine.util.DependencyUtil; +import com.keyman.engine.data.Keyboard; import com.keyman.engine.util.DependencyUtil.LibraryType; +import java.util.Map; + import io.sentry.Breadcrumb; import io.sentry.Sentry; import io.sentry.SentryLevel; @@ -20,19 +24,67 @@ import io.sentry.SentryLevel; public final class KMLog { private static final String TAG = "KMLog"; + private static final String KEYBOARD_TAG = "keyboardId"; + private static final String KEYBOARD_COUNT_TAG = "installedKeyboardCount"; + private static final String MODEL_TAG = "modelId"; + private static final String LANGCODE_TAG = "languageCode"; + + // Some of the methods used to generate debug logging information can, themselves, + // trigger errors that can also trigger the same logging. We must not get + // caught in an infinite loop / stack-overflow; this field helps us avoid states + // that would otherwise cause error-looping, etc. + private static boolean isLogging = false; + + private static void tagDebugInfo() { + String kbdId = ""; + String lngCode = ""; + String modelId = ""; + int kbdCount = 0; + // Do not risk raising a new error while tagging info for another error. + try { + // Both take a context parameter... but don't actually need or use it! + Keyboard kbd = KMManager.getCurrentKeyboardInfo(null); + kbdCount = KMManager.getKeyboardsList(null).size(); + if (kbd != null) { + kbdId = kbd.getKeyboardID(); + lngCode = kbd.getLanguageCode(); + Map modelMap = KMManager.getAssociatedLexicalModel(kbd.getLanguageID()); + if (modelMap != null) { + modelId = modelMap.get(KMKey_LexicalModelID); + if (modelId == null) { + modelId = ""; + } + } + } + } catch (Exception ex) { + String msg = ex.getMessage() == null ? "" : ex.getMessage(); + Sentry.setExtra("debugLoggingError", msg); + } + Sentry.setExtra(KEYBOARD_TAG, kbdId); + Sentry.setExtra(KEYBOARD_COUNT_TAG, "" + kbdCount); + Sentry.setExtra(LANGCODE_TAG, lngCode); + Sentry.setExtra(MODEL_TAG, modelId); + } + /** * Utility to log info and send to Sentry * @param tag String of the caller * @param msg String of the info message */ public static void LogInfo(String tag, String msg) { + if(isLogging) { + return; + } + isLogging = true; if (msg != null && !msg.isEmpty()) { Log.i(tag, msg); if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.captureMessage(msg, SentryLevel.INFO); } } + isLogging = false; } /** @@ -46,9 +98,15 @@ public final class KMLog { return; } + if(isLogging) { + return; + } + isLogging = true; + Log.i(tag, msg); if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) { + isLogging = false; return; } @@ -74,7 +132,9 @@ public final class KMLog { crumb.setData("stacktrace", trace); } } + tagDebugInfo(); Sentry.addBreadcrumb(crumb); + isLogging = false; } /** @@ -83,6 +143,10 @@ public final class KMLog { * @param msg String of the error message */ public static void LogError(String tag, String msg) { + if(isLogging) { + return; + } + isLogging = true; if (msg != null && !msg.isEmpty()) { Log.e(tag, msg); @@ -91,9 +155,11 @@ public final class KMLog { } if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.captureMessage(msg, SentryLevel.ERROR); } } + isLogging = false; } /** @@ -103,6 +169,10 @@ public final class KMLog { * @param e Throwable exception */ public static void LogException(String tag, String msg, Throwable e) { + if(isLogging) { + return; + } + isLogging = true; String errorMsg = ""; if (msg != null && !msg.isEmpty()) { errorMsg = msg + "\n" + e; @@ -116,9 +186,11 @@ public final class KMLog { } if (DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); Sentry.addBreadcrumb(errorMsg); Sentry.captureException(e); } + isLogging = false; } /** @@ -131,7 +203,12 @@ public final class KMLog { */ public static void LogExceptionWithData(String tag, String msg, String objName, Object obj, Throwable e) { + if(isLogging) { + return; + } + isLogging = true; if (obj != null && DependencyUtil.libraryExists(LibraryType.SENTRY) && Sentry.isEnabled()) { + tagDebugInfo(); String objStr = null; try { objStr = obj.toString(); @@ -141,6 +218,10 @@ public final class KMLog { } // Report the original exception LogException(tag, msg, e); + // And remove the exception-specific tagged data, lest it also be + // tracked on subsequent errors not associated with the current call. + Sentry.removeExtra(objName); } + isLogging = false; } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Errors/SentryManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Errors/SentryManager.swift index 441997ce23..aac335227b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Errors/SentryManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Errors/SentryManager.swift @@ -16,10 +16,20 @@ import os.log */ public class SentryManager { private static var _started: Bool = false + + // Set once Manager.shared is fully initialized and safe to reference. + private static var engineHasInitialized: Bool = false public static var hasStarted: Bool { return _started } + + /** + This method should be called once Manager.shared is fully initialized and safe to reference - a precondition needed to enable engine-state logging, such as current keyboard, etc when errors occur. + */ + public static func setEngineInitialized() { + engineHasInitialized = true + } public static func start(sendingEnabled: Bool = true) { // First things first: enable Sentry for crash reporting. @@ -89,6 +99,31 @@ public class SentryManager { return SentryManager.enabled #endif } + + private static func setDebugInfo() { + // We cannot directly use Manager.shared while it's initializing, + // and there are logging lines during that process that can lead here! + guard engineHasInitialized else { + return + } + + let kbd = Manager.shared.currentKeyboard + let modelId = Manager.shared.currentLexicalModelID?.id ?? "" + let kbdId = kbd?.id ?? "" + let langCode = kbd?.lgCode ?? "" + let kbdCnt = "\(Storage.active.userDefaults.userKeyboards?.count ?? 0)" + + let engineState: [String: String] = [ + "installedKeyboardCount": kbdCnt, + "keyboardId" : kbdId, + "languageCode" : langCode, + "modelId" : modelId, + ] + + SentrySDK.configureScope { scope in + scope.setContext(value: engineState, key: "engineState") + } + } /** * Captures a Sentry event and safely bypass the Sentry component if not activated by the app. @@ -96,6 +131,7 @@ public class SentryManager { public static func capture(_ event: Sentry.Event) { // Guarded in case a library consumer decides against initializing Sentry. if _started { + setDebugInfo() SentrySDK.capture(event: event) } } @@ -131,6 +167,7 @@ public class SentryManager { public static func breadcrumb(crumb: Sentry.Breadcrumb) { // Guarded in case a library consumer decides against initializing Sentry. if _started { + setDebugInfo() SentrySDK.addBreadcrumb(crumb) } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 5b415b76c0..58bec247de 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -249,8 +249,9 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), // name: UIResponder.keyboardWillHideNotification, object: nil) - // We used to preload the old KeymanWebViewController, but now that it's embedded within the - // InputViewController, that's not exactly viable. + // Note that the engine has initialized and that Manager.shared + // is accessible for advanced logging info once this method ends. + SentryManager.setEngineInitialized() } // MARK: - Keyboard Notifications diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu index 67dd16eb5a..357743bbfb 100644 --- a/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu +++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu @@ -3,5 +3,5 @@ [D-BUS Service] Name=com.keyman.SystemService1 -Exec=/usr/libexec/systemd-keyman.service +Exec=@LIBEXECDIR@/systemd-keyman.service User=root diff --git a/linux/keyman-system-service/resources/meson.build.in b/linux/keyman-system-service/resources/meson.build.in index 3ddafdaaf3..5003e2e4b3 100644 --- a/linux/keyman-system-service/resources/meson.build.in +++ b/linux/keyman-system-service/resources/meson.build.in @@ -1,11 +1,26 @@ # This file will be appended to meson.build by build.sh +cfg = configuration_data() +cfg.set('LIBEXECDIR', get_option('prefix') / get_option('libexecdir')) + install_data('com.keyman.SystemService1.conf', install_dir: get_option('datadir') / 'dbus-1/system.d/') if systemd.name() == 'libsystemd' install_data('com.keyman.SystemService1.service.systemd', install_dir: get_option('datadir') / 'dbus-1/system-services/', rename: ['com.keyman.SystemService1.service']) else # libelogind or basu - install_data('com.keyman.SystemService1.service.basu', install_dir: get_option('datadir') / 'dbus-1/system-services/') + configure_file( + configuration: cfg, + input: 'com.keyman.SystemService1.service.basu', + output: 'com.keyman.SystemService1.service', + install: true, + install_dir: get_option('datadir') / 'dbus-1/system-services/' + ) endif -install_data('systemd-keyman.service', install_dir: get_option('prefix') / 'lib/systemd/system/') +configure_file( + configuration: cfg, + input: 'systemd-keyman.service.in', + output: 'systemd-keyman.service', + install: true, + install_dir: get_option('prefix') / 'lib/systemd/system/' +) diff --git a/linux/keyman-system-service/resources/systemd-keyman.service b/linux/keyman-system-service/resources/systemd-keyman.service.in similarity index 94% rename from linux/keyman-system-service/resources/systemd-keyman.service rename to linux/keyman-system-service/resources/systemd-keyman.service.in index 7cb67508a8..c3f9ca998c 100644 --- a/linux/keyman-system-service/resources/systemd-keyman.service +++ b/linux/keyman-system-service/resources/systemd-keyman.service.in @@ -8,7 +8,7 @@ Description=Keyman System Service [Service] Type=dbus BusName=com.keyman.SystemService1 -ExecStart=/usr/libexec/keyman-system-service +ExecStart=@LIBEXECDIR@/keyman-system-service Restart=on-failure # Filesystem lockdown diff --git a/oem/firstvoices/ios/exportAppStore.plist b/oem/firstvoices/ios/exportAppStore.plist index 1d826a49bb..35155c59ea 100644 --- a/oem/firstvoices/ios/exportAppStore.plist +++ b/oem/firstvoices/ios/exportAppStore.plist @@ -7,9 +7,9 @@ teamID D7TR486TEH signingCertificate - 227D454DE9320031D7E9C3E02FEB7463B9A33776 + D7B7973A30C90C1A04666667F24B4D5C7E913923 installerSigningCertificate - 227D454DE9320031D7E9C3E02FEB7463B9A33776 + D7B7973A30C90C1A04666667F24B4D5C7E913923 provisioningProfiles com.firstvoices.keyboards