Skip to content

Commit 7ab5c0f

Browse files
authored
Fix watch on Windows (#236)
There are 2 parts to this fix: 1. the `FILE_TREE` modifier, which tells Windows to watch the whole folder recursively. This prevents errors when trying to move/rename a folder, since otherwise it locks them and we get errors.. Got this from here https://stackoverflow.com/a/6265860/4496364 2. preventing dynamic registration of subfolders in the `recursiveWatches` method
1 parent fd44104 commit 7ab5c0f

File tree

5 files changed

+128
-111
lines changed

5 files changed

+128
-111
lines changed

Readme.adoc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,8 +1757,6 @@ paths changed: /Users/lihaoyi/Github/Ammonite/out/i am,/Users/lihaoyi/Github/Amm
17571757
paths changed: /Users/lihaoyi/Github/Ammonite/out/version/log,/Users/lihaoyi/Github/Ammonite/out/version/meta.json,/Users/lihaoyi/Github/Ammonite/out/version
17581758
----
17591759

1760-
`watch` currently only supports Linux and Mac-OSX, and not Windows.
1761-
17621760
== Data Types
17631761

17641762
=== `os.Path`

os/watch/src/WatchServiceWatcher.scala

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import java.io.IOException
55
import java.nio.file.ClosedWatchServiceException
66
import java.util.concurrent.atomic.AtomicBoolean
77
import java.nio.file.StandardWatchEventKinds.{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW}
8-
9-
import com.sun.nio.file.SensitivityWatchEventModifier
8+
import com.sun.nio.file.{ExtendedWatchEventModifier, SensitivityWatchEventModifier}
109

1110
import scala.collection.mutable
1211
import collection.JavaConverters._
12+
import scala.util.Properties.isWin
1313

1414
class WatchServiceWatcher(
1515
roots: Seq[os.Path],
@@ -33,12 +33,17 @@ class WatchServiceWatcher(
3333
val isDir = os.isDir(p, followLinks = false)
3434
logger("WATCH", (p, isDir))
3535
if (isDir) {
36+
// https://stackoverflow.com/a/6265860/4496364
37+
// on Windows we watch only the root directory
38+
val modifiers: Array[WatchEvent.Modifier] = if (isWin)
39+
Array(SensitivityWatchEventModifier.HIGH, ExtendedWatchEventModifier.FILE_TREE)
40+
else Array(SensitivityWatchEventModifier.HIGH)
3641
currentlyWatchedPaths.put(
3742
p,
3843
p.toNIO.register(
3944
nioWatchService,
4045
Array[WatchEvent.Kind[_]](ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW),
41-
SensitivityWatchEventModifier.HIGH
46+
modifiers: _*
4247
)
4348
)
4449
newlyWatchedPaths.append(p)
@@ -68,13 +73,21 @@ class WatchServiceWatcher(
6873
}
6974

7075
def recursiveWatches() = {
71-
while (newlyWatchedPaths.nonEmpty) {
72-
val top = newlyWatchedPaths.remove(newlyWatchedPaths.length - 1)
73-
val listing =
74-
try os.list(top)
75-
catch { case e: java.nio.file.NotDirectoryException => Nil }
76-
for (p <- listing) watchSinglePath(p)
77-
bufferedEvents.add(top)
76+
// no need to recursively watch each folder on windows
77+
// https://stackoverflow.com/a/64030685/4496364
78+
if (isWin) {
79+
// noop
80+
} else {
81+
while (newlyWatchedPaths.nonEmpty) {
82+
val top = newlyWatchedPaths.remove(newlyWatchedPaths.length - 1)
83+
val listing =
84+
try os.list(top)
85+
catch {
86+
case e: java.nio.file.NotDirectoryException => Nil
87+
}
88+
for (p <- listing) watchSinglePath(p)
89+
bufferedEvents.add(top)
90+
}
7891
}
7992
}
8093

os/watch/src/package.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,15 @@ 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-
* `watch` currently only supports Linux and Mac-OSX, and not Windows.
2927
*/
3028
def watch(
3129
roots: Seq[os.Path],
3230
onEvent: Set[os.Path] => Unit,
3331
logger: (String, Any) => Unit = (_, _) => ()
3432
): AutoCloseable = {
3533
val watcher = System.getProperty("os.name") match {
36-
case "Linux" => new os.watch.WatchServiceWatcher(roots, onEvent, logger)
3734
case "Mac OS X" => new os.watch.FSEventsWatcher(roots, onEvent, logger, 0.05)
38-
case osName => throw new Exception(s"watch not supported on operating system: $osName")
35+
case _ => new os.watch.WatchServiceWatcher(roots, onEvent, logger)
3936
}
4037

4138
val thread = new Thread {

os/watch/test/src/WatchTests.scala

Lines changed: 104 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package test.os.watch
22

3+
import scala.util.Properties.isWin
4+
import scala.util.Random
35
import utest._
46

57
object WatchTests extends TestSuite with TestSuite.Retries {
@@ -13,127 +15,135 @@ object WatchTests extends TestSuite with TestSuite.Retries {
1315

1416
val tests = Tests {
1517
test("singleFolder") - _root_.test.os.TestUtil.prep { wd =>
16-
if (_root_.test.os.Unix()) {
17-
val changedPaths = collection.mutable.Set.empty[os.Path]
18-
_root_.os.watch.watch(
19-
Seq(wd),
20-
onEvent = _.foreach(changedPaths.add)
21-
)
18+
val changedPaths = collection.mutable.Set.empty[os.Path]
19+
_root_.os.watch.watch(
20+
Seq(wd),
21+
onEvent = _.foreach(changedPaths.add)
22+
)
2223

2324
// os.write(wd / "lols", "")
2425
// Thread.sleep(100)
2526

26-
changedPaths.clear()
27-
28-
def checkFileManglingChanges(p: os.Path) = {
29-
30-
checkChanges(
31-
os.write(p, ""),
32-
Set(p.subRelativeTo(wd))
33-
)
34-
35-
checkChanges(
36-
os.write.append(p, "hello"),
37-
Set(p.subRelativeTo(wd))
38-
)
39-
40-
checkChanges(
41-
os.write.over(p, "world"),
42-
Set(p.subRelativeTo(wd))
43-
)
44-
45-
checkChanges(
46-
os.truncate(p, 1),
47-
Set(p.subRelativeTo(wd))
48-
)
27+
changedPaths.clear()
4928

50-
checkChanges(
51-
os.remove(p),
52-
Set(p.subRelativeTo(wd))
53-
)
54-
}
55-
def checkChanges(action: => Unit, expectedChangedPaths: Set[os.SubPath]) = synchronized {
56-
changedPaths.clear()
57-
action
58-
Thread.sleep(200)
59-
val changedSubPaths = changedPaths.map(_.subRelativeTo(wd))
60-
assert(expectedChangedPaths == changedSubPaths)
61-
}
62-
63-
checkFileManglingChanges(wd / "test")
29+
def checkFileManglingChanges(p: os.Path) = {
6430

6531
checkChanges(
66-
os.remove(wd / "File.txt"),
67-
Set(os.sub / "File.txt")
32+
os.write(p, Random.nextString(100)),
33+
Set(p.subRelativeTo(wd))
6834
)
6935

7036
checkChanges(
71-
os.makeDir(wd / "my-new-folder"),
72-
Set(os.sub / "my-new-folder")
37+
os.write.append(p, "hello"),
38+
Set(p.subRelativeTo(wd))
7339
)
7440

75-
checkFileManglingChanges(wd / "my-new-folder" / "test")
76-
7741
checkChanges(
78-
os.move(wd / "folder2", wd / "folder3"),
79-
Set(
80-
os.sub / "folder2",
81-
os.sub / "folder3",
82-
os.sub / "folder3" / "nestedA",
83-
os.sub / "folder3" / "nestedA" / "a.txt",
84-
os.sub / "folder3" / "nestedB",
85-
os.sub / "folder3" / "nestedB" / "b.txt"
86-
)
42+
os.write.over(p, "world"),
43+
Set(p.subRelativeTo(wd))
8744
)
8845

8946
checkChanges(
90-
os.copy(wd / "folder3", wd / "folder4"),
91-
Set(
92-
os.sub / "folder4",
93-
os.sub / "folder4" / "nestedA",
94-
os.sub / "folder4" / "nestedA" / "a.txt",
95-
os.sub / "folder4" / "nestedB",
96-
os.sub / "folder4" / "nestedB" / "b.txt"
97-
)
47+
os.truncate(p, 1),
48+
Set(p.subRelativeTo(wd))
9849
)
9950

10051
checkChanges(
101-
os.remove.all(wd / "folder4"),
102-
Set(
103-
os.sub / "folder4",
104-
os.sub / "folder4" / "nestedA",
105-
os.sub / "folder4" / "nestedA" / "a.txt",
106-
os.sub / "folder4" / "nestedB",
107-
os.sub / "folder4" / "nestedB" / "b.txt"
108-
)
52+
os.remove(p),
53+
Set(p.subRelativeTo(wd))
10954
)
55+
}
56+
def checkChanges(action: => Unit, expectedChangedPaths: Set[os.SubPath]) = synchronized {
57+
changedPaths.clear()
58+
action
59+
Thread.sleep(200)
60+
val changedSubPaths = changedPaths.map(_.subRelativeTo(wd))
61+
// on Windows sometimes we get more changes
62+
if (isWin) assert(expectedChangedPaths.subsetOf(changedSubPaths))
63+
else assert(expectedChangedPaths == changedSubPaths)
64+
}
11065

111-
checkFileManglingChanges(wd / "folder3" / "nestedA" / "double-nested-file")
112-
checkFileManglingChanges(wd / "folder3" / "nestedB" / "double-nested-file")
66+
checkFileManglingChanges(wd / "test")
11367

114-
checkChanges(
115-
os.symlink(wd / "newlink", wd / "doesntexist"),
116-
Set(os.sub / "newlink")
117-
)
68+
checkChanges(
69+
os.remove(wd / "File.txt"),
70+
Set(os.sub / "File.txt")
71+
)
11872

119-
checkChanges(
120-
os.symlink(wd / "newlink2", wd / "folder3"),
121-
Set(os.sub / "newlink2")
122-
)
73+
checkChanges(
74+
os.makeDir(wd / "my-new-folder"),
75+
Set(os.sub / "my-new-folder")
76+
)
77+
78+
checkFileManglingChanges(wd / "my-new-folder" / "test")
12379

80+
locally {
81+
val expectedChanges = if (isWin) Set(
82+
os.sub / "folder2",
83+
os.sub / "folder3"
84+
)
85+
else Set(
86+
os.sub / "folder2",
87+
os.sub / "folder3",
88+
os.sub / "folder3" / "nestedA",
89+
os.sub / "folder3" / "nestedA" / "a.txt",
90+
os.sub / "folder3" / "nestedB",
91+
os.sub / "folder3" / "nestedB" / "b.txt"
92+
)
12493
checkChanges(
125-
os.hardlink(wd / "newlink3", wd / "folder3" / "nestedA" / "a.txt"),
126-
System.getProperty("os.name") match {
127-
case "Linux" => Set(os.sub / "newlink3")
128-
case "Mac OS X" =>
129-
Set(
130-
os.sub / "newlink3",
131-
os.sub / "folder3" / "nestedA",
132-
os.sub / "folder3" / "nestedA" / "a.txt"
133-
)
134-
}
94+
os.move(wd / "folder2", wd / "folder3"),
95+
expectedChanges
13596
)
13697
}
98+
99+
checkChanges(
100+
os.copy(wd / "folder3", wd / "folder4"),
101+
Set(
102+
os.sub / "folder4",
103+
os.sub / "folder4" / "nestedA",
104+
os.sub / "folder4" / "nestedA" / "a.txt",
105+
os.sub / "folder4" / "nestedB",
106+
os.sub / "folder4" / "nestedB" / "b.txt"
107+
)
108+
)
109+
110+
checkChanges(
111+
os.remove.all(wd / "folder4"),
112+
Set(
113+
os.sub / "folder4",
114+
os.sub / "folder4" / "nestedA",
115+
os.sub / "folder4" / "nestedA" / "a.txt",
116+
os.sub / "folder4" / "nestedB",
117+
os.sub / "folder4" / "nestedB" / "b.txt"
118+
)
119+
)
120+
121+
checkFileManglingChanges(wd / "folder3" / "nestedA" / "double-nested-file")
122+
checkFileManglingChanges(wd / "folder3" / "nestedB" / "double-nested-file")
123+
124+
checkChanges(
125+
os.symlink(wd / "newlink", wd / "doesntexist"),
126+
Set(os.sub / "newlink")
127+
)
128+
129+
checkChanges(
130+
os.symlink(wd / "newlink2", wd / "folder3"),
131+
Set(os.sub / "newlink2")
132+
)
133+
134+
checkChanges(
135+
os.hardlink(wd / "newlink3", wd / "folder3" / "nestedA" / "a.txt"),
136+
System.getProperty("os.name") match {
137+
case "Mac OS X" =>
138+
Set(
139+
os.sub / "newlink3",
140+
os.sub / "folder3" / "nestedA",
141+
os.sub / "folder3" / "nestedA" / "a.txt"
142+
)
143+
case _ => Set(os.sub / "newlink3")
144+
}
145+
)
146+
137147
}
138148
}
139149
}

testJarWriter/src/TestJarWriter.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import java.lang.InterruptedException;
33

44
public class TestJarWriter {
5-
private static boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
65
public static void main(String[] args) throws InterruptedException {
76
Scanner scanner = new Scanner(System.in);
87
int writeN = Integer.parseInt(args[0]);

0 commit comments

Comments
 (0)