nREPL: make the built-in server middleware-extensible
The built-in nREPL stays minimal (clone/describe/eval/load-file/close) but now composes a middleware stack so a library can add the heavier features (sessions, interruptible-eval, completion, lookup) without bloating core. - A middleware is (fn [handler] (fn [request] ...)); request carries :reply (a thread-safe send that adds id/session) plus the wire fields. List them in deps.edn :nrepl/middleware (symbols -> a middleware fn or a vector of them); jolt.nrepl composes them over the built-in handler. - Public seam: respond, evaluate, register-ops!, new-session, err-msg. - Per-connection send lock so middleware replying from other threads don't interleave bytes. describe advertises built-in + registered ops. deps.clj surfaces :nrepl/middleware; jolt.main passes it to the server. Built-in behavior unchanged when no middleware is declared. Runtime, no re-mint.
This commit is contained in:
parent
d33277c0b2
commit
185b4fd3ca
3 changed files with 130 additions and 73 deletions
|
|
@ -124,7 +124,10 @@
|
||||||
{:roots (vec (distinct (concat project-roots dep-roots)))
|
{:roots (vec (distinct (concat project-roots dep-roots)))
|
||||||
:main-opts main-opts
|
:main-opts main-opts
|
||||||
:tasks (:tasks edn)
|
:tasks (:tasks edn)
|
||||||
:natives (vec (distinct (concat (:jolt/native edn) dep-natives)))})))
|
:natives (vec (distinct (concat (:jolt/native edn) dep-natives)))
|
||||||
|
;; nREPL middleware a library contributes (jolt.nrepl composes them over its
|
||||||
|
;; built-in handler) — symbols resolving to a middleware fn or a vector of them.
|
||||||
|
:nrepl-middleware (:nrepl/middleware edn)})))
|
||||||
|
|
||||||
(defn has-deps-edn? [project-dir]
|
(defn has-deps-edn? [project-dir]
|
||||||
(file-exists? (str project-dir "/deps.edn")))
|
(file-exists? (str project-dir "/deps.edn")))
|
||||||
|
|
|
||||||
|
|
@ -114,12 +114,15 @@
|
||||||
|
|
||||||
(defn- nrepl [more]
|
(defn- nrepl [more]
|
||||||
;; resolve the project (deps on the roots, native libs loaded), then start the
|
;; resolve the project (deps on the roots, native libs loaded), then start the
|
||||||
;; nREPL server so an editor can connect and (require '[some.lib]) live.
|
;; nREPL server so an editor can connect and (require '[some.lib]) live. A
|
||||||
(try (apply-project! (deps/resolve-project (project-dir))) (catch :default _ nil))
|
;; library's middleware (deps.edn :nrepl/middleware) is composed over the
|
||||||
|
;; built-in handler — sessions / interruptible-eval / completion etc.
|
||||||
|
(let [resolved (deps/resolve-project (project-dir))]
|
||||||
|
(apply-project! resolved)
|
||||||
(let [port (or (some-> (first (filter #(not (str/starts-with? % "-")) more)) parse-long)
|
(let [port (or (some-> (first (filter #(not (str/starts-with? % "-")) more)) parse-long)
|
||||||
(parse-long (or (jolt.host/getenv "JOLT_NREPL_PORT") "7888")))]
|
(parse-long (or (jolt.host/getenv "JOLT_NREPL_PORT") "7888")))]
|
||||||
(require 'jolt.nrepl)
|
(require 'jolt.nrepl)
|
||||||
((resolve 'jolt.nrepl/start) port)))
|
((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved)))))
|
||||||
|
|
||||||
(defn- usage []
|
(defn- usage []
|
||||||
(println "usage: jolt <command> [args]")
|
(println "usage: jolt <command> [args]")
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
(ns jolt.nrepl
|
(ns jolt.nrepl
|
||||||
"A minimal nREPL server for jolt, so an editor (CIDER / Calva / Cursive) can
|
"A minimal, extensible nREPL server for jolt, so an editor (CIDER / Calva /
|
||||||
connect and develop a project live. Speaks bencode over a loopback TCP socket
|
Cursive) can connect and develop a project live. Speaks bencode over a loopback
|
||||||
bound through jolt.ffi; supports the ops a client needs to connect and eval:
|
TCP socket bound through jolt.ffi. Built in: clone, describe, eval, load-file,
|
||||||
clone, describe, eval, load-file, close. The project's deps are already on the
|
close — enough to connect and eval, with the project's deps on the roots and
|
||||||
source roots and its native libs loaded (jolt.main applies the project before
|
native libs loaded (jolt.main applies the project first), so (require '[lib])
|
||||||
starting the server), so (require '[some.lib]) works at the REPL.
|
works.
|
||||||
|
|
||||||
|
EXTENSIBLE: a library can add the heavier nREPL features (sessions,
|
||||||
|
interruptible-eval, completion, lookup) as MIDDLEWARE without bloating core. A
|
||||||
|
middleware is `(fn [handler] (fn [request] ...))`; list them in deps.edn under
|
||||||
|
:nrepl/middleware (symbols resolving to a middleware fn, or to a vector of them)
|
||||||
|
and jolt.nrepl composes them over the built-in handler. The request is the
|
||||||
|
decoded bencode map (string keys: \"op\" \"code\" \"ns\" \"id\" \"session\" …)
|
||||||
|
plus :reply — a thread-safe (fn [response-map]) that adds id/session and sends.
|
||||||
|
Public seam for middleware: respond, evaluate, register-ops!, new-session.
|
||||||
|
|
||||||
Writes .nrepl-port in the project dir so editors auto-detect the port."
|
Writes .nrepl-port in the project dir so editors auto-detect the port."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
|
|
@ -109,80 +118,122 @@
|
||||||
(when (<= end (count s)) [(subs s start end) end]))))
|
(when (<= end (count s)) [(subs s start end) end]))))
|
||||||
:else nil))))
|
:else nil))))
|
||||||
|
|
||||||
;; --- eval ------------------------------------------------------------------
|
;; --- public seam for middleware --------------------------------------------
|
||||||
(def ^:private session-counter (atom 0))
|
(def ^:private session-counter (atom 0))
|
||||||
(defn- new-session [] (str "jolt-" (swap! session-counter inc)))
|
(defn new-session
|
||||||
|
"A fresh session id (middleware that implements sessions uses this)."
|
||||||
|
[] (str "jolt-" (swap! session-counter inc)))
|
||||||
|
|
||||||
(defn- err-msg [e]
|
(defn respond
|
||||||
|
"Send a response map for `request` (id/session added, then bencoded)."
|
||||||
|
[request m] ((:reply request) m))
|
||||||
|
|
||||||
|
(defn err-msg
|
||||||
|
"Best-effort message for any thrown value (ex-info, or a raw Chez condition —
|
||||||
|
ex-message is nil for those, so fall back to the host condition text)."
|
||||||
|
[e]
|
||||||
(or (ex-message e)
|
(or (ex-message e)
|
||||||
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
|
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
|
||||||
(pr-str e)))
|
(pr-str e)))
|
||||||
|
|
||||||
(defn- eval-in-ns [ns-str code]
|
(defn evaluate
|
||||||
;; evaluate in the requested ns if it's loaded (CIDER sends :ns with each eval).
|
"Evaluate `code` (optionally in loaded ns `ns-str`), capturing *out*. Returns
|
||||||
;; in-ns — not (binding [*ns* ..]) — sets the ns load-string resolves against on
|
{:value .. :out .. :ns .. :err ..}. in-ns — not (binding [*ns* ..]) — sets the
|
||||||
;; jolt; it persists for the next eval (fine for a serial editor session).
|
ns load-string resolves against on jolt. Reusable by eval middleware."
|
||||||
(when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
|
[code ns-str]
|
||||||
(in-ns (symbol ns-str)))
|
|
||||||
(load-string code))
|
|
||||||
|
|
||||||
(defn- do-eval [code ns-str]
|
|
||||||
;; eval the code, capturing *out*; return {:value .. :out .. :ns .. :err ..}.
|
|
||||||
(let [result (atom nil) err (atom nil)
|
(let [result (atom nil) err (atom nil)
|
||||||
out (with-out-str
|
out (with-out-str
|
||||||
(try (reset! result (eval-in-ns ns-str (wire-> code)))
|
(try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
|
||||||
|
(in-ns (symbol ns-str)))
|
||||||
|
(reset! result (load-string code))
|
||||||
(catch :default e (reset! err (err-msg e)))))]
|
(catch :default e (reset! err (err-msg e)))))]
|
||||||
{:value (when (nil? @err) (pr-str @result))
|
{:value (when (nil? @err) (pr-str @result))
|
||||||
:out out
|
:out out
|
||||||
:ns (str (ns-name *ns*))
|
:ns (str (ns-name *ns*))
|
||||||
:err @err}))
|
:err @err}))
|
||||||
|
|
||||||
(defn- handle-msg [fd msg]
|
;; ops middleware advertise via describe (built-ins + any a library registers).
|
||||||
(let [op (get msg "op")
|
(def ^:private extra-ops (atom #{}))
|
||||||
id (get msg "id")
|
(defn register-ops!
|
||||||
session (or (get msg "session") "none")
|
"Register op name(s) so `describe` advertises them. Call at middleware load."
|
||||||
base (fn [m] (cond-> m id (assoc "id" id) session (assoc "session" session)))
|
[& ops] (swap! extra-ops into (map name ops)))
|
||||||
reply (fn [m] (send-str fd (bencode (base m))))]
|
|
||||||
(cond
|
|
||||||
(= op "clone") (reply {"new-session" (new-session) "status" ["done"]})
|
|
||||||
(= op "close") (reply {"status" ["session-closed" "done"]})
|
|
||||||
(= op "describe") (reply {"status" ["done"]
|
|
||||||
"versions" {"jolt-nrepl" {"major" 0 "minor" 1}}
|
|
||||||
"ops" {"clone" {} "close" {} "describe" {} "eval" {} "load-file" {}}})
|
|
||||||
(or (= op "eval") (= op "load-file"))
|
|
||||||
(let [code (if (= op "load-file") (get msg "file") (get msg "code"))
|
|
||||||
{:keys [value out ns err]} (do-eval code (get msg "ns"))]
|
|
||||||
(when (seq out) (reply {"out" out}))
|
|
||||||
(if err
|
|
||||||
(do (reply {"err" (str err "\n")}) (reply {"ex" (str err) "status" ["eval-error" "done"]}))
|
|
||||||
(reply {"value" value "ns" ns "status" ["done"]})))
|
|
||||||
:else (reply {"status" ["done" "unknown-op"]}))))
|
|
||||||
|
|
||||||
(defn- handle-conn [fd]
|
;; --- built-in handler ------------------------------------------------------
|
||||||
|
(defn- built-in-handler [request]
|
||||||
|
(let [op (get request "op")]
|
||||||
|
(cond
|
||||||
|
(= op "clone") (respond request {"new-session" (new-session) "status" ["done"]})
|
||||||
|
(= op "close") (respond request {"status" ["session-closed" "done"]})
|
||||||
|
(= op "describe") (respond request {"status" ["done"]
|
||||||
|
"versions" {"jolt-nrepl" {"major" 0 "minor" 1}}
|
||||||
|
"ops" (zipmap (into #{"clone" "close" "describe" "eval" "load-file"}
|
||||||
|
@extra-ops)
|
||||||
|
(repeat {}))})
|
||||||
|
(or (= op "eval") (= op "load-file"))
|
||||||
|
(let [code (wire-> (if (= op "load-file") (get request "file") (get request "code")))
|
||||||
|
{:keys [value out ns err]} (evaluate code (get request "ns"))]
|
||||||
|
(when (seq out) (respond request {"out" out}))
|
||||||
|
(if err
|
||||||
|
(do (respond request {"err" (str err "\n")})
|
||||||
|
(respond request {"ex" (str err) "status" ["eval-error" "done"]}))
|
||||||
|
(respond request {"value" value "ns" ns "status" ["done"]})))
|
||||||
|
:else (respond request {"status" ["done" "unknown-op"]}))))
|
||||||
|
|
||||||
|
;; --- middleware composition ------------------------------------------------
|
||||||
|
;; resolve deps.edn :nrepl/middleware symbols to middleware fns. An entry may
|
||||||
|
;; resolve to a single (fn [handler] handler') or to a vector of them (so a
|
||||||
|
;; library can export one `default-middleware` var).
|
||||||
|
(defn- resolve-middleware [syms]
|
||||||
|
(vec (mapcat
|
||||||
|
(fn [sym]
|
||||||
|
(require (symbol (namespace sym)))
|
||||||
|
(let [v (deref (resolve sym))]
|
||||||
|
(if (sequential? v) (map #(if (var? %) (deref %) %) v) [v])))
|
||||||
|
syms)))
|
||||||
|
|
||||||
|
(defn- build-handler [middleware]
|
||||||
|
;; first listed middleware is outermost.
|
||||||
|
(reduce (fn [h mw] (mw h)) built-in-handler (reverse middleware)))
|
||||||
|
|
||||||
|
(defn- handle-conn [fd handler]
|
||||||
|
;; one send lock per connection: eval/session middleware reply from other
|
||||||
|
;; threads, so sends must not interleave.
|
||||||
|
(let [lock (Object.)
|
||||||
|
reply-for (fn [msg]
|
||||||
|
(let [id (get msg "id") session (or (get msg "session") "none")]
|
||||||
|
(fn [m]
|
||||||
|
(locking lock
|
||||||
|
(send-str fd (bencode (cond-> m id (assoc "id" id)
|
||||||
|
session (assoc "session" session))))))))]
|
||||||
(loop [buf ""]
|
(loop [buf ""]
|
||||||
(let [chunk (recv-str fd)]
|
(let [chunk (recv-str fd)]
|
||||||
(if (nil? chunk)
|
(if (nil? chunk)
|
||||||
(c-close fd)
|
(c-close fd)
|
||||||
(let [buf (str buf chunk)
|
(let [rest-buf (loop [b (str buf chunk)]
|
||||||
;; drain every complete message in the buffer
|
|
||||||
rest-buf (loop [b buf]
|
|
||||||
(let [r (bdecode b 0)]
|
(let [r (bdecode b 0)]
|
||||||
(if (nil? r) b
|
(if (nil? r) b
|
||||||
(do (when (map? (first r)) (handle-msg fd (first r)))
|
(do (when (map? (first r))
|
||||||
|
(let [msg (first r)]
|
||||||
|
(try (handler (assoc msg :reply (reply-for msg)))
|
||||||
|
(catch :default e (println "nrepl handler error:" (err-msg e))))))
|
||||||
(recur (subs b (second r)))))))]
|
(recur (subs b (second r)))))))]
|
||||||
(recur rest-buf))))))
|
(recur rest-buf)))))))
|
||||||
|
|
||||||
(defn start
|
(defn start
|
||||||
"Start the nREPL server on `port` (0 = an OS-assigned port is not supported here;
|
"Start the nREPL server on `port` (a concrete port; loopback only). `middleware`
|
||||||
pass a concrete port). Writes .nrepl-port. Blocks accepting connections."
|
is a vector of deps.edn :nrepl/middleware symbols to compose over the built-in
|
||||||
[port]
|
handler. Writes .nrepl-port. Blocks accepting connections."
|
||||||
(let [fd (listen-socket port)]
|
([port] (start port nil))
|
||||||
|
([port middleware]
|
||||||
|
(let [handler (build-handler (resolve-middleware (or middleware [])))
|
||||||
|
fd (listen-socket port)]
|
||||||
(try (spit ".nrepl-port" (str port)) (catch :default _ nil))
|
(try (spit ".nrepl-port" (str port)) (catch :default _ nil))
|
||||||
(println (str "nREPL server started on port " port " (127.0.0.1) — .nrepl-port written"))
|
(println (str "nREPL server started on port " port " (127.0.0.1) — .nrepl-port written"))
|
||||||
|
(when (seq middleware) (println (str ";; middleware: " (str/join " " middleware))))
|
||||||
(println ";; connect your editor; ^C to stop")
|
(println ";; connect your editor; ^C to stop")
|
||||||
(loop []
|
(loop []
|
||||||
(let [conn (c-accept fd ffi/null ffi/null)]
|
(let [conn (c-accept fd ffi/null ffi/null)]
|
||||||
(when (>= conn 0)
|
(when (>= conn 0)
|
||||||
(future (try (handle-conn conn)
|
(future (try (handle-conn conn handler)
|
||||||
(catch :default e (println "nrepl conn error:" (ex-message e)) (c-close conn)))))
|
(catch :default e (println "nrepl conn error:" (err-msg e)) (c-close conn)))))
|
||||||
(recur)))))
|
(recur))))))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue