266 lines
9.6 KiB
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;
|
|
}
|