mirror of
https://github.com/bitsycore/Allofit.git
synced 2026-10-05 12:27:26 +00:00
Fix - An open window with a path query (term containing "/") re-ran the search over the whole index on every FSEvents update, keeping several cores busy while files changed. Index-driven refilters are now throttled to one per second, skipped while the window is closed, minimized or covered, and replayed once when it becomes visible again. Typing stays instant. - Path terms no longer build and lowercase the full path of every record: the folded folder path is precomputed once per folder (interned like parentPath) and matched together with the folded name without allocating the joined string. Path-only queries are about 3x faster. Cleanup - Remove dead code: FSChange.isDir, FileWatcher.latestEventId, IndexState.record(atPath:), the never-set lastQuery preference, AccessManager.reset(), SearchField.onSubmit, an unreachable return. - Merge duplicates: the two Settings size formatters become Formatters.sizeOrDash, the shellQuote wrapper is replaced by AdminShell.quote. - Naming conventions: "in"-prefixed parameter names everywhere (delegate methods, binary helpers, initializers), camelCase impl helpers. - Comment every function and type; fix stale comments. - Debug and release builds are warning-free.
131 lines
4.3 KiB
Swift
131 lines
4.3 KiB
Swift
import Foundation
|
|
import CoreServices
|
|
|
|
// FSChange describes a single path reported by FSEvents along with its flag
|
|
// bits. The flags tell us whether the kernel coalesced events past its
|
|
// history limit, in which case we must rescan the affected subdirectory.
|
|
struct FSChange {
|
|
|
|
// absolute path that changed
|
|
let path: String
|
|
// raw FSEvents flag bits for this change
|
|
let flags: FSEventStreamEventFlags
|
|
// FSEvents id of this change, persisted so the next launch resumes after it
|
|
let eventId: FSEventStreamEventId
|
|
|
|
// true for the synthetic marker FSEvents sends once the replay of
|
|
// historical events (sinceWhen) is finished - not a real file change
|
|
var isHistoryDone: Bool {
|
|
return flags & FSEventStreamEventFlags(kFSEventStreamEventFlagHistoryDone) != 0
|
|
}
|
|
// true when the kernel asks the client to rescan inside this path
|
|
// because events were dropped (history too old or buffer overflow)
|
|
var mustScanSubDirs: Bool {
|
|
return flags & FSEventStreamEventFlags(kFSEventStreamEventFlagMustScanSubDirs) != 0
|
|
}
|
|
}
|
|
|
|
// FileWatcher streams real-time filesystem change notifications using FSEvents.
|
|
// FSEvents is the macOS analogue of the NTFS USN journal: the kernel records
|
|
// every change and we receive coalesced notifications batched by latency.
|
|
//
|
|
// The watcher can be started from a specific event id, which lets us "replay"
|
|
// every change that occurred while the app was closed - the same fast-resume
|
|
// trick Everything uses on Windows.
|
|
//
|
|
// @unchecked Sendable because all mutable state is guarded by the internal
|
|
// NSLock, and the C callback handles concurrency via that same lock.
|
|
final class FileWatcher: @unchecked Sendable {
|
|
|
|
// callback signature receiving an array of change descriptors
|
|
typealias ChangeHandler = ([FSChange]) -> Void
|
|
|
|
// the active FSEvents stream, nil when stopped
|
|
private var stream: FSEventStreamRef?
|
|
// the user-provided change handler, invoked from the FSEvents callback
|
|
private var handler: ChangeHandler?
|
|
// guards stream/handler against concurrent access
|
|
private let lock = NSLock()
|
|
|
|
// stops the stream so its callback never outlives the watcher
|
|
deinit {
|
|
stop()
|
|
}
|
|
|
|
// starts watching inRoots, delivering batches of changes to inHandler
|
|
// starting from inSinceWhen (defaults to "now")
|
|
func start(inRoots: [String],
|
|
inSinceWhen: FSEventStreamEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
|
|
inHandler: @escaping ChangeHandler) {
|
|
stop()
|
|
lock.lock()
|
|
handler = inHandler
|
|
lock.unlock()
|
|
|
|
var vContext = FSEventStreamContext(
|
|
version: 0,
|
|
info: Unmanaged.passUnretained(self).toOpaque(),
|
|
retain: nil,
|
|
release: nil,
|
|
copyDescription: nil
|
|
)
|
|
|
|
// C callback: forwards paths + flags back to the Swift handler
|
|
let vCallback: FSEventStreamCallback = { (_, info, numEvents, eventPaths, eventFlags, eventIds) in
|
|
guard let vInfo = info else { return }
|
|
let vWatcher = Unmanaged<FileWatcher>.fromOpaque(vInfo).takeUnretainedValue()
|
|
let vPathsArray = unsafeBitCast(eventPaths, to: NSArray.self)
|
|
var vChanges: [FSChange] = []
|
|
vChanges.reserveCapacity(numEvents)
|
|
for vI in 0..<numEvents {
|
|
guard let vPath = vPathsArray[vI] as? String else { continue }
|
|
vChanges.append(FSChange(path: vPath, flags: eventFlags[vI], eventId: eventIds[vI]))
|
|
}
|
|
vWatcher.lock.lock()
|
|
let vHandler = vWatcher.handler
|
|
vWatcher.lock.unlock()
|
|
vHandler?(vChanges)
|
|
}
|
|
|
|
// kFSEventStreamCreateFlagIgnoreSelf filters out events caused by
|
|
// this same process - keeps the indexer's own cache writes from
|
|
// showing up in its event stream as phantom file activity.
|
|
let vFlags = FSEventStreamCreateFlags(
|
|
kFSEventStreamCreateFlagFileEvents |
|
|
kFSEventStreamCreateFlagNoDefer |
|
|
kFSEventStreamCreateFlagUseCFTypes |
|
|
kFSEventStreamCreateFlagIgnoreSelf
|
|
)
|
|
|
|
guard let vStream = FSEventStreamCreate(
|
|
nil,
|
|
vCallback,
|
|
&vContext,
|
|
inRoots as CFArray,
|
|
inSinceWhen,
|
|
0.2,
|
|
vFlags
|
|
) else { return }
|
|
|
|
FSEventStreamSetDispatchQueue(vStream, DispatchQueue.global(qos: .utility))
|
|
FSEventStreamStart(vStream)
|
|
lock.lock()
|
|
stream = vStream
|
|
lock.unlock()
|
|
}
|
|
|
|
// stops the watcher and releases the underlying stream
|
|
func stop() {
|
|
lock.lock()
|
|
let vStream = stream
|
|
stream = nil
|
|
handler = nil
|
|
lock.unlock()
|
|
if let vRealStream = vStream {
|
|
FSEventStreamStop(vRealStream)
|
|
FSEventStreamInvalidate(vRealStream)
|
|
FSEventStreamRelease(vRealStream)
|
|
}
|
|
}
|
|
}
|