diff --git a/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala new file mode 100644 index 000000000..47da42d34 --- /dev/null +++ b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala @@ -0,0 +1,33 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.util.concurrent.ConcurrentHashMap +import scala.sys.process.Process + +/** + * Manages forked processes created by sbt. Any process registered + * with RunningProcesses can be killed with the killAll method. In + * particular, this can be used in a signal handler to kill these + * processes when the user inputs ctrl+c. + */ +private[sbt] object RunningProcesses { + val active = ConcurrentHashMap.newKeySet[Process] + def add(process: Process): Unit = active.synchronized { + active.add(process) + () + } + def remove(process: Process): Unit = active.synchronized { + active.remove(process) + () + } + def killAll(): Unit = active.synchronized { + active.forEach(_.destroy()) + active.clear() + } +} diff --git a/main-actions/src/main/scala/sbt/ForkTests.scala b/main-actions/src/main/scala/sbt/ForkTests.scala index 8f6b030c5..39a9c35c3 100755 --- a/main-actions/src/main/scala/sbt/ForkTests.scala +++ b/main-actions/src/main/scala/sbt/ForkTests.scala @@ -17,8 +17,8 @@ import sbt.io.IO import sbt.util.Logger import sbt.ConcurrentRestrictions.Tag import sbt.protocol.testing._ -import sbt.internal.util.ConsoleAppender import sbt.internal.util.Util.{ AnyOps, none } +import sbt.internal.util.{ ConsoleAppender, RunningProcesses } private[sbt] object ForkTests { def apply( @@ -147,7 +147,13 @@ private[sbt] object ForkTests { classOf[ForkMain].getCanonicalName, server.getLocalPort.toString ) - val ec = Fork.java(fork, options) + val p = Fork.java.fork(fork, options) + RunningProcesses.add(p) + val ec = try p.exitValue() + finally { + if (p.isAlive) p.destroy() + RunningProcesses.remove(p) + } val result = if (ec != 0) TestOutput( diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 59bf062f3..09aa2e278 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -480,6 +480,7 @@ object EvaluateTask { def cancelAndShutdown(): Unit = { println("") log.warn("Canceling execution...") + RunningProcesses.killAll() ConcurrentRestrictions.cancelAll() shutdown() } diff --git a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/SbtHandler.scala b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/SbtHandler.scala index 78ec27c87..b8af69c58 100644 --- a/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/SbtHandler.scala +++ b/scripted-sbt-redux/src/main/scala/sbt/scriptedtest/SbtHandler.scala @@ -14,12 +14,14 @@ import java.net.SocketException import scala.sys.process.Process import sbt.internal.scripted.{ StatementHandler, TestFailed } +import sbt.internal.util.RunningProcesses import xsbt.IPC final case class SbtInstance(process: Process, server: IPC.Server) final class SbtHandler(remoteSbtCreator: RemoteSbtCreator) extends StatementHandler { + Signals.register(() => RunningProcesses.killAll()) type State = Option[SbtInstance] @@ -63,6 +65,9 @@ final class SbtHandler(remoteSbtCreator: RemoteSbtCreator) extends StatementHand () } catch { case _: IOException => process.destroy() + } finally { + if (process.isAlive) process.destroy() + RunningProcesses.remove(process) } } @@ -76,6 +81,7 @@ final class SbtHandler(remoteSbtCreator: RemoteSbtCreator) extends StatementHand def newRemote(server: IPC.Server): Process = { val p = remoteSbtCreator.newRemote(server) + RunningProcesses.add(p) try receive("Remote sbt initialization failed", server) catch { case _: SocketException => throw new TestFailed("Remote sbt initialization failed") } p