diff --git a/scala-cli-runner/src/main/scala/org/scastie/scalacli/BspClient.scala b/scala-cli-runner/src/main/scala/org/scastie/scalacli/BspClient.scala index 9405bb644..e84405b1f 100644 --- a/scala-cli-runner/src/main/scala/org/scastie/scalacli/BspClient.scala +++ b/scala-cli-runner/src/main/scala/org/scastie/scalacli/BspClient.scala @@ -76,6 +76,8 @@ object BspClient { Option(diag.getRange.getStart.getLine + Instrument.getMessageLineOffset(isWorksheet, isScalaCli = true)), diag.getMessage() ) + + val scalaCliExec = Seq("cs", "launch", "org.virtuslab.scala-cli:cliBootstrapped:latest.release", "-M", "scala.cli.ScalaCli", "--") } trait ScalaCliServer extends BuildServer with ScalaBuildServer with JvmBuildServer @@ -91,13 +93,14 @@ class BspClient(coloredStackTrace: Boolean, workingDir: Path, compilationTimeout private val localClient = new InnerClient() private val es = Executors.newFixedThreadPool(1) - Process(Seq("scala-cli", "clean", workingDir.toAbsolutePath.toString)).! - Process(Seq("scala-cli", "setup-ide", workingDir.toAbsolutePath.toString)).! + val scalaCliExec = Seq("cs", "launch", "org.virtuslab.scala-cli:cliBootstrapped:latest.release", "-M", "scala.cli.ScalaCli", "--") + Process(scalaCliExec ++ Seq("clean", workingDir.toAbsolutePath.toString)).! + Process(scalaCliExec ++ Seq("setup-ide", workingDir.toAbsolutePath.toString)).! private val processBuilder: java.lang.ProcessBuilder = new java.lang.ProcessBuilder() - val logFile = Files.createFile(workingDir.toAbsolutePath.resolve("bsp.error.log")) + val logFile = workingDir.toAbsolutePath.resolve("bsp.error.log") processBuilder - .command("scala-cli", "--cli-version", "nightly", "bsp", "-v", "-v", "-v", workingDir.toAbsolutePath.toString) + .command((scalaCliExec ++ Seq("bsp", workingDir.toAbsolutePath.toString)):_*) .redirectError(logFile.toFile) val scalaCliServer = processBuilder.start() @@ -258,7 +261,7 @@ class BspClient(coloredStackTrace: Boolean, workingDir: Path, compilationTimeout // Kills the BSP connection and makes this object // un-usable. def end() = { - Process("scala-cli --power bloop exit").! + Process(BspClient.scalaCliExec ++ Seq("--power", "bloop", "exit")).! try { log.info("Sending buildShutdown.") bspServer.buildShutdown().get(30, TimeUnit.SECONDS) @@ -297,10 +300,7 @@ class BspClient(coloredStackTrace: Boolean, workingDir: Path, compilationTimeout log.info("Stopping listening thread.") listening.cancel(true) - Files.readString(logFile).linesIterator.foreach(log.error(_)) } - - Thread.sleep(5000) } class InnerClient extends BuildClient { diff --git a/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliActor.scala b/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliActor.scala index 4f804bd5a..f26b0c862 100644 --- a/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliActor.scala +++ b/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliActor.scala @@ -102,12 +102,13 @@ class ScalaCliActor( isDone = true )) case Left(BspTaskTimeout(msg)) => - Process("scala-cli --power bloop exit").! - Process("scala-cli --power bloop start").! + log.warning("Timeout detected, restarting BSP") + runner.restart() sendProgress(progressActor, author, buildErrorProgress(snippetId, msg, progressId.getAndIncrement(), isTimeout = true)) case Left(RuntimeTimeout(msg)) => sendProgress(progressActor, author, buildErrorProgress(snippetId, msg, progressId.getAndIncrement(), isTimeout = true)) case Left(error) => + log.error(s"Error reported: ${error.msg}") sendProgress(progressActor, author, buildErrorProgress(snippetId, error.msg, progressId.getAndIncrement())) }.recover { case error => diff --git a/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliRunner.scala b/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliRunner.scala index 936373112..928c5632d 100644 --- a/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliRunner.scala +++ b/scala-cli-runner/src/main/scala/org/scastie/scalacli/ScalaCliRunner.scala @@ -68,7 +68,7 @@ case class RunOutput( class ScalaCliRunner(coloredStackTrace: Boolean, workingDir: Path, compilationTimeout: FiniteDuration, reloadTimeout: FiniteDuration) { private val log = Logger("ScalaCliRunner") - private val bspClient = new BspClient(coloredStackTrace, workingDir, compilationTimeout, reloadTimeout) + private var bspClient = new BspClient(coloredStackTrace, workingDir, compilationTimeout, reloadTimeout) private val scalaMain = workingDir.resolve("Main.scala") Files.createDirectories(scalaMain.getParent()) @@ -136,6 +136,11 @@ class ScalaCliRunner(coloredStackTrace: Boolean, workingDir: Path, compilationTi } } + def restart(): Unit = { + bspClient.end() + bspClient = new BspClient(coloredStackTrace, workingDir, compilationTimeout, reloadTimeout) + } + def end(): Unit = bspClient.end() } diff --git a/scala-cli-runner/src/test/scala/org/scastie/scalacli/BspCancellationTest.scala b/scala-cli-runner/src/test/scala/org/scastie/scalacli/BspCancellationTest.scala deleted file mode 100644 index 58246ea89..000000000 --- a/scala-cli-runner/src/test/scala/org/scastie/scalacli/BspCancellationTest.scala +++ /dev/null @@ -1,174 +0,0 @@ -package org.scastie.scalacli - -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.BeforeAndAfterAll -import org.scastie.util.ScastieFileUtil -import java.nio.file.Paths -import org.scastie.api.SnippetId -import org.scastie.api._ -import org.scastie.runtime.api._ -import scala.concurrent.Future -import scala.concurrent.duration._ -import akka.actor.{ActorRef, ActorSystem, Props} -import akka.testkit.TestActor.AutoPilot -import akka.testkit.{ImplicitSender, TestKit, TestProbe} -import org.scastie.runtime.api._ -import org.scastie.api._ -import org.scastie.util.SbtTask -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuiteLike - -import scala.concurrent.duration._ -import akka.testkit.TestActorRef -import org.scastie.util.ScalaCliActorTask -import java.nio.file.Files -import org.scastie.util.StopRunner -import org.scastie.util.RunnerTerminated - -class BspCancellationTest extends TestKit(ActorSystem("BspCancellationTest")) with ImplicitSender with AnyFunSuiteLike with BeforeAndAfterAll { - val workingDir = Files.createTempDirectory("scastie") - println(workingDir) - - private val macroCode = - """import scala.quoted._ - | - |object SleepMacro: - | inline def sleep(inline time: Int) = - | ${ wait('time) } - | - | def wait(x: Expr[Int])(using Quotes): Expr[Any] = - | Thread.sleep(x.valueOrAbort) - | x - |""".stripMargin - Files.writeString(workingDir.resolve("SleepMacro.scala"), macroCode) - - setAutoPilot(new AutoPilot { - def run(sender: ActorRef, msg: Any): AutoPilot = { - sender ! s"reply to $msg" - this - } - }) - - def longCompilation(time: Int): String = - s"""//> using scala 3 - |//> using file SleepMacro.scala - | - |@main def hello = - | SleepMacro.sleep($time) - | println("test") - |""".stripMargin - - - def longRuntime(time: Int): String = - s"""//> using scala 3 - | - |@main def hello = - | Thread.sleep($time) - | println("test") - |""".stripMargin - - test("No bsp timeout") { - runCode(longCompilation(1000), isWorksheet = false)(assertUserOutput("test")) - } - - test("BSP Timeout") { - runCode(longCompilation(15000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => - assert(info.message == "Build Server Timeout Exception" ) - }) - } - - test("BSP Timeout Multiple snippets") { - runCode(longCompilation(30000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => - assert(info.message == "Build Server Timeout Exception" ) - }) - runCode(longCompilation(100), isWorksheet = false, allowFailure = false)(assertUserOutput("test")) - - runCode(longCompilation(30000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => - assert(info.message == "Build Server Timeout Exception" ) - }) - } - - test("No Runtime Timeout") { - runCode(longRuntime(1000), isWorksheet = false)(assertUserOutput("test")) - } - - test("Runtime Timeout") { - runCode(longRuntime(15000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => - assert(info.message == "Timeout exceeded." ) - }) - } - - def assertCompilationInfo(infoAssert: Problem => Any)(progress: SnippetProgress): Boolean = { - - val gotCompilationError = progress.compilationInfos.nonEmpty - - if (gotCompilationError) { - val info = progress.compilationInfos.head - infoAssert(info) - } - - gotCompilationError - } - - def assertUserOutput(outputAssert: ProcessOutput => Any)(progress: SnippetProgress): Boolean = { - progress.userOutput.map(outputAssert(_)) - progress.userOutput.isDefined - } - - override def afterAll(): Unit = { - println("Cleaning processess") - scalaCliActor ! StopRunner - fishForMessage(1.minute, "RunnerTerminated") { - case RunnerTerminated => true - case _ => false - } - TestKit.shutdownActorSystem(system, 1.minute, true) - } - - private val timeout = 45.seconds - - val scalaCliActor = system.actorOf(Props(new ScalaCliActor(isProduction = false, reconnectInfo = None, coloredStackTrace = false, workingDir = workingDir))) - - private var currentId = 0 - private def snippetId = { - val t = currentId - currentId += 1 - SnippetId(t.toString, None) - } - private var firstRun = true - - private def run(inputs: ScalaCliInputs, allowFailure: Boolean = false)(fish: SnippetProgress => Boolean): Unit = { - val ip = "my-ip" - val progressActor = TestProbe() - - scalaCliActor ! ScalaCliActorTask(snippetId, inputs, ip, progressActor.ref) - - val totalTimeout = - if (firstRun) timeout + 10.second - else timeout - - progressActor.fishForMessage(totalTimeout + 100.seconds) { - case progress: SnippetProgress => - val fishResult = fish(progress) - // println(progress -> fishResult) - if ((progress.isFailure && !allowFailure) || (progress.isDone && !fishResult)) - throw new Exception(s"Fail to meet expectation at ${progress}") - else fishResult - } - firstRun = false - } - - private def runCode(code: String, allowFailure: Boolean = false, isWorksheet: Boolean = true)(fish: SnippetProgress => Boolean): Unit = { - run(ScalaCliInputs.default.copy(code = code, isWorksheetMode = isWorksheet), allowFailure)(fish) - } - - private def assertUserOutput( - message: String, - outputType: ProcessOutputType = ProcessOutputType.StdOut - )(progress: SnippetProgress): Boolean = { - val gotHelloMessage = progress.userOutput.exists(out => out.line == message && out.tpe == outputType) -// if (!gotHelloMessage) assert(progress.userOutput.isEmpty) - gotHelloMessage - } - -} diff --git a/scala-cli-runner/src/test/scala/org/scastie/scalacli/ScalaCliRunnerTest.scala b/scala-cli-runner/src/test/scala/org/scastie/scalacli/ScalaCliRunnerTest.scala index 97b265e47..8cd1ff370 100644 --- a/scala-cli-runner/src/test/scala/org/scastie/scalacli/ScalaCliRunnerTest.scala +++ b/scala-cli-runner/src/test/scala/org/scastie/scalacli/ScalaCliRunnerTest.scala @@ -4,6 +4,7 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.BeforeAndAfterAll import org.scastie.util.ScastieFileUtil import java.nio.file.Paths +import java.nio.file.Files import org.scastie.api.SnippetId import org.scastie.api._ import org.scastie.runtime.api._ @@ -25,6 +26,9 @@ import org.scastie.util.StopRunner import org.scastie.util.RunnerTerminated class ScalaCliRunnerTest extends TestKit(ActorSystem("ScalaCliRunnerTest")) with ImplicitSender with AnyFunSuiteLike with BeforeAndAfterAll { + val workingDir = Files.createTempDirectory("scastie") + println(workingDir) + setAutoPilot(new AutoPilot { def run(sender: ActorRef, msg: Any): AutoPilot = { sender ! s"reply to $msg" @@ -322,6 +326,56 @@ class ScalaCliRunnerTest extends TestKit(ActorSystem("ScalaCliRunnerTest")) with }) } + private val macroCode = + """import scala.quoted._ + | + |object SleepMacro: + | inline def sleep(inline time: Int) = + | ${ wait('time) } + | + | def wait(x: Expr[Int])(using Quotes): Expr[Any] = + | Thread.sleep(x.valueOrAbort) + | x + |""".stripMargin + + test("No bsp timeout") { + Files.writeString(workingDir.resolve("SleepMacro.scala"), macroCode) + runCode(longCompilation(1000), isWorksheet = false)(assertUserOutput("test")) + Files.delete(workingDir.resolve("SleepMacro.scala")) + } + + test("BSP Timeout") { + Files.writeString(workingDir.resolve("SleepMacro.scala"), macroCode) + runCode(longCompilation(compilationTimeout.toMillis + 5000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => + assert(info.message == "Build Server Timeout Exception" ) + }) + Files.delete(workingDir.resolve("SleepMacro.scala")) + } + + def longCompilation(time: Long): String = + s"""//> using scala 3 + |//> using file SleepMacro.scala + | + |@main def hello = + | SleepMacro.sleep($time) + | println("test") + |""".stripMargin + + test("BSP Timeout Multiple snippets") { + Files.writeString(workingDir.resolve("SleepMacro.scala"), macroCode) + runCode(longCompilation(compilationTimeout.toMillis + 5000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => + assert(info.message == "Build Server Timeout Exception" ) + }) + runCode(longCompilation(100), isWorksheet = false, allowFailure = false)(assertUserOutput("test")) + + runCode(longCompilation(compilationTimeout.toMillis + 5000), isWorksheet = false, allowFailure = true)(assertCompilationInfo { info => + assert(info.message == "Build Server Timeout Exception" ) + }) + + runCode(longCompilation(100), isWorksheet = false, allowFailure = false)(assertUserOutput("test")) + Files.delete(workingDir.resolve("SleepMacro.scala")) + } + def assertCompilationInfo(infoAssert: Problem => Any)(progress: SnippetProgress): Boolean = { val gotCompilationError = progress.compilationInfos.nonEmpty @@ -352,8 +406,9 @@ class ScalaCliRunnerTest extends TestKit(ActorSystem("ScalaCliRunnerTest")) with } private val timeout = 45.seconds + private val compilationTimeout = 25.seconds - val scalaCliActor = system.actorOf(Props(new ScalaCliActor(runTimeout = timeout, isProduction = false, reconnectInfo = None, coloredStackTrace = false, compilationTimeout = 25.seconds))) + val scalaCliActor = system.actorOf(Props(new ScalaCliActor(runTimeout = timeout, isProduction = false, reconnectInfo = None, coloredStackTrace = false, compilationTimeout = compilationTimeout, workingDir = workingDir))) private var currentId = 0 private def snippetId = {