Skip to content

Commit aa18217

Browse files
committed
server-mode-rewrite: squashed
slf4j-simple for stage WIP refactor WebServiceWithWebSocket: style reimplement server mode * previously it was a very flakey since it just piped input/output streams and would fail for a random 'println' in the code or inherited code * It pretended to handle concurrent requests which was a lie. Now we're programmatically interacting with the ReplDriver * No more manual thread handling, killing of the process etc. refactor for reuse and DRY: server.ReplDriver and ReplDriver refactor: shuffle classes around more refactorings, including optional server auth handling fix and refactor ReplServerTests add test for predefined code, refactor some of the test code test synchronous api readme refactor EmbeddedReplTests: fixup
1 parent 3c989a1 commit aa18217

File tree

13 files changed

+594
-627
lines changed

13 files changed

+594
-627
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,43 @@ The prefix is arbitrary and is only used to specify several credentials in a sin
264264
./scala-repl-pp --server
265265

266266
curl http://localhost:8080/query-sync -X POST -d '{"query": "val foo = 42"}'
267+
# {"success":true,"stdout":"val foo: Int = 42\n",...}
268+
267269
curl http://localhost:8080/query-sync -X POST -d '{"query": "val bar = foo + 1"}'
270+
# {"success":true,"stdout":"val bar: Int = 43\n",...}
271+
272+
curl http://localhost:8080/query-sync -X POST -d '{"query":"println(\"OMG remote code execution!!1!\")"}'
273+
# {"success":true,"stdout":"",...}%
274+
```
275+
276+
Predef code works with server as well:
277+
```
278+
echo val foo = 99 > foo.sc
279+
./scala-repl-pp --server --predef foo.sc
280+
281+
curl -XPOST http://localhost:8080/query-sync -d '{"query":"val baz = foo + 1"}'
282+
# {"success":true,"stdout":"val baz: Int = 100\n",...}
283+
```
284+
285+
There's also has an asynchronous mode:
286+
```
287+
./scala-repl-pp --server
288+
289+
curl http://localhost:8080/query -X POST -d '{"query": "val baz = 93"}'
290+
# {"success":true,"uuid":"e2640fcb-3193-4386-8e05-914b639c3184"}%
291+
292+
curl http://localhost:8080/result/e2640fcb-3193-4386-8e05-914b639c3184
293+
{"success":true,"uuid":"e2640fcb-3193-4386-8e05-914b639c3184","stdout":"val baz: Int = 93\n"}%
294+
```
295+
296+
And there's even a websocket channel that allows you to get notified when the query has finished. For more details and other use cases check out [ReplServerTests.scala](server/src/test/scala/replpp/server/ReplServerTests.scala)
297+
298+
Server-specific configuration options as per `scala-repl-pp --help`:
299+
```
300+
--server-host <value> Hostname on which to expose the REPL server
301+
--server-port <value> Port on which to expose the REPL server
302+
--server-auth-username <value> Basic auth username for the REPL server
303+
--server-auth-password <value> Basic auth password for the REPL server
268304
```
269305

270306
## Embed into your own project

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ lazy val server = project.in(file("server"))
3838
lazy val all = project.in(file("all"))
3939
.dependsOn(core, server)
4040
.enablePlugins(JavaAppPackaging)
41-
.settings(name := "scala-repl-pp-all")
41+
.settings(
42+
name := "scala-repl-pp-all",
43+
libraryDependencies += "org.slf4j" % "slf4j-simple" % "2.0.7" % Optional,
44+
)
4245

4346
ThisBuild / libraryDependencies ++= Seq(
4447
"org.scalatest" %% "scalatest" % ScalaTestVersion % Test,

core/src/main/scala/replpp/Config.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ case class Config(
2424
server: Boolean = false,
2525
serverHost: String = "localhost",
2626
serverPort: Int = 8080,
27-
serverAuthUsername: String = "",
28-
serverAuthPassword: String = "",
27+
serverAuthUsername: Option[String] = None,
28+
serverAuthPassword: Option[String] = None,
2929
) {
3030
/** inverse of `Config.parse` */
3131
lazy val asJavaArgs: Seq[String] = {
@@ -154,11 +154,11 @@ object Config {
154154
.text("Port on which to expose the REPL server")
155155

156156
opt[String]("server-auth-username")
157-
.action((x, c) => c.copy(serverAuthUsername = x))
157+
.action((x, c) => c.copy(serverAuthUsername = Option(x)))
158158
.text("Basic auth username for the REPL server")
159159

160160
opt[String]("server-auth-password")
161-
.action((x, c) => c.copy(serverAuthPassword = x))
161+
.action((x, c) => c.copy(serverAuthPassword = Option(x)))
162162
.text("Basic auth password for the REPL server")
163163

164164
help("help")

core/src/main/scala/replpp/ReplDriver.scala

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ReplDriver(args: Array[String],
2828
greeting: Option[String],
2929
prompt: String,
3030
maxHeight: Option[Int] = None,
31-
classLoader: Option[ClassLoader] = None) extends dotty.tools.repl.ReplDriver(args, out, classLoader) {
31+
classLoader: Option[ClassLoader] = None) extends ReplDriverBase(args, out, classLoader) {
3232

3333
/** Run REPL with `state` until `:quit` command found
3434
* Main difference to the 'original': different greeting, trap Ctrl-c
@@ -82,43 +82,4 @@ class ReplDriver(args: Array[String],
8282
terminal.readLine(completer).linesIterator
8383
}
8484

85-
private def interpretInput(lines: IterableOnce[String], state: State, currentFile: Path): State = {
86-
val parsedLines = Seq.newBuilder[String]
87-
var resultingState = state
88-
89-
def handleImportFileDirective(line: String) = {
90-
val linesBeforeUsingFileDirective = parsedLines.result()
91-
parsedLines.clear()
92-
if (linesBeforeUsingFileDirective.nonEmpty) {
93-
// interpret everything until here
94-
val parseResult = parseInput(linesBeforeUsingFileDirective, resultingState)
95-
resultingState = interpret(parseResult)(using resultingState)
96-
}
97-
98-
// now read and interpret the given file
99-
val pathStr = line.trim.drop(UsingDirectives.FileDirective.length)
100-
val path = resolveFile(currentFile, pathStr)
101-
val linesFromFile = util.linesFromFile(path)
102-
println(s"> importing $path (${linesFromFile.size} lines)")
103-
resultingState = interpretInput(linesFromFile, resultingState, path)
104-
}
105-
106-
for (line <- lines.iterator) {
107-
if (line.trim.startsWith(UsingDirectives.FileDirective))
108-
handleImportFileDirective(line)
109-
else
110-
parsedLines.addOne(line)
111-
}
112-
113-
val parseResult = parseInput(parsedLines.result(), resultingState)
114-
resultingState = interpret(parseResult)(using resultingState)
115-
resultingState
116-
}
117-
118-
private def parseInput(lines: IterableOnce[String], state: State): ParseResult =
119-
parseInput(lines.iterator.mkString(lineSeparator), state)
120-
121-
private def parseInput(input: String, state: State): ParseResult =
122-
ParseResult(input)(using state)
123-
12485
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package replpp
2+
3+
import dotty.tools.MainGenericCompiler.classpathSeparator
4+
import dotty.tools.dotc.Run
5+
import dotty.tools.dotc.ast.{Positioned, tpd, untpd}
6+
import dotty.tools.dotc.classpath.{AggregateClassPath, ClassPathFactory}
7+
import dotty.tools.dotc.config.{Feature, JavaPlatform, Platform}
8+
import dotty.tools.dotc.core.Comments.{ContextDoc, ContextDocstrings}
9+
import dotty.tools.dotc.core.Contexts.{Context, ContextBase, ContextState, FreshContext, ctx, explore}
10+
import dotty.tools.dotc.core.{Contexts, MacroClassLoader, Mode, TyperState}
11+
import dotty.tools.io.{AbstractFile, ClassPath, ClassRepresentation}
12+
import dotty.tools.repl.*
13+
import org.jline.reader.*
14+
import org.slf4j.{Logger, LoggerFactory}
15+
16+
import java.io.PrintStream
17+
import java.lang.System.lineSeparator
18+
import java.net.URL
19+
import java.nio.file.Path
20+
import javax.naming.InitialContext
21+
import scala.annotation.tailrec
22+
import scala.collection.mutable
23+
import scala.jdk.CollectionConverters.*
24+
import scala.util.{Failure, Success, Try}
25+
26+
abstract class ReplDriverBase(args: Array[String], out: PrintStream, classLoader: Option[ClassLoader])
27+
extends dotty.tools.repl.ReplDriver(args, out, classLoader) {
28+
29+
protected def interpretInput(lines: IterableOnce[String], state: State, currentFile: Path): State = {
30+
val parsedLines = Seq.newBuilder[String]
31+
var currentState = state
32+
33+
def handleImportFileDirective(line: String) = {
34+
val linesBeforeUsingFileDirective = parsedLines.result()
35+
parsedLines.clear()
36+
if (linesBeforeUsingFileDirective.nonEmpty) {
37+
// interpret everything until here
38+
val parseResult = parseInput(linesBeforeUsingFileDirective, currentState)
39+
currentState = interpret(parseResult)(using currentState)
40+
}
41+
42+
// now read and interpret the given file
43+
val pathStr = line.trim.drop(UsingDirectives.FileDirective.length)
44+
val path = resolveFile(currentFile, pathStr)
45+
val linesFromFile = util.linesFromFile(path)
46+
println(s"> importing $path (${linesFromFile.size} lines)")
47+
currentState = interpretInput(linesFromFile, currentState, path)
48+
}
49+
50+
for (line <- lines.iterator) {
51+
if (line.trim.startsWith(UsingDirectives.FileDirective))
52+
handleImportFileDirective(line)
53+
else
54+
parsedLines.addOne(line)
55+
}
56+
57+
val parseResult = parseInput(parsedLines.result(), currentState)
58+
interpret(parseResult)(using currentState)
59+
}
60+
61+
private def parseInput(lines: IterableOnce[String], state: State): ParseResult =
62+
parseInput(lines.iterator.mkString(lineSeparator), state)
63+
64+
private def parseInput(input: String, state: State): ParseResult =
65+
ParseResult(input)(using state)
66+
67+
}

core/src/main/scala/replpp/package.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ package object replpp {
8080
def allPredefCode(config: Config): String =
8181
allPredefLines(config).mkString(lineSeparator)
8282

83-
private def allPredefLines(config: Config): Seq[String] = {
83+
def allPredefLines(config: Config): Seq[String] = {
8484
val resultLines = Seq.newBuilder[String]
8585
val visited = mutable.Set.empty[Path]
8686

server/src/it/scala/replpp/server/EmbeddedReplTests.scala

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package replpp.server
22

33
import org.scalatest.matchers.should.Matchers
44
import org.scalatest.wordspec.AnyWordSpec
5-
6-
import java.util.concurrent.Semaphore
5+
import scala.concurrent.Await
6+
import scala.concurrent.duration.Duration
77

88
/** Moved to IntegrationTests, because of some strange interaction with ReplServerTests:
99
* if EmbeddedReplTests would run *before* ReplServerTests, the latter would stall (forever)
@@ -12,34 +12,21 @@ import java.util.concurrent.Semaphore
1212
*/
1313
class EmbeddedReplTests extends AnyWordSpec with Matchers {
1414

15-
"start and shutdown without hanging" in {
16-
val shell = new EmbeddedRepl()
17-
shell.start()
18-
shell.shutdown()
19-
}
20-
2115
"execute commands synchronously" in {
22-
val shell = new EmbeddedRepl()
23-
shell.start()
16+
val repl = new EmbeddedRepl()
2417

25-
shell.query("val x = 0").out shouldBe "val x: Int = 0\n"
26-
shell.query("x + 1").out shouldBe "val res1: Int = 1\n"
18+
repl.query("val x = 0").output shouldBe "val x: Int = 0\n"
19+
repl.query("x + 1").output shouldBe "val res0: Int = 1\n"
2720

28-
shell.shutdown()
21+
repl.shutdown()
2922
}
3023

31-
"execute a command asynchronously" in {
32-
val shell = new EmbeddedRepl()
33-
val mutex = new Semaphore(0)
34-
shell.start()
35-
var resultOut = "uninitialized"
36-
shell.queryAsync("val x = 0") { result =>
37-
resultOut = result.out
38-
mutex.release()
39-
}
40-
mutex.acquire()
41-
resultOut shouldBe "val x: Int = 0\n"
42-
shell.shutdown()
43-
}
24+
"execute a command asynchronously" in {
25+
val repl = new EmbeddedRepl()
26+
val (uuid, futureResult) = repl.queryAsync("val x = 0")
27+
val result = Await.result(futureResult, Duration.Inf)
28+
result shouldBe "val x: Int = 0\n"
29+
repl.shutdown()
30+
}
4431

4532
}

0 commit comments

Comments
 (0)