Send file URL and args to (running) macOS app via command line
Asked Answered
A

1

8

I've been trying to create a way to tell my (running) macOS app to open some files and supply some additional arguments to the command.

For cold-start apps, using the

$ open MyApp.app fileA.txt --args --foo-arg

would launch the app and I would be able to inspect the --foo-arg via UserDefaults/CommandLine/ProcessInfo. However, if the app is already running, the --foo-arg is missing from UserDefaults/ProcessInfo‌/CommandLine.

I've been struggling to wrap my head around a solution here because I have a few requirements which make things a tad more difficult.

Requirements

  1. File paths sent to app must be opened/saved with sandbox permissions
  2. Arguments and file paths must be intercepted by app at the same time.

Potential Solutions

XPC

Some people have suggested I use XPC but after reading about it, I'm not sure how that solution might look?

  • Do I have to create a Launch Agent app-companion which is always running so that it can detect command line operations and pass it to my app?
  • How does this work with sandboxing because each process has their own permission entitlements?

Apple Script

  • Should I use Apple script to tell my app to open these files with arguments, thus getting around the sandboxing feature?
  • When opening files via AppleScript, can I save those files swell?

URL Scheme

I can register my app to have its own URL scheme but the way NSApplicationDelegate handles the incoming URLs comes in two batches. First, the URLs it can open, followed by the URL schemes or the file paths it can't open. ie:

open -a MyApp.app myapp:foo; open -a MyApp.app file.txt

I can probably make this work but it's a tad tacky and I really want to do this the right way.

Alyss answered 30/5, 2020 at 18:32 Comment(0)
H
11

A command-line tool which ingests its arguments and turns them in to Apple Events is the way to go. You can see how this works from the user's point of view by installing the BBEdit command-line tools and then running man bbedit or man bbdiff in a Terminal window.

From your command-line tool's point of view, the "interesting" parts are:

  1. Figure out whether the application is running: +[NSRunningApplication runningApplicationsWithBundleIdentifier:] will help with that.

  2. If the application is not running, then use -[NSWorkspaceURLForApplicationWithBundleIdentifier:] to first locate the application by bundle ID, then -[NSWorkspace launchApplicationAtURL:options:configuration:error:] to launch the application. This will return an NSRunningApplication instance, or NIL and an error. (Make sure to handle the error case.)

  3. Using the NSRunningApplication instance obtained from either step 1 or step 2, you can now use either the NSAppleEventDescriptor APIs or the low-level AppleEvent C APIs to construct an event. (The higher-level API is probably easier to use.)

That would go something like this:

  1. Construct a target descriptor using the processIdentifier from your running application:

    targetDesc = [NSAppleEventDescriptor descriptorWithProcessIdentifier: myRunningApplication.processIdentifier;
    
  2. Construct an "open documents" event, addressed to your target application:

    event = [NSAppleEventDescriptor appleEventWithEventClass: kCoreEventClass eventID: kAEOpenDocuments targetDescriptor: targetDesc returnID: kAutoGenerateReturnID transactionID: kAnyTransactionID];
    

    Note: I use kCoreEventClass/kAEOpenDocuments as an example - if you're trying to open one or more files with additional information, that's fine. If you're doing some other work, then you should invent a four-character code for an event class which is specific to your application, and a four-character event ID which is unique to the operation you're requesting.

  3. Add the command arguments to the event. For each argument, this consists of creating an appropriate descriptor based on the argument's intrinsic type (boolean, int, string, file URL), and then adding it to the event using a keyword parameter.

    (An Apple Event "keyword" is a four-character code. You can invent your own, with constraints (don't use all-lowercase, and you can use ones defined in AEDataModel.h or AERegistry.h where they fit with your needs).

    For each descriptor you create, add it to the event using -[setParamDescriptor: forKeyword:]:

    myURLParamDesc = [NSAppleEventDescriptor descriptorWithFileURL: myFileURL];
    [event setParamDescriptor: myURLParamDesc forKey: kMyFileParamKeyword];
    
  4. When you've added all of the parameters to the event, send it:

    [event sendWithOptions: kAENoReply timeout: FLOAT_MAX error: &error];
    

On the application side, you'll need to use -[NSAppleEventManager setEventHandler: andSelector: forEventClass: andID:]. This will get called for your custom event class and ID that you invented above, at which point you can use the descriptor APIs to pull the event apart and run your operation.

Sandboxing takes care of itself: your application automatically gets a sandboxing extension for files that it's been passed via Apple Events.

Your command-line tool is not sandboxed -- it can't be, because it's run from Terminal and (potentially) other nonsandboxed apps.

However, the tool must be signed with the hardened runtime, and with com.apple.security.automation.apple-events = YES and a com.apple.security.temporary-exception.apple-events naming your application's bundle identifier, so that the tool can send Apple Events to your application.

(And the tool will need an Info.plist with an NSAppleEventsUsageDescription string.)

I've left a fair amount as an exercise for the reader; but hopefully this will get you started.

Hunger answered 1/6, 2020 at 14:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.