diff --git a/TODO.org b/TODO.org index a081e498..84f0b97c 100644 --- a/TODO.org +++ b/TODO.org @@ -1649,16 +1649,22 @@ sanitizer flag to it. Named as the check worth adding next; a day rather than an hour. The x86 backend is not a gap here — that pair is refused by name, because there is no sanitizer pass over hand-written assembly. -** TODO A transient signal 11 on a globals daemon -Seen once, never reproduced, on a daemon whose fixture had just gained a host -global =Vec= and a run-time-new one. A reproduction under load would settle it. +** DONE A transient signal 11 on a globals daemon +CLOSED: [2026-09-25] +Not a segfault. The report was OCaml's signal number, and in OCaml's numbering +-11 is SIGTERM (SIGSEGV is -10). Nothing in the daemon sends itself SIGTERM, so +it was killed from outside. The test binaries now print a signal by name +(=Test_support.signal_name=); a number from =WSIGNALED= is never printed raw. -** TODO test_dev daemons fail to bind under load -Daemons exiting with status 1 or 2 before binding their socket, across most of the -file at once, while the load average is high. No stale socket or leftover daemon -afterwards; green on a quiet machine. Distinct from the registry race and the -stale-park re-run flake, both of which are fixed. A daemon that dies before it -binds died on the compiler's side, before any program it built has run a line. +** DONE test_dev daemons fail to bind under load +CLOSED: [2026-09-25] +The failure was a full /tmp, not the load. /tmp is a tmpfs, and every session +left its =flan-dev-= build directory behind (a test_dev run leaves about +300MB); a few concurrent runs filled it and each daemon after that died on its +link with ENOSPC. The fix is the session removing its own directory on a clean +=close=, which the build-plumbing lane owns. The half-write test's abort also +goes through =aborted= now, which took the same run down with an uncaught +=Wire.Closed=. ** DONE A test binary that hangs is killed by its own alarm A reader branch that forgets to advance loops for ever and the suite waits as long @@ -1799,13 +1805,21 @@ function of that name. Not reproduced: the file type checks, =flan reload= of the same form builds, and a minimal =defclass= + =get= + =return= program compiles. So it is the session path against an installed program, and what is missing is what that daemon had installed at the time. +Also not reproduced against a live daemon (2026-09-25): an =eval= of the +=defclass=, of a =defn= doing the =get= and =return=, and of both in one form, +and an =eval-expr= of the =get=, all succeed. In a session every installed +function of no arguments returning =()= has the type =(CFn [] ())=, not only +=pause= — a bare =pause= or =tick= asked of the session says so — so the +keyword may not be what resolved. The next report wants the exact form sent. -** TODO A digit does not take the restart RET takes -Pressing =0= left the program stopped; RET on the same line resumed it. Both -end in =flan-cnr-take=, but the digit path (=flan-cnr-take-number=, -=flan-cnr.el:559=) scans from =point-min= for the line whose =flan-cnr-index= -matches and calls =take= inside a =save-excursion=. Not reproduced yet — needs -a non-raylib program stopped under a test daemon. +** DONE A digit does not take the restart RET takes +CLOSED: [2026-09-25] +Evil's normal state binds =0= (beginning of line), =1=-=9= (a count) and RET +above the major mode's map, so the digit never reached =flan-cnr-take-number=. +The keys =flan-cnr-mode-map= itself binds are given to Evil's normal and +motion states in that mode; every other key, including what =special-mode-map= +binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly, +diagnostics, lower) have the same exposure and are not changed. ** TODO Eval in the frame, from the break loop An expression is evaluated at a frame boundary, so it sees globals and not the diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el index 72b3dbcd..dea14019 100644 --- a/emacs/flan-cnr.el +++ b/emacs/flan-cnr.el @@ -702,6 +702,24 @@ anyone who would rather TAB always moved." map) "Keys in `flan-cnr-mode'.") +;; Evil's normal state binds 0, the other digits and RET above any major +;; mode's map, so under Evil a digit moved point or started a count and took +;; nothing. The keys this file binds are given to Evil's normal and motion +;; states in this mode. Only those keys: `flan-cnr-mode-map' inherits +;; `special-mode-map', and making it an overriding map would carry h, SPC, < +;; and - over from there as well. Every key not listed stays Evil's. +(with-eval-after-load 'evil + (when (fboundp 'evil-define-key*) + ;; Collected first, and without the parent's bindings, because + ;; `evil-define-key*' writes into the map being walked. + (let ((own nil)) + (map-keymap-internal (lambda (key def) + (when (commandp def) (push (cons key def) own))) + flan-cnr-mode-map) + (dolist (b own) + (evil-define-key* '(normal motion) flan-cnr-mode-map + (vector (car b)) (cdr b)))))) + (define-derived-mode flan-cnr-mode special-mode "flan-break" "What a stopped Flan program is offering." (setq buffer-read-only t)) diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index a0247452..a13ead5d 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -1704,6 +1704,55 @@ stopped program, which is the case where it should fire." (file-name-directory load-file-name)) nil t) +;; The break buffer's keys under Evil, pressed through the command loop rather +;; than called. Evil's normal state binds 0 (beginning of line), 1-9 (a count) +;; and RET (next line) above any major mode's map, so without the buffer's map +;; taking precedence a digit moved point and took nothing. Run only where +;; Evil is installed: -Q loads no packages, so it is looked for in the two +;; places a package install puts it, and turned off again afterwards so nothing +;; above or below this runs under it. +(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/evil-[0-9]*") + (file-expand-wildcards "~/.emacs.d/elpa/evil-[0-9]*") + (file-expand-wildcards "~/.config/emacs/elpa/goto-chg-*") + (file-expand-wildcards "~/.emacs.d/elpa/goto-chg-*"))) + (load-path (append dirs load-path))) + (if (not (require 'evil nil t)) + (message " skip the break buffer under Evil (Evil is not installed)") + (evil-mode 1) + (unwind-protect + (let* ((sent nil) + (flan-cnr-request-function + (lambda (form) (setq sent form) (list :status "ok"))) + (buf (test-flan--cnr + (list :condition "Missing" :restarts '("retry" "skip"))))) + (switch-to-buffer buf) + (evil-initialize-state) + (goto-char (point-min)) + (execute-kbd-macro (kbd "0")) + (test-flan--check "under Evil, 0 takes restart 0" + (equal sent '(:op "restart-at" :index 0 :name "retry"))) + (setq sent nil) + (switch-to-buffer buf) + (execute-kbd-macro (kbd "1")) + (test-flan--check "under Evil, 1 takes restart 1" + (equal sent '(:op "restart-at" :index 1 :name "skip"))) + (setq sent nil) + (switch-to-buffer buf) + (goto-char (point-min)) + (search-forward " 1: ") + (execute-kbd-macro (kbd "RET")) + (test-flan--check "under Evil, RET takes the restart on its line" + (equal sent '(:op "restart-at" :index 1 :name "skip"))) + ;; And the keys the buffer does not bind are still Evil's, not the + ;; ones `special-mode-map' would bring with it. + (switch-to-buffer buf) + (dolist (k '(("h" . evil-backward-char) ("SPC" . evil-forward-char) + ("<" . evil-shift-left) + ("-" . evil-previous-line-first-non-blank))) + (test-flan--check (format "under Evil, %s is still Evil's" (car k)) + (eq (key-binding (kbd (car k))) (cdr k))))) + (evil-mode -1)))) + (message "\n%d checks, %d failures" test-flan--ran test-flan--failures) (kill-emacs (if (> test-flan--failures 0) 1 0)) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 33a39f9d..e8ea4157 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -186,8 +186,8 @@ module Pool = struct name (match status with | Unix.WEXITED n -> Printf.sprintf "with code %d" n - | Unix.WSIGNALED n -> Printf.sprintf "on signal %d" n - | Unix.WSTOPPED n -> Printf.sprintf "stopped on signal %d" n)) + | Unix.WSIGNALED n -> "on " ^ Test_support.signal_name n + | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n)) | None, _ -> Some (Printf.sprintf @@ -195,8 +195,8 @@ module Pool = struct name (match status with | Unix.WEXITED n -> Printf.sprintf "exit %d" n - | Unix.WSIGNALED n -> Printf.sprintf "signal %d" n - | Unix.WSTOPPED n -> Printf.sprintf "stopped, signal %d" n)) + | Unix.WSIGNALED n -> Test_support.signal_name n + | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n)) in ignore (Queue.pop inflight); match msg with diff --git a/test/test_agent.ml b/test/test_agent.ml index 62a24bef..4d04b60a 100644 --- a/test/test_agent.ml +++ b/test/test_agent.ml @@ -149,8 +149,8 @@ let () = fail "agent reload\n got: %S (%s)\n wanted: %S" text (match status with | Unix.WEXITED c -> Printf.sprintf "exit %d" c - | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c - | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) + | Unix.WSIGNALED c -> Test_support.signal_name c + | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c) "1\n1000\n1007\n" end; @@ -309,8 +309,8 @@ let () = %S (exit 1)" text (match !bstat with | Unix.WEXITED c -> Printf.sprintf "exit %d" c - | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c - | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) + | Unix.WSIGNALED c -> Test_support.signal_name c + | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c) "cannot listen\n" end; @@ -766,8 +766,8 @@ let () = fail "abort left status %s, wanted exit 134" (match !lstatus with | Unix.WEXITED c -> Printf.sprintf "exit %d" c - | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c - | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) + | Unix.WSIGNALED c -> Test_support.signal_name c + | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c) (* The other way out, and the one that skips atexit on purpose: [abort] leaves by [_exit] so that it cannot hang on the loader lock, and the socket is therefore unlinked by hand there. A file left here would diff --git a/test/test_dev.ml b/test/test_dev.ml index 5be18307..fe2e9d6d 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -196,6 +196,23 @@ let () = if not (contains_sub other "flan.abi.x86") then fail "a parked program's other refusals were rewritten too: %S" other +(* A daemon's death is reported by the signal's name. [WSIGNALED] carries + OCaml's own numbering, in which SIGTERM is -11, and a SIGTERM printed as + "signal -11" was once recorded as a segfault. A real process, really + terminated, so the number is whatever [waitpid] hands back. *) +let () = + let p = + Unix.create_process "sleep" [| "sleep"; "30" |] Unix.stdin Unix.stdout + Unix.stderr + in + Unix.kill p Sys.sigterm; + match Unix.waitpid [] p with + | _, Unix.WSIGNALED n when Test_support.signal_name n = "SIGTERM" -> () + | _, Unix.WSIGNALED n -> + fail "a process ended by SIGTERM is reported as %s" + (Test_support.signal_name n) + | _, _ -> fail "a process sent SIGTERM did not end on a signal" + let () = match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with | 0 -> @@ -5104,8 +5121,8 @@ let () = (match st with | Unix.WEXITED n -> Printf.sprintf "exit %d" n | Unix.WSIGNALED n when n = Sys.sigpipe -> "killed by SIGPIPE" - | Unix.WSIGNALED n -> Printf.sprintf "signal %d" n - | Unix.WSTOPPED n -> Printf.sprintf "stopped on %d" n); + | Unix.WSIGNALED n -> Test_support.signal_name n + | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n); None | exception Unix.Unix_error _ -> Some (connect rsock) in @@ -5605,9 +5622,12 @@ let () = abort is refused by a program that is *running*, which is what a failure above would leave behind — and this fixture then polls for twenty seconds and parks, so the wait below would be a hang rather - than a report. The signal is for that case only. *) - if status (request c "(:op \"abort\")") <> "ok" then - (try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ()); + than a report. The signal is for that case only. Through [aborted], + because an abort that worked can arrive as the socket closing. *) + (match aborted c with + | Some r when status r <> "ok" -> + (try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ()) + | _ -> ()); (try Unix.close c with Unix.Unix_error _ -> ()); (try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ()) end; diff --git a/test/test_emacs.ml b/test/test_emacs.ml index ecee324d..c2b2ebe1 100644 --- a/test/test_emacs.ml +++ b/test/test_emacs.ml @@ -112,9 +112,10 @@ let () = match !status with | Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d" n | Some (Unix.WSIGNALED n) -> - Printf.sprintf "was killed by signal %d — nothing ran its cleanup, so \ - the socket file is stale rather than gone" n - | Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on signal %d" n + Printf.sprintf "was killed by %s — nothing ran its cleanup, so \ + the socket file is stale rather than gone" + (Test_support.signal_name n) + | Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on %s" (Test_support.signal_name n) | None -> "was still running when the client finished, and was terminated" in (* Read before the removal below, because on a failure this is the evidence diff --git a/test/test_repl.ml b/test/test_repl.ml index 9579130f..752b0679 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -326,9 +326,11 @@ let () = "the daemon had already been killed by SIGPIPE — a reply written \ into a socket whose reader had gone" | Some (Unix.WSIGNALED n) -> - Printf.printf "the daemon had already been killed by signal %d\n" n + Printf.printf "the daemon had already been killed by %s\n" + (Test_support.signal_name n) | Some (Unix.WSTOPPED n) -> - Printf.printf "the daemon was stopped on signal %d\n" n + Printf.printf "the daemon was stopped on %s\n" + (Test_support.signal_name n) | None -> print_endline "the daemon was still running at the end"); Printf.printf "\n-- the program's own output, last 4k --\n%s\n" prog_out; exit 1 diff --git a/test/test_support.ml b/test/test_support.ml index 41228ca6..a64b73a7 100644 --- a/test/test_support.ml +++ b/test/test_support.ml @@ -152,6 +152,24 @@ let rec connect ?(ms = 5000) path = thing after a failed wait is always the report of it. *) let listen_why = ref "" +(* A signal as [WSIGNALED] carries it, by name. OCaml numbers the signals it + knows negatively and in its own order — [Sys.sigterm] is -11 and + [Sys.sigsegv] is -10 — so printing the number reads as the wrong signal to + anyone who knows the POSIX table: a daemon terminated by SIGTERM was once + recorded here as "a transient signal 11", a segfault that never happened. *) +let signal_name n = + let known = + [ Sys.sigabrt, "SIGABRT"; Sys.sigalrm, "SIGALRM"; Sys.sigfpe, "SIGFPE"; + Sys.sighup, "SIGHUP"; Sys.sigill, "SIGILL"; Sys.sigint, "SIGINT"; + Sys.sigkill, "SIGKILL"; Sys.sigpipe, "SIGPIPE"; Sys.sigquit, "SIGQUIT"; + Sys.sigsegv, "SIGSEGV"; Sys.sigterm, "SIGTERM"; Sys.sigusr1, "SIGUSR1"; + Sys.sigusr2, "SIGUSR2"; Sys.sigchld, "SIGCHLD"; Sys.sigbus, "SIGBUS"; + Sys.sigtrap, "SIGTRAP"; Sys.sigxcpu, "SIGXCPU" ] + in + match List.assoc_opt n known with + | Some s -> s + | None -> Printf.sprintf "signal %d" n + let listening ?(ms = 30000) ~pid path = let died = ref None in ignore @@ -171,10 +189,10 @@ let listening ?(ms = 30000) ~pid path = | Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d before binding %s" n path | Some (Unix.WSIGNALED n) -> - Printf.sprintf "was killed by signal %d before binding %s" n path + Printf.sprintf "was killed by %s before binding %s" (signal_name n) path (* Unreachable without WUNTRACED, and here only for exhaustiveness. *) | Some (Unix.WSTOPPED n) -> - Printf.sprintf "stopped on signal %d without binding %s" n path + Printf.sprintf "stopped on %s without binding %s" (signal_name n) path | None -> Printf.sprintf "was still running after %ds without binding %s, so it was the \