fix: keep active ssh downloads alive
This commit is contained in:
@@ -12,7 +12,12 @@ export interface SshRemoteCommandStreamHandlers {
|
||||
|
||||
export interface SshRemoteCommandExecutor {
|
||||
runRemoteCommand(remoteCommand: string, input?: string): Promise<SshCaptureResult>;
|
||||
streamRemoteCommand?(remoteCommand: string, handlers: SshRemoteCommandStreamHandlers, input?: string): Promise<SshCaptureResult>;
|
||||
streamRemoteCommand?(
|
||||
remoteCommand: string,
|
||||
handlers: SshRemoteCommandStreamHandlers,
|
||||
input?: string,
|
||||
options?: { inactivityTimeoutMs?: number },
|
||||
): Promise<SshCaptureResult>;
|
||||
}
|
||||
|
||||
export interface SshFileTransferCommandBuilders {
|
||||
@@ -32,6 +37,7 @@ interface SshFileTransferCliOptions {
|
||||
action: "upload" | "download";
|
||||
localPath: string;
|
||||
remotePath: string;
|
||||
inactivityTimeoutMs?: number;
|
||||
}
|
||||
|
||||
interface SshFileTransferStat {
|
||||
@@ -111,7 +117,7 @@ export async function runSshFileTransferOperation(
|
||||
return 0;
|
||||
}
|
||||
|
||||
const read = await downloadRemoteFileVerified(invocation, executor, builders, options.remotePath, localPath);
|
||||
const read = await downloadRemoteFileVerified(invocation, executor, builders, options.remotePath, localPath, options.inactivityTimeoutMs);
|
||||
const verification = buildTransferVerification(
|
||||
{ side: "remote", path: options.remotePath, ...read.remote },
|
||||
{ side: "local", path: localPath, ...read.local },
|
||||
@@ -143,6 +149,7 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
const action = args[0];
|
||||
if (action !== "upload" && action !== "download") throw new Error("ssh file transfer requires upload or download");
|
||||
const positionals: string[] = [];
|
||||
let inactivityTimeoutMs: number | undefined;
|
||||
for (let index = 1; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg === "--") {
|
||||
@@ -152,6 +159,21 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
if (arg === "--chunk-bytes" || arg === "--block-bytes") {
|
||||
throw new Error(`unsupported ssh ${action} option: ${arg}; downloads use host.ssh.tcp-pool stdout streaming and no longer support the legacy base64 block reader`);
|
||||
}
|
||||
if (arg === "--inactivity-timeout-ms" || arg === "--runtime-timeout-ms") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error(`ssh ${action} ${arg} requires a positive integer value`);
|
||||
index += 1;
|
||||
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(value, `ssh ${action} ${arg}`);
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--inactivity-timeout-ms=")) {
|
||||
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--inactivity-timeout-ms=".length), `ssh ${action} --inactivity-timeout-ms`);
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--runtime-timeout-ms=")) {
|
||||
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--runtime-timeout-ms=".length), `ssh ${action} --runtime-timeout-ms`);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h" || arg === "help") {
|
||||
throw new Error(`ssh ${action} usage: trans <route> ${action} ${action === "upload" ? "<local-file> <remote-file>" : "<remote-file> <local-file>"}`);
|
||||
}
|
||||
@@ -164,8 +186,16 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
const [first, second] = positionals;
|
||||
if (!first || !second) throw new Error(`ssh ${action} paths must be non-empty`);
|
||||
return action === "upload"
|
||||
? { action, localPath: first, remotePath: second }
|
||||
: { action, remotePath: first, localPath: second };
|
||||
? { action, localPath: first, remotePath: second, inactivityTimeoutMs }
|
||||
: { action, remotePath: first, localPath: second, inactivityTimeoutMs };
|
||||
}
|
||||
|
||||
function parsePositiveRuntimeTimeoutMs(value: string, label: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
||||
throw new Error(`${label} must be a positive integer number of milliseconds`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function writeRemoteFileVerified(
|
||||
@@ -210,6 +240,7 @@ async function downloadRemoteFileVerified(
|
||||
builders: SshFileTransferCommandBuilders,
|
||||
remotePath: string,
|
||||
localPath: string,
|
||||
inactivityTimeoutMs?: number,
|
||||
): Promise<SshFileTransferDownloadResult> {
|
||||
if (executor.streamRemoteCommand === undefined) {
|
||||
throw new SshFileTransferError("ssh download requires a tcp-pool stdout stream sink", {
|
||||
@@ -248,7 +279,7 @@ async function downloadRemoteFileVerified(
|
||||
hash.update(chunk);
|
||||
emitDownloadProgress(invocation, remotePath, chunkCount, actualBytes, remote.bytes, chunk.length, startedAtMs);
|
||||
},
|
||||
});
|
||||
}, undefined, { inactivityTimeoutMs });
|
||||
await closeWriteStream(output);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new SshFileTransferError("remote ssh download stream failed", {
|
||||
|
||||
Reference in New Issue
Block a user