Skip to content

Commit

Permalink
Employed activity tracing and tweaked privacy in logging.
Browse files Browse the repository at this point in the history
  • Loading branch information
grigorye committed Dec 5, 2023
1 parent e8f13ed commit f049807
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 35 deletions.
44 changes: 30 additions & 14 deletions URLHelperApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
}

Expand Down
42 changes: 42 additions & 0 deletions URLHelperApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>OSLogPreferences</key>
<dict>
<key>com.grigorye.URLHelperApp</key>
<dict>
<key>AppDelegate</key>
<dict>
<key>Enable-Private-Data</key>
<true/>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Debug</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
<key>OutputFromLaunching</key>
<dict>
<key>Enable-Private-Data</key>
<true/>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Debug</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
<key>ScriptBasedURLResolver</key>
<dict>
<key>Enable-Private-Data</key>
<true/>
<key>Level</key>
<dict>
<key>Enable</key>
<string>Debug</string>
<key>Persist</key>
<string>Info</string>
</dict>
</dict>
</dict>
</dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down
52 changes: 32 additions & 20 deletions URLHelperApp/OutputFromLaunching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? "<non-decodable-from-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)
}
}
Expand All @@ -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 "<non-decodable-from-utf8>"
}
return """
```
\(utf8)
```
"""
}
9 changes: 9 additions & 0 deletions URLHelperApp/ScriptBasedURLResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//

import Foundation
import os.log

private let log = Logger(category: "ScriptBasedURLResolver")

class ScriptBasedURLResolver : URLResolver {

Expand All @@ -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
}
}
Expand Down
3 changes: 2 additions & 1 deletion URLHelperApp/WriteAccessFacilitator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f049807

Please sign in to comment.