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

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,
}
}

Related

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

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.

How to make a Finder Sync Extension change badges in response to outside events

I have a Finder Sync Extension that will display a badge on a file based on the state of a local database. It's straightforward enough to query this database in the requestBadgeIdentifierForURL function, but what if I want the badge to change for a Finder item that's already visible if the state of that database has changed (which can be via a notification through any variety of mechanisms). The documentation (https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/Finder.html) would seem to imply this is possible with this statement:
You might also want to track these URLs, in order to update their
badges whenever their state changes.
The only ways I can imagine this would be possible (and most seem wrong) would be:
call setBadgeIdentifier:forURL from another application that is aware of the change
Launch a thread in the init function of my extension which listens for notifications and calls setBadgeIdentifier:forURL when it receives them
Call some OS API that prompts Finder that the extension should be triggered via requestBadgeIdentifierForURL.
Only the last one seems feasible, and could be managed via the extension informing the outside resource what needs refreshing via the beginObservingDirectoryAtURL/endObservingDirectoryAtURL callbacks, but i don't know what mechanism could do this.

How to determine whether macOS app was started because it needs to handle URL

I have macOS application which registers for custom scheme URL (in Info.plist) and I have a code
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(self.handleGetURL(event:reply:)), forEventClass: UInt32(kInternetEventClass), andEventID: UInt32(kAEGetURL) )
It all works fine, when my application is launched and somebody clicks on a URL (containing this custom scheme).
However, in the case, if my app isn't started then system start is (which is good). Unfortunately, handleGetURL() isn't called.
There are two problems with it:
- How can I learn whether an application was launched a usual way or was triggered by the system to handle url?
- How can I get a URL which I need to handle (if an application wasn't running)
Apparently, registering on applicationDidFinishLaunching is too late. I registered on applicationWillFinishLaunching and everything worked as a charm.

Implementing my own protocol and using it through my browser

I want to create a new protocol so that I can view the data retrieved through the protocol in the browser.
For example, I want to be able to go to myprotocol://www.filepath.com/img.jpg and view the image.
Where myprotocol is defined by myself.
I have read about registering application handling here:
http://msdn.microsoft.com/en-us/library/aa767914%28v=vs.85%29.aspx
with this it is possible to run a desktop exe that receives the url I am trying to access. How would I return the retrieved jpg to the browser for viewing, so that it behaves like a normal protocol, such as http?
Thanks
That registration will allow you to bind an application to the uri, so if launched through windows explorer (including "Run") and from command line, then the app is launched and the uri passed to it as an argument (much like if you double-click a file, the default app for it is launched and the path to the file passed).
For example, your "default" browser will have http:// associated with it in this way.
It is still up to the application itself to have its own handling of the URI when it is passed as an argument. If you want to make a browser handle your new protocol, you will have to write an extension/plugin/add-on/whatever-that-browser's-makers-call-it to add further functionality to the browser. This is a separate job for Firefox, IE, Chrome, Konqueror, Chromium (well, at least it might be sharable with Chrome), etc. with separate APIs to deal with.

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