mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-11 00:30:57 +00:00
fix(#3384): automated AI fix
This commit is contained in:
1 parent
b46910d46e
commit
990bb791a7
7 files changed
+111
-2
No files matched your search
@@ -64,6 +64,7 @@ function startPtyJob(ptyStream, command, options) {
|
||||
expectedPrompt,
|
||||
typedInput = false,
|
||||
echoCommand,
|
||||
onEchoSuppressionPrime,
|
||||
maxBufferedChars = 0,
|
||||
normalizeFinalOutput = true,
|
||||
enforceWallTimeout = false,
|
||||
@@ -812,6 +813,21 @@ function startPtyJob(ptyStream, command, options) {
|
||||
writeInput(`${buildPendingInputClearPrefix(resolvedShellKind)}${wrapped}`);
|
||||
}
|
||||
|
||||
// Prime the renderer's display suppression before the first byte is typed
|
||||
// (issue #3384). Shells whose line editor echoes input, such as BusyBox ash
|
||||
// on OpenWrt, break long echoed lines at the terminal width with CR/LF; the
|
||||
// wrapped fragments no longer contain the marker and would leak through the
|
||||
// per-line echo filter as visible "variables" until the wrapper's own
|
||||
// _I printf runs. Delivering the _I line over the data channel up front
|
||||
// suppresses the whole echo; the _S output releases it, and finish() sends
|
||||
// the _R reset (onProbeAborted) when the command never starts.
|
||||
if (typeof onEchoSuppressionPrime === "function") {
|
||||
try {
|
||||
onEchoSuppressionPrime(marker);
|
||||
} catch {
|
||||
// Display suppression must never prevent the command from starting.
|
||||
}
|
||||
}
|
||||
if (probingShell) {
|
||||
writeInput(`${buildPendingInputClearPrefix(resolvedShellKind)}${buildLiveShellProbe(marker)}`);
|
||||
} else {
|
||||
|
||||
@@ -1420,3 +1420,44 @@ test("startPtyJob types the wrapper one code point per write for strict bastions
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error, "Cancelled");
|
||||
});
|
||||
|
||||
test("echo suppression prime is delivered before the first typed input (#3384)", async () => {
|
||||
for (const probeLiveShell of [false, true]) {
|
||||
const pty = new EventEmitter();
|
||||
const events = [];
|
||||
pty.write = (data) => {
|
||||
if (data === "\x03") return;
|
||||
events.push(`write:${data}`);
|
||||
};
|
||||
const job = startPtyJob(pty, "echo prime-order", {
|
||||
shellKind: "posix",
|
||||
probeLiveShell,
|
||||
timeoutMs: 1000,
|
||||
onEchoSuppressionPrime: (marker) => events.push(`prime:${marker}`),
|
||||
});
|
||||
assert.equal(events[0], `prime:${job.marker}`, "prime precedes every typed byte");
|
||||
assert.ok((events[1] || "").startsWith("write:"), "first write follows the prime");
|
||||
assert.equal(events.filter((event) => event.startsWith("prime:")).length, 1);
|
||||
if (probeLiveShell) {
|
||||
pty.emit("data", `${job.marker}_P:sh\n${job.marker}_Q`);
|
||||
}
|
||||
pty.emit("data", `${job.marker}_S\ndone\n${job.marker}_E:0\n`);
|
||||
const result = await job.resultPromise;
|
||||
assert.equal(result.ok, true, JSON.stringify(result));
|
||||
}
|
||||
});
|
||||
|
||||
test("a throwing echo suppression prime callback still types the command (#3384)", () => {
|
||||
const writes = [];
|
||||
const pty = new EventEmitter();
|
||||
pty.write = (data) => writes.push(String(data));
|
||||
const job = startPtyJob(pty, "echo resilient", {
|
||||
shellKind: "posix",
|
||||
timeoutMs: 1000,
|
||||
onEchoSuppressionPrime: () => {
|
||||
throw new Error("Renderer closed");
|
||||
},
|
||||
});
|
||||
assert.ok(writes.some((data) => data.includes("echo resilient")));
|
||||
job.cancel();
|
||||
});
|
||||
@@ -182,6 +182,10 @@ function registerCattyExecHandlers(ctx) {
|
||||
const contents = electronModule?.webContents?.fromId?.(session.webContentsId);
|
||||
safeSend(contents, "netcatty:data", { sessionId, data: `${marker}_R\n` });
|
||||
},
|
||||
onEchoSuppressionPrime: (marker) => {
|
||||
const contents = electronModule?.webContents?.fromId?.(session.webContentsId);
|
||||
safeSend(contents, "netcatty:data", { sessionId, data: `${marker}_I\n` });
|
||||
},
|
||||
chatSessionId,
|
||||
expectedPrompt: getFreshIdlePrompt(session),
|
||||
typedInput: true,
|
||||
|
||||
@@ -149,6 +149,7 @@ function createExecHandlerApi(ctx) {
|
||||
probeLiveShell: true,
|
||||
bastionKeystrokes: remoteDisallowsExecChannelProbe(session.remoteSshVersion),
|
||||
onProbeAborted: (marker) => echoCommandToSession(session, sessionId, `${marker}_R`, { syntheticEcho: false }),
|
||||
onEchoSuppressionPrime: (marker) => echoCommandToSession(session, sessionId, `${marker}_I`, { syntheticEcho: false }),
|
||||
expectedPrompt: getFreshIdlePrompt(session),
|
||||
typedInput: true,
|
||||
echoCommand: (rawCommand) => echoCommandToSession(session, sessionId, rawCommand),
|
||||
@@ -319,6 +320,7 @@ function createExecHandlerApi(ctx) {
|
||||
probeLiveShell: true,
|
||||
bastionKeystrokes: remoteDisallowsExecChannelProbe(session.remoteSshVersion),
|
||||
onProbeAborted: (marker) => echoCommandToSession(session, sessionId, `${marker}_R`, { syntheticEcho: false }),
|
||||
onEchoSuppressionPrime: (marker) => echoCommandToSession(session, sessionId, `${marker}_I`, { syntheticEcho: false }),
|
||||
chatSessionId,
|
||||
expectedPrompt: getFreshIdlePrompt(session),
|
||||
typedInput: true,
|
||||
|
||||
@@ -1802,6 +1802,34 @@ test("OpenWrt bounded wrapper continuations stay hidden across fragmented echoes
|
||||
}
|
||||
});
|
||||
|
||||
test("primed suppression hides BusyBox ash echo wrapped mid-marker (#3384)", () => {
|
||||
const preload = loadPreloadWithFakeElectron();
|
||||
try {
|
||||
const received = [];
|
||||
const sessionId = 'ash-wrap';
|
||||
const marker = '__NCMCP_mttikd5b_ccbc892e865a115a80c88afdc77b96a6__';
|
||||
preload.api.onSessionData(sessionId, chunk => received.push(chunk));
|
||||
// The exec bridge primes display suppression over the data channel before
|
||||
// the wrapper is typed, mirroring onEchoSuppressionPrime in ptyExec.cjs.
|
||||
preload.handlers.get('netcatty:data')({}, { sessionId, data: `${marker}_I\n` });
|
||||
// BusyBox ash's line editor breaks the echoed first wrapper line at the
|
||||
// terminal width, so the second PTY line carries no complete __NCMCP_
|
||||
// marker and the per-line echo filter alone cannot drop it.
|
||||
const firstLine = ` ${marker}=0; printf '\\n%s\\n' '${marker}_I'`;
|
||||
const secondLine = ` : '${marker}'; ${marker}_cmd='echo visible-output'; \\`;
|
||||
const echo = `${firstLine.slice(0, 55)}\r\n${firstLine.slice(55)}\r\n`
|
||||
+ `${secondLine.slice(0, 70)}\r\n${secondLine.slice(70)}\r\n`
|
||||
+ `> : '${marker}'; printf '%s\\n' '${marker}_S'\r\n`;
|
||||
const data = `${echo}\n${marker}_S\r\nvisible-output\r\n${marker}_E:0\r\n`;
|
||||
for (let offset = 0; offset < data.length; offset += 7) {
|
||||
preload.handlers.get('netcatty:data')({}, { sessionId, data: data.slice(offset, offset + 7) });
|
||||
}
|
||||
assert.equal(received.join(''), 'visible-output\r\n');
|
||||
} finally {
|
||||
preload.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test("ordinary text resembling an OpenWrt continuation is released", async () => {
|
||||
const preload = loadPreloadWithFakeElectron();
|
||||
|
||||
@@ -298,6 +298,12 @@ function createWorkerAiExecHandler({
|
||||
data: `${marker}_R\n`,
|
||||
});
|
||||
},
|
||||
onEchoSuppressionPrime: (marker) => {
|
||||
event?.sender?.send?.("netcatty:data", {
|
||||
sessionId,
|
||||
data: `${marker}_I\n`,
|
||||
});
|
||||
},
|
||||
chatSessionId,
|
||||
expectedPrompt: getFreshIdlePrompt(session),
|
||||
typedInput: true,
|
||||
@@ -502,6 +508,12 @@ function createWorkerAiJobStartHandler({
|
||||
data: `${marker}_R\n`,
|
||||
});
|
||||
},
|
||||
onEchoSuppressionPrime: (marker) => {
|
||||
event?.sender?.send?.("netcatty:data", {
|
||||
sessionId,
|
||||
data: `${marker}_I\n`,
|
||||
});
|
||||
},
|
||||
chatSessionId,
|
||||
expectedPrompt: getFreshIdlePrompt(session),
|
||||
typedInput: true,
|
||||
|
||||
@@ -145,6 +145,7 @@ test("worker AI background jobs start, poll, stop, and block overlapping exec",
|
||||
assert.equal(started.command, "npm test");
|
||||
assert.equal(started.status, "running");
|
||||
assert.equal(started.outputMode, "foreground-mirrored");
|
||||
const marker = await extractMarker(pty.writes);
|
||||
assert.deepEqual(event.rendererMessages, [
|
||||
{
|
||||
channel: "netcatty:data",
|
||||
@@ -154,9 +155,14 @@ test("worker AI background jobs start, poll, stop, and block overlapping exec",
|
||||
syntheticEcho: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
channel: "netcatty:data",
|
||||
payload: {
|
||||
sessionId: "ssh-1",
|
||||
data: `${marker}_I\n`,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const marker = await extractMarker(pty.writes);
|
||||
pty.emit("data", `${marker}_S\r\nready\r\n`);
|
||||
await nextTick();
|
||||
|
||||
|
||||
Reference in new issue
Block a user