fix: harden ssh download stat diagnostics
This commit is contained in:
+14
-1
@@ -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("{");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user