nREPL: surface startup failures and close the listen socket on shutdown
Two pre-existing issues in the nrepl command, exposed when #269 moved the accept loop into a future. jolt.nrepl/start was invoked inside (future ...), so binding the socket happened on a background thread. A bind failure such as the port already being in use was captured into a future that nothing derefs and silently swallowed, leaving the main thread parked in run-main-pump forever with no server and no error. start now binds the socket synchronously before returning, so that failure propagates to the caller and the process exits with a visible message. Only the blocking accept loop runs on a worker thread. There was also no shutdown path: the accept loop ran forever and the listen socket was never closed. start now returns a stop fn that breaks the accept loop, closes the socket to free the port, and removes .nrepl-port. main.clj runs run-main-pump and then calls the stop fn, so a stop-main-pump (from a glimmer app on quit, or from nrepl-evaluated code) shuts the server down cleanly and the process exits.
This commit is contained in:
parent
d21feba486
commit
ad5affe89f
2 changed files with 41 additions and 15 deletions
|
|
@ -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]")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
(future
|
||||||
|
;; A stop closes fd, which makes the blocking accept() return an error; the
|
||||||
|
;; @stopped check then breaks the loop instead of spinning on the dead fd.
|
||||||
(loop []
|
(loop []
|
||||||
(let [conn (c-accept fd ffi/null ffi/null)]
|
(let [conn (c-accept fd ffi/null ffi/null)]
|
||||||
|
(when-not @stopped
|
||||||
(when (>= conn 0)
|
(when (>= conn 0)
|
||||||
(future (try (handle-conn conn handler)
|
(future (try (handle-conn conn handler)
|
||||||
(catch :default e (println "nrepl conn error:" (err-msg e)) (c-close conn)))))
|
(catch :default e (println "nrepl conn error:" (err-msg e)) (c-close conn)))))
|
||||||
(recur))))))
|
(recur)))))
|
||||||
|
(fn stop []
|
||||||
|
(when (compare-and-set! stopped false true)
|
||||||
|
(c-close fd)
|
||||||
|
(try (io/delete-file ".nrepl-port" true) (catch :default _ nil)))
|
||||||
|
nil))))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue