Merge: all six traps park for inspection, two with the resume refused
This commit is contained in:
commit
e6d85c80dd
@ -139,10 +139,23 @@ The containers, strings/UTF-8, sequences, random, and printing layers are decent
|
|||||||
- **`abort()` in the dev runtime** — `flan_dev.c:47-50`, `:103`: reload-name-table
|
- **`abort()` in the dev runtime** — `flan_dev.c:47-50`, `:103`: reload-name-table
|
||||||
exhaustion, intern OOM, and "global changed size" kill the game instead of signalling.
|
exhaustion, intern OOM, and "global changed size" kill the game instead of signalling.
|
||||||
Against the grain of everything else in the runtime; route through `flan_error`.
|
Against the grain of everything else in the runtime; route through `flan_error`.
|
||||||
- **Six trap paths bypass `flan_exit_hook`** and end a merged `flan dev` session
|
- ~~**Six trap paths bypass `flan_exit_hook`** and end a merged `flan dev` session~~ —
|
||||||
(`flan_restart_fail`, `flan_restart_args_fail`, `flan_restart_unarmed`,
|
fixed 2026-09-18. Two corrections to the entry as written. The hook bounds and
|
||||||
`flan_transfer_fail`, `flan_null_alloc_fail`, `flan_free_all_fail` → `rt_die`). Bounds
|
arithmetic park through is `flan_break_hook`, not `flan_exit_hook` — that second one is
|
||||||
and arithmetic already park via the break-loop hook; these should too.
|
normal termination, the one `main` reaches when it ends. And the six could not be routed
|
||||||
|
through it as it stands: `flan_break_hook`'s contract is that the loop may answer by
|
||||||
|
aiming a transfer channel, and these six are called by emitted code that falls off the
|
||||||
|
end with no channel in the call at all, so a restart chosen against one would be accepted
|
||||||
|
and silently dropped. So there is a second hook, `flan_trap_hook`, and a break that
|
||||||
|
refuses the resume with a reason rather than a process that exits before anyone can ask
|
||||||
|
a question. All six park, for two reasons rather than one. Four are guards that fire
|
||||||
|
*before* the operation they guard (`if (!a)` and the capability test both precede
|
||||||
|
`a->proc(...)`), so nothing is half done. The other two — `flan_transfer_fail` and
|
||||||
|
`flan_restart_unarmed` — fire mid-transfer, with this frame's defers possibly half run,
|
||||||
|
and park only to be *looked at*: stopping on a torn unwind is strictly more than exiting
|
||||||
|
before anyone can ask what tore it. Standalone is unchanged: the
|
||||||
|
hook is null in a program that did not import the agent, and the trap still exits 134
|
||||||
|
with the same sentence.
|
||||||
- **Unchecked `malloc` in `flan_argv`** (`flan_rt.c:162`) — the one in the file.
|
- **Unchecked `malloc` in `flan_argv`** (`flan_rt.c:162`) — the one in the file.
|
||||||
- ~~**`v->gen` stale-slice word is maintained and never consulted**~~ — deleted
|
- ~~**`v->gen` stale-slice word is maintained and never consulted**~~ — deleted
|
||||||
2026-09-18 with the second round of the repeal; the header is five words now.
|
2026-09-18 with the second round of the repeal; the header is five words now.
|
||||||
|
|||||||
@ -527,6 +527,52 @@ _Noreturn void flan_bounds_fail(const uint8_t *loc, int64_t loclen,
|
|||||||
void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition,
|
void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition,
|
||||||
void *xfer);
|
void *xfer);
|
||||||
|
|
||||||
|
/* The other half of that, for the traps that have nowhere to resume *to*.
|
||||||
|
*
|
||||||
|
* Six refusals in this file are reached with no transfer channel in their
|
||||||
|
* hands: the emitted code calls them and falls off the end, because the
|
||||||
|
* checker has already decided the program is not going to continue. Under a
|
||||||
|
* merged `flan dev' that decision was being made on the program's behalf and
|
||||||
|
* charged to the *session*, since rt_die is [_exit] and the compiler is in the
|
||||||
|
* same process — a program that named a restart nobody established took the
|
||||||
|
* daemon down with it, which is exactly what the break loop exists to stop.
|
||||||
|
*
|
||||||
|
* So they park instead. Not with [flan_break_hook]: that hook's contract is
|
||||||
|
* that the loop may answer by aiming the channel, and there is no channel
|
||||||
|
* here — a restart chosen against one of these would be a transfer nothing
|
||||||
|
* carries out, which is the "accepted and silently dropped" shape the
|
||||||
|
* thunk-boundary refusal in flan_agent.c already went to some length to avoid.
|
||||||
|
* This hook says the other thing: stop here, let everything be read, and
|
||||||
|
* refuse a resume with a reason when one is asked for. The loop behind it
|
||||||
|
* never returns; if some future one does, the [rt_die] below is still the
|
||||||
|
* answer, which is also what a standalone build does every time, because
|
||||||
|
* nothing installs this outside a dev session.
|
||||||
|
*
|
||||||
|
* All six park, and they do not all park for the same reason. Four are guards
|
||||||
|
* that fire *before* the thing they guard — the null and capability tests
|
||||||
|
* above [a->proc], and the two restart lookups — so nothing is half done and
|
||||||
|
* the frame is as readable as any other. The other two are not: a transfer is
|
||||||
|
* already under way at [flan_transfer_fail] and at [flan_restart_unarmed],
|
||||||
|
* and this frame's defers may be half run, which is what the note on
|
||||||
|
* flan_transfer_fail says a few lines down. Those two park anyway, and only
|
||||||
|
* to be *looked at*: a half-run unwind is a thing worth seeing, and the
|
||||||
|
* resume is refused there for the same reason it is refused at the other
|
||||||
|
* four. Stopping on a torn transfer is strictly more than exiting before
|
||||||
|
* anyone could ask what tore it.
|
||||||
|
*
|
||||||
|
* The name is a trap's name and not a condition's — there is no defstruct
|
||||||
|
* behind [NullAllocator], and the `layout' op will say it cannot place it.
|
||||||
|
* That is already a case the conditions buffer draws (see flan-cnr.el, which
|
||||||
|
* treats a refusal as a reason to show rather than an error): the name is
|
||||||
|
* there to say *which* trap the program is standing in, and the sentence each
|
||||||
|
* site printed just above carries the detail. */
|
||||||
|
void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
|
||||||
|
|
||||||
|
static _Noreturn void rt_trap(const uint8_t *name, int64_t namelen) {
|
||||||
|
if (flan_trap_hook != NULL) flan_trap_hook(name, namelen);
|
||||||
|
rt_die();
|
||||||
|
}
|
||||||
|
|
||||||
/* Must agree with Check.type_id, byte for byte, or a name typed at the break
|
/* Must agree with Check.type_id, byte for byte, or a name typed at the break
|
||||||
* loop matches nothing. FNV-1a over the name, 32 bits. */
|
* loop matches nothing. FNV-1a over the name, 32 bits. */
|
||||||
static uint32_t flan_name_id(const uint8_t *s, int64_t n) {
|
static uint32_t flan_name_id(const uint8_t *s, int64_t n) {
|
||||||
@ -566,13 +612,15 @@ void flan_error(uint32_t type_id, void *condition, void *xfer,
|
|||||||
|
|
||||||
/* Nothing on the restart stack offers the name. It is reported where the
|
/* Nothing on the restart stack offers the name. It is reported where the
|
||||||
* invoke was, because that is the only place that knows what was asked for;
|
* invoke was, because that is the only place that knows what was asked for;
|
||||||
* there is nowhere to resume, so there is nothing else to do. */
|
* there is nowhere to resume *to*, which is not the same as there being
|
||||||
|
* nothing else to do — see rt_trap, which stops here instead of ending the
|
||||||
|
* process, so that the stack that offered no such name can be read. */
|
||||||
_Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen,
|
_Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen,
|
||||||
const uint8_t *name, int64_t namelen) {
|
const uint8_t *name, int64_t namelen) {
|
||||||
rt_flush_out();
|
rt_flush_out();
|
||||||
fprintf(stderr, "%.*s: no restart named %.*s is active\n",
|
fprintf(stderr, "%.*s: no restart named %.*s is active\n",
|
||||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name);
|
(int)loclen, (const char *)loc, (int)namelen, (const char *)name);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"NoSuchRestart", 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The frame the name found does not take these arguments — spec-conditions.md
|
/* The frame the name found does not take these arguments — spec-conditions.md
|
||||||
@ -588,7 +636,7 @@ _Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen,
|
|||||||
fprintf(stderr, "%.*s: restart %.*s takes %.*s, given %.*s\n",
|
fprintf(stderr, "%.*s: restart %.*s takes %.*s, given %.*s\n",
|
||||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
||||||
(int)wantlen, (const char *)want, (int)gotlen, (const char *)got);
|
(int)wantlen, (const char *)want, (int)gotlen, (const char *)got);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"RestartArity", 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A clause with parameters was reached by a transfer that filled none of them
|
/* A clause with parameters was reached by a transfer that filled none of them
|
||||||
@ -606,7 +654,7 @@ _Noreturn void flan_restart_unarmed(const uint8_t *loc, int64_t loclen,
|
|||||||
"break loop yet\n",
|
"break loop yet\n",
|
||||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
||||||
(int)wantlen, (const char *)want);
|
(int)wantlen, (const char *)want);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"RestartUnarmed", 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Something a defer called invoked a restart. A defer is the cleanup a
|
/* Something a defer called invoked a restart. A defer is the cleanup a
|
||||||
@ -620,7 +668,7 @@ _Noreturn void flan_transfer_fail(const uint8_t *loc, int64_t loclen) {
|
|||||||
"%.*s: a defer invoked a restart, which a defer may not do — it is "
|
"%.*s: a defer invoked a restart, which a defer may not do — it is "
|
||||||
"the cleanup a transfer runs on its way out\n",
|
"the cleanup a transfer runs on its way out\n",
|
||||||
(int)loclen, (const char *)loc);
|
(int)loclen, (const char *)loc);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"TransferFromDefer", 17);
|
||||||
}
|
}
|
||||||
|
|
||||||
_Noreturn void flan_slice_fail(const uint8_t *loc, int64_t loclen,
|
_Noreturn void flan_slice_fail(const uint8_t *loc, int64_t loclen,
|
||||||
@ -1311,7 +1359,7 @@ _Noreturn void flan_null_alloc_fail(const uint8_t *loc, int64_t loclen) {
|
|||||||
"%.*s: this allocator is null — a zeroed Allocator was never given "
|
"%.*s: this allocator is null — a zeroed Allocator was never given "
|
||||||
"one\n",
|
"one\n",
|
||||||
(int)loclen, (const char *)loc);
|
(int)loclen, (const char *)loc);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"NullAllocator", 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
_Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
_Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
||||||
@ -1321,7 +1369,7 @@ _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
|||||||
"release, and releasing nothing is not the same as releasing "
|
"release, and releasing nothing is not the same as releasing "
|
||||||
"everything\n",
|
"everything\n",
|
||||||
(int)loclen, (const char *)loc);
|
(int)loclen, (const char *)loc);
|
||||||
rt_die();
|
rt_trap((const uint8_t *)"NoFreeAll", 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── The region requirement, spec-memory.md's arena rule ───────────────
|
/* ── The region requirement, spec-memory.md's arena rule ───────────────
|
||||||
|
|||||||
34
test/programs/dev-trap-free-all.flan
Normal file
34
test/programs/dev-trap-free-all.flan
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
;;;; A program that stops on a trap it cannot be resumed from, for driving the
|
||||||
|
;;;; break loop over one.
|
||||||
|
;;;;
|
||||||
|
;;;; dev-break-bounds.flan is the case where stopping and *resuming* both work:
|
||||||
|
;;;; a bad index signals BoundsError, the walk finds nothing, and the break
|
||||||
|
;;;; loop hands the program back to its own `continue`. This is the other half.
|
||||||
|
;;;; `free-all` on an allocator that has no region to release is a refusal the
|
||||||
|
;;;; emitted code makes no channel for — it calls the trap and falls off the
|
||||||
|
;;;; end — so there is nothing for a chosen restart to transfer into, and until
|
||||||
|
;;;; now that meant `_exit(134)`, which under a merged `flan dev` is the
|
||||||
|
;;;; session and the compiler as well as the program.
|
||||||
|
;;;;
|
||||||
|
;;;; The claim is that the session survives it anyway. The program stops where
|
||||||
|
;;;; it erred, everything is readable, and the *resume* is the only thing
|
||||||
|
;;;; refused — with a sentence saying why, rather than by the process being
|
||||||
|
;;;; gone before anyone could ask.
|
||||||
|
;;;;
|
||||||
|
;;;; The `restart-case` is load-bearing and not scenery: with no restart on the
|
||||||
|
;;;; stack the break loop would say "no restarts are active" and the refusal
|
||||||
|
;;;; that had to be written would never run. `continue` is live, is listed, and
|
||||||
|
;;;; is still not takeable — which is exactly the state this trap leaves a
|
||||||
|
;;;; program in.
|
||||||
|
(import agent "vendor:agent")
|
||||||
|
|
||||||
|
(defn release [] ()
|
||||||
|
;; The heap allocator frees one block and owns no region, so this traps.
|
||||||
|
(free-all (heap-allocator)))
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(agent/start "/tmp/flan-dev-trap-free-all-fallback.sock")
|
||||||
|
(restart-case
|
||||||
|
(release)
|
||||||
|
(continue [] (println "resumed")))
|
||||||
|
0)
|
||||||
18
test/programs/dev-trap-null-alloc.flan
Normal file
18
test/programs/dev-trap-null-alloc.flan
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
;;;; The same claim as dev-trap-free-all.flan over the other allocator trap,
|
||||||
|
;;;; and with nothing on the restart stack.
|
||||||
|
;;;;
|
||||||
|
;;;; `nowhere` is a zeroed `Allocator` — a `defvar` nobody assigned — and
|
||||||
|
;;;; `free-all` on it is the "I released the region" / "I never made one"
|
||||||
|
;;;; collapse the runtime refuses to let a program spell the same way. The
|
||||||
|
;;;; refusal is a trap with no transfer channel, so it parks rather than
|
||||||
|
;;;; resumes, and here there is not even a restart to list: the break loop says
|
||||||
|
;;;; so and stands still, which is the whole difference between a session you
|
||||||
|
;;;; can fix the program in and a daemon that is gone.
|
||||||
|
(import agent "vendor:agent")
|
||||||
|
|
||||||
|
(defvar nowhere Allocator)
|
||||||
|
|
||||||
|
(defn main [] i32
|
||||||
|
(agent/start "/tmp/flan-dev-trap-null-alloc-fallback.sock")
|
||||||
|
(free-all nowhere)
|
||||||
|
0)
|
||||||
149
test/test_dev.ml
149
test/test_dev.ml
@ -1120,6 +1120,155 @@ let () =
|
|||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
(* ── A break over a trap with no way back ─────────────── *)
|
||||||
|
|
||||||
|
(* The block above stops on a bad index and *resumes*: the signal walks the
|
||||||
|
handlers, reaches the break loop, and the program's own [continue]
|
||||||
|
carries it out. Six refusals in the runtime cannot be reached that way.
|
||||||
|
[flan_restart_fail], [flan_restart_args_fail], [flan_restart_unarmed],
|
||||||
|
[flan_transfer_fail], [flan_null_alloc_fail] and [flan_free_all_fail]
|
||||||
|
are called by emitted code that then falls off the end, with no transfer
|
||||||
|
channel anywhere in the call — so there is nothing for a chosen restart
|
||||||
|
to land in, and every one of them called [_exit(134)]. In a merged
|
||||||
|
[flan dev] that is one process, and the session went with the program.
|
||||||
|
|
||||||
|
Two of the six are driven here, and the pair is chosen so that the
|
||||||
|
refusal has something to refuse: [dev-trap-free-all.flan] traps inside a
|
||||||
|
live [restart-case], so [continue] is on the stack, is listed, and is
|
||||||
|
still not takeable; [dev-trap-null-alloc.flan] traps with an empty
|
||||||
|
restart stack, which is the other shape the break loop has to print.
|
||||||
|
|
||||||
|
What is asserted is the difference the change is about. The program is
|
||||||
|
*stopped* and not gone — [describe] answers, and answers [:stopped] — an
|
||||||
|
expression still evaluates while it stands there, which is the proof the
|
||||||
|
daemon is alive and serving, and a resume is refused *with a reason*
|
||||||
|
rather than by the process having exited before anyone could ask. The
|
||||||
|
standalone half of the same claim is test_acceptance.ml's
|
||||||
|
free-all-refused, which still exits 134: nothing installs the hook in a
|
||||||
|
program that did not import the agent. *)
|
||||||
|
let trap_park what prog cond restarts =
|
||||||
|
let tsock = tmp (prog ^ ".sock") and tout = tmp (prog ^ ".out") in
|
||||||
|
(try Sys.remove tsock with Sys_error _ -> ());
|
||||||
|
let tfd =
|
||||||
|
Unix.openfile tout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||||
|
in
|
||||||
|
let tpid =
|
||||||
|
Unix.create_process flan
|
||||||
|
[| flan; "dev"; "programs/" ^ prog; "-s"; tsock |]
|
||||||
|
Unix.stdin tfd Unix.stderr
|
||||||
|
in
|
||||||
|
Unix.close tfd;
|
||||||
|
if not (listening ~pid:tpid tsock) then begin
|
||||||
|
fail "the %s trap daemon %s" what !listen_why;
|
||||||
|
(try Unix.kill tpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
else begin
|
||||||
|
let c = connect tsock in
|
||||||
|
let ask sexp = Wire.parse (Wire.send c sexp; Wire.recv c) in
|
||||||
|
let stopped r =
|
||||||
|
match Wire.field r "stopped" with
|
||||||
|
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
|
let last = ref (ask "(:op \"describe\")") in
|
||||||
|
if not
|
||||||
|
(await (fun () -> last := ask "(:op \"describe\")"; stopped !last))
|
||||||
|
then fail "the %s trap never stopped the program, it ended it" what
|
||||||
|
else begin
|
||||||
|
(* Which trap, by name. There is no [defstruct] behind these — they
|
||||||
|
are traps and not conditions, and [layout] will say it cannot
|
||||||
|
place the name, which flan-cnr.el already draws as a reason. The
|
||||||
|
name is here to say where the program is standing. *)
|
||||||
|
let got =
|
||||||
|
match Wire.string_field !last "condition" with
|
||||||
|
| Some c -> c
|
||||||
|
| None -> ""
|
||||||
|
in
|
||||||
|
if got <> cond then
|
||||||
|
fail "the %s trap is reported as %S, wanted %S" what got cond;
|
||||||
|
(* What is on offer, which at a trap is a list nobody can take. It
|
||||||
|
is still *listed*: "why can I not have that one" is a fair
|
||||||
|
question, and an empty list would make a live restart-case look
|
||||||
|
like it had been unwound. *)
|
||||||
|
let r = ask "(:op \"break\")" in
|
||||||
|
if status r <> "ok" then fail "break at the %s trap: %s" what (status r);
|
||||||
|
(match Wire.field r "restarts" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
let names =
|
||||||
|
List.filter_map
|
||||||
|
(fun (n : Form.t) ->
|
||||||
|
match n.Form.v with Form.Str x -> Some x | _ -> None)
|
||||||
|
l
|
||||||
|
in
|
||||||
|
if names <> restarts then
|
||||||
|
fail "restarts at the %s trap: %s" what
|
||||||
|
(String.concat ", " names)
|
||||||
|
| _ -> fail "break at the %s trap listed no restarts field" what);
|
||||||
|
(* And every one of them named as untakeable. This is the half the
|
||||||
|
editor reads: the terminal listing says so in words, and a
|
||||||
|
[:unreachable] that disagreed with it would be the two ends
|
||||||
|
describing different programs — a restart shown as choosable,
|
||||||
|
chosen, and then refused. *)
|
||||||
|
(match Wire.field r "unreachable" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
let idx =
|
||||||
|
List.filter_map
|
||||||
|
(fun (n : Form.t) ->
|
||||||
|
match n.Form.v with Form.Int i -> Some (Int64.to_int i) | _ -> None)
|
||||||
|
l
|
||||||
|
in
|
||||||
|
if idx <> List.mapi (fun i _ -> i) restarts then
|
||||||
|
fail "unreachable restarts at the %s trap: %s" what
|
||||||
|
(String.concat ", " (List.map string_of_int idx))
|
||||||
|
| _ ->
|
||||||
|
if restarts <> [] then
|
||||||
|
fail "break at the %s trap named no unreachable restarts" what);
|
||||||
|
(* And the refusal, where there is a name to refuse. Answered [err]
|
||||||
|
with the reason rather than [ok] and then dropped, which is the
|
||||||
|
shape that would tell an editor the program had resumed when it
|
||||||
|
had not. *)
|
||||||
|
List.iter
|
||||||
|
(fun name ->
|
||||||
|
let r =
|
||||||
|
ask (Printf.sprintf "(:op \"restart\" :name %s)"
|
||||||
|
(Wire.quote name))
|
||||||
|
in
|
||||||
|
if status r = "ok" then
|
||||||
|
fail "the %s trap accepted a restart it cannot take" what)
|
||||||
|
restarts;
|
||||||
|
(* The session, still a session. This is the whole point: the daemon
|
||||||
|
is answering, the compiler is in this process, and the program is
|
||||||
|
standing still in front of it. *)
|
||||||
|
let r =
|
||||||
|
ask "(:op \"eval-expr\" :code \"(+ 2 2)\" :file \"/tmp/buf.flan\")"
|
||||||
|
in
|
||||||
|
if Wire.string_field r "value" <> Some "4" then
|
||||||
|
fail "an expression while parked at the %s trap: %s" what
|
||||||
|
(Option.value ~default:(status r) (Wire.string_field r "message"));
|
||||||
|
(* Torn down by [abort], not by [close]: a trap parks for good, so
|
||||||
|
there is no resume to wait for and nothing to gain by waiting. *)
|
||||||
|
let r = ask "(:op \"abort\")" in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "abort at the %s trap: %s" what
|
||||||
|
(Option.value ~default:"" (Wire.string_field r "message"))
|
||||||
|
end;
|
||||||
|
Unix.close c;
|
||||||
|
if not
|
||||||
|
(await ~ms:5000 (fun () ->
|
||||||
|
match Unix.waitpid [ Unix.WNOHANG ] tpid with
|
||||||
|
| 0, _ -> false
|
||||||
|
| _ -> true
|
||||||
|
| exception Unix.Unix_error _ -> true))
|
||||||
|
then begin
|
||||||
|
fail "the daemon outlived the %s trap it aborted" what;
|
||||||
|
(try Unix.kill tpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||||
|
(try ignore (Unix.waitpid [] tpid) with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
in
|
||||||
|
trap_park "free-all" "dev-trap-free-all.flan" "NoFreeAll" [ "continue" ];
|
||||||
|
trap_park "null allocator" "dev-trap-null-alloc.flan" "NullAllocator" [];
|
||||||
|
|
||||||
(* ── The locals of a stopped frame ─────────────────────────────── *)
|
(* ── The locals of a stopped frame ─────────────────────────────── *)
|
||||||
|
|
||||||
(* A third daemon, over a program that stops with something worth looking
|
(* A third daemon, over a program that stops with something worth looking
|
||||||
|
|||||||
93
vendor/agent/flan_agent.c
vendored
93
vendor/agent/flan_agent.c
vendored
@ -255,6 +255,10 @@ static int publish(job j) {
|
|||||||
* runtime must not depend on an optional package for either. */
|
* runtime must not depend on an optional package for either. */
|
||||||
extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen,
|
extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen,
|
||||||
void *condition, void *xfer);
|
void *condition, void *xfer);
|
||||||
|
/* The other hook, for the traps that call with no channel — see rt_trap in
|
||||||
|
* flan_rt.c, which carries the argument for why they are two hooks and not
|
||||||
|
* one. This end of it is [trap_stop] below. */
|
||||||
|
extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
|
||||||
extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen,
|
extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen,
|
||||||
void *xfer);
|
void *xfer);
|
||||||
extern int32_t flan_restart_count(void);
|
extern int32_t flan_restart_count(void);
|
||||||
@ -354,6 +358,15 @@ static _Atomic int aborting;
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t gen; /* never reused, never 0 */
|
int32_t gen; /* never reused, never 0 */
|
||||||
|
/* Whether *any* restart on this list can be taken, which is a property of
|
||||||
|
* the break and not of the restarts. [reachable] answers a different
|
||||||
|
* question — that one is per restart, and it is about the thunk boundary.
|
||||||
|
* A break taken by a trap with no transfer channel has a perfectly good
|
||||||
|
* list of live restarts below it and no way to aim at one, so the whole
|
||||||
|
* snapshot is marked instead of each entry: the refusal sentence differs,
|
||||||
|
* and a reader of this struct should not have to infer which case a run of
|
||||||
|
* zeroes in [reachable] meant. */
|
||||||
|
int32_t resumable;
|
||||||
int32_t n;
|
int32_t n;
|
||||||
int32_t total; /* before SNAP_MAX truncated it */
|
int32_t total; /* before SNAP_MAX truncated it */
|
||||||
void *frame[SNAP_MAX];
|
void *frame[SNAP_MAX];
|
||||||
@ -425,12 +438,13 @@ static snapshot *snap_top(void) {
|
|||||||
* to nest, which the caller reports rather than serving a stale one. */
|
* to nest, which the caller reports rather than serving a stale one. */
|
||||||
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
|
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
|
||||||
|
|
||||||
static int snap_push(void) {
|
static int snap_push(int resumable) {
|
||||||
int d = atomic_load(&snap_depth);
|
int d = atomic_load(&snap_depth);
|
||||||
if (d >= BREAK_MAX) return 0;
|
if (d >= BREAK_MAX) return 0;
|
||||||
snapshot *s = &snaps[d];
|
snapshot *s = &snaps[d];
|
||||||
int32_t n = flan_restart_count();
|
int32_t n = flan_restart_count();
|
||||||
s->gen = ++snap_gen;
|
s->gen = ++snap_gen;
|
||||||
|
s->resumable = resumable;
|
||||||
s->total = n;
|
s->total = n;
|
||||||
s->used = 0;
|
s->used = 0;
|
||||||
s->n = 0;
|
s->n = 0;
|
||||||
@ -536,8 +550,16 @@ static _Noreturn void die_now(void) {
|
|||||||
_exit(134);
|
_exit(134);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
/* The loop itself, with one bit of what the caller knows: whether a restart
|
||||||
void *xfer) {
|
* taken here has anywhere to land. [break_loop] passes 1 — it holds the
|
||||||
|
* signaller's channel, and writing a frame into it is how §2 resumes.
|
||||||
|
* [trap_stop] passes 0, and then this is a place to *stand and read* and
|
||||||
|
* nothing more: the stack, the locals, the globals and the restart list are
|
||||||
|
* all still there to be looked at, and only the resume is refused. That is
|
||||||
|
* strictly more than the alternative, which was the whole process exiting
|
||||||
|
* before anyone could ask a question. */
|
||||||
|
static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
||||||
|
void *xfer, int resumable) {
|
||||||
struct timespec step = { 0, 2000000 }; /* 2ms */
|
struct timespec step = { 0, 2000000 }; /* 2ms */
|
||||||
(void)condition;
|
(void)condition;
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
@ -548,7 +570,7 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
* are then the same list, numbered the same way, and the numbers are what a
|
* are then the same list, numbered the same way, and the numbers are what a
|
||||||
* choice is made of. */
|
* choice is made of. */
|
||||||
int32_t my_gen;
|
int32_t my_gen;
|
||||||
if (!snap_push()) {
|
if (!snap_push(resumable)) {
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
|
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
|
||||||
"spinning\n", BREAK_MAX);
|
"spinning\n", BREAK_MAX);
|
||||||
@ -558,7 +580,18 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
{
|
{
|
||||||
snapshot *s = snap_top();
|
snapshot *s = snap_top();
|
||||||
my_gen = s->gen;
|
my_gen = s->gen;
|
||||||
if (s->n == 0)
|
/* Said once, above the list, rather than appended to every line: it is
|
||||||
|
* one fact about the break, and repeating it per restart would read as
|
||||||
|
* each restart having its own separate problem. The list is still
|
||||||
|
* printed, because what is on offer is part of where the program is even
|
||||||
|
* when none of it can be taken — and because the same names come back
|
||||||
|
* from a `restarts' query, and the terminal and the socket must not be
|
||||||
|
* describing two different programs. */
|
||||||
|
if (!s->resumable)
|
||||||
|
fprintf(stderr,
|
||||||
|
" this trap has no transfer channel, so nothing here can be "
|
||||||
|
"resumed into; read the frame, then fix and reload, or abort\n");
|
||||||
|
else if (s->n == 0)
|
||||||
fprintf(stderr, " no restarts are active; abort, or fix and reload\n");
|
fprintf(stderr, " no restarts are active; abort, or fix and reload\n");
|
||||||
for (int32_t i = 0; i < s->n; i++)
|
for (int32_t i = 0; i < s->n; i++)
|
||||||
/* Numbered, because that is how one is taken now, and marked when it is
|
/* Numbered, because that is how one is taken now, and marked when it is
|
||||||
@ -566,7 +599,9 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
* than hidden, since "why can I not have that one" is a fair question
|
* than hidden, since "why can I not have that one" is a fair question
|
||||||
* and silence is how this went wrong the first time. */
|
* and silence is how this went wrong the first time. */
|
||||||
fprintf(stderr, " %2d. restart: %s%s\n", i, s->names + s->off[i],
|
fprintf(stderr, " %2d. restart: %s%s\n", i, s->names + s->off[i],
|
||||||
s->reachable[i] ? "" : " (below this break; cannot be taken)");
|
!s->resumable ? " (cannot be taken from this trap)"
|
||||||
|
: s->reachable[i] ? ""
|
||||||
|
: " (below this break; cannot be taken)");
|
||||||
if (s->total > s->n)
|
if (s->total > s->n)
|
||||||
fprintf(stderr, " ... and %d more, not listed\n", s->total - s->n);
|
fprintf(stderr, " ... and %d more, not listed\n", s->total - s->n);
|
||||||
}
|
}
|
||||||
@ -614,7 +649,7 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
atomic_store(&chosen_ready, 0);
|
atomic_store(&chosen_ready, 0);
|
||||||
snapshot *s = snap_top();
|
snapshot *s = snap_top();
|
||||||
int ok = s != NULL && s->gen == my_gen && take >= 0 && take < s->n
|
int ok = s != NULL && s->gen == my_gen && take >= 0 && take < s->n
|
||||||
&& s->reachable[take];
|
&& s->resumable && s->reachable[take];
|
||||||
if (ok) {
|
if (ok) {
|
||||||
flan_restart_take(s->frame[take], xfer);
|
flan_restart_take(s->frame[take], xfer);
|
||||||
fprintf(stderr, "flan: resuming at restart %d. %s\n", take,
|
fprintf(stderr, "flan: resuming at restart %d. %s\n", take,
|
||||||
@ -644,6 +679,27 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* §2's break, which can resume, and the trap's, which cannot. Two names over
|
||||||
|
* one loop because the two hooks have different signatures and different
|
||||||
|
* promises, and a caller of either should not have to pass a flag it does not
|
||||||
|
* understand.
|
||||||
|
*
|
||||||
|
* [trap_stop] is not marked _Noreturn even though it never returns: the loop
|
||||||
|
* above leaves only by resuming, which needs [resumable], or by [die_now].
|
||||||
|
* Saying so in the type would oblige this function to prove it, and the honest
|
||||||
|
* proof is the [die_now] below — reached only if the loop is ever given a way
|
||||||
|
* out that this call did not ask for, and dying there is what the program did
|
||||||
|
* before any of this existed. */
|
||||||
|
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||||
|
void *xfer) {
|
||||||
|
break_loop_at(name, namelen, condition, xfer, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void trap_stop(const uint8_t *name, int64_t namelen) {
|
||||||
|
break_loop_at(name, namelen, NULL, NULL, 0);
|
||||||
|
die_now();
|
||||||
|
}
|
||||||
|
|
||||||
/* Re-entrant, and it has to be: a thunk this runs may itself error, and the
|
/* Re-entrant, and it has to be: a thunk this runs may itself error, and the
|
||||||
* break loop that catches it polls again from inside that very call. So a job
|
* break loop that catches it polls again from inside that very call. So a job
|
||||||
* is *claimed* — tail advanced past it — before it is run, and both indices
|
* is *claimed* — tail advanced past it — before it is run, and both indices
|
||||||
@ -860,8 +916,12 @@ static void handle_line(char *line, sink *o) {
|
|||||||
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
|
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
|
||||||
for (int32_t i = 0; i < s->n; i++) {
|
for (int32_t i = 0; i < s->n; i++) {
|
||||||
char hdr[32];
|
char hdr[32];
|
||||||
|
/* Both facts fold into the one flag, because the flag answers one
|
||||||
|
* question — can this be taken — and a break taken by a trap can take
|
||||||
|
* none of them. The terminal listing above says the same thing in
|
||||||
|
* words; the two must not describe different programs. */
|
||||||
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
|
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
|
||||||
s->reachable[i] ? '+' : '-');
|
(s->resumable && s->reachable[i]) ? '+' : '-');
|
||||||
if (k > 0) emit(o, hdr, (size_t)k);
|
if (k > 0) emit(o, hdr, (size_t)k);
|
||||||
emit(o, s->names + s->off[i], (size_t)s->len[i]);
|
emit(o, s->names + s->off[i], (size_t)s->len[i]);
|
||||||
reply(o, "\n");
|
reply(o, "\n");
|
||||||
@ -977,6 +1037,16 @@ static void handle_line(char *line, sink *o) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Checked before [reachable], because it is the stronger fact and the one
|
||||||
|
* with the better sentence: at a trap every restart is unreachable, and
|
||||||
|
* answering with the thunk-boundary reason would send the reader looking
|
||||||
|
* for an evaluation that is not there. */
|
||||||
|
if (!s->resumable) {
|
||||||
|
reply(o, "err this break was taken by a trap with no transfer channel, "
|
||||||
|
"so no restart can be taken from it; read the frame, then fix "
|
||||||
|
"and reload, or abort\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!s->reachable[idx]) {
|
if (!s->reachable[idx]) {
|
||||||
/* Refused, with the reason, rather than accepted and dropped. The
|
/* Refused, with the reason, rather than accepted and dropped. The
|
||||||
* transfer would unwind to the thunk this break is inside and stop
|
* transfer would unwind to the thunk this break is inside and stop
|
||||||
@ -1017,6 +1087,12 @@ static void handle_line(char *line, sink *o) {
|
|||||||
reply(o, " is active\n");
|
reply(o, " is active\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!s->resumable) {
|
||||||
|
reply(o, "err this break was taken by a trap with no transfer channel, "
|
||||||
|
"so no restart can be taken from it; read the frame, then fix "
|
||||||
|
"and reload, or abort\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!s->reachable[at]) {
|
if (!s->reachable[at]) {
|
||||||
reply(o, "err restart ");
|
reply(o, "err restart ");
|
||||||
reply(o, line + 8);
|
reply(o, line + 8);
|
||||||
@ -1491,5 +1567,6 @@ int32_t flan_agent_start(const uint8_t *path, int64_t len) {
|
|||||||
* socket and not before it: without a listener there is nobody to ask what
|
* 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. */
|
* to do, and stopping forever is worse than the abort it replaces. */
|
||||||
flan_break_hook = break_loop;
|
flan_break_hook = break_loop;
|
||||||
|
flan_trap_hook = trap_stop;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user