mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-08 00:50:06 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5640383522 | ||
|
|
af57c46bcc | ||
|
|
c03ab890b2 | ||
|
|
7739f8b2c7 | ||
|
|
ac87716436 | ||
|
|
11e18b3d1e |
No files matched your search
@@ -73,6 +73,11 @@ function normalizePassword(password) {
|
||||
return typeof password === "string" ? password : "";
|
||||
}
|
||||
|
||||
// Bare CR/LF used to wake a silent serial console before "login:" appears.
|
||||
function isAutoLoginWakeInput(data) {
|
||||
return typeof data === "string" && /^[\r\n]+$/.test(data);
|
||||
}
|
||||
|
||||
function createTelnetAutoLogin(options = {}) {
|
||||
const username = normalizeUsername(options.username);
|
||||
const password = normalizePassword(options.password);
|
||||
@@ -207,7 +212,26 @@ function createTelnetAutoLogin(options = {}) {
|
||||
notifyIncomplete();
|
||||
}
|
||||
},
|
||||
handleUserInput() {
|
||||
// Silent serial consoles need a bare CR/LF to wake getty before "login:"
|
||||
// appears. While the detector is still active that wake is not a
|
||||
// takeover: keep autofill armed, leave the caller's write on the port,
|
||||
// and do not consume the one-shot continue-prompt response. A later
|
||||
// "Press RETURN to continue" must still be answered. Once the detector
|
||||
// is disabled (expiry, completion, or a prior cancel), the same Enter
|
||||
// notifies like any other user input so callers can drop a deferred
|
||||
// startup command. Any other user input still cancels.
|
||||
handleUserInput(data) {
|
||||
const wakeOnly = isAutoLoginWakeInput(data);
|
||||
if (
|
||||
wakeOnly
|
||||
&& !disabled
|
||||
&& !sentUsername
|
||||
&& !sentPassword
|
||||
&& !isUsernamePrompt(tail)
|
||||
&& !isPasswordPrompt(tail)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
disabled = true;
|
||||
clearExpiryTimer();
|
||||
notifyUserInput();
|
||||
@@ -224,6 +248,7 @@ function createTelnetAutoLogin(options = {}) {
|
||||
|
||||
module.exports = {
|
||||
createTelnetAutoLogin,
|
||||
isAutoLoginWakeInput,
|
||||
isCommandPrompt,
|
||||
isContinuePrompt,
|
||||
isPasswordPrompt,
|
||||
|
||||
@@ -224,6 +224,155 @@ test("telnet auto-login stops when the user starts typing manually", () => {
|
||||
assert.equal(cancelCount, 1);
|
||||
});
|
||||
|
||||
test("telnet auto-login stays armed when a wake-only enter arrives before any login prompt", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// Silent serial consoles need a bare CR to wake getty before "login:" appears.
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login treats a bare newline as a wake and still autofills", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleUserInput("\n");
|
||||
autoLogin.handleUserInput("\r\n");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login notifies when a wake enter arrives after expiry", () => {
|
||||
let cancelCount = 0;
|
||||
let timerFn = null;
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
timeoutMs: 60_000,
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
setTimeout: (fn) => {
|
||||
timerFn = fn;
|
||||
return { unref() {} };
|
||||
},
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
|
||||
// Quiet device: the 60s window elapses with no prompt, which disables the
|
||||
// detector without a user-input notification. Serial startup still has its
|
||||
// 65s fallback armed until this cancellation arrives.
|
||||
timerFn();
|
||||
assert.equal(cancelCount, 0);
|
||||
autoLogin.handleUserInput("\r");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
autoLogin.handleText("Press RETURN to continue");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login still answers a continue prompt after a blind wake enter", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// The user's Enter woke a silent console. It must not consume the
|
||||
// detector's one-shot response when the device then asks to continue.
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("Press RETURN to continue");
|
||||
autoLogin.handleText("\r\nlogin: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 0);
|
||||
assert.deepEqual(writes, ["\r", "admin\r", "secret\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels when a normal character arrives before login", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleUserInput("x");
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels an enter after credentials have started", () => {
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleUserInput("\r");
|
||||
autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
});
|
||||
|
||||
test("telnet auto-login still cancels an enter when a login prompt is already in the tail", () => {
|
||||
let clock = 0;
|
||||
const writes = [];
|
||||
let cancelCount = 0;
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => writes.push(data),
|
||||
now: () => clock,
|
||||
timeoutMs: 60_000,
|
||||
onUserInput: () => { cancelCount += 1; },
|
||||
});
|
||||
|
||||
// After the window, the prompt is recorded but credentials are not sent.
|
||||
clock = 60_001;
|
||||
autoLogin.handleText("login: ");
|
||||
autoLogin.handleUserInput("\r");
|
||||
|
||||
assert.equal(cancelCount, 1);
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
test("telnet auto-login avoids common non-prompt login text", () => {
|
||||
const writes = [];
|
||||
const autoLogin = createTelnetAutoLogin({
|
||||
|
||||
@@ -63,7 +63,7 @@ const { discoverShells } = require("./shellDiscovery.cjs");
|
||||
const { isWindowsAppExecutionAliasPath } = require("../../lib/localShell.cjs");
|
||||
const moshHandshake = require("./moshHandshake.cjs");
|
||||
const tempDirBridge = require("./tempDirBridge.cjs");
|
||||
const { createTelnetAutoLogin } = require("./telnetAutoLogin.cjs");
|
||||
const { createTelnetAutoLogin, isAutoLoginWakeInput } = require("./telnetAutoLogin.cjs");
|
||||
const telnetProtocol = require("./telnetProtocol.cjs");
|
||||
const { createPtyOutputBuffer } = require("./ptyOutputBuffer.cjs");
|
||||
const { enableTcpNoDelay } = require("./tcpNoDelay.cjs");
|
||||
@@ -1713,8 +1713,9 @@ function writeToSessionNow(payload, data, logRewrite = payload.logRewrite) {
|
||||
(session.type === 'telnet-native' || session.type === 'serial')
|
||||
&& !payload.automated
|
||||
&& !isTerminalReportSequence(data)
|
||||
&& !payload.autoLoginInputHandled
|
||||
) {
|
||||
session.autoLogin?.handleUserInput();
|
||||
session.autoLogin?.handleUserInput(data);
|
||||
}
|
||||
|
||||
// Encode keystrokes with the SAME charset the output path decodes with so
|
||||
@@ -1805,6 +1806,8 @@ function writeToSessionWithInterception(
|
||||
paste = null,
|
||||
index = 0,
|
||||
) {
|
||||
// Stays the original payload until ingress classification marks this write.
|
||||
let sessionWritePayload = payload;
|
||||
const writeWithReceipt = (nextData) => {
|
||||
if (paste && !paste.active) return;
|
||||
const current = sessions.get(payload.sessionId);
|
||||
@@ -1817,7 +1820,7 @@ function writeToSessionWithInterception(
|
||||
paste.finish(index, true);
|
||||
return;
|
||||
}
|
||||
const written = writeToSessionNow(payload, nextData, logRewrite);
|
||||
const written = writeToSessionNow(sessionWritePayload, nextData, logRewrite);
|
||||
if (paste) paste.finish(written ? index : undefined);
|
||||
};
|
||||
const bypass = payload?.sensitive === true || isTerminalReportSequence(data);
|
||||
@@ -1830,20 +1833,24 @@ function writeToSessionWithInterception(
|
||||
writeWithReceipt(data);
|
||||
return;
|
||||
}
|
||||
// Cancel the auto-login detector at input ingress, before the asynchronous
|
||||
// Classify auto-login at input ingress, before the asynchronous
|
||||
// interception pipeline: a slow interceptor would otherwise leave the
|
||||
// detector armed while the user's keystrokes are queued, letting a login
|
||||
// prompt that arrives during that wait trigger a saved-credential
|
||||
// transmission after the user has already taken over. Same guard
|
||||
// conditions as writeToSessionNow; handleUserInput is idempotent, so the
|
||||
// later call there stays a no-op.
|
||||
// conditions as writeToSessionNow. Wake-only CR/LF is not idempotent once
|
||||
// credentials start, so mark this write and do not classify those bytes
|
||||
// again when they reach writeToSessionNow. That preserved classification
|
||||
// describes the queued bytes only while they stay wake-only; writeIfCurrent
|
||||
// classifies an interceptor rewrite that is no longer wake-only.
|
||||
if (
|
||||
expectedSession
|
||||
&& (expectedSession.type === 'telnet-native' || expectedSession.type === 'serial')
|
||||
&& !payload.automated
|
||||
&& !isTerminalReportSequence(data)
|
||||
) {
|
||||
expectedSession.autoLogin?.handleUserInput();
|
||||
expectedSession.autoLogin?.handleUserInput(data);
|
||||
sessionWritePayload = { ...payload, autoLoginInputHandled: true };
|
||||
}
|
||||
const writeIfCurrent = (nextData) => {
|
||||
const current = sessions.get(payload.sessionId);
|
||||
@@ -1851,6 +1858,19 @@ function writeToSessionWithInterception(
|
||||
paste?.finish();
|
||||
return;
|
||||
}
|
||||
// An Enter-to-command macro can turn the wake CR/LF classified above
|
||||
// into ordinary text. Classify that transformed value before it is
|
||||
// written. autoLoginInputHandled stays set so writeToSessionNow does not
|
||||
// classify the same write again. A rewrite that is still only CR/LF keeps
|
||||
// the ingress classification: those bytes are not idempotent once
|
||||
// credentials have started.
|
||||
if (
|
||||
sessionWritePayload.autoLoginInputHandled
|
||||
&& !isAutoLoginWakeInput(nextData)
|
||||
&& !isTerminalReportSequence(nextData)
|
||||
) {
|
||||
current.autoLogin?.handleUserInput(nextData);
|
||||
}
|
||||
writeWithReceipt(nextData);
|
||||
};
|
||||
const write = async () => {
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const terminalBridge = require("./terminalBridge.cjs");
|
||||
const { createTelnetAutoLogin } = require("./telnetAutoLogin.cjs");
|
||||
|
||||
function createSerialAutoLoginSession() {
|
||||
const writes = [];
|
||||
const events = [];
|
||||
const session = {
|
||||
type: "serial",
|
||||
protocol: "serial",
|
||||
encoding: "utf-8",
|
||||
serialPort: {
|
||||
write: (data) => {
|
||||
writes.push(typeof data === "string" ? data : Buffer.from(data).toString());
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
session.autoLogin = createTelnetAutoLogin({
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
write: (data) => {
|
||||
session.serialPort.write(data);
|
||||
},
|
||||
onUserInput: () => { events.push("cancelled"); },
|
||||
});
|
||||
return { session, writes, events };
|
||||
}
|
||||
|
||||
test("serial auto-login cancels on interactive user input but not automated writes", () => {
|
||||
let userInputs = 0;
|
||||
@@ -61,6 +87,221 @@ test("serial line-mode buffered input notification cancels auto-login", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake-only enter keeps auto-login armed and still reaches the port", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\r"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\r", "admin\r", "secret\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial character input before login still cancels auto-login", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "x" });
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["x"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake-only enter stays armed when an input interceptor is pending", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorResolved = false;
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (sessionId, data) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
interceptorResolved = true;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\n" });
|
||||
assert.deepEqual(events, []);
|
||||
assert.equal(interceptorResolved, false);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 40));
|
||||
assert.equal(interceptorResolved, true);
|
||||
assert.deepEqual(writes, ["\n"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["\n", "admin\r", "secret\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial wake enter is not cancelled when login arrives during interceptor delay", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorStarted = false;
|
||||
let releaseInterceptor = () => {};
|
||||
const interceptorGate = new Promise((resolve) => {
|
||||
releaseInterceptor = resolve;
|
||||
});
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
interceptorStarted = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
await interceptorGate;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
assert.equal(interceptorStarted, true);
|
||||
|
||||
// Prompt arrives while the wake byte is still queued in the interceptor.
|
||||
session.autoLogin.handleText("login: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
|
||||
releaseInterceptor();
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 2 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r"]);
|
||||
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r", "secret\r"]);
|
||||
} finally {
|
||||
releaseInterceptor();
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial interceptor that rewrites a wake enter into text cancels auto-login", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let interceptorStarted = false;
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
interceptorStarted = true;
|
||||
assert.equal(data, "\r");
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
// Enter-to-command macro: the bare wake CR is no longer what is written.
|
||||
return "uptime\r";
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
assert.equal(interceptorStarted, true);
|
||||
// Ingress still sees a wake, so autofill stays armed until the rewrite lands.
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, []);
|
||||
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 1 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["uptime\r"]);
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, ["uptime\r"]);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial interceptor that rewrites a wake enter into another wake still autofills", async () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
let releaseInterceptor = () => {};
|
||||
const interceptorGate = new Promise((resolve) => {
|
||||
releaseInterceptor = resolve;
|
||||
});
|
||||
terminalBridge.init({
|
||||
sessions: new Map([["s", session]]),
|
||||
electronModule: {},
|
||||
terminalDataPipeline: {
|
||||
has: () => true,
|
||||
interceptInput: async (_sessionId, data) => {
|
||||
assert.equal(data, "\r");
|
||||
await interceptorGate;
|
||||
return "\r\n";
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
terminalBridge.writeToSession({}, { sessionId: "s", data: "\r" });
|
||||
await Promise.resolve();
|
||||
|
||||
session.autoLogin.handleText("login: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r"]);
|
||||
|
||||
releaseInterceptor();
|
||||
const deadline = Date.now() + 500;
|
||||
while (writes.length < 2 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r\n"]);
|
||||
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
assert.deepEqual(events, []);
|
||||
assert.deepEqual(writes, ["admin\r", "\r\n", "secret\r"]);
|
||||
} finally {
|
||||
releaseInterceptor();
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial line-mode notification still cancels auto-login without keystroke data", () => {
|
||||
const { session, writes, events } = createSerialAutoLoginSession();
|
||||
terminalBridge.init({ sessions: new Map([["s", session]]), electronModule: {} });
|
||||
try {
|
||||
terminalBridge.notifySessionUserInput({}, { sessionId: "s" });
|
||||
session.autoLogin.handleText("login: ");
|
||||
session.autoLogin.handleText("\r\nPassword: ");
|
||||
|
||||
assert.deepEqual(events, ["cancelled"]);
|
||||
assert.deepEqual(writes, []);
|
||||
} finally {
|
||||
terminalBridge.cleanupAllSessions();
|
||||
}
|
||||
});
|
||||
|
||||
test("serial sessions without auto-login credentials accept input normally", () => {
|
||||
const writes = [];
|
||||
const session = {
|
||||
|
||||
Reference in new issue
Block a user