flan/vendor/agent/flan_agent.c
Joseph Ferano ff2bd1da12 A socket file must not outlive the process that bound it
The merged daemon now unlinks its socket on the ways out it does not
control as well as the one it does: an atexit for exit(3), which is what a
runtime trap takes, and by hand in die_now and in main's fallback, which
are _exit and skip the chain on purpose.

The client says so too. A refusal on a path that exists is a leftover, not
a daemon declining, and the raw 'Connection refused' has now misdirected
two investigations.

Separately, and it is separate: dev-repl.flan gets dev-robust.flan's
24000-tick budget. Twenty seconds of program under a two-minute test is a
second flake waiting its turn, and it is not the one fixed above -- that
one fails honestly, saying the program exited.
2026-09-14 08:11:35 +07:00

1333 lines
62 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 <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>
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);
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. */
typedef struct { install_fn install; call_fn call; void *handle; } job;
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);
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 */
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(void) {
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->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);
}
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
void *xfer) {
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()) {
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;
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->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->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);
}
}
/* 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);
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];
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
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;
}
}
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->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;
}
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.
*
* Answered while the program is running as well as while it is stopped:
* this reads a table, not a stack, and nothing here walks a chain another
* thread is pushing. Whether the *answer* holds still long enough to be
* worth acting on is the caller's judgement, and the daemon makes it.
*
* 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;
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;
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);
{
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;
}
/* 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){ f, c, transient == NULL ? NULL : h }))
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);
}
}
/* [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;
/* 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;
return 0;
}