Process.run Returns 'The file “<command>” doesn’t exist.' - swift

I'm trying to write a small app to start/stop and display data from a command line "app" someone else wrote. The command executable is installed in '/usr/local/bin'. It outputs status text data to standardOutput while running. I can execute this "command" from the Terminal.app without issue. From swiftUI code I can successfully execute "built-in" commands like ls. However, when (in swiftUI code) I attempt to execute Process.run WITH the new command it throws the exception 'The file “” doesn’t exist.'
Anyone have any ideas? Thanks in advance!
Here's a code snip:
// NOTE: "installedCommand" is just a placeholder for the actual command.
let task = Process()
let connection = Pipe()
let exeUrl = URL(fileURLWithPath: "/usr/local/bin/installedCommand")
//let exeUrl = URL(fileURLWithPath: "/bin/ls") <--works fine
task.executableURL = exeUrl
task.standardOutput = connection
do
{
try task.run()
}
catch
{
print("Error: \(error.localizedDescription)")
return
}

Related

How to run terminal command in swift from any directory?

I'm trying to creating a macOS application that that involves allowing the user to run terminal commands. I am able to run a command, but it runs from a directory inside my app, as far as I can tell. Running pwd returns /Users/<me>/Library/Containers/<My app's bundle identifier>/Data.
How can I chose what directory the command runs from?
I'm also looking for a way to get cd to work, but if I can chose what directory to run the terminal command from, I can handle cd manually.
Here is the code that I'm currently using to run terminal commands:
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
I'm using Xcode 12 on Big Sur.
Thanks!
There is a deprecated property currentDirectoryPath on Process.
On the assumption you won't want to use a deprecated property, after reading its documentation head over to the FileManager and look at is provisions for managing the current directory and their implications.
Or just use cd as you've considered – you are launching a shell (zsh) with a shell command line as an argument. A command line can contain multiple commands separated by semicolons so you can prepend a cd to your command value.
The latter approach avoids changing your current process' current directory.
HTH
To add to CRD's answer, if using the cd approach, you may also consider separating your commands using && to wait for the previous commands to complete successfully before proceeding to the next command that depends on it.
Try the command you wish to run in the terminal and see if it completes as expected
Eg: /bin/bash -c "cd /source/code/ && git pull && swift build"
If everything works as expected you can go ahead and use it in your swift code as so:
shell("cd /source/code/ && git pull && swift build")
On the topic of deprecations, you may want to replace
launchPath with executableURL
and
launch() with run()
Sample implementation with updated code:
#discardableResult
func shell(_ args: String...) -> Int32 {
let task = Foundation.Process()
task.executableURL = URL(fileURLWithPath: "/bin/bash")
task.arguments = ["-c"]
task.arguments = task.arguments! + args
//Set environment variables
var environment = ProcessInfo.processInfo.environment
environment["PATH"]="/usr/bin/swift"
//environment["CREDENTIALS"] = "/path/to/credentials"
task.environment = environment
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch {
// handle errors
print("Error: \(error.localizedDescription)")
}
task.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
let error = String(decoding: errorData, as: UTF8.self)
//Log or return output as desired
print(output)
print("Ran into error while running: \(error)")
return task.terminationStatus
}

Swift code working in playground, but not in project

I tested out some code in a playground and it works as I would expect.
I built an extremely basic (one function, one button, one textfield) project to test the code in and it doesn't work – in fact it hangs up (beach balling).
What might cause this to happen?
Both the playground and the project import Cocoa and Foundation.
The code is below.
It appears to get hung up on this line:
let data = pipe.fileHandleForReading.readDataToEndOfFile()
Here's the code as it is written in the playground (and copied into the project):
import Cocoa
import Foundation
// *** Getting exiftool version number
func exiftoolVersion() -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-ver"]
task.executableURL = URL(fileURLWithPath: "/usr/local/bin/exiftool")
do {
try task.run()
task.waitUntilExit()
}
catch {
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
var output = String(data: data, encoding: .utf8)!
output = output.filter { !$0.isWhitespace }
return output
}
The one way that I know would solve your issue is to go to your .entitlements file and switch turn off App Sandbox, by setting the value to false, as shown below.
<key>com.apple.security.app-sandbox</key>
<false/>
This is because in sandboxed mode you aren't allowed to execute an external program.
Edit: Since it didn't work for you, can you try replacing catch {} with
catch {
print(error)
}
and see what/if there's any error?

How to run Terminal commands from cocoa app?

I have problems running a Terminal command from a Cocoa Application.
The input for the Terminal is real easy: /Users/.../Csvmidi </Users/.../test.csv> /Users/.../Melody.mid
These are three inputs- three actual paths - are just written in a row and seperated by a spac: the first "Csvmidi" runs a Unix Application which converts the test.csv to an actual hearable MIDI file. Through the terminal it works perfectly...
I just don't get it to work via a Cocoa Application.
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh/")
process.arguments = [lblCsvmidi.stringValue,"<"+lblURL.stringValue+">",lblMidi.stringValue]
//I saved the URL of the UNIX program, test.csv and Melody.mid in a lable just to be sure.
//lblCsvmidi --> URL of Csvmidi UNIX program
//lblURL --> URL of test.csv
//lblMidi --> URL of Melody.mid
print(lblCsvmidi.stringValue,"<" + lblURL.stringValue + ">",lblMidi.stringValue)
// this print command was only made to check the arguments in the terminal if they would work --> they do
process.terminationHandler = { (process) in
print("\ndidFinish: \(!process.isRunning)")
}
do {
try process.run()
} catch {}
When I run the code it gives me either the error of an 75: unmatched", but actually there isn't a quotation mark in the command - or I get "permission denied" errors.
I tried several bin folders like ( I really tried almost every possible):
- /bin/zsh
- /bin/csh
- /bin/ksh
- ...
What am i doing wrong --> I haven't found information in other Questions here and the Process, NSTask and Bundle informations from Apple haven't helped me so far.
Thanks!!
The following runs without error on my system:
import Cocoa
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh/")
var args : [String]!
args = []
args.append("-c")
args.append("open '/Users/xxxx/Desktop/myApp.app/Contents/MacOS/myApp'")
process.arguments = args
process.terminationHandler = { (process) in
print("\ndidFinish: \(!process.isRunning)")
}
do {
try process.run()
} catch {}
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
The strings after "-c" would depend on what you are trying to accomplish.

Why does my Swift code work in playground but not in the real cocoa app?

I'm currently trying to automate things in a macOS status bar application.
Now I had tried to make the Kerberos Login in a Process (previous called NSTask). In my playground, the code creates successfully the token. But when I move the code to the real app, it failed. I get this error message: "kinit: resolving credentials cache: malloc: out of memory"
Here is my code:
import Cocoa
// user credentials
let username = "user#example.com"
let password = "password"
// previous called NSTask
let process = Process()
// set process parameters
process.launchPath = "/bin/sh"
process.arguments = ["-c", "/bin/echo \(password) | /usr/bin/kinit --password-file=STDIN \(username)"]
// create a pipe to lauch process
let pipe = Pipe()
process.standardOutput = pipe
// launch process
process.launch()
// get outcome
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
print(output!)
I think that it is a problem with the Credential cache. When I enter the command "klist -A", I get the following error: "klist: krb5_cc_cache_get_first: Failed to open kcm init".
Does anybody know what can I do to get this code running?

xQuartz display not for shell script launched from Swift Process

I have a simple shell script which launches an X11 app. When I execute this shell script form my login shell / terminal xQuartz starts and I get a display. However the process doesn't get a display for xQuartz when running the script from within swift. Any idea how I can get the display?
Also what is the best way to detect if xQuartz is installed? Checking if xterm exists?
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/sh")
let startScriptURL = Bundle.main.url(forResource: "run", withExtension: "sh")
guard let startScriptPath = startScriptURL?.path else {
return
}
process.arguments = [startScriptPath]
do {
try process.run()
} catch let error {
print(error)
}
run.sh:
#!/bin/sh
/opt/X11/bin/xeyes
I figured our how to pass the DISPLAY environment or any environmental variable to Process.
The current environment can be obtained by:
ProcessInfo().environment
So I use now this:
let process = Process()
guard let envDisplay = ProcessInfo().environment["DISPLAY"] else {
print("Please install xQuartz")
return
}
process.environment = ["DISPLAY": envDisplay]
I got the idea from here: Issue launching X11 app via NSTask