A dev daemon started by a test dies with it, a daemon whose socket is gone ends cleanly, the Emacs daemon buffer asks before it is killed and closes the session, and a program's agent socket lives under TMPDIR and goes on a trap or fatal signal.
This commit is contained in:
parent
1243504ac6
commit
2752d57655
@ -900,6 +900,62 @@ It builds the program first, which for a cold project is most of this."
|
||||
(message "flan dev: the daemon exited (%s); see %s"
|
||||
(string-trim (or event "")) flan-daemon-buffer))))
|
||||
|
||||
(defun flan--close-daemon ()
|
||||
"Ask the daemon this Emacs started to end, and give it a moment to.
|
||||
For `kill-buffer-hook' on its buffer and for `kill-emacs-hook': either
|
||||
deletes the process outright, and a daemon killed that way never runs its
|
||||
own cleanup -- its socket, its temporary directory, and under
|
||||
--two-process the program it started. `close' is its own way out. Never
|
||||
signals, so the kill that called it always goes ahead."
|
||||
(let ((proc flan--daemon)
|
||||
(sock flan--daemon-socket))
|
||||
(when (and (process-live-p proc) sock)
|
||||
(if (and (process-live-p flan--connection)
|
||||
(equal flan--socket sock))
|
||||
;; Connected to it: `flan-quit' is exactly this, with its state
|
||||
;; cleared as well.
|
||||
(ignore-errors (flan-quit))
|
||||
;; Not connected, or connected to another session: a connection of
|
||||
;; its own, so the other session is left alone.
|
||||
(ignore-errors
|
||||
(let ((c (make-network-process
|
||||
:name "flan-close" :family 'local
|
||||
:service (flan--short-socket sock)
|
||||
:coding 'binary :noquery t)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(flan--send c '(:op "close"))
|
||||
(let ((deadline (+ (float-time) 2)))
|
||||
(while (and (process-live-p proc)
|
||||
(< (float-time) deadline))
|
||||
(accept-process-output proc 0.05))))
|
||||
(delete-process c))))))))
|
||||
|
||||
(add-hook 'kill-emacs-hook #'flan--close-daemon)
|
||||
|
||||
(defun flan--daemon-of-this-buffer-p ()
|
||||
"Whether the live daemon this Emacs started writes to the current buffer."
|
||||
(and (process-live-p flan--daemon)
|
||||
(eq (process-buffer flan--daemon) (current-buffer))))
|
||||
|
||||
(defun flan--query-kill-daemon ()
|
||||
"Ask before killing the buffer of a live daemon, as CIDER does for nREPL.
|
||||
For `kill-buffer-query-functions'. A yes clears the process's own query,
|
||||
so Emacs does not ask a second time, and `flan--kill-daemon-buffer' then
|
||||
stops the daemon cleanly."
|
||||
(or (not (flan--daemon-of-this-buffer-p))
|
||||
(when (y-or-n-p
|
||||
(format "flan dev is running %s; stop it and kill the buffer? "
|
||||
(file-name-nondirectory
|
||||
(or flan--file flan--daemon-socket "the program"))))
|
||||
(set-process-query-on-exit-flag flan--daemon nil)
|
||||
t)))
|
||||
|
||||
(defun flan--kill-daemon-buffer ()
|
||||
"Stop the daemon cleanly when its buffer is killed; see `flan--close-daemon'."
|
||||
(when (flan--daemon-of-this-buffer-p)
|
||||
(flan--close-daemon)))
|
||||
|
||||
(defun flan--start-daemon (file socket)
|
||||
"Start `flan dev' on FILE listening on SOCKET, and return the process."
|
||||
(let* ((buf (get-buffer-create flan-daemon-buffer))
|
||||
@ -947,13 +1003,19 @@ It builds the program first, which for a cold project is most of this."
|
||||
;; program runs, and `compilation-mode' would claim it as the output of
|
||||
;; one finished command — killing the process on a `recompile', among
|
||||
;; other things it has no business doing to a live session.
|
||||
(flan--daemon-buffer-setup))
|
||||
(flan--daemon-buffer-setup)
|
||||
;; Killing this buffer deletes its process, so the kill asks first and
|
||||
;; then tells the daemon to close.
|
||||
(add-hook 'kill-buffer-query-functions #'flan--query-kill-daemon nil t)
|
||||
(add-hook 'kill-buffer-hook #'flan--kill-daemon-buffer nil t))
|
||||
(make-process
|
||||
:name "flan-daemon" :buffer buf
|
||||
:command args
|
||||
;; The daemon writes its ready line and the program's stderr to stderr,
|
||||
;; and both belong in the same buffer in the order they happened.
|
||||
:connection-type 'pipe :noquery t
|
||||
;; No :noquery: exiting Emacs with a session running asks once, through
|
||||
;; the usual list of live processes, and `kill-emacs-hook' then closes it.
|
||||
:connection-type 'pipe
|
||||
:sentinel #'flan--daemon-sentinel)))
|
||||
|
||||
(defun flan--connect-when-ready (socket proc)
|
||||
|
||||
@ -1592,7 +1592,63 @@ already rely on it — so nothing here is a stand-in for the real thing."
|
||||
;; claim seen from the other side.
|
||||
(test-flan--check "and takes its socket with it"
|
||||
(not (file-exists-p socket2))))
|
||||
(ignore-errors (delete-file socket2)))
|
||||
(ignore-errors (delete-file socket2))
|
||||
|
||||
;; Killing the daemon's buffer asks first, as CIDER does, and a yes stops
|
||||
;; the daemon the way `flan-quit' does rather than deleting the process:
|
||||
;; the daemon, its program (--two-process, so it is a process of its own)
|
||||
;; and the socket all go. A no leaves both the buffer and the daemon.
|
||||
(let ((socket3 (concat socket "-killed-buffer"))
|
||||
(asked 0)
|
||||
(answer nil))
|
||||
(ignore-errors (delete-file socket3))
|
||||
(let ((flan-daemon-args (append flan-daemon-args '("--two-process"))))
|
||||
(flan program socket3))
|
||||
(let* ((proc flan--daemon)
|
||||
(pid (process-id proc))
|
||||
(kids (ignore-errors
|
||||
(mapcar #'string-to-number
|
||||
(split-string
|
||||
(with-temp-buffer
|
||||
(insert-file-contents
|
||||
(format "/proc/%d/task/%d/children" pid pid))
|
||||
(buffer-string))))))
|
||||
(gone (lambda (p)
|
||||
(let ((stat (format "/proc/%d/stat" p)))
|
||||
(or (not (file-exists-p stat))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents stat)
|
||||
(re-search-forward ") \\(.\\)" nil t)
|
||||
(equal (match-string 1) "Z")))))))
|
||||
(cl-letf (((symbol-function 'y-or-n-p)
|
||||
(lambda (&rest _) (setq asked (1+ asked)) answer)))
|
||||
(test-flan--check "the daemon's program is a process of its own"
|
||||
kids)
|
||||
(setq answer nil)
|
||||
(kill-buffer flan-daemon-buffer)
|
||||
(test-flan--check "killing the daemon's buffer asks, and no keeps both"
|
||||
(and (= asked 1)
|
||||
(get-buffer flan-daemon-buffer)
|
||||
(process-live-p proc)))
|
||||
(setq answer t)
|
||||
(kill-buffer flan-daemon-buffer)
|
||||
(let ((deadline (+ (float-time) 5)))
|
||||
(while (and (< (float-time) deadline)
|
||||
(not (and (funcall gone pid)
|
||||
(cl-every gone kids))))
|
||||
(sleep-for 0.05)))
|
||||
(test-flan--check "and yes stops the daemon and its program"
|
||||
(and (= asked 2)
|
||||
(not (get-buffer flan-daemon-buffer))
|
||||
(null flan--daemon)
|
||||
(funcall gone pid)
|
||||
(cl-every gone kids)))
|
||||
(test-flan--check "and removes its socket"
|
||||
(not (file-exists-p socket3)))
|
||||
(setq asked 0)
|
||||
(with-temp-buffer
|
||||
(test-flan--check "no daemon, no question"
|
||||
(and (flan--query-kill-daemon) (= asked 0))))))))
|
||||
|
||||
;; ── One command, one session ──────────────────────────────────────────
|
||||
;;
|
||||
|
||||
112
lib/dev.ml
112
lib/dev.ml
@ -5282,6 +5282,80 @@ let client_grace () =
|
||||
| None -> client_grace_default)
|
||||
| None -> client_grace_default
|
||||
|
||||
(* ── Whether anything can still reach this session ─────────────────── *)
|
||||
|
||||
(* A daemon whose socket file has been deleted, or replaced by another daemon
|
||||
binding the same path, can never be connected to again: an editor finds a
|
||||
session only by that path. So the accept loop looks at the path about once a
|
||||
second and ends the session, cleanly, when it no longer names the file this
|
||||
daemon bound.
|
||||
|
||||
The identity is taken with [stat] on the path right after the bind, never
|
||||
from the listening fd (a socket fd's inode is sockfs's, not the file's), and
|
||||
the watch starts only in [accept_loop], after the bind — so a session still
|
||||
building cannot trip it. [stat] takes a path up to PATH_MAX, so a socket
|
||||
bound through /proc/self/fd because its path is past 107 bytes is looked at
|
||||
by that long path directly; the short symlink an editor connects through is
|
||||
not what is checked. Both daemon shapes bind in this process, so
|
||||
--two-process watches the same file the merged build does. *)
|
||||
type bound = { path : string; dev : int; ino : int }
|
||||
|
||||
let bound_at sock =
|
||||
let path =
|
||||
if Filename.is_relative sock then Filename.concat (Sys.getcwd ()) sock
|
||||
else sock
|
||||
in
|
||||
match Unix.stat path with
|
||||
| s -> Some { path; dev = s.Unix.st_dev; ino = s.Unix.st_ino }
|
||||
| exception Unix.Unix_error _ -> None
|
||||
|
||||
let still_bound b =
|
||||
match Unix.stat b.path with
|
||||
| s -> s.Unix.st_dev = b.dev && s.Unix.st_ino = b.ino
|
||||
| exception Unix.Unix_error _ -> false
|
||||
|
||||
(* A test harness that starts a daemon names itself in FLAN_DEV_HARNESS
|
||||
(test/watchdog.ml), and the daemon dies with its parent and ends when the
|
||||
harness has gone. The parent half is PR_SET_PDEATHSIG, armed in [start].
|
||||
The harness half is for a daemon whose parent is not the harness — one an
|
||||
Emacs under test started — and is read by [accept_loop]. Opt-in only: a
|
||||
daemon an editor or a shell starts must outlive them, as it always has. *)
|
||||
let harness () =
|
||||
match Sys.getenv_opt "FLAN_DEV_HARNESS" with
|
||||
| Some s ->
|
||||
(match int_of_string_opt (String.trim s) with
|
||||
| Some p when p > 0 -> Some p
|
||||
| _ -> None)
|
||||
| None -> None
|
||||
|
||||
let alive pid =
|
||||
match Unix.kill pid 0 with
|
||||
| () -> true
|
||||
| exception Unix.Unix_error (Unix.ESRCH, _, _) -> false
|
||||
| exception Unix.Unix_error _ -> true
|
||||
|
||||
(* Why the session can no longer be reached, if it cannot. *)
|
||||
let unreachable bound =
|
||||
match bound with
|
||||
| Some b when not (still_bound b) ->
|
||||
Some
|
||||
(Printf.sprintf "its socket %s was removed or replaced, so no editor can \
|
||||
reach it" b.path)
|
||||
| _ ->
|
||||
match harness () with
|
||||
| Some pid when not (alive pid) ->
|
||||
Some (Printf.sprintf "the test harness that started it (pid %d) has exited"
|
||||
pid)
|
||||
| _ -> None
|
||||
|
||||
(* The socket file and its short symlink, removed on the way out only while
|
||||
the path still names this daemon's socket: one that was replaced belongs to
|
||||
the daemon that replaced it, and so does the symlink to it. *)
|
||||
let release_socket bound sock ~unlink_short =
|
||||
let mine = match bound with Some b -> still_bound b | None -> true in
|
||||
if mine then (try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
if mine || not (Sys.file_exists sock) then unlink_short sock
|
||||
|
||||
(* [accept] would block past the program's own exit, so it is waited on with
|
||||
a timeout and the child checked each time round: a daemon whose program has
|
||||
finished has nothing left to do, and an editor waiting on it would wait
|
||||
@ -5298,18 +5372,29 @@ let client_grace () =
|
||||
hole: it reads the client, not the program, and all of an Emacs's windows
|
||||
share the one [flan--connection], so closing one of them changes
|
||||
nothing this loop can see. *)
|
||||
let accept_loop ?grace t ls =
|
||||
let accept_loop ?grace ?bound t ls =
|
||||
let grace = match grace with Some g -> g | None -> client_grace () in
|
||||
(* [served] arms the clock and [since] is when the last client let go — set
|
||||
when [serve] returns rather than when [accept] fires, so a connection that
|
||||
is held for an hour is an hour of the clock not running. *)
|
||||
let served = ref false and since = ref (Unix.gettimeofday ()) in
|
||||
let reach_due = ref 0. in
|
||||
let cut_off () =
|
||||
let now = Unix.gettimeofday () in
|
||||
if now < !reach_due then None
|
||||
else begin reach_due := now +. 1.; unreachable bound end
|
||||
in
|
||||
let rec go () =
|
||||
(* The other place the agent clock is read: between connections, which is
|
||||
where a session with no editor attached spends its time. *)
|
||||
agent_check t;
|
||||
match liveness t with
|
||||
| Gone when t.relaunch = None -> ()
|
||||
| _ when (match cut_off () with
|
||||
| Some why ->
|
||||
Printf.eprintf "flan dev: this session is ending; %s\n%!" why;
|
||||
true
|
||||
| None -> false) -> ()
|
||||
| live ->
|
||||
(* A --two-process child that has ended can be started again, so the
|
||||
session waits as a parked one does, on the parked grace. *)
|
||||
@ -5614,6 +5699,7 @@ let two_process ?(debug = false) ?(sanitize = false) ?(x86 = true) ~file ~sock (
|
||||
let ls = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
|
||||
link_short_socket sock;
|
||||
Wire.bind_socket ls sock;
|
||||
let bound = bound_at sock in
|
||||
Unix.listen ls 4;
|
||||
Printf.eprintf "flan dev: %s ready on %s (%.0fms)\n%!" file sock
|
||||
((Unix.gettimeofday () -. t0) *. 1000.);
|
||||
@ -5625,9 +5711,8 @@ let two_process ?(debug = false) ?(sanitize = false) ?(x86 = true) ~file ~sock (
|
||||
| None -> ());
|
||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||
(try Unix.close t.stdout with Unix.Unix_error _ -> ());
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
unlink_short_socket sock)
|
||||
(fun () -> accept_loop t ls);
|
||||
release_socket bound sock ~unlink_short:unlink_short_socket)
|
||||
(fun () -> accept_loop ?bound t ls);
|
||||
(* Here only when the loop returned: an exception out of it has already
|
||||
left through the [finally]. A child killed by a signal is the crash that
|
||||
keeps the directory. *)
|
||||
@ -6478,8 +6563,9 @@ let merged_setup () =
|
||||
let ls = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
|
||||
link_short_socket sock;
|
||||
Wire.bind_socket ls sock;
|
||||
let bound = bound_at sock in
|
||||
Unix.listen ls 4;
|
||||
merged_state := Some (t, ls, sock);
|
||||
merged_state := Some (t, ls, sock, bound);
|
||||
Printf.eprintf "flan dev: %s ready on %s (%.0fms, one process)\n%!" file
|
||||
sock ((Unix.gettimeofday () -. t0) *. 1000.)
|
||||
with
|
||||
@ -6508,7 +6594,7 @@ let merged_setup () =
|
||||
let merged_serve () =
|
||||
match !merged_state with
|
||||
| None -> prerr_endline "flan dev: serve was called before setup"; exit 1
|
||||
| Some (t, ls, sock) ->
|
||||
| Some (t, ls, sock, bound) ->
|
||||
(* The agent is bound by the program on the main thread, which only starts
|
||||
once [merged_setup] has returned. So this does not wait for it: it arms
|
||||
the clock that eventually says it never came, and serves.
|
||||
@ -6529,15 +6615,14 @@ let merged_serve () =
|
||||
[eval] for what a delivery to such a program honestly reports. *)
|
||||
t.agent_watch <- Some (Unix.gettimeofday () +. 10.);
|
||||
let clean =
|
||||
match accept_loop t ls with
|
||||
match accept_loop ?bound t ls with
|
||||
| () -> true
|
||||
| exception e ->
|
||||
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e);
|
||||
false
|
||||
in
|
||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||
unlink_short_socket sock;
|
||||
release_socket bound sock ~unlink_short:unlink_short_socket;
|
||||
(* The program is this process, so a program that crashed never gets here;
|
||||
the one end that does and is not clean is the loop raising. *)
|
||||
if clean then remove_session_dirs t;
|
||||
@ -6615,6 +6700,15 @@ let start_merged ?(debug = false) ?(sanitize = false) ?(x86 = true) ~file ~sock
|
||||
actually deleted. *)
|
||||
let start ?(debug = false) ?(sanitize = false) ?(merged = true) ?(x86 = true)
|
||||
~file ~sock () =
|
||||
(* Under a test harness only (see [harness]): SIGKILL when the parent dies,
|
||||
which the kernel keeps across the exec into a merged build. A parent that
|
||||
died before this was armed is caught by asking whether the harness is
|
||||
still there; one that is not the harness is left to [accept_loop]. *)
|
||||
(match harness () with
|
||||
| Some pid ->
|
||||
Spawn.die_with_parent ();
|
||||
if not (alive pid) then exit 1
|
||||
| None -> ());
|
||||
(* The sanitizers are LLVM passes, and the x86 backend's host is written
|
||||
by hand with no pass run over it. The modules a session sends are not
|
||||
instrumented on either backend; what is checked is the host and the
|
||||
|
||||
@ -4,3 +4,6 @@ external dying : string -> string array -> int = "flan_spawn_dying"
|
||||
|
||||
(** The host number of an OCaml signal number. *)
|
||||
external host_signal : int -> int = "flan_host_signal"
|
||||
|
||||
(** Ask for SIGKILL when this process's parent dies; nothing off Linux. *)
|
||||
external die_with_parent : unit -> unit = "flan_die_with_parent"
|
||||
|
||||
@ -56,3 +56,15 @@ value flan_spawn_dying(value path, value argv) {
|
||||
value flan_host_signal(value s) {
|
||||
return Val_int(caml_convert_signal_number(Int_val(s)));
|
||||
}
|
||||
|
||||
/* The same request made by this process for itself: SIGKILL when whatever is
|
||||
* its parent now dies. Only `flan dev` under a test harness asks
|
||||
* (FLAN_DEV_HARNESS, lib/dev.ml); the kernel keeps it across the exec into a
|
||||
* merged build. */
|
||||
value flan_die_with_parent(value unit) {
|
||||
(void)unit;
|
||||
#ifdef __linux__
|
||||
prctl(PR_SET_PDEATHSIG, SIGKILL);
|
||||
#endif
|
||||
return Val_unit;
|
||||
}
|
||||
|
||||
@ -795,12 +795,17 @@ static void rt_flush_out(void) {
|
||||
* The two functions are kept saying the same thing on purpose. They are the
|
||||
* two ways a Flan program dies where it stands, and a difference between them
|
||||
* would be a difference nobody could predict from the outside. */
|
||||
/* Set by the dev agent once it has bound its own socket, to remove it: the
|
||||
* [_exit] below skips the atexit handler that otherwise would. */
|
||||
void (*flan_die_hook)(void);
|
||||
|
||||
static _Noreturn void rt_die(void) {
|
||||
const char *sock;
|
||||
rt_flush_out();
|
||||
fflush(stderr);
|
||||
sock = getenv("FLAN_DEV_SOCK");
|
||||
if (sock != NULL && *sock != '\0') unlink(sock);
|
||||
if (flan_die_hook != NULL) flan_die_hook();
|
||||
_exit(134);
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
;;;;
|
||||
;;;; Under [flan dev] it binds where the daemon said, exactly as the explicit
|
||||
;;;; form did, and nothing here can tell the difference. Run on its own — which
|
||||
;;;; is what test_agent.ml does with it — it picks a path under /tmp and prints
|
||||
;;;; is what test_agent.ml does with it — it picks a path under TMPDIR and prints
|
||||
;;;; it to stderr, and that printed line is the only way anything could connect.
|
||||
;;;;
|
||||
;;;; The second start is here to be a no-op. It answers 0 like the first, does
|
||||
|
||||
@ -242,7 +242,11 @@ let () =
|
||||
(* The shape is part of the claim: two programs started at once must not
|
||||
choose the same file, and the pid alone would not separate two runs of
|
||||
the same program in sequence. *)
|
||||
let pid_part = Printf.sprintf "/tmp/flan-agent-%d-" apid in
|
||||
(* Under TMPDIR, which the program inherited from this suite. *)
|
||||
let pid_part =
|
||||
Filename.concat (Filename.get_temp_dir_name ())
|
||||
(Printf.sprintf "flan-agent-%d-" apid)
|
||||
in
|
||||
if not (String.length apath > String.length pid_part
|
||||
&& String.sub apath 0 (String.length pid_part) = pid_part)
|
||||
then fail "the chosen path is not this process's: %S" apath;
|
||||
@ -286,6 +290,41 @@ let () =
|
||||
end
|
||||
end
|
||||
end;
|
||||
(* ── The socket goes with a program ended by a signal ──────────────
|
||||
|
||||
SIGTERM runs no atexit, so the agent removes its socket from a handler
|
||||
of its own and then dies of the signal as before. *)
|
||||
let terr = tmp "term.err" in
|
||||
let t1 = ofd (tmp "term.out") and t2 = ofd terr in
|
||||
let tpid = Unix.create_process_env aexe [| aexe |] aenv Unix.stdin t1 t2 in
|
||||
Unix.close t1;
|
||||
Unix.close t2;
|
||||
let tpath () =
|
||||
let text = In_channel.with_open_bin terr In_channel.input_all in
|
||||
List.find_map
|
||||
(fun l ->
|
||||
if String.starts_with ~prefix l then
|
||||
Some (String.sub l (String.length prefix)
|
||||
(String.length l - String.length prefix))
|
||||
else None)
|
||||
(String.split_on_char '\n' text)
|
||||
in
|
||||
(match
|
||||
if await (fun () -> tpath () <> None) then tpath () else None
|
||||
with
|
||||
| None ->
|
||||
fail "a program to be sent SIGTERM never announced its socket";
|
||||
(try Unix.kill tpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
| Some p ->
|
||||
ignore (await (fun () -> Sys.file_exists p));
|
||||
Unix.kill tpid Sys.sigterm;
|
||||
let _, st = Unix.waitpid [] tpid in
|
||||
if st <> Unix.WSIGNALED Sys.sigterm then
|
||||
fail "a program sent SIGTERM did not die of it";
|
||||
if Sys.file_exists p then
|
||||
fail "the socket outlived a program ended by SIGTERM: %S" p);
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||
[ terr; tmp "term.out" ];
|
||||
(* ── A path that cannot be bound ────────────────────────────────── *)
|
||||
|
||||
(* The same program again, told to listen somewhere that does not exist.
|
||||
|
||||
165
test/test_dev.ml
165
test/test_dev.ml
@ -5920,6 +5920,14 @@ let () =
|
||||
(try Unix.kill pid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end
|
||||
else begin
|
||||
(* Past two of the daemon's socket checks with no editor attached:
|
||||
a socket reached through its directory is still its own. *)
|
||||
Unix.sleepf 2.5;
|
||||
(match Unix.waitpid [ Unix.WNOHANG ] pid with
|
||||
| 0, _ -> ()
|
||||
| _ -> fail "a deep TMPDIR (%s): the daemon ended with its socket \
|
||||
in place" shape
|
||||
| exception Unix.Unix_error _ -> ());
|
||||
let c = connect dsock in
|
||||
let said r =
|
||||
Option.value ~default:(status r) (Wire.string_field r "message")
|
||||
@ -5955,6 +5963,163 @@ let () =
|
||||
[ [||]; [| "--two-process" |] ];
|
||||
Own_tmp.remove deep;
|
||||
|
||||
(* ── A daemon nothing can reach any more ends ──────────────────────
|
||||
|
||||
Three ways a session becomes unreachable, each in both shapes: the
|
||||
process that started it is SIGKILLed (the daemon, and under
|
||||
--two-process its program, die with it); the test harness named in
|
||||
FLAN_DEV_HARNESS goes, though the daemon's own parent lives on (the
|
||||
Emacs-started case); and its socket file is deleted. And one way it
|
||||
does not: a socket left alone keeps the daemon running. *)
|
||||
let gone pid =
|
||||
match In_channel.with_open_bin (Printf.sprintf "/proc/%d/stat" pid)
|
||||
In_channel.input_all with
|
||||
| st ->
|
||||
(match String.rindex_opt st ')' with
|
||||
| Some i when i + 2 < String.length st -> st.[i + 2] = 'Z'
|
||||
| _ -> false)
|
||||
| exception Sys_error _ -> true
|
||||
in
|
||||
let children pid =
|
||||
match In_channel.with_open_bin
|
||||
(Printf.sprintf "/proc/%d/task/%d/children" pid pid)
|
||||
In_channel.input_all with
|
||||
| s -> List.filter_map int_of_string_opt (String.split_on_char ' ' (String.trim s))
|
||||
| exception Sys_error _ -> []
|
||||
in
|
||||
let with_harness env =
|
||||
Array.of_list
|
||||
(env
|
||||
@ List.filter
|
||||
(fun v -> not (String.starts_with ~prefix:"FLAN_DEV_HARNESS=" v))
|
||||
(Array.to_list (Unix.environment ())))
|
||||
in
|
||||
List.iter
|
||||
(fun mode ->
|
||||
let shape = if mode = [||] then "one process" else "--two-process" in
|
||||
let argv s =
|
||||
Array.append [| flan; "dev"; "programs/dev-pause.flan"; "-s"; s |] mode
|
||||
in
|
||||
(* The parent SIGKILLed. A harness process of its own, so that the
|
||||
parent can be killed without killing this test. *)
|
||||
let ksock = tmp "orphan-parent.sock" in
|
||||
(try Sys.remove ksock with Sys_error _ -> ());
|
||||
let rd, wr = Unix.pipe () in
|
||||
(match Unix.fork () with
|
||||
| 0 ->
|
||||
Unix.close rd;
|
||||
let d =
|
||||
Unix.create_process flan (argv ksock) Unix.stdin Unix.stdout
|
||||
Unix.stderr
|
||||
in
|
||||
let msg = string_of_int d ^ "\n" in
|
||||
ignore (Unix.write_substring wr msg 0 (String.length msg));
|
||||
Unix.close wr;
|
||||
Unix.sleep 600;
|
||||
Unix._exit 0
|
||||
| h ->
|
||||
Unix.close wr;
|
||||
let ic = Unix.in_channel_of_descr rd in
|
||||
let d = int_of_string (String.trim (input_line ic)) in
|
||||
close_in ic;
|
||||
if not (listening ~pid:d ksock) then
|
||||
fail "a daemon under a harness (%s): %s" shape !listen_why;
|
||||
let kids = children d in
|
||||
if mode <> [||] && kids = [] then
|
||||
fail "a daemon under a harness (%s): no program child found" shape;
|
||||
Unix.kill h Sys.sigkill;
|
||||
ignore (Unix.waitpid [] h);
|
||||
if not (await ~ms:5000 (fun () -> gone d)) then begin
|
||||
fail "a daemon (%s) outlived the process that started it" shape;
|
||||
(try Unix.kill d Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end;
|
||||
List.iter
|
||||
(fun k ->
|
||||
if not (await ~ms:5000 (fun () -> gone k)) then begin
|
||||
fail "a daemon's program (%s) outlived the daemon" shape;
|
||||
(try Unix.kill k Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end)
|
||||
kids);
|
||||
(try Sys.remove ksock with Sys_error _ -> ());
|
||||
|
||||
(* The harness gone, the parent alive. [sleep] stands in for it. *)
|
||||
let hsock = tmp "orphan-harness.sock" in
|
||||
(try Sys.remove hsock with Sys_error _ -> ());
|
||||
let stand_in =
|
||||
Unix.create_process "sleep" [| "sleep"; "600" |] Unix.stdin
|
||||
Unix.stdout Unix.stderr
|
||||
in
|
||||
let d =
|
||||
Unix.create_process_env flan (argv hsock)
|
||||
(with_harness [ "FLAN_DEV_HARNESS=" ^ string_of_int stand_in ])
|
||||
Unix.stdin Unix.stdout Unix.stderr
|
||||
in
|
||||
if not (listening ~pid:d hsock) then
|
||||
fail "a daemon with a harness (%s): %s" shape !listen_why
|
||||
else begin
|
||||
Unix.kill stand_in Sys.sigkill;
|
||||
ignore (Unix.waitpid [] stand_in);
|
||||
let ended =
|
||||
await ~ms:5000 (fun () ->
|
||||
match Unix.waitpid [ Unix.WNOHANG ] d with
|
||||
| 0, _ -> false
|
||||
| _ -> true)
|
||||
in
|
||||
if not ended then
|
||||
fail "a daemon (%s) outlived the test harness that started it" shape
|
||||
end;
|
||||
(try Unix.kill stand_in Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(try Unix.kill d Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] d) with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] stand_in) with Unix.Unix_error _ -> ());
|
||||
(try Sys.remove hsock with Sys_error _ -> ());
|
||||
|
||||
(* The socket: kept, the daemon stays; deleted, it ends, cleanly. *)
|
||||
let ssock = tmp "orphan-socket.sock" in
|
||||
(try Sys.remove ssock with Sys_error _ -> ());
|
||||
let d =
|
||||
Unix.create_process flan (argv ssock) Unix.stdin Unix.stdout
|
||||
Unix.stderr
|
||||
in
|
||||
if not (listening ~pid:d ssock) then
|
||||
fail "a daemon whose socket goes (%s): %s" shape !listen_why
|
||||
else begin
|
||||
let kids = children d in
|
||||
Unix.sleepf 2.5;
|
||||
(match Unix.waitpid [ Unix.WNOHANG ] d with
|
||||
| 0, _ -> ()
|
||||
| _ -> fail "a daemon (%s) ended with its socket in place" shape);
|
||||
Sys.remove ssock;
|
||||
let st = ref None in
|
||||
let ended =
|
||||
await ~ms:5000 (fun () ->
|
||||
match Unix.waitpid [ Unix.WNOHANG ] d with
|
||||
| 0, _ -> false
|
||||
| _, s -> st := Some s; true)
|
||||
in
|
||||
if not ended then
|
||||
fail "a daemon (%s) outlived its deleted socket" shape
|
||||
else begin
|
||||
if !st <> Some (Unix.WEXITED 0) then
|
||||
fail "a daemon (%s) whose socket was deleted did not end cleanly"
|
||||
shape;
|
||||
let dir =
|
||||
Filename.concat (Filename.get_temp_dir_name ())
|
||||
(Printf.sprintf "flan-dev-%d" d)
|
||||
in
|
||||
if Sys.file_exists dir then
|
||||
fail "a daemon (%s) whose socket was deleted left %s" shape dir;
|
||||
List.iter
|
||||
(fun k ->
|
||||
if not (await ~ms:5000 (fun () -> gone k)) then
|
||||
fail "the program (%s) outlived its daemon's socket" shape)
|
||||
kids
|
||||
end
|
||||
end;
|
||||
(try Unix.kill d Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] d) with Unix.Unix_error _ -> ()))
|
||||
[ [||]; [| "--two-process" |] ];
|
||||
|
||||
(* ── Prelude functions shadowed live keep the prelude's own calls ──
|
||||
|
||||
[rand] and then [rand-int] redefined in a running program: the
|
||||
|
||||
@ -76,6 +76,10 @@ let arm ?(seconds = 600) name =
|
||||
label := name;
|
||||
budget := seconds;
|
||||
deadline := Unix.gettimeofday () +. float_of_int seconds;
|
||||
(* Every [flan dev] this binary starts, directly or from an Emacs it runs,
|
||||
dies with its parent and ends once this process is gone (lib/dev.ml,
|
||||
[harness]), so a run killed partway leaves no daemon behind. *)
|
||||
Unix.putenv "FLAN_DEV_HARNESS" (string_of_int (Unix.getpid ()));
|
||||
backstop ()
|
||||
|
||||
(* [f] under a tighter alarm, with the backstop restored afterwards however
|
||||
|
||||
56
vendor/agent/flan_agent.c
vendored
56
vendor/agent/flan_agent.c
vendored
@ -1023,14 +1023,17 @@ static int sock_dir_fd = -1;
|
||||
/* A socket file outlives the process that bound it, and a stale one answers
|
||||
* the next client with ECONNREFUSED — which reads like a program that is there
|
||||
* and refusing rather than one that has gone. So the bind registers its own
|
||||
* removal, and the three ways out of a program each unlink it:
|
||||
* removal, and every way out of a program that runs any code unlinks it:
|
||||
*
|
||||
* ordinary exit this handler, via atexit
|
||||
* break-loop abort die_now, by hand, because it takes _exit
|
||||
* trap rt_die in flan_rt.c, through [flan_die_hook]
|
||||
* orphaned child orphan_die, by hand, for the same reason
|
||||
* fatal signal fatal_unlink: SIGTERM, SIGINT, SIGHUP, SIGQUIT and
|
||||
* SIGABRT, while their disposition is still the default
|
||||
*
|
||||
* Outside a daemon that is the whole story, and the path is under /tmp where
|
||||
* nothing else would ever reclaim it.
|
||||
* Outside a daemon that is the whole story, and the path is under TMPDIR where
|
||||
* nothing else would ever reclaim it. SIGKILL is the one end nothing survives.
|
||||
*
|
||||
* Under [flan dev] none of the three is how a session usually ends, and this
|
||||
* is worth being exact about rather than claiming cover it does not give: the
|
||||
@ -1045,6 +1048,35 @@ static void unlink_bound_sock(void) {
|
||||
if (bound_sock[0] != '\0') unlink(bound_sock);
|
||||
}
|
||||
|
||||
extern void (*flan_die_hook)(void);
|
||||
|
||||
/* unlink and re-raise under the default disposition, so the process still
|
||||
* ends by the signal it was sent: a parent waiting on it sees the same status
|
||||
* as before. Both calls are async-signal-safe. */
|
||||
static void fatal_unlink(int sig) {
|
||||
if (bound_sock[0] != '\0') unlink(bound_sock);
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
/* Only a signal nobody has claimed: a program, or the runtime a merged build
|
||||
* carries, that installed its own handler keeps it. Called after the bind. */
|
||||
static void unlink_on_fatal_signals(void) {
|
||||
static const int sigs[] = { SIGTERM, SIGINT, SIGHUP, SIGQUIT, SIGABRT };
|
||||
size_t i;
|
||||
for (i = 0; i < sizeof sigs / sizeof sigs[0]; i++) {
|
||||
struct sigaction old, sa;
|
||||
if (sigaction(sigs[i], NULL, &old) != 0) continue;
|
||||
if ((old.sa_flags & SA_SIGINFO) != 0 || old.sa_handler != SIG_DFL) continue;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_handler = fatal_unlink;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_RESETHAND;
|
||||
sigaction(sigs[i], &sa, NULL);
|
||||
}
|
||||
flan_die_hook = unlink_bound_sock;
|
||||
}
|
||||
|
||||
/* Every way out of the break loop that is not a resume. [_exit] and not
|
||||
* [exit], because this runs on the game thread while the listener thread may
|
||||
* be inside [dlopen] holding the loader lock — and [exit] runs the atexit
|
||||
@ -2786,6 +2818,7 @@ static int32_t start_on(const char *path) {
|
||||
/* After the bind, because a program that is going to fail to listen should
|
||||
fail on its own terms rather than arrange its death first. */
|
||||
watch_the_daemon();
|
||||
unlink_on_fatal_signals();
|
||||
/* From here an unhandled error stops rather than dying. Installed with the
|
||||
* socket and not before it: without a listener there is nobody to ask what
|
||||
* to do, and stopping forever is worse than the abort it replaces. */
|
||||
@ -2864,14 +2897,25 @@ int32_t flan_agent_start(const uint8_t *path, int64_t len) {
|
||||
* silently reuses a file it may not own. stderr rather than stdout, so a
|
||||
* program whose output is data stays data. */
|
||||
int32_t flan_agent_start_auto(void) {
|
||||
char path[sizeof(((struct sockaddr_un *)0)->sun_path)];
|
||||
/* Room for a TMPDIR too deep for sun_path; [start_on] binds such a path
|
||||
* through its directory. */
|
||||
char path[4096];
|
||||
struct timespec ts;
|
||||
int32_t r;
|
||||
const char *env = daemon_socket();
|
||||
const char *dir = getenv("TMPDIR");
|
||||
size_t dlen;
|
||||
int n;
|
||||
if (env != NULL) return start_on(env) < 0 ? -1 : 0;
|
||||
/* TMPDIR, as everything else Flan makes on the fly: a test run's own
|
||||
* directory, or a user's, is removed with it. /tmp when unset. */
|
||||
if (dir == NULL || dir[0] != '/') dir = "/tmp";
|
||||
dlen = strlen(dir);
|
||||
while (dlen > 1 && dir[dlen - 1] == '/') dlen--;
|
||||
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) ts.tv_nsec = 0;
|
||||
snprintf(path, sizeof path, "/tmp/flan-agent-%ld-%08lx.sock",
|
||||
(long)getpid(), (unsigned long)(ts.tv_nsec & 0xffffffffL));
|
||||
n = snprintf(path, sizeof path, "%.*s/flan-agent-%ld-%08lx.sock", (int)dlen,
|
||||
dir, (long)getpid(), (unsigned long)(ts.tv_nsec & 0xffffffffL));
|
||||
if (n < 0 || (size_t)n >= sizeof path) return -1;
|
||||
r = start_on(path);
|
||||
/* Only when this call is the one that bound: a second [(agent/start)] would
|
||||
* otherwise print a path it did not bind and nothing is listening on. */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user