The code below is a simplified form of part of my code for my Swift Package Manager, Swift 5.6.1, PromiseKit 6.22.1, macOS command-line executable.
It accepts a Mac App Store app ID as the sole argument. If the argument corresponds to an app ID for an app that was installed from the Mac App Store onto your computer, the executable obtains some information from Spotlight via a NSMetadataQuery
, then prints it to stdout.
I was only able to get the threading to work by calling RunLoop.main.run()
. The only way I was able to allow the executable to return instead of being stuck forever on RunLoop.main.run()
was to call exit(0)
in the closure passed to Promise.done()
.
The exit(0)
causes problems for testing. How can I allow the executable to exit without explicitly calling exit(0)
, and how can I improve the threading overall?
I cannot currently use Swift Concurrency (await/async/TaskGroup) because the executable must support macOS versions that don't support Swift Concurrency. A Swift Concurrency solution variant would be useful as additional info, though, because, sometime in the future, I might be able to drop support for macOS versions older than 10.15.
Thanks for any help.
import Foundation
import PromiseKit
guard CommandLine.arguments.count > 1 else {
print("Missing adamID argument")
exit(1)
}
guard let adamID = UInt64(CommandLine.arguments[1]) else {
print("adamID argument must be a UInt64")
exit(2)
}
_ = appInfo(forAdamID: adamID)
.done { appInfo in
if let jsonData = try? JSONSerialization.data(withJSONObject: appInfo),
let jsonString = String(data: jsonData, encoding: .utf8)
{
print(jsonString.replacingOccurrences(of: "\\/", with: "/"))
}
exit(0)
}
RunLoop.main.run()
func appInfo(forAdamID adamID: UInt64) -> Promise<[String: Any]> {
Promise { seal in
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "kMDItemAppStoreAdamID == %d", adamID)
query.searchScopes = ["/Applications"]
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSMetadataQueryDidFinishGathering,
object: query,
queue: .main
) { _ in
query.stop()
defer {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
var appInfo: [String: Any] = [:]
for result in query.results {
if let result = result as? NSMetadataItem {
var attributes = ["kMDItemPath"]
attributes.append(contentsOf: result.attributes)
for attribute in attributes {
let value = result.value(forAttribute: attribute)
switch value {
case let date as Date:
appInfo[attribute] = ISO8601DateFormatter().string(from: date)
default:
appInfo[attribute] = value
}
}
}
}
seal.fulfill(appInfo)
}
DispatchQueue.main.async {
query.start()
}
}
}
them works with RunLoop.main.run():
Stop doing this. Do NOT call "run" on "main". It's an inherently risk practice that creates unnecessary confusion about how the RunLoop system actually works. With both CFRunLoop and RunLoop, you should only run the current run loop.
Why does CFRunLoopRun(), but not RunLoop.main.run(), get stopped by CFRunLoopStop(…)?
Because of a subtle (but documented) detail of run's implementation. The RunLoop.run() documentation says:
Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
Now, compare that to the documentation of RunLoop.run(mode:before:):
Runs the loop once, blocking for input in the specified mode until a given date.
The word "Runs the loop once"
are the magic words. RunLoop.run(mode:before:) is the "core" API and, in fact, CFRunLoopStop works fine with it. RunLoop.run doesn't return because it's actual swift implementation would (literally) be:
while(RunLoop.current.run(mode: .default, before: Date.distantFuture)) {}
...which means CFRunLoopStop actually works as you'd expect, except for the fact that "run" immediately calls into run(mode:before:) again instead of returning.
In any case, here is a small sample that demonstrates exactly what's going on:
//
// main.swift
// RunLoopStopTest
//
// Created by Kevin Elliott on 10/18/24.
//
import Foundation
print("Run RunLoop Run!")
let myTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
print(" Ping...")
DispatchQueue.main.async {
print(" Pong...")
CFRunLoopStop(RunLoop.current.getCFRunLoop())
}
}
print("CFRunLoopRun Called")
CFRunLoopRun()
print("CFRunLoopRun returned\n")
print("RunLoop Called")
RunLoop.current.run(mode: .default, before: Date.distantFuture)
print("RunLoop returned\n")
while(RunLoop.current.run(mode: .default, before: Date.distantFuture)) {
print(" Ding!\n")
}
Which prints this when run:
Run RunLoop Run!
CFRunLoopRun Called
Ping...
Pong...
CFRunLoopRun returned
RunLoop Called
Ping...
Pong...
RunLoop returned
Ping...
Pong...
Ding!
Ping...
Pong...
Ding!
Ping...
Pong...
Ding!
....
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware