flan/vendor/agent/flan_agent.c

1707 lines
82 KiB
C

/* flan_agent — the half of the dev loop that lives in the running program.
*
* A redefinition arrives as a path to a .so (runtime/flan_dev.c and
* Emit.redefinition are what put it there). Two things have to happen to it,
* and they must happen on different threads:
*
* dlopen relocates the module and runs the loader. It is milliseconds,
* unbounded, and takes the loader lock. Doing it on the game thread
* is a dropped frame.
* install is one store per redefined function. It is sub-microsecond, and
* it must happen at a point where no redefined function is on the
* stack — a frame boundary — or a frame runs half in the old code
* and half in the new.
*
* So the listener thread does the loading and hands over a function pointer;
* the game thread calls flan-poll or flan-wait when it is between frames and
* that is when the swap becomes visible. Nothing else in the program needs to
* know the agent exists.
*
* Nothing that published anything is ever dlclosed: a cell holds an address
* inside a module's text, and unloading it would leave every call site
* pointing at unmapped memory. The two modules that are closed are the ones
* nothing can point into — a transient thunk, which installs no bodies, and a
* module refused before it was queued.
*
* This is not a protocol. One line per request, the path to load, and a one
* line answer. The daemon and its nREPL are a separate program that will speak
* to a socket, not something this file grows into.
*
* The compiler is no longer a separate *process*, though. [flan dev] builds one
* binary that is the program and holds the whole compiler, so a request from it
* is a call to [flan_agent_request] below and not a connect on a socket that
* loops back into this address space. It is the same verb table either way —
* [handle_line] writes into a [sink] that is an fd or a buffer — and the same
* hand-off: a module is published to the ring, and the game thread installs it
* at a frame boundary. The socket stays for [--two-process] and for a person
* with nc.
*/
#include <dlfcn.h>
#include <errno.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#if defined(__linux__)
#include <sys/prctl.h>
#endif
typedef void (*install_fn)(void);
/* A module may also carry a thunk to run once — that is C-x C-e, an expression
* compiled into a function with nowhere to be called from. It runs where the
* install happens, on the game thread between frames, because an expression
* that reads the program's state has to see it at a point the program agrees
* is consistent. */
typedef void (*call_fn)(void);
/* The value of the last evaluated expression, copied out under the seqlock in
* runtime/flan_dev.c rather than borrowed: a reader gets bytes of its own, and
* a read that loses the race reports the last complete generation and no
* bytes, which leaves the compiler polling rather than showing it half a
* value.
*
* How big that copy has to be is the runtime's number, and it is asked for
* rather than written down here as well. This file used to carry its own copy
* of the bound and a check that the two had not drifted — which is what you do
* when one of them is sizing a buffer to send through. Nothing here sends
* anything any more, so the bound belongs to whoever owns the storage, and
* changing it is one file's decision. */
int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen,
uint64_t *len);
uint64_t flan_dev_result_cap(void);
/* The watch table, same arrangement and for the same reason: the game thread
* writes it mid-frame into fixed storage that belongs to flan_dev.c, and this
* file only ever copies out of it, on the listener thread. The one addition is
* [enable] — the table is written only while somebody is reading it, so
* opening and closing a watch buffer is a message that arrives here. */
void flan_dev_watch_enable(int on);
int flan_dev_watch_enabled(void);
/* And [reset], which opens a new accumulation window for the numeric slots.
* It moves one counter and touches no slot, so the game thread stays the only
* writer of the table. */
void flan_dev_watch_reset(void);
uint32_t flan_dev_watch_count(void);
int flan_dev_watch_overflowed(void);
int flan_dev_watch_read(uint32_t i, char *nd, uint64_t ncap,
char *vd, uint64_t vcap, uint64_t *vlen);
uint64_t flan_dev_watch_name_cap(void);
uint64_t flan_dev_watch_val_cap(void);
/* The allocation registry, read the same way and on the same thread. The
* renderer's two questions — is this address live, and what died here — are
* asked from inside a compiled thunk and never come through this file; these
* three are the *reader's* side, which has no (Ptr T) to read a type off and
* so has to ask the table what it recorded.
*
* Formatting lives here rather than in flan_dev.c for the reason [watch] and
* [result] already have: this runs on the listener thread, where allocating
* and snprintf are legal, and what the game thread writes stays a fixed
* table nobody has to format to fill. */
int32_t flan_dev_reg_at(const void *p, const char **type, int64_t *typelen,
int64_t *off, int64_t *bytes, int64_t *elem,
int64_t *seq, int64_t *died);
int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts,
int64_t *bytes, const char **types,
int64_t *typelens, int64_t cap, int64_t *unread);
int flan_dev_reg_enabled(void);
int flan_dev_reg_overflowed(void);
/* A ring the listener writes and the game thread reads. One producer, one
* consumer, so two atomics and no lock — the game thread must never block on
* the loader.
*
* A full ring is *refused*, at the sender, with an error. The previous comment
* here claimed it dropped the oldest request, and nothing did that: [publish]
* never read [tail], so the 65th module overwrote the slot the game thread was
* reading — twenty-four bytes of function pointers, copied field by field with
* no atomic anywhere near them, so the consumer could take half of one job and
* half of another and call it. Silent corruption of the one thing in this file
* that gets *called*.
*
* Of the three honest answers, refusing is the only one that reaches the
* person who asked. Dropping loses a reload the sender was told was ok — the
* same lie in quieter clothes. Blocking stalls the accept loop, which serves
* connections inline, so a program that has stopped polling would also stop
* answering [status] and [abort]: the dev loop would have no way to say
* anything to a program that had stopped listening to it. A refusal is a line
* the daemon can show, and the fix is to call agent/poll.
*
* The check is safe to make separately from the store because there is exactly
* one producer at a time — [request_lock] is what keeps that true now that the
* compiler can call in as well as connect — so room, once seen, cannot be
* taken away by anyone: the consumer only ever makes more of it. */
#define QUEUE 64
/* [handle] is set only for a module that declared itself transient — one that
* ran a thunk and left nothing behind. Everything else is kept mapped forever:
* a cell holds an address inside a module's text, and unloading it would leave
* every call site pointing at unmapped memory. */
/* [stopped_only] is the second half of a promise the *daemon* made and cannot
* keep on its own. A module built to render what is at a raw address has that
* address baked into it as an integer literal: the registry blessed the
* address as live at one moment, and the thunk dereferences it at another,
* three round trips and a ~300ms build later. Nothing in between held the
* break. A [restart] arriving mid-build resumes the game thread, and this ring
* would then install and run the thunk at the very next frame boundary —
* mid-frame, against an address the program may have freed since.
*
* So the job carries the condition it was built under, and [flan_agent_poll]
* re-asks it at the moment of truth instead of trusting the answer the sender
* got. The gate lives *here* and nowhere else on purpose: a second check on
* the listener thread, at delivery, would only shrink the window — [depth] can
* flip between that check and this one — and two places emitting the same
* refusal is how two places stop agreeing, which is the note the struct
* comment further down already makes about layouts.
*
* What makes the check in [flan_agent_poll] sound rather than narrower: only
* the game thread polls (agent.flan's [poll-raw] at a frame boundary, and the
* break loop below, which is the same thread parked), and only the game thread
* raises [depth]. So [depth > 0] read from inside a poll means *this* thread
* is inside the break loop right now — it cannot be running a frame at the
* same time — and the stop the daemon asked under is still the stop in force.
* A false negative is possible in one direction only and is the harmless one:
* a break entered after the read refuses a job that would have been legal, and
* refusing is right anyway, because that is a different stop from the one the
* registry answered under.
*
* [handle] is set only for a module that declared itself transient — one that
* ran a thunk and left nothing behind. Everything else is kept mapped forever:
* a cell holds an address inside a module's text, and unloading it would leave
* every call site pointing at unmapped memory. */
typedef struct {
install_fn install;
call_fn call;
void *handle;
int stopped_only;
} job;
/* Said once, in one place, and shipped to the daemon over [refusals] rather
* than written down again at the other end. A refusal is a sentence naming
* what actually happened, and the thing that actually happened is not "the
* gate failed" — it is that the program the person was inspecting started
* running again while the inspector was being compiled. */
static const char *RESUMED =
"the program resumed while this inspection was being built — stop it again "
"and re-ask";
/* How many stopped-only jobs have been dropped, ever. A count and not a flag:
* the daemon reads it before it delivers and again while it waits, and what it
* wants to know is whether one happened *in between*, which a flag somebody
* else could have cleared cannot say.
*
* It is the whole of the channel because the sentence is fixed. One residual,
* left alone deliberately: two inspections in flight at once would let either
* one's refusal surface to the other's waiter. The result buffer they both
* write into has exactly that property already — flan_dev.c says so where the
* seqlock is — and fixing it here without fixing it there would be half a
* repair wearing the whole one's clothes. */
static atomic_ullong refused_while_running;
static job queue[QUEUE];
static atomic_uint head; /* written by the listener */
static atomic_uint tail; /* written by the game thread */
static int listen_fd = -1;
static pthread_t listener;
static atomic_int started;
/* Room for one more. Unsigned subtraction, so the answer survives [head] and
* [tail] wrapping; only their difference means anything. */
static int queue_room(void) {
unsigned h = atomic_load_explicit(&head, memory_order_relaxed);
unsigned t = atomic_load_explicit(&tail, memory_order_acquire);
return (h - t) < QUEUE;
}
/* 0 if the ring is full, having published nothing. Checked here as well as at
* the caller, because a producer that forgot would otherwise reintroduce
* exactly the overwrite this replaced. */
static int publish(job j) {
unsigned h = atomic_load_explicit(&head, memory_order_relaxed);
unsigned t = atomic_load_explicit(&tail, memory_order_acquire);
if (h - t >= QUEUE) return 0;
queue[h % QUEUE] = j;
/* Release: the store to the slot must be visible before the index that
* advertises it. */
atomic_store_explicit(&head, h + 1, memory_order_release);
return 1;
}
/* Returns how many modules were installed. Call it between frames. */
/* ── The break loop, spec-conditions.md §2 ──────────────────────────── */
/* Where "a crash kills the program" stops being true. An unhandled error runs
* this instead of rt_die(), on the frame that erred, with nothing unwound — so
* the condition and every restart between here and the top are still live.
*
* It is the *poll* loop, run from here instead of from the frame boundary, and
* that is the whole design. An expression evaluated while stopped is a module
* the loader thread queues and the game thread runs; if this loop did not
* drain that queue, C-x C-e would hang exactly when it is most wanted.
*
* Installing while stopped is deliberately allowed. The rule that a redefined
* function must not be swapped while it is on the stack is about *mid-frame
* consistency* — half a frame of old code and half of new — and there is no
* frame in progress here. The old body on the stack keeps running; a retry
* restart calls through the cell and reaches the new one. That is the fix-it-
* and-retry loop, and refusing the install would remove the point. */
/* The runtime's end of it. flan_rt.c holds the hook and the lookup; the loop
* itself is here, because it needs the socket and the install queue and the
* runtime must not depend on an optional package for either. */
extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen,
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,
void *xfer);
extern int32_t flan_restart_count(void);
/* The shadow stack (runtime/flan_dev.c). The compiler pushes a frame per Flan
* call in a dev build; these read one, and only ever on the thread that owns
* it. The frame is opaque here on purpose: its shape belongs to flan_dev.c,
* and two files each declaring the struct is how the two stop agreeing. */
extern int32_t flan_dev_frame_count(void);
extern void *flan_dev_frame_at(int32_t i);
extern const char *flan_dev_frame_name(const void *frame, int64_t *len);
extern const char *flan_dev_frame_loc(const void *frame, int64_t *len);
extern int32_t flan_dev_frame_nslots(const void *frame);
extern int32_t flan_dev_frame_slotsig(const void *frame);
extern int32_t flan_dev_frame_refsig(const void *frame);
extern void *flan_dev_frame_slot(const void *frame, int32_t i);
extern const uint8_t *flan_restart_name(int32_t i, int64_t *len);
extern void *flan_restart_frame(int32_t i);
extern void flan_restart_take(void *frame, void *xfer);
/* -- How far down a transfer can actually land ----------------------- */
/* A thunk run from this loop is called through [flan_reload_call], which
* allocates its *own* transfer channel and drops it on return. So a restart
* chosen at a break inside a thunk unwinds as far as the thunk and no
* further: every frame below that C boundary is on the list, is accepted, and
* is silently not taken. Three statements that the program will resume, none
* of them true - which is worse than either resuming or refusing.
*
* The boundary is recorded where it is *made*, at the call, not at the break:
* the frames below it are exactly the ones already on the stack when the
* thunk started, and a restart-case the thunk enters itself is above it and
* reachable. Recording the depth on entering the break loop instead would
* count those too and refuse restarts that work.
*
* Game thread only, saved and restored around the call, so a thunk that
* breaks and whose break runs another thunk nests correctly. */
static int32_t restart_floor;
/* The same boundary, counted in shadow-stack frames. A break inside a C-x C-e
* thunk has that thunk's frames on top of the program's, and "where is my
* program" answered with [eval/7] is not the question anyone asked. Recorded
* at the call for the same reason [restart_floor] is: the frames below it are
* exactly the ones that were already on the stack when the thunk started. */
/* -1, not 0, and the distinction is the whole of it: [restart_floor] can be
* zero meaning "no restarts were on the stack when the thunk started", and
* outside a thunk zero is also the right answer. Frames are the other way
* round — outside a thunk *every* frame is the program's, and zero would say
* none of them are. So "not inside a thunk" gets a value of its own. */
static int32_t frame_floor = -1;
/* What the listener thread hands the stopped game thread. One slot, because
* only one thread is ever stopped. */
/* A *depth*, not a flag. A thunk this loop runs may itself error, and the
* break loop that catches that one is nested inside this one — so a flag is
* wrong twice over: the inner loop clearing it on resume tells the world the
* program is running while the outer loop is still stopped, and every verb
* then answers "not stopped" while the outer loop spins forever with no
* protocol path out. Only kill recovered it. Counting fixes both. */
static _Atomic int depth;
#define BREAK_MAX 8 /* deep enough to nest, shallow
* enough that a loop of breaks
* stops rather than grinds */
static _Atomic int chosen_index;
/* Which snapshot that index is an index *into*. The listener validates a
* choice against the snapshot on top when the request arrives, and the game
* thread resolves it against the snapshot on top when it next looks - and
* those are two reads of a stack that can move between them. An evaluation
* this loop runs may error, push a break of its own, and reach [chosen_ready]
* first, which would take that break's index 2 for the one someone chose from
* the outer list. Depth is not enough to tell them apart: an outer break
* resuming and a new one starting reuses the number. A generation does not.
*
* A mismatch is *left alone* rather than discarded. The listener already
* answered ok for it, so the break it was meant for must still be able to
* take it; the inner loop simply does not claim what is not addressed to it. */
static _Atomic int chosen_gen;
static _Atomic int chosen_ready;
static _Atomic int aborting;
/* -- The snapshot ---------------------------------------------------- */
/* The stopped thread is not holding still. This loop runs [flan_agent_poll],
* which runs arbitrary Flan, and every restart-case that runs pushes and pops
* the one global restart list. Serving names straight off that list hands the
* listener a pointer into a frame that may already have been popped; serving
* *indices* off it is worse still, because an index carries no evidence of
* what it meant - the list and the choice can disagree and nothing can tell.
*
* So the list is read once, on entry, while the thread that owns it is in
* this function and not in Flan, and copied: names into a buffer of our own,
* frames as the addresses a transfer carries. Everything the listener answers
* comes from here and nothing re-reads the live stack. */
#define SNAP_MAX 64 /* restarts offered at one break */
#define SNAP_NAMES 4096 /* bytes of names behind them */
#define FRAME_MAX 64 /* frames listed in a backtrace */
#define FRAME_TEXT 8192 /* bytes of names and locations */
typedef struct {
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 total; /* before SNAP_MAX truncated it */
void *frame[SNAP_MAX];
int32_t off[SNAP_MAX], len[SNAP_MAX];
int32_t reachable[SNAP_MAX];
int32_t used;
char names[SNAP_NAMES];
/* Where the stopped thread is, taken at the same moment and for the same
* reason: the chain is the game thread's, and it is holding still only
* because it is parked in this loop. [fframe] is kept as well as the text,
* because reading a frame's locals means going back to that frame — and to
* that frame rather than to whatever is at index 2 by then. */
int32_t fn; /* frames listed */
int32_t ftotal; /* before FRAME_MAX truncated it */
int32_t fused;
void *fframe[FRAME_MAX];
int32_t fnoff[FRAME_MAX], fnlen[FRAME_MAX];
int32_t floff[FRAME_MAX], fllen[FRAME_MAX];
int32_t fslots[FRAME_MAX];
int32_t fsig[FRAME_MAX]; /* the slot fingerprint of the body
* this frame was compiled from */
int32_t frsig[FRAME_MAX]; /* and the fingerprint of the
* globals that body names */
int32_t fmine[FRAME_MAX]; /* 0 = the evaluation's, not the
* program's */
char ftext[FRAME_TEXT];
} snapshot;
/* One per nested break loop, because an inner break must not answer with the
* outer one's restarts and must not destroy them either - the outer loop is
* still going to need them when the inner one resumes. Same reason [depth] is
* a count and not a flag. */
static snapshot snaps[BREAK_MAX];
static _Atomic int snap_depth; /* published last; 0 = none */
static snapshot *snap_top(void);
/* Where slot [slot] of frame [frame] lives, resolved against the snapshot this
* break took and not against the live chain.
*
* This is what a locals thunk calls. The thunk runs on the stopped game
* thread, from inside this break loop's own poll, and it pushes frames of its
* own while it runs — so "frame 2" means the third frame of the backtrace the
* daemon was shown, not the third frame of whatever the stack looks like by
* the time the thunk is executing. Resolving against the snapshot is the whole
* of the difference, and it is the same reason the restarts are answered from
* there.
*
* NULL for anything it cannot place, and the thunk is built to ask only about
* slots the same snapshot already reported as bound. A NULL would be
* dereferenced, so this is the one place that must not answer optimistically:
* an index that is out of range, a frame that is not in this snapshot, or a
* slot the binding for which had not run, are each a null here and a refusal
* before the thunk is ever built. */
void *flan_agent_frame_slot(int64_t frame, int64_t slot) {
snapshot *s = snap_top();
if (s == NULL) return NULL;
if (frame < 0 || frame >= s->fn) return NULL;
if (slot < 0 || slot > 0x7fffffff) return NULL;
return flan_dev_frame_slot(s->fframe[frame], (int32_t)slot);
}
static snapshot *snap_top(void) {
int d = atomic_load(&snap_depth);
return d <= 0 ? NULL : &snaps[d - 1];
}
/* Called on the game thread with the stack held still. 0 if there is no room
* to nest, which the caller reports rather than serving a stale one. */
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
static int snap_push(int resumable) {
int d = atomic_load(&snap_depth);
if (d >= BREAK_MAX) return 0;
snapshot *s = &snaps[d];
int32_t n = flan_restart_count();
s->gen = ++snap_gen;
s->resumable = resumable;
s->total = n;
s->used = 0;
s->n = 0;
for (int32_t i = 0; i < n && s->n < SNAP_MAX; i++) {
int64_t len = 0;
const uint8_t *nm = flan_restart_name(i, &len);
void *fr = flan_restart_frame(i);
if (nm == NULL || fr == NULL) continue;
if (len < 0) len = 0;
if ((int64_t)s->used + len + 1 > SNAP_NAMES) break;
s->frame[s->n] = fr;
s->off[s->n] = s->used;
s->len[s->n] = (int32_t)len;
/* The outermost [restart_floor] frames are below the thunk boundary. */
s->reachable[s->n] = (i < n - restart_floor);
memcpy(s->names + s->used, nm, (size_t)len);
s->used += (int32_t)len;
s->names[s->used++] = 0;
s->n++;
}
/* And the frames, from the same held-still stack. A deep recursion is
* truncated rather than followed: the innermost frames are the ones the
* question is about, and the count says how many were left out. */
{
int32_t fn = flan_dev_frame_count();
s->ftotal = fn;
s->fused = 0;
s->fn = 0;
for (int32_t i = 0; i < fn && s->fn < FRAME_MAX; i++) {
void *fr = flan_dev_frame_at(i);
int64_t nl = 0, ll = 0;
const char *nm, *lc;
if (fr == NULL) break;
nm = flan_dev_frame_name(fr, &nl);
lc = flan_dev_frame_loc(fr, &ll);
if (nl < 0) nl = 0;
if (ll < 0) ll = 0;
if ((int64_t)s->fused + nl + ll + 2 > FRAME_TEXT) break;
s->fframe[s->fn] = fr;
s->fnoff[s->fn] = s->fused;
s->fnlen[s->fn] = (int32_t)nl;
if (nm != NULL && nl > 0) memcpy(s->ftext + s->fused, nm, (size_t)nl);
s->fused += (int32_t)nl;
s->ftext[s->fused++] = 0;
s->floff[s->fn] = s->fused;
s->fllen[s->fn] = (int32_t)ll;
if (lc != NULL && ll > 0) memcpy(s->ftext + s->fused, lc, (size_t)ll);
s->fused += (int32_t)ll;
s->ftext[s->fused++] = 0;
s->fslots[s->fn] = flan_dev_frame_nslots(fr);
/* Snapshotted with the rest of the frame rather than read later: the
* module this description lives in can be unloaded once the daemon
* installs a replacement, and the comparison happens after that. */
s->fsig[s->fn] = flan_dev_frame_slotsig(fr);
s->frsig[s->fn] = flan_dev_frame_refsig(fr);
/* The outermost [frame_floor] frames are the program's; anything above
* them belongs to the evaluation this break is inside. */
s->fmine[s->fn] = (frame_floor < 0) || (i >= fn - frame_floor);
s->fn++;
}
}
atomic_store(&snap_depth, d + 1);
return 1;
}
static void snap_pop(void) {
int d = atomic_load(&snap_depth);
if (d > 0) atomic_store(&snap_depth, d - 1);
}
/* The condition's class name, so an editor can say what stopped rather than
* only that something did. It is all there is to say: the hook is handed the
* name and an opaque pointer, and nothing at run time can render a value whose
* type it does not know. Written before [broken] is set and read only while
* [broken] is 1, so the listener never sees half of it. */
static char condition_name[128];
int32_t flan_agent_poll(void);
/* 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
* chain and the ELF destructors, which want that same lock. A program asked to
* abort would hang instead of dying, which is the failure mode the break loop
* exists to replace. Nothing here needs an orderly teardown: the streams are
* flushed by hand above every call.
*
* 134 is kept because that is what a trap exits with; see rt_die in
* flan_rt.c. */
static _Noreturn void die_now(void) {
fflush(stdout);
fflush(stderr);
/* The editor's socket, by hand. In a merged `flan dev' this process is the
* listener as well as the program, and _exit skips the atexit handler that
* would otherwise remove it -- deliberately, for the loader-lock reason
* above. A socket file left behind with nothing accepting on it answers the
* next client with ECONNREFUSED, which reads like a daemon that is there and
* refusing rather than one that died. FLAN_DEV_SOCK is unset in an ordinary
* run, and then this does nothing. */
{
const char *sock = getenv("FLAN_DEV_SOCK");
if (sock != NULL && *sock != '\0') unlink(sock);
}
_exit(134);
}
/* The loop itself, with one bit of what the caller knows: whether a restart
* 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 */
(void)condition;
fflush(stdout);
fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n",
(int)namelen, (const char *)name);
/* Taken before anything is printed, and before [depth] says there is a
* break to ask about: the list on the terminal and the list on the socket
* are then the same list, numbered the same way, and the numbers are what a
* choice is made of. */
int32_t my_gen;
if (!snap_push(resumable)) {
fflush(stdout);
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
"spinning\n", BREAK_MAX);
fflush(stderr);
die_now();
}
{
snapshot *s = snap_top();
my_gen = s->gen;
/* 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");
for (int32_t i = 0; i < s->n; i++)
/* Numbered, because that is how one is taken now, and marked when it is
* not takeable - a restart below the thunk boundary is shown rather
* than hidden, since "why can I not have that one" is a fair question
* and silence is how this went wrong the first time. */
fprintf(stderr, " %2d. restart: %s%s\n", i, s->names + s->off[i],
!s->resumable ? " (cannot be taken from this trap)"
: s->reachable[i] ? ""
: " (below this break; cannot be taken)");
if (s->total > s->n)
fprintf(stderr, " ... and %d more, not listed\n", s->total - s->n);
}
fflush(stderr);
/* What the *outer* loop was reporting, restored on the way out: resuming an
* inner break must not leave the outer one describing a condition that has
* already been answered. */
char outer_name[sizeof condition_name];
memcpy(outer_name, condition_name, sizeof outer_name);
{
size_t k = namelen < 0 ? 0 : (size_t)namelen;
if (k >= sizeof condition_name) k = sizeof condition_name - 1;
memcpy(condition_name, name, k);
condition_name[k] = '\0';
}
/* Published last: the name has to be whole before anything advertises that
* there is one to read. */
if (atomic_fetch_add(&depth, 1) + 1 > BREAK_MAX) {
/* Printed without going near the hook: whatever is erroring is erroring
* inside the machinery that reports errors. */
fflush(stdout);
fprintf(stderr,
"flan: %d nested break loops — giving up rather than spinning\n",
BREAK_MAX);
fflush(stderr);
die_now();
}
for (;;) {
flan_agent_poll();
if (atomic_load(&aborting)) {
fflush(stdout);
fprintf(stderr, "flan: aborted at the break loop\n");
die_now();
}
if (atomic_load(&chosen_ready)) {
/* Claimed into a local and the flag cleared *before* the attempt. The
* other order loses a request that was already answered ok: a [restart]
* arriving during the attempt passes its own check, writes a new name
* and sets the flag, and the store below then erases it. Copying also
* keeps strlen off a buffer the listener may be writing. */
/* Read before the claim: a choice addressed to some other break is
* not this one's to consume. */
if (atomic_load(&chosen_gen) != my_gen) { nanosleep(&step, NULL); continue; }
int take = atomic_load(&chosen_index);
atomic_store(&chosen_ready, 0);
snapshot *s = snap_top();
int ok = s != NULL && s->gen == my_gen && take >= 0 && take < s->n
&& s->resumable && s->reachable[take];
if (ok) {
flan_restart_take(s->frame[take], xfer);
fprintf(stderr, "flan: resuming at restart %d. %s\n", take,
s->names + s->off[take]);
fflush(stderr);
memcpy(condition_name, outer_name, sizeof condition_name);
/* Cleared with the resume: an abort that passed its check just as the
* game thread resumed would otherwise stay armed and kill the program
* at the *next* unhandled error, minutes later, in unrelated code,
* giving nobody the chance to choose. */
atomic_store(&aborting, 0);
/* Popped *before* the depth comes down. The other order leaves a
* window where [depth] says the outer break is the current one and
* [snap_top] still answers with the inner one's list, so a request
* arriving in it is validated against a list nobody is looking at. */
snap_pop();
atomic_fetch_sub(&depth, 1);
return;
}
/* The listener checks all of this before answering ok, so reaching here
* means the two disagreed - worth saying loudly rather than looping on
* in silence, which is the failure this whole change is about. */
fprintf(stderr, "flan: restart %d is not one this break can take\n", take);
fflush(stderr);
}
nanosleep(&step, NULL);
}
}
/* §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
* 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
* are re-read each time round rather than cached across the work. Caching them
* and storing tail at the end would rewind it over everything the nested poll
* consumed, and running a C-x C-e thunk a second time is the one thing the
* whole dev loop is careful never to do. Still single-consumer: only the game
* thread writes tail, nesting included. */
int32_t flan_agent_poll(void) {
int32_t n = 0;
for (;;) {
unsigned t = atomic_load_explicit(&tail, memory_order_relaxed);
unsigned h = atomic_load_explicit(&head, memory_order_acquire);
if (t == h) return n;
job j = queue[t % QUEUE];
atomic_store_explicit(&tail, t + 1, memory_order_relaxed);
/* The gate the [job] comment argues for, asked at the only moment whose
* answer is worth anything. Claimed first and then dropped, rather than
* left in the ring for a later stop: the address it was built around was
* blessed by a stop that has already ended, and a job that waits for the
* *next* break would run against a blessing older still.
*
* The handle is closed for the reason the listener closes one it refuses
* for having no installer: a transient module nothing ever points into is
* unreachable the moment this returns, and dlopen refcounts by path, so
* dropping the handle on the floor here would leave a count that re-asking
* the same inspection bumps and nothing brings down. [n] is not bumped —
* nothing was installed, and [n] is what a caller polls to find out that
* something was. */
if (j.stopped_only && atomic_load(&depth) <= 0) {
atomic_fetch_add(&refused_while_running, 1);
if (j.handle != NULL) { dlclose(j.handle); }
continue;
}
if (j.install != NULL) { j.install(); n++; }
/* After the install, so a thunk sees the bodies its own module published.
*
* The floor moves for the duration. [flan_reload_call] holds its own
* transfer channel and drops it on return, so every restart frame that
* was on the stack before this call is unreachable from a break inside
* it. Saved and restored rather than set and cleared: this runs from
* inside break loops, which run from inside thunks. */
if (j.call != NULL) {
int32_t outer = restart_floor;
int32_t oframe = frame_floor;
restart_floor = flan_restart_count();
frame_floor = flan_dev_frame_count();
j.call();
restart_floor = outer;
frame_floor = oframe;
}
if (j.handle != NULL) { dlclose(j.handle); }
}
}
/* The same, but waits up to [ms] for something to arrive first. A game loop
* does not want this; a headless test does, because it makes the reload
* deterministic instead of a race against the frame rate. */
int32_t flan_agent_wait(int32_t ms) {
struct timespec step = { 0, 1000000 }; /* 1ms */
for (int32_t i = 0; i < ms; i++) {
int32_t n = flan_agent_poll();
if (n > 0) return n;
nanosleep(&step, NULL);
}
return flan_agent_poll();
}
/* Where an answer goes. Two kinds, and the verb table below cannot tell them
* apart: a socket, which is how the two-process daemon asks, and a buffer,
* which is how the compiler asks when it is a thread in this same process.
*
* One sink rather than two copies of the verb table. Every answer here is
* assembled from several pieces — a header, a name, a newline — so the seam
* had to be the writing and not the handler, or the handlers would have had
* to be duplicated and would drift. */
typedef struct {
int fd; /* -1 when this is a buffer */
char *buf;
size_t len;
size_t cap;
} sink;
/* MSG_NOSIGNAL rather than write(2). A reply goes out in more than one piece,
* and a sender that has read enough and closed leaves the rest of it writing
* into a closed socket — which is SIGPIPE, whose default action would kill the
* program the agent is embedded in. Suppressing it per call rather than
* installing a handler, because the disposition belongs to the program and not
* to us. */
static void emit(sink *o, const void *p, size_t n) {
if (o->fd >= 0) {
const char *s = (const char *)p;
while (n > 0) {
ssize_t k = send(o->fd, s, n, MSG_NOSIGNAL);
if (k <= 0) return;
s += k;
n -= (size_t)k;
}
return;
}
if (o->len + n + 1 > o->cap) {
size_t want = o->cap ? o->cap : 1024;
while (want < o->len + n + 1) want *= 2;
char *g = realloc(o->buf, want);
/* Nothing useful to do with a failed realloc here: the caller reads
* [len], so a short answer is what it sees, and that is the same shape as
* a socket the peer closed early. */
if (g == NULL) return;
o->buf = g;
o->cap = want;
}
memcpy(o->buf + o->len, p, n);
o->len += n;
o->buf[o->len] = '\0';
}
static void reply(sink *o, const char *s) { emit(o, s, strlen(s)); }
/* A dlopen failure the compiler's two backends are responsible for, turned
* into a sentence that says so.
*
* Flan has two native backends. They agree about every scalar and disagree
* about every aggregate — the x86 dev backend passes a struct by pointer with
* a hidden sret, LLVM classifies it per the SysV psABI — so a redefinition
* module built by one and loaded into a host built by the other links, loads,
* and then dies with SIGSEGV at the first call into a redefined function that
* takes or returns a struct. A dev build therefore defines a marker naming its
* backend and a module holds a pointer to the marker it was built for, which
* is a relocation the loader must resolve while it maps the object. A crossed
* pair has no such symbol and is refused here, before any of the new code
* runs.
*
* What the loader says at that point is "undefined symbol: flan.abi.x86",
* which is true and tells nobody anything. So the marker's name is matched —
* the name, not the loader's phrasing, which is libc's to change — and the
* reason is stated instead. Which marker is missing says which backend built
* the module, and the host is necessarily the other one.
*
* Returns NULL for a failure that is about something else, which is then
* passed through as the loader wrote it. */
static const char *abi_mismatch(const char *err) {
if (err == NULL) return NULL;
if (strstr(err, "flan.abi.x86") != NULL)
return "the module and the running program were built by different "
"backends: the module came from the x86 dev backend and needs "
"flan.abi.x86, which this program does not define. The two "
"backends pass every struct differently, so the pair would die at "
"the first call into a redefined function that takes or returns "
"one. Rebuild the program with --x86 so that both halves agree.";
if (strstr(err, "flan.abi.llvm") != NULL)
return "the module and the running program were built by different "
"backends: the module came from LLVM and needs flan.abi.llvm, "
"which an --x86 program does not define. The two backends pass "
"every struct differently, so the pair would die at the first call "
"into a redefined function that takes or returns one. Rebuild the "
"program without --x86: there is no --x86 spelling for building a "
"redefinition module yet, so the program is the half that has to "
"move.";
return NULL;
}
/* One line, one answer, one module. This is the whole of what the agent is
* asked, and it is reached two ways: from the socket below, and — in a build
* where the compiler is a thread in this same process — by being called. The
* two share this function rather than a protocol.
*
* What does not change with the caller is where the work lands. [dlopen] runs
* here, on whichever thread asked; the install is only ever *published* to the
* ring, and the game thread picks it up at a frame boundary. A direct call
* that installed on the spot would be a frame running half in the old code and
* half in the new. */
/* One request at a time, whoever asks. Until the compiler could call in, the
* accept loop was the only caller and was single-threaded, and the ring's
* "exactly one producer" was true by construction. It is not any more: a
* direct call runs on the compiler thread while the accept loop can be serving
* the same verbs. This lock puts that back rather than weakening the ring —
* [publish]'s room check is separate from its store, and two producers
* interleaving there is the silent overwrite the ring comment above describes.
*
* It is held across the [dlopen], which is milliseconds. That is what the
* accept loop already did to itself by serving connections inline, so no
* caller waits longer than it did before. The game thread never takes it. */
static pthread_mutex_t request_lock = PTHREAD_MUTEX_INITIALIZER;
static void handle_line(char *line, sink *o) {
/* The one verb that is not a module: read back the value of the last
* expression evaluated, with the counter that says whether it is a new
* one. The daemon polls this rather than the agent holding a connection
* open across a frame boundary it does not control. */
/* Only while stopped: what is on offer, and which one to take. Both are
* refused when the program is running, by name, rather than silently
* doing nothing — there is no restart stack to walk from here. */
/* Whether the program is stopped, and on what. Answered while it is
* running too — "running" is an answer, not a refusal — because this is
* the one question an editor asks without knowing the state already, and
* refusing it would leave nothing to poll. */
if (strcmp(line, "status") == 0) {
if ((atomic_load(&depth) > 0)) {
reply(o, "stopped ");
reply(o, condition_name);
reply(o, "\n");
} else
reply(o, "running\n");
return;
}
/* One line per restart, innermost first: the index it is taken by, a flag
* for whether it can be taken at all, and the name. The index leads
* because it is the identity - two frames can offer [retry] and only one
* of them is the one meant, which is the whole reason this is not a list
* of names any more. Read from the snapshot, never from the live stack. */
if (strcmp(line, "restarts") == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
for (int32_t i = 0; i < s->n; i++) {
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,
(s->resumable && s->reachable[i]) ? '+' : '-');
if (k > 0) emit(o, hdr, (size_t)k);
emit(o, s->names + s->off[i], (size_t)s->len[i]);
reply(o, "\n");
}
reply(o, ".\n");
return;
}
/* Where the stopped thread is. One line per frame, innermost first:
* the index, whether the frame is the program's or the evaluation's, how
* many slots it has, where it is written, and its name. Served from the
* snapshot, never from the live chain — the game thread is parked in the
* break loop, but the loop polls, and a poll runs Flan.
*
* Refused while running, like every other break verb and for the same
* reason: a chain read by one thread while another pushes and pops it is
* not a backtrace, it is a race with a plausible shape. */
if (strcmp(line, "backtrace") == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(o, "err no frame snapshot\n"); return; }
if (s->fn == 0 && s->ftotal == 0) {
/* Not "no frames": a release build has no shadow stack at all, and
* answering with an empty backtrace would read as a program with an
* empty stack, which is not a thing that can be stopped. */
reply(o, "err this program was not built with --dev, so it has no "
"shadow stack to walk\n");
return;
}
for (int32_t i = 0; i < s->fn; i++) {
char hdr[64];
/* Both fingerprints go before the location and the location before
* the name, because the name is the one field that can contain a
* space and so has to be last. */
int k = snprintf(hdr, sizeof hdr, "%d %c %d %d %d ", i,
s->fmine[i] ? '+' : '-', s->fslots[i], s->fsig[i],
s->frsig[i]);
if (k > 0) emit(o, hdr, (size_t)k);
if (s->fllen[i] > 0)
emit(o, s->ftext + s->floff[i], (size_t)s->fllen[i]);
else
reply(o, "?");
reply(o, " ");
emit(o, s->ftext + s->fnoff[i], (size_t)s->fnlen[i]);
reply(o, "\n");
}
if (s->ftotal > s->fn) {
char more[64];
int k = snprintf(more, sizeof more, "... %d\n", s->ftotal - s->fn);
if (k > 0) emit(o, more, (size_t)k);
}
reply(o, ".\n");
return;
}
/* Which of a frame's slots have been bound at the point it stopped. One
* line per slot: the index and [+] or [-].
*
* The daemon asks this before it builds a thunk, and that order is the
* safety: an unbound slot is a null address, a thunk that rendered one
* would dereference it, and a program stopped in a break loop is the last
* place to take a fault. It is answered from the snapshot, so the set the
* daemon is told about is the set the thunk will resolve against.
*
* It says nothing about *what* a slot holds, or what it is called. Those
* are facts about the build, and the daemon owns the build — [Tast.fn]
* carries [slots] and [snames] beside each other. Sending them from here
* would be a second copy of them that could drift. */
if (strncmp(line, "locals ", 7) == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(o, "err no frame snapshot\n"); return; }
char *end = NULL;
long at = strtol(line + 7, &end, 10);
if (end == line + 7) { reply(o, "err locals wants a frame index\n"); return; }
if (at < 0 || at >= s->fn) { reply(o, "err no frame at that index\n"); return; }
if (s->fslots[at] == 0) {
reply(o, "err that frame records no slots; it has no named local, or "
"this build does not record them\n");
return;
}
for (int32_t i = 0; i < s->fslots[at]; i++) {
char l[32];
int k = snprintf(l, sizeof l, "%d %c\n", i,
flan_dev_frame_slot(s->fframe[at], i) ? '+' : '-');
if (k > 0) emit(o, l, (size_t)k);
}
reply(o, ".\n");
return;
}
/* Take the i'th, optionally checking that the caller and this snapshot
* still agree on what the i'th is called. The name is not the lookup -
* that is the bug - it is a receipt: a client that listed, prompted, and
* chose has the name in hand for nothing, and sending it turns a bare
* integer into something that can be wrong out loud. */
if (strncmp(line, "restart-at ", 11) == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
char *end = NULL;
long idx = strtol(line + 11, &end, 10);
if (end == line + 11) { reply(o, "err restart-at wants an index\n"); return; }
if (idx < 0 || idx >= s->n) {
reply(o, "err no restart at that index\n");
return;
}
while (*end == ' ') end++;
if (*end != '\0') {
size_t k = strlen(end);
if (k != (size_t)s->len[idx] ||
memcmp(end, s->names + s->off[idx], k) != 0) {
reply(o, "err that index is now ");
reply(o, s->names + s->off[idx]);
reply(o, ", not what you named; list the restarts again\n");
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]) {
/* Refused, with the reason, rather than accepted and dropped. The
* transfer would unwind to the thunk this break is inside and stop
* there, and the program would carry on as if nothing had been
* chosen. */
reply(o, "err restart ");
reply(o, s->names + s->off[idx]);
reply(o, " is below the evaluation this break is inside, so a "
"transfer to it has nowhere to land; choose one offered "
"above it, or abort\n");
return;
}
atomic_store(&chosen_index, (int)idx);
atomic_store(&chosen_gen, s->gen);
/* Published last, so the game thread never reads an index that is about
* to change, or one whose generation has not arrived yet. */
atomic_store(&chosen_ready, 1);
reply(o, "ok\n");
return;
}
/* By name, still, for a person at a raw socket - and now defined as
* exactly [restart-at] on the first index offering the name, which is
* what §4's walk already meant. So the two verbs cannot disagree, and a
* shadowed name is reachable through the other one rather than nowhere. */
if (strncmp(line, "restart ", 8) == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
size_t k = strlen(line + 8);
if (k == 0) { reply(o, "err bad restart name\n"); return; }
int32_t at = -1;
for (int32_t i = 0; i < s->n && at < 0; i++)
if ((size_t)s->len[i] == k && memcmp(s->names + s->off[i], line + 8, k) == 0)
at = i;
if (at < 0) {
reply(o, "err no restart named ");
reply(o, line + 8);
reply(o, " is active\n");
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]) {
reply(o, "err restart ");
reply(o, line + 8);
reply(o, " is below the evaluation this break is inside, so a "
"transfer to it has nowhere to land; choose one offered "
"above it, or abort\n");
return;
}
atomic_store(&chosen_index, at);
atomic_store(&chosen_gen, s->gen);
atomic_store(&chosen_ready, 1);
reply(o, "ok\n");
return;
}
if (strcmp(line, "abort") == 0) {
if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; }
reply(o, "ok\n");
atomic_store(&aborting, 1);
return;
}
/* "watch on" / "watch off" arm and disarm the table; bare "watch" reads it.
*
* Arming is a message rather than something the daemon infers, because the
* program is the writer and it has to be told. Nothing writes the table
* while it is off, which is the whole of "watching costs nothing when nobody
* is watching" — see flan_dev.c. */
if (strcmp(line, "watch on") == 0 || strcmp(line, "watch off") == 0) {
flan_dev_watch_enable(line[6] == 'o' && line[7] == 'n');
reply(o, "ok\n");
return;
}
/* "watch reset" opens a new window for the numeric accumulators: their
* count, range and mean start again from the next sample, while a text slot
* is untouched.
*
* A separate command rather than a side effect of the read, which was the
* tempting shape and is the wrong one. A destructive read makes *looking*
* change what is there, so anything that polls — a test's [await], a second
* editor, a person pressing the read twice — silently shortens the window
* and gets a count that is noise. Reading is free and resetting is a
* decision; the editor makes it once per tick, after the read. */
if (strcmp(line, "watch reset") == 0) {
flan_dev_watch_reset();
reply(o, "ok\n");
return;
}
if (strcmp(line, "watch") == 0) {
/* One line per slot: NAME<tab>VALUE. The name cannot contain a tab — it is
* a C identifier-ish string the program passed — and the value cannot
* contain a raw tab or newline, because everything that reaches it goes
* through an emitter that escapes both. So no framing is needed beyond
* this, which is the same bet render_locals makes on the same grounds.
*
* A header first: the count actually written, and how many names were
* whether any name ever found no slot, so an overflow is reported rather
* than showing up as a value that never appears. A flag rather than a
* count, because the count would be of *writes* — see flan_dev.c. */
uint64_t ncap = flan_dev_watch_name_cap();
uint64_t vcap = flan_dev_watch_val_cap();
char *nb = malloc((size_t)ncap + 1);
char *vb = malloc((size_t)vcap + 1);
if (nb == NULL || vb == NULL) {
free(nb); free(vb);
reply(o, "err out of memory reading the watch table\n");
return;
}
/* Allocating here is fine, for [result]'s reason: this is the listener
* thread. The table the game thread writes is a fixed static. */
uint32_t n = flan_dev_watch_count();
char hdr[64];
int k = snprintf(hdr, sizeof hdr, "%lu %d\n", (unsigned long)n,
flan_dev_watch_overflowed());
if (k > 0) emit(o, hdr, (size_t)k);
for (uint32_t i = 0; i < n; i++) {
uint64_t vlen = 0;
/* A torn slot is *still listed*, with an empty value. Dropping the row
* would make the buffer's rows move under the reader every time the
* game happened to be mid-write, which is far worse to look at than one
* value that is blank for a tick. */
flan_dev_watch_read(i, nb, ncap, vb, vcap, &vlen);
emit(o, nb, strlen(nb));
emit(o, "\t", 1);
if (vlen > 0) emit(o, vb, (size_t)vlen);
emit(o, "\n", 1);
}
free(nb); free(vb);
return;
}
/* How many stopped-only jobs the game thread has dropped for arriving at a
* frame boundary instead of at a break, and the sentence to say about it.
* Answered while running and while stopped alike — it is a count of things
* that have already happened, not a claim about the program's state now.
*
* The count leads and the text follows, because the daemon needs the count
* to decide and the text only to speak. It reads this before it delivers a
* stopped-only module and again while it waits for the value, and a number
* that moved in between is the answer. */
if (strcmp(line, "refusals") == 0) {
char hdr[32];
int k = snprintf(hdr, sizeof hdr, "%llu\n",
(unsigned long long)atomic_load(&refused_while_running));
if (k > 0) emit(o, hdr, (size_t)k);
reply(o, RESUMED);
reply(o, "\n");
return;
}
if (strcmp(line, "result") == 0) {
uint64_t gen = 0, len = 0;
uint64_t cap = flan_dev_result_cap();
char *v = malloc(cap ? (size_t)cap : 1);
if (v == NULL) { reply(o, "err out of memory reading the result\n"); return; }
/* Allocating here is fine and is the distinction that matters: this runs
* on the listener thread, or on the compiler thread in a merged build.
* What the game thread writes into is a fixed static in flan_dev.c
* precisely so that *it* allocates nothing. */
flan_dev_result_read(v, cap, &gen, &len);
char hdr[64];
int k = snprintf(hdr, sizeof hdr, "%llu %llu\n", (unsigned long long)gen,
(unsigned long long)len);
if (k > 0) {
emit(o, hdr, (size_t)k);
if (len > 0) emit(o, v, (size_t)len);
}
free(v);
return;
}
/* "reg at ADDR" — what the allocation registry records for one address.
*
* The reader's side of the table, and the one question the renderer's own
* two cannot answer. Inside a thunk the type at the far end of a pointer is
* already static — (Ptr Enemy) says Enemy — so [reg-live] and [reg-emit]
* only ever needed permission. Somebody pointing at a bare address has no
* (Ptr T) to read a type off, and the recorded name is the whole of what
* there is to go on.
*
* It used to say here that this is answered while the program runs as well
* as while it is stopped, on the grounds that a table is not a stack. The
* table is not a stack and it is still written by the other thread, and the
* answer to one address is a claim that stops being true as it is made — so
* the gate below now says what the daemon already said.
*
* ADDR is read with base 0, so both 0x-hex and decimal arrive; an editor
* that has an address as text has it in one of those two spellings. */
if (strncmp(line, "reg at ", 7) == 0) {
const char *type = NULL;
int64_t typelen = 0, off = 0, bytes = 0, elem = 0, seq = 0, died = 0;
char *end = NULL;
unsigned long long a;
/* Stopped only, like every break verb. Whether an address is still live is
* exactly what a running program is changing, so the answer would describe
* a table the game thread has already moved on from — the daemon refuses
* the question for that reason before it ever reaches here
* (lib/dev.ml, [inspect_addr]), and this says the same thing to anything
* else that asks. The two listing verbs below are the opposite case and
* stay answerable while running: a breakdown is a description of the
* program and not a claim about one address, and the table carries its own
* seqlock so that reading it while it is written is safe. */
if (!(atomic_load(&depth) > 0)) {
reply(o, "err not stopped: whether one address is still live is what a "
"running program is changing, so this is read from a stopped "
"one\n");
return;
}
if (!flan_dev_reg_enabled()) {
reply(o, "err the allocation registry is off; this is not a dev build\n");
return;
}
a = strtoull(line + 7, &end, 0);
if (end == line + 7 || a == 0) {
reply(o, "err reg at wants an address\n");
return;
}
if (!flan_dev_reg_at((const void *)(uintptr_t)a, &type, &typelen, &off,
&bytes, &elem, &seq, &died)) {
/* Never heard of it, which is a fact and not a failure: a stack local,
* a global, or a pointer from C. The daemon says which of those it
* might be; this says only that the table has nothing. */
reply(o, "none\n");
return;
}
{
char hdr[128];
int k = snprintf(hdr, sizeof hdr, "ok %d %lld %lld %lld %lld %lld\t",
died == 0 ? 1 : 0, (long long)off, (long long)bytes,
(long long)elem, (long long)seq, (long long)died);
if (k > 0) emit(o, hdr, (size_t)k);
/* Last, and after a tab, because a type spelling holds spaces —
* "(Vec i32)" — and nothing else on the line does. It cannot hold a tab
* or a newline: it is Types.to_string of a type the programmer wrote. */
if (typelen > 0) emit(o, type, (size_t)typelen);
reply(o, "\n");
}
return;
}
/* "reg types" — the whole table grouped by type spelling; "reg leaks" — the
* same walk with the dead left out.
*
* One verb would have done with a flag, and two exist because the two
* questions are asked at different moments and read differently: a
* breakdown is "what is this program made of", a leak report is "what is
* still held". The *walk* is one function in flan_dev.c for exactly that
* reason — two of them would drift.
*
* A header first, like [watch]: how many rows follow, and whether the table
* ever overflowed. The second is not decoration — an overflowed table has
* blocks in the program that are in nobody's row, so every number below is
* a floor, and a reader that could not tell would quote them as counts. */
if (strcmp(line, "reg types") == 0 || strcmp(line, "reg leaks") == 0) {
enum { REG_ROWS = 256 };
/* Allocated here and freed before the reply is finished, not declared as
* static arrays. 256 rows of four words is 8KB, and static would put that
* 8KB in the BSS of every build this package is linked into — including a
* release build of a game that imports the agent, which never writes a
* row. That is flan_dev.c's own argument against a fixed table, at a
* thirty-second of the size, and it is the same rule: a release build
* carries a null pointer and the declarations.
*
* Allocating is legal here for [watch]'s reason and no other: this runs
* on the listener thread, or on the compiler thread in a merged build.
* The table the game thread writes is a fixed static in flan_dev.c
* precisely so that *it* allocates nothing. */
int64_t *counts = malloc(REG_ROWS * sizeof *counts);
int64_t *bytes = malloc(REG_ROWS * sizeof *bytes);
int64_t *typelens = malloc(REG_ROWS * sizeof *typelens);
const char **types = malloc(REG_ROWS * sizeof *types);
int64_t n, i, unread = 0;
int live_only = line[4] == 'l';
if (counts == NULL || bytes == NULL || typelens == NULL || types == NULL) {
free(counts); free(bytes); free(typelens); free(types);
reply(o, "err out of memory reading the allocation registry\n");
return;
}
if (!flan_dev_reg_enabled()) {
free(counts); free(bytes); free(typelens); free(types);
reply(o, "err the allocation registry is off; this is not a dev build\n");
return;
}
n = flan_dev_reg_by_type(live_only ? 1 : 0, counts, bytes, types, typelens,
REG_ROWS, &unread);
/* A read that did not settle is refused in a sentence rather than sent as
* a header saying zero rows. The header cannot carry the difference: a row
* count is a number, and the number that means "I could not look" is the
* same number that means "nothing is held" — which is the answer this verb
* is most often asked to disprove. An [err] line is already a shape the
* daemon turns into the editor's message, so the distinction survives all
* the way to the person who asked.
*
* Both sentences name the *program's* behaviour and not the mechanism,
* because the mechanism is not actionable and the behaviour is: a table
* being rewritten this hard is a game thread allocating flat out, and the
* useful next move is to ask again, or to ask it while stopped. */
if (n < 0) {
free(counts); free(bytes); free(typelens); free(types);
if (unread > 0) {
char msg[192];
int k = snprintf(msg, sizeof msg,
"err the allocation registry could not be read whole: "
"%lld slot%s were being written through every one of "
"eight attempts, so any rows here would be missing "
"blocks; ask again\n",
(long long)unread, unread == 1 ? "" : "s");
/* Clamped, because [snprintf] returns the length it wanted rather
* than the length it wrote — [flan_dev_reg_emit]'s rule. */
if (k > (int)sizeof msg - 1) k = (int)sizeof msg - 1;
if (k > 0) emit(o, msg, (size_t)k);
} else {
reply(o, "err the allocation registry was being rearranged through "
"every one of eight attempts; no listing taken, rather than "
"one of a table that never existed — ask again\n");
}
return;
}
{
char hdr[64];
int k = snprintf(hdr, sizeof hdr, "%lld %d\n",
(long long)(n < REG_ROWS ? n : REG_ROWS),
flan_dev_reg_overflowed());
if (k > 0) emit(o, hdr, (size_t)k);
}
for (i = 0; i < n && i < REG_ROWS; i++) {
char row[64];
int k = snprintf(row, sizeof row, "%lld %lld\t", (long long)counts[i],
(long long)bytes[i]);
if (k > 0) emit(o, row, (size_t)k);
if (typelens[i] > 0) emit(o, types[i], (size_t)typelens[i]);
reply(o, "\n");
}
free(counts); free(bytes); free(typelens); free(types);
return;
}
/* A module that may only run from a break, and the word for it goes in front
* of the path rather than inside the module. The alternative was a fourth
* exported symbol beside [flan_reload_transient], which would have put the
* decision in both emitters and in the .so's ABI — for a fact that is not
* about the module's *contents* at all. What is stopped-only is the
* *question*, not the code: the same rendering machinery, rooted at a frame
* slot instead of a raw address, is perfectly safe to run while the program
* runs. So it travels with the request. See [job].
*
* Everything after the prefix is the ordinary path, gate included, so a
* stopped-only module with no installer is still refused for having no
* installer. */
int stopped_only = 0;
if (strncmp(line, "stopped-only ", 13) == 0) {
stopped_only = 1;
line += 13;
}
/* Before the dlopen, not after it: a module there is no room to queue is
* one there is no point relocating, and refusing here means no handle is
* taken for it at all. Only one producer runs at a time, so room seen now is
* room still there at [publish] below. */
if (!queue_room()) {
reply(o, "err reload queue full; the program is not calling "
"agent/poll\n");
return;
}
void *h = dlopen(line, RTLD_NOW | RTLD_LOCAL);
if (h == NULL) {
/* [dlerror] is one-shot and the next dl call may clobber what it returned,
* so the pointer is taken once and used for both the test and the reply. */
const char *err = dlerror();
const char *why = abi_mismatch(err);
reply(o, "err ");
reply(o, why != NULL ? why : err);
reply(o, "\n");
return;
}
install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install");
if (f == NULL) {
/* Closed, and this is not an exception to "nothing is ever dlclosed".
* That rule is about a module something *points into* — a cell holding
* an address in its text. This one published nothing: no installer ran,
* so no cell names it, and it is unreachable the moment this function
* returns. What leaked before was the handle value rather than the
* mapping — dlopen refcounts by path, so re-sending the same bad file
* bumped a count nothing could ever bring down, and the one reference
* that could was dropped on the floor here. */
dlclose(h);
reply(o, "err no flan_reload_install\n");
return;
}
/* Optional: only an expression evaluation has one. */
call_fn c = (call_fn)(uintptr_t)dlsym(h, "flan_reload_call");
/* And only one that leaves nothing behind may be unloaded. */
void *transient =
(c == NULL) ? NULL : dlsym(h, "flan_reload_transient");
/* Answer before queueing, not after. The game thread can install and run
* to completion between the two, and a program that exits there would tear
* down this connection with the reply still unwritten — which reaches the
* sender as a reset, not as an answer. */
reply(o, "ok\n");
/* "queued", not "installed": the store happens on the game thread, at a
* time this thread does not get to choose. The room was checked before the
* dlopen and only this thread consumes it, so this cannot fail; it is
* asserted rather than assumed because a silent [publish] that did nothing
* is the failure being fixed. */
if (!publish((job){ .install = f, .call = c,
.handle = transient == NULL ? NULL : h,
.stopped_only = stopped_only }))
fprintf(stderr, "flan: reload queue full after it was checked\n");
return;
}
/* One connection, one line, one module. Loading here rather than in the game
* thread is the whole reason this thread exists. */
static void serve(int fd) {
/* A deadline on the read. The accept loop is single-threaded and serves each
* connection inline, so a client that connects and then sends nothing - an
* editor killed mid-request, a daemon that crashed between connect and send -
* blocks every later request, including the [abort] that would end a stopped
* program. Worse, the requests the client had already given up on are served
* when its socket finally closes, so an abandoned abort can kill the program
* minutes later against a state that has moved on. Two seconds is generous
* for one line. */
{
struct timeval tv = { 2, 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
}
sink o = { fd, NULL, 0, 0 };
char line[4096];
size_t n = 0;
for (;;) {
ssize_t k = read(fd, line + n, sizeof line - n - 1);
if (k <= 0) return;
n += (size_t)k;
line[n] = '\0';
char *nl = strchr(line, '\n');
if (nl == NULL) {
if (n == sizeof line - 1) { reply(&o, "err path too long\n"); return; }
continue;
}
*nl = '\0';
pthread_mutex_lock(&request_lock);
handle_line(line, &o);
pthread_mutex_unlock(&request_lock);
return;
}
}
/* The direct hand-off. In a [flan dev] build the compiler is a thread in this
* process, so a request that used to be a connect, a write and a read on a
* unix socket looping back into this address space is now this call — the same
* verb table, answered into a buffer instead of onto an fd.
*
* The buffer is malloc'd here and freed by the caller. It is not a static,
* because the socket's accept loop can be answering [status] on another thread
* while this runs, and two answers into one buffer is a race with a plausible
* shape.
*
* The rule this must not break is the one that keeps the collector off the
* frame thread: nothing here calls into OCaml and nothing here runs on the
* game thread. A delivery is left in the ring exactly as the listener leaves
* it, and the game thread picks it up when it reaches a frame boundary. */
char *flan_agent_request(const char *line, uint64_t *len) {
char stack[4096];
sink o = { -1, NULL, 0, 0 };
size_t n = strlen(line);
if (n >= sizeof stack) {
/* The same answer the socket gives for the same input. The point of one
* verb table is that the two callers cannot be told apart, and a silent
* empty reply here would be the first place they could. */
reply(&o, "err path too long\n");
*len = (uint64_t)o.len;
return o.buf;
}
memcpy(stack, line, n + 1);
pthread_mutex_lock(&request_lock);
handle_line(stack, &o);
pthread_mutex_unlock(&request_lock);
*len = (uint64_t)o.len;
return o.buf;
}
void flan_agent_request_free(char *p) { free(p); }
static void *accept_loop(void *arg) {
(void)arg;
for (;;) {
int fd = accept(listen_fd, NULL, NULL);
if (fd < 0) { if (errno == EINTR) continue; return NULL; }
serve(fd);
close(fd);
}
}
/* ── Outliving the daemon, which this program must not do ──────────── */
/* Under [--two-process] the compiled program is a child of the daemon, and
* for the whole of its life the daemon is the only thing that will ever end
* it: the program is a game loop or a listener, it has no reason of its own to
* stop, and lib/dev.ml's [Fun.protect ~finally] is what kills it when the
* session closes. That finally block runs when the daemon exits. It does not
* run when the daemon is SIGKILLed — by a test harness tearing down after a
* failure, by a watchdog that fired, by anybody at all — and what is left
* behind is a program with ppid 1, sleeping, holding a window and a socket,
* waiting for requests from a process that no longer exists. Eight of those
* were found on one machine, the oldest six days old. Nothing would ever have
* woken them: there is no timeout anywhere in this file, and none should be
* added — a program between frames is supposed to wait indefinitely.
*
* So the child is told to die with its parent instead, which on Linux is one
* call. PR_SET_PDEATHSIG asks the kernel to deliver a signal to *this* process
* when the process that is currently its parent dies. It is exactly the right
* primitive and it has exactly one sharp edge, which is that it is armed from
* the child and so is armed a moment *after* the fork: if the parent died in
* that moment, the signal it would have sent has already not been sent, and
* the child waits forever having done everything right. The standard answer is
* the one below — arm, then ask who the parent is now, and if it is not the
* one that was expected, deliver the signal by hand. [getppid() == 1] is the
* form this idiom is usually written in and it is wrong here: under a systemd
* user session, or in a container with a subreaper, an orphan is reparented to
* the subreaper and not to init, so the test would pass and the child would
* still be stranded. The daemon therefore puts its own pid in the environment
* and this compares against that, which cannot be fooled by who does the
* adopting.
*
* FLAN_DEV_PARENT is a positive gate and is set by [two_process] and by
* nothing else. A merged build runs this same file and must never arm any of
* this: there the daemon *is* this process, its parent is whoever typed [flan
* dev] — a shell, an emacs, a terminal that is about to be closed — and none
* of those is the session's owner. A person who starts a daemon in one
* terminal and attaches an editor to it from another is doing a normal thing,
* and killing their live session because the shell exited would be a worse bug
* than the leak. Gating on the *absence* of a merged-build variable would have
* covered the same cases today and quietly stopped covering them the first
* time somebody added a third shape; this way the only process that arms is
* the one the daemon explicitly told to.
*
* The one gap left is the window before [agent/start]: a child SIGKILLed out
* from under during the first few milliseconds of its own startup is still
* stranded, because nothing has armed yet. It is not closed here because
* closing it means arming from flan_rt.c, which every program links and which
* would put a Linux-only prctl in the one file that also has to compile for
* wasm. The daemon already refuses to run a program that never reaches
* [agent/start], so every child that lives long enough to matter passes
* through here. */
#if defined(__linux__) && defined(SIGPWR)
#define FLAN_ORPHAN_SIG SIGPWR
#endif
/* Stashed at arming time rather than read from the environment in the handler:
* getenv is not async-signal-safe, and this path can run on any thread at any
* instruction. sun_path's size is the bound because that is where it came
* from. */
static char orphan_sock[sizeof(((struct sockaddr_un *)0)->sun_path)];
#if defined(FLAN_ORPHAN_SIG)
/* Three calls, and the restraint is the point. This runs on whichever thread
* the kernel picked, which may be the game thread halfway through a printf,
* holding stdio's lock — so fprintf here would deadlock against itself and
* fflush(NULL) would deadlock against the other thread. write, unlink and
* _exit are on the async-signal-safe list and nothing else here is.
*
* Not flushing is a real cost and a small one: flan_rt_init sets stdout line
* buffered precisely so that a program's output is observable as it runs, so
* what is lost is at most a partial line. Losing it beats hanging.
*
* The socket is unlinked because this process bound it and no other process
* knows it is stale — the daemon that would have cleaned up is the thing that
* just died. Its directory is left alone: that belongs to the daemon, and
* tidying up someone else's directory from a signal handler is how a signal
* handler becomes a bug.
*
* Status 0, because nothing went wrong. The program did what it was told for
* as long as there was anybody to tell it. */
static void orphan_die(int sig) {
static const char said[] =
"flan dev: the daemon that owns this program is gone, so the program is "
"stopping too\n";
ssize_t ignored;
(void)sig;
ignored = write(2, said, sizeof said - 1);
(void)ignored;
if (orphan_sock[0] != '\0') unlink(orphan_sock);
_exit(0);
}
#endif
static void watch_the_daemon(const char *sock) {
#if defined(FLAN_ORPHAN_SIG)
const char *want_s = getenv("FLAN_DEV_PARENT");
char *end;
long want;
struct sigaction sa;
if (want_s == NULL || want_s[0] == '\0') return;
want = strtol(want_s, &end, 10);
if (end == want_s || *end != '\0' || want <= 0) return;
/* SIGPWR and not SIGTERM, and the collision it avoids is not hypothetical:
* lib/dev.ml's ordinary teardown kills this child with SIGTERM, and in
* --two-process the child's stderr *is* the daemon's stderr. A handler on
* SIGTERM would print "the daemon is gone" into the daemon's own output on
* every clean session close, which is both noise and a sentence that is only
* half true. A signal the program will never otherwise receive keeps the two
* deaths distinguishable, and leaves SIGTERM on its default disposition
* where the existing teardown already relies on it. */
memset(&sa, 0, sizeof sa);
sa.sa_handler = orphan_die;
sigfillset(&sa.sa_mask);
if (sigaction(FLAN_ORPHAN_SIG, &sa, NULL) != 0) return;
strncpy(orphan_sock, sock, sizeof orphan_sock - 1);
if (prctl(PR_SET_PDEATHSIG, FLAN_ORPHAN_SIG) != 0) return;
/* The race, closed. Armed above, so from here on the kernel will tell us;
* this asks whether it already should have. */
if ((long)getppid() != want) raise(FLAN_ORPHAN_SIG);
#else
(void)sock;
#endif
}
/* [path] is a Flan string: ptr and len, not NUL-terminated.
*
* FLAN_AGENT_SOCKET overrides it. A program's source has to name some path,
* and the daemon that launches the program is the one that knows where it
* wants to talk to it — without the override the daemon would have to guess,
* and guessing wrong fails silently: everything compiles, the module is built,
* and nothing ever receives it. */
int32_t flan_agent_start(const uint8_t *path, int64_t len) {
struct sockaddr_un addr;
const char *env = getenv("FLAN_AGENT_SOCKET");
if (atomic_exchange(&started, 1)) return 0;
if (env != NULL && env[0] != '\0') {
path = (const uint8_t *)env;
len = (int64_t)strlen(env);
}
if (len <= 0 || (size_t)len >= sizeof addr.sun_path) return -1;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
memcpy(addr.sun_path, path, (size_t)len);
unlink(addr.sun_path);
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (listen_fd < 0) return -1;
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof addr) < 0) return -1;
if (listen(listen_fd, 4) < 0) return -1;
if (pthread_create(&listener, NULL, accept_loop, NULL) != 0) return -1;
/* After the bind, because the path this hands over is the one that was
actually bound — the environment's override included — and 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(addr.sun_path);
/* 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. */
flan_break_hook = break_loop;
flan_trap_hook = trap_stop;
return 0;
}