How to run Terminal commands from cocoa app? - swift

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.

Related

Shell command via NSTask's Process delayed until my Vapor app quits

I built a Vapor 4 app that is currently deployed on a local Ubuntu 18 server VM, running behind NGINX and serving users without any issues.
Now I would like one of my web server routes to react to specific POSTs by executing a Bash command via Process (this, in order to send messages to a dedicated Slack channel via slack-cli, a tool I already use for other purposes and that is already configured and working both on my development machine and on the Ubuntu server).
With the following code, everything's working as desired when I run my Vapor app on my local machine (i.e.: immediately after the POST to the route the expected message appears in the Slack channel):
// What follows is placed inside my dedicated app.post route, after checking the response is valid...
let slackCLIPath = "/home/linuxbrew/.linuxbrew/bin/" // This is the slack-cli path on the Linux VM; I swap it with "/opt/homebrew/bin/" when running the app on my local Mac
_ = runShellScript("\(slackCLIPath)slack chat send '\(myMessageComingFromThePOST)' '#myChannel'")
// ...
// runShellScript() called above is the dedicated function (coming from [this SO answer](https://stackoverflow.com/a/43014767/3765705) that executes the Shell process, and its code follows:
func runShellScript(_ cmd: String) -> String? {
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/sh"
process.arguments = ["-c", String(format:"%#", cmd)]
process.standardOutput = pipe
let fileHandle = pipe.fileHandleForReading
process.launch()
return String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)
}
My issue is that, when I deploy my app on the Ubuntu server, both in debug and production, the execution of the Shell process does not happen like it did on my Mac: I have no errors logged by Vapor when I POST to that route, but no messages appear in Slack, even if I wait a while!
But here's the tricky part: as soon as I stop my Vapor app on the server, all messages are sent to Slack at once.
After a lot of testing (which obviously included confirming that from the server was possible to post without delays to Slack by using the exact same command passed to NSTask's Process class in my Vapor app), it appears like the Bash command is not executed until my Vapor app quits.
Clearly I'm missing something on how to make Process work "in realtime" with Vapor, and I'll be grateful for all the help I can get.
You need to wait until the task has finished. Looks like you're deadlocking yourself. This is how I run stuff on Linux:
// MARK: - Functions
#discardableResult
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
return try shell(args, returnStdOut: returnStdOut, stdIn: stdIn)
}
#discardableResult
func shell(_ args: [String], returnStdOut: Bool = false, stdIn: Pipe? = nil) throws -> (Int32, Pipe) {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
task.arguments = args
let pipe = Pipe()
if returnStdOut {
task.standardOutput = pipe
}
if let stdIn = stdIn {
task.standardInput = stdIn
}
try task.run()
task.waitUntilExit()
return (task.terminationStatus, pipe)
}
extension Pipe {
func string() -> String? {
let data = self.fileHandleForReading.readDataToEndOfFile()
let result: String?
if let string = String(data: data, encoding: String.Encoding.utf8) {
result = string
} else {
result = nil
}
return result
}
}
Important lines being starting it with try task.run() and waiting with task.waitUntilExit()

Running an UNIX program with swift /cocoa?

I have a problem starting a UNIX Programm (Csvmidi) from an cocoa app. It should convert a csv file to a midi file which actually works when i run it through the Terminal.
In Terminal it works fine by putting the URL of the UNIX file, the .csv file and .mid file in a row. Like this:
/Users/...../Csvmid </Users/.../test.csv> /Users/.../Melody.mid -> and automatically the Midi-File changes.
In Windows it worked with an easy code:
Process.Start("Csvmidi.exe", "test.csv Melody.mid");
In swift I tried to make it work with task.Process() but still it won't work. How can I make it work?
let csvmidiURL = "/Users/.../Csvmidi"
let process = Process()
process.executableURL = URL(fileURLWithPath: csvmidiURL)
process.arguments = [" <" , lblURL.stringValue , "> " , lblMidi.stringValue]
process.terminationHandler = { (process) in
print("\ndidFinish: \(!process.isRunning)")
}
do {
try process.run()
} catch {}
Thanks in advance!

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

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
}

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

Bash script called from native osx app produces the error "Operation not permitted"

What I'm Trying to do
Call a bash script from my native osx app that syncs a local directory on my mac to a remote server.
The bash script uses the following libraries:
fswatch
rsync
The Error messages on the app's console:
watch.sh: line 4: /usr/local/bin/rsync: Operation not permitted
watch.sh: line 6: /usr/local/bin/fswatch: Operation not permitted
What I have done
I have run the script outside the app in the terminal and it works so problem.
I have tried disabling and enabling System Integrity Protection with no effect on the errors.
I tried calling the Bash script from a python script, and it worked without any errors.
Code
OSX App Code
//////////////////////////////////////////
// FUNCTION - VIEW DID LOAD
override func viewDidLoad() {
super.viewDidLoad()
command(args: "sh","watch.sh")
} // END - FUNCTION
////////////////////////////////////////////
// FUNCTION - COMMAND
func command(args: String...) {
// GET SCRIPTS PATH
let scriptsDir = Bundle.main.resourceURL!.appendingPathComponent("scripts").path
// CREATE A PROCESS INSTANCE
let process = Process()
// SET THE PROCESS PARAMETERS
process.launchPath = "/usr/bin/env"
process.currentDirectoryPath = scriptsDir
process.arguments = args
// CREATE A PIPE AND MAKE THE PROCESS
// PUT ALL THE OUTPUT THERE
let pipe = Pipe()
process.standardOutput = pipe
// LAUNCH THE PROCESS
process.launch()
// GET THE DATA
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
print(output!)
} // END - FUNCTION
Bash Script
#!/bin/bash
/usr/local/bin/rsync --rsh="/usr/local/bin/sshpass -p ************* ssh -l username" -azP --delete "/path/to/local" username#111.111.111.111:/path/to/remote
/usr/local/bin/fswatch -o "/path/to/local" | while read f; do
/usr/local/bin/rsync --rsh="/usr/local/bin/sshpass -p ************* ssh -l username" -azP --delete "/path/to/local" username#111.111.111.111:/path/to/remote
done