The merged daemon now unlinks its socket on the ways out it does not control as well as the one it does: an atexit for exit(3), which is what a runtime trap takes, and by hand in die_now and in main's fallback, which are _exit and skip the chain on purpose. The client says so too. A refusal on a path that exists is a leftover, not a daemon declining, and the raw 'Connection refused' has now misdirected two investigations. Separately, and it is separate: dev-repl.flan gets dev-robust.flan's 24000-tick budget. Twenty seconds of program under a two-minute test is a second flake waiting its turn, and it is not the one fixed above -- that one fails honestly, saying the program exited.
1333 lines
62 KiB
C
1333 lines
62 KiB
C
/* flan_agent — the half of the dev loop that lives in the running program.
|
|
*
|
|
* A redefinition arrives as a path to a .so (runtime/flan_dev.c and
|
|
* Emit.redefinition are what put it there). Two things have to happen to it,
|
|
* and they must happen on different threads:
|
|
*
|
|
* dlopen relocates the module and runs the loader. It is milliseconds,
|
|
* unbounded, and takes the loader lock. Doing it on the game thread
|
|
* is a dropped frame.
|
|
* install is one store per redefined function. It is sub-microsecond, and
|
|
* it must happen at a point where no redefined function is on the
|
|
* stack — a frame boundary — or a frame runs half in the old code
|
|
* and half in the new.
|
|
*
|
|
* So the listener thread does the loading and hands over a function pointer;
|
|
* the game thread calls flan-poll or flan-wait when it is between frames and
|
|
* that is when the swap becomes visible. Nothing else in the program needs to
|
|
* know the agent exists.
|
|
*
|
|
* Nothing that published anything is ever dlclosed: a cell holds an address
|
|
* inside a module's text, and unloading it would leave every call site
|
|
* pointing at unmapped memory. The two modules that are closed are the ones
|
|
* nothing can point into — a transient thunk, which installs no bodies, and a
|
|
* module refused before it was queued.
|
|
*
|
|
* This is not a protocol. One line per request, the path to load, and a one
|
|
* line answer. The daemon and its nREPL are a separate program that will speak
|
|
* to a socket, not something this file grows into.
|
|
*
|
|
* 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 <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: 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: NAME<tab>VALUE. 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.
|
|
*
|
|
* Answered while the program is running as well as while it is stopped:
|
|
* this reads a table, not a stack, and nothing here walks a chain another
|
|
* thread is pushing. Whether the *answer* holds still long enough to be
|
|
* worth acting on is the caller's judgement, and the daemon makes it.
|
|
*
|
|
* 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;
|
|
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;
|
|
}
|