It read the generation, then a non-atomic length, then returned the buffer itself — and the agent sent those bytes down a socket some time later, while the game thread was free to be a hundred bytes into the next value. A seqlock cannot validate a read that finishes after it returns, so the pointer was the bug and not the ordering. Made into a real one rather than documented down to what it guaranteed, because what it guaranteed was nothing: the generation told the daemon a new value had arrived and said nothing about whether the bytes it then read were that value. Writing the honest comment would have left the daemon's only way of reading a result unsound with a note beside it. The counter is odd for exactly as long as a value is being written. flan_dev_result_read copies into the caller's buffer and checks the counter either side of the copy, retrying if it moved; a reader that loses the race reports the last complete generation and no bytes, so a daemon polling for a new value keeps polling rather than being shown half of one. The count handed out is the number of complete values, so "has it moved" still means what lib/dev.ml takes it to mean. The agent's buffer is RESULT_MAX, so the copy is never truncated. The race itself has no regression test. Arranging it means landing a socket read inside a render thunk from outside the process, which is the same hook the snapshot generation wants. What is tested is that eval still reads back the value it rendered, through test_dev's existing cases.
655 lines
29 KiB
C
655 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);
|
|
|
|
/* 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);
|
|
|
|
/* 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;
|
|
char v[RESULT_MAX];
|
|
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;
|
|
}
|
|
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;
|
|
}
|