Jay Fields is a software developer at DRW Trading. He has a passion for discovering and maturing innovative solutions. His most recent work has been in the Domain Specific Language space where he's delivered applications that empowered subject matter experts to write the business rules of the applications. He is also very interested in maturing software design through software testing. Jay is a DZone MVB and is not an employee of DZone and has posted 116 posts at DZone. You can read more from them at their website. View Full User Profile

Clojure: Production Web REPL

07.02.2012
| 2424 views |
  • submit to reddit
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))))

  
Published at DZone with permission of Jay Fields, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)