mirror of
https://github.com/xtool-org/xtool.git
synced 2026-06-24 12:21:34 +02:00
3bebb76013
Discriminate with module name Fully working Changes Fix linux changes allow customizing xload path Lint
1040 lines
45 KiB
Swift
1040 lines
45 KiB
Swift
// swiftlint:disable all
|
|
|
|
import Dependencies
|
|
import XUtils
|
|
|
|
struct FileSystemMonitor: Sendable {
|
|
var watch: @Sendable (_ directory: FilePath) async throws -> FileSystemEvents
|
|
}
|
|
|
|
struct FileSystemChangeEvent: Sendable, Hashable {
|
|
let file: FilePath
|
|
}
|
|
|
|
extension FileSystemMonitor: TestDependencyKey {
|
|
static let testValue = FileSystemMonitor(
|
|
watch: unimplemented(),
|
|
)
|
|
}
|
|
|
|
extension DependencyValues {
|
|
var fileSystemMonitor: FileSystemMonitor {
|
|
get { self[FileSystemMonitor.self] }
|
|
set { self[FileSystemMonitor.self] = newValue }
|
|
}
|
|
}
|
|
|
|
struct FileSystemEvents: AsyncSequence, Sendable {
|
|
private let makeIterator: @Sendable () -> AsyncStream<FileSystemChangeEvent>.AsyncIterator
|
|
|
|
init(
|
|
makeAsyncIterator: @Sendable @escaping () -> AsyncStream<FileSystemChangeEvent>.AsyncIterator
|
|
) {
|
|
self.makeIterator = makeAsyncIterator
|
|
}
|
|
|
|
func makeAsyncIterator() -> AsyncStream<FileSystemChangeEvent>.AsyncIterator {
|
|
makeIterator()
|
|
}
|
|
}
|
|
|
|
#if os(macOS)
|
|
|
|
// https://github.com/jgvanwyk/SwiftFileSystemEvents
|
|
|
|
import Foundation
|
|
import CoreServices.FSEvents
|
|
|
|
extension FileSystemMonitor: DependencyKey {
|
|
static let liveValue = FileSystemMonitor { directory in
|
|
guard let url = URL(filePath: directory) else {
|
|
throw Console.Error("Could not start FS monitor: bad file path: \(directory)")
|
|
}
|
|
let (events, cont) = AsyncStream<FileSystemChangeEvent>.makeStream()
|
|
let stream = FileSystemEventStream(
|
|
directoriesToWatch: [url],
|
|
flags: .fileEvents,
|
|
handler: {
|
|
guard let file = FilePath($0.url) else { return }
|
|
cont.yield(.init(file: file))
|
|
}
|
|
)
|
|
let queue = DispatchQueue(label: "fsevents-queue")
|
|
stream.setDispatchQueue(queue)
|
|
try stream.start()
|
|
cont.onTermination = { _ in
|
|
stream.invalidate()
|
|
}
|
|
let onDeinit = OnDeinit { cont.finish() }
|
|
return FileSystemEvents {
|
|
_ = onDeinit
|
|
return events.makeAsyncIterator()
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class OnDeinit: Sendable {
|
|
let perform: @Sendable () -> Void
|
|
init(perform: @Sendable @escaping () -> Void) {
|
|
self.perform = perform
|
|
}
|
|
deinit { perform() }
|
|
}
|
|
|
|
// MARK: FileSystemEventStream
|
|
|
|
/// Register for a stream of notifications of file system events in a list of directories.
|
|
final class FileSystemEventStream: @unchecked Sendable {
|
|
|
|
private var streamRef: FSEventStreamRef! // Will be non-nil after initialisation completes.
|
|
private let handler: (FileSystemEvent) -> Void
|
|
|
|
/// Creates a new file system event stream with the given parameters.
|
|
///
|
|
/// This calls `FSEventStreamCreate(_:_:_:_:_:_:_:)`.
|
|
///
|
|
/// - Parameters:
|
|
/// - directoriesToWatch: An array of URLs representing the directories you wish to
|
|
/// monitor.
|
|
/// - sinceWhen: The service will supply events that have happened after the given
|
|
/// event ID. To ask for events since now pass ``FileSystemEvent/ID-swift.struct/now``.
|
|
/// Defaults to ``FileSystemEvent/ID-swift.struct/now``.
|
|
/// - latency: The number of seconds the service should wait after hearing about an
|
|
/// event from the kernel before passing it to the handler. Specifying a larger
|
|
/// value may result in more effective temporal coalescing, resulting in fewer
|
|
/// callbacks and greater overall efficiency. Defaults to 0.
|
|
/// - flags: Flags that modify the behaviour of the stream being created. See
|
|
/// ``FileSystemEventStream/Flags``. Defaults to `[]`.
|
|
/// - handler: A block that will be called on each event that occurs in the
|
|
/// directories being monitored.
|
|
@available(macOS 10.5, *)
|
|
init(directoriesToWatch: [URL],
|
|
sinceWhen: FileSystemEvent.ID = .now,
|
|
latency: TimeInterval = 0,
|
|
flags: Flags = [],
|
|
handler: @escaping (FileSystemEvent) -> Void) {
|
|
self.handler = handler
|
|
let pathsToWatch: CFArray
|
|
if #available(macOS 13.0, *) {
|
|
pathsToWatch = directoriesToWatch.map { $0.path(percentEncoded: false) } as CFArray
|
|
} else {
|
|
pathsToWatch = directoriesToWatch.map { $0.path } as CFArray
|
|
}
|
|
// We pass an unmanaged pointer to `self` as context info to the stream.
|
|
// `FileSystemEventStream.callback` uses this to call `handler` with each event.
|
|
// As the memory for `self` is managed by Swift, we pass `nil` for both `retain`
|
|
// and `release`.
|
|
var context = FSEventStreamContext(version: 0,
|
|
info: Unmanaged.passUnretained(self).toOpaque(),
|
|
retain: nil,
|
|
release: nil,
|
|
copyDescription: nil)
|
|
// While the return value of `FSEventStreamCreate` is imported in Swift as
|
|
// `FSEventStreamRef?`, the documentation for `FSEventStreamCreate` asserts that
|
|
// its return value will always be a valid `FSEventStreamRef`, so we unwrap the
|
|
// return value here.
|
|
self.streamRef = FSEventStreamCreate(kCFAllocatorDefault,
|
|
Self.callback,
|
|
&context,
|
|
pathsToWatch,
|
|
sinceWhen.rawValue,
|
|
latency,
|
|
flags.rawValue)!
|
|
}
|
|
|
|
deinit {
|
|
FSEventStreamRelease(streamRef)
|
|
}
|
|
|
|
private static let callback: FSEventStreamCallback = { _, info, numEvents, eventPaths, eventFlags, eventIDs in
|
|
guard let info = info else { return }
|
|
let eventPaths = eventPaths.assumingMemoryBound(to: UnsafeMutablePointer<CChar>.self)
|
|
let stream = Unmanaged<FileSystemEventStream>.fromOpaque(info).takeUnretainedValue()
|
|
for index in 0..<numEvents {
|
|
let url = URL(fileURLWithFileSystemRepresentation: eventPaths[index],
|
|
isDirectory: true,
|
|
relativeTo: nil)
|
|
let flags = FileSystemEvent.Flags(rawValue: eventFlags[index])
|
|
let id = FileSystemEvent.ID(rawValue: eventIDs[index])
|
|
let event = FileSystemEvent(url: url, id: id, flags: flags)
|
|
stream.handler(event)
|
|
}
|
|
}
|
|
|
|
/// Fetches the `sinceWhen` property of the stream.
|
|
///
|
|
/// Upon receiving an event (and just before invoking the client's callback) this
|
|
/// attribute is updated to the highest-numbered event ID mentioned in the event.
|
|
///
|
|
/// This calls `FSEventStreamGetLatestEventId`.
|
|
var latestEventID: FileSystemEvent.ID {
|
|
FileSystemEvent.ID(rawValue: FSEventStreamGetLatestEventId(streamRef))
|
|
}
|
|
|
|
/// Fetches the directories supplied to the stream.
|
|
///
|
|
/// This calls `FSEventStreamCopyPathsBeingWatched`.
|
|
var directoriesBeingWatched: [URL] {
|
|
// `FSEventStreamCopyPathsBeingWatched` returns a `CFArray` of `CFStringRef`, which
|
|
// can always be converted to `[String]`.
|
|
let paths = FSEventStreamCopyPathsBeingWatched(streamRef) as! [String]
|
|
let urls: [URL]
|
|
if #available(macOS 13.0, *) {
|
|
urls = paths.map { URL(filePath: $0, directoryHint: .isDirectory) }
|
|
} else {
|
|
urls = paths.map { URL(fileURLWithPath: $0, isDirectory: true) }
|
|
}
|
|
return urls
|
|
}
|
|
|
|
/// Schedules the stream on the specified dispatch queue.
|
|
///
|
|
/// The caller is responsible for ensuring that the stream is scheduled on a dispatch
|
|
/// queue and that the queue is started. If there is a problem scheduling the stream
|
|
/// on the queue an error will be returned when you try to start the stream. To start
|
|
/// receiving events on the stream, call ``FileSystemEventStream/start()``. To remove
|
|
/// the stream from the queue on which it was scheduled, call
|
|
/// ``FileSystemEventStream/setDispatchQueue(_:)`` with a `nil` queue parameter or
|
|
/// call ``FileSystemEventStream/invalidate()`` which will do the same thing.
|
|
///
|
|
/// Note: you must eventually call ``FileSystemEventStream/invalidate()``, and it is
|
|
/// an error to call ``FileSystemEventStream/invalidate()`` without having the stream
|
|
/// either scheduled on a dispatch queue, so do not set the dispatch queue to `nil`
|
|
/// before calling ``FileSystemEventStream/invalidate()``.
|
|
///
|
|
/// This calls `FSEventStreamSetDispatchQueue(_:,_:)`.
|
|
///
|
|
/// - Parameters:
|
|
/// - dispatchQueue: The dispatch queue to use to receive events (or `nil` to stop
|
|
/// receiving events from the stream).
|
|
@available(macOS 10.6, *)
|
|
func setDispatchQueue(_ dispatchQueue: DispatchQueue?) {
|
|
FSEventStreamSetDispatchQueue(streamRef, dispatchQueue)
|
|
}
|
|
|
|
/// Invalidate the stream.
|
|
///
|
|
/// The stream will be unscheduled on any dispatch queue on which it has been scheduled.
|
|
/// This may only be called if the stream has been scheduled on a dispatch queue with
|
|
/// ``FileSystemEventStream/setDispatchQueue(_:)``.
|
|
///
|
|
/// This calls `FSEventStreamInvalidate(_:)`.
|
|
@available(macOS 10.5, *)
|
|
func invalidate() {
|
|
FSEventStreamInvalidate(streamRef)
|
|
}
|
|
|
|
/// Start the stream.
|
|
///
|
|
/// Attempts to register with the File System Events service to receive events per the
|
|
/// parameters in the stream. This can only be called once the stream has been
|
|
/// scheduled on a dispatch queue. Once started, the stream can be stopped with
|
|
/// ``FileSystemEventStream/stop()``.
|
|
///
|
|
/// This ought to always succeed, but if it does not, you should have appropriate
|
|
/// fallback in place.
|
|
///
|
|
/// This calls `FSEventStreamStart(_:)`.
|
|
///
|
|
/// - Throws:
|
|
/// - ``Error/couldNotStartStream`` if the stream could not be started.
|
|
@available(macOS 10.5, *)
|
|
func start() throws {
|
|
guard FSEventStreamStart(streamRef) else { throw Error.couldNotStartStream }
|
|
}
|
|
|
|
/// Asks the File System Events service to flush out any events that have occurred but
|
|
/// have not yet been delivered.
|
|
///
|
|
/// Events may be delayed due to the latency parameter that was supplied when the stream
|
|
/// was created. This flushing occurs asynchronously -- do not expect the events to have
|
|
/// already been delivered by the time this call returns.
|
|
///
|
|
/// This may only be called after you have started the stream with ``start()``.
|
|
///
|
|
/// This calls `FSEventStreamFlushAsync(_:)`.
|
|
///
|
|
/// - Returns: The largest event ID of any event ever queued for this stream, otherwise
|
|
/// zero if no events have been queued for this stream.
|
|
@available(macOS 10.5, *)
|
|
func flushAsync() -> FileSystemEvent.ID {
|
|
FileSystemEvent.ID(rawValue: FSEventStreamFlushAsync(streamRef))
|
|
}
|
|
|
|
/// Asks the File System Events service to flush out any events that have occurred
|
|
/// but have not yet been delivered.
|
|
///
|
|
/// Events may be delayed due to the latency parameter that was supplied when the stream
|
|
/// was created. This flushing occurs synchronously -- by the time this call returns,
|
|
/// your handler will have been invoked for every event that had already/ occurred at
|
|
/// the time you made this call.
|
|
///
|
|
/// This may only be called after you have started the stream with ``start()``.
|
|
///
|
|
/// This calls `FSEventStreamFlushSync(_:)`.
|
|
@available(macOS 10.5, *)
|
|
func flushSync() {
|
|
FSEventStreamFlushSync(streamRef)
|
|
}
|
|
|
|
/// Unregisters with the File System Events service.
|
|
///
|
|
/// Your handler will not be called for this stream while it is stopped. This can only
|
|
/// be called if the stream has been started via ``FileSystemEventStream/start()``.
|
|
/// Once stopped, the stream can be restarted via ``FileSystemEventStream/start()``, at
|
|
/// which point it will resume receiving events from where it left off ("sinceWhen").
|
|
///
|
|
/// This calls `FSEventStreamStop(_:)`.
|
|
@available(macOS 10.5, *)
|
|
func stop() {
|
|
FSEventStreamStop(streamRef)
|
|
}
|
|
|
|
/// Prints a description of the supplied stream to stderr.
|
|
///
|
|
/// For debugging only.
|
|
///
|
|
/// This calls `FSEventStreamShow()`.
|
|
@available(macOS 10.5, *)
|
|
func show() {
|
|
FSEventStreamShow(streamRef)
|
|
}
|
|
|
|
/// Sets directories to be filtered from the event stream.
|
|
///
|
|
/// A maximum of eight directories may be specified.
|
|
///
|
|
/// This calls `FSEventStreamSetExclusionPaths(_:,_:)`.
|
|
@available(macOS 10.9, *)
|
|
func setExclusionDirectories(_ directoryURLs: [URL]) throws {
|
|
let paths: CFArray
|
|
if #available(macOS 13.0, *) {
|
|
paths = directoryURLs.map { $0.path(percentEncoded: false) } as CFArray
|
|
} else {
|
|
paths = directoryURLs.map { $0.path } as CFArray
|
|
}
|
|
guard FSEventStreamSetExclusionPaths(streamRef, paths) else { throw Error.couldNotExcludeDirectories }
|
|
}
|
|
|
|
/// Errors that may be thrown by ``FileSystemEventStream`` methods.
|
|
enum Error: Swift.Error {
|
|
/// Thrown by ``FileSystemEventStream/start()`` if the stream could not be
|
|
/// started.
|
|
case couldNotStartStream
|
|
case couldNotExcludeDirectories
|
|
}
|
|
|
|
/// Flags that can be passed to the file system event stream to modify its behaviour.
|
|
///
|
|
/// This wraps `FSEventStreamCreateFlags`.
|
|
struct Flags: OptionSet, Sendable {
|
|
let rawValue: FSEventStreamCreateFlags
|
|
|
|
init(rawValue: FSEventStreamCreateFlags) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
/// The default.
|
|
///
|
|
/// This wraps `kFSEventStreamCreateFlagNone`.
|
|
static let none = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone))
|
|
|
|
// static let useCFTypes = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagUseCFTypes))
|
|
|
|
/// Change the meaning of the latency parameter.
|
|
///
|
|
/// If you specify this flag and more than latency seconds have elapsed since the
|
|
/// last event, your app will receive the event immediately. The delivery of the
|
|
/// event resets the latency timer and any further events will be delivered after
|
|
/// latency seconds have elapsed. This flag is useful for apps that are interactive
|
|
/// and want to react immediately to changes but avoid getting swamped by
|
|
/// notifications when changes are occurringin rapid succession. If you do not
|
|
/// specify this flag, then when an event occurs after a period of no events, the
|
|
/// latency timer is started. Any events that occur during the next latency seconds
|
|
/// will be delivered as one group (including that first event). The delivery of the
|
|
/// group of events resets the latency timer and any further events will be
|
|
/// delivered after latency seconds. This is the default behavior and is more
|
|
/// appropriate for background, daemon or batch processing apps.
|
|
static let noDefer = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagNoDefer))
|
|
|
|
/// Request notifications of changes along the path to the directory or directories
|
|
/// being watched.
|
|
///
|
|
/// For example, with this flag, if you watch `/foo/bar` and it is renamed to
|
|
/// `/foo/bar.old`, you would receive a RootChanged event. The same is true if the
|
|
/// directory `/foo` were renamed. The event you receive is a special event: the URL
|
|
/// for the event is the original URL you specified, the flag
|
|
/// `FileSystemEvent.Flags.rootChanged` is set, and the event ID `FileSystemEvent.ID`
|
|
/// is zero. RootChanged events are useful to indicate that you should rescan a
|
|
/// particular hierarchy because it changed completely (as opposed to the things
|
|
/// inside of it changing). If you want to track the current location of a directory,
|
|
/// it is best to open the directory before creating the stream so that you have a
|
|
/// file descriptor for it and can issue an `F_GETPATH` `fcntl()` to find the current
|
|
/// path.
|
|
static let watchRoot = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagWatchRoot))
|
|
|
|
/// Do not send events that were triggered by the current process.
|
|
///
|
|
/// This is useful for reducing the volume of events that are sent. It is only
|
|
/// useful if your process might modify the file system hierarchy beneath the
|
|
/// path or paths being monitored. This has no effect on historical events, i.e.,
|
|
/// those delivered before the HistoryDone sentinel event. Also, this does not apply
|
|
/// to RootChanged events because the WatchRoot feature uses a separate mechanism
|
|
/// that is unable to provide information about the responsible process.
|
|
@available(macOS 10.6, *)
|
|
static let ignoreSelf = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagIgnoreSelf))
|
|
|
|
/// Request file-level notifications.
|
|
///
|
|
/// Your stream will receive events about individual files in the hierarchy you are
|
|
/// watching instead of only receiving directory level notifications. Use this flag
|
|
/// with care as it will generate significantly more events than without it.
|
|
@available(macOS 10.7, *)
|
|
static let fileEvents = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents))
|
|
|
|
/// Tag events that were triggered by the current process with the "OwnEvent" flag.
|
|
///
|
|
/// This is only useful if your process might modify the file system hierarchy
|
|
/// beneath the path(s) being monitored and you wish to know which events were
|
|
/// triggered by your process. Note: this has no effect on historical events, i.e.,
|
|
/// those delivered before the HistoryDone sentinel event.
|
|
@available(macOS 10.9, *)
|
|
static let markSelf = Self.init(rawValue: FSEventStreamCreateFlags(kFSEventStreamCreateFlagMarkSelf))
|
|
|
|
// @available(macOS 10.13, *)
|
|
// static let useExtendedData = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamCreateFlagUseExtendedData))
|
|
|
|
/// Reguest full event history.
|
|
///
|
|
/// When requesting historical events it is possible that some events may get
|
|
/// skipped due to the way they are stored. With this flag all historical events
|
|
/// in a given chunk are returned even if their event ID is less than the
|
|
/// `sinceWhen` ID. Put another way, deliver all the events in the first chunk of
|
|
/// historical events that contains the `sinceWhen` ID so that none are skipped even
|
|
/// if their id is less than the `sinceWhen` ID. This overlap avoids any issue with
|
|
/// missing events that happened at/near the time of an unclean restart of the
|
|
/// client process.
|
|
@available(macOS 10.15, *)
|
|
static let fullHistory = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamCreateFlagFullHistory))
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: FileSystemEvent
|
|
|
|
/// A file system event.
|
|
///
|
|
/// Whenever an event occurs in a directory being watched by
|
|
/// ``FileSystemEventStream``, the handler passed to the stream is called with a
|
|
/// ``FileSystemEvent`` encapsulating the event.
|
|
struct FileSystemEvent: Hashable, Sendable {
|
|
|
|
/// The URL of the directory in which the event occured.
|
|
let url: URL
|
|
|
|
|
|
/// The ID for the event.
|
|
let id: ID
|
|
|
|
/// Flags set for the event.
|
|
///
|
|
/// If no flags are set, then there was some change in the directory in which
|
|
/// the event occured.
|
|
let flags: Flags
|
|
|
|
/// The ID of a file system event.
|
|
///
|
|
/// This wraps `FSEventStreamID`. Each file system event has a unique ID. Event IDs
|
|
/// all come from a single global source. They are monotonically increasing per
|
|
/// system, even across reboots and drives coming and going. An event ID may be
|
|
/// passed as the `sinceWhen` parameter to
|
|
/// ``FileSystemEventStream/init(directoriesToWatch:sinceWhen:latency:flags:handler:)``
|
|
/// to register the stream for notifications of all events after the event with the
|
|
/// given ID.
|
|
///
|
|
/// `FSEventStreamID` is just a `UInt64`, so integer wrapping may occur. See
|
|
/// ``Flags-swift.struct/eventIdsWrapped``.
|
|
struct ID: RawRepresentable, Hashable, Comparable, Sendable {
|
|
let rawValue: FSEventStreamEventId
|
|
|
|
static func < (lhs: FileSystemEvent.ID, rhs: FileSystemEvent.ID) -> Bool {
|
|
lhs.rawValue < rhs.rawValue
|
|
}
|
|
|
|
init(rawValue: FSEventStreamEventId) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
static let zero = Self.init(rawValue: 0)
|
|
|
|
/// A special event ID that may be passed as the `sinceWhen` parameter to
|
|
/// ``FileSystemEventStream/init(directoriesToWatch:sinceWhen:latency:flags:handler:)``
|
|
/// in order to receive notifications of all events "since now".
|
|
static let now = Self.init(rawValue: FSEventStreamEventId(kFSEventStreamEventIdSinceNow))
|
|
|
|
/// The most recently generated event ID.
|
|
///
|
|
/// This fetches the most recently generated event ID, system-wide. By the time the ID is
|
|
/// fetched, you have already received events with newer IDs.
|
|
static var current: Self {
|
|
Self.init(rawValue: FSEventsGetCurrentEventId())
|
|
}
|
|
}
|
|
|
|
/// Possible flags for a file system event.
|
|
///
|
|
/// This wraps `FSEventStreamEventFlags`.
|
|
struct Flags: OptionSet, Hashable, Sendable {
|
|
let rawValue: FSEventStreamEventFlags
|
|
|
|
init(rawValue: FSEventStreamEventFlags) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
/// There was some change in the directory at the specific URL supplied in this event.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagNone`.
|
|
static let none = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagNone))
|
|
|
|
/// Your application must rescan not just the directory given in the event, but all
|
|
/// its children, recursively.
|
|
///
|
|
/// This can happen if there was a problem whereby events were coalesced
|
|
/// hierarchically. For example, an event in `/Users/jsmith/Music` and an event in
|
|
/// `/Users/jsmith/Pictures` might be coalesced into an event with this flag set
|
|
/// and path `/Users/jsmith`. If this flag is set you may be able to get an idea of
|
|
/// whether the bottleneck happened in the kernel (less likely) or in your client
|
|
/// (more likely) by checking for the presence of the informational flags
|
|
/// `userDropped` or `kernelDropped`.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagMustScanSubDirs`.
|
|
static let mustScanSubDirs = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagMustScanSubDirs))
|
|
|
|
/// A problem occured in buffering the event in user space.
|
|
///
|
|
/// See ``mustScanSubDirs``.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagUserDropped`.
|
|
static let userDropped = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagUserDropped))
|
|
|
|
/// A problem occured in buffering the event in kernel space.
|
|
///
|
|
/// See ``mustScanSubDirs``.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagKernelDropped`.
|
|
static let kernelDropped = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagKernelDropped))
|
|
|
|
/// The 64-bit event ID counter wrapped around.
|
|
///
|
|
/// If this flag is present, previously-issued event ID's are no longer valid
|
|
/// values for the `sinceWhen` parameter to
|
|
/// ``FileSystemEventStream/init(directoriesToWatch:sinceWhen:latency:flags:handler:)``.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagEventIdsWrapped`.
|
|
static let eventIdsWrapped = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagEventIdsWrapped))
|
|
|
|
/// Marks a sentinel event sent to mark the end of the historical events.
|
|
///
|
|
/// If a ``FileSystemEvent/ID-swift.struct`` was passed as the `sinceWhen` parameter
|
|
/// to the call to
|
|
/// ``FileSystemEventStream/init(directoriesToWatch:sinceWhen:latency:flags:handler:)``
|
|
/// that created this stream, and this value was not
|
|
/// ``FileSystemEvent/ID-swift.struct/now``, then the handler will be called with
|
|
/// each event before `now` (the "historial events"). Once this is finised, the
|
|
/// handler will be invoked with an event (the "history sentinel event") with this
|
|
/// flag set. The URL provided with this event should be ignored.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagHistoryDone`.
|
|
static let historyDone = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagHistoryDone))
|
|
|
|
/// Marks a special event sent when there is a change to one of the directories
|
|
/// along the path to one of the directories you asked to watch.
|
|
///
|
|
/// When this flag is set, the event ID is zero and the path corresponds to one of
|
|
/// the paths you asked to watch (specifically, the one that changed). The path may
|
|
/// no longer exist because it or one of its parents was deleted or renamed. Events
|
|
/// with this flag set will only be sent if you passed the
|
|
/// ``FileSystemEventStream/Flags/watchRoot`` when creating the stream with
|
|
/// ``FileSystemEventStream/init(directoriesToWatch:sinceWhen:latency:flags:handler:)``.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagRootChanged`.
|
|
static let rootChanged = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagRootChanged))
|
|
|
|
/// Marks a special event sent when a volume is mounted underneath one of the paths
|
|
/// being monitored.
|
|
/// The `URL` represents the path to the newly-mounted volume. You will receive
|
|
/// one of these notifications for every volume mount event inside the kernel
|
|
/// (independent of DiskArbitration).
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagMount`.
|
|
static let mount = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagMount))
|
|
|
|
/// Marks a special event sent when a volume is unmounted underneath one of the
|
|
/// paths being monitored.
|
|
///
|
|
/// The path in the event is the path to the directory from which the volume was
|
|
/// unmounted. You will receive one of these notifications for every volume unmount
|
|
/// event inside the kernel.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagUnmount`.
|
|
static let unmount = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagUnmount))
|
|
|
|
/// A file system object was created at the specific URL supplied in this event.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemCreated`.
|
|
@available(macOS 10.7, *)
|
|
static let itemCreated = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated))
|
|
|
|
/// A file system object was removed at the specific URL supplied in this event.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemRemoved`.
|
|
@available(macOS 10.7, *)
|
|
static let itemRemoved = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved))
|
|
|
|
/// A file system object at the specific URL supplied in this event had its metadata modified.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemInodeMetaMod`.
|
|
@available(macOS 10.7, *)
|
|
static let itemInodeMetaMod = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemInodeMetaMod))
|
|
|
|
/// A file system object was renamed at the specific URL supplied in this event.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemRenamed`.
|
|
@available(macOS 10.7, *)
|
|
static let itemRenamed = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemRenamed))
|
|
|
|
/// A file system object at the specific URL supplied in this event had its data modified.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemModified`.
|
|
@available(macOS 10.7, *)
|
|
static let itemModified = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemModified))
|
|
|
|
/// A file system object at the specific URL supplied in this event had its
|
|
/// FinderInfo data modified.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemFinderInfoMod`.
|
|
@available(macOS 10.7, *)
|
|
static let itemFinderInfoMod = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemFinderInfoMod))
|
|
|
|
/// A file system object at the specific URL supplied in this event had its
|
|
/// ownership changed.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemChangeOwner`.
|
|
@available(macOS 10.7, *)
|
|
static let itemChangeOwner = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemChangeOwner))
|
|
|
|
/// A file system object at the specific URL supplied in this event had its
|
|
/// extended attributes modified.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemXattrMod`.
|
|
@available(macOS 10.7, *)
|
|
static let itemXattrMod = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemXattrMod))
|
|
|
|
/// The file system object at the specific URL supplied in this event is a regular file.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemIsFile`.
|
|
@available(macOS 10.7, *)
|
|
static let itemIsFile = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsFile))
|
|
|
|
/// The file system object at the specific URL supplied in this event is a directory.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemIsDir`.
|
|
@available(macOS 10.7, *)
|
|
static let itemIsDir = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsDir))
|
|
|
|
/// The file system object at the specific URL supplied in this event is a symbolic link.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemIsSymlink`.
|
|
@available(macOS 10.7, *)
|
|
static let itemIsSymlink = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsSymlink))
|
|
|
|
/// Indicates the event was triggered by the current process.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/markSelf``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagOwnEvent`.
|
|
@available(macOS 10.9, *)
|
|
static let ownEvent = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagOwnEvent))
|
|
|
|
/// The file system object at the specific URL supplied in this event is a hard link.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemIsHardlink`.
|
|
@available(macOS 10.10, *)
|
|
static let itemIsHardlink = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsHardlink))
|
|
|
|
/// The file system object at the specific URL supplied in this event was the last hard link.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemIsLastHardlink`.
|
|
@available(macOS 10.10, *)
|
|
static let itemIsLastHardlink = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsLastHardlink))
|
|
|
|
/// The file system object at the specific path supplied in this event is a clone or was cloned.
|
|
///
|
|
/// This flag is only ever set if you specified the ``FileSystemEventStream/Flags/fileEvents``
|
|
/// flag when creating the stream.
|
|
///
|
|
/// This wraps `kFSEventStreamEventFlagItemCloned`.
|
|
@available(macOS 10.13, *)
|
|
static let itemCloned = Self.init(rawValue: FSEventStreamEventFlags(kFSEventStreamEventFlagItemCloned))
|
|
}
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
import Foundation
|
|
import XKit
|
|
import CXKit
|
|
import SystemPackage
|
|
|
|
// https://github.com/sersoft-gmbh/swift-inotify
|
|
|
|
extension FileSystemMonitor {
|
|
static let liveValue = FileSystemMonitor { directory in
|
|
let notifier = try Inotifier()
|
|
let events = try await notifier.events(for: directory)
|
|
.compactMap { $0.path.map { FileSystemChangeEvent(file: $0) } }
|
|
.eraseToStream()
|
|
return FileSystemEvents {
|
|
_ = notifier
|
|
return events.makeAsyncIterator()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The notifier object.
|
|
final actor Inotifier {
|
|
/// An asynchronous sequence of events for a certain file path.
|
|
struct PathEvents: AsyncSequence, Sendable {
|
|
@usableFromInline
|
|
let stream: AsyncStream<InotifyEvent>
|
|
|
|
@usableFromInline
|
|
init(stream: AsyncStream<InotifyEvent>) {
|
|
self.stream = stream
|
|
}
|
|
|
|
@inlinable
|
|
func makeAsyncIterator() -> AsyncStream<InotifyEvent>.AsyncIterator {
|
|
stream.makeAsyncIterator()
|
|
}
|
|
}
|
|
|
|
private let fileDescriptor: FileDescriptor
|
|
private var streamTask: Task<Void, Never>?
|
|
private var watches = Dictionary<CInt, Dictionary<UUID, AsyncStream<InotifyEvent>.Continuation>>()
|
|
|
|
/// Creates a new instance.
|
|
init() throws {
|
|
guard case let fd = inotify_init1(0), fd != -1 else { throw Errno(rawValue: errno) }
|
|
fileDescriptor = .init(rawValue: fd)
|
|
}
|
|
|
|
deinit {
|
|
streamTask?.cancel()
|
|
streamTask = nil
|
|
try? fileDescriptor.close()
|
|
}
|
|
|
|
/// Closes this inotify instance. All further calls to this instance will fail.
|
|
func close() throws {
|
|
stopStreaming()
|
|
try fileDescriptor.close()
|
|
}
|
|
|
|
/// Returns the asynchronous events sequence for the given file path.
|
|
/// - Parameters:
|
|
/// - filePath: The file path to watch.
|
|
/// - Returns: The asynchronous sequence of events for the given file path.
|
|
func events(for filePath: FilePath) throws -> PathEvents {
|
|
let wd = filePath.withCString {
|
|
inotify_add_watch(fileDescriptor.rawValue, $0, cin_all_events)
|
|
}
|
|
guard wd != -1 else { throw Errno(rawValue: errno) }
|
|
if streamTask == nil {
|
|
startStreaming()
|
|
}
|
|
let stream = AsyncStream<InotifyEvent> { continuation in
|
|
let sequenceID = UUID()
|
|
watches[wd, default: [:]][sequenceID] = continuation
|
|
continuation.onTermination = { [weak self] _ in
|
|
Task { [weak self] in
|
|
try await self?.removeWatch(forDescriptor: wd, sequenceID: sequenceID)
|
|
}
|
|
}
|
|
}
|
|
return PathEvents(stream: stream)
|
|
}
|
|
|
|
private func startStreaming(restart: Bool = false) {
|
|
assert(restart || streamTask == nil)
|
|
if restart {
|
|
streamTask?.cancel()
|
|
}
|
|
streamTask = Task.detached { [fileDescriptor, weak self] in
|
|
do {
|
|
for try await event in FileStream<inotify_event>(fileDescriptor: fileDescriptor) {
|
|
guard !Task.isCancelled, let self else { return }
|
|
await self.handle(event)
|
|
}
|
|
} catch is CancellationError {
|
|
} catch {
|
|
print("[INOTIFY] Error: \(error)")
|
|
print("[INOTIFY] Restarting stream...")
|
|
await self?.startStreaming(restart: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handle(_ cEvent: inotify_event) {
|
|
guard var watchesToNotify = watches[cEvent.wd] else { return }
|
|
defer {
|
|
if watchesToNotify.isEmpty {
|
|
watches.removeValue(forKey: cEvent.wd)
|
|
} else {
|
|
watches[cEvent.wd] = watchesToNotify
|
|
}
|
|
}
|
|
// FIXME: Deal with connected events using `event.cookie`.
|
|
let event = InotifyEvent(cEvent: cEvent)
|
|
for (watchID, continuation) in watchesToNotify {
|
|
if case .terminated = continuation.yield(event) {
|
|
watchesToNotify.removeValue(forKey: watchID)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stopStreaming() {
|
|
streamTask?.cancel()
|
|
streamTask = nil
|
|
}
|
|
|
|
private func removeWatch(forDescriptor wd: CInt, sequenceID: UUID) throws {
|
|
let status = inotify_rm_watch(fileDescriptor.rawValue, wd)
|
|
guard status != -1 else { throw Errno(rawValue: errno) }
|
|
guard var watchSequences = watches[wd] else { return }
|
|
watchSequences.removeValue(forKey: sequenceID)
|
|
guard watchSequences.isEmpty else { return }
|
|
watches.removeValue(forKey: wd)
|
|
guard watches.isEmpty else { return }
|
|
stopStreaming()
|
|
}
|
|
}
|
|
|
|
/// An event sent by inotify.
|
|
struct InotifyEvent: Equatable, Sendable {
|
|
/// The file path of the event. If nil, the event is not for a file inside of the watch.
|
|
let path: FilePath?
|
|
/// The flags of the event.
|
|
let flags: Flags
|
|
|
|
init(cEvent event: inotify_event) {
|
|
path = withUnsafePointer(to: event) {
|
|
cin_event_name($0).map { FilePath(platformString: $0) }
|
|
}
|
|
flags = .init(rawValue: event.mask)
|
|
}
|
|
}
|
|
|
|
extension InotifyEvent {
|
|
/// A set of flags that can be set on an event.
|
|
struct Flags: OptionSet, Hashable, Sendable {
|
|
typealias RawValue = UInt32
|
|
|
|
let rawValue: RawValue
|
|
|
|
init(rawValue: RawValue) {
|
|
self.rawValue = rawValue
|
|
}
|
|
}
|
|
}
|
|
|
|
extension InotifyEvent.Flags {
|
|
/// File was accessed.
|
|
static let accessed = InotifyEvent.Flags(rawValue: numericCast(IN_ACCESS))
|
|
/// File was modified.
|
|
static let modified = InotifyEvent.Flags(rawValue: numericCast(IN_MODIFY))
|
|
/// Metadata changed.
|
|
static let attributesChanged = InotifyEvent.Flags(rawValue: numericCast(IN_ATTRIB))
|
|
/// A writeable file was closed.
|
|
static let writableFileClosed = InotifyEvent.Flags(rawValue: numericCast(IN_CLOSE_WRITE))
|
|
/// A non-writable file was closed.
|
|
static let nonWritableFileClosed = InotifyEvent.Flags(rawValue: numericCast(IN_CLOSE_NOWRITE))
|
|
/// File was opened.
|
|
static let opened = InotifyEvent.Flags(rawValue: numericCast(IN_OPEN))
|
|
/// File was moved from X.
|
|
static let movedFrom = InotifyEvent.Flags(rawValue: numericCast(IN_MOVED_FROM))
|
|
/// File was moved to Y.
|
|
static let movedTo = InotifyEvent.Flags(rawValue: numericCast(IN_MOVED_TO))
|
|
/// File was created inside the watched path.
|
|
static let fileCreated = InotifyEvent.Flags(rawValue: numericCast(IN_CREATE))
|
|
/// File was deleted inside the watched path.
|
|
static let fileDeleted = InotifyEvent.Flags(rawValue: numericCast(IN_DELETE))
|
|
/// The watched path was deleted.
|
|
static let selfDeleted = InotifyEvent.Flags(rawValue: numericCast(IN_DELETE_SELF))
|
|
/// The watched path was moved.
|
|
static let selfMoved = InotifyEvent.Flags(rawValue: numericCast(IN_MOVE_SELF))
|
|
|
|
/// Event occurred against a directory.
|
|
static let isDirectory = InotifyEvent.Flags(rawValue: numericCast(IN_ISDIR))
|
|
}
|
|
|
|
/// An async sequence that continously streams the generic `Element` type from a given file.
|
|
/// The `Failure` type describes the errors thrown for the sequence. The ``FailureBehavior`` is used to handle errors.
|
|
struct FileStream<Element>: AsyncSequence {
|
|
@usableFromInline
|
|
let _stream: AsyncThrowingStream<Element, Error>
|
|
|
|
/// Creates a new file stream for the given `fileDescriptor`.
|
|
/// The `failureBehavior` defines how errors are handled.
|
|
/// - Parameters:
|
|
/// - fileDescriptor: The file descriptor to stream from.
|
|
/// - failureBehavior: How to handle failures of the underlying stream.
|
|
init(fileDescriptor: FileDescriptor) {
|
|
_stream = .init(Element.self, { Self._gcdImplementation(for: fileDescriptor, using: $0) })
|
|
}
|
|
|
|
@inlinable
|
|
func makeAsyncIterator() -> AsyncThrowingStream<Element, Error>.AsyncIterator {
|
|
_stream.makeAsyncIterator()
|
|
}
|
|
}
|
|
|
|
extension FileStream: Sendable where Element: Sendable {}
|
|
|
|
extension FileStream {
|
|
private static func _gcdImplementation(for fileDescriptor: FileDescriptor,
|
|
using cont: AsyncThrowingStream<Element, Error>.Continuation) {
|
|
let source = _inactiveSource(from: fileDescriptor) {
|
|
cont.yield($0)
|
|
} onFailure: {
|
|
cont.finish(throwing: $0)
|
|
}
|
|
cont.onTermination = { _ in source.cancel() }
|
|
source.activate()
|
|
}
|
|
}
|
|
|
|
#if swift(>=6.2) && canImport(Darwin)
|
|
fileprivate typealias SendableDispatchSource = any DispatchSourceRead
|
|
#else
|
|
fileprivate struct SendableDispatchSource: @unchecked Sendable {
|
|
let source: any DispatchSourceRead
|
|
|
|
func activate() {
|
|
source.activate()
|
|
}
|
|
|
|
func cancel() {
|
|
source.cancel()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
extension FileStream {
|
|
private static func _inactiveSource(from fileDesc: FileDescriptor,
|
|
onElement elementCallback: @escaping @Sendable (sending Element) -> (),
|
|
onFailure failureCallback: @escaping @Sendable (any Error) -> ()) -> SendableDispatchSource {
|
|
#if compiler(>=6.2)
|
|
let unsafeCallback = unsafe unsafeBitCast(elementCallback, to: (@Sendable (Element) -> ()).self)
|
|
#else
|
|
let unsafeCallback = unsafeBitCast(elementCallback, to: (@Sendable (Element) -> ()).self)
|
|
#endif
|
|
@Sendable
|
|
func send(_ value: Element) {
|
|
unsafeCallback(value)
|
|
}
|
|
|
|
let workerQueue = DispatchQueue(label: "de.sersoft.filestreamer.filestream.gcd.worker")
|
|
let source = DispatchSource.makeReadSource(fileDescriptor: fileDesc.rawValue, queue: workerQueue)
|
|
let rawSize = MemoryLayout<Element>.size
|
|
let rawSize64 = UInt64(rawSize)
|
|
var remainingData: UInt64 = 0
|
|
source.setEventHandler {
|
|
do {
|
|
remainingData += .init(source.data)
|
|
guard case let capacity = remainingData / rawSize64, capacity > 0 else { return }
|
|
let buffer = UnsafeMutableBufferPointer<Element>.allocate(capacity: .init(capacity))
|
|
#if compiler(>=6.2)
|
|
defer { unsafe buffer.deallocate() }
|
|
let bytesRead = unsafe try fileDesc.read(into: UnsafeMutableRawBufferPointer(buffer))
|
|
#else
|
|
defer { buffer.deallocate() }
|
|
let bytesRead = try fileDesc.read(into: UnsafeMutableRawBufferPointer(buffer))
|
|
#endif
|
|
if case let noOfValues = bytesRead / rawSize, noOfValues > 0 {
|
|
#if compiler(>=6.2)
|
|
for unsafe value in unsafe buffer.prefix(noOfValues) {
|
|
send(value)
|
|
}
|
|
#else
|
|
for value in buffer.prefix(noOfValues) {
|
|
send(value)
|
|
}
|
|
#endif
|
|
}
|
|
let leftOverBytes = bytesRead % rawSize
|
|
remainingData -= .init(bytesRead - leftOverBytes)
|
|
if leftOverBytes > 0 {
|
|
do {
|
|
try fileDesc.seek(offset: .init(-leftOverBytes), from: .current)
|
|
} catch {
|
|
// If we failed to seek, we need to drop the left-over bytes.
|
|
remainingData -= .init(leftOverBytes)
|
|
throw error // Re-throw to land it in the failureCallback below
|
|
}
|
|
}
|
|
} catch {
|
|
failureCallback(error)
|
|
}
|
|
}
|
|
#if swift(>=6.2) && canImport(Darwin)
|
|
return source
|
|
#else
|
|
return .init(source: source)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#endif
|