Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run in background #738

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/src/main/scala/mdoc/internal/cli/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ case class Settings(
@Description("Start a file watcher and incrementally re-generate the site on file save.")
@ExtraName("w")
watch: Boolean = false,
@Description("Sets the file watcher to run in the background and not ask for user input in order to stop.")
@ExtraName("b")
background: Boolean = false,
@Description(
"Instead of generating a new site, report an error if generating the site would produce a diff " +
"against an existing site. Useful for asserting in CI that a site is up-to-date."
Expand Down
7 changes: 7 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ performance.
> docs/mdoc --watch
```

You can run mdoc in the background so that you can issue other commands to sbt.

```scala
> docs/mdocBgStart
> docs/mdocBgStop
```

See [`--help`](#help) to learn more how to use the command-line interface.

```scala
Expand Down
67 changes: 65 additions & 2 deletions mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package mdoc

import java.io.File
import sbt.Keys._
import sbt._
import sbt.{taskKey, _}

import scala.collection.mutable.ListBuffer

object MdocPlugin extends AutoPlugin {
Expand Down Expand Up @@ -47,6 +48,15 @@ object MdocPlugin extends AutoPlugin {
"If false, do not add mdoc as a library dependency this project. " +
"Default value is true."
)
val mdocBgStart =
inputKey[JobHandle](
"Run mdoc in the background. " +
"By default it runs with arguments --watch and --background (via `mdocBgStart/mdocExtraArguments`)."
)
val mdocBgStop =
taskKey[Unit](
"Stops mdoc that is running in the background."
)
}
val mdocInternalVariables =
settingKey[List[(String, String)]](
Expand All @@ -71,19 +81,23 @@ object MdocPlugin extends AutoPlugin {
"if not provided, the classpath will be formed by resolving the worker dependency"
)

// The macro is overzealous and prevents us using this.
private val showKey = Def.showFullKey.show _

override def projectSettings: Seq[Def.Setting[_]] =
List(
mdocIn := baseDirectory.in(ThisBuild).value / "docs",
mdocOut := target.in(Compile).value / "mdoc",
mdocVariables := Map.empty,
mdocExtraArguments := Nil,
mdocExtraArguments.in(mdocBgStart) := Vector("--watch", "--background"),
mdocJS := None,
mdocJSLibraries := Nil,
mdocJSWorkerClasspath := None,
mdocAutoDependency := true,
mdocInternalVariables := Nil,
mdoc := Def.inputTaskDyn {
validateSettings.value
val _ = validateSettings.value
val parsed = sbt.complete.DefaultParsers.spaceDelimited("<arg>").parsed
val args = Iterator(
mdocExtraArguments.value,
Expand All @@ -93,6 +107,55 @@ object MdocPlugin extends AutoPlugin {
runMain.in(Compile).toTask(s" mdoc.SbtMain $args")
}
}.evaluated,
// Workaround for https://github.com/sbt/sbt/issues/3572.
mdocBgStart := InputTask
.createDyn[Seq[String], JobHandle] {
InputTask.initParserAsInput(Def.setting {
sbt.complete.DefaultParsers.spaceDelimited("<arg>")
})
} {
Def.task { parsed =>
Def.taskDyn {
val _ = validateSettings.value
val args = Iterator(
mdocExtraArguments.in(mdocBgStart).value,
parsed
).flatten.mkString(" ")
val service = bgJobService.value
val spawningTask = resolvedScoped.value
val s = state.value
service.jobs.find(_.spawningTask == spawningTask) match {
case Some(jobHandle) =>
Def.task {
s.log.info(s"mdoc is already running in the background")
jobHandle
}
case None =>
// Use this rather than bgRunMain so that the spawningTask is set correctly.
Defaults.bgRunMainTask(
exportedProductJars.in(Compile),
fullClasspathAsJars.in(Compile),
bgCopyClasspath.in(Compile, bgRunMain),
runner.in(run)
).toTask(s" mdoc.SbtMain $args")
}
}
}
}.evaluated,
mdocBgStop := Def.task {
val service = bgJobService.value
val spawningTask = resolvedScoped.value.copy(key = mdocBgStart.key)
val s = state.value
s.log.debug(s"looking for background job that was spawned by ${showKey(spawningTask)}")
service.jobs.find(_.spawningTask == spawningTask) match {
case Some(jobHandle) =>
s.log.info(s"stopping mdoc")
service.stop(jobHandle)
service.waitFor(jobHandle)
case None =>
s.log.info(s"mdoc is not running in the background")
}
}.value,
dependencyOverrides ++= List(
"org.scala-lang" %% "scala3-library" % scalaVersion.value,
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value,
Expand Down
14 changes: 14 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ThisBuild / scalaVersion := "2.12.17"

enablePlugins(MdocPlugin)

TaskKey[Unit]("check") := {
SbtTest.test(
TestCommand("mdocBgStart", "Waiting for file changes (press enter to interrupt)"),
TestCommand("show version", "[info] 0.1.0-SNAPSHOT"),
TestCommand("mdocBgStart", "mdoc is already running in the background"),
TestCommand("mdocBgStop", "stopping mdoc"),
TestCommand("mdocBgStop", "mdoc is not running in the background"),
TestCommand("exit")
)
}
3 changes: 3 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```scala mdoc
println(example.Example.greeting)
```
77 changes: 77 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import java.nio.charset.StandardCharsets
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.sys.process._

object SbtTest {

def test(commands: TestCommand*) = {
val commandsToSend = new LinkedBlockingQueue[String]()
def sendInput(output: java.io.OutputStream): Unit = {
val newLine = "\n".getBytes(StandardCharsets.UTF_8)
try {
while (true) {
val command = commandsToSend.take()
output.write(command.getBytes(StandardCharsets.UTF_8))
output.write(newLine)
output.flush()
}
} catch {
case _: InterruptedException => // Ignore
} finally {
output.close()
}
}

val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*)
var expectedOutput: Option[String] = Some("[info] started sbt server")
def processOut(out: String): Unit = {
if (expectedOutput.forall(out.endsWith)) {
if (commandQueue.nonEmpty) {
val command = commandQueue.dequeue()
Thread.sleep(command.delay.toMillis)
commandsToSend.put(command.command)
expectedOutput = command.expectedOutput
}
}
println(s"[SbtTest] $out")
}

val error = new StringBuilder()
def processError(err: String): Unit = {
println(s"[SbtTest error] $err")
error.append(err)
}

// TODO: Do we need the -Xmx setting and any other future options?
val command = Seq(
"sbt",
s"-Dplugin.version=${sys.props("plugin.version")}",
"--no-colors",
"--supershell=never"
)
val logger = ProcessLogger(processOut, processError)
val basicIO = BasicIO(withIn = false, logger)
val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError)
val p = command.run(io)

val deadline = 30.seconds.fromNow
Future {
while (p.isAlive()) {
if (deadline.isOverdue()) {
p.destroy()
}
}
}

val code = p.exitValue()

expectedOutput.foreach { expected =>
throw new AssertionError(s"Expected to find output: $expected")
}
assert(code == 0, s"Expected exit code 0 but got $code")
}
}
22 changes: 22 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.concurrent.duration._

/**
* @param command the command to send
* @param expectedOutput expected output of the command
* @param delay time to wait before sending the command
*/
final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration)

object TestCommand {
def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand =
TestCommand(command, Some(expectedOutput), delay)

def apply(command: String, expectedOutput: String): TestCommand =
TestCommand(command, Some(expectedOutput), Duration.Zero)

def apply(command: String, delay: FiniteDuration): TestCommand =
TestCommand(command, None, delay)

def apply(command: String): TestCommand =
TestCommand(command, None, Duration.Zero)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version = 1.8.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package example

object Example {
def greeting = "Hello world!"
}
3 changes: 3 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Not sure why we need to do this. If we don't it fails with java.nio.file.NoSuchFileException.
$ mkdir target/mdoc
> check
21 changes: 21 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.concurrent.duration._
import MdocPlugin._

ThisBuild / scalaVersion := "2.12.17"

enablePlugins(MdocPlugin)

InputKey[Unit]("mdocBg") := Def.inputTaskDyn {
validateSettings.value
val parsed = sbt.complete.DefaultParsers.spaceDelimited("<arg>").parsed
val args = (mdocExtraArguments.value ++ parsed).mkString(" ")
(Compile / bgRunMain).toTask(s" mdoc.SbtMain $args")
}.evaluated

TaskKey[Unit]("check") := {
SbtTest.test(
TestCommand("mdocBg --watch --background", "Waiting for file changes (press enter to interrupt)"),
TestCommand("show version", "[info] 0.1.0-SNAPSHOT", 3.seconds),
TestCommand("exit")
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```scala mdoc
println(example.Example.greeting)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import java.nio.charset.StandardCharsets
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.sys.process._

object SbtTest {

def test(commands: TestCommand*) = {
val commandsToSend = new LinkedBlockingQueue[String]()
def sendInput(output: java.io.OutputStream): Unit = {
val newLine = "\n".getBytes(StandardCharsets.UTF_8)
try {
while (true) {
val command = commandsToSend.take()
output.write(command.getBytes(StandardCharsets.UTF_8))
output.write(newLine)
output.flush()
}
} catch {
case _: InterruptedException => // Ignore
} finally {
output.close()
}
}

val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*)
var expectedOutput: Option[String] = Some("[info] started sbt server")
def processOut(out: String): Unit = {
if (expectedOutput.forall(out.endsWith)) {
if (commandQueue.nonEmpty) {
val command = commandQueue.dequeue()
Thread.sleep(command.delay.toMillis)
commandsToSend.put(command.command)
expectedOutput = command.expectedOutput
}
}
println(s"[SbtTest] $out")
}

val error = new StringBuilder()
def processError(err: String): Unit = {
println(s"[SbtTest error] $err")
error.append(err)
}

// TODO: Do we need the -Xmx setting and any other future options?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO: Do we need the -Xmx setting and any other future options?

I don't think we need any other options for the test, should we remove the comment?

val command = Seq(
"sbt",
s"-Dplugin.version=${sys.props("plugin.version")}",
"--no-colors",
"--supershell=never"
)
val logger = ProcessLogger(processOut, processError)
val basicIO = BasicIO(withIn = false, logger)
val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError)
val p = command.run(io)

val deadline = 30.seconds.fromNow
Future {
while (p.isAlive()) {
if (deadline.isOverdue()) {
p.destroy()
}
}
}

val code = p.exitValue()

expectedOutput.foreach { expected =>
throw new AssertionError(s"Expected to find output: $expected")
}
assert(code == 0, s"Expected exit code 0 but got $code")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.concurrent.duration._

/**
* @param command the command to send
* @param expectedOutput expected output of the command
* @param delay time to wait before sending the command
*/
final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration)

object TestCommand {
def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand =
TestCommand(command, Some(expectedOutput), delay)

def apply(command: String, expectedOutput: String): TestCommand =
TestCommand(command, Some(expectedOutput), Duration.Zero)

def apply(command: String, delay: FiniteDuration): TestCommand =
TestCommand(command, None, delay)

def apply(command: String): TestCommand =
TestCommand(command, None, Duration.Zero)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version = 1.8.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version"))
Loading