From 600628e8e9b9c921e6456b22876a164fb319c4a2 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 19 Nov 2020 18:19:24 -0800 Subject: [PATCH 1/2] Fix watch with metals sbt bsp implementation I discovered that the metals bsp implementation worked very badly with continuous builds. The problem was that metals is able to trigger a bsp compile slightly before the continuous build would trigger. This would cause the ui to get in a bad state. The worst case was that it would actually cause sbt (or the thin client) to exit. A less catastrophic issue was that it was possible for the wrong count to be printed by the continuous message. This commit fixes the issue by more carefully managing the prompt state and only resetting the ui when the prompt is not in the Prompt.Watch state. --- main/src/main/scala/sbt/Main.scala | 13 ++++++++++--- main/src/main/scala/sbt/MainLoop.scala | 3 +-- main/src/main/scala/sbt/internal/Continuous.scala | 8 +++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index d7b857fa4..ca3a7af8e 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -990,12 +990,19 @@ object BuiltinCommands { val exchange = StandardMain.exchange exchange.channelForName(channel) match { case Some(c) if ContinuousCommands.isInWatch(s0, c) => - c.prompt(ConsolePromptEvent(s0)) + if (c.terminal.prompt != Prompt.Watch) { + c.terminal.setPrompt(Prompt.Watch) + c.prompt(ConsolePromptEvent(s0)) + } else if (c.terminal.isSupershellEnabled) { + c.terminal.printStream.print(ConsoleAppender.ClearScreenAfterCursor) + c.terminal.printStream.flush() + } + val s1 = exchange.run(s0) val exec: Exec = getExec(s1, Duration.Inf) val remaining: List[Exec] = - Exec(s"${ContinuousCommands.waitWatch} $channel", None) :: - Exec(FailureWall, None) :: s1.remainingCommands + Exec(FailureWall, None) :: Exec(s"${ContinuousCommands.waitWatch} $channel", None) :: + s1.remainingCommands val newState = s1.copy(remainingCommands = exec +: remaining) if (exec.commandLine.trim.isEmpty) newState else newState.clearGlobalLog diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 00205d251..1943bc083 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -226,9 +226,8 @@ object MainLoop { // temporarily set the prompt to running during task evaluation c.terminal.setPrompt(Prompt.Running) (() => { - c.terminal.setPrompt(prevPrompt) + if (c.terminal.prompt != Prompt.Watch) c.terminal.setPrompt(prevPrompt) ITerminal.set(prevTerminal) - c.terminal.setPrompt(prevPrompt) c.terminal.flush() }) -> progressState.put(Keys.terminalKey, Terminal(c.terminal)) case _ => (() => ()) -> progressState.put(Keys.terminalKey, Terminal(ITerminal.get)) diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index b416446f7..c5ba2aec9 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -1344,8 +1344,7 @@ private[sbt] object ContinuousCommands { private[sbt] val postWatchCommand = watchCommand(postWatch) { (channel, state) => val cs = watchState(state, channel) StandardMain.exchange.channelForName(channel).foreach { c => - c.terminal.setPrompt(Prompt.Watch) - c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel)))) + c.terminal.setPrompt(Prompt.Pending) } val postState = state.get(watchStates) match { case None => state @@ -1360,7 +1359,10 @@ private[sbt] object ContinuousCommands { cs.callbacks.onExit() StandardMain.exchange .channelForName(channel) - .foreach(_.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel))))) + .foreach { c => + c.terminal.setPrompt(Prompt.Pending) + c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel)))) + } afterWatchState.get(watchStates) match { case None => afterWatchState case Some(w) => afterWatchState.put(watchStates, w - channel) From 2b45183d09b7306baffbb919ca45945936ad036a Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 19 Nov 2020 18:44:58 -0800 Subject: [PATCH 2/2] Cleanup user thread task submission I found this code difficult to reason about so I refactored it so that it was easier for me to understand. --- .../scala/sbt/internal/ui/UserThread.scala | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/ui/UserThread.scala b/main-command/src/main/scala/sbt/internal/ui/UserThread.scala index c547994b9..94f239bc0 100644 --- a/main-command/src/main/scala/sbt/internal/ui/UserThread.scala +++ b/main-command/src/main/scala/sbt/internal/ui/UserThread.scala @@ -31,31 +31,33 @@ private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable private[sbt] def reset(state: State): Unit = if (!isClosed.get) { uiThread.synchronized { val task = channel.makeUIThread(state) - def submit(): Thread = { + def submit(): Unit = { val thread: Thread = new Thread(s"sbt-$name-ui-thread") { setDaemon(true) override def run(): Unit = try task.run() - finally uiThread.get match { - case (_, t) if t == this => uiThread.set(null) - case _ => + finally { + uiThread.getAndSet(null) match { + case prev @ (_, th) if th != this => uiThread.set(prev) + case _ => + } } } uiThread.getAndSet((task, thread)) match { case null => thread.start() - case (task, t) if t.getClass != task.getClass => - stopThreadImpl() + case (prevTask, prevThread) if prevTask.getClass != task.getClass => + prevTask.close() + prevThread.joinFor(1.second) thread.start() case t => uiThread.set(t) } - thread } uiThread.get match { - case null => uiThread.set((task, submit())) - case (t, _) if t.getClass == task.getClass => + case null => submit() + case (prevTask, _) if prevTask.getClass == task.getClass => case (t, thread) => stopThreadImpl() - uiThread.set((task, submit())) + submit() } } Option(lastProgressEvent.get).foreach(onProgressEvent)