Send file URL and args to (running) macOS app via command line - macos

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
File paths sent to app must be opened/saved with sandbox permissions
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.

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:
Figure out whether the application is running: +[NSRunningApplication runningApplicationsWithBundleIdentifier:] will help with that.
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.)
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:
Construct a target descriptor using the processIdentifier from your running application:
targetDesc = [NSAppleEventDescriptor descriptorWithProcessIdentifier: myRunningApplication.processIdentifier;
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.
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];
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.

Related

'Second-instance' fires instead of 'open-url' in electron on mac

We have an electron app that we're setting up to launch from protocol links following the structure described here: https://github.com/oikonomopo/electron-deep-linking-mac-win (found from Open app and pass parameters with deep linking using Electron (macOS))
Once the app is installed, we either open the app from finder/launchpad or can invoke the app from a browser using myapp://someparams.
If I invoke myapp://someparams when the app is closed, the app opens and the main process fires the open-url event for mac as expected and I can grab the parameters from the url. If the app was initially opened via this method, re-invoking the myapp://someparams continues to focus the app and fire open-url as expected.
However, if the app was initially opened from the finder, launchpad, or command line, invoking myapp://someparams causes the second-instance event to fire instead and I haven't been able to find a way to get the url that was used to invoke the app. Windows works as expected since the second parameter to the second-instance event contains the protocol as a parameter but that isn't the case with mac.
So the question is - is there a way to grab the protocol/url from the second-instance event on mac? Or is there another way around this?
I did see this snippet from the docs: https://electronjs.org/docs/api/app#apprequestsingleinstancelock
On macOS, the system enforces single instance automatically when users try to open a second instance of your app in Finder, and the open-file and open-url events will be emitted for that. However when users start your app in command line, the system's single instance mechanism will be bypassed, and you have to use this method to ensure single instance.
You should set LSMultipleInstancesProhibited to true in info.plist.
If you are using electron builder, you can set 'LSMultipleInstancesProhibited: true' at mac.extendInfo
For example
mac: {
...
extendInfo: {
LSMultipleInstancesProhibited: true,
}
}

How do you read the bundle identifier of an application from within a sandboxed application?

I'm attempting to add functionality to an application that is similar in function to the "Open With..." menu in the Finder. In the application I'm working on, a user can select a file and chose to open it with a specific application, rather than with the default one.
The problem I'm experiencing is that there appears to be a mis-match in the APIs I think I should be using in order for this to work properly. Specifically, the recommended NSWorkspace API for opening files with a specific application takes a bundle identifier to specifying the application:
[NSWorkspace openURLs:
withAppBundleIdentifier:
options:
additionalEventParamDescriptor:
launchIdentifiers:]
However, the Launch Services API for getting a list of applications that can open a given file returns an array of URLs, each pointing to a compatible application:
CFArrayRef LSCopyApplicationURLsForURL(CFURLRef inURL, LSRolesMask inRoleMask);
Currently, I'm iterating over the array that launch services returns and extracting the bundle identifier by creating a new NSBundle each time:
for (NSURL *applicationURL in applicationURLs) {
NSBundle *applicationBundle = [NSBundle bundleWithURL:applicationURL];
NSString *bundleIdentifier = applicationBundle.bundleIdentifier;
// Do something with bundle identifier...
}
The problem I'm experiencing is that if the applicationURL points to an application that is not under /Applications (or any other directory my sandboxed application has read access to), then I cannot create an NSBundle to read the bundle identifier. Instead, Gate Keeper will output a sandbox violation to Console.app and NSBundle bundleWithURL will return nil.
Is there a way I've overlooked to get the bundle identifier from a URL that I've overlooked? Or is there a different way to read the bundle identifier from an arbitrary file without causing a sandbox violation? Or maybe there's a different way to get a list of all applications that can open a specific URL?
(Note that the Launch Services method to open multiple URLs, LSOpenURLsWithRole has been deprecated. The header file simply says "Use NSWorkspace".)

AppleScript application can't get rights in Accessibility

I'm having trouble with my own AppleScript applications and Accessibility in "Security & Privacy".
I've written an application called "open cubase" that I've granted accessibility rights. I used Apple's advice on how to prevent repeated re-authorization (http://support.apple.com/kb/HT5914). But now even when the application is listed and selected in the Accessibility list, it says that it doesn't have assistive access.
And when I'm using
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/Tcc.db 'SELECT * FROM access WHERE client LIKE "%%"'
to check what's going on, I can see this:
kTCCServiceAccessibility|com.atonus.open-cubase|0|1|0|??
Why is there ?? at the end of that? Is there anyone who would know how to resolve this?
I'm using OSX 10.9.2.
Update, based on feedback from the OP:
The OP's issue is not the use of property statements that normally cause an AppleScript-based application to self-modify the application bundle's embedded Contents/Resources/Scripts/main.scpt script file when property values change at runtime.
However, Apple's workaround at http://support.apple.com/kb/HT5914
IS specifically meant to address not requiring re-authorization as a result of this self-modification issue for a given version of an application.
is NOT meant to allow updating the app (changing its source code or resources) without re-authorization.
For security reasons there is NO way to grant one-time authorization to an app based on its bundle ID and then keep it authorized no matter how it changes (e.g., through updates).
You have two options:
Either: Re-authorize the application every time you update it.
After updating your app, go to System Preferences > Security & Privacy > Privacy > Accessibility and toggle the checkmark next to the list item representing your application (if you application isn't there, drag it there).
Note: With Apple's workaround in place - which for security reasons is NOT a good idea unless you truly need to use property statements that persist their values - it may be sufficient to re-sign the application - haven't verified that.
Or: Use a workaround - not recommended for security reasons:
Make your app an unchanging wrapper that loads the true script code at runtime from a location OUTSIDE the app bundle - that way, the app stays the same and doesn't require re-authorization even if the script file loaded at runtime changes.
Example: Say your true script code - involving code requiring assistive access - is stored as ~/Desktop.test.scpt; your wrapper application, once authorized, can then invoke that script with run script file ((path to home folder as text) & "Desktop:test.scpt")
I don't have a specific explanation, but a recommendation:
Do not use properties (e.g., property FNAME : "Input.txt") in your AppleScript-based applications: AppleScript persists these automatically (preserves their values between runs), but the feature is implemented awkwardly (the persisted values are written to the *.scpt file itself - this is what causes the repeated authorization problem) and flimsily (if you modify your application and save (the *.scpt file at the heart of the) application again, previously persistent values are lost).
If you stay away from properties, the problem with repeated authorization simply goes away (unless you update your application). You can roll your own persistence, e.g., via AppleScript's support for .plist (property-list) files (see the System Events dictionary).
You also won't need the workaround described in the linked support article (http://support.apple.com/kb/HT5914), which is also a plus, given that the workaround is based on opening up a security hole.
As for your specific question:
The ?? is the - unhelpful - representation of the csreq columnn value from the TCC.db database and is not a problem per se; OSX manages that column behind the scenes; it contains a fingerprint of sorts identifying the application in its specific current form (similar to an MD5 hash, though I have no idea what is actually being used), so as to be able to detect tampering later.
However, I suspect you may be looking at the wrong database entry:
I'm puzzled by your bundle ID being com.atonus.open-cubase: if your app is an AppleScript-based *.app bundle, its bundle ID would have the fixed prefix com.apple.ScriptEditor.id., e.g., com.apple.ScriptEditor.id.open-cubase. Did you manually modify the bundle ID via the bundle's Info.plist file, or am I missing something?
When the OS determines tampering/a change in an authorized application:
It resets the allowed column value to 0, i.e., revokes authorization
It resets the csreq column value to NULL.
Thus, after you've seen the ... is not allowed assistive access dialog, the database entry should be reported as kTCCServiceAccessibility|com.atonus.open-cubase|0|0|1| - note the changed Boolean flags and the absence of the ?? at the end.

Retrieving parameters from a custom URL in TideSDK

I'm putting together a desktop application in TideSDK and am having some trouble finding the parameters passed to the application via a custom launch URL.
The application launches when the appropriate protocol is invoked (call it aaa://), but I haven't been able to figure out how to grab the URL string. I read a couple of threads that suggested I could get the string with the Ti.API.application.getArguments() call, but it returns something odd (see below).
// launch application with aaa://some_args_here
var args = Ti.API.application.getArguments();
// returns (StaticBoundList) [ /path/to/app, "-psn_0_721072", ]
I'm not completely surprised that this doesn't seem to work, as the API documentation says the getArguments method returns a list of command line arguments.
Any insight as to how to access the launch URL would be appreciated!
have a look at window.location.search
https://developer.mozilla.org/en-US/docs/DOM/window.location
It doesn't appear that there's currently an "official" way to do this, so I came up with my own solution that seems to work pretty well (though currently its only implemented for OSX).
Searching through the TideSDK source, I found the place where the native application delegate is created. I added a URL launch handler in the app delegate, which stores the launch URL in a new app delegate member, and connected it to the API with a binding in Ti.UI.
If anybody else is interested in this functionality, or have insights as to how to accomplish this task for Windows, please get in touch!
Although I don't have your problem, just want to say the following function works fine for me.
var args = Ti.API.application.getArguments();
is it possible this is the problem of the urlprotocl registry?
my url protocol is set by this : MSDN
thus the value in "Command" is
"C:\YOUR_APP_FOLDER\YOURAPP.exe" "%1"
tested on win7 and winxp,
both successfully get the arguments.

How can a Mac app determine the method used to launch it?

I have a Mac OS X application that is also a protocol handler (just as, for example, Safari is a protocol handler for the HTTP and HTTPS protocols). So when a user clicks a link of the form myscheme://some-kind-of-info in any application at all, my application launches to handle the link.
Now I need to be able to determine if the application was launched by such a link click, or if it was launched by any other method. In other words, it was launched by any method besides a link click. (In those cases, I want the app to stay open, but if it was launched by a link it should quit and ignore the link. This way it only operates when already running.)
Is there some way within the app at startup to introspect and find out that it was launched by a standard method rather than by an AppleScript GetURL event? I'd like to find out through a documented method, rather than - for example - just have my app only open these links after it's been running for a half a second.
You can register a handler for each of the possible Apple Events you'll get on launch, and make note of which one you receive first.
If the application is launched without documents, you'll get kAEOpenApplication.
If it's launched with documents, you'll get kAEOpenDocuments (or
kAEPrintDocuments).
If it's launched with a URL, then (obviously) you'll get kAEGetURL.
There's also kAEOpenContents, but I wasn't able to trigger it easily in my test app; it's probably worth supporting no matter what.
How Cocoa Applications Handle Apple Events documents all of this stuff.
There is one error in there, though; it says that AppleScript's "launch" will send kAEOpenApplication. It won't, it'll send ascr/noop (kASAppleScriptSuite/kASLaunchEvent, defined in ASRegistry.h). I couldn't get the usual Cocoa event handler mechanism to trap this event, so you may need to do some more digging there.
One way you can check if the event is sent at launch is to register the event handlers in your application delegate's applicationWillFinishLaunching: method; they should deliver by the time applicationDidFinishLaunching: is invoked. With that method, you could potentially only check for kAEGetURL.

Resources