Skip to content

ghik/recons

Repository files navigation

Recons - Embeddable Remote Scala Console

Recons allows you to embed a Scala REPL server into your application, and connect to it from outside. This is mostly useful as an advanced troubleshooting utility, giving you almost unlimited access to all the guts and internals of your running application.

Table of Contents generated with DocToc

Features

  • remote, fully-featured Scala console, i.e. with syntax highlighting, tab completion, etc.
  • support for Scala 2 and 3
  • troubleshooting utilities and customizations
  • pre-bundled client packages
  • secure communication with TLS

Supported Scala versions

Recons supports Scala 2 and 3. However, it uses the Scala compiler API, which has no compatibility guarantees. As a result, Recons must be cross-built for every minor Scala version. The currently supported versions are 2.13.14+ and 3.4.2+ (unless a version is very fresh and Recons hasn't been built for it yet).

Because the implementation of Recons copies some code from the compiler, and uses some private APIs via runtime reflection, there's a risk that it may not work with future Scala versions or require more significant changes to keep up with the compiler. Hopefully, it may be possible to propose some refactors to the compiler itself to improve the situation.

Quickstart

Add the following dependency to your build.sbt:

libraryDependencies +=
  "com.github.ghik" % "recons-server" % VERSION cross CrossVersion.full

Determining the classpath

Recons runs the Scala REPL, which wraps a real Scala compiler, which needs an explicit classpath. Typically, you can obtain the classpath of your JVM process from the java.class.path system property:

sys.props("java.class.path").split(File.pathSeparator).toSeq

Warning

Depending on the exact way your application is launched, you may not be able to rely on the java.class.path system property - it may be unset or set to an incorrect value (for the REPL server needs). You may need another way of determining the classpath.

Launching the server

import java.io.File
import com.github.ghik.recons.server.*

val server = new ReplServer(
  classpath = sys.props("java.class.path").split(File.pathSeparator).toSeq,
  tlsConfig = None, // disabling TLS for the sake of brevity of this example
  bindAddress = "localhost",
  bindPort = 6666,
)

server.run()

Connecting to the server

In order to connect to the REPL server running inside your application, you need to use the client binary. You can download it from a Recons release on GitHub.

RECONS_VERSION=<desired recons version>
SCALA_VERSION=<desired scala version>

package=recons-client_$SCALA_VERSION-$RECONS_VERSION
wget https://github.com/ghik/recons/releases/download/v$RECONS_VERSION/$package.zip
unzip $package.zip && cd $package

Then, run the client to connect to the server:

./bin/recons-client -h localhost -p 6666 --no-tls

and you should be able to see a fully-featured Scala REPL, e.g.

Welcome to Recons, based on Scala 3.4.2 (Java 21.0.1, OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

val $ext: com.github.ghik.recons.server.utils.ShellExtensions = com.github.ghik.recons.server.utils.ShellExtensions@46a00624

recons>

Note

For simplicity of the example, we have disabled TLS.

Using standard input and output

The REPL runs in the server JVM process, which is a different process than the client. This means that the standard input and output of the REPL are not connected to the client, like they would in a regular, local Scala REPL. If you invoke scala.Predef.println or similar functions in the remote REPL, you will not see the output in your terminal.

In order to work around this to some extent, Recons shadows println and print methods with ones that use a custom output stream, connected to the client. However, they work only in code written and compiled directly in the REPL.

You can also use out (a PrintStream) to grab direct access to this custom output stream, pass it to other functions, etc.

There is currently no way to read client's standard input inside the REPL.

Security

Recons gives you full access to your application process from outside, and lets you run arbitrary code in it. This is an obvious security risk, so you must make sure to limit access to the remote REPL to power users, and be extremely careful about what you execute in it. Otherwise, you may crash your process, put a thread into an infinite loop, or corrupt the internal state of your application.

Recons binds on localhost address by default. It might make sense to leave it this way, and allow access only from the local machine or container/pod (e.g. in Kubernetes you would need permissions to execute kubectl exec on your application's pods in order to gain access to the REPL).

You can also secure client-server communication with TLS:

  • ReplServer accepts a TlsConfig parameter, which allows you to configure a complete SSLContext and SSLParameters for the server. This effectively allows you to specify (among others):
    • the keystore and truststore
    • enabled protocols and cipher suites
    • client authentication
  • The client accepts --cacert option, as well as --cert and --key options for client authentication. Invoke the client with --help for more details.

Troubleshooting utilities

Since Recons is designed for troubleshooting, it provides some utilities to make it easier. Namely, every REPL session automatically imports ShellExtensions, which give you the following tools:

  • out, print and println - see Using standard input and output.
  • private field and method accessors, e.g. someObject.d.privateField, someObject.d.privateMethod(arg)
  • private static field and method accessors, e.g. statics[SomeClass].privateStaticField, statics[SomeClass].privateStaticMethod(arg)
  • shorter syntax for .asInstanceOf[T], e.g. someObject.as[T] - to complement the private field and method accessors, which return untyped values

Customization

You can customize the REPL in several ways, by specifying:

  • a custom welcome message
  • a custom prompt
  • initial set of bindings and imports

See ReplConfig for more details.