native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
15 kB · 476 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477import CoreServicesimport Darwinimport Dispatchimport Foundation
@MainActorprotocol WorktreeFileEventMonitoring: AnyObject { func cancel()}
@MainActorprotocol WorktreeHeadEventMonitoring: AnyObject { func cancel()}
@MainActorprotocol WorktreeRegistryMonitoring: AnyObject { func cancel()}
@MainActorprotocol RemoteConfigMonitoring: AnyObject { func cancel()}
final class FSEventsWorktreeFileEventMonitor: WorktreeFileEventMonitoring { private let onEvent: @MainActor @Sendable () -> Void private var stream: FSEventStreamRef?
init?( rootURL: URL, onEvent: @escaping @MainActor @Sendable () -> Void ) { self.onEvent = onEvent let path = rootURL.path(percentEncoded: false) var context = FSEventStreamContext( version: 0, info: nil, retain: nil, release: nil, copyDescription: nil ) context.info = Unmanaged.passUnretained(self).toOpaque() let callback: FSEventStreamCallback = { _, callbackInfo, _, _, _, _ in guard let callbackInfo else { return } let monitor = Unmanaged<FSEventsWorktreeFileEventMonitor> .fromOpaque(callbackInfo) .takeUnretainedValue() Task { @MainActor in monitor.onEvent() } } stream = FSEventStreamCreate( nil, callback, &context, [path] as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 1.0, FSEventStreamCreateFlags( kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagWatchRoot ) ) guard let stream else { return nil } FSEventStreamSetDispatchQueue(stream, DispatchQueue.main) guard FSEventStreamStart(stream) else { FSEventStreamInvalidate(stream) FSEventStreamRelease(stream) self.stream = nil return nil } }
func cancel() { guard let stream else { return } FSEventStreamStop(stream) FSEventStreamInvalidate(stream) FSEventStreamRelease(stream) self.stream = nil }}
final class DispatchSourceWorktreeHeadEventMonitor: WorktreeHeadEventMonitoring { private let source: DispatchSourceFileSystemObject
init?( headURL: URL, onEvent: @escaping @MainActor @Sendable (DispatchSource.FileSystemEvent) -> Void ) { let path = headURL.path(percentEncoded: false) let fileDescriptor = open(path, O_EVTONLY) guard fileDescriptor >= 0 else { return nil } let queue = DispatchQueue(label: "worktree-head-event-monitor.\(path)") let source = DispatchSource.makeFileSystemObjectSource( fileDescriptor: fileDescriptor, eventMask: [.write, .rename, .delete, .attrib], queue: queue ) self.source = source source.setEventHandler { @Sendable [weak source] in guard let source else { return } let event = source.data Task { @MainActor in onEvent(event) } } source.setCancelHandler { @Sendable in close(fileDescriptor) } source.resume() }
func cancel() { source.cancel() }}
private enum GitCommonDirectory { static func url(for repositoryRootURL: URL, fileManager: FileManager) -> URL? { let repositoryRootURL = repositoryRootURL.standardizedFileURL let dotGitURL = repositoryRootURL.appending(path: ".git") var isDirectory = ObjCBool(false) if fileManager.fileExists(atPath: dotGitURL.path(percentEncoded: false), isDirectory: &isDirectory) { if isDirectory.boolValue { return dotGitURL.standardizedFileURL } guard let gitdirURL = gitdirURL(from: dotGitURL, relativeTo: repositoryRootURL) else { return nil } return commonDirectoryURL(from: gitdirURL) } if fileManager.fileExists(atPath: repositoryRootURL.appending(path: "HEAD").path(percentEncoded: false)), fileManager.fileExists(atPath: repositoryRootURL.appending(path: "config").path(percentEncoded: false)) { return repositoryRootURL } return nil }
static func isDirectory(_ url: URL, fileManager: FileManager) -> Bool { var isDirectory = ObjCBool(false) return fileManager.fileExists(atPath: url.path(percentEncoded: false), isDirectory: &isDirectory) && isDirectory.boolValue }
private static func gitdirURL(from dotGitURL: URL, relativeTo repositoryRootURL: URL) -> URL? { guard let contents = try? String(contentsOf: dotGitURL, encoding: .utf8), let line = contents.split(whereSeparator: \.isNewline).first else { return nil } let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) let prefix = "gitdir:" guard trimmed.hasPrefix(prefix) else { return nil } let pathPart = trimmed.dropFirst(prefix.count).trimmingCharacters(in: .whitespacesAndNewlines) guard !pathPart.isEmpty else { return nil } return URL(fileURLWithPath: String(pathPart), relativeTo: repositoryRootURL) .standardizedFileURL }
private static func commonDirectoryURL(from gitdirURL: URL) -> URL { let commondirURL = gitdirURL.appending(path: "commondir") guard let contents = try? String(contentsOf: commondirURL, encoding: .utf8), let line = contents.split(whereSeparator: \.isNewline).first else { return gitdirURL.standardizedFileURL } let pathPart = line.trimmingCharacters(in: .whitespacesAndNewlines) guard !pathPart.isEmpty else { return gitdirURL.standardizedFileURL } return URL(fileURLWithPath: pathPart, relativeTo: gitdirURL) .standardizedFileURL }}
@MainActorfinal class GitWorktreeRegistryMonitor: WorktreeRegistryMonitoring { private let worktreesDirectoryURL: URL private let onEvent: @MainActor @Sendable () -> Void private var commonDirectorySource: DispatchSourceFileSystemObject? private var worktreesDirectorySource: DispatchSourceFileSystemObject? private var registryEntrySources: [URL: DispatchSourceFileSystemObject] = [:] private var isWorktreesDirectoryPresent: Bool private var isCancelled = false
init?( repositoryRootURL: URL, onEvent: @escaping @MainActor @Sendable () -> Void, fileManager: FileManager = .default ) { guard let commonGitDirectoryURL = GitCommonDirectory.url(for: repositoryRootURL, fileManager: fileManager) else { return nil } worktreesDirectoryURL = commonGitDirectoryURL.appending(path: "worktrees") self.onEvent = onEvent isWorktreesDirectoryPresent = GitCommonDirectory.isDirectory(worktreesDirectoryURL, fileManager: fileManager) commonDirectorySource = Self.makeVnodeSource(url: commonGitDirectoryURL) { [weak self] in self?.handleCommonDirectoryEvent() } if isWorktreesDirectoryPresent { startWorktreesDirectorySourceIfNeeded() } guard commonDirectorySource != nil || worktreesDirectorySource != nil else { return nil } syncRegistryEntrySources() }
func cancel() { isCancelled = true commonDirectorySource?.cancel() worktreesDirectorySource?.cancel() commonDirectorySource = nil worktreesDirectorySource = nil cancelRegistryEntrySources() }
private func handleCommonDirectoryEvent() { guard !isCancelled else { return } let isPresent = GitCommonDirectory.isDirectory(worktreesDirectoryURL, fileManager: .default) let isWatchingCurrentDirectory = worktreesDirectorySource.map { Self.source($0, watches: worktreesDirectoryURL) } ?? false guard isPresent != isWorktreesDirectoryPresent || (isPresent && !isWatchingCurrentDirectory) else { return } isWorktreesDirectoryPresent = isPresent if isPresent { startWorktreesDirectorySourceIfNeeded() syncRegistryEntrySources() } else { worktreesDirectorySource?.cancel() worktreesDirectorySource = nil cancelRegistryEntrySources() } onEvent() }
private func handleWorktreesDirectoryEvent() { guard !isCancelled else { return } guard GitCommonDirectory.isDirectory(worktreesDirectoryURL, fileManager: .default) else { if isWorktreesDirectoryPresent { isWorktreesDirectoryPresent = false worktreesDirectorySource?.cancel() worktreesDirectorySource = nil cancelRegistryEntrySources() onEvent() } return } startWorktreesDirectorySourceIfNeeded() syncRegistryEntrySources() onEvent() }
// `git worktree move` rewrites `<commonGitDir>/worktrees/<name>/gitdir` in place and touches // nothing else: the registry entry keeps the name it was created under, and neither the common // directory nor `worktrees/` changes, so a vnode source on either stays silent. Watching each // entry's `gitdir` file is what makes a move visible; without it the move is only picked up by // the 30 s active-scene poll, and not at all while the app is in the background. private func handleRegistryEntryEvent() { guard !isCancelled else { return } syncRegistryEntrySources() onEvent() }
private func syncRegistryEntrySources() { let desiredURLs = Self.registryEntryURLs(inWorktreesDirectory: worktreesDirectoryURL, fileManager: .default) for (url, source) in registryEntrySources where !desiredURLs.contains(url) || !Self.source(source, watches: url) { source.cancel() registryEntrySources.removeValue(forKey: url) } for url in desiredURLs where registryEntrySources[url] == nil { registryEntrySources[url] = Self.makeVnodeSource(url: url) { [weak self] in self?.handleRegistryEntryEvent() } } }
// A vnode source follows the open inode, not the path. A replacement can keep // the same URL across coalesced events, so URL membership alone is not enough. private static func source(_ source: DispatchSourceFileSystemObject, watches url: URL) -> Bool { var watchedFile = stat() var currentFile = stat() guard fstat(source.handle, &watchedFile) == 0, stat(url.path(percentEncoded: false), ¤tFile) == 0 else { return false } return watchedFile.st_dev == currentFile.st_dev && watchedFile.st_ino == currentFile.st_ino }
private func cancelRegistryEntrySources() { for source in registryEntrySources.values { source.cancel() } registryEntrySources.removeAll() }
/// One watch target per registry entry: the entry's `gitdir` file when it exists, and otherwise /// the entry directory itself, so that a `gitdir` written moments after `git worktree add` /// created the directory still produces an event and promotes the watch to the file. static func registryEntryURLs( inWorktreesDirectory worktreesDirectoryURL: URL, fileManager: FileManager ) -> Set<URL> { guard let entries = try? fileManager.contentsOfDirectory( at: worktreesDirectoryURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] ) else { return [] } var urls: Set<URL> = [] for entry in entries where GitCommonDirectory.isDirectory(entry, fileManager: fileManager) { let gitdirURL = entry.appending(path: "gitdir").standardizedFileURL if fileManager.fileExists(atPath: gitdirURL.path(percentEncoded: false)) { urls.insert(gitdirURL) } else { urls.insert(entry.standardizedFileURL) } } return urls }
private func startWorktreesDirectorySourceIfNeeded() { if let source = worktreesDirectorySource { guard !Self.source(source, watches: worktreesDirectoryURL) else { return } source.cancel() } worktreesDirectorySource = Self.makeVnodeSource(url: worktreesDirectoryURL) { [weak self] in self?.handleWorktreesDirectoryEvent() } }
private static func makeVnodeSource( url: URL, onEvent: @escaping @MainActor @Sendable () -> Void ) -> DispatchSourceFileSystemObject? { let fileDescriptor = open(url.path(percentEncoded: false), O_EVTONLY) guard fileDescriptor >= 0 else { return nil } let source = DispatchSource.makeFileSystemObjectSource( fileDescriptor: fileDescriptor, eventMask: [.write, .rename, .delete, .attrib], queue: .main ) source.setEventHandler { onEvent() } source.setCancelHandler { close(fileDescriptor) } source.resume() return source }}
@MainActorfinal class GitRemoteConfigMonitor: RemoteConfigMonitoring { private let configURL: URL private let onEvent: @MainActor @Sendable () -> Void private var commonDirectorySource: DispatchSourceFileSystemObject? private var configSource: DispatchSourceFileSystemObject? private var configFingerprint: Data?
init?( repositoryRootURL: URL, onEvent: @escaping @MainActor @Sendable () -> Void, fileManager: FileManager = .default ) { guard let commonGitDirectoryURL = GitCommonDirectory.url(for: repositoryRootURL, fileManager: fileManager) else { return nil } configURL = commonGitDirectoryURL.appending(path: "config") self.onEvent = onEvent configFingerprint = Self.configFingerprint(configURL) commonDirectorySource = Self.makeFileSource( url: commonGitDirectoryURL, eventMask: [.write, .rename, .delete, .attrib] ) { [weak self] in self?.handleCommonDirectoryEvent() } startConfigSourceIfNeeded() guard commonDirectorySource != nil || configSource != nil else { return nil } }
func cancel() { commonDirectorySource?.cancel() configSource?.cancel() commonDirectorySource = nil configSource = nil }
private func handleCommonDirectoryEvent() { restartConfigSource() emitIfConfigChanged() }
private func handleConfigEvent() { emitIfConfigChanged() restartConfigSource() }
private func startConfigSourceIfNeeded() { guard configSource == nil else { return } configSource = Self.makeFileSource(url: configURL, eventMask: [.write, .rename, .delete, .attrib]) { [weak self] in self?.handleConfigEvent() } }
private func restartConfigSource() { configSource?.cancel() configSource = nil startConfigSourceIfNeeded() }
private func emitIfConfigChanged() { let latestFingerprint = Self.configFingerprint(configURL) guard latestFingerprint != configFingerprint else { return } configFingerprint = latestFingerprint onEvent() }
private static func configFingerprint(_ url: URL) -> Data? { try? Data(contentsOf: url) }
private static func makeFileSource( url: URL, eventMask: DispatchSource.FileSystemEvent, onEvent: @escaping @MainActor @Sendable () -> Void ) -> DispatchSourceFileSystemObject? { let fileDescriptor = open(url.path(percentEncoded: false), O_EVTONLY) guard fileDescriptor >= 0 else { return nil } let source = DispatchSource.makeFileSystemObjectSource( fileDescriptor: fileDescriptor, eventMask: eventMask, queue: .main ) source.setEventHandler { onEvent() } source.setCancelHandler { close(fileDescriptor) } source.resume() return source }}