fix: 统一 Windows trans 工作目录
This commit is contained in:
+40
-5
@@ -49,7 +49,11 @@ describe("ssh windows PowerShell safety prelude", () => {
|
||||
expect(prelude).toContain("'PSPath','PSParentPath','PSChildName','PSDrive','PSProvider','ReadCount'");
|
||||
expect(prelude).toContain("Microsoft.PowerShell.Utility\\ConvertTo-Json");
|
||||
expect(prelude).toContain("$PSDefaultParameterValues['Get-Content:Encoding'] = 'utf8'");
|
||||
expect(prelude).toContain("Set-Location -LiteralPath 'C:\\test'");
|
||||
expect(prelude).toContain("[System.IO.Directory]::Exists($unideskRouteCwd)");
|
||||
expect(prelude).toContain("Set-Location -LiteralPath $unideskRouteCwd -ErrorAction Stop");
|
||||
expect(prelude).toContain("[Environment]::CurrentDirectory = (Get-Location).ProviderPath");
|
||||
expect(prelude).toContain("UNIDESK_SSH_CWD_FAILED");
|
||||
expect(prelude).toContain("trans-windows-route-cwd-invalid");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,6 +186,37 @@ describe("ssh windows fs read-only operations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("requires one route cwd for PowerShell, cmd, and their stdin launchers", () => {
|
||||
for (const operation of ["ps", "cmd"] as const) {
|
||||
try {
|
||||
parseSshInvocation("D601:win", [operation]);
|
||||
throw new Error(`expected ${operation} without a route cwd to fail`);
|
||||
} catch (error) {
|
||||
expect(error).toMatchObject({
|
||||
code: "trans-windows-route-cwd-required",
|
||||
argument: "D601:win",
|
||||
});
|
||||
expect((error as Error & { hint?: string }).hint).toContain("quoted heredoc");
|
||||
}
|
||||
}
|
||||
|
||||
const powerShell = parseSshInvocation("D601:win/d/work/研究资料归档-李昂", ["ps"]);
|
||||
const cmd = parseSshInvocation("D601:win/d/work/研究资料归档-李昂", ["cmd"]);
|
||||
const powerShellLauncher = decodedWindowsPowerShellCommand(powerShell.parsed.remoteCommand);
|
||||
const cmdLauncher = decodedWindowsPowerShellCommand(cmd.parsed.remoteCommand);
|
||||
|
||||
expect(powerShell.parsed.requiresStdin).toBe(true);
|
||||
expect(cmd.parsed.requiresStdin).toBe(true);
|
||||
for (const launcher of [powerShellLauncher, cmdLauncher]) {
|
||||
expect(launcher).toContain("D:\\work\\研究资料归档-李昂");
|
||||
expect(launcher).toContain("UNIDESK_SSH_CWD_FAILED");
|
||||
expect(launcher).toContain("trans-windows-route-cwd-invalid");
|
||||
expect(launcher).toContain("[Environment]::CurrentDirectory = (Get-Location).ProviderPath");
|
||||
expect(launcher).toContain("[Console]::In.ReadToEnd()");
|
||||
}
|
||||
expect(cmdLauncher).not.toContain("cd /d");
|
||||
});
|
||||
|
||||
test("rejects unsupported Windows read operations instead of treating them as POSIX", () => {
|
||||
expect(() => parseSshInvocation("D601:win/c/test", ["sed", "-n", "1p", "hello.md"])).toThrow("unsupported ssh win operation: sed");
|
||||
});
|
||||
@@ -355,7 +390,7 @@ describe("ssh stdout bounded streaming", () => {
|
||||
});
|
||||
|
||||
test("formats truncation hint without echoing remote command", () => {
|
||||
const invocation = parseSshInvocation("D601:win", ["ps"]);
|
||||
const invocation = parseSshInvocation("D601:win/c/test", ["ps"]);
|
||||
expect(invocation.parsed.remoteCommand).not.toBeNull();
|
||||
const hint = sshStdoutTruncationHint({
|
||||
invocation,
|
||||
@@ -370,7 +405,7 @@ describe("ssh stdout bounded streaming", () => {
|
||||
const payload = JSON.parse(formatted.slice("UNIDESK_SSH_STDOUT_TRUNCATED ".length)) as Record<string, unknown>;
|
||||
expect(payload.code).toBe("ssh-stdout-truncated");
|
||||
expect(payload.providerId).toBe("D601");
|
||||
expect(payload.route).toBe("D601:win");
|
||||
expect(payload.route).toBe("D601:win/c/test");
|
||||
expect(payload.thresholdBytes).toBe(4096);
|
||||
expect(payload.disclosurePolicy).toMatchObject({
|
||||
name: "unified-cli-dump-preview",
|
||||
@@ -380,7 +415,7 @@ describe("ssh stdout bounded streaming", () => {
|
||||
});
|
||||
|
||||
test("forwarder bounds stdout and emits one truncation hint", () => {
|
||||
const invocation = parseSshInvocation("D601:win", ["ps"]);
|
||||
const invocation = parseSshInvocation("D601:win/c/test", ["ps"]);
|
||||
const forwarded: Buffer[] = [];
|
||||
const stdout = {
|
||||
write(chunk: string | Buffer): boolean {
|
||||
@@ -423,7 +458,7 @@ describe("ssh stdout bounded streaming", () => {
|
||||
});
|
||||
|
||||
test("stderr forwarder uses the same dump guard and marker", () => {
|
||||
const invocation = parseSshInvocation("D601:win", ["ps"]);
|
||||
const invocation = parseSshInvocation("D601:win/c/test", ["ps"]);
|
||||
const forwarded: Buffer[] = [];
|
||||
const stderr = {
|
||||
write(chunk: string | Buffer): boolean {
|
||||
|
||||
+42
-12
@@ -547,6 +547,7 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
return parseWindowsGitInvocation(route, args.slice(1));
|
||||
}
|
||||
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
|
||||
requireWindowsRouteWorkspace(route, "ps");
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
|
||||
const fileArgs = [commandArgs[1]?.replace(/\\/g, "/") ?? "", ...commandArgs.slice(2)];
|
||||
@@ -581,6 +582,7 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
if (operation !== "cmd" && operation !== "cmd.exe") {
|
||||
throw new Error(windowsUnsupportedOperationMessage(route, operation));
|
||||
}
|
||||
requireWindowsRouteWorkspace(route, "cmd");
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length === 0) {
|
||||
return {
|
||||
@@ -590,12 +592,29 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
};
|
||||
}
|
||||
return {
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandArgs.map((value) => windowsCmdArgument(value, route)).join(" "), route.workspace))),
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(
|
||||
buildWindowsCmdLine(commandArgs.map((value) => windowsCmdArgument(value, route)).join(" ")),
|
||||
route.workspace,
|
||||
)),
|
||||
requiresStdin: false,
|
||||
invocationKind: "argv",
|
||||
};
|
||||
}
|
||||
|
||||
function requireWindowsRouteWorkspace(route: ParsedSshRoute, operation: "ps" | "cmd"): asserts route is ParsedSshRoute & { workspace: string } {
|
||||
if (route.workspace !== null) return;
|
||||
const entrypoint = sshDisplayEntrypoint();
|
||||
const routeTemplate = `${route.providerId}:win/<drive>/<path>`;
|
||||
throw new CliInputError(`Windows ${operation} requires an explicit route workspace; refusing to inherit C:\\Windows`, {
|
||||
code: "trans-windows-route-cwd-required",
|
||||
argument: route.raw,
|
||||
usage: operation === "ps"
|
||||
? [`${entrypoint} ${routeTemplate} ps '<PowerShell source>'`, `${entrypoint} ${routeTemplate} ps <<'PS'`]
|
||||
: [`${entrypoint} ${routeTemplate} cmd <command> [args...]`, `${entrypoint} ${routeTemplate} cmd <<'CMD'`],
|
||||
hint: "Put the Windows drive and working directory in the route. Send multiline source directly through a quoted heredoc on stdin.",
|
||||
});
|
||||
}
|
||||
|
||||
function looksLikeLocallyExpandedPowerShellSource(command: string): boolean {
|
||||
return /(?:^|[;{}]\s*)=\s*[^=]/u.test(command);
|
||||
}
|
||||
@@ -628,7 +647,7 @@ function parseWindowsGitInvocation(route: ParsedSshRoute, gitArgs: string[]): Pa
|
||||
if (subcommand === "commit") validateWindowsGitCommitArgs(route, gitArgs);
|
||||
const commandLine = ["git", ...gitArgs].map((value) => windowsCmdArgument(value, route)).join(" ");
|
||||
return {
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandLine, route.workspace))),
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandLine), route.workspace)),
|
||||
requiresStdin: false,
|
||||
invocationKind: "argv",
|
||||
};
|
||||
@@ -769,22 +788,16 @@ function validateWindowsGitCommitArgs(route: ParsedSshRoute, gitArgs: string[]):
|
||||
throw new Error(`ssh win git commit would open an editor; use ${entrypoint} ${route.raw} git commit -m <message>, ${entrypoint} ${route.raw} git commit --no-edit, or wrap an interactive command with ${entrypoint} ${route.raw} ps <<'PS'`);
|
||||
}
|
||||
|
||||
function buildWindowsCmdLine(userCommand: string, cwd: string | null): string {
|
||||
function buildWindowsCmdLine(userCommand: string): string {
|
||||
const parts = [
|
||||
"chcp 65001>nul",
|
||||
'set "PYTHONUTF8=1"',
|
||||
'set "PYTHONIOENCODING=utf-8"',
|
||||
];
|
||||
if (cwd !== null) parts.push(`cd /d ${windowsCmdQuote(cwd)}`);
|
||||
parts.push(userCommand);
|
||||
return parts.join(" && ");
|
||||
}
|
||||
|
||||
function windowsCmdQuote(value: string): string {
|
||||
if (/[\r\n"]/u.test(value)) throw new Error("ssh win workspace path must not contain quotes or newlines");
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
function windowsCmdArgument(value: string, route: ParsedSshRoute): string {
|
||||
if (value.length === 0) return "\"\"";
|
||||
if (/[\r\n"]/u.test(value) || /[&|<>^%!]/u.test(value)) {
|
||||
@@ -883,7 +896,23 @@ export function powerShellSingleQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function buildWindowsCmdLauncherScript(cmdLine: string): string {
|
||||
function windowsPowerShellRouteCwdGuardLines(cwd: string | null): string[] {
|
||||
if (cwd === null) return [];
|
||||
return [
|
||||
`$unideskRouteCwd = ${powerShellSingleQuote(cwd)};`,
|
||||
"try {",
|
||||
" if (-not [System.IO.Directory]::Exists($unideskRouteCwd)) { throw 'route cwd unavailable' }",
|
||||
" Set-Location -LiteralPath $unideskRouteCwd -ErrorAction Stop;",
|
||||
" [Environment]::CurrentDirectory = (Get-Location).ProviderPath;",
|
||||
"} catch {",
|
||||
" $unideskCwdError = [ordered]@{ code = 'trans-windows-route-cwd-invalid'; failureKind = 'cwd-failed'; cwd = $unideskRouteCwd; message = 'requested Windows route cwd does not exist or is not accessible' };",
|
||||
" [Console]::Error.WriteLine('UNIDESK_SSH_CWD_FAILED ' + ($unideskCwdError | Microsoft.PowerShell.Utility\\ConvertTo-Json -Compress));",
|
||||
" exit 2;",
|
||||
"}",
|
||||
];
|
||||
}
|
||||
|
||||
function buildWindowsCmdLauncherScript(cmdLine: string, cwd: string | null): string {
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop';",
|
||||
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
@@ -891,6 +920,7 @@ function buildWindowsCmdLauncherScript(cmdLine: string): string {
|
||||
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"$env:PYTHONUTF8 = '1';",
|
||||
"$env:PYTHONIOENCODING = 'utf-8';",
|
||||
...windowsPowerShellRouteCwdGuardLines(cwd),
|
||||
`& ${powerShellSingleQuote(windowsCmdExeNativePath)} /d /s /c ${powerShellSingleQuote(cmdLine)};`,
|
||||
"exit $LASTEXITCODE;",
|
||||
].join(" ");
|
||||
@@ -902,7 +932,6 @@ function buildWindowsCmdStdinLauncherScript(cwd: string | null): string {
|
||||
"chcp 65001>nul",
|
||||
'set "PYTHONUTF8=1"',
|
||||
'set "PYTHONIOENCODING=utf-8"',
|
||||
...(cwd === null ? [] : [`cd /d ${windowsCmdQuote(cwd)}`]),
|
||||
];
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop';",
|
||||
@@ -912,6 +941,7 @@ function buildWindowsCmdStdinLauncherScript(cwd: string | null): string {
|
||||
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"$env:PYTHONUTF8 = '1';",
|
||||
"$env:PYTHONIOENCODING = 'utf-8';",
|
||||
...windowsPowerShellRouteCwdGuardLines(cwd),
|
||||
"$script = [Console]::In.ReadToEnd();",
|
||||
"$script = $script -replace \"`r`n\", \"`n\";",
|
||||
"$script = $script -replace \"`r\", \"`n\";",
|
||||
@@ -993,8 +1023,8 @@ export function windowsPowerShellScriptPrelude(cwd: string | null): string {
|
||||
"$PSDefaultParameterValues['Get-Content:Encoding'] = 'utf8'",
|
||||
"$env:PYTHONUTF8 = '1'",
|
||||
"$env:PYTHONIOENCODING = 'utf-8'",
|
||||
...windowsPowerShellRouteCwdGuardLines(cwd),
|
||||
...windowsPowerShellJsonSafetyPreludeLines,
|
||||
...(cwd === null ? [] : [`Set-Location -LiteralPath ${powerShellSingleQuote(cwd)}`]),
|
||||
].join("\r\n") + "\r\n";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user