mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-08 00:50:06 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cbcb73b61 | ||
|
|
d3eb3007bb |
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user