A test binary names the signal that killed a daemon, and the break buffer keeps its keys under Evil

This commit is contained in:
Joseph Ferano 2026-09-25 07:40:58 +07:00
commit f986c0728a
9 changed files with 159 additions and 37 deletions

View File

@ -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 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. there is no sanitizer pass over hand-written assembly.
** TODO A transient signal 11 on a globals daemon ** DONE A transient signal 11 on a globals daemon
Seen once, never reproduced, on a daemon whose fixture had just gained a host CLOSED: [2026-09-25]
global =Vec= and a run-time-new one. A reproduction under load would settle it. 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 ** DONE test_dev daemons fail to bind under load
Daemons exiting with status 1 or 2 before binding their socket, across most of the CLOSED: [2026-09-25]
file at once, while the load average is high. No stale socket or leftover daemon The failure was a full /tmp, not the load. /tmp is a tmpfs, and every session
afterwards; green on a quiet machine. Distinct from the registry race and the left its =flan-dev-<pid>= build directory behind (a test_dev run leaves about
stale-park re-run flake, both of which are fixed. A daemon that dies before it 300MB); a few concurrent runs filled it and each daemon after that died on its
binds died on the compiler's side, before any program it built has run a line. 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 ** 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 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 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 compiles. So it is the session path against an installed program, and what is
missing is what that daemon had installed at the time. 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 ** DONE A digit does not take the restart RET takes
Pressing =0= left the program stopped; RET on the same line resumed it. Both CLOSED: [2026-09-25]
end in =flan-cnr-take=, but the digit path (=flan-cnr-take-number=, Evil's normal state binds =0= (beginning of line), =1=-=9= (a count) and RET
=flan-cnr.el:559=) scans from =point-min= for the line whose =flan-cnr-index= above the major mode's map, so the digit never reached =flan-cnr-take-number=.
matches and calls =take= inside a =save-excursion=. Not reproduced yet — needs The keys =flan-cnr-mode-map= itself binds are given to Evil's normal and
a non-raylib program stopped under a test daemon. 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 ** TODO Eval in the frame, from the break loop
An expression is evaluated at a frame boundary, so it sees globals and not the An expression is evaluated at a frame boundary, so it sees globals and not the

View File

@ -702,6 +702,24 @@ anyone who would rather TAB always moved."
map) map)
"Keys in `flan-cnr-mode'.") "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" (define-derived-mode flan-cnr-mode special-mode "flan-break"
"What a stopped Flan program is offering." "What a stopped Flan program is offering."
(setq buffer-read-only t)) (setq buffer-read-only t))

View File

@ -1704,6 +1704,55 @@ stopped program, which is the case where it should fire."
(file-name-directory load-file-name)) (file-name-directory load-file-name))
nil t) 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) (message "\n%d checks, %d failures" test-flan--ran test-flan--failures)
(kill-emacs (if (> test-flan--failures 0) 1 0)) (kill-emacs (if (> test-flan--failures 0) 1 0))

View File

@ -186,8 +186,8 @@ module Pool = struct
name name
(match status with (match status with
| Unix.WEXITED n -> Printf.sprintf "with code %d" n | Unix.WEXITED n -> Printf.sprintf "with code %d" n
| Unix.WSIGNALED n -> Printf.sprintf "on signal %d" n | Unix.WSIGNALED n -> "on " ^ Test_support.signal_name n
| Unix.WSTOPPED n -> Printf.sprintf "stopped on signal %d" n)) | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n))
| None, _ -> | None, _ ->
Some Some
(Printf.sprintf (Printf.sprintf
@ -195,8 +195,8 @@ module Pool = struct
name name
(match status with (match status with
| Unix.WEXITED n -> Printf.sprintf "exit %d" n | Unix.WEXITED n -> Printf.sprintf "exit %d" n
| Unix.WSIGNALED n -> Printf.sprintf "signal %d" n | Unix.WSIGNALED n -> Test_support.signal_name n
| Unix.WSTOPPED n -> Printf.sprintf "stopped, signal %d" n)) | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n))
in in
ignore (Queue.pop inflight); ignore (Queue.pop inflight);
match msg with match msg with

View File

@ -149,8 +149,8 @@ let () =
fail "agent reload\n got: %S (%s)\n wanted: %S" text fail "agent reload\n got: %S (%s)\n wanted: %S" text
(match status with (match status with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c | Unix.WEXITED c -> Printf.sprintf "exit %d" c
| Unix.WSIGNALED c -> Printf.sprintf "signal %d" c | Unix.WSIGNALED c -> Test_support.signal_name c
| Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
"1\n1000\n1007\n" "1\n1000\n1007\n"
end; end;
@ -309,8 +309,8 @@ let () =
%S (exit 1)" text %S (exit 1)" text
(match !bstat with (match !bstat with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c | Unix.WEXITED c -> Printf.sprintf "exit %d" c
| Unix.WSIGNALED c -> Printf.sprintf "signal %d" c | Unix.WSIGNALED c -> Test_support.signal_name c
| Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
"cannot listen\n" "cannot listen\n"
end; end;
@ -766,8 +766,8 @@ let () =
fail "abort left status %s, wanted exit 134" fail "abort left status %s, wanted exit 134"
(match !lstatus with (match !lstatus with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c | Unix.WEXITED c -> Printf.sprintf "exit %d" c
| Unix.WSIGNALED c -> Printf.sprintf "signal %d" c | Unix.WSIGNALED c -> Test_support.signal_name c
| Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c) | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
(* The other way out, and the one that skips atexit on purpose: [abort] (* 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 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 socket is therefore unlinked by hand there. A file left here would

View File

@ -196,6 +196,23 @@ let () =
if not (contains_sub other "flan.abi.x86") then if not (contains_sub other "flan.abi.x86") then
fail "a parked program's other refusals were rewritten too: %S" other 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 () = let () =
match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with
| 0 -> | 0 ->
@ -5104,8 +5121,8 @@ let () =
(match st with (match st with
| Unix.WEXITED n -> Printf.sprintf "exit %d" n | Unix.WEXITED n -> Printf.sprintf "exit %d" n
| Unix.WSIGNALED n when n = Sys.sigpipe -> "killed by SIGPIPE" | Unix.WSIGNALED n when n = Sys.sigpipe -> "killed by SIGPIPE"
| Unix.WSIGNALED n -> Printf.sprintf "signal %d" n | Unix.WSIGNALED n -> Test_support.signal_name n
| Unix.WSTOPPED n -> Printf.sprintf "stopped on %d" n); | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n);
None None
| exception Unix.Unix_error _ -> Some (connect rsock) | exception Unix.Unix_error _ -> Some (connect rsock)
in in
@ -5605,9 +5622,12 @@ let () =
abort is refused by a program that is *running*, which is what a abort is refused by a program that is *running*, which is what a
failure above would leave behind — and this fixture then polls for failure above would leave behind — and this fixture then polls for
twenty seconds and parks, so the wait below would be a hang rather twenty seconds and parks, so the wait below would be a hang rather
than a report. The signal is for that case only. *) than a report. The signal is for that case only. Through [aborted],
if status (request c "(:op \"abort\")") <> "ok" then because an abort that worked can arrive as the socket closing. *)
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ()); (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 Unix.close c with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ()) (try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
end; end;

View File

@ -112,9 +112,10 @@ let () =
match !status with match !status with
| Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d" n | Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d" n
| Some (Unix.WSIGNALED n) -> | Some (Unix.WSIGNALED n) ->
Printf.sprintf "was killed by signal %d — nothing ran its cleanup, so \ Printf.sprintf "was killed by %s — nothing ran its cleanup, so \
the socket file is stale rather than gone" n the socket file is stale rather than gone"
| Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on signal %d" n (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" | None -> "was still running when the client finished, and was terminated"
in in
(* Read before the removal below, because on a failure this is the evidence (* Read before the removal below, because on a failure this is the evidence

View File

@ -326,9 +326,11 @@ let () =
"the daemon had already been killed by SIGPIPE — a reply written \ "the daemon had already been killed by SIGPIPE — a reply written \
into a socket whose reader had gone" into a socket whose reader had gone"
| Some (Unix.WSIGNALED n) -> | 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) -> | 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"); | 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; Printf.printf "\n-- the program's own output, last 4k --\n%s\n" prog_out;
exit 1 exit 1

View File

@ -152,6 +152,24 @@ let rec connect ?(ms = 5000) path =
thing after a failed wait is always the report of it. *) thing after a failed wait is always the report of it. *)
let listen_why = ref "" 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 listening ?(ms = 30000) ~pid path =
let died = ref None in let died = ref None in
ignore ignore
@ -171,10 +189,10 @@ let listening ?(ms = 30000) ~pid path =
| Some (Unix.WEXITED n) -> | Some (Unix.WEXITED n) ->
Printf.sprintf "exited with status %d before binding %s" n path Printf.sprintf "exited with status %d before binding %s" n path
| Some (Unix.WSIGNALED n) -> | 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. *) (* Unreachable without WUNTRACED, and here only for exhaustiveness. *)
| Some (Unix.WSTOPPED n) -> | 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 -> | None ->
Printf.sprintf Printf.sprintf
"was still running after %ds without binding %s, so it was the \ "was still running after %ds without binding %s, so it was the \