flan/test/agent_hooks.c

266 lines
9.6 KiB
C

/* agent_hooks.c — the break loop's snapshot and its choice handoff, driven
* from inside the stopped thread.
*
* Every other break-loop test talks to a program from outside it, over the
* socket, and so can only ever see the interleavings a 2ms poll happens to
* produce. Two properties need a specific one:
*
* - a choice validated against one break and then met by a *different*
* break, nested inside the first, before the first looks for it;
* - a restart list longer than the snapshot holds, by count and by bytes.
*
* [flan_agent_break_poll_hook] runs on the stopped thread once per turn of
* the loop, where a thunk from the poll would run. Requests go through
* [flan_agent_request], the in-process path, which is the same verb table the
* socket reaches. The program under the hook is test/programs/agent-hooks.flan,
* which has no [main]; this file is the entry point, as reload_host.c is.
*
* argv: the mode, and the socket path the agent binds (it binds one to
* install its hooks, and nothing here ever connects to it).
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
void flan_rt_init(int32_t argc, char **argv);
int32_t flan_agent_start(const uint8_t *path, int64_t len);
char *flan_agent_request(const char *line, uint64_t *len);
void flan_agent_request_free(char *p);
extern void (*flan_agent_break_poll_hook)(void);
extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen,
void *condition, void *xfer);
void *flan_restart_push_c(const uint8_t *name, int64_t namelen,
const uint8_t *report, int64_t reportlen);
void flan_restart_pop_c(void *frame);
/* The trailing ptr is the transfer channel every Flan signature carries. */
extern int32_t flan_deep(int32_t n, void *xfer) __asm__("flan.deep");
extern int32_t flan_wide(int32_t n, void *xfer) __asm__("flan.wide");
/* One request, its reply printed to [buf] and returned. */
static char reply_buf[1 << 16];
static const char *ask(const char *line) {
uint64_t n = 0;
char *r = flan_agent_request(line, &n);
if (n >= sizeof reply_buf) n = sizeof reply_buf - 1;
memcpy(reply_buf, r ? r : "", (size_t)n);
reply_buf[n] = '\0';
flan_agent_request_free(r);
return reply_buf;
}
/* ── The snapshot's two caps ─────────────────────────────────────────
*
* SNAP_MAX is 64 entries and SNAP_NAMES is 4096 bytes of names. [deep 70]
* puts 71 restarts on the stack with one-letter names, so the count is what
* runs out; [wide 30] puts 31 with 200-byte names, so the bytes run out after
* twenty. What is checked is that the listing stops cleanly at the cap —
* every entry it does list is whole and takeable — and that the terminal
* says how many were left out. The take is the proof that the indices in
* the truncated listing still name the frames they claim to. */
static int listed, longest, shortest, taken_once;
static const char *take_line;
static void list_and_take(void) {
const char *r, *p;
if (taken_once) return;
taken_once = 1;
r = ask("restarts");
listed = 0;
longest = 0;
shortest = 1 << 30;
for (p = r; *p != '\0' && *p != '.';) {
const char *nl = strchr(p, '\n');
const char *name;
int len;
if (nl == NULL) break;
/* "I F NAME\tARITY\tSIG\tLOC\tREPORT"; the name ends at the tab. */
name = strchr(p, ' ');
name = name ? strchr(name + 1, ' ') : NULL;
if (name != NULL) {
const char *tab = memchr(name + 1, '\t', (size_t)(nl - name - 1));
len = (int)((tab != NULL ? tab : nl) - name - 1);
} else
len = -1;
if (len > longest) longest = len;
if (len < shortest) shortest = len;
listed++;
p = nl + 1;
}
printf("listed %d\n", listed);
printf("names %d..%d\n", shortest, longest);
printf("take %s", ask(take_line));
fflush(stdout);
}
static int snapmax(void) {
void *xfer = NULL;
int32_t v;
take_line = "restart-at 5 retry";
flan_agent_break_poll_hook = list_and_take;
v = flan_deep(70, &xfer);
flan_agent_break_poll_hook = NULL;
printf("returned %d\n", v);
return 0;
}
static int snapnames(void) {
void *xfer = NULL;
int32_t v;
take_line = "restart-at 19";
flan_agent_break_poll_hook = list_and_take;
v = flan_wide(30, &xfer);
flan_agent_break_poll_hook = NULL;
printf("returned %d\n", v);
return 0;
}
/* ── A choice that lands inside the poll, with a break nested under it ──
*
* The outer break offers outer-b (0) and outer-a (1). On its first turn the
* hook chooses outer-a — validated against the outer snapshot, stamped with
* its generation — and then, still inside that turn, a second break starts
* on top, which is what a thunk that errors does. The inner break's list
* begins with [inner] and has outer-a at index 2, so an index 1 read without
* its generation would resume the inner break into outer-b: the wrong break,
* and the wrong frame.
*
* What must happen instead: the inner break turns past the choice that is
* not addressed to it, for as many turns as it is left alone, and resumes
* only on a choice made against its own list.
*
* The choice slot is one slot, so the inner choice overwrites the outer one;
* the inner break puts the outer one back when it is left, and the outer
* break takes it without being asked again. */
static int level, inner_turns, outer_turns, reasked;
static void *outer_a, *outer_b, *inner;
static void stale_hook(void) {
if (level == 1) {
outer_turns++;
if (outer_turns == 1) {
void *xin = NULL;
printf("outer choice %s", ask("restart-at 1 outer-a"));
inner = flan_restart_push_c((const uint8_t *)"inner", 5, NULL, 0);
level = 2;
flan_break_hook((const uint8_t *)"Inner", 5, NULL, &xin);
level = 1;
printf("inner turns %d\n", inner_turns);
printf("inner resumed into %s\n",
xin == inner ? "inner"
: xin == outer_a ? "outer-a"
: xin == outer_b ? "outer-b"
: "nothing");
flan_restart_pop_c(inner);
fflush(stdout);
return;
}
/* The outer break is still here, so its choice did not survive. */
reasked++;
ask("restart-at 1 outer-a");
return;
}
inner_turns++;
if (inner_turns == 5) {
printf("status %s", ask("status"));
printf("inner choice %s", ask("restart-at 0 inner"));
fflush(stdout);
}
}
static int stale(void) {
void *xout = NULL;
outer_a = flan_restart_push_c((const uint8_t *)"outer-a", 7, NULL, 0);
outer_b = flan_restart_push_c((const uint8_t *)"outer-b", 7, NULL, 0);
level = 1;
flan_agent_break_poll_hook = stale_hook;
flan_break_hook((const uint8_t *)"Outer", 5, NULL, &xout);
flan_agent_break_poll_hook = NULL;
printf("outer re-asked %d\n", reasked);
printf("outer resumed into %s\n",
xout == outer_a ? "outer-a"
: xout == outer_b ? "outer-b"
: "nothing");
flan_restart_pop_c(outer_b);
flan_restart_pop_c(outer_a);
return 0;
}
/* ── A restart accepted with a read queued behind it ────────────────
*
* The inspector's [dyn] read is a job for the stopped thread, named for the
* stop. Requests take effect in the order they were sent: one queued before a
* restart is accepted runs in the stop, and then the restart is taken; one
* asked for after acceptance is refused at the door. */
extern uint64_t flan_dynword(void *xfer) __asm__("flan.dynword");
int32_t flan_agent_poll(void);
static int resuming_done;
static char r_queued[128], r_take[128], r_late[128];
static pthread_t resumer;
/* The listener's side of it, from another thread: the break loop is asleep
* between turns when these land, as a request from the editor would be. */
static void *resume_from_outside(void *arg) {
const char *line = arg;
struct timespec pause = { 0, 500000 };
nanosleep(&pause, NULL);
snprintf(r_queued, sizeof r_queued, "%s", ask(line));
snprintf(r_take, sizeof r_take, "%s", ask("restart-at 0 retry"));
snprintf(r_late, sizeof r_late, "%s", ask(line));
return NULL;
}
static void resuming_hook(void) {
static char line[64];
void *x = NULL;
if (resuming_done) return;
resuming_done = 1;
snprintf(line, sizeof line, "dyn %llu",
(unsigned long long)flan_dynword(&x));
pthread_create(&resumer, NULL, resume_from_outside, line);
}
static long refusal_count(void) { return strtol(ask("refusals"), NULL, 10); }
static int resuming(void) {
void *xfer = NULL;
int32_t v;
long before;
flan_agent_break_poll_hook = resuming_hook;
v = flan_deep(0, &xfer);
flan_agent_break_poll_hook = NULL;
pthread_join(resumer, NULL);
printf("queued %stake %slate %s", r_queued, r_take, r_late);
printf("returned %d\n", v);
/* The read wrote the result buffer, whose generation starts at zero. */
printf("read %s\n", strtol(ask("result"), NULL, 10) > 0 ? "ran" : "did not run");
before = refusal_count();
flan_agent_poll();
printf("dropped %ld\n", refusal_count() - before);
return 0;
}
int main(int argc, char **argv) {
flan_rt_init(argc, argv);
if (argc < 3) {
fprintf(stderr, "usage: %s snapmax|snapnames|stale SOCKET\n", argv[0]);
return 2;
}
if (flan_agent_start((const uint8_t *)argv[2], (int64_t)strlen(argv[2])) != 0) {
fprintf(stderr, "the agent did not start on %s\n", argv[2]);
return 2;
}
setvbuf(stdout, NULL, _IOLBF, 0);
if (strcmp(argv[1], "snapmax") == 0) return snapmax();
if (strcmp(argv[1], "snapnames") == 0) return snapnames();
if (strcmp(argv[1], "stale") == 0) return stale();
if (strcmp(argv[1], "resuming") == 0) return resuming();
fprintf(stderr, "unknown mode %s\n", argv[1]);
return 2;
}