From c24e7da844c282a8e20dfd6dc82e99a965889b82 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 25 Nov 2020 13:16:34 -0800 Subject: [PATCH 1/3] Rethrow InterruptedException instead of ClosedException There are cases where sbt will incorrectly shutdown if the jline reader is interrupted while filling the input buffer. To fix this we can throw an InterruptedException instead of a ClosedException. The repro for this was start `sbt`, input `~compile` and while sbt was starting up, open a source file with vim using the metals bsp integration. sbt server would end up shutting down everytime after a single compilation iteration. --- .../util-logging/src/main/scala/sbt/internal/util/JLine3.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala index 64d84c1da..a4811a2cc 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala @@ -172,7 +172,7 @@ private[sbt] object JLine3 { if (buffer.isEmpty && !peek) fillBuffer() (if (peek) buffer.peek else buffer.take) match { case null => -2 - case i => if (i == -3) throw new ClosedException else i + case i => if (i == -3) throw new InterruptedException else i } } override def peek(timeout: Long): Int = buffer.peek() match { From 90ca463c700567b751c6bf6d13fb6946abe16928 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 25 Nov 2020 14:03:10 -0800 Subject: [PATCH 2/3] Add signal registration in LineReader It is possible for the signal handler to get in a state where it has no effect in the shell. When this happens, entering ctrl+c does not exit the shell. To ensure that ctrl+c always exits the shell, we can register a signal handler in the line reader that write -1 to the terminal input stream, which should cause the line reader to return an exit command. --- .../src/main/scala/sbt/internal/util/LineReader.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala index aa022d3bb..ef1a345b2 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala @@ -121,6 +121,10 @@ object LineReader { // ignore } historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f)) + val signalRegistration = terminal match { + case _: Terminal.ConsoleTerminal => Some(Signals.register(() => terminal.write(-1))) + case _ => None + } try terminal.withRawInput { Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt))) } catch { @@ -132,6 +136,7 @@ object LineReader { _: UncheckedIOException => throw new InterruptedException } finally { + signalRegistration.foreach(_.remove()) terminal.prompt.reset() term.close() } From 5c508e4275743079e96ad96fa062adb59e57e9f9 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 25 Nov 2020 13:07:14 -0800 Subject: [PATCH 3/3] Fix waitWatch failure handling The waitWatch command is very similar to shell in that it should override the onFailure command to be itself. It also enqueues itself to remaining commands whenever it reads a new command which made it unnecessary to append waitWatch to the runCommmand in Continuous. --- main/src/main/scala/sbt/Main.scala | 10 ++++++---- main/src/main/scala/sbt/internal/Continuous.scala | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 99e37e963..f2e7a2e9e 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -1003,10 +1003,12 @@ object BuiltinCommands { val s1 = exchange.run(s0) val exec: Exec = getExec(s1, Duration.Inf) - val remaining: List[Exec] = - Exec(FailureWall, None) :: Exec(s"${ContinuousCommands.waitWatch} $channel", None) :: - s1.remainingCommands - val newState = s1.copy(remainingCommands = exec +: remaining) + val wait = s"${ContinuousCommands.waitWatch} $channel" + val onFailure = + s1.onFailure.map(of => if (of.commandLine == Shell) of.withCommandLine(wait) else of) + val waitExec = Exec(wait, None) + val remaining: List[Exec] = Exec(FailureWall, None) :: waitExec :: s1.remainingCommands + val newState = s1.copy(remainingCommands = exec +: remaining, onFailure = onFailure) if (exec.commandLine.trim.isEmpty) newState else newState.clearGlobalLog case _ => s0 diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index c5ba2aec9..92801f32b 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -116,7 +116,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { case None => StandardMain.exchange.run(s) -> ConsoleChannel.defaultName } val ws = ContinuousCommands.setupWatchState(channel, initialCount, commands, s1) - s"${ContinuousCommands.runWatch} $channel" :: ws + s"${ContinuousCommands.runWatch} $channel" :: s"${ContinuousCommands.waitWatch} $channel" :: ws } @deprecated("The input task version of watch is no longer available", "1.4.0") @@ -1279,7 +1279,7 @@ private[sbt] object ContinuousCommands { case None => state case Some(cs) => val pre = StashOnFailure :: s"$preWatch $channel" :: Nil - val post = FailureWall :: PopOnFailure :: s"$postWatch $channel" :: s"$waitWatch $channel" :: Nil + val post = FailureWall :: PopOnFailure :: s"$postWatch $channel" :: Nil pre ::: cs.commands.toList ::: post ::: state } }