cURL through NSTask not terminating if a pipe is present - swift

I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.

Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.

Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.

Related

Disable touch bar when taking a screen shot in macOS with Swift

I'm making a simple macOS screenshot to clipboard application with swift for a university project and, to perform the screenshots, I'm using the screencapture utility from macOS.
What I have is a function like this one:
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launchPath = "/bin/zsh"
task.arguments = ["-c", command]
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
And then I have a button that calls the function:
shell("screencapture -i -c")
The problem is that when the button is clicked, the screen capture utility is indeed called, however my mackbook pro touchbar is activated with the utility UI, which I don't want since there are options that the user is not supposed to have.
So, the question is: how can I disable the touchbar when the shell method is called?
Thank you!
You can get the process ID pid of the touchbar using
ps -e | grep "ControlStrip"
Then you can stop it temporarily using the command
kill -STOP pid
Then after that you can reactivate the touch ID using
kill -CONT pid
This technique won't turn off the touchbar but rather will freeze it (touch won't work). No action can be taken using touchbar unless it is resumed.
One liner to get process ID of touchbar in terminal:
pid=$(ps -e | grep "ControlStrip" | awk 'NR==1{print $1}');

Call Process with arguments in Swift macOS App

I'm trying to run a Process from my Swift macOS app. But, I'm having trouble formatting the arguments to my Process.
So far, the following code is working well:
let task = Process()
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = "/usr/local/bin"
task.environment = environment
task.currentDirectoryPath = "/usr/local/bin"
task.executableURL = URL(fileURLWithPath: "/usr/local/bin/convert")
task.arguments = [filename, filename+".pdf"]
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
...
try! task.run()
When I try and add the arguments "-trim", "-fuzz 10%" to the existing task.arguments the program crashes. The process error is:
New error: convert: unrecognized option `-fuzz 10%' # error/convert.c/ConvertImageCommand/1718.
How do I properly format these arguments to run the process?
When you run commands on the command line, your shell typically splits arguments to pass to a program by whitespace — when you run a command like
/usr/local/bin/convert -trim -fuzz 10% <input> <output>
the shell will split those arguments and pass them along to convert like so:
["/usr/local/bin/convert", "-trim", "-fuzz", "10%", "<input>", "<output>"]
When convert goes to parse the arguments you pass in, it iterates over those arguments and attempts to match them against a known list that it accepts. It looks up -trim, which it recognizes, and -fuzz, which it recognizes, and it accepts 10% as the argument to -fuzz... and so on.
When you execute a program directly from your code, whether through Process, or the lower level execve or posix_spawn, the arguments that you pass in are forwarded directly to the receiving process. In your original case, this means that convert received:
["/usr/local/bin/convert", "-trim", "-fuzz 10%", "<input>", "<output>"]
convert, and many other programs, don't further split up arguments on whitespace themselves, so when it tried to match the entirety of -fuzz 10% as a single command line option (space and all), it couldn't find anything.
This is equivalent to running
/usr/local/bin/convert -trim "-fuzz 10%" <input> <output>
on the command line, where the quotes prevent your shell from splitting words on whitespace. When I run that command locally on my machine, I get the exact same error:
convert: unrecognized option `-fuzz 10%' # error/convert.c/ConvertImageCommand/1718.
The solution is to do the same splitting your shell might do, and pass in each argument separately as its own string:
task.arguments = ["-trim", "-fuzz", "10%", filename, filename+".pdf"]

Using an process to run AppleScript codes with Arguments in Swift

To run AppleScript using processes in Swift is needed something like this
let process = Process()
if process.isRunning == false {
let pipe = Pipe()
process.launchPath = "/usr/bin/osascript"
process.arguments = ["/Users/user/Documents/activateApplication.scpt"]
process.standardError = pipe
process.launch()
}
but how, using processes I can send more arguments? like "Safari" and use this arguments in the destination script?
the activateApplication.scpt is:
on run appName
tell application appName to activate
end run
This is documented in the osascript man page. Pass the script arguments as additional arguments after the script file, and they will appear as a list in the first (and only) argument of your run handler. Your Swift code would look something like this:
process.arguments = ["/Users/user/Documents/activateApplication.scpt", "Safari"]
...and your script would look like this:
on run argv
tell app (item 1 of argv) to activate
end

NSTask process hangs reading from my pipe [duplicate]

I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.
Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.
Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.

Random hang by macOS task/process when specifying stderr pipe [duplicate]

I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.
Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.
Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.