diff --git a/mac/Config/Config.xcodeproj/project.pbxproj b/mac/Config/Config.xcodeproj/project.pbxproj index 3f6f7356efc..1a24c59f002 100644 --- a/mac/Config/Config.xcodeproj/project.pbxproj +++ b/mac/Config/Config.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ D83272A82F57614400F71698 /* KeymanSettings in Frameworks */ = {isa = PBXBuildFile; productRef = D83272A72F57614400F71698 /* KeymanSettings */; }; + D88B79CA30472B3B00F10D14 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D88B79C930472B3B00F10D14 /* Sentry-Dynamic */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -70,6 +71,7 @@ buildActionMask = 2147483647; files = ( D83272A82F57614400F71698 /* KeymanSettings in Frameworks */, + D88B79CA30472B3B00F10D14 /* Sentry-Dynamic in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -140,6 +142,7 @@ name = Config; packageProductDependencies = ( D83272A72F57614400F71698 /* KeymanSettings */, + D88B79C930472B3B00F10D14 /* Sentry-Dynamic */, ); productName = Config; productReference = D88F03C62F50ED5000C02A31 /* Keyman Configuration.app */; @@ -220,6 +223,9 @@ ); mainGroup = D88F03BD2F50ED5000C02A31; minimizedProjectReferenceProxies = 1; + packageReferences = ( + D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + ); preferredProjectObjectVersion = 77; productRefGroup = D88F03C72F50ED5000C02A31 /* Products */; projectDirPath = ""; @@ -629,11 +635,27 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.26.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ D83272A72F57614400F71698 /* KeymanSettings */ = { isa = XCSwiftPackageProductDependency; productName = KeymanSettings; }; + D88B79C930472B3B00F10D14 /* Sentry-Dynamic */ = { + isa = XCSwiftPackageProductDependency; + package = D88B79C830472B3B00F10D14 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = "Sentry-Dynamic"; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = D88F03BE2F50ED5000C02A31 /* Project object */; diff --git a/mac/Config/Config/AddKeyboardView.swift b/mac/Config/Config/AddKeyboardView.swift index 3a337a667e4..4fe3e5fa155 100644 --- a/mac/Config/Config/AddKeyboardView.swift +++ b/mac/Config/Config/AddKeyboardView.swift @@ -9,6 +9,7 @@ import SwiftUI import KeymanSettings +import OSLog struct AddKeyboardView: View { @EnvironmentObject var settings: SettingsContainer @@ -52,7 +53,7 @@ struct AddKeyboardView: View { // Placement determines where on the bar it sits ToolbarItem(placement: .cancellationAction) { Button("Close") { - print("close button clicked") + Logger.app.debug("AddKeyboardView close button clicked") dismissAddKeyboardView() if settings.isInstallationInProgress() { settings.userCanceledPackageInstallation() @@ -61,7 +62,7 @@ struct AddKeyboardView: View { } } .onDisappear { - print("AddKeyboardView onDisappear") + Logger.app.debug("AddKeyboardView onDisappear") downloadCoordinator.cancelActiveDownload() } .alert("Package Installation Failed", isPresented: $downloadCoordinator.loadPackageFailed) { @@ -75,11 +76,13 @@ struct AddKeyboardView: View { if let helper = downloadCoordinator.installHelper { PackageConfirmationView(installHelper: helper) { accepted in if accepted { - print("installing validated package: \(helper.packageName ?? "unknown package")") + Logger.download.info("installing validated package: \(helper.packageName ?? "unknown package", privacy: .public)") + LogUtil.infoBreadcrumb("installing validated package: \(helper.packageName ?? "unknown package")", category: .download) do { try settings.installPackage() } catch { - print("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error.localizedDescription)") + Logger.download.error("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error as NSError)", category: .download) } } else { settings.userCanceledPackageInstallation() diff --git a/mac/Config/Config/ConfigApp.swift b/mac/Config/Config/ConfigApp.swift index 329ccdcfc70..c462b15f60a 100644 --- a/mac/Config/Config/ConfigApp.swift +++ b/mac/Config/Config/ConfigApp.swift @@ -9,12 +9,12 @@ import SwiftUI import KeymanSettings import OSLog +import Sentry extension Logger { private static let configSubsystem = ConfigAppUtil.configBundleId - static let package = Logger(subsystem: configSubsystem, category: "package") + static let app = Logger(subsystem: configSubsystem, category: "app") static let download = Logger(subsystem: configSubsystem, category: "download") - static let ui = Logger(subsystem: configSubsystem, category: "ui") } @main @@ -24,7 +24,15 @@ struct ConfigApp: App { @Environment(\.openWindow) private var openWindow init() { - print("tier: \(ConfigAppUtil.appTier)") + Logger.app.log("Starting Keyman Configuration, version: \(ConfigAppUtil.versionWithTag), versionWithTag: \(ConfigAppUtil.versionWithTag)") + let sentryDsnUrl = "https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522" + + // Initialize Sentry only once here + SentrySDK.start { options in + options.dsn = sentryDsnUrl + options.releaseName = ConfigAppUtil.versionGitTag + options.environment = ConfigAppUtil.sentryEnvironment + } } var body: some Scene { diff --git a/mac/Config/Config/DownloadCoordinator.swift b/mac/Config/Config/DownloadCoordinator.swift index cde03e62cba..735887d737e 100644 --- a/mac/Config/Config/DownloadCoordinator.swift +++ b/mac/Config/Config/DownloadCoordinator.swift @@ -48,7 +48,7 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega decisionHandler(.cancel) return } - Logger.download.info("received url: \(urlString, privacy: .public)") + Logger.download.log("received url: \(urlString, privacy: .public)") // if the url matches the install url pattern, then cancel the request, // build the standard URLRequest for a package installation and send it @@ -58,13 +58,15 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega // get the package id (though it appears to be identifying a keyboard in the URL) let matchPackageId = String(match.4) if let downloadUrl = self.settings?.buildDownloadPackageUrl(for: matchPackageId) { - Logger.download.info("package install, download url = \(downloadUrl.absoluteString, privacy: .public)") - + Logger.download.info("package install, download url = \(downloadUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("package install, download url = \(downloadUrl.cleanUrlPath())", category: .download) + let newRequest = URLRequest(url: downloadUrl) DispatchQueue.main.async { webView.startDownload(using: newRequest) { download in - Logger.download.info("download initiated to \(newRequest.url?.absoluteString ?? "nil", privacy: .public)") + Logger.download.info("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil", privacy: .public)") + LogUtil.infoBreadcrumb("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil")", category: .download) download.delegate = self self.setupDownloadTracking(download) } @@ -74,11 +76,13 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega else if urlString.contains(DownloadCoordinator.regexRoot) || urlString.contains(DownloadCoordinator.regexGo) { Logger.download.info("requested root or go url: load in webview") + LogUtil.infoBreadcrumb("requested root or go url: load in webview", category: .download) decisionHandler(.allow) } else { Logger.download.info("default case, open in external browser") + LogUtil.infoBreadcrumb("default case, open in external browser", category: .download) decisionHandler(.cancel) if let targetUrl = URL(string: urlString) { @@ -126,6 +130,7 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega guard let keymanSettings = self.settings else { Logger.download.error("tried to access settings before they were intialized") + LogUtil.errorBreadcrumb("tried to access settings before they were intialized", category: .download) self.loadPackageFailed = true self.loadFailureMessage = InstallPackageError.internalError.localizedDescription completionHandler(nil) @@ -138,6 +143,7 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega do { if let helper = try keymanSettings.initiateKmpFileDownload(kmpFilename: suggestedFilename) { Logger.download.info("download suggested filename: \(suggestedFilename, privacy: .public)") + LogUtil.infoBreadcrumb("download suggested filename: \(suggestedFilename)", category: .download) self.loadFailureMessage = nil // Reset previous error self.loadPackageFailed = false @@ -146,7 +152,8 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega completionHandler(helper.temporaryKmpFileLocation) } } catch { - Logger.download.error("could not initiate package download, error: \(String(describing: error), privacy: .public)") + Logger.download.error("could not initiate package download, error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("could not initiate package download, error: \(error as NSError)", category: .download) self.loadPackageFailed = true self.loadFailureMessage = error.localizedDescription completionHandler(nil) @@ -158,7 +165,9 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega self.progressObserver = nil if let downloadDestination = installHelper?.temporaryKmpFileLocation { - Logger.download.info("download of \(downloadDestination.path, privacy: .public) was successful.") + Logger.download.info("download of \(downloadDestination.cleanUrlPath(), privacy: .public) was successful.") + LogUtil.infoBreadcrumb("download of \(downloadDestination.cleanUrlPath()) was successful.", category: .download) + if let settings { do { try settings.packageDownloadComplete(kmpFileUrl: downloadDestination) @@ -173,7 +182,8 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega } public func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) { - Logger.download.error("download failed with error: \(String(describing: error), privacy: .public)") + Logger.download.error("download failed with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("download failed with error: \(error as NSError)", category: .download) self.isDownloading = false self.progressObserver = nil self.loadPackageFailed = true @@ -187,6 +197,7 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { // The web process crashed. Reload the webview safely. Logger.download.error("webkit process terminated unexpectedly: reloading content") + LogUtil.errorBreadcrumb("webkit process terminated unexpectedly: reloading content", category: .download) webView.reload() } } diff --git a/mac/Config/Config/InstallDebugView.swift b/mac/Config/Config/InstallDebugView.swift index f6f67dc2615..bfc1abf1bc6 100644 --- a/mac/Config/Config/InstallDebugView.swift +++ b/mac/Config/Config/InstallDebugView.swift @@ -62,10 +62,7 @@ struct InstallDebugView: View { _ = installation.validateUserHasRestarted() } Button("Set Displayed Complete") { - let beforeDisplayed = installation.getHasDisplayedInstallationComplete() installation.setHasDisplayedInstallationComplete() - let afterDisplayed = installation.getHasDisplayedInstallationComplete() - print("hasDisplayedInstallComplete = \(beforeDisplayed) -> \(afterDisplayed)") } Button("debug") { installation.debug() @@ -76,9 +73,6 @@ struct InstallDebugView: View { Button("Kill Keyman") { _ = installation.killKeymanInputMethod() } - Button("Uninstall") { - installation.uninstall() - } Spacer() } .padding() diff --git a/mac/Config/Config/KeyboardSearchView.swift b/mac/Config/Config/KeyboardSearchView.swift index aae6afa2bbd..9a3900da60b 100644 --- a/mac/Config/Config/KeyboardSearchView.swift +++ b/mac/Config/Config/KeyboardSearchView.swift @@ -12,6 +12,7 @@ import SwiftUI import Combine import WebKit import KeymanSettings +import OSLog struct KeyboardSearchView: NSViewRepresentable { @ObservedObject var coordinator: DownloadCoordinator @@ -22,7 +23,8 @@ struct KeyboardSearchView: NSViewRepresentable { /** Creates the underlying NSView (WKWebView) for macOS */ func makeNSView(context: Context) -> WKWebView { - print("makeNSView called") + Logger.app.debug("KeyboardSearchView makeNSView called") + let webView = WKWebView() // assign the coordinator as the navigation delegate @@ -41,7 +43,7 @@ struct KeyboardSearchView: NSViewRepresentable { func updateNSView(_ nsView: WKWebView, context: Context) { if coordinator.settings == nil { coordinator.settings = self.settings - print("updateNSView, settings intialized for coordinator") + Logger.app.debug("KeyboardSearchView updateNSView, settings intialized for coordinator") } } } diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index d7266bf2126..31ea85eacc9 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -8,6 +8,8 @@ import SwiftUI import KeymanSettings +import OSLog +import Sentry struct MainConfigView: View { @@ -54,7 +56,17 @@ struct MainConfigView: View { var body: some View { TabView (selection: $selectedTab) { VStack { - // the add keyboard button + // uncomment this Button to force sentry error (must edit scheme and disable 'Debug executable' to test) + /* + Button("Capture Sentry Error") { + let testError = NSError(domain: "SentryTest", code: 404, userInfo: [NSLocalizedDescriptionKey: "Testing Sentry from Keyman Config on Mac"]) + SentrySDK.capture(error: testError) + } + .padding() + .buttonStyle(.borderedProminent) + .tint(.red) + */ + // the add keyboard button LabelButtonView( action: { isShowingAddKeyboardSheet = true }, label: "Add Keyboard", @@ -96,7 +108,9 @@ struct MainConfigView: View { ) { Button("Delete", role: .destructive) { if let uuid = idToDelete { - print("deleting package.id: \(uuid)") + Logger.app.info("deleting package.id: \(uuid)") + LogUtil.infoBreadcrumb("deleting package.id: \(uuid)", category: .app) + // use multiple expanded states? //expandedStates.removeValue(forKey: uuid) @@ -156,13 +170,15 @@ struct MainConfigView: View { packageInstallHelper = nil if accepted { - print("installing validated package: \(helper.packageName ?? "unknown package")") + Logger.app.info("installing validated package: \(helper.packageName ?? "unknown package", privacy: .public)") + LogUtil.infoBreadcrumb("installing validated package: \(helper.packageName ?? "unknown package")", category: .app) do { try settings.installPackage() } catch { self.alertMessage = error.localizedDescription self.isShowingDropKmpAlert = true - print("failed to install package: \(helper.packageName ?? "unknown package") with error: \(error.localizedDescription)") + Logger.app.error("failed to install package: \(helper.packageName ?? "unknown package", privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("failed to install package: \(helper.packageName ?? "unknown package"), error: \(error)", category: .app) } } else { settings.userCanceledPackageInstallation() diff --git a/mac/Config/Installation/InstallationCheck.swift b/mac/Config/Installation/InstallationCheck.swift index 283792d004f..0d153bb7321 100644 --- a/mac/Config/Installation/InstallationCheck.swift +++ b/mac/Config/Installation/InstallationCheck.swift @@ -14,8 +14,9 @@ import Foundation import KeymanSettings +import OSLog -public enum InstallationPhase { +public enum InstallationPhase: String { case inputMethodMissing case inputMethodOutdated case evaluatingInstallation @@ -37,7 +38,7 @@ public enum InstallationPhase { } } -enum InstallationStateCondition { +enum InstallationStateCondition: String { case stale case new case inProgress @@ -112,8 +113,8 @@ public class InstallationCheck { // the input method is valid, examine the installation state recorded on disk // let installationStateCondition = InstallationCheck.evaluateInstallationState(state: installState, for: keymanVersion); - print("installationStateCondition: \(installationStateCondition)") - + Logger.app.log("installationStateCondition: \(installationStateCondition.rawValue, privacy: .public)") + switch installationStateCondition { case .inProgress: self.installationState = installState // resume with the existing installation @@ -181,7 +182,7 @@ public class InstallationCheck { * for testing purposes, by specifying `kTestConfigVersion` in config app's standard UserDefaults */ static func isVersionCurrent(inputMethodVersion: String, configurationVersion: String) -> Bool { - print("isVersionCurrent, comparing input method version: \(inputMethodVersion) and config app version: \(configurationVersion)") + Logger.app.log("isVersionCurrent, comparing input method version: \(inputMethodVersion, privacy: .public) and config app version: \(configurationVersion, privacy: .public)") return inputMethodVersion == configurationVersion } @@ -190,7 +191,8 @@ public class InstallationCheck { * checks the current state of Accessibility permissions */ func registerObservers() { - print("InstallationCheck registerObservers") + Logger.app.debug("InstallationCheck registerObservers") + DistributedNotificationCenter.default().addObserver( self, selector: #selector(self.handleAccessibilityResponse(_:)), @@ -206,11 +208,12 @@ public class InstallationCheck { @objc func handleAccessibilityResponse(_ notification: Notification) { var installCompleted = false - print("handleAccessibilityResponse") // Extract message from the notification if available if let message = notification.object as? String { let permissionGranted = self.processAccessibilityResponse(with: message) + Logger.app.debug("handleAccessibilityResponse, message: \(message, privacy: .public)") + if let state = self.installationState { installCompleted = state.isComplete } @@ -231,7 +234,7 @@ public class InstallationCheck { } } } else { - print("accessibilityStateResponse received but did not include message") + Logger.app.debug("handleAccessibilityResponse, received but did not include message") } } @@ -244,8 +247,8 @@ public class InstallationCheck { .minute(.twoDigits) .second(.twoDigits) .secondFraction(.fractional(3)) - print("processAccessibilityResponse received message: \(message), time: \(Date().formatted(timeStyle))") - + Logger.app.debug("processAccessibilityResponse received message: \(message, privacy: .public), time: \(Date().formatted(timeStyle), privacy: .public)") + // if the message indicates that access was granted, then return true return !message.isEmpty && message == kAccessibilityPermissionGrantedMessage } @@ -313,8 +316,9 @@ public class InstallationCheck { * Creates a InstallationState object describing a new installation */ func createNewInstallationState(with neededTasks: Set) -> InstallationState { - print("completeNewInstallationEvaluation: created new installation state") - var fullTaskList = neededTasks + Logger.app.debug("completeNewInstallationEvaluation: created new installation state") + + var fullTaskList = neededTasks // add prepareNewInstall InstallationTask fullTaskList.insert(InstallationTask.createNewInstallationTask(type: .prepareNewInstall)) @@ -331,10 +335,10 @@ public class InstallationCheck { func checkForRepair(accessibilityPermissionGranted: Bool) { // check whether the installation requires repair if let state = self.createRepairInstallationState(accessibilityPermissionGranted: accessibilityPermissionGranted) { - print("checkForRepair completed: repair is required") + Logger.app.log("checkForRepair completed: repair is required") self.applyRepairedInstallationState(state: state) } else { - print("checkForRepair completed: no repair needed") + Logger.app.log("checkForRepair completed: no repair needed") } } diff --git a/mac/Config/Installation/InstallationContainer.swift b/mac/Config/Installation/InstallationContainer.swift index 38dbdd665cd..50c5f2d9dcc 100644 --- a/mac/Config/Installation/InstallationContainer.swift +++ b/mac/Config/Installation/InstallationContainer.swift @@ -9,6 +9,7 @@ import SwiftUI import Combine import KeymanSettings +import OSLog // in-app notifications sent public extension Notification.Name { @@ -40,10 +41,12 @@ public class InstallationContainer : ObservableObject { // create the settings repository, gaining access to the app group UserDefaults do { defaultsRepo = try DefaultsRepository(suiteName: InputMethodUtil.groupId) - print("Found group container") + Logger.app.log("found group container") } catch UserDefaultsError.unknownSuite { + Logger.app.error("group container not found: \(UserDefaultsError.unknownSuite)") fatalError("Group container not found.") } catch { + Logger.app.error("unable to access settings in group container: \(error as NSError, privacy: .public)") fatalError("Unable to access settings in group container.") } @@ -70,7 +73,8 @@ public class InstallationContainer : ObservableObject { * register observers to learn of results of InstallationState evaluation */ func registerObservers() { - print("InstallationContainer registerObservers") + Logger.app.debug("InstallationContainer registerObservers") + NotificationCenter.default.addObserver( self, selector: #selector(self.handleStartNewInstallation(_:)), @@ -101,7 +105,8 @@ public class InstallationContainer : ObservableObject { * called when `NSNotification.Name.startNewInstallation` is received */ @objc func handleStartNewInstallation(_ notification: Notification) { - print("handleStartNewInstallation received") + Logger.app.debug("handleStartNewInstallation received") + // the evaluation is done self.installationCheck.isEvaluatingNewInstallation = false } @@ -110,7 +115,7 @@ public class InstallationContainer : ObservableObject { * called when `NSNotification.Name.startInstallationRepair` is received */ @objc func handleStartInstallationRepair(_ notification: Notification) { - print("handleStartInstallationRepair received") + Logger.app.debug("handleStartInstallationRepair received") // notify observers NotificationCenter.default.post(name: .installationRepairStarted, object: nil, userInfo: nil) @@ -180,7 +185,8 @@ public class InstallationContainer : ObservableObject { public func currentTask() -> InstallationTask? { guard let state = self.installationState else { return nil } guard self.installationPhase.hasTasks else { - print("the installation phase \(self.installationPhase) has no tasks"); + Logger.app.error("the installation phase \(self.installationPhase.rawValue, privacy: .public) has no tasks") + LogUtil.errorBreadcrumb("the installation phase \(self.installationPhase.rawValue) has no tasks", category: .app) return nil } @@ -211,7 +217,8 @@ public class InstallationContainer : ObservableObject { func executeTask(_ task: InstallationTask) { guard self.installationState != nil else { return } guard self.installationPhase.hasTasks else { - print("the installation phase \(self.installationPhase) has no tasks"); + Logger.app.error("executeTask: the installation phase \(self.installationPhase.rawValue) has no tasks") + LogUtil.errorBreadcrumb("executeTask: the installation phase \(self.installationPhase.rawValue) has no tasks", category: .app) return } @@ -247,7 +254,7 @@ public class InstallationContainer : ObservableObject { * the property in InstallationCheck with the new reference. */ public func updateTaskAsCompleted(taskType: InstallationTaskType) { - print("executeTask: \(taskType.rawValue) completed") + Logger.app.debug("executeTask: \(taskType.rawValue, privacy: .public) completed") if let existingState = self.installationState { let updatedState = InstallationState.createCopyWithCompletedTask(from: existingState, with: taskType) self.installationCheck.installationState = updatedState @@ -269,8 +276,8 @@ public class InstallationContainer : ObservableObject { */ public func migrateData() -> Bool { let success = self.inputMethodUtil.invokeKeymanInputMethodMigration() - print("migration suceeded: \(success)") - + Logger.app.debug("migration suceeded: \(success)") + // check whether if success { NotificationCenter.default.post(name: .dataMigrated, object: nil) @@ -348,10 +355,10 @@ public class InstallationContainer : ObservableObject { if let timeRestartRequested = state.dateRestartRequested { if let mostRecentStartupTime = self.getMostRecentRestartTime() { hasRestarted = mostRecentStartupTime > timeRestartRequested - print("mostRecentStartupTime: \(mostRecentStartupTime), timeRestartRequested: \(timeRestartRequested)") + Logger.app.debug("mostRecentStartupTime: \(mostRecentStartupTime), timeRestartRequested: \(timeRestartRequested)") } } - print("validateRestarted: \(hasRestarted)") + Logger.app.debug("validateRestarted: \(hasRestarted)") return hasRestarted } @@ -386,7 +393,7 @@ public class InstallationContainer : ObservableObject { let enabled = inputMethodUtil.isKeymanInputMethodEnabled() let running = inputMethodUtil.isKeymanInputMethodRunning() - print("Keyman status, version: \(version), enabled: \(enabled), running: \(running), permissionGranted: \(permissionString)") + Logger.app.debug("Keyman status, version: \(version, privacy: .private), enabled: \(enabled), running: \(running), permissionGranted: \(permissionString)") } /** @@ -394,7 +401,7 @@ public class InstallationContainer : ObservableObject { */ public func registerKeymanInputMethod() -> Bool { let success = self.inputMethodUtil.registerKeymanInputMethod() - print("registerKeymanInputMethod suceeded: \(success)") + Logger.app.debug("registerKeymanInputMethod suceeded: \(success)") return success } @@ -404,8 +411,8 @@ public class InstallationContainer : ObservableObject { */ public func selectKeymanInputMethod() -> Bool { let success = self.inputMethodUtil.selectKeymanInputMethod() - print("selectKeymanInputMethod suceeded: \(success)") - + Logger.app.debug("selectKeymanInputMethod suceeded: \(success)") + return success } @@ -426,7 +433,7 @@ public class InstallationContainer : ObservableObject { success = self.inputMethodUtil.enableKeymanInputMethod() } - print("enableKeymanInputMethod suceeded: \(success)") + Logger.app.debug("enableKeymanInputMethod suceeded: \(success)") return success } @@ -445,8 +452,8 @@ public class InstallationContainer : ObservableObject { var requested = false requested = self.inputMethodUtil.invokeKeymanInputMethodRequestAccess() - print("requestAccessibility called, requested: \(requested)") - + Logger.app.debug("requestAccessibility called, requested: \(requested)") + return requested } @@ -463,12 +470,4 @@ public class InstallationContainer : ObservableObject { public func disableKeymanInputMethod() -> Bool { return self.inputMethodUtil.disableKeymanInputMethod() } - - /** - * uninstall the Keyman Input Method - * not functional with default security settings! - */ - public func uninstall() { - self.inputMethodUtil.uninstallKeyman() - } } diff --git a/mac/Keyman.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mac/Keyman.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..581bbf535db --- /dev/null +++ b/mac/Keyman.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "d6053e837382e5b9950aabdaac059058993d8ef6619f809121310d22cd4216b5", + "pins" : [ + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa", + "state" : { + "revision" : "a00a9784fff7193cbb0f9fae509fec8107e07744", + "version" : "9.26.1" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" + } + } + ], + "version" : 3 +} diff --git a/mac/KeymanSettings/Package.resolved b/mac/KeymanSettings/Package.resolved index 7237100b7bc..abfdf2cecea 100644 --- a/mac/KeymanSettings/Package.resolved +++ b/mac/KeymanSettings/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "6a08f242f30fc84e86db579a4645b0b4cb13779957938c0ed895c98c0838d707", + "originHash" : "04c07e57c9c1c1c0bfd7062e1f880a134d063796215b35b16cdebf4fc3b4c0a9", "pins" : [ + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa", + "state" : { + "revision" : "a00a9784fff7193cbb0f9fae509fec8107e07744", + "version" : "9.26.1" + } + }, { "identity" : "zipfoundation", "kind" : "remoteSourceControl", diff --git a/mac/KeymanSettings/Package.swift b/mac/KeymanSettings/Package.swift index ff9c162e558..ff06d9bdc21 100644 --- a/mac/KeymanSettings/Package.swift +++ b/mac/KeymanSettings/Package.swift @@ -17,7 +17,8 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")) + .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")), + .package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.26.1") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -25,7 +26,8 @@ let package = Package( .target( name: "KeymanSettings", dependencies: [ - .product(name: "ZIPFoundation", package: "ZIPFoundation") + .product(name: "ZIPFoundation", package: "ZIPFoundation"), + .product(name: "Sentry-Dynamic", package: "sentry-cocoa") ], path: "Sources", resources: [ diff --git a/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift index 597061e2513..5212efc17e4 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift @@ -8,10 +8,12 @@ import Foundation import OSLog +import Sentry extension Logger { private static let settingsSubsystem = "com.keyman.settings" - static let settings = Logger(subsystem: settingsSubsystem, category: "settings") + static let setup = Logger(subsystem: settingsSubsystem, category: "setup") + static let data = Logger(subsystem: settingsSubsystem, category: "data") } public struct ConfigAppUtil { @@ -20,7 +22,9 @@ public struct ConfigAppUtil { // executes exactly once, the first time any config variable is read private static let configMap: [String: String]? = { guard let map = Bundle.main.infoDictionary?["Keyman"] as? [String: String] else { - fatalError("Keyman dictionary not found in main app bundle.") + let message = "Keyman dictionary not found in main app bundle." + LogUtil.errorBreadcrumb(message, category: .setup) + fatalError(message) } return map }() @@ -64,4 +68,8 @@ public struct ConfigAppUtil { return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" } } + + static func captureSentryError(_ error: Error) { + SentrySDK.capture(error: error) + } } diff --git a/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift b/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift index 6c1f9e3cd60..f467f51c925 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/DefaultsRepo.swift @@ -18,6 +18,5 @@ public protocol DefaultsRepo { func writeEnabledKeyboards(enabledKeyboardsArray: [String]) func readSelectedKeyboard() -> String func writeSelectedKeyboard(keyboardName: String) - func logDefaults() func clearDefaults() } diff --git a/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift index 43dc62c170d..3d4860094b0 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/InputMethodUtil.swift @@ -10,6 +10,7 @@ import Foundation import Carbon.HIToolbox import AppKit +import OSLog public enum KeymanVersionCheckError: Error { case inputMethodNotFound @@ -51,11 +52,11 @@ public class InputMethodUtil { */ public func keymanInputMethodExists() -> Bool { guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { - print("Keyman input method not found, failed to create input method url") + Logger.setup.log("Keyman input method not found, failed to create input method url") return false } - return FileManager.default.fileExists(atPath: inputMethodUrl.path) + return FileManager.default.fileExists(atPath: inputMethodUrl.path(percentEncoded: false)) } /** @@ -117,13 +118,13 @@ public class InputMethodUtil { /** * uninstalls the Keyman input method - * note: not useful to expose to users as default security systems prevent us from deleting the app + * note: commenting out for now as default security settings prevent us from deleting the app */ - public func uninstallKeyman() { - _ = self.killKeymanInputMethod() - _ = self.disableKeymanInputMethod() - self.deleteKeyman() - } +// public func uninstallKeyman() { +// _ = self.killKeymanInputMethod() +// _ = self.disableKeymanInputMethod() +// self.deleteKeyman() +// } /** * Returns version number string for the specifed app located at `~/Library/Input Methods` @@ -144,7 +145,7 @@ public class InputMethodUtil { guard let appVersionString = infoDictionary["CFBundleShortVersionString"] as? String else { throw KeymanVersionCheckError.versionNotFound } - + return appVersionString } @@ -167,15 +168,13 @@ public class InputMethodUtil { } public func invokeKeymanInputMethodMigration() -> Bool { - print("invokeKeymanInputMethodMigration()") + Logger.setup.log("invokeKeymanInputMethodMigration()") return self.invokeKeymanInputMethodAsSubProcess(argument: kMigrateCommand) == 0 } public func invokeKeymanInputMethodRequestAccess() -> Bool { var success = false do { - print("invokeKeymanInputMethodRequestAccess()") - // because we are launching Keyman with a specific command line argument // for this request, we must kill it first _ = self.killKeymanInputMethod() @@ -183,7 +182,8 @@ public class InputMethodUtil { try self.launchKeymanInputMethodAsSeparateProcess(argument: kAccessCommand) success = true } catch { - print("error requesting access: \(error)") + Logger.setup.error("error requesting Accessibility from input method: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error requesting Accessibility from input method: \(error as NSError)", category: .setup) } return success @@ -197,8 +197,8 @@ public class InputMethodUtil { * It contains a message with a value of `granted` or `not-granted` */ func invokeKeymanInputMethodCheckAccess() throws { - print("invokeKeymanInputMethodCheckAccess()") - + Logger.setup.info("invokeKeymanInputMethodCheckAccess()") + LogUtil.infoBreadcrumb("invokeKeymanInputMethodCheckAccess()", category: .setup) // because we are launching Keyman with a specific command line argument // for this request, we must kill it first _ = self.killKeymanInputMethod() @@ -214,13 +214,13 @@ public class InputMethodUtil { let process = Process() if let executableUrl = self.pathUtil.buildInputMethodExecutableUrl(fileName: self.keymanInputMethodApplicationName) { process.executableURL = executableUrl - print("invoking Keyman at: \(String(describing: process.executableURL))") + Logger.setup.info("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()), privacy: .public)") + LogUtil.infoBreadcrumb("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()))", category: .setup) process.arguments = [argument] } var currentEnv = ProcessInfo.processInfo.environment - print("current env: \(String(describing: currentEnv))") - + currentEnv["__CFBundleIdentifier"] = InputMethodUtil.keymanBundleId // set bundle ID to that of the Keyman input method process.environment = currentEnv @@ -229,10 +229,10 @@ public class InputMethodUtil { process.waitUntilExit() // wait for it to finish result = Int(process.terminationStatus) } catch { - print("Failed to run process: \(error)") + Logger.setup.error("Failed to run process: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("Failed to run process: \(error as NSError)", category: .setup) } - print("invokeKeymanInputMethod() result: \(result)") return result } @@ -246,16 +246,15 @@ public class InputMethodUtil { } guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { - print("launchKeymanInputMethodAsSeparateProcess, failed to create input method url") + Logger.setup.error("launchKeymanInputMethodAsSeparateProcess, failed to create input method url") + LogUtil.errorBreadcrumb("launchKeymanInputMethodAsSeparateProcess, failed to create input method url", category: .setup) throw KeymanInvocationError.inputMethodNotFound } NSWorkspace.shared.openApplication(at: inputMethodUrl, configuration: openConfig) { (app, error) in if let error = error { - print("Could not launch Keyman input method at \(inputMethodUrl), due to error: \(error.localizedDescription), code: \(error._code)") - Thread.callStackSymbols.forEach { symbol in - print(symbol) - } + Logger.setup.error("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError)", category: .setup) } } } @@ -268,7 +267,8 @@ public class InputMethodUtil { do { try self.invokeKeymanInputMethodCheckAccess() } catch { - print("invoking Keyman failed: \(error.localizedDescription)") + Logger.setup.error("invoking Keyman failed: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("invoking Keyman failed: \(error as NSError)", category: .setup) } let timeStyle = Date.FormatStyle() @@ -276,7 +276,7 @@ public class InputMethodUtil { .minute(.twoDigits) .second(.twoDigits) .secondFraction(.fractional(3)) - print("doAsyncAccessibilityCheck, listening across process boundaries, time: \(Date().formatted(timeStyle))") + Logger.setup.log("doAsyncAccessibilityCheck, listening across process boundaries, time: \(Date().formatted(timeStyle))") } /** @@ -287,11 +287,11 @@ public class InputMethodUtil { let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) var didTerminate = false - print("Running app count for \(bundleId) = \(runningApps.count)") + Logger.setup.debug("Running app count for \(bundleId, privacy: .public) = \(runningApps.count)") if let runningApp = runningApps.first { let processId = runningApp.processIdentifier didTerminate = runningApp.terminate() - print("process \(processId) for \(bundleId) was terminated: \(didTerminate)") + Logger.setup.log("process \(processId) for \(bundleId, privacy: .public) was terminated: \(didTerminate)") } return didTerminate @@ -314,7 +314,8 @@ public class InputMethodUtil { let inputSourceList = TISCreateInputSourceList(properties as CFDictionary, true) guard let sources = inputSourceList?.takeRetainedValue() as? [TISInputSource], let targetSource = sources.first else { - print("Error: Could not find the specified input source.") + Logger.setup.error("Could not find the specified input source with bundleID: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Could not find the specified input source with bundleID: \(bundleId)", category: .setup) return(nil) } @@ -333,44 +334,22 @@ public class InputMethodUtil { // Bridge the CFTypeRef to an Unmanaged and then to a Swift String if let inputMethodEnabled = Unmanaged.fromOpaque(cfType).takeUnretainedValue() as? Bool { enabled = inputMethodEnabled - print("is enabled: \(enabled)") + Logger.setup.info("isInputMethodEnabled: \(enabled)") + LogUtil.infoBreadcrumb("isInputMethodEnabled: \(enabled)", category: .setup) } else { - print("could not read retrieved enabled property for bundleId: \(bundleId)") + Logger.setup.error("Could not read retrieved enabled property for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Could not read retrieved enabled property for bundleId: \(bundleId)", category: .setup) } } else { - print("Failed to get enabled property for bundleId: \(bundleId)") + Logger.setup.error("Failed to get enabled property for bundleId: \(bundleId, privacy: .public)") } } else { - print("Failed to get input source for bundleId: \(bundleId)") + Logger.setup.error("Failed to get input source for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("Failed to get input source for bundleId: \(bundleId)", category: .setup) } return enabled } - /** - * returns true if the input method with the specified bundleId is capable of being enabled - */ - func isInputMethodEnableCapable(bundleId: String) -> Bool { - var enableCapable = false - - if let inputSource = self.getInputSource(bundleId: bundleId) { - let enableCapableValue = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceIsEnableCapable) - if let cfType = enableCapableValue { - // Bridge the CFTypeRef to an Unmanaged and then to a Swift String - if let capable = Unmanaged.fromOpaque(cfType).takeUnretainedValue() as? Bool { - enableCapable = capable - print("is enable capable: \(enableCapable)") - } else { - print("could not read retrieved enable capable property for bundleId: \(bundleId)") - } - } else { - print("Failed to get enable capable property for bundleId: \(bundleId)") - } - } else { - print("Failed to get input source for bundleId: \(bundleId)") - } - return enableCapable - } - /** * register the newly installed input method with the specified bundleId * this will allow a `TISInputSourceRef` to be obtained to access the input source @@ -379,7 +358,8 @@ public class InputMethodUtil { var success = false guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else { - print("registerInputMethod, failed to create input method url") + Logger.setup.error("registerInputMethod, failed to create input method url for bundleId: \(bundleId, privacy: .public)") + LogUtil.errorBreadcrumb("registerInputMethod, failed to create input method url for bundleId: \(bundleId)", category: .setup) return false } let cfUrl = inputMethodUrl as CFURL @@ -388,9 +368,10 @@ public class InputMethodUtil { success = result == noErr if (success) { - print("registerInputMethod for bundle ID '\(bundleId)': success") + Logger.setup.log("registerInputMethod for bundle ID '\(bundleId, privacy: .public)': success") } else { - print("registerInputMethod for bundle ID '\(bundleId)' failed, result = \(result)") + Logger.setup.error("registerInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("registerInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) } return success @@ -405,9 +386,10 @@ public class InputMethodUtil { let result = TISEnableInputSource(inputSource) success = result == noErr if (success) { - print("enableInputMethod for bundle ID '\(bundleId)': success") + Logger.setup.log("enableInputMethod for bundle ID '\(bundleId, privacy: .public)': success") } else { - print("enableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)") + Logger.setup.error("enableInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("enableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) } } return success @@ -422,27 +404,32 @@ public class InputMethodUtil { let result = TISDisableInputSource(inputSource) success = result == noErr if (success) { - print("disableInputMethod for bundle ID '\(bundleId)': success") + Logger.setup.log("disableInputMethod for bundle ID '\(bundleId, privacy: .public)': success") } else { - print("disableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)") + Logger.setup.error("disableInputMethod for bundle ID '\(bundleId, privacy: .public)' failed, result = \(result)") + LogUtil.errorBreadcrumb("disableInputMethod for bundle ID '\(bundleId)' failed, result = \(result)", category: .setup) } } return success } - func deleteKeyman() { - let fileManager = FileManager.default - if let keymanFile = self.pathUtil.buildInputMethodPathUrl(fileName: keymanInputMethodApplicationName) { - do { - try fileManager.removeItem(at: keymanFile) - print("Successfully deleted Keyman.app") - } catch { - print("Error deleting Keyman.app: \(error)") - } - } else { - print("Keyman.app not found") - } - } + /** + * deletes the Keyman input method + * note: commenting out for now as default security settings prevent us from deleting the app + */ +// func deleteKeyman() { +// let fileManager = FileManager.default +// if let keymanFile = self.pathUtil.buildInputMethodPathUrl(fileName: keymanInputMethodApplicationName) { +// do { +// try fileManager.removeItem(at: keymanFile) +// print("Successfully deleted Keyman.app") +// } catch { +// print("Error deleting Keyman.app: \(error)") +// } +// } else { +// print("Keyman.app not found") +// } +// } /** * select the input source with the specified input source id and return true if successful @@ -454,16 +441,18 @@ public class InputMethodUtil { let inputSourceList = TISCreateInputSourceList(properties as CFDictionary, false) guard let sources = inputSourceList?.takeRetainedValue() as? [TISInputSource], let targetSource = sources.first else { - print("Error: Could not find the input source '\(inputSourceId)'.") + Logger.setup.error("Error: Could not find the input source '\(inputSourceId, privacy: .public)'.") + LogUtil.errorBreadcrumb("Error: Could not find the input source '\(inputSourceId)", category: .setup) return false } let result = TISSelectInputSource(targetSource) if result != noErr { - print("Error selecting input source '\(inputSourceId)': \(result)") - return false + Logger.setup.error("Error selecting input source '\(inputSourceId, privacy: .public)'.") + LogUtil.errorBreadcrumb("Error selecting input source '\(inputSourceId)", category: .setup) + return false } else { - print("Successfully selected input source '\(inputSourceId)'.") + Logger.setup.log("Successfully selected input source '\(inputSourceId, privacy: .public)'.") return true } } diff --git a/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift b/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift new file mode 100644 index 00000000000..027c9fb190f --- /dev/null +++ b/mac/KeymanSettings/Sources/KeymanSettings/LogUtil.swift @@ -0,0 +1,66 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2026-09-01 + * + * Convenience messages for logging to Sentry + */ + +import Sentry + +import Foundation + +/** + * Extend URL to add function that cleans path strings, removing the home directory from the path. + */ +extension URL { + /** + * If the path contains the user's home, replace it with ~ as the home directory name + * may contain the user's name and should not be written to the logs. + */ + public func cleanUrlPath() -> String { + guard self.isFileURL else { return self.absoluteString } + + let unescapedPath = self.path(percentEncoded: false) + let homeDirectory = NSHomeDirectory() + + if unescapedPath.hasPrefix(homeDirectory) { + let relativeComponent = unescapedPath.dropFirst(homeDirectory.count) + return "~\(relativeComponent)" + } + + return unescapedPath + } +} + +public struct LogUtil { + public enum LogCategory: String { + case setup // related to start of app and installation + case data // related to settings and reading and writing packages + case app // general app functionality and UI + case download // downloading keyboard packages + } + + public static func debugBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .debug) + } + + public static func infoBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .info) + } + + public static func warningBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .warning) + } + + public static func errorBreadcrumb(_ message: String, category: LogCategory) { + addBreadcrumb(message: message, category: category.rawValue, level: .error) + } + + // Private helper to talk to Sentry + private static func addBreadcrumb(message: String, category: String, level: SentryLevel) { + let crumb = Breadcrumb(level: level, category: category) + crumb.message = message + SentrySDK.addBreadcrumb(crumb) + } +} diff --git a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift index f4932bedd27..c8b53fa0500 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift @@ -24,6 +24,8 @@ import Foundation import Combine import ZIPFoundation +import OSLog +import Sentry public enum InstallPackageError: LocalizedError { case packageInstallationAlreadyInProgress @@ -116,7 +118,8 @@ public class SettingsContainer : ObservableObject { let languageQueryItem = URLQueryItem(name:"lang", value: languageString) searchUrl.append(queryItems:[languageQueryItem]) } - print("full searchUrl: \(searchUrl.absoluteString)") + Logger.setup.info("full searchUrl: \(searchUrl.absoluteString, privacy: .public)") + LogUtil.infoBreadcrumb("full searchUrl: \(searchUrl.absoluteString)", category: .setup) return searchUrl } @@ -131,7 +134,8 @@ public class SettingsContainer : ObservableObject { var primaryLanguage: String? if let language = systemLanguages.first { primaryLanguage = language - print("primary system language: \(String(describing: primaryLanguage))") + Logger.setup.info("primary system language: \(language, privacy: .public)") + LogUtil.infoBreadcrumb("primary system language: \(language)", category: .setup) } return primaryLanguage @@ -146,20 +150,24 @@ public class SettingsContainer : ObservableObject { // create the package repository, gaining access to the app group container directory do { try self.packageRepository = PackageRepository() - print("Found documents group container") + Logger.data.log("Found documents group container") } catch KeymanPathError.groupContainerNotFound { + Logger.data.error("Document group container not found") fatalError("Document group container not found.") } catch { + Logger.data.error("Unable to access documents in group container, error \(error as NSError, privacy: .public)") fatalError("Unable to access documents in group container.") } // create the settings repository, gaining access to the app group UserDefaults do { try self.defaultsRepository = DefaultsRepository(suiteName: InputMethodUtil.groupId) - print("Found defaults group container") + Logger.data.log("Found defaults group container") } catch UserDefaultsError.unknownSuite { + Logger.data.error("Defaults group container not found") fatalError("Defaults group container not found.") } catch { + Logger.data.error("Unable to access defaults in group container, error \(error as NSError, privacy: .public)") fatalError("Unable to access defaults in group container: \(error.localizedDescription).") } @@ -246,7 +254,7 @@ public class SettingsContainer : ObservableObject { * Called when user chooses to cancel downgrade of package */ public func userCanceledPackageInstallation() { - print("user cancelled package installation") + Logger.data.log("User cancelled package installation") self.packageInstall?.cleanupFailedInstallation() self.packageInstall = nil @@ -256,7 +264,7 @@ public class SettingsContainer : ObservableObject { * Called when user chooses to cancel downgrade of package */ public func packageInstallationFailed() { - print("packageInstallationFailed") + Logger.data.log("Package installation failed") self.packageInstall?.cleanupFailedInstallation() self.packageInstall = nil @@ -284,7 +292,8 @@ public class SettingsContainer : ObservableObject { */ public func findInstalledPackage(with id: UUID) -> KeymanPackage? { guard let package = self.installedPackages.first(where: { $0.id == id }) else { - print ("Error: could not find package with UUID: \(id)") + Logger.setup.error("error: could not find package with UUID: \(id)") + LogUtil.errorBreadcrumb("error: could not find package with UUID: \(id)", category: .setup) return nil } @@ -297,8 +306,6 @@ public class SettingsContainer : ObservableObject { public func removeInstalledPackage(with id: UUID) { if let package = findInstalledPackage(with: id) { self.removeInstalledPackage(package: package) - } else { - print("could not find package with id: \(id)") } } @@ -329,7 +336,8 @@ public class SettingsContainer : ObservableObject { */ public func isKeyboardEnabled(packageId: UUID, keyboardKey: String) -> Bool { guard let package = self.findInstalledPackage(with: packageId) else { - print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)") + Logger.setup.error("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)") + LogUtil.errorBreadcrumb("isKeyboardEnabled, not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup) return false } @@ -342,11 +350,13 @@ public class SettingsContainer : ObservableObject { */ public func setKeyboardEnabled(packageId: UUID, keyboardKey: String, enabled: Bool) { guard let package = self.findInstalledPackage(with: packageId) else { - print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)") + Logger.setup.error("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey, privacy: .public)") + LogUtil.errorBreadcrumb("setKeyboardEnabled, could not read keyboard state for package: \(packageId) and keyboard: \(keyboardKey)", category: .setup) return } - print ("setKeyboardEnabled for \(keyboardKey) setting to \(enabled)") + Logger.setup.info("setKeyboardEnabled for \(keyboardKey, privacy: .public) setting to \(enabled)") + LogUtil.infoBreadcrumb("setKeyboardEnabled for \(keyboardKey)", category: .setup) package.enableKeyboard(keyboardKey: keyboardKey, enabled: enabled) // update persisted state in UserDefaults enabledKeyboards array @@ -410,9 +420,9 @@ public class SettingsContainer : ObservableObject { let enabledKeyboardKeys = self.defaultsRepository.readEnabledKeyboards() if (enabledKeyboardKeys.isSubset(of: installedKeyboardKeys)) { - print("only installed keyboards are listed as enabled: no need to update defaults") + Logger.setup.info("only installed keyboards are listed as enabled: no need to update defaults") } else { - print("enabled keyboards list contains uninstalled keyboards: align with enabled keyboards list") + Logger.setup.info("enabled keyboards list contains uninstalled keyboards: align with enabled keyboards list") let installedEnabledKeyboardKeys = enabledKeyboardKeys.intersection(installedKeyboardKeys) self.defaultsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys)) } @@ -472,7 +482,8 @@ public class SettingsContainer : ObservableObject { * Delegates to the PackageInstallHelper instance to decide whether the package should be installed. */ public func packageDownloadComplete(kmpFileUrl: URL) throws { - print ("packageDownloadComplete \(kmpFileUrl)") + Logger.setup.info("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("packageDownloadComplete \(kmpFileUrl.cleanUrlPath())", category: .setup) do { try self.packageInstall?.prepareToInstall(for: kmpFileUrl) @@ -505,7 +516,8 @@ public class SettingsContainer : ObservableObject { self.installedPackages[index] = package self.addEnabledKeyboards(for: package) } else { - print("Error: package '\(package.packageName)' not found for replacement") + Logger.setup.error("Error: package '\(package.packageName, privacy: .public)' not found for replacement") + LogUtil.errorBreadcrumb("Error: package '\(package.packageName)' not found for replacement", category: .setup) } } } diff --git a/mac/KeymanSettings/Sources/Model/Keyboard.swift b/mac/KeymanSettings/Sources/Model/Keyboard.swift index b368fd4b0fb..12a83023cef 100644 --- a/mac/KeymanSettings/Sources/Model/Keyboard.swift +++ b/mac/KeymanSettings/Sources/Model/Keyboard.swift @@ -10,6 +10,7 @@ import Foundation import AppKit +import OSLog public class Keyboard: Identifiable, Hashable, Equatable { @@ -84,9 +85,9 @@ public class Keyboard: Identifiable, Hashable, Equatable { * validate whether a corresponding kmx file exists for this keyboard */ public func validateKmxFile(in packageDirectory: URL) throws { - let kmxFilePath = self.deriveKmxFileUrl(from: packageDirectory).path - if !FileManager.default.fileExists(atPath: kmxFilePath) { - print("** error: could not find kmx file \(kmxFilePath)") + let kmxFilePath = self.deriveKmxFileUrl(from: packageDirectory) + if !FileManager.default.fileExists(atPath: kmxFilePath.path(percentEncoded: false)) { + Logger.data.error("error: could not find kmx file \(kmxFilePath.cleanUrlPath(), privacy: .public)") throw LoadPackageError.missingKmxFile } } diff --git a/mac/KeymanSettings/Sources/Model/KeymanPackage.swift b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift index 3387ff3ac33..1d144552d85 100644 --- a/mac/KeymanSettings/Sources/Model/KeymanPackage.swift +++ b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift @@ -12,6 +12,7 @@ import AppKit import Cocoa import CoreImage import CoreImage.CIFilterBuiltins +import OSLog public class KeymanPackage: Identifiable, Hashable, Equatable { static let defaultImage: NSImage? = { @@ -19,7 +20,8 @@ public class KeymanPackage: Identifiable, Hashable, Equatable { if let imageUrl = Bundle.module.url(forResource: "SideImage", withExtension: "bmp") { image = NSImage(contentsOf: imageUrl) } else { - print("Error: Could not find SideImage.bmp in the module bundle.") + Logger.setup.error("error: could not find SideImage.bmp in the module bundle") + LogUtil.errorBreadcrumb("error: could not find SideImage.bmp in the module bundle", category: .setup) } return image }() @@ -231,7 +233,7 @@ public class KeymanPackage: Identifiable, Hashable, Equatable { if comparisonResult == .orderedAscending { // keyman version is too old meetsRequiredVersion = false - print("for package '\(self.packageName)' keyman version \(keymanVersion) is older than required version \(minimumKeymanVersion)") + Logger.data.log("validateKeymanVersionForPackage for package: '\(self.packageName, privacy: .public)' keyman version \(keymanVersion, privacy: .public) is older than required version \(minimumKeymanVersion, privacy: .public)") } else { meetsRequiredVersion = true } diff --git a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift index e7e8af18ab2..f1c0639fc13 100644 --- a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift @@ -151,22 +151,6 @@ public class DefaultsRepository: DefaultsRepo { } } - /** - * for debugging: prints UserDefaults values to the console - * with app group UserDefaults, there is no way to view from the command line - * (unlike standard application-level UserDefaults) - */ - public func logDefaults() { - print("UserDefaults:") - print("\(kSelectedKeyboardKey): \(self.readSelectedKeyboard())") - print("\(kDataModelVersionKey): \(self.readDataModelVersion())") - print("\(kForceSentryErrorKey): \(self.readForceSentryError())") - print("\(kShowOskOnActivateKey): \(self.readShowOskOnActivate())") - print("\(kEnabledKeyboardsKey): \(self.readEnabledKeyboards())") - print("\(kPersistedOptionsKey): \(self.readPersistedOptions())") - print("\(kInstallationState): \(self.readInstallationState()?.description ?? "nil")") - } - /** * for debugging: clear all the entries for the app group UserDefaults * unlike standard application-level UserDefaults, there is no way to view from the command line diff --git a/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift b/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift index 3cc1767b49a..bc3b9c8c820 100644 --- a/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift +++ b/mac/KeymanSettings/Sources/Persistence/KeymanPaths.swift @@ -9,6 +9,7 @@ */ import Foundation +import OSLog /** * Three directory trees are represented by the following properties, one in active use @@ -48,11 +49,12 @@ public struct KeymanPaths { // if for some reason it doesn't exist, create it let fileManager = FileManager.default - if !fileManager.fileExists(atPath: fontsDirectory.path) { + if !fileManager.fileExists(atPath: fontsDirectory.path(percentEncoded: false)) { do { try fileManager.createDirectory(at: fontsDirectory, withIntermediateDirectories: true, attributes: nil) } catch { - print("error: could not create fonts directory: \(error.localizedDescription)") + Logger.setup.error("error: could not create fonts directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error: could not create fonts directory: \(error as NSError)", category: .setup) } } @@ -107,23 +109,14 @@ public struct KeymanPaths { self.keyman19PackagesDirectory = KeymanPaths.buildKeyman19PackagesUrl(container: containerDir) self.keyman19TempDirectory = KeymanPaths.buildKeyman19TempUrl(container: containerDir) - //self.logPaths() + self.logPaths() } - /* - fileprivate func logPaths() { - ConfigLogger.shared.testLogger.debug("documents: \(self.keyman17DocumentsDirectory!.absoluteString)") - ConfigLogger.shared.testLogger.debug("keyman 17 packages: \(self.keyman17PackagesDirectory!.absoluteString)") - - ConfigLogger.shared.testLogger.debug("support directory: \(self.keyman18SupportDirectory!.absoluteString)") - ConfigLogger.shared.testLogger.debug("support keyman directory: \(self.keyman18DataDirectory!.absoluteString)") - ConfigLogger.shared.testLogger.debug("keyman 18 packages: \(self.keyman18PackagesDirectory!.absoluteString)") - - ConfigLogger.shared.testLogger.debug("container: \(self.keyman19ContainerDirectory!.absoluteString)") - ConfigLogger.shared.testLogger.debug("preferences: \(self.keyman19PreferencesDirectory!.absoluteString)") - ConfigLogger.shared.testLogger.debug("keyman 19 packages: \(self.keyman19PackagesDirectory!.absoluteString)") - } - */ + fileprivate func logPaths() { + Logger.setup.debug("container: \(self.keyman19ContainerDirectory.cleanUrlPath())") + Logger.setup.debug("preferences: \(self.keyman19PreferencesDirectory.cleanUrlPath())") + Logger.setup.debug("keyman 19 packages: \(self.keyman19PackagesDirectory.cleanUrlPath())") + } /** * build the URL to specified file in the Input Methods directory @@ -142,8 +135,8 @@ public struct KeymanPaths { inputMethodUrl = inputMethodDirectoryUrl.appendingPathComponent(fileName, isDirectory: false) return inputMethodUrl } catch { - // ConfigLogger.shared.testLogger.debug("\(error)") - print("\(error)") + Logger.setup.error("buildInputMethodPathUrl error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildInputMethodPathUrl error: \(error as NSError)", category: .setup) return nil } } @@ -156,6 +149,8 @@ public struct KeymanPaths { let executableName = inputMethodUrl.deletingPathExtension().lastPathComponent return inputMethodUrl.appendingPathComponent("Contents/MacOS/\(executableName)") } else { + Logger.setup.error("buildInputMethodExecutableUrl error: could not build input method executable directory") + LogUtil.errorBreadcrumb("buildInputMethodExecutableUrl error: could not build input method executable directory", category: .setup) return nil } } @@ -175,7 +170,8 @@ public struct KeymanPaths { ) return documentsDirectoryUrl } catch { - print("\(error)") + Logger.setup.error("buildDocumentsUrl error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildDocumentsUrl error: \(error as NSError)", category: .setup) return nil } } @@ -187,7 +183,8 @@ public struct KeymanPaths { if let keyman17PackagesDirectory = documents?.appendingPathComponent(preKeyman19PackagesDirectoryName, isDirectory: true) { return keyman17PackagesDirectory } else { - print("could not build keyman17 packages directory") + Logger.setup.error("buildKeyman17PackagesUrl error: could not build keyman17 packages directory") + LogUtil.errorBreadcrumb("buildKeyman17PackagesUrl error: could not build keyman17 packages directory", category: .setup) return nil } } @@ -208,7 +205,8 @@ public struct KeymanPaths { return supportDirectoryUrl } catch { - print("\(error)") + Logger.setup.error("buildSupportDirectory error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("buildSupportDirectory error: \(error as NSError)", category: .setup) return nil } } diff --git a/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift index eda824f9733..b5d7db38ca5 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageInstallHelper.swift @@ -11,6 +11,7 @@ import Foundation import CoreText +import OSLog public enum PackageInstallationType { case newPackage(String) @@ -69,8 +70,8 @@ public class PackageInstallHelper: Identifiable { * Indicates that a package has been downloaded and can be prepared for installation */ public func packageDownloadComplete(for kmpFileUrl: URL) throws { - print ("packageDownloadComplete \(kmpFileUrl)") - + Logger.data.log("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + try self.prepareToInstall(for: kmpFileUrl) } @@ -79,8 +80,8 @@ public class PackageInstallHelper: Identifiable { * */ public func prepareToInstall(for kmpFileUrl: URL) throws { - print ("prepareToInstall \(kmpFileUrl)") - + Logger.data.log("prepareToInstall \(kmpFileUrl.cleanUrlPath(), privacy: .public)") + do { // unzip to the temp directory try self.packageRepository.unzipKmpFile(at: kmpFileUrl, to: self.temporaryPackageLocation) @@ -103,7 +104,8 @@ public class PackageInstallHelper: Identifiable { self.packageInstallationType = self.determinePackageInstallationType(newPackage: package) } catch { self.cleanupFailedInstallation() - print ("package installation failed with error '\(error)' for \(kmpFileUrl)") + Logger.data.error("package installation failed for \(kmpFileUrl.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("package installation failed for \(kmpFileUrl.cleanUrlPath()) with error: \(error as NSError)", category: .data) throw error } } @@ -112,11 +114,12 @@ public class PackageInstallHelper: Identifiable { * Install the new package and replace existing package if necessary */ public func installPackage() throws { - print ("installPackage \(self.packageToInstall?.packageName ?? "unknown package")") + Logger.data.info ("installPackage \(self.packageToInstall?.packageName ?? "unknown package", privacy: .public)") + LogUtil.infoBreadcrumb("installPackage \(self.packageToInstall?.packageName ?? "unknown package")", category: .data) // prepareToInstall will always set this guard let installationType = self.packageInstallationType else { - print("error: installationType not set before call to installPackage") + Logger.data.error("error: installationType not set before call to installPackage") throw InstallPackageError.internalError } @@ -145,13 +148,16 @@ public class PackageInstallHelper: Identifiable { let comparisonResult = newVersion.compare(existingVersion, options: .numeric) if comparisonResult == .orderedAscending { - print("package downgrade: new version is older than existing version") + Logger.data.info("package downgrade: new version is older than existing version") + LogUtil.infoBreadcrumb("package downgrade: new version is older than existing version", category: .data) installationType = PackageInstallationType.replaceNewerPackage(newPackage.packageName, existingVersion, newVersion) } else if comparisonResult == .orderedDescending { - print("package upgrade: new version is newer than existing version") + Logger.data.info("package upgrade: new version is newer than existing version") + LogUtil.infoBreadcrumb("package upgrade: new version is newer than existing version", category: .data) installationType = PackageInstallationType.replaceOlderPackage(newPackage.packageName, existingVersion, newVersion) } else { - print("new and existing package versions are identical") + Logger.data.info("new and existing package versions are identical") + LogUtil.infoBreadcrumb("new and existing package versions are identical", category: .data) installationType = PackageInstallationType.replaceSameVersionPackage(newPackage.packageName) } } @@ -168,7 +174,7 @@ public class PackageInstallHelper: Identifiable { let fileManager = FileManager.default guard let installLocation = self.installPackageLocation else { - print("error: installPackageLocation not set when installing fonts") + Logger.data.error("error: installPackageLocation not set when installing fonts") return } @@ -179,7 +185,7 @@ public class PackageInstallHelper: Identifiable { includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles]) } catch { - print("error: unable to get contents of directory at \(installLocation.path) with error: \(String(describing: error))") + Logger.data.error("error: unable to get contents of package fonts directory at \(installLocation.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)") } for fontUrl in fileUrls { @@ -187,14 +193,16 @@ public class PackageInstallHelper: Identifiable { if ext == "ttf" || ext == "otf" { // if a font fails to install, log error and continue guard self.validateFont(at: fontUrl) else { - print("error: the font \(fontUrl.lastPathComponent) is not valid") + Logger.data.error("error: the font \(fontUrl.lastPathComponent, privacy: .public) is not valid") + LogUtil.errorBreadcrumb("error: the font \(fontUrl.lastPathComponent) is not valid", category: .data) continue } do { try self.copyFontToFontsDirectory(at: fontUrl) try self.registerFontWithSystem(at: fontUrl) } catch { - print("error: the font \(fontUrl.lastPathComponent) could not be installed with error: \(String(describing: error))") + Logger.data.error("error: the font \(fontUrl.lastPathComponent, privacy: .public) could not be installed with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error: the font \(fontUrl.lastPathComponent) could not be installed with error: \(error as NSError)", category: .data) } } } @@ -220,13 +228,15 @@ public class PackageInstallHelper: Identifiable { let fileManager = FileManager.default // remove the font from the fonts directory just in case it is an old one - if fileManager.fileExists(atPath: fontDestinationUrl.path) { - print("removed existing font: \(fontDestinationUrl.lastPathComponent)") + if fileManager.fileExists(atPath: fontDestinationUrl.path(percentEncoded: false)) { + Logger.data.info("removed existing font: \(fontDestinationUrl.lastPathComponent, privacy: .public)") + LogUtil.infoBreadcrumb("removed existing font: \(fontDestinationUrl.lastPathComponent)", category: .data) try? fileManager.removeItem(at: fontDestinationUrl) } try fileManager.copyItem(at: fontUrl, to: fontDestinationUrl) - print("added font: \(fontDestinationUrl.lastPathComponent)") + Logger.data.info("added font: \(fontDestinationUrl.lastPathComponent, privacy: .public)") + LogUtil.infoBreadcrumb("added font: \(fontDestinationUrl.lastPathComponent)", category: .data) } /** @@ -253,12 +263,14 @@ public class PackageInstallHelper: Identifiable { // code 105 = kCTFontManagerErrorAlreadyRegistered // It is safe to ignore because the font is if errorCode == 105 { - print("font \(fontUrl.lastPathComponent) is already registered.") + Logger.data.info("font \(fontUrl.lastPathComponent) is already registered") + LogUtil.infoBreadcrumb("font \(fontUrl.lastPathComponent) is already registered", category: .data) continue } // if it's any other error, capture it to throw later - print("registerFontWithSystem failed for \(fontUrl.lastPathComponent), error: \(String(describing: cfError))") + Logger.data.error("registerFontWithSystem failed for \(fontUrl.lastPathComponent), error: \(cfError as CFError, privacy: .public)") + LogUtil.errorBreadcrumb("registerFontWithSystem failed for \(fontUrl.lastPathComponent), error: \(cfError as CFError)", category: .data) registrationError = InstallPackageError.fontRegistrationError } @@ -301,7 +313,8 @@ public class PackageInstallHelper: Identifiable { do { try self.deleteDownloadedKmpFile() } catch { - print("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)") + Logger.data.error("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("installNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) } } @@ -318,7 +331,8 @@ public class PackageInstallHelper: Identifiable { do { try self.deleteDownloadedKmpFile() } catch { - print("replaceExistingPackageWithNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)") + Logger.data.error("replaceExistingPackageWithNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("replaceExistingPackageWithNewPackage failed to delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) } } try self.movePackageFromTemporaryToInstalled() @@ -334,13 +348,15 @@ public class PackageInstallHelper: Identifiable { do { try self.deleteDownloadedKmpFile() } catch { - print("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent)") + Logger.data.error("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("cleanupFailedInstallation did not delete downloaded .kmp file: \(self.temporaryKmpFileLocation.lastPathComponent), error: \(error as NSError)", category: .data) } } do { try self.deleteUnzippedPackage() } catch { - print("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryPackageLocation.lastPathComponent)") + Logger.data.error("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryKmpFileLocation.lastPathComponent, privacy: .public), error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("cleanupFailedInstallation did not delete downloaded package: \(self.temporaryKmpFileLocation.lastPathComponent), error as NSError)", category: .data) } } diff --git a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift index 02e43251355..7d73104c104 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift @@ -9,6 +9,7 @@ */ import Foundation +import OSLog public enum LoadPackageError: LocalizedError { case invalidUrl @@ -65,7 +66,8 @@ public class PackageRepository: PackageRepo { try package.validate() installedPackages.append(package) } catch { - print("validation failed for \(url) with error: \(error)") + Logger.data.error("validation failed for \(url.lastPathComponent, privacy: .public) with error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("validation failed for \(url.lastPathComponent) with error: \(error as NSError)", category: .data) } } @@ -78,7 +80,9 @@ public class PackageRepository: PackageRepo { * */ public func loadSinglePackage(packageUrl: URL) throws -> KeymanPackage { - print("loadSinglePackage from url: \(packageUrl)") + Logger.data.info("loadSinglePackage from url: \(packageUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("loadSinglePackage from url: \(packageUrl.cleanUrlPath())", category: .data) + guard let source = try readPackageFromDirectory(packageDirectoryUrl: packageUrl) else { throw LoadPackageError.invalidUrl } let package = KeymanPackage(packageUrl: packageUrl, packageSource: source) @@ -90,12 +94,15 @@ public class PackageRepository: PackageRepo { * delete the package from disk */ public func deletePackage(package: KeymanPackage) { - print("deleting package: \(package.sourceDirectoryUrl)") + Logger.data.info("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data) do { try FileManager.default.removeItem(at: package.sourceDirectoryUrl) - print("deleted package: \(package.sourceDirectoryUrl)") + Logger.data.info("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data) } catch { - print("could not delete directory: \(error.localizedDescription)") + Logger.data.error("could not delete directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("could not delete directory: \(error as NSError)", category: .data) } } @@ -108,19 +115,23 @@ public class PackageRepository: PackageRepo { let packageTempDirectory = pathUtil.keyman19TempDirectory // create the keyman-packages directory if it doesn't already exist - if !FileManager.default.fileExists(atPath: packageDirectory.path) { + if !FileManager.default.fileExists(atPath: packageDirectory.path(percentEncoded: false)) { try FileManager.default.createDirectory(at: packageDirectory, withIntermediateDirectories: true, attributes: nil) - print("Created directory: \(packageDirectory.path)") + Logger.data.info("Created directory: \(packageDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Created directory: \(packageDirectory.cleanUrlPath())", category: .data) } else { - print("Directory already exists: \(packageDirectory.path)") + Logger.data.info("Directory already exists: \(packageDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Directory already exists: \(packageDirectory.cleanUrlPath())", category: .data) } // create the temp directory if it doesn't already exist - if !FileManager.default.fileExists(atPath: packageTempDirectory.path) { + if !FileManager.default.fileExists(atPath: packageTempDirectory.path(percentEncoded: false)) { try FileManager.default.createDirectory(at: packageTempDirectory, withIntermediateDirectories: true, attributes: nil) - print("Created directory: \(packageTempDirectory.path)") + Logger.data.info("Created directory: \(packageTempDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Created directory: \(packageTempDirectory.cleanUrlPath())", category: .data) } else { - print("Directory already exists: \(packageTempDirectory.path)") + Logger.data.info("Directory already exists: \(packageTempDirectory.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("Directory already exists: \(packageTempDirectory.cleanUrlPath())", category: .data) } } @@ -141,9 +152,11 @@ public class PackageRepository: PackageRepo { try fileManager.removeItem(at: fileURL) } - print("successfully cleared temp directory") + Logger.data.info("successfully cleared temp directory") + LogUtil.infoBreadcrumb("successfully cleared temp directory", category: .data) } catch { - print("error clearing temp directory: \(error.localizedDescription)") + Logger.data.error("error clearing temp directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("error clearing temp directory: \(error as NSError)", category: .data) } } @@ -174,9 +187,11 @@ public class PackageRepository: PackageRepo { public func unzipKmpFile(at kmpFileUrl: URL, to packageDestinationUrl: URL) throws { do { try FileManager.default.unzipItem(at: kmpFileUrl, to: packageDestinationUrl) - print("Successfully unzipped the file!") + Logger.data.info("successfully unzipped the file") + LogUtil.infoBreadcrumb("successfully unzipped the file", category: .data) } catch { - print("Extraction failed: \(error.localizedDescription)") + Logger.data.error("extraction failed: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("extraction failed: \(error as NSError)", category: .data) throw LoadPackageError.unzipError } } @@ -193,7 +208,7 @@ public class PackageRepository: PackageRepo { */ func directoryExistsAtPath(directoryUrl: URL) -> Bool { var isDirectory: ObjCBool = false - let exists = FileManager.default.fileExists(atPath: directoryUrl.path, isDirectory: &isDirectory) + let exists = FileManager.default.fileExists(atPath: directoryUrl.path(percentEncoded: false), isDirectory: &isDirectory) return exists && isDirectory.boolValue } @@ -226,15 +241,18 @@ public class PackageRepository: PackageRepo { packageMap[itemUrl] = packageSource } } catch let error as LoadPackageError { - print("** package at \(itemUrl) could not be loaded: \(error.localizedDescription)") + Logger.data.error("package at \(itemUrl.cleanUrlPath(), privacy: .public) could not be loaded: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("package at \(itemUrl.cleanUrlPath()) could not be loaded: \(error as NSError)", category: .data) } } } } catch { - print("Failed to read directory: \(error.localizedDescription)") + Logger.data.error("failed to read directory: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("failed to read directory: \(error as NSError)", category: .data) } - print("\(packageMap.count) packages read") + Logger.data.info("readPackageSource: \(packageMap.count) packages read") + LogUtil.infoBreadcrumb("readPackageSource: \(packageMap.count) packages read", category: .data) return packageMap } @@ -242,11 +260,12 @@ public class PackageRepository: PackageRepo { * check the specified directory for the kmp.json file and read it if it exists */ func readPackageFromDirectory(packageDirectoryUrl: URL) throws -> PackageSource? { - print("readPackageFromDirectory from url: \(packageDirectoryUrl)") + Logger.data.info("readPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath(), privacy: .public)") + LogUtil.infoBreadcrumb("readPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath())", category: .data) var packageSource: PackageSource? = nil let kmpJsonFileUrl = packageDirectoryUrl.appendingPathComponent(packageFileName) - if !FileManager.default.fileExists(atPath: kmpJsonFileUrl.path) { + if !FileManager.default.fileExists(atPath: kmpJsonFileUrl.path(percentEncoded: false)) { throw LoadPackageError.kmpJsonFileNotFound } @@ -272,7 +291,8 @@ public class PackageRepository: PackageRepo { throw error } catch { // otherwise convert the error to a LoadPackageError error - print("readPackage error: \(error.localizedDescription)") + Logger.data.error("readPackage error: \(error as NSError, privacy: .public)") + LogUtil.errorBreadcrumb("readPackage error: \(error as NSError)", category: .data) throw LoadPackageError.kmpJsonFileUnreadable } return packageSource diff --git a/mac/KeymanSettings/Sources/Util/ConfigLogger.swift b/mac/KeymanSettings/Sources/Util/ConfigLogger.swift deleted file mode 100644 index f9322d6a75c..00000000000 --- a/mac/KeymanSettings/Sources/Util/ConfigLogger.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Logger.swift -// KeyFig -// -// Created by Shawn - SIL on 12/9/25. -// - -import OSLog - -class ConfigLogger { - //static let shared = ConfigLogger() - - fileprivate let subsystem = ConfigAppUtil.configBundleId - fileprivate let testCategory = "test" - public let testLogger: Logger - - fileprivate init() { - testLogger = Logger(subsystem: subsystem, category: testCategory) - - testLogger.debug("ConfigLogger instance created.") - } -} diff --git a/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift index 84065244d77..7719f9aa433 100644 --- a/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift +++ b/mac/KeymanSettings/Tests/KeymanSettingsTests/KeymanSettingsTests.swift @@ -9,12 +9,13 @@ import Testing import Foundation +import OSLog @testable import KeymanSettings @Suite("Settings Container") struct SettingsContainersTests { fileprivate init() async throws { - print("init") + Logger.setup.info("init") } @Test("Check settings creation") @MainActor func testSettingsCreation() async throws { @@ -138,7 +139,7 @@ import Foundation @Suite("Check Keyman paths") struct KeymanPathsTests { fileprivate init() async throws { - print("init") + Logger.setup.info("init") } @Test("Check Keyman 17 documents directory") func testKeyman17DocumentsDirectory() async throws { @@ -191,7 +192,7 @@ import Foundation let moabiteKeyboardKey = "/sil_extinct/moabite.kmx" fileprivate init() async throws { - print("init Settings") + Logger.setup.info("init") do { try self.settingsRepo = DefaultsRepository(suiteName: "test.suite.name") } catch UserDefaultsError.unknownSuite { diff --git a/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift b/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift index 3e90186a43a..6a99b6d92be 100644 --- a/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift +++ b/mac/KeymanSettings/Tests/KeymanSettingsTests/RepoStubs.swift @@ -49,12 +49,6 @@ class DefaultsRepoStub: DefaultsRepo { } - func logDefaults() { - print("UserDefaults:") - print("\("KMSelectedKeyboardsKey"): \(self.readSelectedKeyboard())") - print("\("KMEnabledKeyboardsKey"): \(self.readEnabledKeyboards())") - } - func clearDefaults() { selectedKeyboard = "" enabledKeyboards = []