1707 lines
82 KiB
C
1707 lines
82 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 <signal.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>
|
|
#if defined(__linux__)
|
|
#include <sys/prctl.h>
|
|
#endif
|
|
|
|
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, int64_t *unread);
|
|
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. */
|
|
/* [stopped_only] is the second half of a promise the *daemon* made and cannot
|
|
* keep on its own. A module built to render what is at a raw address has that
|
|
* address baked into it as an integer literal: the registry blessed the
|
|
* address as live at one moment, and the thunk dereferences it at another,
|
|
* three round trips and a ~300ms build later. Nothing in between held the
|
|
* break. A [restart] arriving mid-build resumes the game thread, and this ring
|
|
* would then install and run the thunk at the very next frame boundary —
|
|
* mid-frame, against an address the program may have freed since.
|
|
*
|
|
* So the job carries the condition it was built under, and [flan_agent_poll]
|
|
* re-asks it at the moment of truth instead of trusting the answer the sender
|
|
* got. The gate lives *here* and nowhere else on purpose: a second check on
|
|
* the listener thread, at delivery, would only shrink the window — [depth] can
|
|
* flip between that check and this one — and two places emitting the same
|
|
* refusal is how two places stop agreeing, which is the note the struct
|
|
* comment further down already makes about layouts.
|
|
*
|
|
* What makes the check in [flan_agent_poll] sound rather than narrower: only
|
|
* the game thread polls (agent.flan's [poll-raw] at a frame boundary, and the
|
|
* break loop below, which is the same thread parked), and only the game thread
|
|
* raises [depth]. So [depth > 0] read from inside a poll means *this* thread
|
|
* is inside the break loop right now — it cannot be running a frame at the
|
|
* same time — and the stop the daemon asked under is still the stop in force.
|
|
* A false negative is possible in one direction only and is the harmless one:
|
|
* a break entered after the read refuses a job that would have been legal, and
|
|
* refusing is right anyway, because that is a different stop from the one the
|
|
* registry answered under.
|
|
*
|
|
* [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;
|
|
int stopped_only;
|
|
} job;
|
|
|
|
/* Said once, in one place, and shipped to the daemon over [refusals] rather
|
|
* than written down again at the other end. A refusal is a sentence naming
|
|
* what actually happened, and the thing that actually happened is not "the
|
|
* gate failed" — it is that the program the person was inspecting started
|
|
* running again while the inspector was being compiled. */
|
|
static const char *RESUMED =
|
|
"the program resumed while this inspection was being built — stop it again "
|
|
"and re-ask";
|
|
|
|
/* How many stopped-only jobs have been dropped, ever. A count and not a flag:
|
|
* the daemon reads it before it delivers and again while it waits, and what it
|
|
* wants to know is whether one happened *in between*, which a flag somebody
|
|
* else could have cleared cannot say.
|
|
*
|
|
* It is the whole of the channel because the sentence is fixed. One residual,
|
|
* left alone deliberately: two inspections in flight at once would let either
|
|
* one's refusal surface to the other's waiter. The result buffer they both
|
|
* write into has exactly that property already — flan_dev.c says so where the
|
|
* seqlock is — and fixing it here without fixing it there would be half a
|
|
* repair wearing the whole one's clothes. */
|
|
static atomic_ullong refused_while_running;
|
|
|
|
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);
|
|
/* The other hook, for the traps that call with no channel — see rt_trap in
|
|
* flan_rt.c, which carries the argument for why they are two hooks and not
|
|
* one. This end of it is [trap_stop] below. */
|
|
extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
|
|
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 */
|
|
/* Whether *any* restart on this list can be taken, which is a property of
|
|
* the break and not of the restarts. [reachable] answers a different
|
|
* question — that one is per restart, and it is about the thunk boundary.
|
|
* A break taken by a trap with no transfer channel has a perfectly good
|
|
* list of live restarts below it and no way to aim at one, so the whole
|
|
* snapshot is marked instead of each entry: the refusal sentence differs,
|
|
* and a reader of this struct should not have to infer which case a run of
|
|
* zeroes in [reachable] meant. */
|
|
int32_t resumable;
|
|
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(int resumable) {
|
|
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->resumable = resumable;
|
|
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);
|
|
}
|
|
|
|
/* The loop itself, with one bit of what the caller knows: whether a restart
|
|
* taken here has anywhere to land. [break_loop] passes 1 — it holds the
|
|
* signaller's channel, and writing a frame into it is how §2 resumes.
|
|
* [trap_stop] passes 0, and then this is a place to *stand and read* and
|
|
* nothing more: the stack, the locals, the globals and the restart list are
|
|
* all still there to be looked at, and only the resume is refused. That is
|
|
* strictly more than the alternative, which was the whole process exiting
|
|
* before anyone could ask a question. */
|
|
static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
|
void *xfer, int resumable) {
|
|
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(resumable)) {
|
|
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;
|
|
/* Said once, above the list, rather than appended to every line: it is
|
|
* one fact about the break, and repeating it per restart would read as
|
|
* each restart having its own separate problem. The list is still
|
|
* printed, because what is on offer is part of where the program is even
|
|
* when none of it can be taken — and because the same names come back
|
|
* from a `restarts' query, and the terminal and the socket must not be
|
|
* describing two different programs. */
|
|
if (!s->resumable)
|
|
fprintf(stderr,
|
|
" this trap has no transfer channel, so nothing here can be "
|
|
"resumed into; read the frame, then fix and reload, or abort\n");
|
|
else 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->resumable ? " (cannot be taken from this trap)"
|
|
: 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->resumable && 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);
|
|
}
|
|
}
|
|
|
|
/* §2's break, which can resume, and the trap's, which cannot. Two names over
|
|
* one loop because the two hooks have different signatures and different
|
|
* promises, and a caller of either should not have to pass a flag it does not
|
|
* understand.
|
|
*
|
|
* [trap_stop] is not marked _Noreturn even though it never returns: the loop
|
|
* above leaves only by resuming, which needs [resumable], or by [die_now].
|
|
* Saying so in the type would oblige this function to prove it, and the honest
|
|
* proof is the [die_now] below — reached only if the loop is ever given a way
|
|
* out that this call did not ask for, and dying there is what the program did
|
|
* before any of this existed. */
|
|
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
|
void *xfer) {
|
|
break_loop_at(name, namelen, condition, xfer, 1);
|
|
}
|
|
|
|
static void trap_stop(const uint8_t *name, int64_t namelen) {
|
|
break_loop_at(name, namelen, NULL, NULL, 0);
|
|
die_now();
|
|
}
|
|
|
|
/* 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);
|
|
/* The gate the [job] comment argues for, asked at the only moment whose
|
|
* answer is worth anything. Claimed first and then dropped, rather than
|
|
* left in the ring for a later stop: the address it was built around was
|
|
* blessed by a stop that has already ended, and a job that waits for the
|
|
* *next* break would run against a blessing older still.
|
|
*
|
|
* The handle is closed for the reason the listener closes one it refuses
|
|
* for having no installer: a transient module nothing ever points into is
|
|
* unreachable the moment this returns, and dlopen refcounts by path, so
|
|
* dropping the handle on the floor here would leave a count that re-asking
|
|
* the same inspection bumps and nothing brings down. [n] is not bumped —
|
|
* nothing was installed, and [n] is what a caller polls to find out that
|
|
* something was. */
|
|
if (j.stopped_only && atomic_load(&depth) <= 0) {
|
|
atomic_fetch_add(&refused_while_running, 1);
|
|
if (j.handle != NULL) { dlclose(j.handle); }
|
|
continue;
|
|
}
|
|
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];
|
|
/* Both facts fold into the one flag, because the flag answers one
|
|
* question — can this be taken — and a break taken by a trap can take
|
|
* none of them. The terminal listing above says the same thing in
|
|
* words; the two must not describe different programs. */
|
|
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
|
|
(s->resumable && 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;
|
|
}
|
|
}
|
|
/* Checked before [reachable], because it is the stronger fact and the one
|
|
* with the better sentence: at a trap every restart is unreachable, and
|
|
* answering with the thunk-boundary reason would send the reader looking
|
|
* for an evaluation that is not there. */
|
|
if (!s->resumable) {
|
|
reply(o, "err this break was taken by a trap with no transfer channel, "
|
|
"so no restart can be taken from it; read the frame, then fix "
|
|
"and reload, or abort\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->resumable) {
|
|
reply(o, "err this break was taken by a trap with no transfer channel, "
|
|
"so no restart can be taken from it; read the frame, then fix "
|
|
"and reload, or abort\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;
|
|
}
|
|
/* How many stopped-only jobs the game thread has dropped for arriving at a
|
|
* frame boundary instead of at a break, and the sentence to say about it.
|
|
* Answered while running and while stopped alike — it is a count of things
|
|
* that have already happened, not a claim about the program's state now.
|
|
*
|
|
* The count leads and the text follows, because the daemon needs the count
|
|
* to decide and the text only to speak. It reads this before it delivers a
|
|
* stopped-only module and again while it waits for the value, and a number
|
|
* that moved in between is the answer. */
|
|
if (strcmp(line, "refusals") == 0) {
|
|
char hdr[32];
|
|
int k = snprintf(hdr, sizeof hdr, "%llu\n",
|
|
(unsigned long long)atomic_load(&refused_while_running));
|
|
if (k > 0) emit(o, hdr, (size_t)k);
|
|
reply(o, RESUMED);
|
|
reply(o, "\n");
|
|
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, unread = 0;
|
|
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, &unread);
|
|
/* A read that did not settle is refused in a sentence rather than sent as
|
|
* a header saying zero rows. The header cannot carry the difference: a row
|
|
* count is a number, and the number that means "I could not look" is the
|
|
* same number that means "nothing is held" — which is the answer this verb
|
|
* is most often asked to disprove. An [err] line is already a shape the
|
|
* daemon turns into the editor's message, so the distinction survives all
|
|
* the way to the person who asked.
|
|
*
|
|
* Both sentences name the *program's* behaviour and not the mechanism,
|
|
* because the mechanism is not actionable and the behaviour is: a table
|
|
* being rewritten this hard is a game thread allocating flat out, and the
|
|
* useful next move is to ask again, or to ask it while stopped. */
|
|
if (n < 0) {
|
|
free(counts); free(bytes); free(typelens); free(types);
|
|
if (unread > 0) {
|
|
char msg[192];
|
|
int k = snprintf(msg, sizeof msg,
|
|
"err the allocation registry could not be read whole: "
|
|
"%lld slot%s were being written through every one of "
|
|
"eight attempts, so any rows here would be missing "
|
|
"blocks; ask again\n",
|
|
(long long)unread, unread == 1 ? "" : "s");
|
|
/* Clamped, because [snprintf] returns the length it wanted rather
|
|
* than the length it wrote — [flan_dev_reg_emit]'s rule. */
|
|
if (k > (int)sizeof msg - 1) k = (int)sizeof msg - 1;
|
|
if (k > 0) emit(o, msg, (size_t)k);
|
|
} else {
|
|
reply(o, "err the allocation registry was being rearranged through "
|
|
"every one of eight attempts; no listing taken, rather than "
|
|
"one of a table that never existed — ask again\n");
|
|
}
|
|
return;
|
|
}
|
|
{
|
|
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;
|
|
}
|
|
/* A module that may only run from a break, and the word for it goes in front
|
|
* of the path rather than inside the module. The alternative was a fourth
|
|
* exported symbol beside [flan_reload_transient], which would have put the
|
|
* decision in both emitters and in the .so's ABI — for a fact that is not
|
|
* about the module's *contents* at all. What is stopped-only is the
|
|
* *question*, not the code: the same rendering machinery, rooted at a frame
|
|
* slot instead of a raw address, is perfectly safe to run while the program
|
|
* runs. So it travels with the request. See [job].
|
|
*
|
|
* Everything after the prefix is the ordinary path, gate included, so a
|
|
* stopped-only module with no installer is still refused for having no
|
|
* installer. */
|
|
int stopped_only = 0;
|
|
if (strncmp(line, "stopped-only ", 13) == 0) {
|
|
stopped_only = 1;
|
|
line += 13;
|
|
}
|
|
/* 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){ .install = f, .call = c,
|
|
.handle = transient == NULL ? NULL : h,
|
|
.stopped_only = stopped_only }))
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* ── Outliving the daemon, which this program must not do ──────────── */
|
|
|
|
/* Under [--two-process] the compiled program is a child of the daemon, and
|
|
* for the whole of its life the daemon is the only thing that will ever end
|
|
* it: the program is a game loop or a listener, it has no reason of its own to
|
|
* stop, and lib/dev.ml's [Fun.protect ~finally] is what kills it when the
|
|
* session closes. That finally block runs when the daemon exits. It does not
|
|
* run when the daemon is SIGKILLed — by a test harness tearing down after a
|
|
* failure, by a watchdog that fired, by anybody at all — and what is left
|
|
* behind is a program with ppid 1, sleeping, holding a window and a socket,
|
|
* waiting for requests from a process that no longer exists. Eight of those
|
|
* were found on one machine, the oldest six days old. Nothing would ever have
|
|
* woken them: there is no timeout anywhere in this file, and none should be
|
|
* added — a program between frames is supposed to wait indefinitely.
|
|
*
|
|
* So the child is told to die with its parent instead, which on Linux is one
|
|
* call. PR_SET_PDEATHSIG asks the kernel to deliver a signal to *this* process
|
|
* when the process that is currently its parent dies. It is exactly the right
|
|
* primitive and it has exactly one sharp edge, which is that it is armed from
|
|
* the child and so is armed a moment *after* the fork: if the parent died in
|
|
* that moment, the signal it would have sent has already not been sent, and
|
|
* the child waits forever having done everything right. The standard answer is
|
|
* the one below — arm, then ask who the parent is now, and if it is not the
|
|
* one that was expected, deliver the signal by hand. [getppid() == 1] is the
|
|
* form this idiom is usually written in and it is wrong here: under a systemd
|
|
* user session, or in a container with a subreaper, an orphan is reparented to
|
|
* the subreaper and not to init, so the test would pass and the child would
|
|
* still be stranded. The daemon therefore puts its own pid in the environment
|
|
* and this compares against that, which cannot be fooled by who does the
|
|
* adopting.
|
|
*
|
|
* FLAN_DEV_PARENT is a positive gate and is set by [two_process] and by
|
|
* nothing else. A merged build runs this same file and must never arm any of
|
|
* this: there the daemon *is* this process, its parent is whoever typed [flan
|
|
* dev] — a shell, an emacs, a terminal that is about to be closed — and none
|
|
* of those is the session's owner. A person who starts a daemon in one
|
|
* terminal and attaches an editor to it from another is doing a normal thing,
|
|
* and killing their live session because the shell exited would be a worse bug
|
|
* than the leak. Gating on the *absence* of a merged-build variable would have
|
|
* covered the same cases today and quietly stopped covering them the first
|
|
* time somebody added a third shape; this way the only process that arms is
|
|
* the one the daemon explicitly told to.
|
|
*
|
|
* The one gap left is the window before [agent/start]: a child SIGKILLed out
|
|
* from under during the first few milliseconds of its own startup is still
|
|
* stranded, because nothing has armed yet. It is not closed here because
|
|
* closing it means arming from flan_rt.c, which every program links and which
|
|
* would put a Linux-only prctl in the one file that also has to compile for
|
|
* wasm. The daemon already refuses to run a program that never reaches
|
|
* [agent/start], so every child that lives long enough to matter passes
|
|
* through here. */
|
|
|
|
#if defined(__linux__) && defined(SIGPWR)
|
|
#define FLAN_ORPHAN_SIG SIGPWR
|
|
#endif
|
|
|
|
/* Stashed at arming time rather than read from the environment in the handler:
|
|
* getenv is not async-signal-safe, and this path can run on any thread at any
|
|
* instruction. sun_path's size is the bound because that is where it came
|
|
* from. */
|
|
static char orphan_sock[sizeof(((struct sockaddr_un *)0)->sun_path)];
|
|
|
|
#if defined(FLAN_ORPHAN_SIG)
|
|
/* Three calls, and the restraint is the point. This runs on whichever thread
|
|
* the kernel picked, which may be the game thread halfway through a printf,
|
|
* holding stdio's lock — so fprintf here would deadlock against itself and
|
|
* fflush(NULL) would deadlock against the other thread. write, unlink and
|
|
* _exit are on the async-signal-safe list and nothing else here is.
|
|
*
|
|
* Not flushing is a real cost and a small one: flan_rt_init sets stdout line
|
|
* buffered precisely so that a program's output is observable as it runs, so
|
|
* what is lost is at most a partial line. Losing it beats hanging.
|
|
*
|
|
* The socket is unlinked because this process bound it and no other process
|
|
* knows it is stale — the daemon that would have cleaned up is the thing that
|
|
* just died. Its directory is left alone: that belongs to the daemon, and
|
|
* tidying up someone else's directory from a signal handler is how a signal
|
|
* handler becomes a bug.
|
|
*
|
|
* Status 0, because nothing went wrong. The program did what it was told for
|
|
* as long as there was anybody to tell it. */
|
|
static void orphan_die(int sig) {
|
|
static const char said[] =
|
|
"flan dev: the daemon that owns this program is gone, so the program is "
|
|
"stopping too\n";
|
|
ssize_t ignored;
|
|
(void)sig;
|
|
ignored = write(2, said, sizeof said - 1);
|
|
(void)ignored;
|
|
if (orphan_sock[0] != '\0') unlink(orphan_sock);
|
|
_exit(0);
|
|
}
|
|
#endif
|
|
|
|
static void watch_the_daemon(const char *sock) {
|
|
#if defined(FLAN_ORPHAN_SIG)
|
|
const char *want_s = getenv("FLAN_DEV_PARENT");
|
|
char *end;
|
|
long want;
|
|
struct sigaction sa;
|
|
if (want_s == NULL || want_s[0] == '\0') return;
|
|
want = strtol(want_s, &end, 10);
|
|
if (end == want_s || *end != '\0' || want <= 0) return;
|
|
/* SIGPWR and not SIGTERM, and the collision it avoids is not hypothetical:
|
|
* lib/dev.ml's ordinary teardown kills this child with SIGTERM, and in
|
|
* --two-process the child's stderr *is* the daemon's stderr. A handler on
|
|
* SIGTERM would print "the daemon is gone" into the daemon's own output on
|
|
* every clean session close, which is both noise and a sentence that is only
|
|
* half true. A signal the program will never otherwise receive keeps the two
|
|
* deaths distinguishable, and leaves SIGTERM on its default disposition
|
|
* where the existing teardown already relies on it. */
|
|
memset(&sa, 0, sizeof sa);
|
|
sa.sa_handler = orphan_die;
|
|
sigfillset(&sa.sa_mask);
|
|
if (sigaction(FLAN_ORPHAN_SIG, &sa, NULL) != 0) return;
|
|
strncpy(orphan_sock, sock, sizeof orphan_sock - 1);
|
|
if (prctl(PR_SET_PDEATHSIG, FLAN_ORPHAN_SIG) != 0) return;
|
|
/* The race, closed. Armed above, so from here on the kernel will tell us;
|
|
* this asks whether it already should have. */
|
|
if ((long)getppid() != want) raise(FLAN_ORPHAN_SIG);
|
|
#else
|
|
(void)sock;
|
|
#endif
|
|
}
|
|
|
|
/* [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;
|
|
/* After the bind, because the path this hands over is the one that was
|
|
actually bound — the environment's override included — and because a
|
|
program that is going to fail to listen should fail on its own terms
|
|
rather than arrange its death first. */
|
|
watch_the_daemon(addr.sun_path);
|
|
/* 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;
|
|
flan_trap_hook = trap_stop;
|
|
return 0;
|
|
}
|