A re-run under --two-process builds the program again from the session and starts it in a new process, and the daemon outlives a finished child
This commit is contained in:
parent
c30f52c5a7
commit
f67498c789
6
TODO.org
6
TODO.org
@ -1543,12 +1543,6 @@ A finished program parks instead of dying, and a daemon op wakes it and re-enter
|
|||||||
Globals are not reset between runs — the process never died. Rules out a fresh
|
Globals are not reset between runs — the process never died. Rules out a fresh
|
||||||
process per run.
|
process per run.
|
||||||
|
|
||||||
** NEXT Re-run does not work under --two-process
|
|
||||||
Decided 2026-09-25: re-run under =--two-process= starts a fresh child, installed redefinitions included, and says that globals start over because the process is new.
|
|
||||||
A finished child process is genuinely gone, so there is nothing to wake. Re-run is
|
|
||||||
merged-build only, and since the default backend runs merged it is no longer the
|
|
||||||
blocked case.
|
|
||||||
|
|
||||||
** DONE An accepted re-run reads as running
|
** DONE An accepted re-run reads as running
|
||||||
CLOSED: [2026-09-21]
|
CLOSED: [2026-09-21]
|
||||||
A caller that asked for a re-run and then waited for the program to park was
|
A caller that asked for a re-run and then waited for the program to park was
|
||||||
|
|||||||
152
lib/dev.ml
152
lib/dev.ml
@ -28,11 +28,15 @@ type t = {
|
|||||||
(* The running program. [Some pid] is the two-process daemon, which launched
|
(* The running program. [Some pid] is the two-process daemon, which launched
|
||||||
it; [None] is the merged build, where the program is *this* process and
|
it; [None] is the merged build, where the program is *this* process and
|
||||||
the compiler is a thread inside it. That is the whole of the difference at
|
the compiler is a thread inside it. That is the whole of the difference at
|
||||||
this layer — see [merged_setup] for why there is no third case. *)
|
this layer — see [merged_setup] for why there is no third case. A re-run
|
||||||
child : int option;
|
under --two-process replaces the child with a new one. *)
|
||||||
|
mutable child : int option;
|
||||||
agent : string; (* where it listens for modules *)
|
agent : string; (* where it listens for modules *)
|
||||||
dir : string; (* modules are built here, one per eval *)
|
dir : string; (* modules are built here, one per eval *)
|
||||||
stdout : Unix.file_descr; (* the program's output, on its way to here *)
|
mutable stdout : Unix.file_descr; (* the program's output, on its way here *)
|
||||||
|
(* --two-process only: build the program again from the session as it is
|
||||||
|
now and start it, answering the new child and its stdout. *)
|
||||||
|
relaunch : (unit -> int * Unix.file_descr) option;
|
||||||
out : Buffer.t; (* ...buffered until an editor asks for it *)
|
out : Buffer.t; (* ...buffered until an editor asks for it *)
|
||||||
mutable n : int; (* dlopen caches by path: never reuse one *)
|
mutable n : int; (* dlopen caches by path: never reuse one *)
|
||||||
(* Bookkeeping for disassembly, and the reason it can exist at all: the
|
(* Bookkeeping for disassembly, and the reason it can exist at all: the
|
||||||
@ -822,7 +826,8 @@ let build_module (c : Session.change) ~debug ~out =
|
|||||||
spelled once so that every op tells the same story.
|
spelled once so that every op tells the same story.
|
||||||
|
|
||||||
[gone] is what all of them used to say and is now said only where it is
|
[gone] is what all of them used to say and is now said only where it is
|
||||||
true: there is no process left and nothing short of a new one will help.
|
true: there is no process left and nothing short of a new one will help —
|
||||||
|
which, under --two-process, a re-run is.
|
||||||
|
|
||||||
[parked] is the new half, and the sentence it appends is the whole point of
|
[parked] is the new half, and the sentence it appends is the whole point of
|
||||||
the distinction. Somebody reading it has a program that is *there* — its
|
the distinction. Somebody reading it has a program that is *there* — its
|
||||||
@ -833,7 +838,7 @@ let build_module (c : Session.change) ~debug ~out =
|
|||||||
refused for want of a frame boundary and an op refused for want of a stopped
|
refused for want of a frame boundary and an op refused for want of a stopped
|
||||||
stack are refused by the same state for different causes, and a reader who
|
stack are refused by the same state for different causes, and a reader who
|
||||||
cannot tell them apart cannot tell what to do instead. *)
|
cannot tell them apart cannot tell what to do instead. *)
|
||||||
let gone = "the program exited; restart flan dev"
|
let gone = "the program exited; M-x flan-rerun starts it again"
|
||||||
|
|
||||||
let parked_msg why =
|
let parked_msg why =
|
||||||
why
|
why
|
||||||
@ -3658,7 +3663,58 @@ let abort t =
|
|||||||
park, the next request went out while the first run had not started, and the
|
park, the next request went out while the first run had not started, and the
|
||||||
pair of them produced one run — or, a moment later, a refusal saying the
|
pair of them produced one run — or, a moment later, a refusal saying the
|
||||||
program was already running. Both faces are gone with the lag. *)
|
program was already running. Both faces are gone with the lag. *)
|
||||||
|
(* Under --two-process a finished child is gone and there is no thread to
|
||||||
|
wake, so a re-run is a new process: the program is built again from the
|
||||||
|
session as it stands, which puts every accepted redefinition in it from
|
||||||
|
the start, and its globals start over. *)
|
||||||
|
let relaunch_child t relaunch =
|
||||||
|
match liveness t with
|
||||||
|
| Live | Parked ->
|
||||||
|
error
|
||||||
|
(if parked_break t then
|
||||||
|
"the program is stopped at a break, so it cannot be started again \
|
||||||
|
until that ends: resume it or abort it"
|
||||||
|
else
|
||||||
|
"the program is still running; a re-run starts it again in a new \
|
||||||
|
process once this one has finished. Close its window, or let it \
|
||||||
|
finish, and ask again")
|
||||||
|
| Gone ->
|
||||||
|
let s = t.session in
|
||||||
|
(match Session.stale_sites s.Session.built s.Session.program with
|
||||||
|
| (x : Session.stale) :: _ as ss ->
|
||||||
|
error ~loc:(Loc.to_string x.Session.at)
|
||||||
|
(Printf.sprintf
|
||||||
|
"a re-run builds the program again, and %d call%s compiled for a \
|
||||||
|
signature %s function no longer has, starting with %s calling %s \
|
||||||
|
here. Recompile the caller with C-c C-c, or change %s back, and \
|
||||||
|
ask again"
|
||||||
|
(List.length ss)
|
||||||
|
(if List.length ss = 1 then " was" else "s were")
|
||||||
|
(if List.length ss = 1 then "its" else "their")
|
||||||
|
x.Session.caller x.Session.target x.Session.target)
|
||||||
|
| [] ->
|
||||||
|
(match relaunch () with
|
||||||
|
| child, rd ->
|
||||||
|
drain t;
|
||||||
|
(try Unix.close t.stdout with Unix.Unix_error _ -> ());
|
||||||
|
t.stdout <- rd;
|
||||||
|
t.child <- Some child;
|
||||||
|
t.finished <- false;
|
||||||
|
t.died <- None;
|
||||||
|
(* Every body is in the new host now, so no module owns one. *)
|
||||||
|
Hashtbl.reset t.owners;
|
||||||
|
ok
|
||||||
|
[ ":note "
|
||||||
|
^ Wire.quote
|
||||||
|
"started the program again in a new process, built with \
|
||||||
|
every change loaded so far; its globals start over, \
|
||||||
|
because the process is new" ]
|
||||||
|
| exception Failure m -> error m))
|
||||||
|
|
||||||
let rerun t =
|
let rerun t =
|
||||||
|
match t.relaunch with
|
||||||
|
| Some relaunch -> relaunch_child t relaunch
|
||||||
|
| None ->
|
||||||
match liveness t with
|
match liveness t with
|
||||||
| Gone -> error gone
|
| Gone -> error gone
|
||||||
(* A file started with no [main] runs a stub that returns at once; running
|
(* A file started with no [main] runs a stub that returns at once; running
|
||||||
@ -5057,8 +5113,11 @@ let accept_loop ?grace t ls =
|
|||||||
where a session with no editor attached spends its time. *)
|
where a session with no editor attached spends its time. *)
|
||||||
agent_check t;
|
agent_check t;
|
||||||
match liveness t with
|
match liveness t with
|
||||||
| Gone -> ()
|
| Gone when t.relaunch = None -> ()
|
||||||
| (Live | Parked) as live ->
|
| 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. *)
|
||||||
|
let live = if live = Gone then Parked else live in
|
||||||
let idle = Unix.gettimeofday () -. !since in
|
let idle = Unix.gettimeofday () -. !since in
|
||||||
if orphaned ~grace ~served:!served ~idle live then
|
if orphaned ~grace ~served:!served ~idle live then
|
||||||
(* The measured gap and not the threshold it crossed: the threshold is
|
(* The measured gap and not the threshold it crossed: the threshold is
|
||||||
@ -5071,7 +5130,10 @@ let accept_loop ?grace t ls =
|
|||||||
else
|
else
|
||||||
(* The program's pipe is in the same select as the listening socket: it
|
(* The program's pipe is in the same select as the listening socket: it
|
||||||
has to be drained whether or not an editor is asking for anything. *)
|
has to be drained whether or not an editor is asking for anything. *)
|
||||||
match Unix.select [ ls; t.stdout ] [] [] 0.2 with
|
(* Not once it has read EOF: an ended child's pipe is readable for
|
||||||
|
ever, and the loop would spin on it. *)
|
||||||
|
let fds = if t.finished then [ ls ] else [ ls; t.stdout ] in
|
||||||
|
match Unix.select fds [] [] 0.2 with
|
||||||
| [], _, _ -> go ()
|
| [], _, _ -> go ()
|
||||||
| ready, _, _ when not (List.mem ls ready) -> drain t; go ()
|
| ready, _, _ when not (List.mem ls ready) -> drain t; go ()
|
||||||
| _ ->
|
| _ ->
|
||||||
@ -5246,19 +5308,22 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
against each module as it loads either way, but a breakpoint set on a line
|
against each module as it loads either way, but a breakpoint set on a line
|
||||||
in the .flan buffer needs a line table on both sides — the host's to fire
|
in the .flan buffer needs a line table on both sides — the host's to fire
|
||||||
before the first C-c C-c, the module's to follow the reload. *)
|
before the first C-c C-c, the module's to follow the reload. *)
|
||||||
let _, kept =
|
|
||||||
Build.executable
|
|
||||||
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
|
||||||
Build.debug; Build.x86 }
|
|
||||||
~csrcs ~lflags session.Session.host ~out:exe
|
|
||||||
in
|
|
||||||
(* Host and modules are chosen together, which is the whole licence: an
|
(* Host and modules are chosen together, which is the whole licence: an
|
||||||
[--x86] host gets [--x86] modules because one flag set both, and the
|
[--x86] host gets [--x86] modules because one flag set both, and the
|
||||||
source [Build.executable] kept is assembly rather than IR. *)
|
source [Build.executable] kept is assembly rather than IR. *)
|
||||||
let host_ll = Filename.concat dir (if x86 then "host.s" else "host.ll") in
|
let host_ll = Filename.concat dir (if x86 then "host.s" else "host.ll") in
|
||||||
(match kept with
|
let build_host () =
|
||||||
| Some src -> (try Sys.rename src host_ll with Sys_error _ -> ())
|
let _, kept =
|
||||||
| None -> ());
|
Build.executable
|
||||||
|
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
||||||
|
Build.debug; Build.x86 }
|
||||||
|
~csrcs ~lflags session.Session.host ~out:exe
|
||||||
|
in
|
||||||
|
match kept with
|
||||||
|
| Some src -> (try Sys.rename src host_ll with Sys_error _ -> ())
|
||||||
|
| None -> ()
|
||||||
|
in
|
||||||
|
build_host ();
|
||||||
let agent = Filename.concat dir "agent.sock" in
|
let agent = Filename.concat dir "agent.sock" in
|
||||||
|
|
||||||
(* The program's source names some socket path; the daemon is the one that
|
(* The program's source names some socket path; the daemon is the one that
|
||||||
@ -5294,23 +5359,39 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
the pipe it was writing to; that also meant the pipe could never reach
|
the pipe it was writing to; that also meant the pipe could never reach
|
||||||
EOF while the child lived, so "wait for EOF on the daemon's end" was never
|
EOF while the child lived, so "wait for EOF on the daemon's end" was never
|
||||||
the mechanism it looked like it could be. *)
|
the mechanism it looked like it could be. *)
|
||||||
let rd, wr = Unix.pipe ~cloexec:true () in
|
let spawn () =
|
||||||
let child = Unix.create_process exe [| exe |] Unix.stdin wr Unix.stderr in
|
(* A socket file a previous child left behind would answer the wait below
|
||||||
Unix.close wr;
|
before this child has bound anything. *)
|
||||||
Unix.set_nonblock rd;
|
(try Unix.unlink agent with Unix.Unix_error _ -> ());
|
||||||
|
let rd, wr = Unix.pipe ~cloexec:true () in
|
||||||
(* Wait for it to bind before accepting an evaluation. One that arrives first
|
let child = Unix.create_process exe [| exe |] Unix.stdin wr Unix.stderr in
|
||||||
would fail for a reason that reads like a compiler bug. *)
|
Unix.close wr;
|
||||||
if not (await (fun () -> Sys.file_exists agent)) then begin
|
Unix.set_nonblock rd;
|
||||||
(try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ());
|
(* Wait for it to bind before accepting an evaluation. One that arrives
|
||||||
failwith
|
first would fail for a reason that reads like a compiler bug. *)
|
||||||
("the program did not open its agent socket at " ^ agent
|
if not (await (fun () -> Sys.file_exists agent)) then begin
|
||||||
^ ". Under --two-process every edit reaches the program through that \
|
(try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ());
|
||||||
socket.")
|
(try Unix.close rd with Unix.Unix_error _ -> ());
|
||||||
end;
|
failwith
|
||||||
|
("the program did not open its agent socket at " ^ agent
|
||||||
|
^ ". Under --two-process every edit reaches the program through that \
|
||||||
|
socket.")
|
||||||
|
end;
|
||||||
|
(child, rd)
|
||||||
|
in
|
||||||
|
let child, rd = spawn () in
|
||||||
|
(* A re-run: the host is built again from the session as it stands, so
|
||||||
|
every redefinition accepted so far is in the new process from its first
|
||||||
|
instruction rather than delivered to it later. *)
|
||||||
|
let relaunch () =
|
||||||
|
Session.rehost session;
|
||||||
|
build_host ();
|
||||||
|
spawn ()
|
||||||
|
in
|
||||||
|
|
||||||
let t =
|
let t =
|
||||||
{ session; child = Some child; agent; dir; stdout = rd;
|
{ session; child = Some child; agent; dir; stdout = rd;
|
||||||
|
relaunch = Some relaunch;
|
||||||
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
||||||
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
||||||
park_noted = false; died = None; dropped = 0 }
|
park_noted = false; died = None; dropped = 0 }
|
||||||
@ -5324,9 +5405,12 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
((Unix.gettimeofday () -. t0) *. 1000.);
|
((Unix.gettimeofday () -. t0) *. 1000.);
|
||||||
Fun.protect
|
Fun.protect
|
||||||
~finally:(fun () ->
|
~finally:(fun () ->
|
||||||
(try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ());
|
(match t.child with
|
||||||
|
| Some child ->
|
||||||
|
(try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ())
|
||||||
|
| None -> ());
|
||||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||||
(try Unix.close rd with Unix.Unix_error _ -> ());
|
(try Unix.close t.stdout with Unix.Unix_error _ -> ());
|
||||||
(try Unix.unlink sock with Unix.Unix_error _ -> ()))
|
(try Unix.unlink sock with Unix.Unix_error _ -> ()))
|
||||||
(fun () -> accept_loop t ls);
|
(fun () -> accept_loop t ls);
|
||||||
(* Here only when the loop returned: an exception out of it has already
|
(* Here only when the loop returned: an exception out of it has already
|
||||||
@ -6169,7 +6253,7 @@ let merged_setup () =
|
|||||||
with Unix.Unix_error _ -> Sys.executable_name
|
with Unix.Unix_error _ -> Sys.executable_name
|
||||||
in
|
in
|
||||||
let t =
|
let t =
|
||||||
{ session; child = None; agent; dir; stdout = rd;
|
{ session; child = None; agent; dir; stdout = rd; relaunch = None;
|
||||||
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
out = Buffer.create 4096; n = 0; gen = 0; owners = Hashtbl.create 32;
|
||||||
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
host_ll; host_exe = exe; finished = false; agent_watch = None;
|
||||||
park_noted = false; died = None; dropped = 0 }
|
park_noted = false; died = None; dropped = 0 }
|
||||||
|
|||||||
@ -98,6 +98,5 @@ let rerun ?(stopped = false) () =
|
|||||||
its window, or let it finish, and ask again"
|
its window, or let it finish, and ask again"
|
||||||
| _ ->
|
| _ ->
|
||||||
Error
|
Error
|
||||||
"this session's program is a process of its own, so there is no parked \
|
"this process has no program thread of its own, so there is nothing \
|
||||||
thread here to send round again; it is the merged build that can re-run \
|
here to run again"
|
||||||
a program, not --two-process"
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ type t = {
|
|||||||
mutable decls : Ast.decl list; (* post-Load: flat, one namespace *)
|
mutable decls : Ast.decl list; (* post-Load: flat, one namespace *)
|
||||||
mutable program : Tast.program; (* the last thing that checked *)
|
mutable program : Tast.program; (* the last thing that checked *)
|
||||||
mutable env : Check.env; (* the same, as the checker sees it *)
|
mutable env : Check.env; (* the same, as the checker sees it *)
|
||||||
host : Tast.program; (* what the process was built from *)
|
mutable host : Tast.program; (* what the process was built from *)
|
||||||
pkgs : Load.pkg list; (* alias, directory, names owned *)
|
pkgs : Load.pkg list; (* alias, directory, names owned *)
|
||||||
(* Every [defmacro] this session can expand a call to: the imports', under
|
(* Every [defmacro] this session can expand a call to: the imports', under
|
||||||
their aliases, and the buffer's own, under the names the buffer writes.
|
their aliases, and the buffer's own, under the names the buffer writes.
|
||||||
@ -782,6 +782,13 @@ let restore t h =
|
|||||||
newest one and no older activation is left running. *)
|
newest one and no older activation is left running. *)
|
||||||
let rerun t = t.live <- SM.empty
|
let rerun t = t.live <- SM.empty
|
||||||
|
|
||||||
|
(* The process is about to be built again from what the session holds now
|
||||||
|
(a --two-process re-run), so that becomes what it was built from. *)
|
||||||
|
let rehost t =
|
||||||
|
t.host <- t.program;
|
||||||
|
t.built <- record_built t.env t.program t.program.Tast.fns SM.empty;
|
||||||
|
t.live <- SM.empty
|
||||||
|
|
||||||
(* [forms], when given, are [src] already read — [pruned] runs this over a
|
(* [forms], when given, are [src] already read — [pruned] runs this over a
|
||||||
file a form fewer each round and has no text for the subset. [base] is the
|
file a form fewer each round and has no text for the subset. [base] is the
|
||||||
file an [(import ...)] in them is resolved against, the session's own when
|
file an [(import ...)] in them is resolved against, the session's own when
|
||||||
|
|||||||
@ -5121,20 +5121,69 @@ let () =
|
|||||||
ignore (ask "(:op \"describe\")");
|
ignore (ask "(:op \"describe\")");
|
||||||
contains_sub (Buffer.contents seen) "42"))
|
contains_sub (Buffer.contents seen) "42"))
|
||||||
then fail "--two-process: the reload was never installed";
|
then fail "--two-process: the reload was never installed";
|
||||||
(* And the one verb this shape cannot have. Running [main] again means
|
(* A re-run here is a new process. Refused while the child runs; once
|
||||||
waking a thread that parked inside this process, and here the program
|
it has finished, the program is built again from the session, so the
|
||||||
is a child: when it finishes it is gone, and there is nothing to wake.
|
redefined [step] is what the new run's first line prints — the host
|
||||||
Refused by naming what this daemon is rather than with the message a
|
the daemon started with would print 1. *)
|
||||||
merged one gives, because "the program is already running" would send
|
|
||||||
somebody back to try again after it had exited — and [--x86] arrives
|
|
||||||
here too, since it refuses the merged daemon for the -rdynamic reason
|
|
||||||
given below. *)
|
|
||||||
let r = ask "(:op \"rerun\")" in
|
let r = ask "(:op \"rerun\")" in
|
||||||
let why = Option.value ~default:(status r) (Wire.string_field r "message") in
|
let why = Option.value ~default:(status r) (Wire.string_field r "message") in
|
||||||
if status r <> "error" then
|
if status r <> "error" || not (contains_sub why "still running") then
|
||||||
fail "--two-process answered a rerun it cannot perform"
|
fail "--two-process: a rerun while the child runs answered %s: %s"
|
||||||
else if not (contains_sub why "two-process") then
|
(status r) why;
|
||||||
fail "--two-process refuses a rerun as: %s" why;
|
(* Two more deliveries take the program past its last two waits. *)
|
||||||
|
List.iter
|
||||||
|
(fun n ->
|
||||||
|
let r =
|
||||||
|
ask
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"eval\" :code \"(defn step [] i64 %d)\" \
|
||||||
|
:file \"/tmp/buf.flan\")" n)
|
||||||
|
in
|
||||||
|
if status r <> "ok" then fail "--two-process: eval %d was refused" n;
|
||||||
|
if not
|
||||||
|
(await (fun () ->
|
||||||
|
ignore (ask "(:op \"describe\")");
|
||||||
|
contains_sub (Buffer.contents seen) (string_of_int n)))
|
||||||
|
then fail "--two-process: %d was never installed" n)
|
||||||
|
[ 43; 44 ];
|
||||||
|
(* 44 was the old child's last line, so anything from here on is the
|
||||||
|
new child's. *)
|
||||||
|
Buffer.clear seen;
|
||||||
|
let taken = ref (ask "(:op \"describe\")") in
|
||||||
|
if not
|
||||||
|
(await ~ms:10000 (fun () ->
|
||||||
|
taken := ask "(:op \"rerun\")";
|
||||||
|
status !taken = "ok"))
|
||||||
|
then
|
||||||
|
fail "--two-process: a rerun after the child finished: %s"
|
||||||
|
(Option.value ~default:(status !taken)
|
||||||
|
(Wire.string_field !taken "message"))
|
||||||
|
else begin
|
||||||
|
let note = Option.value ~default:"" (Wire.string_field !taken "note") in
|
||||||
|
if not (contains_sub note "globals start over") then
|
||||||
|
fail "--two-process: the rerun's note does not say the globals \
|
||||||
|
start over: %S" note;
|
||||||
|
if not
|
||||||
|
(await (fun () ->
|
||||||
|
ignore (ask "(:op \"describe\")");
|
||||||
|
contains_sub (Buffer.contents seen) "\n"))
|
||||||
|
then fail "--two-process: the new child printed nothing"
|
||||||
|
else if not (String.starts_with ~prefix:"44\n" (Buffer.contents seen))
|
||||||
|
then
|
||||||
|
fail "--two-process: the new child did not start with the \
|
||||||
|
redefinition: %S" (Buffer.contents seen);
|
||||||
|
(* And it is reachable: a delivery to the new child installs. *)
|
||||||
|
let r =
|
||||||
|
ask
|
||||||
|
"(:op \"eval\" :code \"(defn step [] i64 45)\" :file \"/tmp/buf.flan\")"
|
||||||
|
in
|
||||||
|
if status r <> "ok" then fail "--two-process: eval after rerun refused";
|
||||||
|
if not
|
||||||
|
(await (fun () ->
|
||||||
|
ignore (ask "(:op \"describe\")");
|
||||||
|
contains_sub (Buffer.contents seen) "45"))
|
||||||
|
then fail "--two-process: the new child never installed a delivery"
|
||||||
|
end;
|
||||||
ignore (ask "(:op \"close\")");
|
ignore (ask "(:op \"close\")");
|
||||||
Unix.close tc
|
Unix.close tc
|
||||||
end;
|
end;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user