During investment time,
I’ve been writing an iOS client to pass(1)
.
As part of the app’s onboarding, the user is asked to upload, via iTunes, their
PGP private key to the application’s documents directory.
I want the application to detect when this file is written to the device, then read the file and add its content to the keychain. Apple has helpfully published some code relating to watching for changes in the documents directory, which I happily implemented… leading to a runtime crash of my application. It seems the file was not ready for reading when I attempted to read it.
Here’s a class with the problematic code:
import Foundation
fileprivate let queue = DispatchQueue(label: "FileMonitorQueue", target: .main)
final class FileWatcher {
let fsSource: DispatchSourceFileSystemObject
var readSource: DispatchSourceRead! // explained later
init(in directory: URL, filename: String) {
let fileDescriptor = open(directory.path, O_EVTONLY)
fsSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileDescriptor,
eventMask: .write,
queue: queue
)
fsSource.setEventHandler {
/* do whatever you like here */
}
fsSource.setCancelHandler {
close(fileDescriptor)
}
fsSource.resume()
}
deinit {
fsSource.cancel()
}
}
(n.b. some error handling has been left out for brevity; see this gist for a more complete version)
The problem is that the blocked passed to setEventHandler
fires as soon as
writing begins, not when the write completes.
To remedy that, we can create a DispatchSourceRead
which has an event
handler that fires when a file becomes readable.
fsSource.setEventHandler {
do {
let fileHandle = try FileHandle(forReadingFrom: documentsDirectory.appendingPathComponent(filename))
self.readSource = DispatchSource.makeReadSource(fileDescriptor: fileHandle.fileDescriptor, queue: queue)
self.readSource.setEventHandler {
fileHandle.readToEndOfFileInBackgroundAndNotify()
}
self.readSource.resume()
self.readSource.setCancelHandler {
fileHandle.closeFile()
}
} catch {
/* error handling here */
}
}
Now we can listen to NSFileHandleReadToEndOfFileCompletion
using NotificationCenter
:
import Foundation
final class FileListener {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handler),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil
)
}
@objc func handler() {
/* file is available for reading here */
}
}
And voila: