fix Ctrl-C swallowing 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, so the line typed next was silently discarded. Register a signal event hook that empties the line, the way bash does, and clear the flag around readline instead of eating the line: a Ctrl-C during a command still aborts the turn, and one during a multi-line task still abandons it. Two pty-based regression tests cover the idle-prompt and mid-line cases.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/wait.h>
|
||||
#include <pty.h>
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user