fix: close the REPL connection on Ctrl+C
Exit the readline loop on interruption so normal WebSocket shutdown completes. Verify Ctrl+C in a Linux pseudo-terminal against the real image server and confirm it remains available after disconnect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
750b709d35
commit
d0850fe6d4
|
|
@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Exit the REPL on Ctrl+C and complete the WebSocket close handshake instead
|
||||||
|
of returning to the prompt.
|
||||||
- Complete the WebSocket close handshake when the CLI exits, preventing normal
|
- Complete the WebSocket close handshake when the CLI exits, preventing normal
|
||||||
disconnects from producing a connection-reset protocol error on the server.
|
disconnects from producing a connection-reset protocol error on the server.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ mise run repl
|
||||||
The REPL defaults to `127.0.0.1:3737`. To select another loopback endpoint,
|
The REPL defaults to `127.0.0.1:3737`. To select another loopback endpoint,
|
||||||
use `cargo run -p runa-cli -- --connect 127.0.0.1:4000 repl` and start the server with
|
use `cargo run -p runa-cli -- --connect 127.0.0.1:4000 repl` and start the server with
|
||||||
the corresponding `--listen` address. Quitting the REPL disconnects only that
|
the corresponding `--listen` address. Quitting the REPL disconnects only that
|
||||||
client; actors and source files survive until the server exits.
|
client; actors and source files survive until the server exits. Ctrl+C and
|
||||||
|
Ctrl+D also exit the REPL and complete the WebSocket close handshake.
|
||||||
Reconnect and use `ps` to recover their references. REPL bindings and
|
Reconnect and use `ps` to recover their references. REPL bindings and
|
||||||
command history are session-local and are not restored on reconnect.
|
command history are session-local and are not restored on reconnect.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,10 @@ fn run_interactive_repl(server: &mut dyn Connection) -> Result<(), Box<dyn std::
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(ReadlineError::Interrupted) => writeln!(output, "^C")?,
|
Err(ReadlineError::Interrupted) => {
|
||||||
|
writeln!(output, "^C")?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
Err(ReadlineError::Eof) => {
|
Err(ReadlineError::Eof) => {
|
||||||
writeln!(output)?;
|
writeln!(output)?;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ the actor tree and open snapshot documents. Manual refresh remains available.
|
||||||
|
|
||||||
## WebSocket transport and notifications
|
## WebSocket transport and notifications
|
||||||
|
|
||||||
On `quit`, `exit`, or end-of-input, the CLI completes the WebSocket close
|
On `quit`, `exit`, Ctrl+C, or end-of-input, the CLI completes the WebSocket close
|
||||||
handshake with a 5-second deadline before releasing the connection. A failed
|
handshake with a 5-second deadline before releasing the connection. A failed
|
||||||
handshake is reported rather than treating an abrupt reset as a normal close.
|
handshake is reported rather than treating an abrupt reset as a normal close.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,56 @@ import { promisify } from "node:util";
|
||||||
import { ImageClient } from "../src/client";
|
import { ImageClient } from "../src/client";
|
||||||
import { startImage } from "./server";
|
import { startImage } from "./server";
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Ctrl+C exits a terminal REPL with a clean WebSocket handshake",
|
||||||
|
{ timeout: 10000, skip: process.platform !== "linux" },
|
||||||
|
async (t) => {
|
||||||
|
const image = await startImage();
|
||||||
|
t.after(() => image.stop());
|
||||||
|
const executable = resolve(__dirname, "../../../target/debug/runa-cli");
|
||||||
|
const terminal = spawn("script", ["-q", "-e", "-c", '"$RUNA_CLI_TEST" repl', "/dev/null"], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TERM: "xterm",
|
||||||
|
RUNA_CONNECT: image.address,
|
||||||
|
RUNA_CLI_TEST: executable,
|
||||||
|
},
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
t.after(() => {
|
||||||
|
if (terminal.exitCode === null && terminal.signalCode === null) terminal.kill();
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let interrupted = false;
|
||||||
|
terminal.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
if (!interrupted && stdout.includes("runa> ")) {
|
||||||
|
interrupted = true;
|
||||||
|
terminal.stdin.write("\x03");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
terminal.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
});
|
||||||
|
const [code] = await once(terminal, "exit");
|
||||||
|
assert.ok(interrupted, stdout + stderr);
|
||||||
|
assert.equal(code, 0, stdout + stderr);
|
||||||
|
assert.equal(stderr, "");
|
||||||
|
assert.match(stdout, /\^C/);
|
||||||
|
assert.doesNotMatch(stdout, /protocol error|reset without closing handshake/i);
|
||||||
|
assert.doesNotMatch(image.diagnostics(), /protocol error|reset without closing handshake/i);
|
||||||
|
const inspector = await ImageClient.connect(image.address, (error) => {
|
||||||
|
assert.equal(error, undefined);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
assert.deepEqual(await inspector.listActors(), []);
|
||||||
|
} finally {
|
||||||
|
inspector.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"one-shot CLI commands share the image, report errors, and close cleanly",
|
"one-shot CLI commands share the image, report errors, and close cleanly",
|
||||||
{ timeout: 15000 },
|
{ timeout: 15000 },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue