Skip to end of metadata
Go to start of metadata

Problems

Programs often want to provide REPLs to users in contexts when a) network communication is desired, b) capturing stdio is difficult, or c) when more than one REPL session is desired. In addition, tools that want to support REPLs and simultaneous conversations with the host are difficult with a single stdio REPL as currently provided by Clojure.

Tooling and users often need to enable a REPL on a program without changing the program, e.g. without asking author or program to include code to start a REPL host of some sort. Thus a solution must be externally and declaratively configured.

Proposal

A REPL is just a special case of a socket service. Rather than provide a socket server REPL, provide a built-in socket server that composes with the existing repl function.

Encapsulate configuration mechanism from invoked service function.

  1. Socket server to accept connections on address and port, then invoke an accept function.
    1. Runtime configuration via data.
    2. Existing applications can start a socket server without changing their code.
    3. The Clojure runtime should support configuration and running of multiple servers.
  2. Extend existing REPL (clojure.main/repl) capabilities:
    1. Currently configuration expects function, but can't pass function via data configuration.
    2. Need means of control via REPL keyword commands
    3. Ability to communicate only with data (changes made in 1.7 for this - exceptions, etc as data)
  3. Shared socket repl "sessions"
    1. Socket repl exists in a "session"
    2. Tool connection on a socket repl can "attach" to an existing (user) session, allowing access to the session's repl environment (dynamic vars, etc)

Summary

 
Summary of the proposed changes:
 
FacetDescriptionOpen issues
Server config

Specify each server config via system property that is EDN string containing a map.
-Dclojure.server.NAME="{:address \"127.0.0.1\"  :\port 5555 :accept clojure.repl/repl}"

For each server, properties are:

  • address - host or address, default to loopback
  • port - port, required
  • accept - namespaced symbol of function to invoke on socket accept, required
  • args - optional sequential args to apply to accept fn
  • bind-err - whether to bind *err* in socket, default true
  • server-daemon - whether socket server thread should be a daemon, default true
  • client-daemon - whether socket client thread should be a daemon, default true
 
Server config,
external file 

Alternately to embedding the EDN map in a property, could also load from external file:
-Dclojure.server.NAME=foo.edn

Not currently included
Server config,
REPL shortcut
For the special (and presumably common case) of running only a REPL with defaults on a socket server:
-Dclojure.repl=127.0.0.1:5555 
Not currently included
Runtime startupAt Clojure runtime startup, parse the system properties and start each configured server. 
Server stateVar containing map of server and session state, used to control the servers and share session state across sessions. 
Session identifierDynamic var *session* bound to {:server server-name :client client-id} in each socket client connection. 
REPL commands

At socket repl, keywords in "repl" namespace are caught and handled specially by the repl.

:repl/quit - acts like ^D to "quit" the repl, allowing nested repl control 
:repl/sessions - returns vector of session ids for this server
:repl/attach <session> - attach to existing repl session, interpret \*ns* etc in attached session context
:repl/detach - detach from attached session

 

 

Socket Server

The Socket Server will be a general facility available as part of the Clojure runtime, initiated one or more times based on configuration.

Configuration

  • Multiple socket servers
  • Config per socket server
    • Socket server properties
      • Address (default = loopback) as host or address (IPv4 or IPv6)
      • Port (required)
      • Name (required, defines server context)
      • Server thread config - daemon (default = true)
      • Server socket config - timeout, etc (omit these for now)
    • Client connection properties
      • Client thread config - daemon (default = true)
      • Bind err - whether to bind \*err* to socket output stream (default = true)
      • Namespaced symbol identifying a socket accept function to invoke - implies loading the ns containing the function early in runtime 
      • Arguments to that function - EDNable strings

Code example:


Configuration Source

Clojure should support the configuration of one or more socket servers in any Clojure runtime based on runtime configuration (no code changes in the app required).

The primary means of specifying configuration will be a system property that is a map literal edn string:

Or load the configuration from an external edn file:

Or for the expected common case of the repl, a shortcut that takes the host and port and takes care of the name and accept function for you:

 

 

Configuration alternatives (background, not currently used)

Other alternatives for runtime configuration without changing existing application code:

  • System properties
  • File at known path location
  • Resource at known classpath location
    • problematic for things like uberjar deployment
  • JDK ServiceLoader - allows discovery of all service implementers on the classpath
    • kind of a pain to create a jar with proper manifest to wire this up
    • problematic for things like uberjar deployment
  • Java agent
    • Requires packaging in separate jar file
    • Modify JVM startup properties to point to jar file
    • Agent is started prior to main(), which probably isn't very useful for us as the clojure runtime is not yet available - would need to have some way to have it wait for runtime initiation
    • Agent can also be injected and started later but this is an involved process
    • Can deploy same agent multiple times, so could have multiple server instances 
  • JMX 
    • not very useful for startup, but could be potentially interesting for providing a JMX service to start a server later
    • you could use something like JConsole to connect and launch a socket repl in a running process, or by connecting to the Attach API (requires having tools.jar on your classpath)

System Property alternatives

Could pack arguments into a single string with positional args:

where the values are: [address port accept-fn & fn-args] and "myrepl" is the "name" of the server (would get into thread names and maybe available in a bound var or something). That is not valid edn so we'd need to just split on spaces. The positional args don't leave a lot of room for variability or expansion

Or if we did commas, no quotes needed at all:

edn form would require quotes:

Or break args apart:

Server State

To support server control and the ability to attach to remote sessions, create a map of socket server/session data stored in a var with the following structure:

Session state then has well known "session identifier" for stashing and retrieving session-specific state: {:server server-name :session session-id}.

Behavior

On startup, Clojure runtime will:

  • Read configuration (see above) for set of socket servers and their configuration
  • For each socket server:
    • Read socket server configuration and apply defaults
    • Start socket server with address, port, name
    • Update server state
    • Load the namespace of the accept function
    • Start socket server thread
    • Start loop in server thread that:
      • Accepts on server socket
      • Starts client thread (could also reuse from pool)
      • Binds
        • *in* = LineNumberingPushbackReader of InputStreamReader of client socket InputStream
        • *out* = BufferedWriter of OutputStreamWriter of client socket OutputStream
        • *err* = same as *out* (if bind-err is true)
        • clojure.core.server/*session* = {:server server-name :session session-id}
      • Invokes accept-fn with args

Control

  • Server socket can be stopped by retrieving the socket from the server state map and closing it
  • Server loop will stop automatically when the socket is closed
  • Client socket is closed if any exception occurs in the accept function or function completes
  • Client thread ends when client socket is closed
  • All servers can be stopped via clojure.core.server/stop-servers.

Error Handling

Error situations and how they are handled:

ThreadScenarioResult
Main startup threadServer configuration is invalid or incompleteThrows exception, Clojure runtime will not start
Server threadServer socket fails to bind (ex: port already in use)Throws exception, Clojure runtime will not start
Server threadServer socket dies (ex: network error)Server thread dies, server state cleaned up
TBD: who and how to notify in this case? 
Client threadAccept function throws errorClient thread dies, client state cleaned up
TBD: who and how to notify in this case? 
Client threadREPL expression throws exceptionPrint exception to error stream
Client threadREPL command throws exceptionPrint exception to error stream
   

In the two cases marked above, the threads will properly clean up sockets and server info state and the exception will propagate out the top of the server and client threads. Since no exception handler is installed, the default uncaught exception handler can be installed to catch these if desired in the current patch. Alternately, we could provide a more explicit hook and/or store the exception in a var for retrieval. In the server case, the server socket could be restarted, however this could lead to killing a server by putting it in a retry loop if (for example) it became detached from the network.

Sessions

To allow a tool to share repl context with a user's repl, we want to have two repl client connections that share a "session", essentially the dynamic context in which they evaluate things. This allows an internal tool repl to get access to dynamic variables like \*ns* or potentially any other dynamic variable binding state. 

To record binding state:

  • After every eval in the socket repl, get and store the current bindings in the server map

To use binding state:

  • Allow a socket client repl to attach to a different session - this is done by recording the attached session id in this session's state 
  • Before every eval, if client is attached
    • Get attached session's bindings
    • Replace the stream bindings (in/out/err) with the client repl's bindings (don't want to hijack those)
    • Push the attached session's bindings
  • After every eval, if client is attached
    • Pop the bindings

REPL Extensions

REPL Configuration

Currently the clojure.main/repl function takes optional kwargs to specify the properties of the repl in the form of functions. The properties loaded by the socket server will likely be read from strings, not be actual functions. Need to extend the function to resolve symbols to the referred function.

REPL Control

REPL should have a means of control that is distinguishable from normal REPL input. For now, we are presuming these will be keywords with the "repl" namespace:

  • :repl/quit - exits the current repl function, similar to Ctrl-D in some REPL environments. This can be used to exit a nested REPL without killing an entire connection.
  • :repl/sessions - return vector of running session ids
  • :repl/attach {:server server-name :client session-id} - attach to existing session and interpret vars in terms of that session
  • :repl/detach - revert to last session or if top-level session, no change

REPL Data Communication

Two known areas where a REPL communicated information in a human-oriented form rather than a data form were:

  • Print of an object without a print-method  (function, namespace, future, etc)
  • Print of a stack trace encountered during eval

These two cases have been addressed with new data forms in recent commits.

Labels: