Skip to content

Commit 13b2c53

Browse files
add latency support (& remarks in the README)
1 parent daa3c7f commit 13b2c53

3 files changed

Lines changed: 89 additions & 4 deletions

File tree

Example/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3232

3333
report(url)
3434

35-
FolderContentMonitor(url: url)
35+
FolderContentMonitor(url: url, latency: 0)
3636
.asObservable()
3737

3838
// Ignore Finder folder settings

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,75 @@ let changedFile = FolderContentMonitor(url: folderUrl)
6767
})
6868
```
6969

70+
## A Note on Latency
71+
72+
A latency of 0.0 (default value) can produce too much noise. Experiment with slightly higher values so the system can coalesce events when appropriate.
73+
74+
When you run the example app to see which kinds of events are fired, make sure to use TextEdit to create and modify a file so you see what kinds of events are bound to happen. Here's an annotated log:
75+
76+
```
77+
// Create file in folder
78+
79+
texteditfile.txt changed (isFile, renamed, xattrsModified)
80+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
81+
82+
83+
// Save changes to file
84+
85+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
86+
texteditfile.txt.sb-56afa5c6-DmdqsL changed (isFile, renamed)
87+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
88+
texteditfile.txt changed (isFile, renamed, finderInfoModified, inodeMetaModified, xattrsModified)
89+
texteditfile.txt.sb-56afa5c6-DmdqsL changed (isFile, modified, removed, renamed, changeOwner)
90+
texteditfile.txt changed (isFile, renamed, finderInfoModified, inodeMetaModified, xattrsModified)
91+
```
92+
93+
You see that overwriting a file _atomically_ will fire a lot of events when you use a modern document-based macOS app like TextEdit. The authors interpretation of these events is: "get rid of the original file, move in temp file with changes, copy temp file to original file's path, then get rid of the temp file". It could mean the original file is renamed to the temporary looking name just as well as far as I know. (Which, apparently, isn't much.)
94+
95+
Now see the log for the same actions with a latency of 1 second:
96+
97+
```
98+
// Create file in folder
99+
100+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
101+
102+
// Save changes to file
103+
104+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
105+
texteditfile.txt.sb-56afa5c6-SOiDRl changed (isFile, renamed)
106+
texteditfile.txt changed (isFile, renamed, finderInfoModified, inodeMetaModified, xattrsModified)
107+
texteditfile.txt.sb-56afa5c6-SOiDRl changed (isFile, modified, removed, renamed, changeOwner)
108+
```
109+
110+
(Doesn't get any better than this.)
111+
112+
So maybe a latency of slightly above >0.0 can help get rid of noise. Makes even more sense when you coalesce the `RxSwift.Observable` events in the end.
113+
114+
Note that other editors like TextMate 2 don't write to files with the same mechanism and only generate a single event, similar to what you'd expect from file changes originating in the shell:
115+
116+
```
117+
texteditfile.txt changed (isFile, modified, xattrsModified)
118+
```
119+
120+
121+
## Event Interpretation
122+
123+
Look at the repository's issues -- there's still a lot of room for improvement. This boils down to applying _interpretation_ and _heuristics_. In other words, it might break or be utterly wrong.
124+
125+
At the moment, each and every FSEvent is forwarded to the callback (or observer).
126+
127+
But FSEvents sometimes come in pairs, like:
128+
129+
```
130+
texteditfile.txt changed (isFile, renamed, finderInfoModified, xattrsModified)
131+
texteditfile.txt.sb-56afa5c6-SOiDRl changed (isFile, renamed)
132+
```
133+
134+
That means the library could try to make sense of event pairs and reduce them to single events for the client. Instead of forwarding an event of the form "`texteditfile.txt.sb-56afa5c6-SOiDRl` was renamed" which, in client's terms, will be interpreted as "the file was moved in there", the library could fire a single "edited" event.
135+
136+
Also noteworthy: the `removed` event will not be fired when trashing a file from Finder even though it was fired when TextEdit saved file changes and got rid of the intermediate result file. Some `renamed` events thus really are `removed` events and you need to check the file at the URL for existence after the event comes in. With a certain latency, or else you may pick up the about-to-be-trashed file _before_ its being moved is completed.
137+
138+
70139
## License
71140

72141
Copyright (c) 2016 RxSwiftCommunity https://github.com/RxSwiftCommunity

RxFileMonitor/FolderContentMonitor.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,43 @@ public class FolderContentMonitor {
2121
public let pathsToWatch: [String]
2222
public private(set) var hasStarted = false
2323
private var streamRef: FSEventStreamRef!
24-
24+
public let latency: CFTimeInterval
2525
public private(set) var lastEventId: FSEventStreamEventId
2626

2727
/// - parameter url: Folder to monitor.
2828
/// - parameter sinceWhen: Reference event for the subscription. Default
2929
/// is `kFSEventStreamEventIdSinceNow`.
30+
/// - parameter latency: Interval (in seconds) to allow coalescing events.
3031
/// - parameter callback: Callback for incoming file system events. Can be ignored
3132
/// when you use the monitor `asObservable`
3233
public convenience init(
3334
url: URL,
3435
sinceWhen: FSEventStreamEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
36+
latency: CFTimeInterval = 0,
3537
callback: ((FolderContentChangeEvent) -> Void)? = nil) {
3638

37-
self.init(pathsToWatch: [url.path], sinceWhen: sinceWhen, callback: callback)
39+
self.init(
40+
pathsToWatch: [url.path],
41+
sinceWhen: sinceWhen,
42+
latency: latency,
43+
callback: callback)
3844
}
3945

4046
/// - parameter pathsToWatch: Collection of file or folder paths.
4147
/// - parameter sinceWhen: Reference event for the subscription. Default
4248
/// is `kFSEventStreamEventIdSinceNow`.
49+
/// - parameter latency: Interval (in seconds) to allow coalescing events.
4350
/// - parameter callback: Callback for incoming file system events. Can be ignored
4451
/// when you use the monitor `asObservable`
4552
public init(
4653
pathsToWatch: [String],
4754
sinceWhen: FSEventStreamEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
55+
latency: CFTimeInterval = 0,
4856
callback: ((FolderContentChangeEvent) -> Void)? = nil) {
4957

5058
self.lastEventId = sinceWhen
5159
self.pathsToWatch = pathsToWatch
60+
self.latency = latency
5261
self.callback = callback
5362
}
5463

@@ -63,7 +72,14 @@ public class FolderContentMonitor {
6372
var context = FSEventStreamContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
6473
context.info = Unmanaged.passUnretained(self).toOpaque()
6574
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
66-
streamRef = FSEventStreamCreate(kCFAllocatorDefault, eventCallback, &context, pathsToWatch as CFArray, lastEventId, 0, flags)
75+
streamRef = FSEventStreamCreate(
76+
kCFAllocatorDefault,
77+
eventCallback,
78+
&context,
79+
pathsToWatch as CFArray,
80+
lastEventId,
81+
latency,
82+
flags)
6783

6884
FSEventStreamScheduleWithRunLoop(streamRef, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
6985
FSEventStreamStart(streamRef)

0 commit comments

Comments
 (0)