DispatchSource crash under swift 6

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!

This is the same issue brought up here, and this is the explanation for why this is happening. Marking the closure passed to setCancelHandler as @Sendable will fix this issue.

2 Likes