Compare commits

..
5 changed files with 51 additions and 71 deletions

No files matched your search

@@ -698,51 +698,3 @@ test('connection log host snapshot includes custom host icon fields', () => {
},
);
});
for (const scenario of [
{ name: 'single orphan', active: 'one', sessions: [{ id: 'one' }], available: false, expected: null },
{ name: 'two orphans', active: 'one', sessions: [{ id: 'one' }, { id: 'two' }], available: true, expected: 'global' },
{ name: 'hidden active session', active: 'hidden', sessions: [{ id: 'one' }, { id: 'two' }, { id: 'hidden', hiddenFromTabs: true }], available: true, expected: null },
{ name: 'non-terminal tab', active: 'vault', sessions: [{ id: 'one' }, { id: 'two' }], available: true, expected: null },
{ name: 'workspace', active: 'workspace', sessions: [{ id: 'one', workspaceId: 'workspace' }, { id: 'two', workspaceId: 'workspace' }], available: false, expected: 'workspace' },
]) {
for (const scheme of ['pc', 'mac'] as const) {
test(`broadcast shortcut ${scheme}: ${scenario.name} only consumes an executed toggle`, () => {
const target = new FakeHTMLElement();
let prevented = 0;
let stopped = 0;
let globalEnabled = false;
let workspaceEnabled = false;
const toggles: string[] = [];
const event = {
key: 'b', code: 'KeyB', ctrlKey: scheme === 'pc', metaKey: scheme === 'mac',
altKey: false, shiftKey: false, target, composedPath: () => [target],
preventDefault: () => { prevented += 1; },
stopPropagation: () => { stopped += 1; },
} as unknown as KeyboardEvent;
const actionCtx = {
activeTabStore: { getActiveTabId: () => scenario.active },
editorTabs: [], orderedTabs: [], settings: {}, toEditorTabId: (id: string) => id,
sessions: scenario.sessions,
workspaces: scenario.active === 'workspace' ? [{ id: 'workspace' }] : [],
canUseGlobalBroadcast: scenario.available,
toggleGlobalBroadcast: () => { globalEnabled = !globalEnabled; toggles.push('global'); },
toggleBroadcast: (id: string) => { assert.equal(id, 'workspace'); workspaceEnabled = !workspaceEnabled; toggles.push('workspace'); },
};
const invoke = () => handleGlobalHotkeyKeyDownImpl(() => ({
HOTKEY_DEBUG: false, closeTabKeyStr: '', hotkeyScheme: scheme,
keyBindings: DEFAULT_KEY_BINDINGS, matchesKeyBinding,
executeHotkeyAction: (action: string, e: KeyboardEvent) => executeHotkeyActionImpl(() => actionCtx, action, e),
}), event);
invoke();
assert.equal(globalEnabled, scenario.expected === 'global');
assert.equal(workspaceEnabled, scenario.expected === 'workspace');
invoke();
assert.equal(globalEnabled, false);
assert.equal(workspaceEnabled, false);
assert.deepEqual(toggles, scenario.expected ? [scenario.expected, scenario.expected] : []);
assert.equal(prevented, scenario.expected ? 2 : 0);
assert.equal(stopped, scenario.expected ? 2 : 0);
});
}
}
+2 -14
View File
@@ -313,16 +313,6 @@ export function handleGlobalHotkeyKeyDownImpl(getCtx: AppContextGetter, e: Keybo
continue;
}
// An unavailable broadcast action must leave the chord to the terminal
// (notably tmux's Ctrl+B prefix). Dispatch once before consuming it.
if (binding.action === 'broadcast') {
if (executeHotkeyAction(binding.action, e) === true) {
e.preventDefault();
e.stopPropagation();
}
return;
}
e.preventDefault();
e.stopPropagation();
if (HOTKEY_DEBUG) {
@@ -973,14 +963,12 @@ export function executeHotkeyActionImpl(getCtx: AppContextGetter, action: string
const activeWs = workspaces.find(w => w.id === currentId);
if (activeWs) {
toggleBroadcast(activeWs.id);
return true;
} else if (sessions.some((session: TerminalSession) => (
session.id === currentId && !session.workspaceId && !session.hiddenFromTabs
session.id === currentId && !session.workspaceId
)) && canUseGlobalBroadcast) {
toggleGlobalBroadcast();
return true;
}
return false;
break;
}
case 'openSettings':
handleOpenSettingsRef.current();
-7
View File
@@ -252,8 +252,6 @@ export function AppSideEffects() {
runSnippet,
getOrderedWorkTabs,
toggleBroadcast,
toggleGlobalBroadcast,
canUseGlobalBroadcast,
logViews,
closeLogView,
copySession,
@@ -908,8 +906,6 @@ export function AppSideEffects() {
// array thrash does not rebuild appTerminalDomain (see domain isolation).
const sessionsRef = useRef(sessions);
sessionsRef.current = sessions;
const canUseGlobalBroadcastRef = useRef(canUseGlobalBroadcast);
canUseGlobalBroadcastRef.current = canUseGlobalBroadcast;
// Logs live in connectionLogsStore — keep a ref fresh without re-rendering App.
const connectionLogsRef = useRef(getConnectionLogsSnapshot().connectionLogs);
useEffect(() => {
@@ -1080,8 +1076,6 @@ export function AppSideEffects() {
terminalPaneMagnificationRef,
toEditorTabId,
toggleBroadcast,
toggleGlobalBroadcast,
canUseGlobalBroadcast: canUseGlobalBroadcastRef.current,
toggleScriptsSidePanelRef,
toggleSidePanelRef,
toggleWorkspaceViewMode,
@@ -1096,7 +1090,6 @@ export function AppSideEffects() {
splitSessionWithCurrentShell,
moveFocusInWorkspace,
toggleBroadcast,
toggleGlobalBroadcast,
toggleWorkspaceViewMode,
confirmIfBusyLocalTerminal,
]);
+7 -1
View File
@@ -635,7 +635,13 @@ function createSessionOpsApi(ctx) {
_rc_cwd=$(readlink "/proc/$1/cwd" 2>/dev/null)
if [ -n "$_rc_cwd" ]; then printf '%s\\n' "$_rc_cwd"; return 0; fi
if command -v lsof >/dev/null 2>&1; then
_rc_cwd=$(LC_ALL=C lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n1)
# An unknown-type record can put a readlink error in its name field.
# Only a confirmed directory name is usable as an upload destination.
_rc_cwd=$(LC_ALL=C lsof -a -p "$1" -d cwd -Fnt 2>/dev/null | awk '
/^f/ { is_dir=0 }
/^t/ { is_dir=($0 == "tDIR" || $0 == "tVDIR") }
/^n/ && is_dir { print substr($0, 2); exit }
')
if [ -n "$_rc_cwd" ]; then printf 'NETCATTY_LSOF_CWD=%s\\n' "$_rc_cwd"; return 0; fi
fi
return 1
@@ -29,7 +29,7 @@ function makeApi(session, siblingSessions = [], overrides = {}) {
sessions,
setTimeout: overrides.setTimeout || setTimeout,
clearTimeout: overrides.clearTimeout || clearTimeout,
quoteShellArg,
quoteShellArg: overrides.quoteShellArg || quoteShellArg,
log: () => {},
});
}
@@ -123,6 +123,47 @@ test("session cwd probe decodes the marked lsof pathname", async () => {
assert.deepEqual(result, { success: true, cwd: "/srv/中文" });
});
test("lsof cwd fallback accepts only directory records, not diagnostic names (#3237)", async () => {
let script;
const api = makeApi({
shellPid: "4242",
connRef: { count: 1 },
stream: {},
conn: {
exec(_command, callback) {
callback(null, makePwdStream("/srv/app", "4242"));
},
},
}, [], {
quoteShellArg(value) {
if (!script && value.includes("read_shell_cwd()")) script = value;
return quoteShellArg(value);
},
});
await api.getSessionPwd(null, { sessionId: "session-1" });
const helper = script.match(/ read_shell_cwd\(\) \{[\s\S]*?\n \}/)?.[0];
assert.ok(helper, "exercise the actual remote cwd helper");
const { spawnSync } = require("node:child_process");
for (const [output, expected] of [
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: Permission denied)\n", null],
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: No such file or directory)\n", null],
["p4242\nfcwd\nn/proc/4242/cwd\n", null],
["p4242\nfcwd\ntDIR\nn/srv/app\n", "/srv/app"],
["p4242\nfcwd\ntVDIR\nn/usr/home/alice\n", "/usr/home/alice"],
["p4242\nfcwd\ntDIR\nn/tmp/literal (readlink: Permission denied)\n", "/tmp/literal (readlink: Permission denied)"],
["p4242\nfcwd\ntDIR\nn/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87\n", "/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87"],
]) {
const result = spawnSync("sh", ["-c", `
readlink() { return 1; }
lsof() { printf '%s' "$LSOF_TEST_OUTPUT"; }
${helper}
read_shell_cwd 4242
`], { encoding: "utf8", env: { ...process.env, LSOF_TEST_OUTPUT: output } });
assert.equal(result.status, expected === null ? 1 : 0, output);
assert.equal(result.stdout, expected === null ? "" : `NETCATTY_LSOF_CWD=${expected}\n`, output);
}
});
test("session cwd probe closes a remote command that exceeds its timeout", async () => {
const stream = new EventEmitter();
stream.stderr = new EventEmitter();