Skip to content

Commit 2db17f0

Browse files
Merge pull request #1716 from alexarchambault/restart-bloop-server-if-needed
Restart Bloop server if it exited
2 parents 0dabcfc + c8bbe73 commit 2db17f0

File tree

11 files changed

+150
-62
lines changed

11 files changed

+150
-62
lines changed

modules/build/src/main/scala/scala/build/Bloop.scala

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import coursier.util.Task
66
import dependency.parser.ModuleParser
77
import dependency.{AnyDependency, DependencyLike, ScalaParameters, ScalaVersion}
88

9-
import java.io.File
9+
import java.io.{File, IOException}
1010

11+
import scala.annotation.tailrec
1112
import scala.build.EitherCps.{either, value}
13+
import scala.build.bloop.BuildServer
1214
import scala.build.blooprifle.BloopRifleConfig
1315
import scala.build.errors.{BuildException, ModuleFormatError}
1416
import scala.build.internal.CsLoggerUtil._
@@ -17,33 +19,49 @@ import scala.jdk.CollectionConverters._
1719

1820
object Bloop {
1921

22+
private object BrokenPipeInCauses {
23+
@tailrec
24+
def unapply(ex: Throwable): Option[IOException] =
25+
ex match {
26+
case null => None
27+
case ex: IOException if ex.getMessage == "Broken pipe" => Some(ex)
28+
case ex: IOException if ex.getMessage == "Connection reset by peer" => Some(ex)
29+
case _ => unapply(ex.getCause)
30+
}
31+
}
32+
2033
def compile(
2134
projectName: String,
22-
bloopServer: bloop.BloopServer,
35+
buildServer: BuildServer,
2336
logger: Logger,
2437
buildTargetsTimeout: FiniteDuration
25-
): Boolean = {
38+
): Either[Throwable, Boolean] =
39+
try {
40+
logger.debug("Listing BSP build targets")
41+
val results = buildServer.workspaceBuildTargets()
42+
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
43+
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)
2644

27-
logger.debug("Listing BSP build targets")
28-
val results = bloopServer.server.workspaceBuildTargets()
29-
.get(buildTargetsTimeout.length, buildTargetsTimeout.unit)
30-
val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName)
31-
32-
val buildTarget = buildTargetOpt.getOrElse {
33-
throw new Exception(
34-
s"Expected to find project '$projectName' in build targets (only got ${results.getTargets.asScala.map("'" + _.getDisplayName + "'").mkString(", ")})"
35-
)
36-
}
45+
val buildTarget = buildTargetOpt.getOrElse {
46+
throw new Exception(
47+
s"Expected to find project '$projectName' in build targets (only got ${results.getTargets.asScala.map("'" + _.getDisplayName + "'").mkString(", ")})"
48+
)
49+
}
3750

38-
logger.debug(s"Compiling $projectName with Bloop")
39-
val compileRes = bloopServer.server.buildTargetCompile(
40-
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
41-
).get()
51+
logger.debug(s"Compiling $projectName with Bloop")
52+
val compileRes = buildServer.buildTargetCompile(
53+
new bsp4j.CompileParams(List(buildTarget.getId).asJava)
54+
).get()
4255

43-
val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
44-
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
45-
success
46-
}
56+
val success = compileRes.getStatusCode == bsp4j.StatusCode.OK
57+
logger.debug(if (success) "Compilation succeeded" else "Compilation failed")
58+
Right(success)
59+
}
60+
catch {
61+
case ex @ BrokenPipeInCauses(e) =>
62+
logger.debug(s"Caught $ex while exchanging with Bloop server, assuming Bloop server exited")
63+
Left(ex)
64+
}
4765

4866
def bloopClassPath(
4967
dep: AnyDependency,

modules/build/src/main/scala/scala/build/bsp/BspImpl.scala

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -362,18 +362,20 @@ final class BspImpl(
362362
): BloopSession = {
363363
val logger = reloadableOptions.logger
364364
val buildOptions = reloadableOptions.buildOptions
365-
val bloopServer = BloopServer.buildServer(
366-
reloadableOptions.bloopRifleConfig,
367-
"scala-cli",
368-
Constants.version,
369-
(inputs.workspace / Constants.workspaceDirName).toNIO,
370-
Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO,
371-
localClient,
372-
threads.buildThreads.bloop,
373-
logger.bloopRifleLogger
374-
)
365+
val createBloopServer =
366+
() =>
367+
BloopServer.buildServer(
368+
reloadableOptions.bloopRifleConfig,
369+
"scala-cli",
370+
Constants.version,
371+
(inputs.workspace / Constants.workspaceDirName).toNIO,
372+
Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO,
373+
localClient,
374+
threads.buildThreads.bloop,
375+
logger.bloopRifleLogger
376+
)
375377
val remoteServer = new BloopCompiler(
376-
bloopServer,
378+
createBloopServer,
377379
20.seconds,
378380
strictBloopJsonCheck = buildOptions.internal.strictBloopJsonCheckOrDefault
379381
)

modules/build/src/main/scala/scala/build/compiler/BloopCompiler.scala

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package scala.build.compiler
22

3+
import scala.annotation.tailrec
34
import scala.build.{Bloop, Logger, Position, Positioned, Project, bloop}
45
import scala.concurrent.duration.FiniteDuration
56

67
final class BloopCompiler(
7-
val bloopServer: bloop.BloopServer,
8+
createServer: () => bloop.BloopServer,
89
buildTargetsTimeout: FiniteDuration,
910
strictBloopJsonCheck: Boolean
1011
) extends ScalaCompiler {
12+
private var currentBloopServer: bloop.BloopServer =
13+
createServer()
14+
def bloopServer: bloop.BloopServer =
15+
currentBloopServer
16+
1117
def jvmVersion: Option[Positioned[Int]] =
1218
Some(
1319
Positioned(
@@ -25,8 +31,26 @@ final class BloopCompiler(
2531
def compile(
2632
project: Project,
2733
logger: Logger
28-
): Boolean =
29-
Bloop.compile(project.projectName, bloopServer, logger, buildTargetsTimeout)
34+
): Boolean = {
35+
@tailrec
36+
def helper(remainingAttempts: Int): Boolean =
37+
Bloop.compile(project.projectName, bloopServer.server, logger, buildTargetsTimeout) match {
38+
case Right(res) => res
39+
case Left(ex) =>
40+
if (remainingAttempts > 1) {
41+
logger.debug(s"Seems Bloop server exited (got $ex), trying to restart one")
42+
currentBloopServer = createServer()
43+
helper(remainingAttempts - 1)
44+
}
45+
else
46+
throw new Exception(
47+
"Seems compilation server exited, and wasn't able to restart one",
48+
ex
49+
)
50+
}
51+
52+
helper(2)
53+
}
3054

3155
def shutdown(): Unit =
3256
bloopServer.shutdown()

modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ final class BloopCompilerMaker(
1919
buildClient: BuildClient,
2020
logger: Logger
2121
): BloopCompiler = {
22-
val buildServer = BloopServer.buildServer(
23-
config,
24-
"scala-cli",
25-
Constants.version,
26-
workspace.toNIO,
27-
classesDir.toNIO,
28-
buildClient,
29-
threads,
30-
logger.bloopRifleLogger
31-
)
32-
new BloopCompiler(buildServer, 20.seconds, strictBloopJsonCheck)
22+
val createBuildServer =
23+
() =>
24+
BloopServer.buildServer(
25+
config,
26+
"scala-cli",
27+
Constants.version,
28+
workspace.toNIO,
29+
classesDir.toNIO,
30+
buildClient,
31+
threads,
32+
logger.bloopRifleLogger
33+
)
34+
new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck)
3335
}
3436
}

modules/build/src/test/scala/scala/build/tests/markdown/MarkdownCodeWrapperTests.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package scala.build.tests.markdown
22

33
import scala.build.internal.markdown.{MarkdownCodeBlock, MarkdownCodeWrapper}
44
import com.eed3si9n.expecty.Expecty.expect
5-
import os.RelPath
65

76
import scala.build.Position
87
import scala.build.errors.{BuildException, MarkdownUnclosedBackticksError}

modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package scala.cli.commands.dependencyupdate
22

33
import caseapp.*
4-
import os.Path
54

65
import scala.build.actionable.ActionableDependencyHandler
76
import scala.build.actionable.ActionableDiagnostic.ActionableDependencyUpdateDiagnostic
@@ -102,7 +101,7 @@ object DependencyUpdate extends ScalaCommand[DependencyUpdateOptions] {
102101
}
103102

104103
private def updateDependencies(
105-
file: Path,
104+
file: os.Path,
106105
diagnostics: Seq[(Position.File, ActionableDependencyUpdateDiagnostic)]
107106
): String = {
108107
val fileContent = os.read(file)

modules/integration/src/test/scala/scala/cli/integration/BloopTests.scala

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package scala.cli.integration
22

33
import com.eed3si9n.expecty.Expecty.expect
4-
import os.proc
54

65
import scala.cli.integration.util.BloopUtil
6+
import scala.concurrent.duration.Duration
7+
import scala.concurrent.{Await, ExecutionContext, Future}
78

89
class BloopTests extends ScalaCliSuite {
910

10-
def runScalaCli(args: String*): proc = os.proc(TestUtil.cli, args)
11+
def runScalaCli(args: String*): os.proc = os.proc(TestUtil.cli, args)
1112

1213
private lazy val bloopDaemonDir =
1314
BloopUtil.bloopDaemonDir(runScalaCli("directories").call().out.text())
@@ -139,4 +140,42 @@ class BloopTests extends ScalaCliSuite {
139140
.call(cwd = root / Constants.workspaceDirName)
140141
}
141142
}
143+
144+
test("Restart Bloop server while watching") {
145+
TestUtil.withThreadPool("bloop-restart-test", 2) { pool =>
146+
val timeout = Duration("20 seconds")
147+
def readLine(stream: os.SubProcess.OutputStream): String = {
148+
implicit val ec = ExecutionContext.fromExecutorService(pool)
149+
val readLineF = Future {
150+
stream.readLine()
151+
}
152+
Await.result(readLineF, timeout)
153+
}
154+
def content(message: String) =
155+
s"""object Hello {
156+
| def main(args: Array[String]): Unit =
157+
| println("$message")
158+
|}
159+
|""".stripMargin
160+
val sourcePath = os.rel / "Hello.scala"
161+
val inputs = TestInputs(
162+
sourcePath -> content("Hello")
163+
)
164+
inputs.fromRoot { root =>
165+
val proc = os.proc(TestUtil.cli, "run", "-w", ".")
166+
.spawn(cwd = root)
167+
val firstLine = readLine(proc.stdout)
168+
expect(firstLine == "Hello")
169+
170+
os.proc(TestUtil.cli, "bloop", "exit")
171+
.call(cwd = root)
172+
173+
os.write.over(root / sourcePath, content("Foo"))
174+
val secondLine = readLine(proc.stdout)
175+
expect(secondLine == "Foo")
176+
177+
proc.destroy()
178+
}
179+
}
180+
}
142181
}

modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -954,9 +954,8 @@ abstract class BspTestDefinitions(val scalaVersionOpt: Option[String])
954954
test("workspace/reload --dependency option") {
955955
val inputs = TestInputs(
956956
os.rel / "ReloadTest.scala" ->
957-
s"""import os.pwd
958-
|object ReloadTest {
959-
| println(pwd)
957+
s"""object ReloadTest {
958+
| println(os.pwd)
960959
|}
961960
|""".stripMargin
962961
)

modules/integration/src/test/scala/scala/cli/integration/PublishTestDefinitions.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package scala.cli.integration
22

33
import com.eed3si9n.expecty.Expecty.expect
4-
import os.RelPath
54

65
import java.nio.file.Paths
76
import java.util.zip.ZipFile
@@ -41,9 +40,9 @@ abstract class PublishTestDefinitions(val scalaVersionOpt: Option[String])
4140
val scalaSuffix: String =
4241
if (actualScalaVersion.startsWith("3.")) "_3"
4342
else "_" + actualScalaVersion.split('.').take(2).mkString(".")
44-
val expectedArtifactsDir: RelPath =
43+
val expectedArtifactsDir: os.RelPath =
4544
os.rel / "org" / "virtuslab" / "scalacli" / "test" / s"simple$scalaSuffix" / "0.2.0-SNAPSHOT"
46-
val expectedJsArtifactsDir: RelPath =
45+
val expectedJsArtifactsDir: os.RelPath =
4746
os.rel / "org" / "virtuslab" / "scalacli" / "test" / s"simple_sjs1$scalaSuffix" / "0.2.0-SNAPSHOT"
4847
}
4948

modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package scala.cli.integration
22

3-
import os.{CommandResult, Path}
4-
53
import java.io.File
64
import java.net.ServerSocket
75
import java.util.Locale
@@ -78,6 +76,16 @@ object TestUtil {
7876

7977
def threadPool(prefix: String, size: Int): ExecutorService =
8078
Executors.newFixedThreadPool(size, daemonThreadFactory(prefix))
79+
def withThreadPool[T](prefix: String, size: Int)(f: ExecutorService => T): T = {
80+
var pool: ExecutorService = null
81+
try {
82+
pool = threadPool(prefix, size)
83+
f(pool)
84+
}
85+
finally
86+
if (pool != null)
87+
pool.shutdown()
88+
}
8189

8290
def scheduler(prefix: String): ScheduledExecutorService =
8391
Executors.newSingleThreadScheduledExecutor(daemonThreadFactory(prefix))
@@ -129,13 +137,13 @@ object TestUtil {
129137
def relPathStr(relPath: os.RelPath): String =
130138
(Seq.fill(relPath.ups)("..") ++ relPath.segments).mkString(File.separator)
131139

132-
def kill(pid: Int): CommandResult =
140+
def kill(pid: Int): os.CommandResult =
133141
if (Properties.isWin)
134142
os.proc("taskkill", "/F", "/PID", pid).call()
135143
else
136144
os.proc("kill", pid).call()
137145

138-
def pwd: Path =
146+
def pwd: os.Path =
139147
if (Properties.isWin)
140148
os.Path(os.pwd.toIO.getCanonicalFile)
141149
else

0 commit comments

Comments
 (0)