flan/vendor/agent/flan_agent.c
Joseph Ferano 0c9f043bb1 Stamp a choice with the break it was chosen from
The snapshot made the listing stand still; it did not make the handoff safe.
A choice is validated against the snapshot on top when the request lands and
resolved against the snapshot on top when the game thread next looks, and
between those the loop runs evaluations — one that errors pushes a break of
its own, whose loop reaches the flag first and takes its own index 2 for the
one somebody picked off the outer list. That is the failure this change exists
to end, arriving by a different door.

So each snapshot carries a generation, a choice carries the generation it was
validated against, and a loop claims only what is addressed to it. A mismatch
is left set rather than dropped: the listener already answered ok, so the break
it was meant for must still be able to take it. Depth could not do this — an
outer break resuming and a new one starting reuses the number. The snapshot is
also popped before the depth comes down now, so the two never describe
different breaks.

The client's own new path gets tested too: the candidate table is pure, so the
shapes a real daemon will not easily produce are checked directly, and the
break-and-resume test now goes through restart-at rather than by name.
flan-dev-unreachable-restarts was dead on arrival — flan-break reads
:unreachable off its own reply — and is gone.
2026-09-12 05:02:14 +07:00

648 lines
29 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 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.
*
* 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);
const char *flan_dev_result_get(uint64_t *gen, uint64_t *len);
/* 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. Overflow drops the oldest request rather than stalling; a dev
* loop that queues 64 reloads between two frames has a bigger problem. */
#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;
static void publish(job j) {
unsigned h = atomic_load_explicit(&head, memory_order_relaxed);
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);
}
/* 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);
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);
exit(134);
}
{
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);
exit(134);
}
for (;;) {
flan_agent_poll();
if (atomic_load(&aborting)) {
fflush(stdout);
fprintf(stderr, "flan: aborted at the break loop\n");
exit(134);
}
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;
const char *v = flan_dev_result_get(&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;
}
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) { 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. */
publish((job){ f, c, transient == NULL ? NULL : h });
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;
}