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.
This commit is contained in:
Ethan Atkins 2020-06-27 11:00:57 -07:00
parent ae2899baae
commit 261084bbb2
3 changed files with 27 additions and 16 deletions

View File

@ -538,6 +538,7 @@ object Terminal {
case _ => None case _ => None
} }
} }
private[sbt] def startedByRemoteClient = props.isDefined
/** /**
* Creates an instance of [[Terminal]] that delegates most of its methods to an underlying * Creates an instance of [[Terminal]] that delegates most of its methods to an underlying

View File

@ -1019,22 +1019,31 @@ object BuiltinCommands {
val exchange = StandardMain.exchange val exchange = StandardMain.exchange
val welcomeState = displayWelcomeBanner(s0) val welcomeState = displayWelcomeBanner(s0)
val s1 = exchange run welcomeState val s1 = exchange run welcomeState
exchange prompt ConsolePromptEvent(s0) /*
val minGCInterval = Project * It is possible for sbt processes to leak if two are started simultaneously
.extract(s1) * by a remote client and only one is able to start a server. This seems to
.getOpt(Keys.minForcegcInterval) * happen primarily on windows.
.getOrElse(GCUtil.defaultMinForcegcInterval) */
val exec: Exec = getExec(s1, minGCInterval) if (Terminal.startedByRemoteClient && !exchange.hasServer) {
val newState = s1 Exec("shutdown", None) +: s1
.copy( } else {
onFailure = Some(Exec(Shell, None)), exchange prompt ConsolePromptEvent(s0)
remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands val minGCInterval = Project
) .extract(s1)
.setInteractive(true) .getOpt(Keys.minForcegcInterval)
val res = .getOrElse(GCUtil.defaultMinForcegcInterval)
if (exec.commandLine.trim.isEmpty) newState val exec: Exec = getExec(s1, minGCInterval)
else newState.clearGlobalLog val newState = s1
res .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 = def startServer: Command =

View File

@ -53,6 +53,7 @@ private[sbt] final class CommandExchange {
private val nextChannelId: AtomicInteger = new AtomicInteger(0) private val nextChannelId: AtomicInteger = new AtomicInteger(0)
private[this] val lastState = new AtomicReference[State] private[this] val lastState = new AtomicReference[State]
private[this] val currentExecRef = new AtomicReference[Exec] private[this] val currentExecRef = new AtomicReference[Exec]
private[sbt] def hasServer = server.isDefined
def channels: List[CommandChannel] = channelBuffer.toList def channels: List[CommandChannel] = channelBuffer.toList