fix: harden ssh download stat diagnostics

This commit is contained in:
AgentRun Artificer
2026-06-11 17:15:58 +08:00
parent bb0f0ec497
commit f3055c6617
3 changed files with 82 additions and 12 deletions
+14 -1
View File
@@ -67,11 +67,24 @@ function normalizeErrorPayload(command: string, error: unknown): Record<string,
}
if (error instanceof Error) {
const debug = process.env.UNIDESK_CLI_DEBUG === "1" || process.env.UNIDESK_CLI_FULL_ERROR === "1" || process.env.UNIDESK_CLI_RAW_ERROR === "1";
return { name: error.name, message, stack: error.stack ?? null, ...(debug ? { debug: true } : {}) };
return {
name: error.name,
message,
...(sshFileTransferErrorDetails(error) ?? {}),
stack: error.stack ?? null,
...(debug ? { debug: true } : {}),
};
}
return { message };
}
function sshFileTransferErrorDetails(error: Error): Record<string, unknown> | null {
if (error.name !== "SshFileTransferError") return null;
const details = (error as Error & { details?: unknown }).details;
if (typeof details !== "object" || details === null || Array.isArray(details)) return null;
return { details };
}
function parseStructuredErrorMessage(command: string, message: string): Record<string, unknown> | null {
if (!shouldSuppressStack(command) && !shouldSuppressStack(commandPrefixFromMessage(message))) return null;
const jsonStart = message.indexOf("{");
+28 -9
View File
@@ -354,7 +354,7 @@ async function statRemoteFile(
remotePath: string,
): Promise<SshFileTransferStat> {
const stat = await checkedFileTransfer(invocation, executor, builders, "stat", [remotePath]);
return parseFileTransferStat(stat.stdout, remotePath);
return parseFileTransferStat(stat.stdout, stat.stderr, remotePath);
}
async function checkedFileTransfer(
@@ -373,8 +373,8 @@ async function checkedFileTransfer(
operation,
args: args.slice(0, 4),
exitCode: result.exitCode,
stdout: result.stdout.slice(-2000),
stderr: result.stderr.slice(-4000),
stdout: transferTextSnapshot(result.stdout, { headChars: operation === "stat" ? 500 : 120, tailChars: 500 }),
stderr: transferTextSnapshot(result.stderr, { headChars: 120, tailChars: 1000 }),
});
}
@@ -389,16 +389,35 @@ function buildFileTransferRemoteCommand(
return builders.buildRouteCommand(route, ["sh", "-c", posixFileTransferScript(), "unidesk-file-transfer", operation, ...args], { stdin: hasInput });
}
function parseFileTransferStat(stdout: string, remotePath: string): SshFileTransferStat {
const [bytesText, sha256] = stdout.trim().split(/\s+/u);
const bytes = Number(bytesText);
if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
function parseFileTransferStat(stdout: string, stderr: string, remotePath: string): SshFileTransferStat {
const lines = stdout.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
for (const line of lines) {
const [bytesText, sha256, extra] = line.split(/\s+/u);
const bytes = Number(bytesText);
if (extra === undefined && Number.isSafeInteger(bytes) && bytes >= 0 && /^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
return { bytes, sha256: sha256! };
}
}
{
throw new SshFileTransferError("remote file transfer stat returned invalid metadata", {
remotePath,
stdout: stdout.slice(0, 500),
expected: "<bytes> <sha256>",
stdout: transferTextSnapshot(stdout, { headChars: 500, tailChars: 500 }),
stderr: transferTextSnapshot(stderr, { headChars: 120, tailChars: 1000 }),
candidateLines: lines.slice(0, 8).map((line) => line.slice(0, 200)),
candidateLineCount: lines.length,
});
}
return { bytes, sha256: sha256! };
}
function transferTextSnapshot(value: string, limits: { headChars: number; tailChars: number }): Record<string, unknown> {
return {
bytes: Buffer.byteLength(value, "utf8"),
chars: value.length,
head: value.slice(0, limits.headChars),
tail: value.slice(Math.max(0, value.length - limits.tailChars)),
truncated: value.length > limits.headChars + limits.tailChars,
};
}
function assertTransferStat(label: string, pathName: string, expected: SshFileTransferStat, actual: SshFileTransferStat): void {