Process() working in one project but not the other

Hi Guys,

I am new to swift and want to create a little Menu Bar app to launch VMs in VirtualBox.

To be able to do that I need to generate a list of VMs and that can be easily done from the commandline with "VBoxManager list vms". So the idea was born to use Process and execute it in the background, collect the output and generate the menu from it.

That does work here in my Shell project:

 func list_vms() -> String {
    let task = Process()
    task.executableURL = URL(fileURLWithPath: "/usr/local/bin/VBoxManage")
    let option = "list"
    let command = "vms"
    task.arguments = [option, command]
    let outputPipe = Pipe()
    let errorPipe = Pipe()

    task.standardOutput = outputPipe
    task.standardError = errorPipe
    do {
        try task.run()
        } catch {
            print("Error launching VBoxManage")
    }
    let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(decoding: outputData, as: UTF8.self)

    return output
}

But the exact same code in an "App" project instead of "Command Line Tool" fails with VBoxManager not being executed. I have no idea why. That doesn't seem logical to me.

Anyone here that can point me into the right direction?

2 Likes

Is there a sandbox in the way?


In general, printing the actual error can provide more information too:

print(error.localizedDescription, error)

Seems to be an issue with VBoxManage being a shell script when running from a MacOS applications. I made a small test app using your function, and ran into the same problem, saying that "VBoxManage was not found". I looked at VBoxManage, saw that is was a shell script that ran the actual VBoxManage executable in the VirtualBox.app. Replaced the string with the pathname to the actual executable (/Applications/VirtualBox.app/Contents/MacOS/VBoxManage), and the error went away. I don't have VMs actually running right now, so I can't verify that command actually worked, but, it at least addressed the error

Thanks guys for looking into it! When I print a more detailed error thats what I am getting:

    Error launching VBoxManage
<NSConcretePipe: 0x600000249200>
The file “VBoxManage” doesn’t exist. Error Domain=NSCocoaErrorDomain Code=4 "The file “VBoxManage” doesn’t exist." UserInfo={NSFilePath=/usr/local/bin/VBoxManage}

Strangely this works perfectly in the "Command Line Tool" project type. Below is the complete script (doesnt seem to be able to attache a ZIP file.

import Foundation

func matches(for regex: String, in text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex)
        let results = regex.matches(in: text,
                                    range: NSRange(text.startIndex..., in: text))
        return results.map {
            String(text[Range($0.range, in: text)!])
        }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

func list_vms() -> String {
    let task = Process()
    task.executableURL = URL(fileURLWithPath: "/usr/local/bin/VBoxManage")
    let option = "list"
    let command = "vms"
    task.arguments = [option, command]
    let outputPipe = Pipe()
    let errorPipe = Pipe()

    task.standardOutput = outputPipe
    task.standardError = errorPipe
    do {
        try task.run()
        } catch {
            print("Error launching VBoxManage")
    }
    let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(decoding: outputData, as: UTF8.self)
    
    // let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
    // let error = String(decoding: errorData, as: UTF8.self)

    return output
}

let vmlist = list_vms()
let matched = matches(for: "\"(.*?)\"",in: vmlist)

for match in matched {
   print(match.replacingOccurrences(of: "\"", with: "", options: NSString.CompareOptions.literal, range:nil))
}

For the Shell one it doesn't matter wether I use "/usr/local/bin/VBoxManage" or what Jonathan suggested "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage". But for the App with Jonathan's the error goes away but I am not getting the results like in the shell one. This is really confusing...

I am also facing a similar issue where the standardOutput doesn't produce output. Try printing debug statements in outputPipe.readabilityHandler.

In my case outputPipe.readabilityHandler closure gets executed however, inside the closure pipe.availableData is an empty string every time.

For some scripts it works and for some it doesn't .... I am guessing (I could be wrong) some scripts might not be using standard output and might be using other means to print to the shell.

The weirdest thing remains as mistery. Why is this working perfecly (always) in a command line project but the exact same code fails in a project with User Interface?

Here is the ZIP file for the project just as command line tool:

It works all the time.

Earlier SDGGiesbrecht wrote:

Is there a sandbox in the way?

That’s the most likely cause of this problem. Did you check for it?

Here is the ZIP file for the project just as command line tool:

Right. Any chance you can post the app code, that is, the thing that doesn’t work?

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Hi Quinn,

The ZIP file is the code that works. But I am using exactly the same functions/code in an app type of project instead of using in a command line project. Its also posted above into the thread.

The command line project does exactly what is expected. But in the app (executing the code by calling it from a menu) it either does not produce an output or with the path set to "/usr/local/bin/VBoxManage" it plainly fails with the program not able to find "VBoxManage".

Again in the command line tool project it works with "/usr/local/bin/VBoxManage" or "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage" the same way.

Here how I am calling the functions by a menu selection:

@obj func GenerateList() {
let vmlist = list_vms()
let matched = matches(for: "\"(.*?)\"".in: vmlist)

for match in matched {
  print(match....

Is there a sandbox in place for "App Projects" that does not exist for "Command Line Projects"?

I have pushed the not working version into my Github: barApp Github

Is there a sandbox in place for "App Projects" that does not exist for
"Command Line Projects"?

Apps — and various other components, like app extensions and XPC Services — can opt in to the App Sandbox. This puts strict limits on what the app can do. For example, a sandboxed app can’t access arbitrary files in the user’s home directory.

Apps must opt in to the App Sandbox [1]. If you created the app from one of the built-in templates, like macOS > App, that opts you in to the sandbox automatically.

You can see whether your app is sandboxed by looking for the App Sandbox pane in Xcode’s Signing & Capabilities editor.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] Except for Mac App Store apps, where the store ingestion process will reject any app that’s not sandboxed.

1 Like

Thanks Quinn, after learning about how to check for the Sandbox I did some further investigation. I could not see any restriction that is in place that would explain the behaviour. However, after removing the Sandbox entirely the code worked as expected!

I would prefer keeping a Sandbox as this is the expected practice. But not sure if that is possible.

The App Sandbox blocks access to much of the file system. The only thing that’s guaranteed to be accessible is your app’s container and the resources needed by the various system frameworks. Everything else could potentially be blocked.

In your specific case /usr/local/bin/ is definitely blocked by the sandbox.

I would prefer keeping a Sandbox as this is the expected practice.

Fair enough. You have two options here:

The former is better if you’re deploying to a wide range of users; the latter is easy and perfectly reasonable if the scope of your deployment is narrow.


If you have follow-up questions about this stuff, I’m going to recommend that you bounce over to DevForums, and specifically the Core OS > Processes topic area. We’re way off piste for Swift Forums.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Excellent! Thanks Quinn! Learned a lot.

I'm sorry for reviving this old thread, but I have the same issue and the suggested solution doesn't work for me. By the way the link to the forums doesn't work either.

The difference in my case is that the executable is a symlink:
/usr/local/bin/ffmpeg -> /usr/local/Cellar/ffmpeg/4.3_1/bin/ffmpeg

The NSOpenPanel seems to actually resolve the symlink, and I can read the file contents, but I can't run it.

Here is some sample code:

        let panel = NSOpenPanel()
        panel.begin { response in
            guard response == .OK, let url = panel.url else {
                return
            }
            print("> \(url.path)")
            do {
                let data = try Data(contentsOf: url)
                print("> \(data.count) bytes")
                let p = Process()
                p.executableURL = url
                try p.run()
            } catch {
                print("ERROR: \(error.localizedDescription)")
            }
        }

This generates the following output:

> /usr/local/Cellar/ffmpeg/4.3_1/bin/ffmpeg
> 297536 bytes
ERROR: The file “ffmpeg” doesn’t exist.

By the way the link to the forums doesn't work either.

Indeed. The launch of the new DevForums has broken links to specific topic areas (r. 65713673). I’d go back and fix the link in my post but, alas, it is no longer editable to me. Hence this follow-up.

My new advice is that you post to DevForums with the App Sandbox tag.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Does using resolvingSymlinksInPath() first solve the problem?

NSOpenPanel is already resolving the symlink, so when I select /usr/local/bin/ffmpeg I get back the resolved URL from the callback.

In retrospect I think that the symlink is a red herring because I get the same behavior when I open /usr/local/Cellar/ffmpeg/4.3_1/bin/ffmpeg directly.

To follow up on this, I did post the same question on the Apple Forums: Running ffmpeg from a sandbox | Apple Developer Forums

The answer is that it is not possible to execute a user selected (or downloaded) binary from a sandboxed app. You can only bundle it together with the app.

In practice this means that no sandboxed app can use any GPL tool (unless the app itself is GPL of course).

I don't understand why you say that you can't use a GPL tool. You could, for example, create a bin directory, copy ffmpeg into the bin directory as part of a runtime script in Xcode, then go through the signing/notarizing processes needed to sandbox the app, which would code-sign ffmpeg. Your application runs the ffmpeg executable when you need to. I thought GPL licensing was only for the executable, in this case, ffmpeg, not the calling application. If thet was the case, I should not be able to run GPL programs from my shell (tcsh, which is not GPL'ed). Your application is acting like a shell, in that instance.

As far as I understand the licensing, the problem is not running it, but distributing it. You are not allowed to distribute a GPL binary (again as far as I know).

In any case, you are still bundling the executable in that case. The problem of not being able to run any not-bundled executable, even if the user selects it was news to me.

You're allowed to distribute a GPL'd binary, you just need to provide the source code used to compile it.

1 Like