Статьи

Clojure: производственная сеть REPL

РЕПЛ является мощным инструментом. Я широко использую REPL в разработке, и недавно я добавил веб-REPL для каждого из наших производственных приложений.

Контекст

Производственный REPL не для слабонервных. Я бы не стал добавлять REPL в производство в 95% проектов, частью которых я был; однако в настоящее время я работаю с двумя другими разработчиками, которым полностью доверяю возможности производственного REPL. Как простое правило, я бы не дал доступ к производственному REPL любому, у кого нет корневого доступа к серверу — если вам нельзя доверять с одним, скорее всего, вам нельзя доверять с помощью разное. Однако, если вам можно доверять, я бы предпочел, чтобы у вас были все доступные инструменты.

Боковая панель : самая интересная история, которую я слышал о REPL в prod, была следующей: отладка программы, работающей на оборудовании стоимостью 100 миллионов долларов, которое находится на расстоянии 100 миллионов миль, является интересным опытом. Наличие цикла чтения-проверки-печати на космическом корабле оказалось неоценимым в поиске и устранении проблемы. —
Лиспинг в JPL

Мотивация

Не поймите меня неправильно, я не регулярно пересматриваю функции в производстве. Я редко использую продукт REPL, но когда я делаю, я почти всегда использую его для чтения справочных данных. В очень редком случае я оберну существующую функцию новой версией, которая регистрирует значение входящих аргументов. Короче говоря, я использую продукт REPL (опять же, экономно) для сбора дополнительных данных, а не для исправления ошибок. Я не говорю, что никогда не использовал бы это, чтобы исправить ошибку, но это был бы чрезвычайно исключительный случай.

Код

На самом деле очень легко подключить веб-REPL к приложению Clojure, которое уже имеет веб-интерфейс. Приложения Clojure, над которыми я работаю, обычно имеют веб-сокеты, а в следующем решении используются веб-сокеты; однако нет никаких причин, по которым вы не могли бы использовать простые веб-запросы и ответы. Наконец, я на самом деле не писал этот код. Кто-то из
DRW написал это (я понятия не имею, кто), и с тех пор его копировали несколько раз. Без лишних слов, код:

<!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))))