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)
|
||||
(parse-long (or (jolt.host/getenv "JOLT_NREPL_PORT") "7888")))]
|
||||
(require 'jolt.nrepl)
|
||||
;; Run the accept loop on a worker thread so the primordial (main) thread
|
||||
;; stays free to own the GUI main loop. glimmer's run marshals its startup
|
||||
;; onto this thread via jolt.host/call-on-main-thread — on macOS GTK quartz,
|
||||
;; g_application_run must run on the main thread or AppKit aborts when it
|
||||
;; sets the main menu.
|
||||
(future ((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved)))
|
||||
(jolt.host/run-main-pump))))
|
||||
;; start binds the socket synchronously on this (primordial) thread, so a
|
||||
;; failure like the port already being in use surfaces here and exits rather
|
||||
;; than being swallowed by a background thread. It then runs the accept loop
|
||||
;; on a worker thread and returns a stop fn, leaving this thread free to own
|
||||
;; the GUI main loop: glimmer's run marshals its startup here via
|
||||
;; jolt.host/call-on-main-thread — on macOS GTK quartz, g_application_run
|
||||
;; 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 []
|
||||
(println "usage: jolt <command> [args]")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
Writes .nrepl-port in the project dir so editors auto-detect the port."
|
||||
(:require [clojure.string :as str]
|
||||
[clojure.java.io :as io]
|
||||
[jolt.ffi :as ffi]))
|
||||
|
||||
;; --- sockets (loopback server) ---------------------------------------------
|
||||
|
|
@ -222,18 +223,38 @@
|
|||
(defn start
|
||||
"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
|
||||
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 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))
|
||||
(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")
|
||||
(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 []
|
||||
(let [conn (c-accept fd ffi/null ffi/null)]
|
||||
(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))))))
|
||||
(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