Clojure: Production Web REPL
Join the DZone community and get the full member experience.
Join For FreeContext
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
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, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments