Merge pull request #271 from jolt-lang/fix/nrepl-startup-and-shutdown

nREPL: surface startup failures and close the listen socket on shutdown
This commit is contained in:
Dmitri Sotnikov 2026-06-29 18:17:26 +00:00 committed by GitHub
commit 2a8783649e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 15 deletions

View file

@ -188,13 +188,18 @@
(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)
;; Run the accept loop on a worker thread so the primordial (main) thread ;; start binds the socket synchronously on this (primordial) thread, so a
;; stays free to own the GUI main loop. glimmer's run marshals its startup ;; failure like the port already being in use surfaces here and exits rather
;; onto this thread via jolt.host/call-on-main-thread — on macOS GTK quartz, ;; than being swallowed by a background thread. It then runs the accept loop
;; g_application_run must run on the main thread or AppKit aborts when it ;; on a worker thread and returns a stop fn, leaving this thread free to own
;; sets the main menu. ;; the GUI main loop: glimmer's run marshals its startup here via
(future ((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved))) ;; jolt.host/call-on-main-thread — on macOS GTK quartz, g_application_run
(jolt.host/run-main-pump)))) ;; must run on the main thread or AppKit aborts when it sets the main menu.
(let [stop ((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved))]
;; park here until something calls jolt.host/stop-main-pump, then shut the
;; server down cleanly (close the socket, remove .nrepl-port) and return.
(jolt.host/run-main-pump)
(when stop (stop))))))
(defn- usage [] (defn- usage []
(println "usage: jolt <command> [args]") (println "usage: jolt <command> [args]")

View file

@ -17,6 +17,7 @@
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]
[clojure.java.io :as io]
[jolt.ffi :as ffi])) [jolt.ffi :as ffi]))
;; --- sockets (loopback server) --------------------------------------------- ;; --- sockets (loopback server) ---------------------------------------------
@ -222,18 +223,38 @@
(defn start (defn start
"Start the nREPL server on `port` (a concrete port; loopback only). `middleware` "Start the nREPL server on `port` (a concrete port; loopback only). `middleware`
is a vector of deps.edn :nrepl/middleware symbols to compose over the built-in is a vector of deps.edn :nrepl/middleware symbols to compose over the built-in
handler. Writes .nrepl-port. Blocks accepting connections." handler.
Binds the socket synchronously, so a startup failure (e.g. the port is already
in use) is thrown to the caller rather than swallowed by the accept thread, then
accepts connections on a background thread and returns immediately. Writes
.nrepl-port. Does NOT block the caller keeps the process alive (jolt.main
parks the main thread in jolt.host/run-main-pump).
Returns a zero-arg stop fn: it stops the accept loop, closes the listen socket
(freeing the port), and removes .nrepl-port. Calling it more than once is a
no-op."
([port] (start port nil)) ([port] (start port nil))
([port middleware] ([port middleware]
(let [handler (build-handler (resolve-middleware (or middleware []))) (let [handler (build-handler (resolve-middleware (or middleware [])))
fd (listen-socket port)] fd (listen-socket port) ; throws on bind/listen failure
stopped (atom false)]
(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)))) (when (seq middleware) (println (str ";; middleware: " (str/join " " middleware))))
(println ";; connect your editor; ^C to stop") (println ";; connect your editor; ^C to stop")
(loop [] (future
(let [conn (c-accept fd ffi/null ffi/null)] ;; A stop closes fd, which makes the blocking accept() return an error; the
(when (>= conn 0) ;; @stopped check then breaks the loop instead of spinning on the dead fd.
(future (try (handle-conn conn handler) (loop []
(catch :default e (println "nrepl conn error:" (err-msg e)) (c-close conn))))) (let [conn (c-accept fd ffi/null ffi/null)]
(recur)))))) (when-not @stopped
(when (>= conn 0)
(future (try (handle-conn conn handler)
(catch :default e (println "nrepl conn error:" (err-msg e)) (c-close conn)))))
(recur)))))
(fn stop []
(when (compare-and-set! stopped false true)
(c-close fd)
(try (io/delete-file ".nrepl-port" true) (catch :default _ nil)))
nil))))