diff --git a/README.md b/README.md index 481bdf5..40d9444 100644 --- a/README.md +++ b/README.md @@ -416,12 +416,13 @@ where you are when you press it: | -------------------- | ------------------------------------------------------------------- | | Running a command | The process group is killed, exit 130 is reported, the turn ends. | | Waiting on the API | The request is abandoned and the turn ends. | -| Part way through a line | The half-typed line is thrown away. | -| Sitting at the prompt | Solo survives and stays usable, but the flag lingers, so the next line you type is silently discarded. | +| Part way through a line | The half-typed line is cleared; the next line runs normally. | +| Mid way through a multi-line task | The whole task is abandoned. | +| Sitting at the prompt | Solo survives and stays usable; the next line runs normally. | -That last case is the one wart: press Ctrl-C once at an idle prompt and your -next command vanishes without a word. Press it twice and the second one goes -through, so it is self-correcting in practice. +The half-typed line is cleared by Solo itself: readline only redraws the +prompt on Ctrl-C and leaves the text in its buffer, so Solo registers a +signal hook that empties the line, the way bash does. ## Example session diff --git a/main.c b/main.c index 72bc9a2..7d50ef0 100644 --- a/main.c +++ b/main.c @@ -42,6 +42,16 @@ static int GN(json_object *o, const char *k, int d) { json_object *v = G(o, k); static volatile sig_atomic_t IN; static void onint(int s) { (void)s; IN = 1; } +/* readline calls this from its input loop, in a safe context, after it has + dealt with a signal caught while it was reading a line. A Ctrl-C only + redraws the prompt: the half-typed text stays in the buffer, so clear it + here, the way bash does. The flag is left set on purpose; the prompt loop + clears it, so a Ctrl-C during a command still aborts the turn, and one + during a multi-line task still abandons it. */ +static int ci_hook(void) { + if(IN) { rl_replace_line("", 1); rl_redisplay(); } + return 0; +} static char *EP, *KEY, *MOD, *RSN, *SF, *TMP, *SYS, *SELF, SH[256] = "/bin/sh"; static int TO = 120, QUIT, CLR = 1; @@ -402,6 +412,7 @@ int main(int argc, char **argv) { if(!(e = getenv("SHELL")) || access(e, X_OK)) e = access("/bin/bash", X_OK) ? "/bin/sh" : "/bin/bash"; snprintf(SH, sizeof SH, "%s", e); signal(SIGINT, onint); signal(SIGPIPE, SIG_IGN); + rl_signal_event_hook = ci_hook; create(); load(); if((e = getenv("SOLO_MODEL")) && *e) { setstr(&MOD, e); A(SSN, "model", S(e)); } if((e = getenv("SOLO_TEMP")) && *e) { TEMP = atof(e); A(SSN, "temperature", D(TEMP)); } @@ -416,8 +427,9 @@ int main(int argc, char **argv) { free(task); while(!QUIT) { snprintf(pb, sizeof pb, "\001%s\002> \001%s\002", CLR ? BLD : "", CLR ? OF : ""); + IN = 0; /* a Ctrl-C during a command must not reach the line about to be typed */ if(!(ln = readline(pb))) { puts(""); break; } - if(IN) { IN = 0; free(ln); continue; } + IN = 0; /* a Ctrl-C during the line only cleared that line, not this turn */ ac = ln; for(;;) { size_t l = strlen(ac); diff --git a/test.sh b/test.sh index 79415c6..6f88f3a 100755 --- a/test.sh +++ b/test.sh @@ -119,6 +119,71 @@ LEFT=$(ps -o args= 2>/dev/null | grep -c '^sleep 41$') [ "$LEFT" -eq 0 ] ok $? 'a timed out command takes its process group with it' "$LEFT process(es) left" +# --- Ctrl-C must not eat the next line -------------------------------- +# readline only redraws the prompt on Ctrl-C: the half-typed text +# stays in its buffer and the interrupt flag outlives the prompt. +# Both used to swallow the next command silently. This needs a real +# tty, so the agent runs on a pty driven by a small helper. +D=$WORK/ctrlc +cat >"$WORK/ptyrun.c" <<'PTY_EOF' +/* Drive PROG on a pty from a script on stdin. A line "INT" sends + SIGINT, a line "RAW text" writes text without a newline, and any + other line is written with a newline appended. A short pause + follows every step. Everything PROG prints is relayed to stdout. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + int m, st; pid_t k; char buf[4096], line[4096]; struct timeval tv; fd_set fs; + if(argc < 2) { fprintf(stderr, "usage: ptyrun PROG [ARG...] < script\n"); return 2; } + k = forkpty(&m, 0, 0, 0); + if(k < 0) { perror("forkpty"); return 2; } + if(!k) { execvp(argv[1], argv + 1); _exit(127); } + fcntl(m, F_SETFL, O_NONBLOCK); + while(fgets(line, sizeof line, stdin)) { + size_t l = strlen(line); + while(l && (line[l - 1] == '\n' || line[l - 1] == '\r')) line[--l] = 0; + usleep(400000); + if(!strcmp(line, "INT")) { kill(k, SIGINT); continue; } + if(!strncmp(line, "RAW ", 4)) { if(write(m, line + 4, strlen(line + 4)) < 0) break; continue; } + if(write(m, line, strlen(line)) < 0 || write(m, "\n", 1) < 0) break; + } + for(;;) { + FD_ZERO(&fs); FD_SET(m, &fs); tv.tv_sec = 10; tv.tv_usec = 0; + if(select(m + 1, &fs, 0, 0, &tv) <= 0) break; + st = read(m, buf, sizeof buf); + if(st <= 0) break; + if(write(1, buf, st) < 0) break; + } + kill(k, SIGKILL); + waitpid(k, &st, 0); + return 0; +} +PTY_EOF +# forkpty lives in libc since glibc 2.34; older systems need -lutil. +cc -Wall -Wextra -O2 -std=c99 -o "$WORK/ptyrun" "$WORK/ptyrun.c" 2>/dev/null \ + || cc -Wall -Wextra -O2 -std=c99 -o "$WORK/ptyrun" "$WORK/ptyrun.c" -lutil 2>/dev/null +[ -x "$WORK/ptyrun" ] || { echo "test: the pty helper did not build" >&2; exit 1; } +mkdir -p "$D" +( cd "$D" && printf 'INT\n!echo PTY_CI\n/quit\n' | "$WORK/ptyrun" "$SOLO" ) \ + >"$WORK/ctrlc.out" 2>&1 +grep -q -- '--- \$ echo PTY_CI' "$WORK/ctrlc.out" +ok $? 'a command typed after Ctrl-C at the prompt still runs' +( cd "$D" && printf 'RAW echo partial\nINT\n!echo PTY_FRESH\n/quit\n' | "$WORK/ptyrun" "$SOLO" ) \ + >"$WORK/ctrlc2.out" 2>&1 +grep -q -- '--- \$ echo PTY_FRESH' "$WORK/ctrlc2.out" +ok $? 'a command typed after Ctrl-C mid-line still runs' +if grep -q 'partialPTY_FRESH\|partial!echo' "$WORK/ctrlc2.out"; then rc=1; else rc=0; fi +ok "$rc" 'Ctrl-C mid-line clears the half-typed text' + # --- the tool description must quote the real timeout ------------------------- D=$WORK/desc printf '/quit\n' | solo_in "$D" SOLO_TIMEOUT=42 >/dev/null 2>&1