fix: stabilize ssh artifact downloads
This commit is contained in:
@@ -53,7 +53,9 @@ class SshFileTransferError extends Error {
|
||||
const fileTransferReadBlockBytes = 45_000;
|
||||
const fileTransferWriteB64ArgvLimit = 48_000;
|
||||
const fileTransferWriteB64ChunkChars = 12_000;
|
||||
const fileTransferReadBlockMaxAttempts = 3;
|
||||
const fileTransferReadBlockMaxAttempts = 12;
|
||||
const fileTransferReadEmptyRetryDelayMs = 250;
|
||||
const fileTransferProgressEveryChunks = 64;
|
||||
|
||||
export function isSshFileTransferOperation(args: string[]): boolean {
|
||||
const subcommand = args[0] ?? "";
|
||||
@@ -210,6 +212,7 @@ async function readRemoteFileVerified(
|
||||
chunks.push(chunk);
|
||||
actualBytes += chunk.length;
|
||||
chunkCount += 1;
|
||||
emitDownloadProgress(invocation, remotePath, blockIndex, chunkCount, actualBytes, remote.bytes, chunk.length);
|
||||
}
|
||||
const content = Buffer.concat(chunks);
|
||||
const actual = { bytes: content.length, sha256: sha256HexBuffer(content) };
|
||||
@@ -234,11 +237,19 @@ async function readRemoteBase64BlockWithRetry(
|
||||
const encoded = read.stdout.replace(/\s+/gu, "");
|
||||
if (encoded.length > 0) return encoded;
|
||||
attemptErrors.push({ attempt, exitCode: read.exitCode, stdoutBytes: read.stdout.length, stderrTail: read.stderr.slice(-500) });
|
||||
if (attempt < fileTransferReadBlockMaxAttempts) {
|
||||
emitEmptyReadRetryProgress(invocation, remotePath, blockIndex, attempt, expectedBytes, actualBytes);
|
||||
await delayMs(fileTransferReadEmptyRetryDelayMs);
|
||||
}
|
||||
} catch (error) {
|
||||
attemptErrors.push({
|
||||
attempt,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
if (attempt < fileTransferReadBlockMaxAttempts) {
|
||||
emitEmptyReadRetryProgress(invocation, remotePath, blockIndex, attempt, expectedBytes, actualBytes);
|
||||
await delayMs(fileTransferReadEmptyRetryDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new SshFileTransferError("remote download returned an empty block before EOF after retries", {
|
||||
@@ -252,6 +263,54 @@ async function readRemoteBase64BlockWithRetry(
|
||||
});
|
||||
}
|
||||
|
||||
function emitDownloadProgress(
|
||||
invocation: ParsedSshInvocation,
|
||||
remotePath: string,
|
||||
blockIndex: number,
|
||||
chunkCount: number,
|
||||
actualBytes: number,
|
||||
expectedBytes: number,
|
||||
lastChunkBytes: number,
|
||||
): void {
|
||||
if (chunkCount !== 1 && actualBytes < expectedBytes && chunkCount % fileTransferProgressEveryChunks !== 0) return;
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: "unidesk.ssh.download.progress",
|
||||
route: invocation.route.raw,
|
||||
providerId: invocation.providerId,
|
||||
remotePath,
|
||||
blockIndex,
|
||||
chunks: chunkCount,
|
||||
actualBytes,
|
||||
expectedBytes,
|
||||
lastChunkBytes,
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
function emitEmptyReadRetryProgress(
|
||||
invocation: ParsedSshInvocation,
|
||||
remotePath: string,
|
||||
blockIndex: number,
|
||||
attempt: number,
|
||||
expectedBytes: number,
|
||||
actualBytes: number,
|
||||
): void {
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: "unidesk.ssh.download.empty-read-retry",
|
||||
route: invocation.route.raw,
|
||||
providerId: invocation.providerId,
|
||||
remotePath,
|
||||
blockIndex,
|
||||
attempt,
|
||||
maxAttempts: fileTransferReadBlockMaxAttempts,
|
||||
actualBytes,
|
||||
expectedBytes,
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
function delayMs(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function statRemoteFile(
|
||||
invocation: ParsedSshInvocation,
|
||||
executor: SshRemoteCommandExecutor,
|
||||
|
||||
Reference in New Issue
Block a user