diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index bee82e9b5..9a85658f0 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -16,6 +16,7 @@ import sbt.internal.server.ServerHandler import sbt.internal.util.{ AttributeKey, Terminal } import sbt.librarymanagement.ModuleID import sbt.util.Level +import scala.concurrent.duration.FiniteDuration object BasicKeys { val historyPath = AttributeKey[Option[File]]( @@ -83,6 +84,13 @@ object BasicKeys { 10000 ) + val serverIdleTimeout = + AttributeKey[Option[FiniteDuration]]( + "serverIdleTimeOut", + "If set to a defined value, sbt server will exit if it goes at least the specified duration without receiving any commands.", + 10000 + ) + // Unlike other BasicKeys, this is not used directly as a setting key, // and severLog / logLevel is used instead. private[sbt] val serverLogLevel = diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 59e030808..71320e636 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -378,6 +378,7 @@ object Defaults extends BuildCommon { interactionService :== CommandLineUIService, autoStartServer := true, serverHost := "127.0.0.1", + serverIdleTimeout := Some(new FiniteDuration(7, TimeUnit.DAYS)), serverPort := 5000 + (Hash .toHex(Hash(appConfiguration.value.baseDirectory.toString)) .## % 1000), diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 30ff9df2d..9265c223f 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -95,6 +95,7 @@ object Keys { val serverHost = SettingKey(BasicKeys.serverHost) val serverAuthentication = SettingKey(BasicKeys.serverAuthentication) val serverConnectionType = SettingKey(BasicKeys.serverConnectionType) + val serverIdleTimeout = SettingKey(BasicKeys.serverIdleTimeout) val windowsServerSecurityLevel = SettingKey(BasicKeys.windowsServerSecurityLevel) val fullServerHandlers = SettingKey(BasicKeys.fullServerHandlers) val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.") diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 8ee5c2e4c..80f4ab595 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -24,6 +24,7 @@ import Keys.{ templateResolverInfos, autoStartServer, serverHost, + serverIdleTimeout, serverLog, serverPort, serverAuthentication, @@ -52,6 +53,7 @@ import sbt.util.{ Show, Level } import sjsonnew.JsonFormat import language.experimental.macros +import scala.concurrent.duration.FiniteDuration sealed trait ProjectDefinition[PR <: ProjectReference] { @@ -515,6 +517,7 @@ object Project extends ProjectExtra { val startSvr: Option[Boolean] = get(autoStartServer) val host: Option[String] = get(serverHost) val port: Option[Int] = get(serverPort) + val timeout: Option[Option[FiniteDuration]] = get(serverIdleTimeout) val authentication: Option[Set[ServerAuthentication]] = get(serverAuthentication) val connectionType: Option[ConnectionType] = get(serverConnectionType) val srvLogLevel: Option[Level.Value] = (logLevel in (ref, serverLog)).get(structure.data) @@ -534,6 +537,7 @@ object Project extends ProjectExtra { .setCond(serverHost.key, host) .setCond(serverAuthentication.key, authentication) .setCond(serverConnectionType.key, connectionType) + .setCond(serverIdleTimeout.key, timeout) .put(historyPath.key, history) .put(templateResolverInfos.key, trs) .setCond(shellPrompt.key, prompt) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 36985a5c0..92d526292 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -71,14 +71,36 @@ private[sbt] final class CommandExchange { state: Option[State], logger: Logger ): Exec = { - @tailrec def impl(deadline: Option[Deadline]): Exec = { + val idleDeadline = state.flatMap { s => + lastState.set(s) + s.get(BasicKeys.serverIdleTimeout) match { + case Some(Some(d)) => Some(d.fromNow) + case _ => None + } + } + @tailrec def impl(gcDeadline: Option[Deadline], idleDeadline: Option[Deadline]): Exec = { state.foreach(s => prompt(ConsolePromptEvent(s))) - def poll: Option[Exec] = + def poll: Option[Exec] = { + val deadline = gcDeadline.toSeq ++ idleDeadline match { + case s @ Seq(_, _) => Some(s.min) + case s => s.headOption + } Option(deadline match { case Some(d: Deadline) => - commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS) + commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS) match { + case null if idleDeadline.fold(false)(_.isOverdue) => + state.foreach { s => + s.get(BasicKeys.serverIdleTimeout) match { + case Some(Some(d)) => s.log.info(s"sbt idle timeout of $d expired") + case _ => + } + } + Exec("exit", Some(CommandSource(ConsoleChannel.defaultName))) + case x => x + } case _ => commandQueue.take }) + } poll match { case Some(exec) if exec.source.fold(true)(s => channels.exists(_.name == s.channelName)) => exec.commandLine match { @@ -92,24 +114,24 @@ private[sbt] final class CommandExchange { } match { case Some(c) if c.isAttached => c.shutdown(false) - impl(deadline) + impl(gcDeadline, idleDeadline) case _ => exec } case _ => exec } case None => - val newDeadline = if (deadline.fold(false)(_.isOverdue())) { + val newDeadline = if (gcDeadline.fold(false)(_.isOverdue())) { GCUtil.forceGcWithInterval(interval, logger) None - } else deadline - impl(newDeadline) + } else gcDeadline + impl(newDeadline, idleDeadline) } } // Do not manually run GC until the user has been idling for at least the min gc interval. impl(interval match { case d: FiniteDuration => Some(d.fromNow) case _ => None - }) + }, idleDeadline) } private def addConsoleChannel(): Unit =