I've got some code which monitors changes to my app's Documents directory which crashes under Swift 6. Stack looks like:
|#0|0x00000001054443f8 in _dispatch_assert_queue_fail ()|
|---|---|
|#1|0x0000000105444384 in dispatch_assert_queue ()|
|#2|0x0000000244f8a400 in swift_task_isCurrentExecutorImpl ()|
|#3|0x0000000104a75dd0 in closure #2 in Model.startMonitoring() ()|
|#4|0x0000000104a75cb8 in thunk for @escaping @callee_guaranteed () -> () ()|
Complete repro is the following. Just run and click the button.
import SwiftUI
import Observation
@Observable
class Model {
private var directoryFileDescriptor: CInt = -1
private var source: DispatchSourceFileSystemObject?
@MainActor
init() {
startMonitoring()
}
deinit {
stopMonitoring()
}
@MainActor
func startMonitoring() {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// Open the directory to get a file descriptor.
directoryFileDescriptor = open(documentsURL.path, O_EVTONLY)
guard directoryFileDescriptor >= 0 else {
print("Failed to open directory.")
return
}
// Create the dispatch source.
source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: directoryFileDescriptor,
eventMask: [.write, .delete, .rename],
queue: DispatchQueue.global()
)
// Set the event handler.
source?.setEventHandler { [weak self] in
Task { @MainActor in
guard let self = self else { return }
self.scan()
}
}
// Set the cancel handler to close the file descriptor.
source?.setCancelHandler {
// This callback will crash even without doing anything.
// #2 0x0000000244f8a400 in swift_task_isCurrentExecutorImpl ()
// print("cancel handler")
// Task { @MainActor in
// guard let self = self else {
// print("self is nil")
// return
// }
// close(self.directoryFileDescriptor)
// self.directoryFileDescriptor = -1
// }
}
// Start monitoring.
source?.resume()
}
func stopMonitoring() {
source?.cancel()
source = nil
}
func scan() {
print("scanning...")
}
}
struct ContentView: View {
@State var model: Model?
var body: some View {
VStack {
Button("Delete Model") {
model = nil
}
}
.padding()
.onAppear {
model = Model()
}
}
}
#Preview {
ContentView()
}
So I suppose using the DispatchSource
API, I've evaded the static concurrency checks and I'm hitting some dynamic check? I'm just not sure how I could use setCancelHandler
with Swift 6. Ultimately I need my scan
method, which triggers UI changes, to run on @MainActor
, hence the annotations.
Edit: For now I just made the model @unchecked Sendable
and used DispatchQueue.main.async
. Full code is here: FileBrowser/Sources/FileBrowser/FileBrowserModel.swift at main · audulus/FileBrowser · GitHub
thanks for any help!