Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Clojure: Production Web REPL

DZone's Guide to

Clojure: Production Web REPL

· Java Zone ·
Free Resource

The CMS developers love. Open Source, API-first and Enterprise-grade. Try BloomReach CMS for 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))))

 
 

BloomReach CMS: the API-first CMS of the future. Open-source & enterprise-grade. - As a Java developer, you will feel at home using Maven builds and your favorite IDE (e.g. Eclipse or IntelliJ) and continuous integration server (e.g. Jenkins). Manage your Java objects using Spring Framework, write your templates in JSP or Freemarker. Try for free.

Topics:

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}