From f395fc1177adb349e4e1994047e4fa29ce412195 Mon Sep 17 00:00:00 2001 From: Grigory Entin Date: Tue, 5 Dec 2023 02:58:15 +0100 Subject: [PATCH 1/3] Added https://github.com/jaredsinclair/etcetera as a submodule. --- .gitmodules | 3 +++ Submodules/etcetera | 1 + 2 files changed, 4 insertions(+) create mode 160000 Submodules/etcetera diff --git a/.gitmodules b/.gitmodules index 1274489..6e9a4c5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "Submodules/ReusableWorkflows"] path = Submodules/ReusableWorkflows url = https://github.com/grigorye/ReusableWorkflows +[submodule "Submodules/etcetera"] + path = Submodules/etcetera + url = https://github.com/jaredsinclair/etcetera diff --git a/Submodules/etcetera b/Submodules/etcetera new file mode 160000 index 0000000..de9036d --- /dev/null +++ b/Submodules/etcetera @@ -0,0 +1 @@ +Subproject commit de9036d29a3eb0182010d95d15740dd022255214 From e8f13edc4e3d80c210c22aec73f51f90d5c7e0fb Mon Sep 17 00:00:00 2001 From: Grigory Entin Date: Tue, 5 Dec 2023 02:58:32 +0100 Subject: [PATCH 2/3] Added OSActivity.swift from https://github.com/jaredsinclair/etcetera. --- URLHelperApp/OSActivity.swift | 1 + 1 file changed, 1 insertion(+) create mode 120000 URLHelperApp/OSActivity.swift diff --git a/URLHelperApp/OSActivity.swift b/URLHelperApp/OSActivity.swift new file mode 120000 index 0000000..d6647e3 --- /dev/null +++ b/URLHelperApp/OSActivity.swift @@ -0,0 +1 @@ +../Submodules/etcetera/Sources/Etcetera/UnifiedLogging/OSActivity.swift \ No newline at end of file From f049807e9f9fe7ad6d23a8b3e08b136e3b7d12e6 Mon Sep 17 00:00:00 2001 From: Grigory Entin Date: Tue, 5 Dec 2023 03:13:51 +0100 Subject: [PATCH 3/3] Employed activity tracing and tweaked privacy in logging. --- URLHelperApp/AppDelegate.swift | 44 +++++++++++++------ URLHelperApp/Info.plist | 42 ++++++++++++++++++ URLHelperApp/OutputFromLaunching.swift | 52 ++++++++++++++--------- URLHelperApp/ScriptBasedURLResolver.swift | 9 ++++ URLHelperApp/WriteAccessFacilitator.swift | 3 +- 5 files changed, 115 insertions(+), 35 deletions(-) diff --git a/URLHelperApp/AppDelegate.swift b/URLHelperApp/AppDelegate.swift index 242ef29..ba261d2 100644 --- a/URLHelperApp/AppDelegate.swift +++ b/URLHelperApp/AppDelegate.swift @@ -17,11 +17,14 @@ private let urlResolver: URLResolver = ScriptBasedURLResolver() class AppDelegate : NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { - log.info("Did finish launching.") + let leave = Activity("Finish Launching").enter(); defer { leave() } + let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String + log.info("Bundle version: \(bundleVersion)") } func application(_ application: NSApplication, open urls: [URL]) { - log.info("Opening \(urls).") + let leave = Activity("Open URLs").enter(); defer { leave() } + log.info("URLs: \(urls)") let task = Task { let urlsByAppBundleIdentifier = try await resolve(urls) for (appBundleIdentifier, urls) in urlsByAppBundleIdentifier { @@ -31,23 +34,30 @@ class AppDelegate : NSObject, NSApplicationDelegate { Task { do { try await task.result.get() - log.info("Succeeded with opening \(urls).") + log.info("Open succeeded") } catch { - log.error("Failed to open \(urls): \(error).") + log.error("Open failed: \(error)") } } } } private func resolve(_ urls: [URL]) async throws -> [String: [URL]] { - try await withThrowingTaskGroup(of: (url: URL, appBundleIdentifier: String)?.self) { group in + let leave = Activity("Get Route For URLs").enter(); defer { leave() } + return try await withThrowingTaskGroup(of: (url: URL, appBundleIdentifier: String)?.self) { group in for url in urls { group.addTask { guard let resolution = try await urlResolver.resolveURL(url) else { - log.error("Unable to resolve \(url).") + log.error("Unable to resolve \(url)") return nil } - return (resolution.finalURL, resolution.appBundleIdentifier) + let finalURL = resolution.finalURL + let appBundleIdentifier = resolution.appBundleIdentifier + if url != finalURL { + log.info("Rewrote \(url) into \(finalURL)") + } + log.info("Got \(appBundleIdentifier, privacy: .public) for opening \(finalURL)") + return (finalURL, appBundleIdentifier) } } return try await group.compactMap { $0 }.reduce([:]) { acc, x in @@ -66,25 +76,31 @@ private func open(_ urls: [URL], withAppWithBundleIdentifier appBundleIdentifier } private func open(urls: [URL], withAppAtURL appURL: URL) async throws { - log.info("Using \(appURL) to open \(urls).") + let leave = Activity("Open URLs With Single App").enter(); defer { leave() } + log.info("URLs: \(urls)") + log.info("App: \(appURL.standardizedFileURL.path)") let configuration = NSWorkspace.OpenConfiguration() configuration.promptsUserIfNeeded = true do { - try await workspace.open(urls, withApplicationAt: appURL, configuration: configuration) - log.info("Succeeded with using \(appURL) to open \(urls).") + do { + let leave = Activity("Route Open Into Workspace").enter(); defer { leave() } + try await workspace.open(urls, withApplicationAt: appURL, configuration: configuration) + } + log.info("Open succeeded") } catch { - log.error("Failed to use \(appURL) to open \(urls): \(error).") + log.error("Open failed: \(error)") throw error } } private func resolveAppURL(forBundleIdentifier bundleIdentifier: String) -> URL? { - log.info("Resolving URL for app bundle identifier \(bundleIdentifier).") + let leave = Activity("Resolve App By Bundle Identifier").enter(); defer { leave() } + log.info("App bundle identifier: \(bundleIdentifier)") guard let appURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) else { - log.error("Could not get URL for app with bundle identifier \(bundleIdentifier).") + log.error("No app has bundle identifier \(bundleIdentifier)") return nil } - log.info("Resolved app bundle identifier \(bundleIdentifier) into \(appURL).") + log.info("Resolved app: \(appURL.standardizedFileURL.path)") return appURL } diff --git a/URLHelperApp/Info.plist b/URLHelperApp/Info.plist index d61bf66..9879e4e 100644 --- a/URLHelperApp/Info.plist +++ b/URLHelperApp/Info.plist @@ -2,6 +2,48 @@ + OSLogPreferences + + com.grigorye.URLHelperApp + + AppDelegate + + Enable-Private-Data + + Level + + Enable + Debug + Persist + Info + + + OutputFromLaunching + + Enable-Private-Data + + Level + + Enable + Debug + Persist + Info + + + ScriptBasedURLResolver + + Enable-Private-Data + + Level + + Enable + Debug + Persist + Info + + + + CFBundleDocumentTypes diff --git a/URLHelperApp/OutputFromLaunching.swift b/URLHelperApp/OutputFromLaunching.swift index 6aa1245..04d794f 100644 --- a/URLHelperApp/OutputFromLaunching.swift +++ b/URLHelperApp/OutputFromLaunching.swift @@ -4,48 +4,48 @@ import os.log private let log = Logger(category: "OutputFromLaunching") func outputFromLaunching(executableURL: URL, arguments: [String]) async throws -> Data { - log.info("Executing \(executableURL.standardizedFileURL.path) with \(arguments).") + let leave = Activity("Shell Output").enter(); defer { leave() } + let captureResultActivity = Activity("Capture Result") + log.info("Executable: \(executableURL.standardizedFileURL.path, privacy: .public)") + log.info("Arguments: \(arguments)") return try await withCheckedThrowingContinuation { c in - let standardOutputPipe = Pipe() - let standardErrorPipe = Pipe() - + let stdoutPipe = Pipe() + let stderrPipe = Pipe() let terminationHandler = { (process: Process) in - let standardErrorData = standardErrorPipe.fileHandleForReading.readDataToEndOfFile() - log.error("Error data: \(standardErrorData).") - let standardOutputData = standardOutputPipe.fileHandleForReading.readDataToEndOfFile() - log.info("Output data: \(standardOutputData).") + let leave = captureResultActivity.enter(); defer { leave() } + let stdout = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderr = stderrPipe.fileHandleForReading.readDataToEndOfFile() + log.info("Stdout: \(formatted(output: stdout), privacy: .public)") let terminationReason = process.terminationReason - guard case .exit = terminationReason else { - log.error("Exec failed. Not exited normally: \(String(describing: terminationReason)).") - log.error("Stderr: \(String(data: standardOutputData, encoding: .utf8) ?? "null").") + guard case .exit = process.terminationReason else { + log.error("Stderr: \(formatted(output: stderr), privacy: .private)") + log.error("Failed with reason: \(String(describing: terminationReason))") throw OutputFromLaunchingError.badTerminationReason(terminationReason) } let terminationStatus = process.terminationStatus guard 0 == terminationStatus else { - log.error("Exec failed. Termination status: \(terminationStatus).") + log.error("Stderr: \(formatted(output: stderr), privacy: .private)") + log.error("Failed with exit status: \(terminationStatus)") throw OutputFromLaunchingError.badTerminationStatus(terminationStatus) } - log.info("Exec succeeded.") - log.info("Stdout: \(String(data: standardOutputData, encoding: .utf8) ?? "").") - return standardOutputData + return stdout } - let process = { $0.executableURL = executableURL $0.arguments = arguments - $0.standardOutput = standardOutputPipe - $0.standardError = standardErrorPipe + $0.standardOutput = stdoutPipe + $0.standardError = stderrPipe $0.terminationHandler = { process in let r = Result { try terminationHandler(process) } c.resume(with: r) } return $0 } (Process()) - do { try process.run() + log.info("Launch succeeded") } catch { - log.error("Launch failed: \(error).") + log.error("Launch failed: \(error)") c.resume(throwing: error) } } @@ -55,3 +55,15 @@ enum OutputFromLaunchingError: Error { case badTerminationReason(Process.TerminationReason) case badTerminationStatus(Int32) } + +private func formatted(output data: Data) -> String { + guard let utf8 = String(data: data, encoding: .utf8) else { + return "" + } + return """ + + ``` + \(utf8) + ``` + """ +} diff --git a/URLHelperApp/ScriptBasedURLResolver.swift b/URLHelperApp/ScriptBasedURLResolver.swift index 85bfffb..71a1b82 100644 --- a/URLHelperApp/ScriptBasedURLResolver.swift +++ b/URLHelperApp/ScriptBasedURLResolver.swift @@ -7,6 +7,9 @@ // import Foundation +import os.log + +private let log = Logger(category: "ScriptBasedURLResolver") class ScriptBasedURLResolver : URLResolver { @@ -24,19 +27,25 @@ class ScriptBasedURLResolver : URLResolver { } func makeSureResolverScriptExists(resolverURL: URL) async throws -> URL? { + let leave = Activity("Make Sure Resolver Script Exists").enter(); defer { leave() } + log.info("Attempting to copy \(self.bundledResolverURL.path, privacy: .public)") + log.info("Destination: \(resolverURL.standardizedFileURL.path, privacy: .public)") do { try fileManager.copyItem(at: bundledResolverURL, to: resolverURL) return resolverURL } catch { switch error { case CocoaError.fileWriteFileExists: + log.info("The script already exists: we're done") return resolverURL case CocoaError.fileWriteNoPermission: + log.info("User permission required") guard let updatedResolverURL = try await facilitateWriteAccessForURLResolverScript(at: resolverURL) else { return nil } return try await makeSureResolverScriptExists(resolverURL: updatedResolverURL) default: + log.error("Copy failed: \(error)") throw error } } diff --git a/URLHelperApp/WriteAccessFacilitator.swift b/URLHelperApp/WriteAccessFacilitator.swift index bdbb141..cc4b0fe 100644 --- a/URLHelperApp/WriteAccessFacilitator.swift +++ b/URLHelperApp/WriteAccessFacilitator.swift @@ -15,7 +15,8 @@ private var appName: String { } func facilitateWriteAccessForURLResolverScript(at url: URL) async throws -> URL? { - try await facilitateWriteAccessViaUserInteraction(to: url, message: String(localized: "Select the location for the resolver script for \(appName)")) + let leave = Activity("Facilitate Write Access To Resolver Script").enter(); defer { leave() } + return try await facilitateWriteAccessViaUserInteraction(to: url, message: String(localized: "Select the location for the resolver script for \(appName)")) } @MainActor