/* flan_agent — the half of the dev loop that lives in the running program. * * A redefinition arrives as a path to a .so (runtime/flan_dev.c and * Emit.redefinition are what put it there). Two things have to happen to it, * and they must happen on different threads: * * dlopen relocates the module and runs the loader. It is milliseconds, * unbounded, and takes the loader lock. Doing it on the game thread * is a dropped frame. * install is one store per redefined function. It is sub-microsecond, and * it must happen at a point where no redefined function is on the * stack — a frame boundary — or a frame runs half in the old code * and half in the new. * * So the listener thread does the loading and hands over a function pointer; * the game thread calls flan-poll or flan-wait when it is between frames and * that is when the swap becomes visible. Nothing else in the program needs to * know the agent exists. * * Nothing is ever dlclosed: a cell holds an address inside a module's text, * and unloading it would leave every call site pointing at unmapped memory. * * This is not a protocol. One line per request, the path to load, and a one * line answer. The daemon and its nREPL are a separate program that will speak * to a socket, not something this file grows into. */ #include #include #include #include #include #include #include #include #include #include #include #include typedef void (*install_fn)(void); /* A module may also carry a thunk to run once — that is C-x C-e, an expression * compiled into a function with nowhere to be called from. It runs where the * install happens, on the game thread between frames, because an expression * that reads the program's state has to see it at a point the program agrees * is consistent. */ typedef void (*call_fn)(void); const char *flan_dev_result_get(uint64_t *gen, uint64_t *len); /* A ring the listener writes and the game thread reads. One producer, one * consumer, so two atomics and no lock — the game thread must never block on * the loader. Overflow drops the oldest request rather than stalling; a dev * loop that queues 64 reloads between two frames has a bigger problem. */ #define QUEUE 64 /* [handle] is set only for a module that declared itself transient — one that * ran a thunk and left nothing behind. Everything else is kept mapped forever: * a cell holds an address inside a module's text, and unloading it would leave * every call site pointing at unmapped memory. */ typedef struct { install_fn install; call_fn call; void *handle; } job; static job queue[QUEUE]; static atomic_uint head; /* written by the listener */ static atomic_uint tail; /* written by the game thread */ static int listen_fd = -1; static pthread_t listener; static atomic_int started; static void publish(job j) { unsigned h = atomic_load_explicit(&head, memory_order_relaxed); queue[h % QUEUE] = j; /* Release: the store to the slot must be visible before the index that * advertises it. */ atomic_store_explicit(&head, h + 1, memory_order_release); } /* Returns how many modules were installed. Call it between frames. */ /* ── The break loop, spec-conditions.md §2 ──────────────────────────── */ /* Where "a crash kills the program" stops being true. An unhandled error runs * this instead of rt_die(), on the frame that erred, with nothing unwound — so * the condition and every restart between here and the top are still live. * * It is the *poll* loop, run from here instead of from the frame boundary, and * that is the whole design. An expression evaluated while stopped is a module * the loader thread queues and the game thread runs; if this loop did not * drain that queue, C-x C-e would hang exactly when it is most wanted. * * Installing while stopped is deliberately allowed. The rule that a redefined * function must not be swapped while it is on the stack is about *mid-frame * consistency* — half a frame of old code and half of new — and there is no * frame in progress here. The old body on the stack keeps running; a retry * restart calls through the cell and reaches the new one. That is the fix-it- * and-retry loop, and refusing the install would remove the point. */ /* The runtime's end of it. flan_rt.c holds the hook and the lookup; the loop * itself is here, because it needs the socket and the install queue and the * runtime must not depend on an optional package for either. */ extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition, void *xfer); extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen, void *xfer); extern int32_t flan_restart_count(void); extern const uint8_t *flan_restart_name(int32_t i, int64_t *len); /* What the listener thread hands the stopped game thread. One slot, because * only one thread is ever stopped. */ static _Atomic int broken; /* the game thread is in the loop */ static char chosen[128]; static _Atomic int chosen_ready; static _Atomic int aborting; /* The condition's class name, so an editor can say what stopped rather than * only that something did. It is all there is to say: the hook is handed the * name and an opaque pointer, and nothing at run time can render a value whose * type it does not know. Written before [broken] is set and read only while * [broken] is 1, so the listener never sees half of it. */ static char condition_name[128]; int32_t flan_agent_poll(void); static void break_loop(const uint8_t *name, int64_t namelen, void *condition, void *xfer) { struct timespec step = { 0, 2000000 }; /* 2ms */ (void)condition; fflush(stdout); fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n", (int)namelen, (const char *)name); { int32_t n = flan_restart_count(); if (n == 0) fprintf(stderr, " no restarts are active; abort, or fix and reload\n"); for (int32_t i = 0; i < n; i++) { int64_t len = 0; const uint8_t *nm = flan_restart_name(i, &len); if (nm != NULL) fprintf(stderr, " restart: %.*s\n", (int)len, (const char *)nm); } } fflush(stderr); { 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. */ atomic_store(&broken, 1); for (;;) { flan_agent_poll(); if (atomic_load(&aborting)) { fflush(stdout); fprintf(stderr, "flan: aborted at the break loop\n"); exit(134); } if (atomic_load(&chosen_ready)) { int32_t ok = flan_break_resume((const uint8_t *)chosen, (int64_t)strlen(chosen), xfer); atomic_store(&chosen_ready, 0); if (ok) { fprintf(stderr, "flan: resuming at restart %s\n", chosen); fflush(stderr); atomic_store(&broken, 0); return; } fprintf(stderr, "flan: no restart named %s is active\n", chosen); 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. */ if (j.call != NULL) { j.call(); } if (j.handle != NULL) { dlclose(j.handle); } } } /* The same, but waits up to [ms] for something to arrive first. A game loop * does not want this; a headless test does, because it makes the reload * deterministic instead of a race against the frame rate. */ int32_t flan_agent_wait(int32_t ms) { struct timespec step = { 0, 1000000 }; /* 1ms */ for (int32_t i = 0; i < ms; i++) { int32_t n = flan_agent_poll(); if (n > 0) return n; nanosleep(&step, NULL); } return flan_agent_poll(); } /* MSG_NOSIGNAL rather than write(2). A reply goes out in more than one piece, * and a sender that has read enough and closed leaves the rest of it writing * into a closed socket — which is SIGPIPE, whose default action would kill the * program the agent is embedded in. Suppressing it per call rather than * installing a handler, because the disposition belongs to the program and not * to us. */ static void reply(int fd, const char *s) { size_t n = strlen(s); while (n > 0) { ssize_t k = send(fd, s, n, MSG_NOSIGNAL); if (k <= 0) return; s += k; n -= (size_t)k; } } /* One connection, one line, one module. Loading here rather than in the game * thread is the whole reason this thread exists. */ static void serve(int fd) { char line[4096]; size_t n = 0; for (;;) { ssize_t k = read(fd, line + n, sizeof line - n - 1); if (k <= 0) return; n += (size_t)k; line[n] = '\0'; char *nl = strchr(line, '\n'); if (nl == NULL) { if (n == sizeof line - 1) { reply(fd, "err path too long\n"); return; } continue; } *nl = '\0'; /* The one verb that is not a module: read back the value of the last * expression evaluated, with the counter that says whether it is a new * one. The daemon polls this rather than the agent holding a connection * open across a frame boundary it does not control. */ /* Only while stopped: what is on offer, and which one to take. Both are * refused when the program is running, by name, rather than silently * doing nothing — there is no restart stack to walk from here. */ /* Whether the program is stopped, and on what. Answered while it is * running too — "running" is an answer, not a refusal — because this is * the one question an editor asks without knowing the state already, and * refusing it would leave nothing to poll. */ if (strcmp(line, "status") == 0) { if (atomic_load(&broken)) { reply(fd, "stopped "); reply(fd, condition_name); reply(fd, "\n"); } else reply(fd, "running\n"); return; } if (strcmp(line, "restarts") == 0) { if (!atomic_load(&broken)) { reply(fd, "err not stopped\n"); return; } int32_t n = flan_restart_count(); for (int32_t i = 0; i < n; i++) { int64_t len = 0; const uint8_t *nm = flan_restart_name(i, &len); if (nm != NULL) { send(fd, nm, (size_t)len, MSG_NOSIGNAL); reply(fd, "\n"); } } reply(fd, ".\n"); return; } if (strncmp(line, "restart ", 8) == 0) { if (!atomic_load(&broken)) { reply(fd, "err not stopped\n"); return; } size_t k = strlen(line + 8); if (k == 0 || k >= sizeof chosen) { reply(fd, "err bad restart name\n"); return; } /* Checked here, against the stack the stopped thread is holding still, * rather than accepted and found wrong after the reply has gone. "ok" * has to mean the program will resume. */ { int32_t n = flan_restart_count(), found = 0; for (int32_t i = 0; i < n && !found; i++) { int64_t len = 0; const uint8_t *nm = flan_restart_name(i, &len); found = nm != NULL && (size_t)len == k && memcmp(nm, line + 8, k) == 0; } if (!found) { reply(fd, "err no restart named "); reply(fd, line + 8); reply(fd, " is active\n"); return; } } memcpy(chosen, line + 8, k + 1); /* Published last, so the game thread never reads a half-written name. */ atomic_store(&chosen_ready, 1); reply(fd, "ok\n"); return; } if (strcmp(line, "abort") == 0) { if (!atomic_load(&broken)) { reply(fd, "err not stopped\n"); return; } reply(fd, "ok\n"); atomic_store(&aborting, 1); return; } if (strcmp(line, "result") == 0) { uint64_t gen = 0, len = 0; const char *v = flan_dev_result_get(&gen, &len); char hdr[64]; int k = snprintf(hdr, sizeof hdr, "%llu %llu\n", (unsigned long long)gen, (unsigned long long)len); if (k > 0) { send(fd, hdr, (size_t)k, MSG_NOSIGNAL); if (len > 0) send(fd, v, (size_t)len, MSG_NOSIGNAL); } return; } void *h = dlopen(line, RTLD_NOW | RTLD_LOCAL); if (h == NULL) { reply(fd, "err "); reply(fd, dlerror()); reply(fd, "\n"); return; } install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install"); if (f == NULL) { reply(fd, "err no flan_reload_install\n"); return; } /* Optional: only an expression evaluation has one. */ call_fn c = (call_fn)(uintptr_t)dlsym(h, "flan_reload_call"); /* And only one that leaves nothing behind may be unloaded. */ void *transient = (c == NULL) ? NULL : dlsym(h, "flan_reload_transient"); /* Answer before queueing, not after. The game thread can install and run * to completion between the two, and a program that exits there would tear * down this connection with the reply still unwritten — which reaches the * sender as a reset, not as an answer. */ reply(fd, "ok\n"); /* "queued", not "installed": the store happens on the game thread, at a * time this thread does not get to choose. */ publish((job){ f, c, transient == NULL ? NULL : h }); return; } } static void *accept_loop(void *arg) { (void)arg; for (;;) { int fd = accept(listen_fd, NULL, NULL); if (fd < 0) { if (errno == EINTR) continue; return NULL; } serve(fd); close(fd); } } /* [path] is a Flan string: ptr and len, not NUL-terminated. * * FLAN_AGENT_SOCKET overrides it. A program's source has to name some path, * and the daemon that launches the program is the one that knows where it * wants to talk to it — without the override the daemon would have to guess, * and guessing wrong fails silently: everything compiles, the module is built, * and nothing ever receives it. */ int32_t flan_agent_start(const uint8_t *path, int64_t len) { struct sockaddr_un addr; const char *env = getenv("FLAN_AGENT_SOCKET"); if (atomic_exchange(&started, 1)) return 0; if (env != NULL && env[0] != '\0') { path = (const uint8_t *)env; len = (int64_t)strlen(env); } if (len <= 0 || (size_t)len >= sizeof addr.sun_path) return -1; memset(&addr, 0, sizeof addr); addr.sun_family = AF_UNIX; memcpy(addr.sun_path, path, (size_t)len); unlink(addr.sun_path); listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd < 0) return -1; if (bind(listen_fd, (struct sockaddr *)&addr, sizeof addr) < 0) return -1; if (listen(listen_fd, 4) < 0) return -1; if (pthread_create(&listener, NULL, accept_loop, NULL) != 0) return -1; /* From here an unhandled error stops rather than dying. Installed with the * socket and not before it: without a listener there is nobody to ask what * to do, and stopping forever is worse than the abort it replaces. */ flan_break_hook = break_loop; return 0; }