Xcode Extension + Helper Mac App + Launch Arguments?

987 Views Asked by At

Note: It looks like a similar question exists here: Launch Helper Application with Launch Arguments in the Sandbox but I am providing a more thorough example with source code below.

A short preface:

I would like to write an Xcode Source Editor Extension (new in Xcode 8) that, when triggered, launches a companion Mac application I am writing, and passes the Mac application the lines of the source file the user was viewing when they triggered the extension.

The helper Mac application would then provide the user with an interface for performing it's editing features. When the user is done with their changes, they press some sort of a "Save" or "Commit" button, and the changes are then propogated back to the Xcode extension, and then back to the original source file itself.

What I have so far:

I have created a bare-bones Mac application for my helper mac app. All it does currently is, in it's applicationDidFinishLaunching(...) implementation in it's Application Delegate, attempts to build a string of the passed in launch arguments, and display that string as the message body of an alert. See below (note I've tried using both ProcessInfo.processInfo.arguments as well as CommandLine.arguments) :

func applicationDidFinishLaunching(_ aNotification: Notification) {  

    let args = ProcessInfo.processInfo.arguments  

    var argString = ""  
    for arg in args {  
        argString += ", \(arg)"  
    }  

    let alert = NSAlert()  
    alert.addButton(withTitle: "OK")  
    alert.messageText = argString  
    alert.runModal()  

}  

I have created a fairly boiler-plate Xcode extension that, when invoked via the perform(with ...) function, launches my companion Mac helper app. I have tried launching the helper app several ways, including:

Using NSWorkSpace's launchApplication(at: options: configuration:) :

class SourceEditorCommand: NSObject, XCSourceEditorCommand {  

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {  

        defer {  
            completionHandler(nil)  
        }  

        guard let url = NSWorkspace.shared().urlForApplication(withBundleIdentifier: "com.something.TestMacApp") else {  
            print("Couldn't find URL")  
            return  
        }  

        let options: NSWorkspaceLaunchOptions = NSWorkspaceLaunchOptions()  

        var configuration: [String: Any] = [String: Any]()  
        configuration["foo"] = "bar"  
        configuration[NSWorkspaceLaunchConfigurationArguments] = ["foobar"]  
        configuration[NSWorkspaceLaunchConfigurationEnvironment] = ["innerFoo" : "innerBar"]  

        do {  
            try NSWorkspace.shared().launchApplication(at: url, options: options, configuration: configuration)  
        } catch {  
            print("Failed")  
        }  

    }  

}  

Using a custom Process instance to run a bash command, trying both "open" and "fork":

class SourceEditorCommand: NSObject, XCSourceEditorCommand {  

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {  

        defer {  
            completionHandler(nil)  
        }  

        runCommand(command: "open -b com.something.TestMacApp --args --foo=\"bar\"")  

    }  

    func runCommand(command: String) {  
        let task = Process()  
        task.launchPath = "/bin/sh"  
        task.arguments = ["-c", command]  
        task.launch()  
    }  

}  

The Problem

I have archived / exported the helper Mac app and put it in the Applications folder. When I build and run the Xcode extension and test it out, the helper Mac app successfuly launches, but it never gets the custom launch arguments in applicationDidFinishLaunching(...).

I have read in several places, including the documentation for the constant keys for NSWorkSpace's configuration options here: https://developer.apple.com/reference/appkit/nsworkspacelaunchconfigurationarguments that "This constant is not available to sandboxed apps."

When I run the same bash from Terminal:

open -b com.something.TestMacApp --args --foo="bar"

The helper app successfuly reads in the passed --args and displays them in the alert. My fear is this simply isn't possible due to App Sandboxing, but I'm hoping there is another solution that I'm missing. Some other alternative approaches that would also work if they're possible:

  1. Instead of a helper Mac application, if it is possible to make the Xcode extension itself have an interface, then that would solve the problem right there. However I don't believe this is possible.

  2. I could also perhaps launch the helper Mac app, and then communicate with it after it is launched, although again I think perhaps the sandboxing issue may come into play.

For what its worth, I am primarily an iOS developer.

Thanks for any help,

  • Adam Eisfeld
1

There are 1 best solutions below

3
On

First, you are doing this wrong. The correct (as in what Apple says to use) way to launch the container App is to do the following:

  1. In the Info.plist of the container app, put something similar to the following (change the com.cannasoftware.RoboDocument to the appropriate app name and robodocument007 to strings appropriate to your product) :

    enter image description here

  2. In your plugin code use the following to start up the app:

    let customurl = NSURL.init(string: "robodocument007://")
    NSWorkspace.shared().open(customurl as! URL)
    
  3. Use standard inter-process communication to chat between your application and the plugin (DistributedNotificationCenter works quite well here).

It’s not as easy or as trivial as using launch arguments, but I think you will be happier with the results.