After almost a month of coding during sleepless nights my favourite side-project so far, @UpdatesXcode, I have uncovered a world of problems to myself.
At the time, I was rushing to finish all of the nice little state handling-related improvements, such as, error handling, or support for a proper blocking of user’s input when there’s an ongoing installation process is running behind the scenes.
I was supposed to finish XcodeUpdates right before #iOSDevHappyHour which happened on 19th of December, 2020 (shout out to @codeine_coding for such a great implementation of a simple idea of gathering people together!).
On my b-day last week I was rushing myself to finally hit that Archive button to see how the app would work under normal conditions. (Note: I should have done this long before…)
So I did! I’ve managed to finish everything I wanted prior to #iOSDevHappyHour which finally made my idealistic mind satisfied. But not for long…
The Problem
Once archived, I have launched the app, as a normal test case, to see how it behaves. Little did I know that my satisfaction would be brought downhill immediately after launching that app…
Technical Background
XcodeUpdates is an app, 99% of which is written in SwiftUI.
The “back end” of the SwiftUI code is a utility called “xcodes” developed by RobotsAndPencils (thank you Brandon for such an amazing tool written in Swift!).
`xcodes` on its own doesn’t provide as much functionality as I needed for XcodeUpdates to work.
So I have created a fork of `xcodes` where all of the required additional functionality would be developed.
`xcodes` is invoked by XcodeUpdates for every single operation. Every little button press is actually running `xcodes` behind the scenes.
A special version of `xcodes` is packed with the application as a signed binary.
A `Process` is created to run `xcodes` utility. Arguments are passed via my own implementation of data exchange mechanism between “user input” and CLI.
It works as with a normal back end, there is a “Request” that is being sent to the CLI and there’s a “Response” that is received after some very basic parsing of the output.
Running a Child Process from within the Cocoa/SwiftUI
In order to launch/run a child process, Foundation provides us with a few basic types: NSTask (Process), NSPipe (Pipe) and NSFileHandle (FileHandle).
Once created, a Process instance takes its arguments, gets configured with Pipe instance and then gets run.
let inputPipe = Pipe() let outputPipe = Pipe() let errorPipe = Pipe() let process = Process() process.arguments = ... process.standardInput = inputPipe.fileHandlerForWriting process.standardOutput = outputPipe.fileHandleForReading process.standardError = errorPipe.fileHandleForReading outputPipe..fileHandleForReading.readabilityHandler = { data in ... } try process.run() process.waitUntilExit()
There are multiple ways of how FileHandle can provide what a child process outputs:
- NSNotification-based approach
- availableData approach
- “handler” approach
FileHandle exposes an API called “readabilityHandler” which is “always” invoked when a file descriptor (that is encapsulated by FileHandle) has new data available.
In common case, a child process calls exit(0) after each invocation.
But in some rare cases, `xcodes` provides an output without calling the `exit()` API. And that’s when the weirdness begins…
References
- Source code for the fork of `xcodes` is available here: https://github.com/art-divin/xcodes
- Source code for XcodeUpdates is available here: https://github.com/art-divin/XcodeUpdates
- NSFileHandleDataAvailable: https://developer.apple.com/documentation/foundation/nsnotification/name/1415913-nsfilehandledataavailable
- availableData-based approach of reading all of the output from a child process: https://groups.google.com/g/des-moines-cocoaheads/c/IygQulT48VA
- @UpdatesXcodes at Twitter
Navigation
2 thoughts on “The Adventures with NSTask & co. (Part 1)”