From 261084bbb235aafb0c75fcb3ea0f19b48fb877a0 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 27 Jun 2020 11:00:57 -0700 Subject: [PATCH] Avoid leaking sbt processes On windows, it is sometimes possible to leak an sbt process if two processes are started simultaneously by a remote client at the same time. When this happens, the second process is unable to create a server because of the first process and it also has no io streams because the the client detaches its streams. We can detect this in the shell command and prevent the process from persisting as a zombie. --- .../scala/sbt/internal/util/Terminal.scala | 1 + main/src/main/scala/sbt/Main.scala | 41 +++++++++++-------- .../scala/sbt/internal/CommandExchange.scala | 1 + 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index b1aa51318..bd0fb7774 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -538,6 +538,7 @@ object Terminal { case _ => None } } + private[sbt] def startedByRemoteClient = props.isDefined /** * Creates an instance of [[Terminal]] that delegates most of its methods to an underlying diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index a2c85eb6e..58f2e20cd 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -1019,22 +1019,31 @@ object BuiltinCommands { val exchange = StandardMain.exchange val welcomeState = displayWelcomeBanner(s0) val s1 = exchange run welcomeState - exchange prompt ConsolePromptEvent(s0) - val minGCInterval = Project - .extract(s1) - .getOpt(Keys.minForcegcInterval) - .getOrElse(GCUtil.defaultMinForcegcInterval) - val exec: Exec = getExec(s1, minGCInterval) - val newState = s1 - .copy( - onFailure = Some(Exec(Shell, None)), - remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands - ) - .setInteractive(true) - val res = - if (exec.commandLine.trim.isEmpty) newState - else newState.clearGlobalLog - res + /* + * It is possible for sbt processes to leak if two are started simultaneously + * by a remote client and only one is able to start a server. This seems to + * happen primarily on windows. + */ + if (Terminal.startedByRemoteClient && !exchange.hasServer) { + Exec("shutdown", None) +: s1 + } else { + exchange prompt ConsolePromptEvent(s0) + val minGCInterval = Project + .extract(s1) + .getOpt(Keys.minForcegcInterval) + .getOrElse(GCUtil.defaultMinForcegcInterval) + val exec: Exec = getExec(s1, minGCInterval) + val newState = s1 + .copy( + onFailure = Some(Exec(Shell, None)), + remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands + ) + .setInteractive(true) + val res = + if (exec.commandLine.trim.isEmpty) newState + else newState.clearGlobalLog + res + } } def startServer: Command = diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index aa35b0cf6..25f74d7e1 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -53,6 +53,7 @@ private[sbt] final class CommandExchange { private val nextChannelId: AtomicInteger = new AtomicInteger(0) private[this] val lastState = new AtomicReference[State] private[this] val currentExecRef = new AtomicReference[Exec] + private[sbt] def hasServer = server.isDefined def channels: List[CommandChannel] = channelBuffer.toList