Skip to content

Commit

Permalink
Implement client-side run
Browse files Browse the repository at this point in the history
**Problem**
`run` task has been emulated via function call inside of a sandboxed classloader,
and blocking the command processing of sbt server loop.
This poses isolation and availability issues.

**Solution**
This implements client-side run where the server creates a sandbox environment, and sends the information to the client,
and the client forks a new JVM to perform the run.
The client-side behavior has been implemented in sbtn side already.
  • Loading branch information
eed3si9n committed Mar 9, 2025
1 parent e595914 commit 95fac04
Show file tree
Hide file tree
Showing 30 changed files with 354 additions and 319 deletions.
7 changes: 5 additions & 2 deletions main-command/src/main/scala/sbt/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ final case class State(
}

def source: Option[CommandSource] =
currentCommand match {
currentCommand match
case Some(x) => x.source
case _ => None
}
def isNetworkCommand: Boolean =
source match
case Some(s) => s.channelName.startsWith("network")
case _ => false
}

/**
Expand Down
189 changes: 9 additions & 180 deletions main/src/main/scala/sbt/Defaults.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import sbt.internal.server.{
BspCompileTask,
BuildServerProtocol,
BuildServerReporter,
ClientJob,
Definition,
LanguageServerProtocol,
ServerHandler,
Expand Down Expand Up @@ -141,6 +140,7 @@ import xsbti.compile.{
TransactionalManagerType
}
import sbt.internal.IncrementalTest
import sbt.internal.RunUtil

object Defaults extends BuildCommon {
final val CacheDirectoryName = "cache"
Expand Down Expand Up @@ -241,7 +241,7 @@ object Defaults extends BuildCommon {
getRootPaths(out, app) + ("CSR_CACHE" -> coursierCache)
},
fileConverter := MappedFileConverter(rootPaths.value, allowMachinePath.value)
) ++ BuildServerProtocol.globalSettings ++ ClientJob.globalSettings
) ++ BuildServerProtocol.globalSettings

private[sbt] def getRootPaths(out: NioPath, app: AppConfiguration): ListMap[String, NioPath] =
val base = app.baseDirectory.getCanonicalFile.toPath
Expand Down Expand Up @@ -391,6 +391,7 @@ object Defaults extends BuildCommon {
aggregate :== true,
maxErrors :== 100,
fork :== false,
clientSide :== true,
initialize :== {},
templateResolverInfos :== Nil,
templateDescriptions :== TemplateCommandUtil.defaultTemplateDescriptions,
Expand Down Expand Up @@ -1035,27 +1036,12 @@ object Defaults extends BuildCommon {
})
pickMainClassOrWarn(discoveredMainClasses.value, streams.value.log, logWarning)
},
runMain := foregroundRunMainTask.evaluated,
run := foregroundRunTask.evaluated,
runBlock := {
val r = run.evaluated
val service = bgJobService.value
service.waitForTry(r.handle).get
()
},
runMainBlock := {
val r = runMain.evaluated
val service = bgJobService.value
service.waitForTry(r.handle).get
()
},
fgRun := runTask(fullClasspath, (run / mainClass), (run / runner)).evaluated,
fgRunMain := runMainTask(fullClasspath, (run / runner)).evaluated,
copyResources := copyResourcesTask.value,
// note that we use the same runner and mainClass as plain run
mainBgRunMainTaskForConfig(This),
mainBgRunTaskForConfig(This)
) ++ inTask(run)(runnerSettings ++ newRunnerSettings) ++ compileIncrementalTaskSettings
) ++ RunUtil.configTasks(This) ++ inTask(run)(
runnerSettings ++ newRunnerSettings
) ++ compileIncrementalTaskSettings

private lazy val configGlobal = globalDefaults(
Seq(
Expand Down Expand Up @@ -1869,122 +1855,6 @@ object Defaults extends BuildCommon {
/** Implements `cleanFiles` task. */
private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] }

private def termWrapper(canonical: Boolean, echo: Boolean): (() => Unit) => (() => Unit) =
(f: () => Unit) =>
() => {
val term = ITerminal.get
if (!canonical) {
term.enterRawMode()
if (echo) term.setEchoEnabled(echo)
} else if (!echo) term.setEchoEnabled(false)
try f()
finally {
if (!canonical) term.exitRawMode()
if (!echo) term.setEchoEnabled(true)
}
}
def bgRunMainTask(
products: Initialize[Task[Classpath]],
classpath: Initialize[Task[Classpath]],
copyClasspath: Initialize[Boolean],
scalaRun: Initialize[Task[ScalaRun]]
): Initialize[InputTask[JobHandle]] = {
val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) =>
Defaults.runMainParser(s, names getOrElse Nil)
)
Def.inputTask {
val service = bgJobService.value
val (mainClass, args) = parser.parsed
val hashClasspath = (bgRunMain / bgHashClasspath).value
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
val converter = fileConverter.value
service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) =>
val cp =
if copyClasspath.value then
service.copyClasspath(
products.value,
classpath.value,
workingDir,
hashClasspath,
converter,
)
else classpath.value
given FileConverter = fileConverter.value
scalaRun.value match
case r: Run =>
val loader = r.newLoader(cp.files)
(
Some(loader),
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
)
case sr =>
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
}
}
}

def bgRunTask(
products: Initialize[Task[Classpath]],
classpath: Initialize[Task[Classpath]],
mainClassTask: Initialize[Task[Option[String]]],
copyClasspath: Initialize[Boolean],
scalaRun: Initialize[Task[ScalaRun]]
): Initialize[InputTask[JobHandle]] =
val parser = Def.spaceDelimited()
Def.inputTask {
val args = parser.parsed
val service = bgJobService.value
val mainClass = mainClassTask.value getOrElse sys.error("No main class detected.")
val hashClasspath = (bgRun / bgHashClasspath).value
val wrapper = termWrapper(canonicalInput.value, echoInput.value)
val converter = fileConverter.value
service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) =>
val cp =
if copyClasspath.value then
service.copyClasspath(
products.value,
classpath.value,
workingDir,
hashClasspath,
converter
)
else classpath.value
given FileConverter = converter
scalaRun.value match
case r: Run =>
val loader = r.newLoader(cp.files)
(
Some(loader),
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
)
case sr =>
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
}
}

// `runMain` calls bgRunMain in the background and pauses the current channel
def foregroundRunMainTask: Initialize[InputTask[EmulateForeground]] =
Def.inputTask {
val handle = bgRunMain.evaluated
handle match
case threadJobHandle: AbstractBackgroundJobService#ThreadJobHandle =>
threadJobHandle.isAutoCancel = true
case _ => ()
EmulateForeground(handle)
}

// `run` task calls bgRun in the background and pauses the current channel
def foregroundRunTask: Initialize[InputTask[EmulateForeground]] =
Def.inputTask {
val handle = bgRun.evaluated
handle match {
case threadJobHandle: AbstractBackgroundJobService#ThreadJobHandle =>
threadJobHandle.isAutoCancel = true
case _ =>
}
EmulateForeground(handle)
}

def runMainTask(
classpath: Initialize[Task[Classpath]],
scalaRun: Initialize[Task[ScalaRun]]
Expand All @@ -2005,15 +1875,7 @@ object Defaults extends BuildCommon {
classpath: Initialize[Task[Classpath]],
mainClassTask: Initialize[Task[Option[String]]],
scalaRun: Initialize[Task[ScalaRun]]
): Initialize[InputTask[Unit]] =
val parser = Def.spaceDelimited()
Def.inputTask {
val in = parser.parsed
val mainClass = mainClassTask.value getOrElse sys.error("No main class detected.")
val cp = classpath.value
given FileConverter = fileConverter.value
scalaRun.value.run(mainClass, cp.files, in, streams.value.log).get
}
): Initialize[InputTask[Unit]] = RunUtil.serverSideRunTask(classpath, mainClassTask, scalaRun)

def runnerTask: Setting[Task[ScalaRun]] = runner := runnerInit.value

Expand Down Expand Up @@ -2173,26 +2035,6 @@ object Defaults extends BuildCommon {
)
)

def mainBgRunTask = mainBgRunTaskForConfig(Select(Runtime))
def mainBgRunMainTask = mainBgRunMainTaskForConfig(Select(Runtime))

private def mainBgRunTaskForConfig(c: ScopeAxis[ConfigKey]) =
bgRun := bgRunTask(
exportedProductJars,
This / c / This / fullClasspathAsJars,
run / mainClass,
bgRun / bgCopyClasspath,
run / runner
).evaluated

private def mainBgRunMainTaskForConfig(c: ScopeAxis[ConfigKey]) =
bgRunMain := bgRunMainTask(
exportedProductJars,
This / c / This / fullClasspathAsJars,
bgRunMain / bgCopyClasspath,
run / runner
).evaluated

def discoverMainClasses(analysis: CompileAnalysis): Seq[String] = analysis match {
case analysis: Analysis =>
analysis.infos.allInfos.values.map(_.getMainClasses).flatten.toSeq.sorted
Expand Down Expand Up @@ -2649,10 +2491,10 @@ object Defaults extends BuildCommon {
lazy val configSettings: Seq[Setting[?]] =
Classpaths.configSettings ++ configTasks ++ configPaths ++ packageConfig ++
Classpaths.compilerPluginConfig ++ deprecationSettings ++
BuildServerProtocol.configSettings ++ ClientJob.configSettings
BuildServerProtocol.configSettings

lazy val compileSettings: Seq[Setting[?]] =
configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ Classpaths.addUnmanagedLibrary
configSettings ++ RunUtil.configTasks(Select(Runtime)) ++ Classpaths.addUnmanagedLibrary

lazy val testSettings: Seq[Setting[?]] = configSettings ++ testTasks

Expand Down Expand Up @@ -4685,19 +4527,6 @@ trait BuildExtra extends BuildCommon with DefExtra {
r.run(mainClass, cp.files, baseArguments ++ args, streams.value.log).get
}

def runTask(
config: Configuration,
mainClass: String,
arguments: String*
): Initialize[Task[Unit]] =
Def.task {
given FileConverter = fileConverter.value
val cp = (config / fullClasspath).value
val r = (config / run / runner).value
val s = streams.value
r.run(mainClass, cp.files, arguments, s.log).get
}

// public API
/** Returns a vector of settings that create custom run input task. */
@nowarn
Expand Down
9 changes: 3 additions & 6 deletions main/src/main/scala/sbt/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,8 @@ object Keys {
// Run Keys
val selectMainClass = taskKey[Option[String]]("Selects the main class to run.").withRank(BMinusTask)
val mainClass = taskKey[Option[String]]("Defines the main class for packaging or running.").withRank(BPlusTask)
val run = inputKey[EmulateForeground]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask)
val runBlock = inputKey[Unit]("Runs a main class, and blocks until it's done.").withRank(DTask)
val runMain = inputKey[EmulateForeground]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val runMainBlock = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(DTask)
val run = inputKey[Unit | ClientJobParams]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask)
val runMain = inputKey[Unit | ClientJobParams]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask)
val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting)
Expand All @@ -332,6 +330,7 @@ object Keys {
val discoveredJavaHomes = settingKey[Map[String, File]]("Discovered Java home directories")
val javaHomes = settingKey[Map[String, File]]("The user-defined additional Java home directories")
val fullJavaHomes = settingKey[Map[String, File]]("Combines discoveredJavaHomes and custom javaHomes.").withRank(CTask)
val clientSide = settingKey[Boolean]("If true, takes the action on the client-side")

val javaOptions = taskKey[Seq[String]]("Options passed to a new JVM when forking.").withRank(BPlusTask)
val envVars = taskKey[Map[String, String]]("Environment variables used when forking a new JVM").withRank(BTask)
Expand Down Expand Up @@ -484,8 +483,6 @@ object Keys {

@cacheLevel(include = Array.empty)
val bspReporter = taskKey[BuildServerReporter]("").withRank(DTask)
val clientJob = inputKey[ClientJobParams]("Translates a task into a job specification").withRank(Invisible)
val clientJobRunInfo = inputKey[ClientJobParams]("Translates the run task into a job specification").withRank(Invisible)

val csrCacheDirectory = settingKey[File]("Coursier cache directory. Uses -Dsbt.coursier.home or Coursier's default.").withRank(CSetting)
val csrMavenProfiles = settingKey[Set[String]]("").withRank(CSetting)
Expand Down
Loading

0 comments on commit 95fac04

Please sign in to comment.