A client that leaves before its reply arrives, which used to kill the daemon

Eight hit-and-run connections against the robustness daemon, then a check
that the session is still there and still knows what it installed. Fails
with 'killed by SIGPIPE' against the parent of this commit.
This commit is contained in:
Joseph Ferano 2026-09-14 08:02:11 +07:00
parent fbafee4957
commit 4074eb7b7f

View File

@ -3044,11 +3044,66 @@ let () =
| None -> fail "(probe-one) produced no value")
| Some r -> fail "(probe-one): %s %s" (status r) (message r)
| None -> ());
(try
ignore (Wire.send c "(:op \"close\")");
ignore (Wire.recv c)
with _ -> ());
(try Unix.close c with Unix.Unix_error _ -> ())
(* ── An editor that leaves before its reply does ──────────────── *)
(* The failure this closes was a flake in test_emacs, and it read like
nothing it was: two break-loop checks failing, and then
"cannot reconnect: Connection refused" on a socket path that plainly
existed. A write into a socket whose reader has gone is SIGPIPE, and
SIGPIPE's default action is to kill the process which in the merged
build is the program, the compiler and the listener at once, and
leaves the socket file behind for the next client to be refused on.
It is not a contrived shape: reconnecting tears the old connection
down, and a reply already on its way out lands in the gap.
[c] is closed first because the daemon serves one connection at a
time: a second one would sit in the backlog until this one ended, and
then there would be no reply in flight to lose. *)
(try Unix.close c with Unix.Unix_error _ -> ());
let hit_and_run () =
let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
(try
Unix.connect s (Unix.ADDR_UNIX rsock);
Wire.send s "(:op \"describe\")"
with Unix.Unix_error _ -> ());
(try Unix.close s with Unix.Unix_error _ -> ())
in
(* Several, because the first write into a freshly closed socket can
still land in a buffer nobody will read; the second is the one that
is refused. *)
for _ = 1 to 8 do hit_and_run (); ignore (Unix.select [] [] [] 0.02) done;
let c =
match Unix.waitpid [ Unix.WNOHANG ] rpid with
| 0, _ -> Some (connect rsock)
| _, st ->
(* Named rather than numbered, because [WSIGNALED] carries OCaml's
own signal numbering and "signal -8" would send the next reader
to the wrong page. SIGPIPE is the answer this check expects to
see when it fails. *)
fail "an editor that left before its reply did ended the daemon (%s)"
(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);
None
| exception Unix.Unix_error _ -> Some (connect rsock)
in
(match c with
| None -> ()
| Some c ->
(match Wire.parse (Wire.send c "(:op \"describe\")"; Wire.recv c) with
| r ->
if not (contains_sub (Form.to_string (Option.get (Wire.field r "fns")))
"probe-one")
then fail "the session lost track of the program after a client left"
| exception e ->
fail "the session did not answer after a client left: %s"
(Printexc.to_string e));
(try
ignore (Wire.send c "(:op \"close\")");
ignore (Wire.recv c)
with _ -> ());
(try Unix.close c with Unix.Unix_error _ -> ()))
end;
(try Unix.kill rpid Sys.sigkill with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] rpid) with Unix.Unix_error _ -> ());