Compare commits

...
Author SHA1 Message Date
bincxz 1192d4210b fix(cli): release consumed second-instance argument copies 2026-08-31 12:31:12 +08:00
2 changed files with 92 additions and 7 deletions

No files matched your search

+8 -7
View File
@@ -1205,12 +1205,13 @@ if (!gotLock) {
const deepLinkQueueItems = collectSshDeepLinkQueueItems(secondInstanceArgv, {
includeSchemeUrls: sshDeepLinkEnabled,
});
if (jmsDeepLinkUrls.length > 0 || deepLinkQueueItems.ssh.length > 0 || deepLinkQueueItems.telnet.length > 0) {
redactPuttyCommandLinePasswords(secondInstanceArgv);
// The regrouped event argv still holds the raw -pw value; scrub it too.
if (secondInstanceArgv !== argv) {
redactPuttyCommandLinePasswords(argv);
}
redactPuttyCommandLinePasswords(secondInstanceArgv);
if (rawLaunchArgv) {
// Parsing and subsequent routing use the independent ordered copy.
// Release both consumed transport buffers: Chromium may have moved the
// password away from -pw, so adjacency-based redaction is unsafe here.
rawLaunchArgv.length = 0;
argv.length = 0;
}
if (jmsDeepLinkUrls.length > 0) {
if (jmsDeepLinkEnabled) {
@@ -1238,7 +1239,7 @@ if (!gotLock) {
} else {
// Still bring the app forward when Explorer launched us but the path
// failed validation — silent no-op feels like a broken menu item.
console.warn("[Main] Open-terminal-path args present but no valid path resolved:", argv);
console.warn("[Main] Open-terminal-path args present but no valid path resolved:", secondInstanceArgv);
if (!focusMainWindow()) {
void createAndShowMainWindow().catch((err) => {
console.error("[Main] Failed to recreate window on open-terminal-path:", err);
@@ -0,0 +1,84 @@
const assert = require("node:assert/strict");
const { readFileSync } = require("node:fs");
const test = require("node:test");
const vm = require("node:vm");
const deepLink = require("./deepLink.cjs");
const { collectOpenTerminalPathArgs } = require("./openTerminalPath.cjs");
function createHarness() {
const source = readFileSync(require.resolve("./main.cjs"), "utf8");
const start = source.indexOf('app.on("second-instance"');
const end = source.indexOf("// Application lifecycle", start);
assert.ok(start >= 0 && end > start);
let handle;
const queued = [];
const paths = [];
let focusCount = 0;
const context = vm.createContext({
...deepLink,
app: { on: (_event, handler) => { handle = handler; } },
sshDeepLinkEnabled: false,
jmsDeepLinkEnabled: false,
queueSshDeepLink: (url) => queued.push(url),
queueTelnetDeepLink: (url) => queued.push(url),
queueJmsDeepLink: (url) => queued.push(url),
collectOpenTerminalPathArgs,
resolveOpenTerminalPathsFromArgs: (argv, { baseDirectory }) => {
paths.push({ argv: Array.from(argv), baseDirectory });
return ["/resolved"];
},
queueResolvedOpenTerminalPaths: (resolved) => paths.push(Array.from(resolved)),
focusMainWindow: () => { focusCount += 1; return true; },
});
vm.runInContext(source.slice(start, end), context);
return { handle, queued, paths, focused: () => focusCount };
}
for (const password of ["secret", "space :/@ password", "-flag-like-password"]) {
test(`second-instance transport copies are released without changing the SSH password: ${password}`, () => {
const h = createHarness();
const eventArgv = ["netcatty", "-ssh", "-P", "-pw", "alice@localhost", "2222", password];
const additionalData = { rawLaunchArgv: ["-ssh", "alice@localhost", "-P", "2222", "-pw", password] };
h.handle(null, eventArgv, "/working-directory", additionalData);
assert.equal(h.queued.length, 1);
const url = new URL(h.queued[0]);
assert.equal(decodeURIComponent(url.password), password);
assert.equal(url.hostname, "localhost");
assert.equal(url.port, "2222");
assert.equal(eventArgv.length, 0, "discard the consumed, potentially reordered event arguments");
assert.equal(additionalData.rawLaunchArgv.length, 0, "discard the consumed forwarding copy");
});
}
test("failed command-line parsing still clears temporary transport copies", () => {
const h = createHarness();
const eventArgv = ["netcatty", "-raw", "-pw", "localhost", "secret"];
const additionalData = { rawLaunchArgv: ["-raw", "localhost", "-pw", "secret"] };
h.handle(null, eventArgv, "/working-directory", additionalData);
assert.deepEqual(h.queued, []);
assert.equal(h.focused(), 1);
assert.equal(eventArgv.length, 0);
assert.equal(additionalData.rawLaunchArgv.length, 0);
});
test("legacy second instances without additional data retain ordered-argv support", () => {
const h = createHarness();
const eventArgv = ["netcatty", "-ssh", "alice@localhost", "-pw", "secret"];
h.handle(null, eventArgv, "/working-directory");
assert.equal(new URL(h.queued[0]).password, "secret");
assert.equal(eventArgv.includes("secret"), false);
});
test("Explorer launch routing uses the ordered copy after transport cleanup", () => {
const h = createHarness();
const ordered = ["--open-terminal-path", "relative-folder"];
const eventArgv = ["netcatty", ...ordered];
const additionalData = { rawLaunchArgv: [...ordered] };
h.handle(null, eventArgv, "/working-directory", additionalData);
assert.deepEqual(h.paths, [
{ argv: ["netcatty", ...ordered], baseDirectory: "/working-directory" },
["/resolved"],
]);
assert.equal(eventArgv.length, 0);
assert.equal(additionalData.rawLaunchArgv.length, 0);
});