Created
June 19, 2025 12:49
-
-
Save adam-zethraeus/1c34a8319c94578909905ddcbe2ae73d to your computer and use it in GitHub Desktop.
DirectoryChangesAsyncSequence
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Foundation | |
| import Dispatch | |
| import CoreServices | |
| import Foundation | |
| import os | |
| /// An AsyncSequence which emits when a file or folder is changed. | |
| public final class DirectoryChanges: AsyncSequence { | |
| public enum Failure: Error { | |
| case InvalidFileURL | |
| case DirectoryMissing | |
| case NotDirectory | |
| case FailedToOpen | |
| } | |
| public func makeAsyncIterator() -> AsyncThrowingStream<Element, any Error>.Iterator { | |
| builder().makeAsyncIterator() | |
| } | |
| public typealias AsyncIterator = AsyncThrowingStream<Element, any Error>.Iterator | |
| public typealias Element = () | |
| private let builder: () -> AsyncThrowingStream<Element, any Error> | |
| public init(_ url: URL) { | |
| builder = { | |
| let (stream, cont) = AsyncThrowingStream<Element, any Error>.makeStream() | |
| let fileURL = url.standardizedFileURL | |
| var objCBool: ObjCBool = false | |
| guard url.isFileURL else { | |
| cont.finish(throwing: Failure.InvalidFileURL) | |
| return stream | |
| } | |
| guard FileManager.default.fileExists(atPath: fileURL.path(), isDirectory: &objCBool) else { | |
| cont.finish(throwing: Failure.DirectoryMissing) | |
| return stream | |
| } | |
| guard objCBool.boolValue == true else { | |
| cont.finish(throwing: Failure.NotDirectory) | |
| return stream | |
| } | |
| let it = DirectoryWatcher() | |
| cont.onTermination = { _ in | |
| it.stop() | |
| } | |
| let didWatch = it.watch(path: url.path) { | |
| cont.yield() | |
| } | |
| guard didWatch else { | |
| cont.finish(throwing: Failure.FailedToOpen) | |
| return stream | |
| } | |
| return stream | |
| } | |
| } | |
| } | |
| import Foundation | |
| public final class DirectoryWatcher { | |
| public init(){} | |
| deinit { | |
| stop() | |
| } | |
| public typealias Callback = () -> Void | |
| private var dirFD : Int32 = -1 { | |
| didSet { | |
| if oldValue != -1 { | |
| close(oldValue) | |
| } | |
| } | |
| } | |
| private var dispatchSource : DispatchSourceFileSystemObject? | |
| public func watch(path: String, callback: @escaping Callback) -> Bool { | |
| dirFD = open(path, O_EVTONLY) | |
| if dirFD < 0 { | |
| return false | |
| } | |
| let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: dirFD, eventMask: .write, queue: DispatchQueue.main) | |
| dispatchSource.setEventHandler { | |
| callback() | |
| } | |
| dispatchSource.setCancelHandler { [weak self] in | |
| self?.dirFD = -1 | |
| } | |
| self.dispatchSource = dispatchSource | |
| dispatchSource.resume() | |
| return true | |
| } | |
| public func stop() { | |
| guard let dispatchSource = dispatchSource else { | |
| return | |
| } | |
| dispatchSource.setEventHandler(handler: nil) | |
| dispatchSource.cancel() | |
| self.dispatchSource = nil | |
| } | |
| } | |
| for try await _ in DirectoryChanges(URL(filePath: "/Users/adamz/Developer/ambience/mobile-ios")) { | |
| print("change") | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment