From d3f8cc81611bc570acfb698f9e82cb0b9ae4f380 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 22 Aug 2020 14:35:00 -0700 Subject: [PATCH] Kill external processes on sigint On linux and mac, entering ctrl+c will automatically kill any forked processes that were created by the sbt server because sigint is automatically forwarded to the child process. This is not the case on windows where it is necessary to forcibly kill these processes. --- .../sbt/internal/util/RunningProcesses.scala | 33 +++++++++++++++++++ .../src/main/scala/sbt/ForkTests.scala | 10 ++++-- main/src/main/scala/sbt/EvaluateTask.scala | 1 + .../scala/sbt/scriptedtest/SbtHandler.scala | 6 ++++ 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala 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