Ctrl-C killed meowc and left every compiler it had spawned running, still holding CPU and still about to write into the build directory. There were no signal handlers at all. SIGINT and SIGTERM now cancel the same way a build failure does, and the run returns cleanly so the cache is still written and the exit status is 130. A second signal falls through to the default handler, so an unresponsive child cannot trap the user. Signalling a single pid was not enough: a compiler driver spawns its own children, and those were reparented to init and survived. Each action is now placed in its own process group with setsid, and cancellation signals the group, which also makes the existing failure path reach grandchildren. Unix.wait is retried on EINTR so a signal arriving mid-wait is not fatal.
169 lines
5.8 KiB
OCaml
169 lines
5.8 KiB
OCaml
type result = { built : int; cached : int; failed : int; aborted : int; interrupted : bool }
|
|
|
|
type state = Blocked | Ready | Running | Done | Failed
|
|
|
|
let inputs_of (n : Graph.node) =
|
|
match n.depfile with
|
|
| None -> n.ins
|
|
| Some d -> (
|
|
match Dep.of_file d with
|
|
| Some l -> List.sort_uniq compare (n.ins @ l)
|
|
| None -> n.ins)
|
|
|
|
let key_of n = Cache.key ~cmd:(Exec.show n.Graph.cmd) ~inputs:(inputs_of n)
|
|
|
|
let up_to_date cache (n : Graph.node) =
|
|
let outputs_present = List.for_all Sys.file_exists n.Graph.outs in
|
|
let have_deps = match n.Graph.depfile with None -> true | Some d -> Sys.file_exists d in
|
|
outputs_present && have_deps && Cache.current cache (List.hd n.Graph.outs) = Some (key_of n)
|
|
|
|
let run g ~selected ~jobs ~cache ~verbose ~keep_going ~on_start ~on_done =
|
|
let nodes = g.Graph.nodes in
|
|
let n = Array.length nodes in
|
|
let state = Array.make n Blocked in
|
|
let pending = Array.make n 0 in
|
|
let rdeps = Array.make n [] in
|
|
Array.iter
|
|
(fun (v : Graph.node) ->
|
|
pending.(v.id) <- List.length v.deps;
|
|
List.iter (fun d -> rdeps.(d) <- v.id :: rdeps.(d)) v.deps)
|
|
nodes;
|
|
let wanted i = Hashtbl.mem selected i in
|
|
let ready = Queue.create () in
|
|
Array.iter (fun (v : Graph.node) -> if wanted v.id && pending.(v.id) = 0 then Queue.add v.id ready) nodes;
|
|
let running = Hashtbl.create 8 in
|
|
let cancelled = Hashtbl.create 8 in
|
|
let built = ref 0 and cached = ref 0 and failed = ref 0 and aborted_count = ref 0 in
|
|
let interrupted = ref false in
|
|
let stopping () = !interrupted || (!failed > 0 && not keep_going) in
|
|
let release i =
|
|
List.iter
|
|
(fun j ->
|
|
if wanted j then begin
|
|
pending.(j) <- pending.(j) - 1;
|
|
if pending.(j) = 0 && state.(j) = Blocked then (state.(j) <- Ready; Queue.add j ready)
|
|
end)
|
|
rdeps.(i)
|
|
in
|
|
let skip i =
|
|
state.(i) <- Failed;
|
|
List.iter (fun j -> if wanted j && state.(j) = Blocked then pending.(j) <- pending.(j) - 1) rdeps.(i)
|
|
in
|
|
let spawn (v : Graph.node) =
|
|
List.iter (fun o -> Fs.mkdir_p (Filename.dirname o)) v.outs;
|
|
let tmp = Filename.temp_file "meowc" ".log" in
|
|
let fd = Unix.openfile tmp [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
|
|
let nul = Exec.devnull () in
|
|
flush stdout;
|
|
let pid =
|
|
match Unix.fork () with
|
|
| 0 ->
|
|
(try ignore (Unix.setsid ()) with Unix.Unix_error _ -> ());
|
|
(try
|
|
Unix.dup2 nul Unix.stdin;
|
|
Unix.dup2 fd Unix.stdout;
|
|
Unix.dup2 fd Unix.stderr;
|
|
Unix.close fd;
|
|
Unix.close nul;
|
|
Unix.execvp v.cmd.(0) v.cmd
|
|
with _ -> ());
|
|
Unix._exit 127
|
|
| pid -> pid
|
|
| exception Unix.Unix_error (e, _, _) ->
|
|
Unix.close fd;
|
|
Unix.close nul;
|
|
Diag.error "cannot fork for %s: %s" v.cmd.(0) (Unix.error_message e)
|
|
in
|
|
Unix.close fd;
|
|
Unix.close nul;
|
|
state.(v.id) <- Running;
|
|
on_start v;
|
|
Hashtbl.replace running pid (v, tmp)
|
|
in
|
|
let cancel_running () =
|
|
Hashtbl.iter
|
|
(fun pid _ ->
|
|
Hashtbl.replace cancelled pid ();
|
|
try Unix.kill (-pid) Sys.sigterm with Unix.Unix_error _ -> ())
|
|
running
|
|
in
|
|
let discard (v : Graph.node) =
|
|
List.iter Cache.forget v.outs;
|
|
Cache.drop cache (List.hd v.outs);
|
|
List.iter (fun o -> try Sys.remove o with Sys_error _ -> ()) v.outs;
|
|
skip v.id
|
|
in
|
|
let rec wait_child () =
|
|
match Unix.wait () with
|
|
| r -> Some r
|
|
| exception Unix.Unix_error (Unix.EINTR, _, _) -> wait_child ()
|
|
| exception Unix.Unix_error (Unix.ECHILD, _, _) -> None
|
|
in
|
|
let reap () =
|
|
match wait_child () with
|
|
| None -> Hashtbl.reset running
|
|
| Some (pid, status) -> (
|
|
match Hashtbl.find_opt running pid with
|
|
| None -> ()
|
|
| Some (v, tmp) ->
|
|
Hashtbl.remove running pid;
|
|
let aborted = Hashtbl.mem cancelled pid in
|
|
Hashtbl.remove cancelled pid;
|
|
let ok = status = Unix.WEXITED 0 in
|
|
let log = try Fs.read tmp with Sys_error _ -> "" in
|
|
(try Sys.remove tmp with Sys_error _ -> ());
|
|
if ok then begin
|
|
incr built;
|
|
state.(v.id) <- Done;
|
|
List.iter Cache.forget v.outs;
|
|
(match v.depfile with Some d -> Cache.forget d | None -> ());
|
|
Cache.put cache (List.hd v.outs) (key_of v);
|
|
release v.id;
|
|
on_done ~ok ~log v;
|
|
if verbose then print_endline (" " ^ Style.dim (Exec.show v.cmd))
|
|
end
|
|
else if aborted then begin
|
|
incr aborted_count;
|
|
discard v
|
|
end
|
|
else begin
|
|
incr failed;
|
|
discard v;
|
|
on_done ~ok ~log v;
|
|
if verbose then print_endline (" " ^ Style.dim (Exec.show v.cmd));
|
|
if not keep_going then cancel_running ()
|
|
end)
|
|
in
|
|
let take_signals () =
|
|
let handle _ =
|
|
if not !interrupted then begin
|
|
interrupted := true;
|
|
Sys.set_signal Sys.sigint Sys.Signal_default;
|
|
Sys.set_signal Sys.sigterm Sys.Signal_default;
|
|
cancel_running ()
|
|
end
|
|
in
|
|
(Sys.signal Sys.sigint (Sys.Signal_handle handle), Sys.signal Sys.sigterm (Sys.Signal_handle handle))
|
|
in
|
|
let prev_int, prev_term = take_signals () in
|
|
let restore_signals () =
|
|
Sys.set_signal Sys.sigint prev_int;
|
|
Sys.set_signal Sys.sigterm prev_term
|
|
in
|
|
while (not (Queue.is_empty ready) && not (stopping ())) || Hashtbl.length running > 0 do
|
|
while (not (Queue.is_empty ready)) && Hashtbl.length running < jobs && not (stopping ()) do
|
|
let i = Queue.pop ready in
|
|
let v = nodes.(i) in
|
|
if up_to_date cache v then begin
|
|
incr cached;
|
|
state.(i) <- Done;
|
|
release i
|
|
end
|
|
else spawn v
|
|
done;
|
|
if Hashtbl.length running > 0 then reap ()
|
|
else if Queue.is_empty ready then ()
|
|
done;
|
|
restore_signals ();
|
|
{ built = !built; cached = !cached; failed = !failed; aborted = !aborted_count; interrupted = !interrupted }
|