MacOS 应用程序 - 为什么已经打开的应用程序的 CommandLine.arguments 不包含传递的 --args 参数?

MacOS app - why does CommandLine.arguments of an already open app not contain the passed --args arguments?

我有一个 macOS 应用程序,大多数用户通过将图像拖放到应用程序图标或我的菜单栏图标上来使用它。一些用户还通过 运行 以下命令通过终端使用我的应用程序:

open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg

我的应用程序处理在 AppDelegate 的 func application(_ sender: NSApplication, openFiles filenames: [String]) 方法中传递的 link/links。

目前为止一切正常。如果我的应用程序已经打开,openFiles 仍然会被 MacOS 使用新的图像路径调用,并且我的应用程序会打开一个新的 window 来显示它。这一切都很好。

现在我希望用户能够将某些参数传递到我的应用程序。例如:

open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg --args full

这里我想接收full参数。我阅读了其他一些使用 CommandLine.arguments API 的帖子。但这似乎不包含参数。每次 CommandLine.arguments 的值等于:

["/Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app/Contents/MacOS/AppName", "-NSDocumentRevisionsDebugMode", "YES"]

我认为这是因为 CommandLine.arguments 仅在应用程序最初启动并且参数被传递到 main 函数时才起作用。但是对于已经打开的应用程序,这些不会被传递,因为 main 不会再次为已经 运行 的应用程序调用。

如何实现?

使用 open 命令的 -n 选项,您将始终获得一个可以通过这种方式接收参数的应用程序的新实例。但是,您可能不希望您的应用程序有多个实例。

这意味着如果应用程序已经 运行,您需要一个小型命令行程序来执行带有参数的请求。如果应用程序还不是 运行,它可能应该使用参数启动应用程序。

命令行程序和 GUI 应用程序之间有多种通信方式。根据具体要求,它们各有优缺点。

命令行工具

然而,实际过程总是一样的。这里是 DistributedNotificationCenter 的示例,命令行工具可能如下所示:

import AppKit


func stdError(_ msg: String) {
    guard let msgData = msg.data(using: .utf8) else { return }
    FileHandle.standardError.write(msgData)
}


private func sendRequest(with args: [String]) {
    if let json = try? JSONEncoder().encode(args) {
        
        DistributedNotificationCenter.default().postNotificationName(Notification.Name(rawValue: "\(bundleIdent).openRequest"),
                                                                     object: String(data: json, encoding: .utf8),
                                                                     userInfo: nil,
                                                                     deliverImmediately: true)
    }
}

let bundleIdent = "com.software7.test.NotificationReceiver"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter { [=10=].bundleIdentifier == bundleIdent }.isEmpty
let args = Array(CommandLine.arguments.dropFirst())

if(!isRunning) {
    if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdent) {
        let configuration = NSWorkspace.OpenConfiguration()
        configuration.arguments = args
        NSWorkspace.shared.openApplication(at: url,
                                           configuration: configuration,
                                           completionHandler: { (app, error) in
                                            if app == nil {
                                                stdError("starting \(bundleIdent) failed with error: \(String(describing: error))")
                                            }
                                            exit(0)
                                           })
    } else {
        stdError("app with bundle id \(bundleIdent) not found")
    }
} else {
    sendRequest(with: args)
    exit(0)
}


dispatchMain()

注意:由于 DistributedNotificationCenter 无法再使用当前 macOS 版本发送 userInfo,因此参数简单地转换为 JSON 并与对象参数一起传递。

应用程序

然后实际应用程序可以使用 applicationDidFinishLaunching 来确定它是否是使用参数重新启动的。如果是,则评估参数。它还为通知注册了一个观察者。收到通知后,JSON 将转换为参数。在这两种情况下,它们都只是简单地显示在警报中。可能看起来像这样:

import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let bundleIdent = "com.software7.test.NotificationReceiver"
        DistributedNotificationCenter.default().addObserver(self,
                                                            selector: #selector(requestReceived(_:)),
                                                            name: Notification.Name(rawValue: "\(bundleIdent).openRequest"),
                                                            object: nil)
        let args = Array(CommandLine.arguments.dropFirst())
        if !args.isEmpty {
            processArgs(args)
        }
    }
    
    private func processArgs(_ args: [String]) {
        let arguments = args.joined(separator: "\n")
        InfoDialog.show("request with arguments:", arguments)
    }
    
    @objc private func requestReceived(_ request: Notification) {
        if let jsonStr = request.object as? String {
            if let json = jsonStr.data(using: .utf8) {
                if let args = try? JSONDecoder().decode([String].self, from: json) {
                    processArgs(args)
                }
            }
        }
    }
}


struct InfoDialog {
    
    static func show(_ title: String, _ info: String) {
        let alert = NSAlert()
        alert.messageText = title
        alert.informativeText = info
        alert.alertStyle = .informational
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }
        
}

如前所述,应根据具体要求选择合适的进程间通信方式,但流程始终大致相同。

测试