How to catch error when setting launchpath in NSTask - swift

I am trying to run a commandline tool using swift 2 on a mac (10.10):
let task = NSTask()
task.launchPath = "/path/to/wrong/binary"
task.launch()
// NSPipe() stuff to catch output
task.waitUntilExit()
// never reached
if (task.terminationStatus != 0){
NSLog("uh oh")
}
Since the path is wrong, my program dies with launch path not accessible. However, I don't know, how to catch this error. Using do { try } catch {} around task.launch() does not work, because it does not throw an exception, looking at the terminationStatus is also not working, since it is never reached.
How can I catch a wrong launchPath?
Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81)
Target: x86_64-apple-darwin14.5.0

In Mac OS X High Sierra, launch() is deprecated.
You would use run() instead:
let process = Process()
// ...
do {
process.run()
} catch let error as NSError {
print(error.localizedDescription)
// ...
}
Also have a look at the Apple Dev Docs

unfortunately, there is no chance to catch runtime exceptions. with try / catch you can recovery from trowing error, not from runtime exception. you can create you own exception handler, but you are still not able to recover from it. try to lunch from NSTask some common shell with command as a parameter and next use a pipe to return os errors to you own code.
import Foundation
let task = Process()
let pipe = Pipe()
task.launchPath = "/bin/bash"
task.arguments = ["-c","unknown"]
task.standardOutput = pipe
task.launch()
let handle = pipe.fileHandleForReading
let data = handle.readDataToEndOfFile()
let dataString = String(data: data, encoding: .utf8)
print(dataString ?? "")
will print
/bin/bash: unknown: command not found

You're not supposed to catch it. Your computer, the local environment that you app is running in, is deemed as a reliable medium. Therefore, NSTask is designed to expect that any path you feed it is guaranteed to exist. This holds up fine as folks are usually using NSTask to launch standard system executables (bash, diff, rsync, etc...). The crash on an mistyped path is meant to help you find bugs in your code easily. If you're launching your own custom-built binaries embedded in the app's bundle, you should really be using XPC instead.
If you absolutely need to use NSTask to launch unreliable paths, before you create your object, you need to validate the path manually using NSFileManager's fileExistsAtPath(_:isDirectory:) and react accordingly. If even the POSIX permissions flags are unreliable (at which point, your app likely has some serious security problems; if this is a consumer application project, I recommend you rethink your design), you'll also want to check the file's NSFilePosixPermissions attribute to make sure it's executable.

Related

Swift Process with Psuedo Terminal (PTY)

I'm looking for some insight and guidance on using the Foundation.Process type with a PTY (Psuedo Terminal) so that the subprocess can accept input and behave as if it was running via a terminal.
The reason for needing a PTY is that for programs like ssh or in my case (xcodes) which ask for user input including passwords, running these via Foundation.Process does not display the prompts to the user as the output is usually buffered (this works fine in the Xcode debugger console but when running via a real terminal that is buffered the prompts are never displayed in the terminal)
Looking at other threads it seems like correct approach here is create a PTY and use the filehandles to attach to the Process.
While I've got this to work to the point where prompts are now shown, I cant seem to figure out how to pass input back to the process as these are being controlled by the PTY.
Here is my Process setup:
let process = Process()
// Setup the process with path, args, etc...
// Setup the PTY handles
var parentDescriptor: Int32 = 0
var childDescriptor: Int32 = 0
guard Darwin.openpty(&parentDescriptor, &childDescriptor, nil, nil, nil) != -1 else {
  fatalError("Failed to spawn PTY")
}
parentHandle = FileHandle(fileDescriptor: parentDescriptor, closeOnDealloc: true)
childHandle = FileHandle(fileDescriptor: childDescriptor, closeOnDealloc: true)
process.standardInput = childHandle
process.standardOutput = childHandle
process.standardError = childHandle
With this setup I then read the parent handle and output any result it gets (such as the input prompts):
parentHandle?.readabilityHandler = { handle in
  guard let line = String(data: handle.availableData, encoding: .utf8), !line.isEmpty else {
    return
  }
  logger.notice("\(line)")
}
When process.run() is executed the program runs and I can see it asks for Apple ID: input in my terminal, however, when typing input into the terminal the process does not seem to react to this input.
I've tried forwarding the FileHandle.standardInput:
FileHandle.standardInput.readabilityHandler = { handle in
  parentHandle?.write(handle.availableData)
}
But this doesn't seem to work either.
What is the recommended way to setup a PTY with Foundation.Process for executing arbitrary programs and having them behave as if they were being run in a terminal context?
Most of the resources I found online are about other languages and I'd like to stick with Foundation.Process vs. doing anything custom in C/C++ if possible as it just makes it easier to reason about / maintain. The resources for Swift on this topic are very lacking and I've checked out some open source projects that claim to do this but most require manually sending input to the PTY handle vs. accepting them from the user in a terminal.
Any insight / help is very much appreciated!

Calling nettop from swift: "Unable to allocate a kernel control socket"

I am writing a macOS GUI wrapper for the command-line nettop utility.
When I call nettop -P -L 1 from the Terminal app through zsh, it displays proper output, but when I launch the process from within my Swift app, it displays the following in my app:
nettop[19213:2351456] [NetworkStatistics] Unable to allocate a kernel control socket nettop: NStatManagerCreate failed
Is this some sort of permissions or sandboxing issue, and if so, how do I get my app to request the proper permissions?
Code snippet:
func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}
...
struct ContentView: View {
var body: some View {
Text(shell(launchPath: "/usr/bin/nettop", arguments: ["-P", "-L", "1"]).0 ?? "No Output")
}
}
This is, indeed, a sandbox issue: nettop operates by creating a com.apple.network.statistics PF_SYSTEM/SYSPROTO_CONTROL socket (an example of the source can be seen at http://newosxbook.com/code/listings/17-1-lsock.c), which can be sandboxed.
Access to this socket is restricted by two layers:
The com.apple.private.network.statistics entitlement is enforced when net.statistics_privcheck sysctl is set to 1. That's not your case, since you're execing nettop, which has the entitlement anyway
The sandbox profile prevents the creation of system sockets unless explicitly allowed by a
(allow network-outbound
(control-name "com.apple.network.statistics")
(control-name "com.apple.netsrc"))
rule.
It seems that the latter case is what happens in your case, though from your detail it's not clear if the fault is in your own app's sandbox profile or the exec'ed nettop. To determine this, try to create the socket yourself - if you can do that, then it's the exec's problem. If you can't, it's your own profile. It's possible to create your own sandbox profiles, but that's something Apple won't ever allow publicly and will probably get you kicked out of the App Store.
Another test - run your app directly from the command line. (that is, in a terminal, then /Applications/path/to/your.app/Contents/MacOS/yourApp) . This way, it is not launched by xpcproxy, and will suffer from less sandboxing constraints.

Get full list of running processes (including those owned by root) with Swift

I am developing an app for monitoring the highest consuming processes in Swift, but I'm stuck at the part of obtaining the list of processes that are currently running. I've tried a lot of things, such as:
Running the top or ps aux | less commands and parsing the output.
I tried using this code to run the top command and pass the output to a NSPipe in order to parse it later, but I can't seem to run the command because it gives the error Couldn't posix_spawn: error 13, and I couldn't find anything on the internet on how to fix this, so I had to find another way.
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.launchPath = "/usr/bin"
task.arguments = ["top"]
task.launch()
task.waitUntilExit()
let data = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
Using NSWorkspace.shared.runningApplications
I saw this stack overflow question regarding the same topic, but it isn't answered (one comment references another thread that answers how to do it in C, but it isn't what I actually expected). The thread's OP used the code below in order to get the full list of running processes, but it only returns the user-owned ones, so it isn't really useful.
let workspace = NSWorkspace.shared
let applications = workspace.runningApplications
for application in applications {
if let url = (application.executableURL?.absoluteString) {
os_log("%{public}s", log:scribe, type:.debug, url)
}
}
}
Conclusion
Is there a way I can get a list of running processes in macOS (including those owned by root) in Swift? If there's another way through which I could retrieve at least the two most CPU-consuming processes that would do as well.
Thanks in advance.

I can't use Process in a function

I have a problem I would like to use the function Process() but when I call it in a function I get
Use of unresolved identifier 'Process'
But when I call it directly in my code it works.
Someone knows why?
As you neither mentioned which Platform you are targeting to nor provided any kind of code, you'll need to take note of the following things:
Process() was renamed to CommandLine() in Swift 3.0
If you are targeting iOS, CommandLine() isn't available, so you'll need to do the following:
Using NSTask() on iOS:
The best way to achieve this, or at least the one I prefer the most (also I didn't find any other way to do this), is to use a custom Objective-C header file which creates the object NSTask() and everything it needs.
Then, in order to use this code with Swift, you'll need to create a Bridging-Header, in which you'll need to import the NSTask.h for it to be exposed to Swift and being able to use it in your Swift code.
Once done this, just use the following function in your code whenever you want to run a task:
func task(launchPath: String, arguments: String...) -> NSString {
let task = NSTask.init()
task?.setLaunchPath(launchPath)
task?.arguments = arguments
// Create a Pipe and make the task
// put all the output there
let pipe = Pipe()
task?.standardOutput = pipe
// Launch the task
task?.launch()
task?.waitUntilExit()
// Get the data
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return output!
}
And call it like this:
task(launchPath: "/usr/bin/echo", arguments: "Hello World")
This will also return the value, so you can even display it by doing:
print(task(launchPath: "/usr/bin/echo", arguments: "Hello, World!"))
Which will print:
~> Hello, World!
For this to work and not throwing an NSInternalInconsistencyException, you'll need to set the launchPath to the executable's full path instead to just the directory containing it.
You'll also need to set all the command arguments separated by commas.
Tested on both iPad Mini 2 (iOS 12.1 ~> Jailbroken) and iPhone Xr (iOS 12.2 ~> not jailbroken).
NOTE: Even though this works both on non-jailbroken and jailbroken devices, your App will be rejected on the AppStore, as #ClausJørgensen said:
You're using private APIs, so it'll be rejected on the App Store. Also, Xcode 11 has some new functionality that will trigger a build failure when using certain private APIs.
I'd only recommend the usage of this for apps that won't be uploaded to the App Store, otherwise, try to achieve what you want without using commands, there sure will be any other way to do that.
If your app is targeting jailbroken iOS devices and will be uploaded to a third-party store like Cydia, Zebra, Thunderbolt or Sileo, then this would work correctly.

Repeat Process() n times in Swift 3

I am making a macOS app. I have a task (a script) that I am running with Process() in Swift 3. When I press a button (button.State == NSOnState), I would like the task to repeating n times, and terminate earlier until the button is pressed again (button.State == NSOffState).
I looked up how to repeat a task, and it looks possible with a simple for loop – for i in {1..n}.
Now, the problem I am having is that it doesn't seem to be possible to call a task multiple times. When I try to call the task the second time, I get an error in the console:
[General] task already launched
Here is my code:
#IBAction func buttonPressed(_ sender: Any) {
let script = "for i in {1..5}; do echo \"hi\"; done; sleep 1"
let task = Process()
task.terminationHandler = self.commandTerminationHandler
task.launchPath = "/bin/bash"
task.arguments = ["-c", script]
if button.state == NSOnState {
task.launch() // launches task
task.waitUntilExit() // waits until task has been completed (about 1 second)
task.terminate() // (should) terminate the task. (The console error occurs with or without this line)
task.launch() // tries launching the task again, but this results in the console error.
print("The task was launched twice")
} else {
// task.terminate()
}
}
I googled this error, and found [this][https://forums.macrumors.com/threads/nstask-not-terminating.1617855/#post-17677541]:
The error isn't that the task is still running. It's that the task has already run and completed, and can't be started again. You'll need to create a new NSTask object to run the task again.
So I need to make a new NSTask (or Process as of Swift 3) and keep making new ones to repeat the code forever. This sound very complicated (as if I'm using a workaround) and is probably inefficient.
Is there a better way to repeat a Process in Swift 3?
For the sake of completeness, I'd also like to mention that I considered using for i in {1..n} do ... done directly in script. This has one problem:
It doesn't look like it's possible to stop the task when the button is pressed again. This is because if I run task.terminate(), I get the error "task not launched." The only way I can stop it is by running killall bash in my Terminal, which doesn't seem like a nice solution. To do this in Xcode, I'd need make a Process to kill bash with bash... which is strange.
I'm having the same issue. A potential fix that I've come across online is to set the task to nil after terminating it, then reassigning parameters such as path, arguments, etc before running it again. However this can only be done in Objective C; Swift has very strict rules when it comes to nil (and probably for good reasons).
You can run 2 different script at the same time. One of them you want to run should be running and the other always check the another script at every turn and this should be like recursive. The another script will kill the another script until the button is pressed.
If you set your task item to a var instead of a let you can repeatedly set task to a new process object inside your loop.
var task = Process()
task.launchPath = ....
task.arguments = [...]
task = Process()
task.launchPath = ....
task.arguements = [...]