The 4K result cap and condition_name[128] are on the agent's socket path, which is why the sanitizer corpus cannot reach them: a program in the sweep has no socket and nobody on the other end of it. test_agent has both. A 5000-byte string literal evaluated into the running program comes back as exactly 4096 bytes ending in the ellipsis result_end puts there to say it clamped — and it comes back through the seqlock's copy, so the cap and the new reader are pinned by the same case. The header also shows the generation as 1, which is the count of complete values rather than the raw counter. A condition class of 198 characters comes back from `status` as 127 and a terminator. Aborting out of that break is what pins the exit status at 134 now that the loop leaves with _exit rather than exit. SNAP_MAX, SNAP_NAMES and the dev registry's overflow guard are still read rather than tested. Sixty-five nested restart-cases and four thousand interned names are a lot of program to write for a clamp each, and neither is on a path this session changed. flan_dev_result_cap() exists so the size is asked for rather than written down in two files: "the copy is never truncated" is only true while the agent's buffer and the runtime's bound agree, and the agent checks that where the copy happens. The pipe the queue program blocks on is close-on-exec, or the child inherits the write end and its own stdin never reaches end of file — it sat in its last read waiting for a byte only it could send.
742 lines
33 KiB
C
742 lines
33 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.
|
|
*/
|
|
|
|
#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. The buffer below is RESULT_MAX, so
|
|
* the copy is never truncated; a read that loses the race reports the last
|
|
* complete generation and no bytes, which leaves the daemon polling rather
|
|
* than showing it half a value. */
|
|
#define RESULT_MAX 4096
|
|
int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen,
|
|
uint64_t *len);
|
|
uint64_t flan_dev_result_cap(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 — this thread — 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);
|
|
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;
|
|
|
|
/* 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 */
|
|
|
|
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];
|
|
} 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) {
|
|
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++;
|
|
}
|
|
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);
|
|
_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;
|
|
restart_floor = flan_restart_count();
|
|
j.call();
|
|
restart_floor = outer;
|
|
}
|
|
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();
|
|
}
|
|
|
|
/* 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 reply(int fd, const char *s) {
|
|
size_t n = strlen(s);
|
|
while (n > 0) {
|
|
ssize_t k = send(fd, s, n, MSG_NOSIGNAL);
|
|
if (k <= 0) return;
|
|
s += k;
|
|
n -= (size_t)k;
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
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(fd, "err path too long\n"); return; }
|
|
continue;
|
|
}
|
|
*nl = '\0';
|
|
/* 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(fd, "stopped ");
|
|
reply(fd, condition_name);
|
|
reply(fd, "\n");
|
|
} else
|
|
reply(fd, "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(fd, "err not stopped\n"); return; }
|
|
snapshot *s = snap_top();
|
|
if (s == NULL) { reply(fd, "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) send(fd, hdr, (size_t)k, MSG_NOSIGNAL);
|
|
send(fd, s->names + s->off[i], (size_t)s->len[i], MSG_NOSIGNAL);
|
|
reply(fd, "\n");
|
|
}
|
|
reply(fd, ".\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(fd, "err not stopped\n"); return; }
|
|
snapshot *s = snap_top();
|
|
if (s == NULL) { reply(fd, "err no restart snapshot\n"); return; }
|
|
char *end = NULL;
|
|
long idx = strtol(line + 11, &end, 10);
|
|
if (end == line + 11) { reply(fd, "err restart-at wants an index\n"); return; }
|
|
if (idx < 0 || idx >= s->n) {
|
|
reply(fd, "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(fd, "err that index is now ");
|
|
reply(fd, s->names + s->off[idx]);
|
|
reply(fd, ", 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(fd, "err restart ");
|
|
reply(fd, s->names + s->off[idx]);
|
|
reply(fd, " 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(fd, "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(fd, "err not stopped\n"); return; }
|
|
snapshot *s = snap_top();
|
|
if (s == NULL) { reply(fd, "err no restart snapshot\n"); return; }
|
|
size_t k = strlen(line + 8);
|
|
if (k == 0) { reply(fd, "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(fd, "err no restart named ");
|
|
reply(fd, line + 8);
|
|
reply(fd, " is active\n");
|
|
return;
|
|
}
|
|
if (!s->reachable[at]) {
|
|
reply(fd, "err restart ");
|
|
reply(fd, line + 8);
|
|
reply(fd, " 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(fd, "ok\n");
|
|
return;
|
|
}
|
|
if (strcmp(line, "abort") == 0) {
|
|
if (!(atomic_load(&depth) > 0)) { reply(fd, "err not stopped\n"); return; }
|
|
reply(fd, "ok\n");
|
|
atomic_store(&aborting, 1);
|
|
return;
|
|
}
|
|
if (strcmp(line, "result") == 0) {
|
|
uint64_t gen = 0, len = 0;
|
|
char v[RESULT_MAX];
|
|
/* The one thing that could make the copy truncate, checked where it
|
|
* would happen rather than trusted to two files holding the same
|
|
* number. */
|
|
if (flan_dev_result_cap() > sizeof v) {
|
|
reply(fd, "err the agent's result buffer is smaller than the "
|
|
"runtime's\n");
|
|
return;
|
|
}
|
|
flan_dev_result_read(v, sizeof v, &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) {
|
|
send(fd, hdr, (size_t)k, MSG_NOSIGNAL);
|
|
if (len > 0) send(fd, v, (size_t)len, MSG_NOSIGNAL);
|
|
}
|
|
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 this thread produces, so room seen now is room
|
|
* still there at [publish] below. */
|
|
if (!queue_room()) {
|
|
reply(fd, "err reload queue full; the program is not calling "
|
|
"agent/poll\n");
|
|
return;
|
|
}
|
|
void *h = dlopen(line, RTLD_NOW | RTLD_LOCAL);
|
|
if (h == NULL) {
|
|
reply(fd, "err ");
|
|
reply(fd, dlerror());
|
|
reply(fd, "\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(fd, "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(fd, "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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|