The Adventures with NSTask & co. (Part 1)

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:

  1. NSNotification-based approach
  2. availableData approach
  3. “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

Navigation

2 thoughts on “The Adventures with NSTask & co. (Part 1)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s