Restore watchOnTermination

At some point the watchOnTermination callback stopped working. I'm not
exactly sure how or why that happened but it is fairly straightforward
to restore. The one tricky thing was that the callback has the signature
(Watch.Action, _, _, _) => State, which requires propagating the action
to the failWatch command. The easiest way to do this was to add a
mutable field to the ContinuousState. This is rather ugly and reflects
some poor design choices but a more comprehensive refactor is out of
the scope of this fix.

This commit adds a scripted test that ensures that the callback is
invoked both in the successful and unsuccessful watch cases. In each
case the callback deletes a file and we ensure that the file is indeed
absent after the watch exits.
This commit is contained in:
Ethan Atkins 2022-04-09 15:05:54 -07:00 committed by Eugene Yokota
parent 64c7071ff2
commit ca7c872e27
5 changed files with 70 additions and 21 deletions

View File

@ -1128,7 +1128,28 @@ private[sbt] object Continuous extends DeprecatedContinuous {
val callbacks: Callbacks,
val dynamicInputs: mutable.Set[DynamicInput],
val pending: Boolean,
var failAction: Option[Watch.Action],
) {
def this(
count: Int,
commands: Seq[String],
beforeCommandImpl: (State, mutable.Set[DynamicInput]) => State,
afterCommand: State => State,
afterWatch: State => State,
callbacks: Callbacks,
dynamicInputs: mutable.Set[DynamicInput],
pending: Boolean,
) = this(
count,
commands,
beforeCommandImpl,
afterCommand,
afterWatch,
callbacks,
dynamicInputs,
pending,
None
)
def beforeCommand(state: State): State = beforeCommandImpl(state, dynamicInputs)
def incremented: ContinuousState = withCount(count + 1)
def withPending(p: Boolean) =
@ -1323,7 +1344,8 @@ private[sbt] object ContinuousCommands {
case Watch.Prompt => stop.map(_ :: s"$PromptChannel ${channel.name}" :: Nil mkString ";")
case Watch.Run(commands) =>
stop.map(_ +: commands.map(_.commandLine).filter(_.nonEmpty) mkString "; ")
case Watch.HandleError(_) =>
case a @ Watch.HandleError(_) =>
cs.failAction = Some(a)
stop.map(_ :: s"$failWatch ${channel.name}" :: Nil mkString "; ")
case _ => stop
}
@ -1353,27 +1375,31 @@ private[sbt] object ContinuousCommands {
}
cs.afterCommand(postState)
}
private[sbt] val stopWatchCommand = watchCommand(stopWatch) { (channel, state) =>
state.get(watchStates).flatMap(_.get(channel)) match {
case Some(cs) =>
val afterWatchState = cs.afterWatch(state)
cs.callbacks.onExit()
StandardMain.exchange
.channelForName(channel)
.foreach { c =>
c.terminal.setPrompt(Prompt.Pending)
c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel))))
private[this] val exitWatchShared = (error: Boolean) =>
(channel: String, state: State) =>
state.get(watchStates).flatMap(_.get(channel)) match {
case Some(cs) =>
val afterWatchState = cs.afterWatch(state)
cs.callbacks.onExit()
StandardMain.exchange
.channelForName(channel)
.foreach { c =>
c.terminal.setPrompt(Prompt.Pending)
c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel))))
}
val newState = afterWatchState.get(watchStates) match {
case None => afterWatchState
case Some(w) => afterWatchState.put(watchStates, w - channel)
}
afterWatchState.get(watchStates) match {
case None => afterWatchState
case Some(w) => afterWatchState.put(watchStates, w - channel)
}
case _ => state
}
}
private[sbt] val failWatchCommand = watchCommand(failWatch) { (channel, state) =>
state.fail
}
val commands = cs.commands.mkString("; ")
val count = cs.count
val action = cs.failAction.getOrElse(Watch.CancelWatch)
val st = cs.callbacks.onTermination(action, commands, count, newState)
if (error) st.fail else st
case _ => if (error) state.fail else state
}
private[sbt] val stopWatchCommand = watchCommand(stopWatch)(exitWatchShared(false))
private[sbt] val failWatchCommand = watchCommand(failWatch)(exitWatchShared(true))
/*
* Creates a FileTreeRepository where it is safe to call close without inadvertently cancelling
* still active watches.

View File

@ -0,0 +1,15 @@
watchOnIteration := { (count, project, commands) =>
Watch.CancelWatch
}
watchOnTermination := { (action, count, command, state) =>
action match {
case Watch.CancelWatch =>
java.nio.file.Files.delete(java.nio.file.Paths.get("foo.txt"))
case Watch.HandleError(e) =>
if (e.getMessage == "fail")
java.nio.file.Files.delete(java.nio.file.Paths.get("bar.txt"))
else
throw new IllegalStateException("unexpected error")
}
state
}

View File

@ -0,0 +1,8 @@
$ exists foo.txt
> ~compile
$ absent foo.txt
> set watchOnIteration := { (_, _, _) => new Watch.HandleError(new IllegalStateException("fail")) }
$ exists bar.txt
-> ~compile
$ absent bar.txt