Skip to content

Commit 5443f89

Browse files
authored
Improvements to oslib.watch (#386)
- Remove extraneous `println`s in `WatchServiceWatcher` - Add `filter: os.Path => Boolean` parameter to `watch` - Correctly handle `OVERFLOW` events. - Bump scala versions to allow compilation on newer JDKs.
1 parent 4993aa1 commit 5443f89

File tree

4 files changed

+53
-24
lines changed

4 files changed

+53
-24
lines changed

build.mill

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import de.tobiasroeser.mill.vcs.version.VcsVersion
1111

1212
val communityBuildDottyVersion = sys.props.get("dottyVersion").toList
1313

14-
val scala213Version = "2.13.14"
14+
val scala213Version = "2.13.16"
1515

1616
val scalaVersions = Seq(
17-
"3.3.1",
18-
"2.12.17",
17+
"3.3.5",
18+
"2.12.20",
1919
scala213Version
2020
) ++ communityBuildDottyVersion
2121

os/watch/src/FSEventsWatcher.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ package os.watch
22

33
import com.sun.jna.{NativeLong, Pointer}
44

5+
import scala.util.control.NonFatal
6+
57
class FSEventsWatcher(
68
srcs: Seq[os.Path],
79
onEvent: Set[os.Path] => Unit,
8-
logger: (String, Any) => Unit = (_, _) => (),
10+
filter: os.Path => Boolean,
11+
logger: (String, Any) => Unit,
912
latency: Double
1013
) extends Watcher {
1114
private[this] var closed = false
12-
private[this] val existingFolders = collection.mutable.Set.empty[os.Path]
1315
private[this] val callback = new FSEventStreamCallback {
1416
def invoke(
1517
streamRef: FSEventStreamRef,
@@ -22,20 +24,18 @@ class FSEventsWatcher(
2224
val length = numEvents.intValue
2325
val pathStrings = eventPaths.getStringArray(0, length)
2426
logger("FSEVENT", pathStrings)
25-
val paths = pathStrings.map(os.Path(_))
27+
val paths = pathStrings.iterator.map(os.Path(_)).filter(filter).toArray
2628
val nestedPaths = collection.mutable.Buffer.empty[os.Path]
2729
// When folders are moved, OS-X does not emit file events for all sub-paths
2830
// within the new folder, so we are forced to walk that folder and emit the
2931
// paths ourselves
3032
for (p <- paths) {
31-
if (!os.isDir(p, followLinks = false)) existingFolders.remove(p)
32-
else {
33-
existingFolders.add(p)
34-
try os.walk.stream(p).foreach(nestedPaths.append(_))
35-
catch { case e: Throwable => /*do nothing*/ }
33+
if (os.isDir(p, followLinks = false)) {
34+
try os.walk.stream(p).foreach(p => if (filter(p)) nestedPaths.append(p))
35+
catch { case NonFatal(_) => /*do nothing*/ }
3636
}
3737
}
38-
onEvent((paths ++ nestedPaths).toSet)
38+
onEvent((paths.iterator ++ nestedPaths.iterator).toSet)
3939
}
4040
}
4141

os/watch/src/WatchServiceWatcher.scala

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import scala.util.Properties.isWin
1414
class WatchServiceWatcher(
1515
roots: Seq[os.Path],
1616
onEvent: Set[os.Path] => Unit,
17-
logger: (String, Any) => Unit = (_, _) => ()
17+
filter: os.Path => Boolean,
18+
logger: (String, Any) => Unit
1819
) extends Watcher {
19-
2020
val nioWatchService = FileSystems.getDefault.newWatchService()
2121
val currentlyWatchedPaths = mutable.Map.empty[os.Path, WatchKey]
2222
val newlyWatchedPaths = mutable.Buffer.empty[os.Path]
@@ -46,7 +46,7 @@ class WatchServiceWatcher(
4646
modifiers: _*
4747
)
4848
)
49-
newlyWatchedPaths.append(p)
49+
if (filter(p)) newlyWatchedPaths.append(p)
5050
}
5151
bufferedEvents.add(p)
5252
}
@@ -61,12 +61,33 @@ class WatchServiceWatcher(
6161

6262
logger("WATCH KINDS", events.map(_.kind()))
6363

64+
def logWarning(msg: String): Unit = {
65+
System.err.println(s"[oslib.watch] (path=$p) $msg")
66+
}
67+
68+
def logWarningContextNull(e: WatchEvent[_]): Unit = {
69+
logWarning(
70+
s"Context is null for event kind='${e.kind().name()}' of class ${e.kind().`type`().getName}, " +
71+
s"this should never happen."
72+
)
73+
}
74+
6475
for (e <- events) {
65-
bufferedEvents.add(p / e.context().toString)
76+
if (e.kind() == OVERFLOW) {
77+
logWarning("Overflow detected, some filesystem changes may not be registered.")
78+
} else {
79+
contextSafe(e) match {
80+
case Some(ctx) => bufferedEvents.add(p / ctx.toString)
81+
case None => logWarningContextNull(e)
82+
}
83+
}
6684
}
6785

6886
for (e <- events if e.kind() == ENTRY_CREATE) {
69-
watchSinglePath(p / e.context().toString)
87+
contextSafe(e) match {
88+
case Some(ctx) => watchSinglePath(p / ctx.toString)
89+
case None => logWarningContextNull(e)
90+
}
7091
}
7192

7293
watchKey.reset()
@@ -83,7 +104,8 @@ class WatchServiceWatcher(
83104
val listing =
84105
try os.list(top)
85106
catch {
86-
case e: java.nio.file.NotDirectoryException => Nil
107+
case _: java.nio.file.NotDirectoryException | _: java.nio.file.NoSuchFileException =>
108+
Nil
87109
}
88110
for (p <- listing) watchSinglePath(p)
89111
bufferedEvents.add(top)
@@ -124,10 +146,10 @@ class WatchServiceWatcher(
124146

125147
} catch {
126148
case e: InterruptedException =>
127-
println("Interrupted, exiting: " + e)
149+
logger("Interrupted, exiting.", e)
128150
isRunning.set(false)
129151
case e: ClosedWatchServiceException =>
130-
println("Watcher closed, exiting: " + e)
152+
logger("Watcher closed, exiting.", e)
131153
isRunning.set(false)
132154
}
133155
}
@@ -137,7 +159,7 @@ class WatchServiceWatcher(
137159
isRunning.set(false)
138160
nioWatchService.close()
139161
} catch {
140-
case e: IOException => println("Error closing watcher: " + e)
162+
case e: IOException => logger("Error closing watcher.", e)
141163
}
142164
}
143165

@@ -146,4 +168,6 @@ class WatchServiceWatcher(
146168
onEvent(bufferedEvents.toSet)
147169
bufferedEvents.clear()
148170
}
171+
172+
def contextSafe[A](e: WatchEvent[A]): Option[A] = Option(e.context())
149173
}

os/watch/src/package.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,20 @@ package object watch {
2424
* changes happening within the watched roots folder, apart from the path
2525
* at which the change happened. It is up to the `onEvent` handler to query
2626
* the filesystem and figure out what happened, and what it wants to do.
27+
*
28+
* @param filter when new paths under `roots` are created, this function is
29+
* invoked with each path. If it returns `false`, the path is
30+
* not watched.
2731
*/
2832
def watch(
2933
roots: Seq[os.Path],
3034
onEvent: Set[os.Path] => Unit,
31-
logger: (String, Any) => Unit = (_, _) => ()
35+
logger: (String, Any) => Unit = (_, _) => (),
36+
filter: os.Path => Boolean = _ => true
3237
): AutoCloseable = {
3338
val watcher = System.getProperty("os.name") match {
34-
case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, logger, 0.05)
35-
case _ => new os.watch.WatchServiceWatcher(roots, onEvent, logger)
39+
case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, filter, logger, 0.05)
40+
case _ => new os.watch.WatchServiceWatcher(roots, onEvent, filter, logger)
3641
}
3742

3843
val thread = new Thread {

0 commit comments

Comments
 (0)