[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
172
components/terminal/sftpCwd.ts
Normal file
172
components/terminal/sftpCwd.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import type { TerminalCwdSource } from "../../application/state/terminalCwdStore";
|
||||
|
||||
type SessionPwdResult = {
|
||||
success: boolean;
|
||||
cwd?: string | null;
|
||||
};
|
||||
|
||||
type SessionPwdOptions = {
|
||||
allowHomeFallback?: boolean;
|
||||
/**
|
||||
* When true, fall back to the same-uid login shell cwd after su/sudo.
|
||||
* When omitted, follows allowHomeFallback (backend default).
|
||||
*/
|
||||
allowLoginShellFallback?: boolean;
|
||||
};
|
||||
|
||||
export type RendererCwdSource = TerminalCwdSource;
|
||||
export type TerminalCwdChangeMeta = { source?: RendererCwdSource };
|
||||
|
||||
type ResolvePreferredTerminalCwdOptions = {
|
||||
rendererCwd?: string | null;
|
||||
rendererCwdSource?: RendererCwdSource;
|
||||
sessionId?: string | null;
|
||||
getSessionPwd: (sessionId: string, options?: SessionPwdOptions) => Promise<SessionPwdResult>;
|
||||
/** When true, always probe the backend instead of trusting renderer cwd. */
|
||||
preferFreshBackend?: boolean;
|
||||
/** When false, a failed backend probe must not return a cached renderer cwd. */
|
||||
allowRendererFallback?: boolean;
|
||||
/** Require the active shell cwd; never substitute the login shell or home directory. */
|
||||
requireActiveShellCwd?: boolean;
|
||||
};
|
||||
|
||||
const normalizeCwd = (cwd?: string | null): string | null => {
|
||||
if (typeof cwd !== "string" || cwd.trim().length === 0) return null;
|
||||
return cwd;
|
||||
};
|
||||
|
||||
export type TerminalCwdTracker = {
|
||||
getRendererCwd: () => string | undefined;
|
||||
getRendererCwdSource: () => RendererCwdSource | undefined;
|
||||
setRendererCwd: (
|
||||
cwd?: string | null,
|
||||
source?: RendererCwdSource,
|
||||
) => string | undefined;
|
||||
markRendererCwdStale: () => void;
|
||||
clearRendererCwd: () => void;
|
||||
};
|
||||
|
||||
export const createTerminalCwdTracker = (): TerminalCwdTracker => {
|
||||
let rendererCwd: string | undefined;
|
||||
let rendererCwdSource: RendererCwdSource | undefined;
|
||||
|
||||
return {
|
||||
getRendererCwd: () => rendererCwd,
|
||||
getRendererCwdSource: () => rendererCwdSource,
|
||||
setRendererCwd: (cwd, source = "unknown") => {
|
||||
rendererCwd = normalizeCwd(cwd) ?? undefined;
|
||||
rendererCwdSource = rendererCwd ? source : undefined;
|
||||
return rendererCwd;
|
||||
},
|
||||
markRendererCwdStale: () => {
|
||||
if (rendererCwd) rendererCwdSource = "stale";
|
||||
},
|
||||
clearRendererCwd: () => {
|
||||
rendererCwd = undefined;
|
||||
rendererCwdSource = undefined;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Invalidate both the terminal-local provenance and the shared SFTP-follow cwd. */
|
||||
export const invalidateTerminalCwdAfterCommand = (
|
||||
tracker: TerminalCwdTracker,
|
||||
sessionId: string,
|
||||
onSnapshotCwdInvalidated: () => void,
|
||||
onTerminalCwdChange?: (sessionId: string, cwd: string | null) => void,
|
||||
): void => {
|
||||
onSnapshotCwdInvalidated();
|
||||
tracker.markRendererCwdStale();
|
||||
onTerminalCwdChange?.(sessionId, null);
|
||||
};
|
||||
|
||||
export const resolvePreferredTerminalCwd = async ({
|
||||
rendererCwd,
|
||||
rendererCwdSource = "unknown",
|
||||
sessionId,
|
||||
getSessionPwd,
|
||||
preferFreshBackend = false,
|
||||
allowRendererFallback = true,
|
||||
requireActiveShellCwd = false,
|
||||
}: ResolvePreferredTerminalCwdOptions): Promise<string | null> => {
|
||||
const knownCwd = normalizeCwd(rendererCwd);
|
||||
if (requireActiveShellCwd && knownCwd && rendererCwdSource === "osc7") {
|
||||
return knownCwd;
|
||||
}
|
||||
const canUseRendererFallback = allowRendererFallback && (
|
||||
!requireActiveShellCwd || rendererCwdSource === "osc7"
|
||||
);
|
||||
if (!preferFreshBackend && knownCwd && canUseRendererFallback) return knownCwd;
|
||||
if (!sessionId) return canUseRendererFallback ? knownCwd : null;
|
||||
|
||||
try {
|
||||
const result = await getSessionPwd(
|
||||
sessionId,
|
||||
// Disable ~ guessing so we do not open SFTP on a fabricated home path,
|
||||
// while retaining the legacy login-shell fallback only for callers that
|
||||
// do not require proof of the active shell directory (#2886).
|
||||
preferFreshBackend
|
||||
? {
|
||||
allowHomeFallback: false,
|
||||
allowLoginShellFallback: !requireActiveShellCwd,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const backendCwd = result.success ? normalizeCwd(result.cwd) : null;
|
||||
return backendCwd ?? (canUseRendererFallback ? knownCwd : null);
|
||||
} catch {
|
||||
return canUseRendererFallback ? knownCwd : null;
|
||||
}
|
||||
};
|
||||
|
||||
export const PROBE_SESSION_CWD_AFTER_COMMAND_MS = 150;
|
||||
|
||||
export type ProbeBackendSessionCwdAfterCommandOptions = {
|
||||
sessionId: string;
|
||||
osc7SignalAtCommand: number;
|
||||
getOsc7Signal: () => number;
|
||||
getSessionPwd: (sessionId: string, options?: SessionPwdOptions) => Promise<SessionPwdResult>;
|
||||
canProbe?: () => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
/** Probe backend pwd when OSC 7 did not report after a command. */
|
||||
export const probeBackendSessionCwdAfterCommand = async ({
|
||||
sessionId,
|
||||
osc7SignalAtCommand,
|
||||
getOsc7Signal,
|
||||
getSessionPwd,
|
||||
canProbe = () => true,
|
||||
}: ProbeBackendSessionCwdAfterCommandOptions): Promise<string | null> => {
|
||||
if (getOsc7Signal() !== osc7SignalAtCommand) return null;
|
||||
const allowed = await canProbe();
|
||||
if (!allowed || getOsc7Signal() !== osc7SignalAtCommand) return null;
|
||||
|
||||
try {
|
||||
// This result is published to SFTP follow as the active shell cwd. Do not
|
||||
// let the backend substitute the login shell or home directory after a
|
||||
// command such as sudo/su changed the interactive shell identity.
|
||||
const result = await getSessionPwd(sessionId, {
|
||||
allowHomeFallback: false,
|
||||
allowLoginShellFallback: false,
|
||||
});
|
||||
if (getOsc7Signal() !== osc7SignalAtCommand) return null;
|
||||
return result.success ? normalizeCwd(result.cwd) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const scheduleBackendCwdProbeAfterCommand = (
|
||||
options: ProbeBackendSessionCwdAfterCommandOptions & {
|
||||
onProbedCwd: (cwd: string) => void;
|
||||
delayMs?: number;
|
||||
},
|
||||
): (() => void) => {
|
||||
const delayMs = options.delayMs ?? PROBE_SESSION_CWD_AFTER_COMMAND_MS;
|
||||
const timeoutId = setTimeout(() => {
|
||||
void probeBackendSessionCwdAfterCommand(options).then((cwd) => {
|
||||
if (cwd) options.onProbedCwd(cwd);
|
||||
});
|
||||
}, delayMs);
|
||||
return () => clearTimeout(timeoutId);
|
||||
};
|
||||
Reference in New Issue
Block a user