/* 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 #include #include #include #include #include #include #include #include #include #include #include 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; }