Over a million developers have joined DZone.

Clojure: Production Web REPL

· Java Zone

Bitbucket is for the code that takes us to Mars, decodes the human genome, or drives your next car. What will your code do? Get started with Bitbucket today, it's free.

A REPL is a powerful tool. I use the REPL extensively in development, and I recently added a web REPL to each of our production applications.

Context
A production REPL is not for the faint of heart. I wouldn't add a REPL to production in 95% of the projects I've been a part of; however, I'm currently working with two other developers that I completely trust with the power of a production REPL. As a simple rule, I wouldn't give access to a production REPL to anyone that doesn't also have root access to the server - if you can't be trusted with one, it's likely that you can't be trusted with the other. However, if you can be trusted, I'd rather you have all of your tools available to you.
sidebar: the most interesting story I've heard about a REPL in prod was the following - Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem. -- Lisping at JPL
Motivation
Don't get the wrong impression, I'm not regularly redefining functions in production. I rarely use a prod REPL, but when I do, I almost always use it to read reference data. On the very rare occasion, I'll wrap an existing function with a new version that logs the value of the incoming arguments. In short, I use the prod REPL (again, sparingly) to gather more data, not to fix outstanding bugs. I'm not saying I would never use it to fix a bug, but that would be an extremely exceptional case.

Code
It's actually very easy to plug a web REPL into a Clojure application that already has a web interface. The Clojure applications I work on tend to have web sockets, and the following solution uses web sockets; however, there's no reason that you couldn't use simple web requests and responses. Lastly, I didn't actually write this code. Someone at DRW wrote it (I have no idea who) and it's been copy-pasted several times since then. Without further adieu, the code:
<!DOCTYPE HTML>
<html>
<head>
    <link href="css/web-repl.css" rel="stylesheet" type="text/css">
    <script src="js/lib/jquery-1.5.min.js" type="text/javascript"></script>
    <script src="js/lib/jquery.websocket-0.0.1.js" type="text/javascript"></script>
    <script src="js/lib/jquery.console.js" type="text/javascript"></script>
    <script src="js/web-repl.js" type="text/javascript"></script>
    <title>Web REPL</title>
</head>
<body>
<div id="console"/>
</body>
</html>

var ws = null;

$(document).ready(function () {
    ws = $.websocket("ws://" + window.location.host + "/websocket", {
        events: {
            'web-repl-response': function(info) {
                currentCallback([
                    {msg: info.response,
                        className:"jquery-console-message-value"}
                ]);
            }
        }
    });

    $("#console").console({
        promptLabel: 'Clojure> ',
        commandValidate:function(line) {
            if (line == "") {
                return false;
            }
            else {
                return true;
            }
        },
        commandHandle:function(line, callback) {
            currentCallback = callback;
            ws.send('selfish', {type: "web-repl", command: line});
        },
        welcomeMessage:'Enter some Clojure code, and it will be evaluated ON THE SERVER -- CAREFUL!!!.',
        autofocus:true,
        animateScroll:true,
        promptHistory:true
    })
});

(ns web-repl
  (:require clojure.main)
  (:use [clojure.stacktrace :only [root-cause]]))

(defonce repl-sessions (ref {}))

(defn current-bindings []
  (binding [*ns* *ns*
            *warn-on-reflection* *warn-on-reflection*
            *math-context* *math-context*
            *print-meta* *print-meta*
            *print-length* *print-length*
            *print-level* *print-level*
            *compile-path* (System/getProperty "clojure.compile.path" "classes")
            *command-line-args* *command-line-args*
            *assert* *assert*
            *1 nil
            *2 nil
            *3 nil
            *e nil]
    (get-thread-bindings)))

(defn bindings-for [session-key]
  (when-not (@repl-sessions session-key)
    (dosync
      (commute repl-sessions assoc session-key (current-bindings))))
  (@repl-sessions session-key))

(defn store-bindings-for [session-key]
  (dosync
    (commute repl-sessions assoc session-key (current-bindings))))

(defmacro with-session [session-key & body]
  `(with-bindings (bindings-for ~session-key)
    (let [r# ~@body]
      (store-bindings-for ~session-key)
      r#)))

(defn do-eval [txt session-key]
  (with-session session-key
    (let [form (binding [*read-eval* false] (read-string txt))]
      (with-open [writer (java.io.StringWriter.)]
        (binding [*out* writer]
          (try
            (let [r (pr-str (eval form))]
              (str (.toString writer) (str r)))
            (catch Exception e (str (root-cause e)))))))))

#console {
    height: 820px;
    background: #eee;
    margin: 10px;
    border-radius: 5px;
    -moz-border-radius: 5px;
    border: 1px solid #aaa;
}

#console div.jquery-console-inner {
    height: 800px;
    margin: 10px 10px;
    overflow: auto;
    text-align: left;
}

#console div.jquery-console-welcome {
    color: #ef0505;
    font-family: sans-serif;
    font-weight: bold;
    padding: 0.1em;
}

#console div.jquery-console-message-value {
    color: #0066FF;
    font-family: monospace;
    padding: 0.1em;
}

#console div.jquery-console-prompt-box {
    color: #444;
    font-family: monospace;
}

#console div.jquery-console-focus span.jquery-console-cursor {
    background: #333;
    color: #eee;
    font-weight: bold;
}

#console div.jquery-console-message-error {
    color: #ef0505;
    font-family: sans-serif;
    font-weight: bold;
    padding: 0.1em;
}

#console div.jquery-console-message-success {
    color: #187718;
    font-family: monospace;
    padding: 0.1em;
}

#console span.jquery-console-prompt-label {
    font-weight: bold;
}

; the code I work with has a publish-fn that we use to publish json back to a web-socket.
; therefore, the following line is all we need to add to our application to get the web REPL working
(publish-fn (hash-map :type :web-repl-response :response (web-repl/do-eval command publish-fn))))

 
 

Bitbucket is the Git solution for professional teams who code with a purpose, not just as a hobby. Get started today, it's free.

Topics:

Published at DZone with permission of Jay Fields, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}