Skip to content

Instantly share code, notes, and snippets.

@palmin
Created June 26, 2024 13:33
Show Gist options
  • Save palmin/5ab085cd2fa02293a9ddc40bcbca4c6e to your computer and use it in GitHub Desktop.
Save palmin/5ab085cd2fa02293a9ddc40bcbca4c6e to your computer and use it in GitHub Desktop.
// check if folder contains file with given name after having performed
// file coordination
func fileExists(_ filename: String, in folder: URL) async throws -> Bool {
let found: Bool = try await withCheckedThrowingContinuation { cont in
let array = NSMutableArray()
@Sendable func complete(_ ok: Bool, _ error: Error?) {
objc_sync_enter(array)
let completed = array.count > 0
array.add(1)
objc_sync_exit(self)
if completed { return }
if let error = error {
cont.resume(throwing: error)
} else {
cont.resume(returning: ok)
}
}
// fire timer after timeout
let coordinator = NSFileCoordinator()
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
coordinator.cancel()
complete(false, TimeoutError())
}
let queue = OperationQueue()
coordinator.coordinate(with: [.readingIntent(with: folder)], queue: queue) { error in
do {
if let error = error {
throw error
}
let children = try FileManager.default.contentsOfDirectory(at: folder,
includingPropertiesForKeys: nil)
let filenames = children.map(\.lastPathComponent)
let found = filenames.contains(where: { $0 == filename })
complete(found, nil)
} catch {
complete(false, error)
}
}
}
return found
}
@mattmassicotte
Copy link

I've slightly modified this to compile. I'm not sure about how safe this actually is though. I think the life-cycle of the queue and coordinator are being extended by the (unsafe) capture in DispatchQueue.global().asyncAfter. And that could also cause the coordinator to be dealloced on a background thread, which make also be further unsafe.

@preconcurrency import Foundation

struct TimeoutError: Error {

}

class Completer: @unchecked Sendable {
    let array = NSMutableArray()
    let continuation: CheckedContinuation<Bool, any Error>

    init(continuation: CheckedContinuation<Bool, any Error>) {
        self.continuation = continuation
    }

    func complete(_ ok: Bool, _ error: Error?) {
        objc_sync_enter(array)
        let completed = array.count > 0
        array.add(1)
        objc_sync_exit(array) // self -> array?
        if completed { return }

        if let error = error {
            continuation.resume(throwing: error)
        } else {
            continuation.resume(returning: ok)
        }
    }
}

// check if folder contains file with given name after having performed
// file coordination
func fileExists(_ filename: String, in folder: URL) async throws -> Bool {
    let found: Bool = try await withCheckedThrowingContinuation { cont in
        let completer = Completer(continuation: cont)

        // fire timer after timeout
        let coordinator = NSFileCoordinator()
        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
            coordinator.cancel() // <- compiler catches this, and documenation explicitly says it is unsafe
            completer.complete(false, TimeoutError())
        }

        // does this actually work? I'm not sure the life-cycles of queue/coordinator are
        // being managed explicitly enough
        let queue = OperationQueue()
        coordinator.coordinate(with: [.readingIntent(with: folder)], queue: queue) { error in
            do {
                if let error = error {
                    throw error
                }

                let children = try FileManager.default.contentsOfDirectory(at: folder,
                                                                           includingPropertiesForKeys: nil)
                let filenames = children.map(\.lastPathComponent)
                let found = filenames.contains(where: { $0 == filename })
                completer.complete(found, nil)

            } catch {
                completer.complete(false, error)
            }
        }
    }
    return found
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment