Skip to content
6 changes: 3 additions & 3 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import de.tobiasroeser.mill.vcs.version.VcsVersion

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

val scala213Version = "2.13.14"
val scala213Version = "2.13.16"

val scalaVersions = Seq(
"3.3.1",
"2.12.17",
"3.3.5",
"2.12.20",
scala213Version
) ++ communityBuildDottyVersion

Expand Down
18 changes: 9 additions & 9 deletions os/watch/src/FSEventsWatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package os.watch

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

import scala.util.control.NonFatal

class FSEventsWatcher(
srcs: Seq[os.Path],
onEvent: Set[os.Path] => Unit,
logger: (String, Any) => Unit = (_, _) => (),
filter: os.Path => Boolean,
logger: (String, Any) => Unit,
latency: Double
) extends Watcher {
private[this] var closed = false
private[this] val existingFolders = collection.mutable.Set.empty[os.Path]
private[this] val callback = new FSEventStreamCallback {
def invoke(
streamRef: FSEventStreamRef,
Expand All @@ -22,20 +24,18 @@ class FSEventsWatcher(
val length = numEvents.intValue
val pathStrings = eventPaths.getStringArray(0, length)
logger("FSEVENT", pathStrings)
val paths = pathStrings.map(os.Path(_))
val paths = pathStrings.iterator.map(os.Path(_)).filter(filter).toArray
val nestedPaths = collection.mutable.Buffer.empty[os.Path]
// When folders are moved, OS-X does not emit file events for all sub-paths
// within the new folder, so we are forced to walk that folder and emit the
// paths ourselves
for (p <- paths) {
if (!os.isDir(p, followLinks = false)) existingFolders.remove(p)
else {
existingFolders.add(p)
try os.walk.stream(p).foreach(nestedPaths.append(_))
catch { case e: Throwable => /*do nothing*/ }
if (os.isDir(p, followLinks = false)) {
try os.walk.stream(p).foreach(p => if (filter(p)) nestedPaths.append(p))
catch { case NonFatal(_) => /*do nothing*/ }
}
}
onEvent((paths ++ nestedPaths).toSet)
onEvent((paths.iterator ++ nestedPaths.iterator).toSet)
}
}

Expand Down
44 changes: 36 additions & 8 deletions os/watch/src/WatchServiceWatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import scala.util.Properties.isWin
class WatchServiceWatcher(
roots: Seq[os.Path],
onEvent: Set[os.Path] => Unit,
logger: (String, Any) => Unit = (_, _) => ()
filter: os.Path => Boolean,
logger: (String, Any) => Unit
) extends Watcher {
import WatchServiceWatcher.WatchEventOps

val nioWatchService = FileSystems.getDefault.newWatchService()
val currentlyWatchedPaths = mutable.Map.empty[os.Path, WatchKey]
Expand Down Expand Up @@ -46,7 +48,7 @@ class WatchServiceWatcher(
modifiers: _*
)
)
newlyWatchedPaths.append(p)
if (filter(p)) newlyWatchedPaths.append(p)
}
bufferedEvents.add(p)
}
Expand All @@ -61,12 +63,33 @@ class WatchServiceWatcher(

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

def logWarning(msg: String): Unit = {
System.err.println(s"[oslib.watch] (path=$p) $msg")
}

def logWarningContextNull(e: WatchEvent[_]): Unit = {
logWarning(
s"Context is null for event kind='${e.kind().name()}' of class ${e.kind().`type`().getName}, " +
s"this should never happen."
)
}

for (e <- events) {
bufferedEvents.add(p / e.context().toString)
if (e.kind() == OVERFLOW) {
logWarning("Overflow detected, some filesystem changes may not be registered.")
} else {
e.contextSafe match {
case Some(ctx) => bufferedEvents.add(p / ctx.toString)
case None => logWarningContextNull(e)
}
}
}

for (e <- events if e.kind() == ENTRY_CREATE) {
watchSinglePath(p / e.context().toString)
e.contextSafe match {
case Some(ctx) => watchSinglePath(p / ctx.toString)
case None => logWarningContextNull(e)
}
}

watchKey.reset()
Expand All @@ -83,7 +106,7 @@ class WatchServiceWatcher(
val listing =
try os.list(top)
catch {
case e: java.nio.file.NotDirectoryException => Nil
case _: java.nio.file.NotDirectoryException | _: java.nio.file.NoSuchFileException => Nil
}
for (p <- listing) watchSinglePath(p)
bufferedEvents.add(top)
Expand Down Expand Up @@ -124,10 +147,10 @@ class WatchServiceWatcher(

} catch {
case e: InterruptedException =>
println("Interrupted, exiting: " + e)
logger("Interrupted, exiting.", e)
isRunning.set(false)
case e: ClosedWatchServiceException =>
println("Watcher closed, exiting: " + e)
logger("Watcher closed, exiting.", e)
isRunning.set(false)
}
}
Expand All @@ -137,7 +160,7 @@ class WatchServiceWatcher(
isRunning.set(false)
nioWatchService.close()
} catch {
case e: IOException => println("Error closing watcher: " + e)
case e: IOException => logger("Error closing watcher.", e)
}
}

Expand All @@ -147,3 +170,8 @@ class WatchServiceWatcher(
bufferedEvents.clear()
}
}
object WatchServiceWatcher {
implicit class WatchEventOps[A](private val e: WatchEvent[A]) extends AnyVal {
def contextSafe: Option[A] = Option(e.context())
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturaz let's make this a local private helper method rather than a public extension method, as we use extension methods very rarely in the com-lihaoyi projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does making the extension helper private work? I feel that it's better to have .contextSafe pop-up in autocomplete when you are typing evt.context. Helper function doesn't have that discoverability.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make it a normal method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware of the benefits of extension methods, but just stylistically we basically have ~never used them in these projects, and I don't feel like starting now haha.

11 changes: 8 additions & 3 deletions os/watch/src/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,20 @@ package object watch {
* changes happening within the watched roots folder, apart from the path
* at which the change happened. It is up to the `onEvent` handler to query
* the filesystem and figure out what happened, and what it wants to do.
*
* @param filter when new paths under `roots` are created, this function is
* invoked with each path. If it returns `false`, the path is
* not watched.
*/
def watch(
roots: Seq[os.Path],
onEvent: Set[os.Path] => Unit,
logger: (String, Any) => Unit = (_, _) => ()
logger: (String, Any) => Unit = (_, _) => (),
filter: os.Path => Boolean = _ => true
): AutoCloseable = {
val watcher = System.getProperty("os.name") match {
case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, logger, 0.05)
case _ => new os.watch.WatchServiceWatcher(roots, onEvent, logger)
case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, filter, logger, 0.05)
case _ => new os.watch.WatchServiceWatcher(roots, onEvent, filter, logger)
}

val thread = new Thread {
Expand Down