Compare commits

...
Author SHA1 Message Date
bincxzandCursor 0cda4a39b7 fix(sftp): bound shared write OPEN drain after channel error
Channel error kept sharedWriteOpenDrain pending for a late OPEN, but a
dead channel that never invokes the callback hung the transfer and lease.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:56:23 +08:00
bincxzandCursor 90494c0c06 fix(sftp): retain verification channel errors across sample gaps
A shared-channel error between verification sample races was dropped
when rejectPending was null, so the next read could hang forever.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:27:13 +08:00
bincxzandCursor 1478316806 fix(sftp): recheck cancel after verification sample race
A responsive last-sample READ can finish after cancel before the 2s
force-settle fires; without a post-race cancelled check the download
was reported complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:19:19 +08:00
bincxzandCursor 74ae5e0a48 fix(sftp): force-settle stalled shared verification READs on cancel
After concurrent download ranges finish, sample verification still awaited
bare sftp.read outside forceSettleOnError. Shared/sudo cancel cannot end
the session, so a hung verification callback held the transfer and lease.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:00:22 +08:00
bincxzandCursor cba714f31c fix(sftp): keep shared write OPEN drain pending on channel error
Codex P2: channel error completed sharedWriteOpenDrain before the OPEN
callback, allowing cleanup/fallback to race a late truncating OPEN.
Match the cancel path — settle the wait, but release the drain only after
the OPEN callback closes any late handle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 13:00:18 +08:00
bincxzandCursor 532cf6cc7d fix(sftp): await shared write OPEN drain without timeout
Codex P2: the 10s drain cap still let cleanup race a slower late OPEN.
Await sharedWriteOpenDrain unbounded so stage cleanup cannot run until
the write OPEN callback has closed its handle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 12:52:32 +08:00
bincxzandCursor 747847e71a fix(sftp): keep shared write OPEN drain past cancel timeout
Codex P2: the 2s cancel settle unblocked UX but cleanup could still race
a later truncating OPEN. Publish sharedWriteOpenDrain until the OPEN
callback finishes (including late opens), and await it in upload finally
before returning to stage cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 12:44:02 +08:00
bincxzandCursor af9cef2ee9 fix(sftp): drain shared write OPEN before cancel cleanup
Codex P2: cancelling a pending shared upload OPEN settled immediately,
so stage cleanup could run before a late truncating OPEN recreated the
remote path. Shared write opens now wait for the OPEN callback (or a
drain timeout), close the handle, then settle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 12:34:18 +08:00
bincxzandCursor a7f96c9412 fix(sftp): dispose isolated channel on CLOSE timeout
Codex P2: success-path CLOSE timeout with disposeChannel:true was
non-fatal and returned the channel to the pool. Shared/sudo stays
non-fatal; isolated downloads now surface completedWithUnhealthyChannel
so the channel is ended instead of reused.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 12:25:55 +08:00
bincxzandCursor a3b111377c fix(sftp): harden sudo shared download settle and fanout gates
Address Codex P2 (channel error during stalled OPEN never settled) and
multi-agent review: keep concurrent-shared READ sudo-only with a
per-session fast slot, settle OPEN sync throws without leaking listeners,
force-settle cancel without awaiting shared CLOSE, and report stream
strategy after fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 12:13:45 +08:00
陈奇 50ef8fac57 fix(sftp): force-settle cancelled shared download READs
Codex P2 on d855e69c: shared/sudo downloads passed forceSettleOnError
false, so cancel while an sftp.read callback never arrived hung forever
(abortChannel cannot end the shared session). Downloads have no remote
WRITEs to drain, so always force-settle on cancel/error without calling
sftp.end(). Shared uploads still wait for in-flight WRITEs.
2026-08-05 11:12:11 +08:00
bincxz d855e69c23 fix(sftp): settle shared OPEN cancel without ending sudo session
Codex P2 on PR #2720: disposeChannel:false made pre-OPEN abort a no-op
while openSftpHandle had no cancel reject, so cancel during a stalled
OPEN hung forever and held the SFTP lease. openSftpHandleForTransfer
rejects the pending OPEN on cancel without sftp.end() on shared channels,
closes late handles best-effort, and keeps isolated-channel end behavior.
2026-08-05 11:00:23 +08:00
netcatty-bot 2c369403db fix: address Codex review on PR #2720 2026-08-04 23:25:08 +00:00
netcatty-bot 10ff5fe94d fix(#2719): automated Cursor CLI fix 2026-08-04 18:35:30 +00:00
2 changed files with 1851 additions and 38 deletions

No files matched your search

+439 -38
View File
@@ -978,6 +978,9 @@ function takePendingCancel(transferId) {
const admittedActiveByResource = new Map();
let admittedTransferLimit = 2;
const isolatedDownloadChannelPools = new WeakMap();
// Sudo downloads share the browse channel; cap concurrent 64-READ fanout the
// same way isolated channels use FAST_DOWNLOAD_CHANNELS_PER_SESSION (#1507).
const sharedFastDownloadSlots = new WeakMap();
// Cache live SFTP clients where remote cp is known to be unavailable, so we
// skip repeated failed exec attempts without retaining closed session ids.
const cpUnavailableSet = new WeakSet();
@@ -1365,6 +1368,22 @@ function releaseIsolatedDownloadChannel(client, sftp, options = {}) {
scheduleIdleIsolatedDownloadChannel(client, sftp);
}
function tryAcquireSharedFastDownloadSlot(client) {
const inFlight = sharedFastDownloadSlots.get(client) || 0;
if (inFlight >= FAST_DOWNLOAD_CHANNELS_PER_SESSION) return false;
sharedFastDownloadSlots.set(client, inFlight + 1);
return true;
}
function releaseSharedFastDownloadSlot(client) {
const inFlight = sharedFastDownloadSlots.get(client) || 0;
if (inFlight <= 1) {
sharedFastDownloadSlots.delete(client);
return;
}
sharedFastDownloadSlots.set(client, inFlight - 1);
}
async function acquireIsolatedDownloadChannel(client, transfer) {
const pool = getIsolatedDownloadChannelPool(client);
if (transfer?.cancelled) return null;
@@ -1902,12 +1921,171 @@ async function uploadFile(
throw error;
}
function openSftpHandle(sftp, filePath, flags) {
return new Promise((resolve, reject) => {
sftp.open(filePath, flags, (error, handle) => {
if (error) reject(error);
else resolve(handle);
/**
* Open a remote SFTP handle while transfer.abort can reject the wait without
* depending on sftp.end(). Isolated channels still pass abortChannel that ends
* the subsystem; shared/sudo channels pass a no-op end path and only reject.
* A late OPEN handle after cancel / channel error is closed best-effort when
* disposeChannel is false so the shared session does not leak handles.
* Channel `error` while OPEN is pending must reject here: callers only check
* recorded channelError after this await returns, so a dead channel that never
* invokes the OPEN callback would otherwise hang the transfer and SFTP lease.
* Shared write opens ("w" / "r+" / …) publish transfer.sharedWriteOpenDrain and
* keep it pending until the OPEN callback finishes (including late opens after
* a cancel settle timeout), so upload cleanup awaits the drain before deleting
* the remote stage. Channel-error / cancel-settle paths that never receive an
* OPEN callback arm a short drain force-complete so a dead channel cannot hold
* the transfer and SFTP lease forever.
*
* @param {{ disposeChannel?: boolean, abortChannel?: (() => void) | null }} [options]
*/
function openSftpHandleForTransfer(sftp, filePath, flags, transfer, options = {}) {
const disposeChannel = options.disposeChannel !== false;
const abortChannel = typeof options.abortChannel === "function"
? options.abortChannel
: null;
// ssh2 string flags: "r" is read-only; anything else can create/truncate.
const isWriteOpen = String(flags ?? "r") !== "r";
const trackSharedWriteDrain = !disposeChannel && isWriteOpen;
let resolveSharedWriteDrain = null;
if (trackSharedWriteDrain) {
transfer.sharedWriteOpenDrain = new Promise((resolve) => {
resolveSharedWriteDrain = resolve;
});
}
return new Promise((resolve, reject) => {
let settled = false;
let openDrainTimer = null;
let drainForceTimer = null;
const previousAbort = transfer.abort;
const clearDrainForceTimer = () => {
if (!drainForceTimer) return;
clearTimeout(drainForceTimer);
drainForceTimer = null;
};
const completeSharedWriteDrain = () => {
clearDrainForceTimer();
if (!resolveSharedWriteDrain) return;
const resolveDrain = resolveSharedWriteDrain;
resolveSharedWriteDrain = null;
resolveDrain();
};
// When OPEN wait settles without an OPEN callback (channel error, or cancel
// settle timeout), keep drain pending for a late truncating OPEN — but force
// complete if the callback never arrives so cleanup/lease cannot hang.
const armSharedWriteDrainForceComplete = () => {
if (!trackSharedWriteDrain || !resolveSharedWriteDrain || drainForceTimer) return;
drainForceTimer = setTimeout(() => {
drainForceTimer = null;
completeSharedWriteDrain();
}, 2000);
};
const settle = (fn, value) => {
if (settled) return;
settled = true;
if (openDrainTimer) {
clearTimeout(openDrainTimer);
openDrainTimer = null;
}
if (transfer.abort === abortDuringOpen) {
transfer.abort = previousAbort;
}
try { sftp.removeListener?.("error", onOpenChannelError); } catch { /* ignore */ }
fn(value);
};
const abortDuringOpen = () => {
transfer.cancelled = true;
// Shared write OPEN can truncate/create the remote path. A settle timeout
// unblocks the transfer UX, but sharedWriteOpenDrain stays pending until
// the OPEN callback finishes (or the drain force-complete) so cleanup
// cannot race a late truncating OPEN.
if (!disposeChannel && isWriteOpen && !settled) {
try { abortChannel?.(); } catch { /* ignore */ }
if (!openDrainTimer) {
openDrainTimer = setTimeout(() => {
settle(reject, new Error("Transfer cancelled"));
armSharedWriteDrainForceComplete();
}, 2000);
}
return;
}
// Settle before abortChannel so a synchronous OPEN callback from
// sftp.end() cannot win the promise with a channel-close error first.
settle(reject, new Error("Transfer cancelled"));
try { abortChannel?.(); } catch { /* ignore */ }
if (!trackSharedWriteDrain) completeSharedWriteDrain();
};
const onOpenChannelError = (error) => {
settle(reject, error || new Error("SFTP channel error"));
// Shared write: keep drain pending for a late OPEN close, but bound it so
// a dead channel that never invokes the OPEN callback cannot hang forever.
if (!trackSharedWriteDrain) {
completeSharedWriteDrain();
} else {
armSharedWriteDrainForceComplete();
}
};
if (transfer.cancelled) {
completeSharedWriteDrain();
reject(new Error("Transfer cancelled"));
return;
}
transfer.abort = abortDuringOpen;
sftp.on?.("error", onOpenChannelError);
try {
sftp.open(filePath, flags, (error, handle) => {
if (settled) {
// Cancel / channel error already settled (possibly via drain timeout).
// Still close a late shared handle, then release the drain barrier so
// cleanup can run.
if (!error && handle && !disposeChannel) {
closeSftpHandle(sftp, handle).then(
completeSharedWriteDrain,
completeSharedWriteDrain,
);
return;
}
completeSharedWriteDrain();
return;
}
if (error) {
settle(reject, error);
completeSharedWriteDrain();
return;
}
if (transfer.cancelled) {
const finishCancel = () => {
settle(reject, new Error("Transfer cancelled"));
completeSharedWriteDrain();
};
if (handle && !disposeChannel) {
// Close before settle/drain so shared write cleanup runs after the
// truncating OPEN handle is released.
closeSftpHandle(sftp, handle).then(finishCancel, finishCancel);
return;
}
finishCancel();
return;
}
settle(resolve, handle);
completeSharedWriteDrain();
});
} catch (error) {
// Sync throw (destroyed channel / bad state) must still settle and drop
// the error listener; otherwise the shared browse channel leaks listeners.
settle(reject, error);
completeSharedWriteDrain();
}
});
}
@@ -1982,19 +2160,82 @@ async function verifyFastDownloadSamples(sftp, remoteHandle, localHandle, fileSi
Math.max(0, Math.floor((fileSize - sampleSize) / 2)),
Math.max(0, fileSize - sampleSize),
])];
for (const position of offsets) {
if (transfer.cancelled) throw new Error("Transfer cancelled");
const length = Math.min(sampleSize, fileSize - position);
const remoteBuffer = Buffer.allocUnsafe(length);
const localBuffer = Buffer.allocUnsafe(length);
await readSftpRange(sftp, remoteHandle, remoteBuffer, position, length);
await readLocalRange(localHandle, localBuffer, position, length);
if (!remoteBuffer.equals(localBuffer)) {
const error = new Error("Transfer source content changed during transfer");
error.noTransferFallback = true;
error.sourceChanged = true;
throw error;
// Verification READs sit outside runPausableConcurrentRanges. After ranges
// settle, the prior abort hook is a no-op, so shared/sudo cancel (or a
// channel error without a READ callback) would hang forever without a local
// force-settle — same 2s grace as forceSettleOnError for download READs.
let rejectPending = null;
let forceSettleTimer = null;
let verifyDone = false;
let channelError = null;
const previousAbort = transfer.abort;
const cancelError = () => new Error("Transfer cancelled");
const abortDuringVerify = () => {
transfer.cancelled = true;
try { previousAbort?.(); } catch { /* ignore */ }
if (verifyDone || forceSettleTimer) return;
forceSettleTimer = setTimeout(() => {
forceSettleTimer = null;
rejectPending?.(cancelError());
}, 2000);
};
const onVerifyChannelError = (error) => {
// Remember across sample gaps: an error between races (after one sample
// resolves, before the next rejectPending is installed) must still fail
// the next sample instead of letting readSftpRange hang forever.
channelError = channelError || error || new Error("SFTP channel error");
rejectPending?.(channelError);
};
transfer.abort = abortDuringVerify;
sftp.on?.("error", onVerifyChannelError);
try {
if (transfer.cancelled) throw cancelError();
if (channelError) throw channelError;
for (const position of offsets) {
if (transfer.cancelled) throw cancelError();
if (channelError) throw channelError;
const length = Math.min(sampleSize, fileSize - position);
const remoteBuffer = Buffer.allocUnsafe(length);
const localBuffer = Buffer.allocUnsafe(length);
await Promise.race([
(async () => {
await readSftpRange(sftp, remoteHandle, remoteBuffer, position, length);
await readLocalRange(localHandle, localBuffer, position, length);
if (!remoteBuffer.equals(localBuffer)) {
const error = new Error("Transfer source content changed during transfer");
error.noTransferFallback = true;
error.sourceChanged = true;
throw error;
}
})(),
new Promise((_, reject) => {
rejectPending = reject;
if (channelError) {
reject(channelError);
return;
}
if (transfer.cancelled) abortDuringVerify();
}),
]);
// Cancel during a responsive sample can win the race above without the
// force-settle timer firing. Recheck so we never report complete after
// cancel (especially the last sample on direct/non-staged downloads).
if (transfer.cancelled) throw cancelError();
if (channelError) throw channelError;
}
} finally {
verifyDone = true;
if (forceSettleTimer) {
clearTimeout(forceSettleTimer);
forceSettleTimer = null;
}
rejectPending = null;
if (transfer.abort === abortDuringVerify) {
transfer.abort = previousAbort;
}
try { sftp.removeListener?.("error", onVerifyChannelError); } catch { /* ignore */ }
}
}
@@ -2459,10 +2700,11 @@ async function runPausableConcurrentRanges({
if (settled) return;
if (error) terminalError = terminalError || error;
if (active > 0) {
// Only an isolated channel may force-settle after aborting the
// subsystem. A shared channel cannot discard outstanding WRITE
// callbacks safely because the caller may clean up or reuse the
// same remote path while those requests are still in flight.
// forceSettleOnError callers may abandon in-flight ranges after a
// short grace: isolated channels (after sftp.end), and shared
// downloads (READ-only — no remote WRITEs to drain). Shared uploads
// must keep forceSettleOnError false so outstanding WRITEs finish
// before the caller reuses or cleans up the remote path.
if (terminalError && forceSettleOnError) {
if (!forceFinishTimer) {
forceFinishTimer = setTimeout(() => {
@@ -2522,6 +2764,9 @@ async function runPausableConcurrentRanges({
void copyRange(position, length)
.then(() => {
// After force-settle, abandoned ranges must not publish progress
// or advance checkpoints into the caller's truncate/fallback window.
if (settled) return;
transferred += length;
completedRanges.set(position, position + length);
while (completedRanges.has(contiguousCheckpoint)) {
@@ -2531,8 +2776,12 @@ async function runPausableConcurrentRanges({
}
publishContiguousCheckpoint(false);
})
.catch((error) => abort(error))
.catch((error) => {
if (settled) return;
abort(error);
})
.finally(() => {
if (settled) return;
active -= 1;
settlePauseWaiters();
if (terminalError || transfer.cancelled) {
@@ -2619,8 +2868,9 @@ async function uploadFileConcurrent(
channelError = channelError || error;
};
sftp.on?.("error", onChannelError);
// Install cancel before OPEN so a stalled remote open can still end an
// isolated channel (runPausableConcurrentRanges would install this later).
// Install cancel before OPEN so a stalled remote open can still settle:
// isolated channels end the subsystem; shared/sudo reject OPEN without end().
// (runPausableConcurrentRanges would install abort later, after OPEN.)
const abortChannel = () => {
if (disposeChannel) {
try { sftp.end?.(); } catch { /* ignore */ }
@@ -2676,7 +2926,13 @@ async function uploadFileConcurrent(
digestHandle = await fs.promises.open(ephemeralDigestPath, "r");
}
if (transfer.cancelled) throw new Error("Transfer cancelled");
remoteHandle = await openSftpHandle(sftp, remotePath, checkpoint > 0 ? "r+" : "w");
remoteHandle = await openSftpHandleForTransfer(
sftp,
remotePath,
checkpoint > 0 ? "r+" : "w",
transfer,
{ disposeChannel, abortChannel },
);
if (channelError) throw channelError;
if (transfer.cancelled) throw new Error("Transfer cancelled");
@@ -2734,6 +2990,19 @@ async function uploadFileConcurrent(
}
throw error;
} finally {
// Shared write OPEN may still be in flight after a cancel settle timeout or
// channel error. Await the drain barrier before returning so
// runRemoteUploadTransaction cannot clean up the stage before a late
// truncating OPEN finishes (Codex P2 on 747847e7). openSftpHandleForTransfer
// force-completes the drain if the OPEN callback never arrives on a dead
// channel, so this await cannot hang forever.
const sharedWriteOpenDrain = transfer.sharedWriteOpenDrain;
if (sharedWriteOpenDrain) {
await sharedWriteOpenDrain.catch(() => {});
if (transfer.sharedWriteOpenDrain === sharedWriteOpenDrain) {
transfer.sharedWriteOpenDrain = null;
}
}
transfer.readStream = null;
transfer.waitForPause = null;
transfer.cancelPauseWait = null;
@@ -2788,6 +3057,11 @@ async function uploadFileConcurrent(
* durable byte; out-of-order range completion cannot advance past a hole.
* Once pause is requested, no new ranges are scheduled and we wait for every
* in-flight range before acknowledging it.
*
* @param {{ disposeChannel?: boolean }} [options]
* disposeChannel — when true (isolated channel), cancel/abort may call
* sftp.end(); the caller still returns healthy channels to the download pool.
* When false (shared browse / sudo session), never call sftp.end().
*/
async function downloadFileResumableFast(
remotePath,
@@ -2796,18 +3070,26 @@ async function downloadFileResumableFast(
fileSize,
transfer,
sendProgress,
options = {},
) {
const disposeChannel = options.disposeChannel !== false;
const checkpoint = Math.max(0, Math.min(transfer.checkpointBytes || 0, fileSize));
let channelError = null;
const onChannelError = (error) => {
channelError = channelError || error;
};
sftp.on?.("error", onChannelError);
// Install cancel before OPEN so a stalled remote open can still end the
// isolated channel (runPausableConcurrentRanges would install this later).
// Install cancel before OPEN so a stalled remote open can still settle:
// isolated channels end the subsystem; shared/sudo reject OPEN without end().
// (runPausableConcurrentRanges would install abort later, after OPEN.)
const abortChannel = () => {
if (disposeChannel) {
try { sftp.end?.(); } catch { /* ignore */ }
}
};
const abortEarly = () => {
transfer.cancelled = true;
try { sftp.end?.(); } catch { }
abortChannel();
};
transfer.abort = abortEarly;
@@ -2816,7 +3098,13 @@ async function downloadFileResumableFast(
let failed = false;
try {
if (transfer.cancelled) throw new Error("Transfer cancelled");
remoteHandle = await openSftpHandle(sftp, remotePath, "r");
remoteHandle = await openSftpHandleForTransfer(
sftp,
remotePath,
"r",
transfer,
{ disposeChannel, abortChannel },
);
if (channelError) throw channelError;
if (transfer.cancelled) throw new Error("Transfer cancelled");
localHandle = await fs.promises.open(localPath, checkpoint > 0 ? "r+" : "w+");
@@ -2834,8 +3122,11 @@ async function downloadFileResumableFast(
await writeLocalRange(localHandle, buffer, position, length);
},
sendProgress,
abortChannel: () => sftp.end?.(),
abortChannel,
sftp,
// Always force-settle on cancel/error: downloads have no remote WRITEs
// to drain. Shared/sudo paths cannot sftp.end() a stuck READ, so without
// this a hung read callback would hold the transfer and SFTP lease forever.
forceSettleOnError: true,
});
if (channelError) throw channelError;
@@ -2861,11 +3152,52 @@ async function downloadFileResumableFast(
}
}
let remoteCloseError = null;
if (remoteHandle && !failed && !transfer.cancelled) {
try {
await closeSftpHandle(sftp, remoteHandle);
} catch (error) {
remoteCloseError = error;
// Close remote handles while the channel is still live. On disposeChannel
// cancel/failure the channel is about to be ended (or already dead); a
// CLOSE request would hang forever with no callback from ssh2.
if (remoteHandle) {
const skipClose = disposeChannel && (failed || transfer.cancelled);
if (!skipClose) {
if (failed || transfer.cancelled) {
// Shared cancel/failure already settled the transfer; do not await
// CLOSE (adds up to 2s on a dead channel). Best-effort close only.
closeSftpHandle(sftp, remoteHandle).catch(() => {});
} else {
// Success path: bound CLOSE so a hung callback cannot block lease
// release. Tag the watchdog so real server "timed out" CLOSE errors
// are not swallowed by message matching.
let closeTimeout = null;
try {
await Promise.race([
closeSftpHandle(sftp, remoteHandle),
new Promise((_, reject) => {
closeTimeout = setTimeout(() => {
const timeoutError = new Error("SFTP close timed out");
timeoutError.sftpCloseTimedOut = true;
reject(timeoutError);
}, 2000);
}),
]);
} catch (error) {
if (error?.sftpCloseTimedOut) {
console.warn(
"[transferBridge] SFTP CLOSE timed out; abandoning remote handle",
transfer.transferId || "",
disposeChannel ? "(isolated — dispose channel)" : "(shared — keep session)",
);
// Shared/sudo: timeout is non-fatal (session stays alive).
// Isolated: mark unhealthy so downloadFile disposes instead of
// returning a possibly-dead channel to the pool.
if (disposeChannel) {
remoteCloseError = error;
}
} else {
remoteCloseError = error;
}
} finally {
if (closeTimeout) clearTimeout(closeTimeout);
}
}
}
}
if (!failed && !transfer.cancelled && !remoteCloseError && channelError) {
@@ -2878,9 +3210,12 @@ async function downloadFileResumableFast(
throw error;
}
if (!failed && !transfer.cancelled && remoteCloseError) {
const error = new Error("The isolated SFTP channel failed while closing", { cause: remoteCloseError });
error.completedWithUnhealthyChannel = true;
throw error;
if (disposeChannel) {
const error = new Error("The isolated SFTP channel failed while closing", { cause: remoteCloseError });
error.completedWithUnhealthyChannel = true;
throw error;
}
throw remoteCloseError;
}
}
}
@@ -3032,7 +3367,73 @@ async function downloadFile(
}
}
// Sudo cannot open an isolated secondary channel, so elevated downloads must
// pipeline READs on the shared browse session (#2719). Cap concurrent shared
// fanout with the same per-session budget as isolated fast downloads (#1507).
// Non-sudo isolated misses stay on serial createReadStream below.
if (
client.__netcattySudoMode
&& typeof sftp.open === "function"
&& typeof sftp.read === "function"
&& tryAcquireSharedFastDownloadSlot(client)
) {
try {
try {
transfer.downloadStrategy = "concurrent-shared";
logTransferDiag(transfer, "strategy", {
strategy: "concurrent-shared",
fields: {
chunk: formatDiagBytes(TRANSFER_CHUNK_SIZE),
concurrency: DOWNLOAD_TRANSFER_CONCURRENCY,
},
});
await downloadFileResumableFast(
remotePath,
localPath,
sftp,
fileSize,
transfer,
sendProgress,
{ disposeChannel: false },
);
if (initialSource) {
const latestSource = await runCancelablePreflight(() => client.stat(remotePath));
await assertDownloadSourceAfterTransfer(initialSource, latestSource, fileSize, {
localPath,
client,
remotePath,
signal: transfer.signal,
});
}
return;
} catch (err) {
if (transfer.cancelled) throw err;
if (err?.noTransferFallback) throw err;
// Concurrent ranges may leave sparse tails past the contiguous
// checkpoint; truncate the actual local target before stream resume.
const sharedCheckpoint = Math.max(0, Math.min(transfer.checkpointBytes || 0, fileSize));
try {
await fs.promises.truncate(localPath, sharedCheckpoint);
} catch (truncateError) {
if (!(sharedCheckpoint === 0 && truncateError?.code === "ENOENT")) {
throw truncateError;
}
}
sendProgress(sharedCheckpoint, fileSize, { force: true, checkpointBytes: sharedCheckpoint });
// Clear so the stream fallback below reports the strategy that actually ran.
transfer.downloadStrategy = null;
console.warn(
"[transferBridge] concurrent shared download failed, falling back to a compatible stream:",
err?.message || String(err),
);
}
} finally {
releaseSharedFastDownloadSlot(client);
}
}
// Fallback: sequential stream piping
transfer.downloadStrategy = transfer.downloadStrategy || "stream";
const checkpoint = Math.max(0, Math.min(transfer.checkpointBytes || 0, fileSize));
if (checkpoint >= fileSize) {
// Planned snapshot is already fully staged (including zero-byte snapshots).
File diff suppressed because it is too large. Load diff