11package mill .runner
22
3- import mill .internal .Colors
4- import mill .define .internal .Watchable
5- import mill .define .PathRef
63import mill .api .SystemStreams
74import mill .api .internal .internal
5+ import mill .define .PathRef
6+ import mill .define .internal .Watchable
7+ import mill .internal .Colors
88
99import java .io .InputStream
1010import scala .annotation .tailrec
11+ import scala .util .Using
1112
1213/**
1314 * Logic around the "watch and wait" functionality in Mill: re-run on change,
@@ -17,40 +18,68 @@ import scala.annotation.tailrec
1718object Watching {
1819 case class Result [T ](watched : Seq [Watchable ], error : Option [String ], result : T )
1920
21+ trait Evaluate [T ] {
22+ def apply (enterKeyPressed : Boolean , previousState : Option [T ]): Result [T ]
23+ }
24+
25+ case class WatchArgs (
26+ setIdle : Boolean => Unit ,
27+ colors : Colors
28+ )
29+
30+ /**
31+ * @param ringBell whether to emit bells
32+ * @param watch if false just runs once and returns
33+ */
2034 def watchLoop [T ](
2135 ringBell : Boolean ,
22- watch : Boolean ,
36+ watch : Option [ WatchArgs ] ,
2337 streams : SystemStreams ,
24- setIdle : Boolean => Unit ,
25- evaluate : (Boolean , Option [T ]) => Result [T ],
26- colors : Colors
38+ evaluate : Evaluate [T ]
2739 ): (Boolean , T ) = {
28- var prevState : Option [T ] = None
29- var enterKeyPressed = false
30- while (true ) {
31- val Result (watchables, errorOpt, result) = evaluate(enterKeyPressed, prevState)
32- prevState = Some (result)
40+ def handleError (errorOpt : Option [String ]): Unit = {
3341 errorOpt.foreach(streams.err.println)
34- if (ringBell) {
35- if (errorOpt.isEmpty) println(" \u0007 " )
36- else {
37- println(" \u0007 " )
38- Thread .sleep(250 )
39- println(" \u0007 " )
40- }
41- }
42+ doRingBell(hasError = errorOpt.isDefined)
43+ }
4244
43- if (! watch) {
44- return (errorOpt.isEmpty, result)
45- }
45+ def doRingBell (hasError : Boolean ): Unit = {
46+ if (! ringBell) return
4647
47- val alreadyStale = watchables.exists(w => ! validate(w))
48- enterKeyPressed = false
49- if (! alreadyStale) {
50- enterKeyPressed = Watching .watchAndWait(streams, setIdle, streams.in, watchables, colors)
48+ println(" \u0007 " )
49+ if (hasError) {
50+ // If we have an error ring the bell again
51+ Thread .sleep(250 )
52+ println(" \u0007 " )
5153 }
5254 }
53- ???
55+
56+ watch match {
57+ case None =>
58+ val Result (watchables, errorOpt, result) =
59+ evaluate(enterKeyPressed = false , previousState = None )
60+ handleError(errorOpt)
61+ (errorOpt.isEmpty, result)
62+
63+ case Some (watchArgs) =>
64+ var prevState : Option [T ] = None
65+ var enterKeyPressed = false
66+
67+ while (true ) {
68+ val Result (watchables, errorOpt, result) = evaluate(enterKeyPressed, prevState)
69+ prevState = Some (result)
70+ handleError(errorOpt)
71+
72+ // Do not enter watch if already stale, re-evaluate instantly.
73+ val alreadyStale = watchables.exists(w => ! validateAnyWatchable(w))
74+ if (alreadyStale) {
75+ enterKeyPressed = false
76+ } else {
77+ enterKeyPressed = watchAndWait(streams, watchArgs.setIdle, streams.in, watchables, watchArgs.colors)
78+ }
79+ }
80+ // QUESTION: this never exits?
81+ throw new IllegalStateException (" unreachable" )
82+ }
5483 }
5584
5685 def watchAndWait (
@@ -61,28 +90,50 @@ object Watching {
6190 colors : Colors
6291 ): Boolean = {
6392 setIdle(true )
64- val watchedPaths = watched.collect { case p : Watchable .Path => p.p }
65- val watchedValues = watched.size - watchedPaths.size
93+ val (watchedPollables, watchedPaths) = watched.partitionMap {
94+ case w : Watchable .Pollable => Left (w)
95+ case p : Watchable .Path => Right (p)
96+ }
97+ val watchedValueCount = watched.size - watchedPaths.size
6698
67- val watchedValueStr = if (watchedValues == 0 ) " " else s " and $watchedValues other values "
99+ val watchedValueStr =
100+ if (watchedValueCount == 0 ) " " else s " and $watchedValueCount other values "
68101
69102 streams.err.println(
70103 colors.info(
71104 s " Watching for changes to ${watchedPaths.size} paths $watchedValueStr... (Enter to re-run, Ctrl-C to exit) "
72105 ).toString
73106 )
74107
75- val enterKeyPressed = statWatchWait(watched, stdin)
76- setIdle(false )
77- enterKeyPressed
108+ @ volatile var pathChangesDetected = false
109+ Using .resource(os.watch.watch(
110+ watchedPaths.map(path => os.Path (path.p)),
111+ onEvent = _ => pathChangesDetected = true ,
112+ logger = (eventType, data) => {
113+ streams.out.println(colors.info(s " [watch] $eventType: ${pprint.apply(data)}" ))
114+ }
115+ )) { _ =>
116+ val enterKeyPressed =
117+ statWatchWait(watchedPollables, stdin, notifiablesChanged = () => pathChangesDetected)
118+ setIdle(false )
119+ enterKeyPressed
120+ }
78121 }
79122
80- // Returns `true` if enter key is pressed to re-run tasks explicitly
81- def statWatchWait (watched : Seq [Watchable ], stdin : InputStream ): Boolean = {
123+ /**
124+ * @param notifiablesChanged returns true if any of the notifiables have changed
125+ *
126+ * @return `true` if enter key is pressed to re-run tasks explicitly, false if changes in watched files occured.
127+ */
128+ def statWatchWait (
129+ watched : Seq [Watchable .Pollable ],
130+ stdin : InputStream ,
131+ notifiablesChanged : () => Boolean
132+ ): Boolean = {
82133 val buffer = new Array [Byte ](4 * 1024 )
83134
84135 @ tailrec def statWatchWait0 (): Boolean = {
85- if (watched.forall(w => validate(w))) {
136+ if (! notifiablesChanged() && watched.forall(w => validate(w))) {
86137 if (lookForEnterKey()) {
87138 true
88139 } else {
@@ -96,11 +147,13 @@ object Watching {
96147 if (stdin.available() == 0 ) false
97148 else stdin.read(buffer) match {
98149 case 0 | - 1 => false
99- case n =>
150+ case bytesRead =>
100151 buffer.indexOf('\n ' ) match {
101152 case - 1 => lookForEnterKey()
102- case i =>
103- if (i >= n) lookForEnterKey()
153+ case index =>
154+ // If we found the newline further than the bytes read, that means it's not from this read and thus we
155+ // should try reading again.
156+ if (index >= bytesRead) lookForEnterKey()
104157 else true
105158 }
106159 }
@@ -109,13 +162,23 @@ object Watching {
109162 statWatchWait0()
110163 }
111164
112- def validate (w : Watchable ) = poll(w) == signature(w)
113- def poll (w : Watchable ) = w match {
165+ /** @return true if the watchable did not change. */
166+ inline def validate (w : Watchable .Pollable ): Boolean = validateAnyWatchable(w)
167+
168+ /**
169+ * As [[validate ]] but accepts any [[Watchable ]] for the cases when we do not want to use a notification system.
170+ *
171+ * Normally you should use [[validate ]] so that types would guide your implementation.
172+ */
173+ def validateAnyWatchable (w : Watchable ): Boolean = poll(w) == signature(w)
174+
175+ def poll (w : Watchable ): Long = w match {
114176 case Watchable .Path (p, quick, sig) =>
115177 new PathRef (os.Path (p), quick, sig, PathRef .Revalidate .Once ).recomputeSig()
116178 case Watchable .Value (f, sig, pretty) => f()
117179 }
118- def signature (w : Watchable ) = w match {
180+
181+ def signature (w : Watchable ): Long = w match {
119182 case Watchable .Path (p, quick, sig) =>
120183 new PathRef (os.Path (p), quick, sig, PathRef .Revalidate .Once ).sig
121184 case Watchable .Value (f, sig, pretty) => sig
0 commit comments