From ea823f105101fe8a1ae62c45cabdb6b0256e2262 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 23 Jun 2020 18:58:02 -0700 Subject: [PATCH] Add server idle timeout This commit adds the ability for sbt to automatically shut itself down if it has been idle for some duration of time. The motivation is that if the user may not realize they have an sbt server running in the background that is using resources. We don't want to be too aggressive with the idle timeout because that can reduce the efficacy of the thin client. A value of one week is chosen so that users can enjoy a long weekend and when they return to their computer, they won't have to restart sbt. If they haven't used the server in at least a week, it seems prudent to just kill it. --- .../src/main/scala/sbt/BasicKeys.scala | 8 ++++ main/src/main/scala/sbt/Defaults.scala | 1 + main/src/main/scala/sbt/Keys.scala | 1 + main/src/main/scala/sbt/Project.scala | 4 ++ .../scala/sbt/internal/CommandExchange.scala | 38 +++++++++++++++---- 5 files changed, 44 insertions(+), 8 deletions(-) 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 =