diff --git a/README.md b/README.md index c911105..9121b5d 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,24 @@ compiler. They come with a from-source Chez install; a distro `chezscheme` package ships only the runtime, so `build` won't link a binary there. RFC 0007 (`docs/rfc/`) covers the design and the three-mode model. +## Standalone joltc binary + +`make` builds joltc itself into a single self-contained native binary — the +runtime, compiler, `jolt-core`/`stdlib` source, and the Chez boots are baked in, +so the result runs and `build`s jolt apps on a machine with neither Chez nor a C +compiler. Build it on a host that *does* have both. + +```bash +make joltc-release # => target/release/joltc (optimize-level 3, compressed) +make joltc-debug # => target/debug/joltc (optimize-level 0, inspector + debug info) +make joltc # re-mint the seed first, then both +``` + +`make joltc` re-mints the seed so the embedded compiler image is current before +linking; use `joltc-release`/`joltc-debug` directly to skip that when the seed is +already minted. Like `build`, both require Chez's kernel development files +(`libkernel.a`, `scheme.h`) and a C compiler. + ## Architecture A small Chez runtime (`host/chez/*.ss`: value model, persistent collections, seqs, diff --git a/host/chez/java/concurrency.ss b/host/chez/java/concurrency.ss index 75fe146..3469e07 100644 --- a/host/chez/java/concurrency.ss +++ b/host/chez/java/concurrency.ss @@ -459,6 +459,27 @@ (jolt-main-job-val job) (raise (jolt-main-job-val job)))))))) +(define jolt-pump-kih + (lambda () + (for-each (lambda (th) (guard (e (#t #f)) (th))) + (reverse (unbox jolt-shutdown-hooks))) + (exit 0))) + +;; Park the calling thread until a keyboard interrupt (^C), then run the shutdown +;; hooks and exit. Unlike run-main-pump (whose tight recursive condition-wait +;; loop elides Chez's interrupt poll points, so the handler never fires), this +;; uses a single condition-wait — the form Chez reliably interrupts. The nREPL +;; server parks here; SIGINT is unblocked in this thread first (it was masked by +;; jolt-block-sigint so the accept loop inherited a blocked mask and couldn't +;; absorb ^C in its foreign accept() call). +(define jolt-park-mu (make-mutex)) +(define jolt-park-cv (make-condition)) +(define (jolt-park-until-interrupt) + (keyboard-interrupt-handler jolt-pump-kih) + (jolt-set-sigint-blocked #f) + (with-mutex jolt-park-mu (condition-wait jolt-park-cv jolt-park-mu)) + jolt-nil) + (define (jolt-run-main-pump) (with-mutex jolt-main-queue-mu (set-box! jolt-main-pump-stop #f) @@ -508,6 +529,51 @@ (condition-broadcast jolt-main-queue-cv)) jolt-nil) +;; Shutdown hooks run by jolt-pump-kih (the keyboard-interrupt-handler installed by +;; park-until-interrupt) before (exit 0), so a foreground server (nREPL) can close +;; its socket and drop .nrepl-port on ^C instead of Chez's default mutex-corrupting +;; abort. Newest-first; each hook is isolated so one failing hook can't block the exit. +(define jolt-shutdown-hooks (box '())) +(define (jolt-add-shutdown-hook thunk) + (set-box! jolt-shutdown-hooks (cons thunk (unbox jolt-shutdown-hooks))) + jolt-nil) + +;; Per-thread SIGINT mask. A worker thread parked in a foreign call (the nREPL +;; accept loop in c-accept, or a conn handler) can't run Chez's keyboard-interrupt +;; handler on ^C, so if SIGINT is delivered there the process hangs. Block SIGINT +;; in the primordial thread BEFORE forking such workers (they inherit the mask), +;; then park-until-interrupt unblocks it in the primordial once its handler is +;; installed, so ^C is always delivered to the parked thread. pthread_sigmask/ +;; sigaddset are libc/libpthread symbols, resolvable once the process object is +;; loaded (as the socket fns already are). 128 bytes covers Linux's 1024-bit +;; sigset_t and is larger than macOS's 4-byte one. +(define c-pthread-sigmask + (foreign-procedure "pthread_sigmask" (int u8* u8*) int)) +(define c-sigemptyset (foreign-procedure "sigemptyset" (u8*) int)) +(define c-sigaddset (foreign-procedure "sigaddset" (u8* int) int)) +;; POSIX SIG_BLOCK/SIG_UNBLOCK numerics differ by platform: Linux/glibc 0/1, +;; Darwin/macOS 1/2 (SIG_UNBLOCK is SIG_BLOCK+1 on both). Resolve SIG_BLOCK for +;; this host from the machine-type symbol — macOS builds contain "osx". +(define jolt-sig-block-how + (let* ((s (symbol->string (machine-type))) + (n (string-length s))) + (let loop ((i 0)) + (cond + ((> (+ i 3) n) 0) ; default: Linux/glibc + ((string=? (substring s i (+ i 3)) "osx") 1) ; Darwin/macOS + (else (loop (+ i 1))))))) +(define (jolt-set-sigint-blocked block?) + (let ((set (make-bytevector 128 0)) + (old (make-bytevector 128 0))) + (c-sigemptyset set) + (c-sigaddset set 2) ; SIGINT = 2 + (c-pthread-sigmask (if block? jolt-sig-block-how (+ jolt-sig-block-how 1)) set old) + jolt-nil)) + (def-var! "jolt.host" "call-on-main-thread" jolt-call-on-main-thread) (def-var! "jolt.host" "run-main-pump" jolt-run-main-pump) (def-var! "jolt.host" "stop-main-pump" jolt-stop-main-pump) +(def-var! "jolt.host" "add-shutdown-hook" jolt-add-shutdown-hook) +(def-var! "jolt.host" "block-sigint" (lambda () (jolt-set-sigint-blocked #t))) +(def-var! "jolt.host" "park-until-interrupt" jolt-park-until-interrupt) +(def-var! "jolt.host" "delete-file" delete-file) diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj index b8b0d36..bf088af 100644 --- a/jolt-core/jolt/main.clj +++ b/jolt-core/jolt/main.clj @@ -200,10 +200,22 @@ ;; 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. + ;; Block SIGINT in this (primordial) thread before starting the server so the + ;; accept-loop future — and the conn-handler futures it spawns — inherit a + ;; blocked SIGINT mask. Without this, ^C lands on the accept loop blocked in + ;; c-accept (a foreign call), where Chez can't fire the keyboard-interrupt + ;; handler, and the server hangs. park-until-interrupt unblocks SIGINT here + ;; once its own ^C handler is installed, so ^C reaches this thread and the + ;; shutdown hooks run cleanly. + (jolt.host/block-sigint) (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) + ;; register stop so ^C (handled by park-until-interrupt) closes the socket + ;; and drops .nrepl-port on the way out. + (jolt.host/add-shutdown-hook stop) + ;; park here until ^C (handled by park-until-interrupt's keyboard-interrupt- + ;; handler, which runs the shutdown hooks and exits). The accept loop + ;; inherited SIGINT-blocked above, so ^C is delivered to this thread. + (jolt.host/park-until-interrupt) (when stop (stop)))))) (defn- usage [] diff --git a/jolt-core/jolt/nrepl.clj b/jolt-core/jolt/nrepl.clj index d886c16..e73f92d 100644 --- a/jolt-core/jolt/nrepl.clj +++ b/jolt-core/jolt/nrepl.clj @@ -29,9 +29,9 @@ (ffi/defcfn c-bind "bind" [:int :pointer :int] :int) (ffi/defcfn c-listen "listen" [:int :int] :int) (ffi/defcfn c-setsockopt "setsockopt" [:int :int :int :pointer :int] :int) -(ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking) -(ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking) -(ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking) + (ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking) + (ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking) + (ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking) (ffi/defcfn c-close "close" [:int] :int) (def ^:private AF-INET 2) @@ -243,18 +243,18 @@ (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 [] + (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))))) - (fn stop [] - (when (compare-and-set! stopped false true) - (c-close fd) - (try (io/delete-file ".nrepl-port" true) (catch :default _ nil))) - nil)))) + (fn stop [] + (when (compare-and-set! stopped false true) + (c-close fd) + (jolt.host/delete-file ".nrepl-port")) + nil))))