Run debug REPL in dedicated thread instead of master timer polling
ober
ba64e5ed4fe3c0c6b3cb2b4d18b72d92dabf8187
--- a/lib/jerboa-emacs/debug-repl.sls +++ b/lib/jerboa-emacs/debug-repl.sls @@ -9,14 +9,11 @@ getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) (std sugar) (std srfi srfi-13) (jerboa repl-socket) - (jerboa-emacs async) (jerboa core) (jerboa runtime)) + (jerboa core) (jerboa runtime)) (def *repl-listen-fd* #f) - (def *repl-client-fd* #f) - (def *repl-line-buf* "") (def *repl-actual-port* #f) - (def *repl-token* #f) - (def *repl-authed* #f) - (def *repl-prompted* #f) + (def *repl-running* #f) + (def *repl-thread* #f) (def *repl-port-file* (string-append (getenv "HOME") "/.jerboa-repl-port")) (def (write-repl-port-file! port-num) @@ -32,57 +29,45 @@ (with-catch (lambda _ (void)) (lambda () (delete-file *repl-port-file*))))) - (def (repl-send! str) - "Send a string to the connected client. No-op if no client." - (when *repl-client-fd* - (unless (repl-socket-write *repl-client-fd* str) - (repl-disconnect!)))) - (def (repl-disconnect!) - "Close the client connection and reset state for next accept." - (when *repl-client-fd* - (with-catch - (lambda _ (void)) - (lambda () (repl-socket-close *repl-client-fd*)))) - (set! *repl-client-fd* #f) (set! *repl-line-buf* "") - (set! *repl-authed* #f) (set! *repl-prompted* #f)) (def help-text " ,help This help message\n ,threads List active threads\n ,state Show state summary\n ,gc Force GC and show heap info\n ,quit Close this REPL connection\n <expr> Evaluate arbitrary Chez Scheme expression\n") - (def (process-repl-line! line) - "Process one REPL command line. Returns #t to continue, #f to disconnect." + (def (process-repl-line line client-fd) + "Process one REPL command line. Returns #t to continue, #f to disconnect." (let ([cmd (string-trim-both line)]) (cond [(string=? cmd "") #t] [(string=? cmd ",quit") - (repl-send! "Connection closed.\n") + (repl-fd-send! client-fd "Connection closed.\n") #f] - [(string=? cmd ",help") (repl-send! help-text) #t] + [(string=? cmd ",help") + (repl-fd-send! client-fd help-text) + #t] [(string=? cmd ",threads") (with-catch - (lambda (e) (repl-send! " (error listing threads)\n")) - (lambda () (repl-send! " master-timer (active)\n"))) + (lambda (e) + (repl-fd-send! client-fd " (error listing threads)\n")) + (lambda () + (repl-fd-send! client-fd " REPL thread (active)\n"))) #t] [(string=? cmd ",state") - (repl-send! + (repl-fd-send! + client-fd (string-append " listen-fd: " (number->string (or *repl-listen-fd* -1)) - "\n client-fd: " (number->string (or *repl-client-fd* -1)) + "\n client-fd: " (number->string client-fd) "\n bytes-allocated: " (number->string (bytes-allocated)) "\n")) #t] [(string=? cmd ",gc") (with-catch (lambda (e) - (repl-send! - (string-append - " GC skipped (would deadlock). bytes-allocated: " - (number->string (bytes-allocated)) - "\n"))) + (repl-fd-send! client-fd " (error reading GC stats)\n")) (lambda () - (collect 0) - (repl-send! - (string-append - " GC done (gen 0). bytes-allocated: " - (number->string (bytes-allocated)) - "\n")))) + (repl-fd-send! + client-fd + (string-append " bytes-allocated: " + (number->string (bytes-allocated)) "\n collections: " + (number->string (collections)) + "\n Note: (collect) not safe from REPL thread; GC runs automatically.\n")))) #t] [else (with-catch @@ -95,86 +80,22 @@ (display-condition e (current-output-port))))))]) - (repl-send! (string-append "ERROR: " msg "\n")))) + (repl-fd-send! + client-fd + (string-append "ERROR: " msg "\n")))) (lambda () (let* ([result (eval (read (open-input-string cmd)) (interaction-environment))] [out (open-output-string)]) (write result out) - (repl-send! + (repl-fd-send! + client-fd (string-append (get-output-string out) "\n"))))) #t]))) - (def (debug-repl-tick!) - "Non-blocking REPL poll. Called from the master timer.\n Tries to accept a connection or read data from an existing client." - (when *repl-listen-fd* - (with-catch - (lambda (e) (void)) - (lambda () - (cond - [(not *repl-client-fd*) - (let ([cfd (repl-socket-accept *repl-listen-fd*)]) - (when cfd - (set! *repl-client-fd* cfd) - (set! *repl-line-buf* "") - (set! *repl-prompted* #f) - (if *repl-token* - (begin - (set! *repl-authed* #f) - (repl-send! "token: ")) - (begin - (set! *repl-authed* #t) - (repl-send! - "jerboa debug REPL — type ,help for commands\n")))))] - [*repl-client-fd* - (when (and *repl-authed* (not *repl-prompted*)) - (repl-send! "jerboa-dbg> \n") - (set! *repl-prompted* #t)) - (let ([data (repl-socket-read *repl-client-fd*)]) - (cond - [(string? data) - (set! *repl-line-buf* - (string-append *repl-line-buf* data)) - (repl-process-lines!)] - [(eq? data 'eof) (repl-disconnect!)]))]))))) - (def (repl-process-lines!) - "Extract and process complete lines from *repl-line-buf*." - (let loop () - (let ([nl (repl-string-index *repl-line-buf* #\newline)]) - (when nl - (let ([line (substring *repl-line-buf* 0 nl)] - [rest (substring - *repl-line-buf* - (+ nl 1) - (string-length *repl-line-buf*))]) - (set! *repl-line-buf* rest) - (let ([line (if (and (> (string-length line) 0) - (char=? - (string-ref - line - (- (string-length line) 1)) - #\return)) - (substring - line - 0 - (- (string-length line) 1)) - line)]) - (if *repl-authed* - (let ([continue? (process-repl-line! line)]) - (if continue? - (begin (set! *repl-prompted* #f) (loop)) - (repl-disconnect!))) - (let ([tok (string-trim-both line)]) - (if (string=? tok *repl-token*) - (begin - (set! *repl-authed* #t) - (repl-send! - "jerboa debug REPL — type ,help for commands\n") - (set! *repl-prompted* #f) - (loop)) - (begin - (repl-send! "Access denied.\n") - (repl-disconnect!))))))))))) + (def (repl-fd-send! fd str) + "Send string to fd. Returns #t on success, #f on error." + (repl-socket-write fd str)) (def (repl-string-index str ch) "Return the index of the first occurrence of ch in str, or #f." (let ([len (string-length str)]) @@ -183,31 +104,110 @@ [(>= i len) #f] [(char=? (string-ref str i) ch) i] [else (loop (+ i 1))])))) + (def (handle-client! client-fd token) + "Handle one client connection. Blocks until client disconnects or ,quit.\n Uses non-blocking reads + thread-sleep! for GC safety." + (with-catch + (lambda (e) (void)) + (lambda () + (let ([authed (if token + (begin + (repl-fd-send! client-fd "token: ") + (let-values ([(tok-line rest) + (repl-read-line + client-fd + "")]) + (and tok-line + (string=? + (string-trim-both tok-line) + token)))) + #t)]) + (when authed + (repl-fd-send! + client-fd + "jerboa debug REPL — type ,help for commands\n") + (let loop ([buf ""]) + (when *repl-running* + (repl-fd-send! client-fd "jerboa-dbg> ") + (let-values ([(line rest) + (repl-read-line client-fd buf)]) + (when (and line *repl-running*) + (when (process-repl-line line client-fd) + (loop rest)))))))))) + (with-catch + (lambda _ (void)) + (lambda () (repl-socket-close client-fd)))) + (def (repl-read-line client-fd buf) + "Read one line from client-fd using non-blocking reads + thread-sleep!.\n Returns (values line remaining-buffer) or (values #f \"\") on EOF/disconnect.\n Carries over leftover data in buf from previous reads." + (let loop ([buf buf]) + (if (not *repl-running*) + (values #f "") + (let ([nl (repl-string-index buf #\newline)]) + (if nl + (let ([line (substring buf 0 nl)] + [rest (substring + buf + (+ nl 1) + (string-length buf))]) + (values + (if (and (> (string-length line) 0) + (char=? + (string-ref + line + (- (string-length line) 1)) + #\return)) + (substring line 0 (- (string-length line) 1)) + line) + rest)) + (let ([data (repl-socket-read client-fd)]) + (cond + [(string? data) (loop (string-append buf data))] + [(eq? data 'eof) (values #f "")] + [else (thread-sleep! 0.05) (loop buf)]))))))) + (def (repl-accept-loop! token) + "Main accept loop. Runs in the dedicated REPL thread.\n Accepts one client at a time (single-connection REPL)." + (let loop () + (when *repl-running* + (let ([cfd (with-catch + (lambda _ #f) + (lambda () + (repl-socket-accept *repl-listen-fd*)))]) + (if cfd + (begin (handle-client! cfd token) (loop)) + (begin (thread-sleep! 0.1) (loop))))))) (def (start-debug-repl! port-num . args) - "Start the TCP debug REPL on 127.0.0.1:port-num.\n Optional second argument: token string for authentication.\n Use port 0 for OS-assigned ephemeral port.\n Returns the actual port number and writes ~/.jerboa-repl-port.\n\n THREAD-FREE: Registers a periodic tick with the master timer instead\n of creating a background thread. All socket I/O is non-blocking." + "Start the TCP debug REPL on 127.0.0.1:port-num.\n Optional second argument: token string for authentication.\n Use port 0 for OS-assigned ephemeral port.\n Returns the actual port number and writes ~/.jerboa-repl-port.\n\n Runs in a dedicated background thread — independent of Qt event loop." (stop-debug-repl!) (let ([token (if (null? args) #f (car args))]) (let-values ([(fd actual-port) (repl-socket-listen "127.0.0.1" port-num)]) (set! *repl-listen-fd* fd) (set! *repl-actual-port* actual-port) - (set! *repl-token* token) - (set! *repl-authed* (not token)) - (set! *repl-client-fd* #f) - (set! *repl-line-buf* "") - (set! *repl-prompted* #f) + (set! *repl-running* #t) (write-repl-port-file! actual-port) - (schedule-periodic! 'debug-repl 100 debug-repl-tick!) + (set! *repl-thread* + (let ([t (make-thread + (lambda () + (with-catch + (lambda (e) (void)) + (lambda () (repl-accept-loop! token)))) + 'debug-repl)]) + (thread-start! t) + t)) actual-port))) (def (stop-debug-repl!) "Stop the debug REPL server and clean up." - (repl-disconnect!) + (set! *repl-running* #f) (when *repl-listen-fd* (with-catch (lambda _ (void)) (lambda () (repl-socket-close *repl-listen-fd*))) (set! *repl-listen-fd* #f) (set! *repl-actual-port* #f)) + (when *repl-thread* + (with-catch + (lambda _ (void)) + (lambda () (thread-sleep! 0.2))) + (set! *repl-thread* #f)) (delete-repl-port-file!)) (def (debug-repl-port) "Return the actual port number the debug REPL is listening on, or #f if stopped." --- a/src/jerboa-emacs/debug-repl.ss +++ b/src/jerboa-emacs/debug-repl.ss @@ -2,10 +2,14 @@ ;;; TCP REPL server for debugging a running jerboa-emacs instance. ;;; Connect with: nc 127.0.0.1 <port> ;;; -;;; THREAD-FREE DESIGN: No Chez threads are created. All socket I/O uses -;;; non-blocking FFI calls polled from the master timer (schedule-periodic!). -;;; This prevents GC deadlocks caused by Chez threads blocking in foreign -;;; calls (accept/read) that can't respond to stop-the-world GC signals. +;;; DEDICATED THREAD DESIGN: The REPL runs in its own background thread, +;;; independent of the Qt event loop and master timer. This means the REPL +;;; stays responsive even when the UI is hung or the master timer is blocked. +;;; +;;; GC safety: Uses non-blocking sockets + thread-sleep! between polls. +;;; thread-sleep! auto-deactivates the Chez thread (decrements active_threads), +;;; allowing stop-the-world GC to proceed without waiting for this thread. +;;; No foreign call ever blocks — accept/read return immediately (EAGAIN). ;;; ;;; API: ;;; (start-debug-repl! port) → actual-port @@ -19,20 +23,16 @@ (import :std/sugar :std/srfi/13 - :jerboa/repl-socket - :jerboa-emacs/async) + :jerboa/repl-socket) ;;;============================================================================ ;;; State ;;;============================================================================ (def *repl-listen-fd* #f) ;; listen socket fd (non-blocking) -(def *repl-client-fd* #f) ;; connected client fd (non-blocking), or #f -(def *repl-line-buf* "") ;; partial line accumulator (def *repl-actual-port* #f) ;; port number we're listening on -(def *repl-token* #f) ;; auth token (or #f for no auth) -(def *repl-authed* #f) ;; #t after successful auth (or if no token) -(def *repl-prompted* #f) ;; #t if we've sent the prompt for current line +(def *repl-running* #f) ;; #t while REPL thread should keep running +(def *repl-thread* #f) ;; the background thread handle (def *repl-port-file* (string-append (getenv "HOME") "/.jerboa-repl-port")) @@ -54,27 +54,6 @@ (lambda () (delete-file *repl-port-file*))))) ;;;============================================================================ -;;; Low-level I/O helpers -;;;============================================================================ - -(def (repl-send! str) - "Send a string to the connected client. No-op if no client." - (when *repl-client-fd* - (unless (repl-socket-write *repl-client-fd* str) - ;; Write failed — client disconnected - (repl-disconnect!)))) - -(def (repl-disconnect!) - "Close the client connection and reset state for next accept." - (when *repl-client-fd* - (with-catch (lambda _ (void)) - (lambda () (repl-socket-close *repl-client-fd*)))) - (set! *repl-client-fd* #f) - (set! *repl-line-buf* "") - (set! *repl-authed* #f) - (set! *repl-prompted* #f)) - -;;;============================================================================ ;;; Help text ;;;============================================================================ @@ -88,51 +67,47 @@ ") ;;;============================================================================ -;;; Command processing +;;; Command processing (runs in REPL thread) ;;;============================================================================ -(def (process-repl-line! line) - "Process one REPL command line. Returns #t to continue, #f to disconnect." +(def (process-repl-line line client-fd) + "Process one REPL command line. Returns #t to continue, #f to disconnect." (let ((cmd (string-trim-both line))) (cond ((string=? cmd "") #t) ((string=? cmd ",quit") - (repl-send! "Connection closed.\n") + (repl-fd-send! client-fd "Connection closed.\n") #f) ((string=? cmd ",help") - (repl-send! help-text) + (repl-fd-send! client-fd help-text) #t) ((string=? cmd ",threads") (with-catch - (lambda (e) (repl-send! " (error listing threads)\n")) + (lambda (e) (repl-fd-send! client-fd " (error listing threads)\n")) (lambda () - (repl-send! " master-timer (active)\n"))) + (repl-fd-send! client-fd " REPL thread (active)\n"))) #t) ((string=? cmd ",state") - (repl-send! + (repl-fd-send! client-fd (string-append " listen-fd: " (number->string (or *repl-listen-fd* -1)) - "\n client-fd: " (number->string (or *repl-client-fd* -1)) + "\n client-fd: " (number->string client-fd) "\n bytes-allocated: " (number->string (bytes-allocated)) "\n")) #t) ((string=? cmd ",gc") - ;; Safe to call (collect) now — no REPL thread is blocked in a - ;; foreign call (we run from the master timer, not a separate thread). - ;; NOTE: Other threads (IPC accept) may still be blocked. - ;; Use (collect 0) for a minor collection that's less likely to - ;; need all threads at rendezvous. + ;; (collect) requires all threads at rendezvous — can't safely call + ;; from a background thread. Just report stats instead. (with-catch (lambda (e) - (repl-send! - (string-append " GC skipped (would deadlock). bytes-allocated: " - (number->string (bytes-allocated)) "\n"))) + (repl-fd-send! client-fd " (error reading GC stats)\n")) (lambda () - (collect 0) - (repl-send! - (string-append " GC done (gen 0). bytes-allocated: " - (number->string (bytes-allocated)) "\n")))) + (repl-fd-send! client-fd + (string-append + " bytes-allocated: " (number->string (bytes-allocated)) + "\n collections: " (number->string (collections)) + "\n Note: (collect) not safe from REPL thread; GC runs automatically.\n")))) #t) (else ;; Evaluate as Chez Scheme expression @@ -143,96 +118,22 @@ (with-output-to-string (lambda () (display-condition e (current-output-port)))))))) - (repl-send! (string-append "ERROR: " msg "\n")))) + (repl-fd-send! client-fd (string-append "ERROR: " msg "\n")))) (lambda () (let* ((result (eval (read (open-input-string cmd)) (interaction-environment))) (out (open-output-string))) (write result out) - (repl-send! (string-append (get-output-string out) "\n"))))) + (repl-fd-send! client-fd (string-append (get-output-string out) "\n"))))) #t)))) ;;;============================================================================ -;;; Tick — called from master timer every 100ms +;;; I/O helpers ;;;============================================================================ -(def (debug-repl-tick!) - "Non-blocking REPL poll. Called from the master timer. - Tries to accept a connection or read data from an existing client." - (when *repl-listen-fd* - (with-catch - (lambda (e) (void)) ;; swallow any unexpected errors in the tick - (lambda () - (cond - ;; No client — try to accept one - ((not *repl-client-fd*) - (let ((cfd (repl-socket-accept *repl-listen-fd*))) - (when cfd - (set! *repl-client-fd* cfd) - (set! *repl-line-buf* "") - (set! *repl-prompted* #f) - ;; Check if token auth is needed - (if *repl-token* - (begin - (set! *repl-authed* #f) - (repl-send! "token: ")) - (begin - (set! *repl-authed* #t) - (repl-send! "jerboa debug REPL — type ,help for commands\n")))))) - - ;; Client connected — try to read data - (*repl-client-fd* - ;; Send prompt if needed - (when (and *repl-authed* (not *repl-prompted*)) - (repl-send! "jerboa-dbg> \n") - (set! *repl-prompted* #t)) - - ;; Non-blocking read - (let ((data (repl-socket-read *repl-client-fd*))) - (cond - ((string? data) - ;; Got data — append to line buffer and process complete lines - (set! *repl-line-buf* (string-append *repl-line-buf* data)) - (repl-process-lines!)) - ((eq? data 'eof) - ;; Client disconnected - (repl-disconnect!)) - ;; #f = EAGAIN, nothing to read — try again next tick - )))))))) - -(def (repl-process-lines!) - "Extract and process complete lines from *repl-line-buf*." - (let loop () - (let ((nl (repl-string-index *repl-line-buf* #\newline))) - (when nl - (let ((line (substring *repl-line-buf* 0 nl)) - (rest (substring *repl-line-buf* (+ nl 1) - (string-length *repl-line-buf*)))) - (set! *repl-line-buf* rest) - ;; Strip trailing CR (from telnet/nc on some systems) - (let ((line (if (and (> (string-length line) 0) - (char=? (string-ref line (- (string-length line) 1)) #\return)) - (substring line 0 (- (string-length line) 1)) - line))) - (if *repl-authed* - ;; Normal REPL command - (let ((continue? (process-repl-line! line))) - (if continue? - (begin - (set! *repl-prompted* #f) ;; need new prompt - (loop)) - (repl-disconnect!))) - ;; Auth check - (let ((tok (string-trim-both line))) - (if (string=? tok *repl-token*) - (begin - (set! *repl-authed* #t) - (repl-send! "jerboa debug REPL — type ,help for commands\n") - (set! *repl-prompted* #f) - (loop)) - (begin - (repl-send! "Access denied.\n") - (repl-disconnect!))))))))))) +(def (repl-fd-send! fd str) + "Send string to fd. Returns #t on success, #f on error." + (repl-socket-write fd str)) (def (repl-string-index str ch) "Return the index of the first occurrence of ch in str, or #f." @@ -244,6 +145,91 @@ (else (loop (+ i 1))))))) ;;;============================================================================ +;;; Client handler (runs in REPL thread) +;;;============================================================================ + +(def (handle-client! client-fd token) + "Handle one client connection. Blocks until client disconnects or ,quit. + Uses non-blocking reads + thread-sleep! for GC safety." + (with-catch + (lambda (e) (void)) ;; silently close on any error + (lambda () + ;; Auth phase + (let ((authed (if token + (begin + (repl-fd-send! client-fd "token: ") + (let-values (((tok-line rest) (repl-read-line client-fd ""))) + (and tok-line + (string=? (string-trim-both tok-line) token)))) + #t))) + (when authed + (repl-fd-send! client-fd "jerboa debug REPL — type ,help for commands\n") + ;; Main REPL loop — carry leftover buffer between reads + (let loop ((buf "")) + (when *repl-running* + (repl-fd-send! client-fd "jerboa-dbg> ") + (let-values (((line rest) (repl-read-line client-fd buf))) + (when (and line *repl-running*) + (when (process-repl-line line client-fd) + (loop rest)))))))))) + ;; Always close the client fd + (with-catch (lambda _ (void)) + (lambda () (repl-socket-close client-fd)))) + +(def (repl-read-line client-fd buf) + "Read one line from client-fd using non-blocking reads + thread-sleep!. + Returns (values line remaining-buffer) or (values #f \"\") on EOF/disconnect. + Carries over leftover data in buf from previous reads." + (let loop ((buf buf)) + (if (not *repl-running*) + (values #f "") + (let ((nl (repl-string-index buf #\newline))) + (if nl + ;; Got a complete line — return it and the remaining buffer + (let ((line (substring buf 0 nl)) + (rest (substring buf (+ nl 1) (string-length buf)))) + ;; Strip trailing CR + (values + (if (and (> (string-length line) 0) + (char=? (string-ref line (- (string-length line) 1)) #\return)) + (substring line 0 (- (string-length line) 1)) + line) + rest)) + ;; Need more data — non-blocking read + (let ((data (repl-socket-read client-fd))) + (cond + ((string? data) + (loop (string-append buf data))) + ((eq? data 'eof) + (values #f "")) + (else + ;; EAGAIN — sleep briefly then retry + ;; thread-sleep! auto-deactivates for GC + (thread-sleep! 0.05) + (loop buf))))))))) + +;;;============================================================================ +;;; Accept loop (runs in REPL thread) +;;;============================================================================ + +(def (repl-accept-loop! token) + "Main accept loop. Runs in the dedicated REPL thread. + Accepts one client at a time (single-connection REPL)." + (let loop () + (when *repl-running* + (let ((cfd (with-catch (lambda _ #f) + (lambda () (repl-socket-accept *repl-listen-fd*))))) + (if cfd + (begin + (handle-client! cfd token) + (loop)) + (begin + ;; No connection pending — sleep briefly then retry + ;; thread-sleep! auto-deactivates for GC + (thread-sleep! 0.1) + (loop))))))) + +;;;============================================================================ ;;; Public API ;;;============================================================================ @@ -253,34 +239,42 @@ Use port 0 for OS-assigned ephemeral port. Returns the actual port number and writes ~/.jerboa-repl-port. - THREAD-FREE: Registers a periodic tick with the master timer instead - of creating a background thread. All socket I/O is non-blocking." + Runs in a dedicated background thread — independent of Qt event loop." ;; Stop any existing server (stop-debug-repl!) (let ((token (if (null? args) #f (car args)))) (let-values (((fd actual-port) (repl-socket-listen "127.0.0.1" port-num))) (set! *repl-listen-fd* fd) (set! *repl-actual-port* actual-port) - (set! *repl-token* token) - (set! *repl-authed* (not token)) - (set! *repl-client-fd* #f) - (set! *repl-line-buf* "") - (set! *repl-prompted* #f) + (set! *repl-running* #t) (write-repl-port-file! actual-port) - ;; Register periodic tick — polls every 100ms from master timer - (schedule-periodic! 'debug-repl 100 debug-repl-tick!) + ;; Spawn dedicated REPL thread + (set! *repl-thread* + (let ((t (make-thread + (lambda () + (with-catch + (lambda (e) (void)) + (lambda () (repl-accept-loop! token)))) + 'debug-repl))) + (thread-start! t) + t)) actual-port))) (def (stop-debug-repl!) "Stop the debug REPL server and clean up." - ;; Close client if connected - (repl-disconnect!) - ;; Close listen socket + ;; Signal thread to stop + (set! *repl-running* #f) + ;; Close listen socket (will cause accept to fail, unblocking the thread) (when *repl-listen-fd* (with-catch (lambda _ (void)) (lambda () (repl-socket-close *repl-listen-fd*))) (set! *repl-listen-fd* #f) (set! *repl-actual-port* #f)) + ;; Wait briefly for thread to notice + (when *repl-thread* + (with-catch (lambda _ (void)) + (lambda () (thread-sleep! 0.2))) + (set! *repl-thread* #f)) (delete-repl-port-file!)) (def (debug-repl-port)