/* flan_agent — the half of the dev loop that lives in the running program. * * A redefinition arrives as a path to a .so (runtime/flan_dev.c and * Emit.redefinition are what put it there). Two things have to happen to it, * and they must happen on different threads: * * dlopen relocates the module and runs the loader. It is milliseconds, * unbounded, and takes the loader lock. Doing it on the game thread * is a dropped frame. * install is one store per redefined function. It is sub-microsecond, and * it must happen at a point where no redefined function is on the * stack — a frame boundary — or a frame runs half in the old code * and half in the new. * * So the listener thread does the loading and hands over a function pointer; * the game thread calls flan-poll or flan-wait when it is between frames and * that is when the swap becomes visible. Nothing else in the program needs to * know the agent exists. * * Nothing that published anything is ever dlclosed: a cell holds an address * inside a module's text, and unloading it would leave every call site * pointing at unmapped memory. The two modules that are closed are the ones * nothing can point into — a transient thunk, which installs no bodies, and a * module refused before it was queued. * * This is not a protocol. One line per request, the path to load, and a one * line answer. The daemon and its nREPL are a separate program that will speak * to a socket, not something this file grows into. * * The compiler is no longer a separate *process*, though. [flan dev] builds one * binary that is the program and holds the whole compiler, so a request from it * is a call to [flan_agent_request] below and not a connect on a socket that * loops back into this address space. It is the same verb table either way — * [handle_line] writes into a [sink] that is an fd or a buffer — and the same * hand-off: a module is published to the ring, and the game thread installs it * at a frame boundary. The socket stays for [--two-process] and for a person * with nc. */ #include #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); /* The value of the last evaluated expression, copied out under the seqlock in * runtime/flan_dev.c rather than borrowed: a reader gets bytes of its own, and * a read that loses the race reports the last complete generation and no * bytes, which leaves the compiler polling rather than showing it half a * value. * * How big that copy has to be is the runtime's number, and it is asked for * rather than written down here as well. This file used to carry its own copy * of the bound and a check that the two had not drifted — which is what you do * when one of them is sizing a buffer to send through. Nothing here sends * anything any more, so the bound belongs to whoever owns the storage, and * changing it is one file's decision. */ int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen, uint64_t *len); uint64_t flan_dev_result_cap(void); /* The watch table, same arrangement and for the same reason: the game thread * writes it mid-frame into fixed storage that belongs to flan_dev.c, and this * file only ever copies out of it, on the listener thread. The one addition is * [enable] — the table is written only while somebody is reading it, so * opening and closing a watch buffer is a message that arrives here. */ void flan_dev_watch_enable(int on); int flan_dev_watch_enabled(void); /* And [reset], which opens a new accumulation window for the numeric slots. * It moves one counter and touches no slot, so the game thread stays the only * writer of the table. */ void flan_dev_watch_reset(void); uint32_t flan_dev_watch_count(void); int flan_dev_watch_overflowed(void); int flan_dev_watch_read(uint32_t i, char *nd, uint64_t ncap, char *vd, uint64_t vcap, uint64_t *vlen); uint64_t flan_dev_watch_name_cap(void); uint64_t flan_dev_watch_val_cap(void); /* The allocation registry, read the same way and on the same thread. The * renderer's two questions — is this address live, and what died here — are * asked from inside a compiled thunk and never come through this file; these * three are the *reader's* side, which has no (Ptr T) to read a type off and * so has to ask the table what it recorded. * * Formatting lives here rather than in flan_dev.c for the reason [watch] and * [result] already have: this runs on the listener thread, where allocating * and snprintf are legal, and what the game thread writes stays a fixed * table nobody has to format to fill. */ int32_t flan_dev_reg_at(const void *p, const char **type, int64_t *typelen, int64_t *off, int64_t *bytes, int64_t *elem, int64_t *seq, int64_t *died); int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts, int64_t *bytes, const char **types, int64_t *typelens, int64_t cap); int flan_dev_reg_enabled(void); int flan_dev_reg_overflowed(void); /* A ring the listener writes and the game thread reads. One producer, one * consumer, so two atomics and no lock — the game thread must never block on * the loader. * * A full ring is *refused*, at the sender, with an error. The previous comment * here claimed it dropped the oldest request, and nothing did that: [publish] * never read [tail], so the 65th module overwrote the slot the game thread was * reading — twenty-four bytes of function pointers, copied field by field with * no atomic anywhere near them, so the consumer could take half of one job and * half of another and call it. Silent corruption of the one thing in this file * that gets *called*. * * Of the three honest answers, refusing is the only one that reaches the * person who asked. Dropping loses a reload the sender was told was ok — the * same lie in quieter clothes. Blocking stalls the accept loop, which serves * connections inline, so a program that has stopped polling would also stop * answering [status] and [abort]: the dev loop would have no way to say * anything to a program that had stopped listening to it. A refusal is a line * the daemon can show, and the fix is to call agent/poll. * * The check is safe to make separately from the store because there is exactly * one producer at a time — [request_lock] is what keeps that true now that the * compiler can call in as well as connect — so room, once seen, cannot be * taken away by anyone: the consumer only ever makes more of it. */ #define QUEUE 64 /* [handle] is set only for a module that declared itself transient — one that * ran a thunk and left nothing behind. Everything else is kept mapped forever: * a cell holds an address inside a module's text, and unloading it would leave * every call site pointing at unmapped memory. */ typedef struct { install_fn install; call_fn call; void *handle; } job; static job queue[QUEUE]; static atomic_uint head; /* written by the listener */ static atomic_uint tail; /* written by the game thread */ static int listen_fd = -1; static pthread_t listener; static atomic_int started; /* Room for one more. Unsigned subtraction, so the answer survives [head] and * [tail] wrapping; only their difference means anything. */ static int queue_room(void) { unsigned h = atomic_load_explicit(&head, memory_order_relaxed); unsigned t = atomic_load_explicit(&tail, memory_order_acquire); return (h - t) < QUEUE; } /* 0 if the ring is full, having published nothing. Checked here as well as at * the caller, because a producer that forgot would otherwise reintroduce * exactly the overwrite this replaced. */ static int publish(job j) { unsigned h = atomic_load_explicit(&head, memory_order_relaxed); unsigned t = atomic_load_explicit(&tail, memory_order_acquire); if (h - t >= QUEUE) return 0; queue[h % QUEUE] = j; /* Release: the store to the slot must be visible before the index that * advertises it. */ atomic_store_explicit(&head, h + 1, memory_order_release); return 1; } /* Returns how many modules were installed. Call it between frames. */ /* ── The break loop, spec-conditions.md §2 ──────────────────────────── */ /* Where "a crash kills the program" stops being true. An unhandled error runs * this instead of rt_die(), on the frame that erred, with nothing unwound — so * the condition and every restart between here and the top are still live. * * It is the *poll* loop, run from here instead of from the frame boundary, and * that is the whole design. An expression evaluated while stopped is a module * the loader thread queues and the game thread runs; if this loop did not * drain that queue, C-x C-e would hang exactly when it is most wanted. * * Installing while stopped is deliberately allowed. The rule that a redefined * function must not be swapped while it is on the stack is about *mid-frame * consistency* — half a frame of old code and half of new — and there is no * frame in progress here. The old body on the stack keeps running; a retry * restart calls through the cell and reaches the new one. That is the fix-it- * and-retry loop, and refusing the install would remove the point. */ /* The runtime's end of it. flan_rt.c holds the hook and the lookup; the loop * itself is here, because it needs the socket and the install queue and the * runtime must not depend on an optional package for either. */ extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition, void *xfer); extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen, void *xfer); extern int32_t flan_restart_count(void); /* The shadow stack (runtime/flan_dev.c). The compiler pushes a frame per Flan * call in a dev build; these read one, and only ever on the thread that owns * it. The frame is opaque here on purpose: its shape belongs to flan_dev.c, * and two files each declaring the struct is how the two stop agreeing. */ extern int32_t flan_dev_frame_count(void); extern void *flan_dev_frame_at(int32_t i); extern const char *flan_dev_frame_name(const void *frame, int64_t *len); extern const char *flan_dev_frame_loc(const void *frame, int64_t *len); extern int32_t flan_dev_frame_nslots(const void *frame); extern int32_t flan_dev_frame_slotsig(const void *frame); extern int32_t flan_dev_frame_refsig(const void *frame); extern void *flan_dev_frame_slot(const void *frame, int32_t i); extern const uint8_t *flan_restart_name(int32_t i, int64_t *len); extern void *flan_restart_frame(int32_t i); extern void flan_restart_take(void *frame, void *xfer); /* -- How far down a transfer can actually land ----------------------- */ /* A thunk run from this loop is called through [flan_reload_call], which * allocates its *own* transfer channel and drops it on return. So a restart * chosen at a break inside a thunk unwinds as far as the thunk and no * further: every frame below that C boundary is on the list, is accepted, and * is silently not taken. Three statements that the program will resume, none * of them true - which is worse than either resuming or refusing. * * The boundary is recorded where it is *made*, at the call, not at the break: * the frames below it are exactly the ones already on the stack when the * thunk started, and a restart-case the thunk enters itself is above it and * reachable. Recording the depth on entering the break loop instead would * count those too and refuse restarts that work. * * Game thread only, saved and restored around the call, so a thunk that * breaks and whose break runs another thunk nests correctly. */ static int32_t restart_floor; /* The same boundary, counted in shadow-stack frames. A break inside a C-x C-e * thunk has that thunk's frames on top of the program's, and "where is my * program" answered with [eval/7] is not the question anyone asked. Recorded * at the call for the same reason [restart_floor] is: the frames below it are * exactly the ones that were already on the stack when the thunk started. */ /* -1, not 0, and the distinction is the whole of it: [restart_floor] can be * zero meaning "no restarts were on the stack when the thunk started", and * outside a thunk zero is also the right answer. Frames are the other way * round — outside a thunk *every* frame is the program's, and zero would say * none of them are. So "not inside a thunk" gets a value of its own. */ static int32_t frame_floor = -1; /* What the listener thread hands the stopped game thread. One slot, because * only one thread is ever stopped. */ /* A *depth*, not a flag. A thunk this loop runs may itself error, and the * break loop that catches that one is nested inside this one — so a flag is * wrong twice over: the inner loop clearing it on resume tells the world the * program is running while the outer loop is still stopped, and every verb * then answers "not stopped" while the outer loop spins forever with no * protocol path out. Only kill recovered it. Counting fixes both. */ static _Atomic int depth; #define BREAK_MAX 8 /* deep enough to nest, shallow * enough that a loop of breaks * stops rather than grinds */ static _Atomic int chosen_index; /* Which snapshot that index is an index *into*. The listener validates a * choice against the snapshot on top when the request arrives, and the game * thread resolves it against the snapshot on top when it next looks - and * those are two reads of a stack that can move between them. An evaluation * this loop runs may error, push a break of its own, and reach [chosen_ready] * first, which would take that break's index 2 for the one someone chose from * the outer list. Depth is not enough to tell them apart: an outer break * resuming and a new one starting reuses the number. A generation does not. * * A mismatch is *left alone* rather than discarded. The listener already * answered ok for it, so the break it was meant for must still be able to * take it; the inner loop simply does not claim what is not addressed to it. */ static _Atomic int chosen_gen; static _Atomic int chosen_ready; static _Atomic int aborting; /* -- The snapshot ---------------------------------------------------- */ /* The stopped thread is not holding still. This loop runs [flan_agent_poll], * which runs arbitrary Flan, and every restart-case that runs pushes and pops * the one global restart list. Serving names straight off that list hands the * listener a pointer into a frame that may already have been popped; serving * *indices* off it is worse still, because an index carries no evidence of * what it meant - the list and the choice can disagree and nothing can tell. * * So the list is read once, on entry, while the thread that owns it is in * this function and not in Flan, and copied: names into a buffer of our own, * frames as the addresses a transfer carries. Everything the listener answers * comes from here and nothing re-reads the live stack. */ #define SNAP_MAX 64 /* restarts offered at one break */ #define SNAP_NAMES 4096 /* bytes of names behind them */ #define FRAME_MAX 64 /* frames listed in a backtrace */ #define FRAME_TEXT 8192 /* bytes of names and locations */ typedef struct { int32_t gen; /* never reused, never 0 */ int32_t n; int32_t total; /* before SNAP_MAX truncated it */ void *frame[SNAP_MAX]; int32_t off[SNAP_MAX], len[SNAP_MAX]; int32_t reachable[SNAP_MAX]; int32_t used; char names[SNAP_NAMES]; /* Where the stopped thread is, taken at the same moment and for the same * reason: the chain is the game thread's, and it is holding still only * because it is parked in this loop. [fframe] is kept as well as the text, * because reading a frame's locals means going back to that frame — and to * that frame rather than to whatever is at index 2 by then. */ int32_t fn; /* frames listed */ int32_t ftotal; /* before FRAME_MAX truncated it */ int32_t fused; void *fframe[FRAME_MAX]; int32_t fnoff[FRAME_MAX], fnlen[FRAME_MAX]; int32_t floff[FRAME_MAX], fllen[FRAME_MAX]; int32_t fslots[FRAME_MAX]; int32_t fsig[FRAME_MAX]; /* the slot fingerprint of the body * this frame was compiled from */ int32_t frsig[FRAME_MAX]; /* and the fingerprint of the * globals that body names */ int32_t fmine[FRAME_MAX]; /* 0 = the evaluation's, not the * program's */ char ftext[FRAME_TEXT]; } snapshot; /* One per nested break loop, because an inner break must not answer with the * outer one's restarts and must not destroy them either - the outer loop is * still going to need them when the inner one resumes. Same reason [depth] is * a count and not a flag. */ static snapshot snaps[BREAK_MAX]; static _Atomic int snap_depth; /* published last; 0 = none */ static snapshot *snap_top(void); /* Where slot [slot] of frame [frame] lives, resolved against the snapshot this * break took and not against the live chain. * * This is what a locals thunk calls. The thunk runs on the stopped game * thread, from inside this break loop's own poll, and it pushes frames of its * own while it runs — so "frame 2" means the third frame of the backtrace the * daemon was shown, not the third frame of whatever the stack looks like by * the time the thunk is executing. Resolving against the snapshot is the whole * of the difference, and it is the same reason the restarts are answered from * there. * * NULL for anything it cannot place, and the thunk is built to ask only about * slots the same snapshot already reported as bound. A NULL would be * dereferenced, so this is the one place that must not answer optimistically: * an index that is out of range, a frame that is not in this snapshot, or a * slot the binding for which had not run, are each a null here and a refusal * before the thunk is ever built. */ void *flan_agent_frame_slot(int64_t frame, int64_t slot) { snapshot *s = snap_top(); if (s == NULL) return NULL; if (frame < 0 || frame >= s->fn) return NULL; if (slot < 0 || slot > 0x7fffffff) return NULL; return flan_dev_frame_slot(s->fframe[frame], (int32_t)slot); } static snapshot *snap_top(void) { int d = atomic_load(&snap_depth); return d <= 0 ? NULL : &snaps[d - 1]; } /* Called on the game thread with the stack held still. 0 if there is no room * to nest, which the caller reports rather than serving a stale one. */ static int32_t snap_gen; /* monotone; 0 is "no snapshot" */ static int snap_push(void) { int d = atomic_load(&snap_depth); if (d >= BREAK_MAX) return 0; snapshot *s = &snaps[d]; int32_t n = flan_restart_count(); s->gen = ++snap_gen; s->total = n; s->used = 0; s->n = 0; for (int32_t i = 0; i < n && s->n < SNAP_MAX; i++) { int64_t len = 0; const uint8_t *nm = flan_restart_name(i, &len); void *fr = flan_restart_frame(i); if (nm == NULL || fr == NULL) continue; if (len < 0) len = 0; if ((int64_t)s->used + len + 1 > SNAP_NAMES) break; s->frame[s->n] = fr; s->off[s->n] = s->used; s->len[s->n] = (int32_t)len; /* The outermost [restart_floor] frames are below the thunk boundary. */ s->reachable[s->n] = (i < n - restart_floor); memcpy(s->names + s->used, nm, (size_t)len); s->used += (int32_t)len; s->names[s->used++] = 0; s->n++; } /* And the frames, from the same held-still stack. A deep recursion is * truncated rather than followed: the innermost frames are the ones the * question is about, and the count says how many were left out. */ { int32_t fn = flan_dev_frame_count(); s->ftotal = fn; s->fused = 0; s->fn = 0; for (int32_t i = 0; i < fn && s->fn < FRAME_MAX; i++) { void *fr = flan_dev_frame_at(i); int64_t nl = 0, ll = 0; const char *nm, *lc; if (fr == NULL) break; nm = flan_dev_frame_name(fr, &nl); lc = flan_dev_frame_loc(fr, &ll); if (nl < 0) nl = 0; if (ll < 0) ll = 0; if ((int64_t)s->fused + nl + ll + 2 > FRAME_TEXT) break; s->fframe[s->fn] = fr; s->fnoff[s->fn] = s->fused; s->fnlen[s->fn] = (int32_t)nl; if (nm != NULL && nl > 0) memcpy(s->ftext + s->fused, nm, (size_t)nl); s->fused += (int32_t)nl; s->ftext[s->fused++] = 0; s->floff[s->fn] = s->fused; s->fllen[s->fn] = (int32_t)ll; if (lc != NULL && ll > 0) memcpy(s->ftext + s->fused, lc, (size_t)ll); s->fused += (int32_t)ll; s->ftext[s->fused++] = 0; s->fslots[s->fn] = flan_dev_frame_nslots(fr); /* Snapshotted with the rest of the frame rather than read later: the * module this description lives in can be unloaded once the daemon * installs a replacement, and the comparison happens after that. */ s->fsig[s->fn] = flan_dev_frame_slotsig(fr); s->frsig[s->fn] = flan_dev_frame_refsig(fr); /* The outermost [frame_floor] frames are the program's; anything above * them belongs to the evaluation this break is inside. */ s->fmine[s->fn] = (frame_floor < 0) || (i >= fn - frame_floor); s->fn++; } } atomic_store(&snap_depth, d + 1); return 1; } static void snap_pop(void) { int d = atomic_load(&snap_depth); if (d > 0) atomic_store(&snap_depth, d - 1); } /* The condition's class name, so an editor can say what stopped rather than * only that something did. It is all there is to say: the hook is handed the * name and an opaque pointer, and nothing at run time can render a value whose * type it does not know. Written before [broken] is set and read only while * [broken] is 1, so the listener never sees half of it. */ static char condition_name[128]; int32_t flan_agent_poll(void); /* Every way out of the break loop that is not a resume. [_exit] and not * [exit], because this runs on the game thread while the listener thread may * be inside [dlopen] holding the loader lock — and [exit] runs the atexit * chain and the ELF destructors, which want that same lock. A program asked to * abort would hang instead of dying, which is the failure mode the break loop * exists to replace. Nothing here needs an orderly teardown: the streams are * flushed by hand above every call. * * 134 is kept because that is what a trap exits with; see rt_die in * flan_rt.c. */ static _Noreturn void die_now(void) { fflush(stdout); fflush(stderr); /* The editor's socket, by hand. In a merged `flan dev' this process is the * listener as well as the program, and _exit skips the atexit handler that * would otherwise remove it -- deliberately, for the loader-lock reason * above. A socket file left behind with nothing accepting on it answers the * next client with ECONNREFUSED, which reads like a daemon that is there and * refusing rather than one that died. FLAN_DEV_SOCK is unset in an ordinary * run, and then this does nothing. */ { const char *sock = getenv("FLAN_DEV_SOCK"); if (sock != NULL && *sock != '\0') unlink(sock); } _exit(134); } static void break_loop(const uint8_t *name, int64_t namelen, void *condition, void *xfer) { struct timespec step = { 0, 2000000 }; /* 2ms */ (void)condition; fflush(stdout); fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n", (int)namelen, (const char *)name); /* Taken before anything is printed, and before [depth] says there is a * break to ask about: the list on the terminal and the list on the socket * are then the same list, numbered the same way, and the numbers are what a * choice is made of. */ int32_t my_gen; if (!snap_push()) { fflush(stdout); fprintf(stderr, "flan: %d nested break loops - giving up rather than " "spinning\n", BREAK_MAX); fflush(stderr); die_now(); } { snapshot *s = snap_top(); my_gen = s->gen; if (s->n == 0) fprintf(stderr, " no restarts are active; abort, or fix and reload\n"); for (int32_t i = 0; i < s->n; i++) /* Numbered, because that is how one is taken now, and marked when it is * not takeable - a restart below the thunk boundary is shown rather * than hidden, since "why can I not have that one" is a fair question * and silence is how this went wrong the first time. */ fprintf(stderr, " %2d. restart: %s%s\n", i, s->names + s->off[i], s->reachable[i] ? "" : " (below this break; cannot be taken)"); if (s->total > s->n) fprintf(stderr, " ... and %d more, not listed\n", s->total - s->n); } fflush(stderr); /* What the *outer* loop was reporting, restored on the way out: resuming an * inner break must not leave the outer one describing a condition that has * already been answered. */ char outer_name[sizeof condition_name]; memcpy(outer_name, condition_name, sizeof outer_name); { size_t k = namelen < 0 ? 0 : (size_t)namelen; if (k >= sizeof condition_name) k = sizeof condition_name - 1; memcpy(condition_name, name, k); condition_name[k] = '\0'; } /* Published last: the name has to be whole before anything advertises that * there is one to read. */ if (atomic_fetch_add(&depth, 1) + 1 > BREAK_MAX) { /* Printed without going near the hook: whatever is erroring is erroring * inside the machinery that reports errors. */ fflush(stdout); fprintf(stderr, "flan: %d nested break loops — giving up rather than spinning\n", BREAK_MAX); fflush(stderr); die_now(); } for (;;) { flan_agent_poll(); if (atomic_load(&aborting)) { fflush(stdout); fprintf(stderr, "flan: aborted at the break loop\n"); die_now(); } if (atomic_load(&chosen_ready)) { /* Claimed into a local and the flag cleared *before* the attempt. The * other order loses a request that was already answered ok: a [restart] * arriving during the attempt passes its own check, writes a new name * and sets the flag, and the store below then erases it. Copying also * keeps strlen off a buffer the listener may be writing. */ /* Read before the claim: a choice addressed to some other break is * not this one's to consume. */ if (atomic_load(&chosen_gen) != my_gen) { nanosleep(&step, NULL); continue; } int take = atomic_load(&chosen_index); atomic_store(&chosen_ready, 0); snapshot *s = snap_top(); int ok = s != NULL && s->gen == my_gen && take >= 0 && take < s->n && s->reachable[take]; if (ok) { flan_restart_take(s->frame[take], xfer); fprintf(stderr, "flan: resuming at restart %d. %s\n", take, s->names + s->off[take]); fflush(stderr); memcpy(condition_name, outer_name, sizeof condition_name); /* Cleared with the resume: an abort that passed its check just as the * game thread resumed would otherwise stay armed and kill the program * at the *next* unhandled error, minutes later, in unrelated code, * giving nobody the chance to choose. */ atomic_store(&aborting, 0); /* Popped *before* the depth comes down. The other order leaves a * window where [depth] says the outer break is the current one and * [snap_top] still answers with the inner one's list, so a request * arriving in it is validated against a list nobody is looking at. */ snap_pop(); atomic_fetch_sub(&depth, 1); return; } /* The listener checks all of this before answering ok, so reaching here * means the two disagreed - worth saying loudly rather than looping on * in silence, which is the failure this whole change is about. */ fprintf(stderr, "flan: restart %d is not one this break can take\n", take); fflush(stderr); } nanosleep(&step, NULL); } } /* Re-entrant, and it has to be: a thunk this runs may itself error, and the * break loop that catches it polls again from inside that very call. So a job * is *claimed* — tail advanced past it — before it is run, and both indices * are re-read each time round rather than cached across the work. Caching them * and storing tail at the end would rewind it over everything the nested poll * consumed, and running a C-x C-e thunk a second time is the one thing the * whole dev loop is careful never to do. Still single-consumer: only the game * thread writes tail, nesting included. */ int32_t flan_agent_poll(void) { int32_t n = 0; for (;;) { unsigned t = atomic_load_explicit(&tail, memory_order_relaxed); unsigned h = atomic_load_explicit(&head, memory_order_acquire); if (t == h) return n; job j = queue[t % QUEUE]; atomic_store_explicit(&tail, t + 1, memory_order_relaxed); if (j.install != NULL) { j.install(); n++; } /* After the install, so a thunk sees the bodies its own module published. * * The floor moves for the duration. [flan_reload_call] holds its own * transfer channel and drops it on return, so every restart frame that * was on the stack before this call is unreachable from a break inside * it. Saved and restored rather than set and cleared: this runs from * inside break loops, which run from inside thunks. */ if (j.call != NULL) { int32_t outer = restart_floor; int32_t oframe = frame_floor; restart_floor = flan_restart_count(); frame_floor = flan_dev_frame_count(); j.call(); restart_floor = outer; frame_floor = oframe; } if (j.handle != NULL) { dlclose(j.handle); } } } /* The same, but waits up to [ms] for something to arrive first. A game loop * does not want this; a headless test does, because it makes the reload * deterministic instead of a race against the frame rate. */ int32_t flan_agent_wait(int32_t ms) { struct timespec step = { 0, 1000000 }; /* 1ms */ for (int32_t i = 0; i < ms; i++) { int32_t n = flan_agent_poll(); if (n > 0) return n; nanosleep(&step, NULL); } return flan_agent_poll(); } /* Where an answer goes. Two kinds, and the verb table below cannot tell them * apart: a socket, which is how the two-process daemon asks, and a buffer, * which is how the compiler asks when it is a thread in this same process. * * One sink rather than two copies of the verb table. Every answer here is * assembled from several pieces — a header, a name, a newline — so the seam * had to be the writing and not the handler, or the handlers would have had * to be duplicated and would drift. */ typedef struct { int fd; /* -1 when this is a buffer */ char *buf; size_t len; size_t cap; } sink; /* MSG_NOSIGNAL rather than write(2). A reply goes out in more than one piece, * and a sender that has read enough and closed leaves the rest of it writing * into a closed socket — which is SIGPIPE, whose default action would kill the * program the agent is embedded in. Suppressing it per call rather than * installing a handler, because the disposition belongs to the program and not * to us. */ static void emit(sink *o, const void *p, size_t n) { if (o->fd >= 0) { const char *s = (const char *)p; while (n > 0) { ssize_t k = send(o->fd, s, n, MSG_NOSIGNAL); if (k <= 0) return; s += k; n -= (size_t)k; } return; } if (o->len + n + 1 > o->cap) { size_t want = o->cap ? o->cap : 1024; while (want < o->len + n + 1) want *= 2; char *g = realloc(o->buf, want); /* Nothing useful to do with a failed realloc here: the caller reads * [len], so a short answer is what it sees, and that is the same shape as * a socket the peer closed early. */ if (g == NULL) return; o->buf = g; o->cap = want; } memcpy(o->buf + o->len, p, n); o->len += n; o->buf[o->len] = '\0'; } static void reply(sink *o, const char *s) { emit(o, s, strlen(s)); } /* A dlopen failure the compiler's two backends are responsible for, turned * into a sentence that says so. * * Flan has two native backends. They agree about every scalar and disagree * about every aggregate — the x86 dev backend passes a struct by pointer with * a hidden sret, LLVM classifies it per the SysV psABI — so a redefinition * module built by one and loaded into a host built by the other links, loads, * and then dies with SIGSEGV at the first call into a redefined function that * takes or returns a struct. A dev build therefore defines a marker naming its * backend and a module holds a pointer to the marker it was built for, which * is a relocation the loader must resolve while it maps the object. A crossed * pair has no such symbol and is refused here, before any of the new code * runs. * * What the loader says at that point is "undefined symbol: flan.abi.x86", * which is true and tells nobody anything. So the marker's name is matched — * the name, not the loader's phrasing, which is libc's to change — and the * reason is stated instead. Which marker is missing says which backend built * the module, and the host is necessarily the other one. * * Returns NULL for a failure that is about something else, which is then * passed through as the loader wrote it. */ static const char *abi_mismatch(const char *err) { if (err == NULL) return NULL; if (strstr(err, "flan.abi.x86") != NULL) return "the module and the running program were built by different " "backends: the module came from the x86 dev backend and needs " "flan.abi.x86, which this program does not define. The two " "backends pass every struct differently, so the pair would die at " "the first call into a redefined function that takes or returns " "one. Rebuild the program with --x86 so that both halves agree."; if (strstr(err, "flan.abi.llvm") != NULL) return "the module and the running program were built by different " "backends: the module came from LLVM and needs flan.abi.llvm, " "which an --x86 program does not define. The two backends pass " "every struct differently, so the pair would die at the first call " "into a redefined function that takes or returns one. Rebuild the " "program without --x86: there is no --x86 spelling for building a " "redefinition module yet, so the program is the half that has to " "move."; return NULL; } /* One line, one answer, one module. This is the whole of what the agent is * asked, and it is reached two ways: from the socket below, and — in a build * where the compiler is a thread in this same process — by being called. The * two share this function rather than a protocol. * * What does not change with the caller is where the work lands. [dlopen] runs * here, on whichever thread asked; the install is only ever *published* to the * ring, and the game thread picks it up at a frame boundary. A direct call * that installed on the spot would be a frame running half in the old code and * half in the new. */ /* One request at a time, whoever asks. Until the compiler could call in, the * accept loop was the only caller and was single-threaded, and the ring's * "exactly one producer" was true by construction. It is not any more: a * direct call runs on the compiler thread while the accept loop can be serving * the same verbs. This lock puts that back rather than weakening the ring — * [publish]'s room check is separate from its store, and two producers * interleaving there is the silent overwrite the ring comment above describes. * * It is held across the [dlopen], which is milliseconds. That is what the * accept loop already did to itself by serving connections inline, so no * caller waits longer than it did before. The game thread never takes it. */ static pthread_mutex_t request_lock = PTHREAD_MUTEX_INITIALIZER; static void handle_line(char *line, sink *o) { /* The one verb that is not a module: read back the value of the last * expression evaluated, with the counter that says whether it is a new * one. The daemon polls this rather than the agent holding a connection * open across a frame boundary it does not control. */ /* Only while stopped: what is on offer, and which one to take. Both are * refused when the program is running, by name, rather than silently * doing nothing — there is no restart stack to walk from here. */ /* Whether the program is stopped, and on what. Answered while it is * running too — "running" is an answer, not a refusal — because this is * the one question an editor asks without knowing the state already, and * refusing it would leave nothing to poll. */ if (strcmp(line, "status") == 0) { if ((atomic_load(&depth) > 0)) { reply(o, "stopped "); reply(o, condition_name); reply(o, "\n"); } else reply(o, "running\n"); return; } /* One line per restart, innermost first: the index it is taken by, a flag * for whether it can be taken at all, and the name. The index leads * because it is the identity - two frames can offer [retry] and only one * of them is the one meant, which is the whole reason this is not a list * of names any more. Read from the snapshot, never from the live stack. */ if (strcmp(line, "restarts") == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } snapshot *s = snap_top(); if (s == NULL) { reply(o, "err no restart snapshot\n"); return; } for (int32_t i = 0; i < s->n; i++) { char hdr[32]; int k = snprintf(hdr, sizeof hdr, "%d %c ", i, s->reachable[i] ? '+' : '-'); if (k > 0) emit(o, hdr, (size_t)k); emit(o, s->names + s->off[i], (size_t)s->len[i]); reply(o, "\n"); } reply(o, ".\n"); return; } /* Where the stopped thread is. One line per frame, innermost first: * the index, whether the frame is the program's or the evaluation's, how * many slots it has, where it is written, and its name. Served from the * snapshot, never from the live chain — the game thread is parked in the * break loop, but the loop polls, and a poll runs Flan. * * Refused while running, like every other break verb and for the same * reason: a chain read by one thread while another pushes and pops it is * not a backtrace, it is a race with a plausible shape. */ if (strcmp(line, "backtrace") == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } snapshot *s = snap_top(); if (s == NULL) { reply(o, "err no frame snapshot\n"); return; } if (s->fn == 0 && s->ftotal == 0) { /* Not "no frames": a release build has no shadow stack at all, and * answering with an empty backtrace would read as a program with an * empty stack, which is not a thing that can be stopped. */ reply(o, "err this program was not built with --dev, so it has no " "shadow stack to walk\n"); return; } for (int32_t i = 0; i < s->fn; i++) { char hdr[64]; /* Both fingerprints go before the location and the location before * the name, because the name is the one field that can contain a * space and so has to be last. */ int k = snprintf(hdr, sizeof hdr, "%d %c %d %d %d ", i, s->fmine[i] ? '+' : '-', s->fslots[i], s->fsig[i], s->frsig[i]); if (k > 0) emit(o, hdr, (size_t)k); if (s->fllen[i] > 0) emit(o, s->ftext + s->floff[i], (size_t)s->fllen[i]); else reply(o, "?"); reply(o, " "); emit(o, s->ftext + s->fnoff[i], (size_t)s->fnlen[i]); reply(o, "\n"); } if (s->ftotal > s->fn) { char more[64]; int k = snprintf(more, sizeof more, "... %d\n", s->ftotal - s->fn); if (k > 0) emit(o, more, (size_t)k); } reply(o, ".\n"); return; } /* Which of a frame's slots have been bound at the point it stopped. One * line per slot: the index and [+] or [-]. * * The daemon asks this before it builds a thunk, and that order is the * safety: an unbound slot is a null address, a thunk that rendered one * would dereference it, and a program stopped in a break loop is the last * place to take a fault. It is answered from the snapshot, so the set the * daemon is told about is the set the thunk will resolve against. * * It says nothing about *what* a slot holds, or what it is called. Those * are facts about the build, and the daemon owns the build — [Tast.fn] * carries [slots] and [snames] beside each other. Sending them from here * would be a second copy of them that could drift. */ if (strncmp(line, "locals ", 7) == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } snapshot *s = snap_top(); if (s == NULL) { reply(o, "err no frame snapshot\n"); return; } char *end = NULL; long at = strtol(line + 7, &end, 10); if (end == line + 7) { reply(o, "err locals wants a frame index\n"); return; } if (at < 0 || at >= s->fn) { reply(o, "err no frame at that index\n"); return; } if (s->fslots[at] == 0) { reply(o, "err that frame records no slots; it has no named local, or " "this build does not record them\n"); return; } for (int32_t i = 0; i < s->fslots[at]; i++) { char l[32]; int k = snprintf(l, sizeof l, "%d %c\n", i, flan_dev_frame_slot(s->fframe[at], i) ? '+' : '-'); if (k > 0) emit(o, l, (size_t)k); } reply(o, ".\n"); return; } /* Take the i'th, optionally checking that the caller and this snapshot * still agree on what the i'th is called. The name is not the lookup - * that is the bug - it is a receipt: a client that listed, prompted, and * chose has the name in hand for nothing, and sending it turns a bare * integer into something that can be wrong out loud. */ if (strncmp(line, "restart-at ", 11) == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } snapshot *s = snap_top(); if (s == NULL) { reply(o, "err no restart snapshot\n"); return; } char *end = NULL; long idx = strtol(line + 11, &end, 10); if (end == line + 11) { reply(o, "err restart-at wants an index\n"); return; } if (idx < 0 || idx >= s->n) { reply(o, "err no restart at that index\n"); return; } while (*end == ' ') end++; if (*end != '\0') { size_t k = strlen(end); if (k != (size_t)s->len[idx] || memcmp(end, s->names + s->off[idx], k) != 0) { reply(o, "err that index is now "); reply(o, s->names + s->off[idx]); reply(o, ", not what you named; list the restarts again\n"); return; } } if (!s->reachable[idx]) { /* Refused, with the reason, rather than accepted and dropped. The * transfer would unwind to the thunk this break is inside and stop * there, and the program would carry on as if nothing had been * chosen. */ reply(o, "err restart "); reply(o, s->names + s->off[idx]); reply(o, " is below the evaluation this break is inside, so a " "transfer to it has nowhere to land; choose one offered " "above it, or abort\n"); return; } atomic_store(&chosen_index, (int)idx); atomic_store(&chosen_gen, s->gen); /* Published last, so the game thread never reads an index that is about * to change, or one whose generation has not arrived yet. */ atomic_store(&chosen_ready, 1); reply(o, "ok\n"); return; } /* By name, still, for a person at a raw socket - and now defined as * exactly [restart-at] on the first index offering the name, which is * what §4's walk already meant. So the two verbs cannot disagree, and a * shadowed name is reachable through the other one rather than nowhere. */ if (strncmp(line, "restart ", 8) == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } snapshot *s = snap_top(); if (s == NULL) { reply(o, "err no restart snapshot\n"); return; } size_t k = strlen(line + 8); if (k == 0) { reply(o, "err bad restart name\n"); return; } int32_t at = -1; for (int32_t i = 0; i < s->n && at < 0; i++) if ((size_t)s->len[i] == k && memcmp(s->names + s->off[i], line + 8, k) == 0) at = i; if (at < 0) { reply(o, "err no restart named "); reply(o, line + 8); reply(o, " is active\n"); return; } if (!s->reachable[at]) { reply(o, "err restart "); reply(o, line + 8); reply(o, " is below the evaluation this break is inside, so a " "transfer to it has nowhere to land; choose one offered " "above it, or abort\n"); return; } atomic_store(&chosen_index, at); atomic_store(&chosen_gen, s->gen); atomic_store(&chosen_ready, 1); reply(o, "ok\n"); return; } if (strcmp(line, "abort") == 0) { if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped\n"); return; } reply(o, "ok\n"); atomic_store(&aborting, 1); return; } /* "watch on" / "watch off" arm and disarm the table; bare "watch" reads it. * * Arming is a message rather than something the daemon infers, because the * program is the writer and it has to be told. Nothing writes the table * while it is off, which is the whole of "watching costs nothing when nobody * is watching" — see flan_dev.c. */ if (strcmp(line, "watch on") == 0 || strcmp(line, "watch off") == 0) { flan_dev_watch_enable(line[6] == 'o' && line[7] == 'n'); reply(o, "ok\n"); return; } /* "watch reset" opens a new window for the numeric accumulators: their * count, range and mean start again from the next sample, while a text slot * is untouched. * * A separate command rather than a side effect of the read, which was the * tempting shape and is the wrong one. A destructive read makes *looking* * change what is there, so anything that polls — a test's [await], a second * editor, a person pressing the read twice — silently shortens the window * and gets a count that is noise. Reading is free and resetting is a * decision; the editor makes it once per tick, after the read. */ if (strcmp(line, "watch reset") == 0) { flan_dev_watch_reset(); reply(o, "ok\n"); return; } if (strcmp(line, "watch") == 0) { /* One line per slot: NAMEVALUE. The name cannot contain a tab — it is * a C identifier-ish string the program passed — and the value cannot * contain a raw tab or newline, because everything that reaches it goes * through an emitter that escapes both. So no framing is needed beyond * this, which is the same bet render_locals makes on the same grounds. * * A header first: the count actually written, and how many names were * whether any name ever found no slot, so an overflow is reported rather * than showing up as a value that never appears. A flag rather than a * count, because the count would be of *writes* — see flan_dev.c. */ uint64_t ncap = flan_dev_watch_name_cap(); uint64_t vcap = flan_dev_watch_val_cap(); char *nb = malloc((size_t)ncap + 1); char *vb = malloc((size_t)vcap + 1); if (nb == NULL || vb == NULL) { free(nb); free(vb); reply(o, "err out of memory reading the watch table\n"); return; } /* Allocating here is fine, for [result]'s reason: this is the listener * thread. The table the game thread writes is a fixed static. */ uint32_t n = flan_dev_watch_count(); char hdr[64]; int k = snprintf(hdr, sizeof hdr, "%lu %d\n", (unsigned long)n, flan_dev_watch_overflowed()); if (k > 0) emit(o, hdr, (size_t)k); for (uint32_t i = 0; i < n; i++) { uint64_t vlen = 0; /* A torn slot is *still listed*, with an empty value. Dropping the row * would make the buffer's rows move under the reader every time the * game happened to be mid-write, which is far worse to look at than one * value that is blank for a tick. */ flan_dev_watch_read(i, nb, ncap, vb, vcap, &vlen); emit(o, nb, strlen(nb)); emit(o, "\t", 1); if (vlen > 0) emit(o, vb, (size_t)vlen); emit(o, "\n", 1); } free(nb); free(vb); return; } if (strcmp(line, "result") == 0) { uint64_t gen = 0, len = 0; uint64_t cap = flan_dev_result_cap(); char *v = malloc(cap ? (size_t)cap : 1); if (v == NULL) { reply(o, "err out of memory reading the result\n"); return; } /* Allocating here is fine and is the distinction that matters: this runs * on the listener thread, or on the compiler thread in a merged build. * What the game thread writes into is a fixed static in flan_dev.c * precisely so that *it* allocates nothing. */ flan_dev_result_read(v, cap, &gen, &len); char hdr[64]; int k = snprintf(hdr, sizeof hdr, "%llu %llu\n", (unsigned long long)gen, (unsigned long long)len); if (k > 0) { emit(o, hdr, (size_t)k); if (len > 0) emit(o, v, (size_t)len); } free(v); return; } /* "reg at ADDR" — what the allocation registry records for one address. * * The reader's side of the table, and the one question the renderer's own * two cannot answer. Inside a thunk the type at the far end of a pointer is * already static — (Ptr Enemy) says Enemy — so [reg-live] and [reg-emit] * only ever needed permission. Somebody pointing at a bare address has no * (Ptr T) to read a type off, and the recorded name is the whole of what * there is to go on. * * It used to say here that this is answered while the program runs as well * as while it is stopped, on the grounds that a table is not a stack. The * table is not a stack and it is still written by the other thread, and the * answer to one address is a claim that stops being true as it is made — so * the gate below now says what the daemon already said. * * ADDR is read with base 0, so both 0x-hex and decimal arrive; an editor * that has an address as text has it in one of those two spellings. */ if (strncmp(line, "reg at ", 7) == 0) { const char *type = NULL; int64_t typelen = 0, off = 0, bytes = 0, elem = 0, seq = 0, died = 0; char *end = NULL; unsigned long long a; /* Stopped only, like every break verb. Whether an address is still live is * exactly what a running program is changing, so the answer would describe * a table the game thread has already moved on from — the daemon refuses * the question for that reason before it ever reaches here * (lib/dev.ml, [inspect_addr]), and this says the same thing to anything * else that asks. The two listing verbs below are the opposite case and * stay answerable while running: a breakdown is a description of the * program and not a claim about one address, and the table carries its own * seqlock so that reading it while it is written is safe. */ if (!(atomic_load(&depth) > 0)) { reply(o, "err not stopped: whether one address is still live is what a " "running program is changing, so this is read from a stopped " "one\n"); return; } if (!flan_dev_reg_enabled()) { reply(o, "err the allocation registry is off; this is not a dev build\n"); return; } a = strtoull(line + 7, &end, 0); if (end == line + 7 || a == 0) { reply(o, "err reg at wants an address\n"); return; } if (!flan_dev_reg_at((const void *)(uintptr_t)a, &type, &typelen, &off, &bytes, &elem, &seq, &died)) { /* Never heard of it, which is a fact and not a failure: a stack local, * a global, or a pointer from C. The daemon says which of those it * might be; this says only that the table has nothing. */ reply(o, "none\n"); return; } { char hdr[128]; int k = snprintf(hdr, sizeof hdr, "ok %d %lld %lld %lld %lld %lld\t", died == 0 ? 1 : 0, (long long)off, (long long)bytes, (long long)elem, (long long)seq, (long long)died); if (k > 0) emit(o, hdr, (size_t)k); /* Last, and after a tab, because a type spelling holds spaces — * "(Vec i32)" — and nothing else on the line does. It cannot hold a tab * or a newline: it is Types.to_string of a type the programmer wrote. */ if (typelen > 0) emit(o, type, (size_t)typelen); reply(o, "\n"); } return; } /* "reg types" — the whole table grouped by type spelling; "reg leaks" — the * same walk with the dead left out. * * One verb would have done with a flag, and two exist because the two * questions are asked at different moments and read differently: a * breakdown is "what is this program made of", a leak report is "what is * still held". The *walk* is one function in flan_dev.c for exactly that * reason — two of them would drift. * * A header first, like [watch]: how many rows follow, and whether the table * ever overflowed. The second is not decoration — an overflowed table has * blocks in the program that are in nobody's row, so every number below is * a floor, and a reader that could not tell would quote them as counts. */ if (strcmp(line, "reg types") == 0 || strcmp(line, "reg leaks") == 0) { enum { REG_ROWS = 256 }; /* Allocated here and freed before the reply is finished, not declared as * static arrays. 256 rows of four words is 8KB, and static would put that * 8KB in the BSS of every build this package is linked into — including a * release build of a game that imports the agent, which never writes a * row. That is flan_dev.c's own argument against a fixed table, at a * thirty-second of the size, and it is the same rule: a release build * carries a null pointer and the declarations. * * Allocating is legal here for [watch]'s reason and no other: this runs * on the listener thread, or on the compiler thread in a merged build. * The table the game thread writes is a fixed static in flan_dev.c * precisely so that *it* allocates nothing. */ int64_t *counts = malloc(REG_ROWS * sizeof *counts); int64_t *bytes = malloc(REG_ROWS * sizeof *bytes); int64_t *typelens = malloc(REG_ROWS * sizeof *typelens); const char **types = malloc(REG_ROWS * sizeof *types); int64_t n, i; int live_only = line[4] == 'l'; if (counts == NULL || bytes == NULL || typelens == NULL || types == NULL) { free(counts); free(bytes); free(typelens); free(types); reply(o, "err out of memory reading the allocation registry\n"); return; } if (!flan_dev_reg_enabled()) { free(counts); free(bytes); free(typelens); free(types); reply(o, "err the allocation registry is off; this is not a dev build\n"); return; } n = flan_dev_reg_by_type(live_only ? 1 : 0, counts, bytes, types, typelens, REG_ROWS); { char hdr[64]; int k = snprintf(hdr, sizeof hdr, "%lld %d\n", (long long)(n < REG_ROWS ? n : REG_ROWS), flan_dev_reg_overflowed()); if (k > 0) emit(o, hdr, (size_t)k); } for (i = 0; i < n && i < REG_ROWS; i++) { char row[64]; int k = snprintf(row, sizeof row, "%lld %lld\t", (long long)counts[i], (long long)bytes[i]); if (k > 0) emit(o, row, (size_t)k); if (typelens[i] > 0) emit(o, types[i], (size_t)typelens[i]); reply(o, "\n"); } free(counts); free(bytes); free(typelens); free(types); return; } /* Before the dlopen, not after it: a module there is no room to queue is * one there is no point relocating, and refusing here means no handle is * taken for it at all. Only one producer runs at a time, so room seen now is * room still there at [publish] below. */ if (!queue_room()) { reply(o, "err reload queue full; the program is not calling " "agent/poll\n"); return; } void *h = dlopen(line, RTLD_NOW | RTLD_LOCAL); if (h == NULL) { /* [dlerror] is one-shot and the next dl call may clobber what it returned, * so the pointer is taken once and used for both the test and the reply. */ const char *err = dlerror(); const char *why = abi_mismatch(err); reply(o, "err "); reply(o, why != NULL ? why : err); reply(o, "\n"); return; } install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install"); if (f == NULL) { /* Closed, and this is not an exception to "nothing is ever dlclosed". * That rule is about a module something *points into* — a cell holding * an address in its text. This one published nothing: no installer ran, * so no cell names it, and it is unreachable the moment this function * returns. What leaked before was the handle value rather than the * mapping — dlopen refcounts by path, so re-sending the same bad file * bumped a count nothing could ever bring down, and the one reference * that could was dropped on the floor here. */ dlclose(h); reply(o, "err no flan_reload_install\n"); return; } /* Optional: only an expression evaluation has one. */ call_fn c = (call_fn)(uintptr_t)dlsym(h, "flan_reload_call"); /* And only one that leaves nothing behind may be unloaded. */ void *transient = (c == NULL) ? NULL : dlsym(h, "flan_reload_transient"); /* Answer before queueing, not after. The game thread can install and run * to completion between the two, and a program that exits there would tear * down this connection with the reply still unwritten — which reaches the * sender as a reset, not as an answer. */ reply(o, "ok\n"); /* "queued", not "installed": the store happens on the game thread, at a * time this thread does not get to choose. The room was checked before the * dlopen and only this thread consumes it, so this cannot fail; it is * asserted rather than assumed because a silent [publish] that did nothing * is the failure being fixed. */ if (!publish((job){ f, c, transient == NULL ? NULL : h })) fprintf(stderr, "flan: reload queue full after it was checked\n"); return; } /* One connection, one line, one module. Loading here rather than in the game * thread is the whole reason this thread exists. */ static void serve(int fd) { /* A deadline on the read. The accept loop is single-threaded and serves each * connection inline, so a client that connects and then sends nothing - an * editor killed mid-request, a daemon that crashed between connect and send - * blocks every later request, including the [abort] that would end a stopped * program. Worse, the requests the client had already given up on are served * when its socket finally closes, so an abandoned abort can kill the program * minutes later against a state that has moved on. Two seconds is generous * for one line. */ { struct timeval tv = { 2, 0 }; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); } sink o = { fd, NULL, 0, 0 }; char line[4096]; size_t n = 0; for (;;) { ssize_t k = read(fd, line + n, sizeof line - n - 1); if (k <= 0) return; n += (size_t)k; line[n] = '\0'; char *nl = strchr(line, '\n'); if (nl == NULL) { if (n == sizeof line - 1) { reply(&o, "err path too long\n"); return; } continue; } *nl = '\0'; pthread_mutex_lock(&request_lock); handle_line(line, &o); pthread_mutex_unlock(&request_lock); return; } } /* The direct hand-off. In a [flan dev] build the compiler is a thread in this * process, so a request that used to be a connect, a write and a read on a * unix socket looping back into this address space is now this call — the same * verb table, answered into a buffer instead of onto an fd. * * The buffer is malloc'd here and freed by the caller. It is not a static, * because the socket's accept loop can be answering [status] on another thread * while this runs, and two answers into one buffer is a race with a plausible * shape. * * The rule this must not break is the one that keeps the collector off the * frame thread: nothing here calls into OCaml and nothing here runs on the * game thread. A delivery is left in the ring exactly as the listener leaves * it, and the game thread picks it up when it reaches a frame boundary. */ char *flan_agent_request(const char *line, uint64_t *len) { char stack[4096]; sink o = { -1, NULL, 0, 0 }; size_t n = strlen(line); if (n >= sizeof stack) { /* The same answer the socket gives for the same input. The point of one * verb table is that the two callers cannot be told apart, and a silent * empty reply here would be the first place they could. */ reply(&o, "err path too long\n"); *len = (uint64_t)o.len; return o.buf; } memcpy(stack, line, n + 1); pthread_mutex_lock(&request_lock); handle_line(stack, &o); pthread_mutex_unlock(&request_lock); *len = (uint64_t)o.len; return o.buf; } void flan_agent_request_free(char *p) { free(p); } static void *accept_loop(void *arg) { (void)arg; for (;;) { int fd = accept(listen_fd, NULL, NULL); if (fd < 0) { if (errno == EINTR) continue; return NULL; } serve(fd); close(fd); } } /* [path] is a Flan string: ptr and len, not NUL-terminated. * * FLAN_AGENT_SOCKET overrides it. A program's source has to name some path, * and the daemon that launches the program is the one that knows where it * wants to talk to it — without the override the daemon would have to guess, * and guessing wrong fails silently: everything compiles, the module is built, * and nothing ever receives it. */ int32_t flan_agent_start(const uint8_t *path, int64_t len) { struct sockaddr_un addr; const char *env = getenv("FLAN_AGENT_SOCKET"); if (atomic_exchange(&started, 1)) return 0; if (env != NULL && env[0] != '\0') { path = (const uint8_t *)env; len = (int64_t)strlen(env); } if (len <= 0 || (size_t)len >= sizeof addr.sun_path) return -1; memset(&addr, 0, sizeof addr); addr.sun_family = AF_UNIX; memcpy(addr.sun_path, path, (size_t)len); unlink(addr.sun_path); listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd < 0) return -1; if (bind(listen_fd, (struct sockaddr *)&addr, sizeof addr) < 0) return -1; if (listen(listen_fd, 4) < 0) return -1; if (pthread_create(&listener, NULL, accept_loop, NULL) != 0) return -1; /* From here an unhandled error stops rather than dying. Installed with the * socket and not before it: without a listener there is nobody to ask what * to do, and stopping forever is worse than the abort it replaces. */ flan_break_hook = break_loop; return 0; }