src/vs/platform/agentHost/node/agentSdkDownloader.ts

626 LOC · 562 covered · 64 uncovered · 70 ranges · 406 concepts · 21 introducers · 230 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentSdkDownloader.ts ×15
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as fs from 'fs';
7 > import * as tar from 'tar';
8 > import { VSBuffer } from '../../../base/common/buffer.js';
9 > import { CancellationToken } from '../../../base/common/cancellation.js';
10 > import { CancellationError } from '../../../base/common/errors.js';
11 > import { Emitter, Event } from '../../../base/common/event.js';
12 > import { Disposable } from '../../../base/common/lifecycle.js';
13 > import * as path from '../../../base/common/path.js';
14 > import { format2 } from '../../../base/common/strings.js';
15 > import { URI } from '../../../base/common/uri.js';
16 > import { generateUuid } from '../../../base/common/uuid.js';
17 > import { detectLibcSync, type LibcFamily } from '../../../base/node/libc.js';
18 > import { INativeEnvironmentService } from '../../environment/common/environment.js';
19 > import { FileOperationError, FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
20 > import { createDecorator } from '../../instantiation/common/instantiation.js';
21 > import { ILogService } from '../../log/common/log.js';
22 > import { IProductService } from '../../product/common/productService.js';
23 > import { IRequestService } from '../../request/common/request.js';
24 > import { IRequestContext } from '../../../base/parts/request/common/request.js';
25 >
26 > // #region Per-package strategy
27 >
28 > /**
29 > * One agent-SDK package the downloader can fetch. Holds the per-package
30 > * knowledge that varies between Claude, Codex, and any future provider —
31 > * the package id, the env var that acts as a dev override, and one
32 > * boolean covering the only mapping detail that differs between SDKs
33 > * today (Claude has separate `linux-*-musl` SKUs; Codex's Linux binary
34 > * is statically musl-linked and ships as a single `linux-*` SKU).
35 > *
36 > * The downloader itself is package-agnostic: it consumes this interface and
37 > * never branches on `id`. Concrete `IAgentSdkPackage` instances live in
38 > * their owning agent module (e.g. `ClaudeSdkPackage` in
39 > * `claude/claudeAgentSdkService.ts`, `CodexSdkPackage` in
40 > * `codex/codexAgent.ts`) so Claude-specific / Codex-specific knowledge
41 > * stays in those modules — the downloader doesn't name the providers it
42 > * serves.
43 > *
44 > * Each shipped `product.json` carries one `{version, urlTemplate}` per
45 > * SDK. The downloader substitutes `{sdkTarget}` (resolved via
46 > * `resolveSdkTarget(pkg)`) into the template to get the per-target
47 > * tarball URL. This shape supports macOS Universal builds, where the
48 > * same `product.json` is shared by arm64 and x64 launches.
49 > */
50 > export interface IAgentSdkPackage {
51 > /** Key under `product.agentSdks` — e.g. `'claude'`, `'codex'`. */
52 > readonly id: string;
53 > /**
54 > * Brand display name for user-facing progress, e.g. `'Claude'`, `'Codex'`.
55 > * The downloader puts this on {@link IAgentSdkDownloadProgress.displayName}
56 > * so clients can build a localized "Downloading {displayName} agent" label.
57 > */
58 > readonly displayName: string;
59 > /** Env var that, when set, becomes the SDK root and short-circuits the download. */
60 > readonly devOverrideEnvVar: string;
61 > /**
62 > * True iff this SDK publishes separate `linux-{x64,arm64}-musl`
63 > * packages alongside the glibc default. Claude does; Codex doesn't
64 > * (its Linux binary is statically musl-linked and runs on both).
65 > */
66 > readonly hasSeparateMuslLinuxPackage: boolean;
67 > }
68 >
69 > /**
70 > * Per-host info used by `resolveSdkTarget`. Defaulted from the running
71 > * process; tests inject synthetic values to exercise targets the test
72 > * host doesn't actually run on (Universal-launch case, musl, etc.).
73 > */
74 > export interface ISdkTargetHost {
75 > readonly platform: NodeJS.Platform;
76 > readonly arch: string;
77 > readonly libc: LibcFamily | undefined;
78 > }
79 >
80 > const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
81 > const SUPPORTED_ARCHES = new Set<string>(['x64', 'arm64']);
82 >
83 > /**
84 > * Resolves the build's `sdkTarget` suffix for the given host. Defaults
85 > * to the current Node process — production callers omit `host`; tests
86 > * pass a synthetic host to cover targets the test machine can't reach
87 > * (Universal launches from a single-arch host, musl Linux on macOS CI,
88 > * etc.).
89 > *
90 > * - claude on glibc Linux: `linux-x64` / `linux-arm64`
91 > * - claude on musl Linux: `linux-x64-musl` / `linux-arm64-musl`
92 > * - codex Linux (any libc): `linux-x64` / `linux-arm64`
93 > * - everywhere else: `<platform>-<arch>`
94 > *
95 > * Returns `undefined` when no SDK applies (`armhf`, web, etc.); the
96 > * downloader treats that the same as "no product config" and never
97 > * registers the provider.
98 > *
99 > * Mirror of the build pipeline's `getSdkTargetForBuild` (in
100 > * `build/agent-sdk/common.ts`) translated from build-time
101 > * `vscodePlatform` to runtime `process.platform` + libc detection.
102 > * Keep the two in sync when adding new target SKUs.
103 > */
104 > export function resolveSdkTarget(
105 > pkg: Pick<IAgentSdkPackage, 'hasSeparateMuslLinuxPackage'>, agentSdkDownloader.ts ×2
106 > host: ISdkTargetHost = { platform: process.platform, arch: process.arch, libc: detectLibcSync() },
107 > ): string | undefined {
108 > if (!SUPPORTED_PLATFORMS.has(host.platform) || !SUPPORTED_ARCHES.has(host.arch)) {
109 > return undefined; agentSdkDownloader.ts ×1
110 > }
111 > if (host.platform === 'linux' && pkg.hasSeparateMuslLinuxPackage && host.libc === 'musl') { agentSdkDownloader.ts ×2
112 > return `linux-${host.arch}-musl`; agentSdkDownloader.ts ×1
113 > }
114 > return `${host.platform}-${host.arch}`; agentSdkDownloader.ts ×1
115 > }
117 > // #endregion
118 >
119 > // #region Service decorator
120 >
121 > export const IAgentSdkDownloader = createDecorator<IAgentSdkDownloader>('agentSdkDownloader');
122 >
123 > /** Lifecycle phase of a single SDK download (downloader-internal). */
124 > export type AgentSdkDownloadPhase = 'started' | 'progress' | 'completed' | 'failed';
125 >
126 > /**
127 > * A process-global download-progress sample fired on
128 > * {@link IAgentSdkDownloader.onDidDownloadProgress}. The downloader owns the
129 > * lifecycle: one `started`, throttled `progress` frames, then exactly one
130 > * terminal `completed` / `failed` — all sharing a `downloadId`. Concurrent
131 > * `loadSdkRoot` callers for the same tarball are deduped, so they observe one
132 > * shared download (one `downloadId`).
133 > */
134 > export interface IAgentSdkDownloadProgress {
135 > /** Stable id for one download; coalesces frames and distinguishes concurrent fetches. */
136 > readonly downloadId: string;
137 > /** Package id, e.g. `'claude'` / `'codex'`. */
138 > readonly packageId: string;
139 > /** Brand display name, e.g. `'Claude'`. */
140 > readonly displayName: string;
141 > /** Lifecycle phase of this frame. */
142 > readonly phase: AgentSdkDownloadPhase;
143 > /** Bytes written so far. Monotonically non-decreasing within a `downloadId`. */
144 > readonly receivedBytes: number;
145 > /** Total bytes from `Content-Length`, or `undefined` when unknown (indeterminate). */
146 > readonly totalBytes: number | undefined;
147 > /** Short, non-localized failure reason; present only when `phase: 'failed'`. */
148 > readonly error?: string;
149 > }
150 >
151 > export interface IAgentSdkDownloader {
152 > readonly _serviceBrand: undefined;
153 >
154 > /**
155 > * Fires while a tarball is being fetched (cold cache only): one `started`,
156 > * throttled `progress` samples, then one terminal `completed` / `failed`.
157 > * Never fires for dev-override or cache-hit resolutions (no bytes move).
158 > * Process-global so a single subscriber (the protocol server) can forward
159 > * progress to clients regardless of which session triggered the fetch.
160 > */
161 > readonly onDidDownloadProgress: Event<IAgentSdkDownloadProgress>;
162 >
163 > /**
164 > * Returns the absolute path of the SDK root directory — the directory that
165 > * contains the package's `node_modules/` subtree. Callers resolve the
166 > * package-specific entrypoint from there themselves.
167 > *
168 > * Resolution order:
169 > * 1. dev-override env var (returned unchanged)
170 > * 2. on-disk cache hit (`.complete` sentinel present)
171 > * 3. download from `product.agentSdks?.[pkg.id]` with
172 > * `{sdkTarget}` substituted into the urlTemplate
173 > *
174 > * Repeated failures are latched for {@link LOAD_FAILURE_NEGATIVE_CACHE_MS}
175 > * so a misconfigured CDN doesn't get hammered on every SDK method call.
176 > */
177 > loadSdkRoot(pkg: IAgentSdkPackage, token: CancellationToken): Promise<string>;
178 >
179 > /**
180 > * Cheap, synchronous gate used at startup to decide whether to register
181 > * the corresponding agent provider. True iff the dev override is set, OR
182 > * (`product.agentSdks?.[pkg.id]` is populated AND `pkg.currentSdkTarget()`
183 > * resolves — i.e. an SDK exists for this host). Does NOT trigger a
184 > * download.
185 > */
186 > isAvailable(pkg: IAgentSdkPackage): boolean;
187 >
188 > /**
189 > * True iff {@link loadSdkRoot} would resolve WITHOUT a network download —
190 > * the dev override is set, or a completed cache for the configured version
191 > * already exists on disk. False when product config is present but the
192 > * cache is cold (a fetch would be required), and false when neither an
193 > * override nor product config is configured.
194 > *
195 > * Performs at most a single sentinel `exists` check and never downloads.
196 > * Eager / background callers (e.g. a provider listing its sessions at
197 > * startup) use this to avoid kicking off a multi-second cold download
198 > * before the user has asked for anything.
199 > */
200 > isSdkResolvableWithoutDownload(pkg: IAgentSdkPackage): Promise<boolean>;
201 > }
202 >
203 > // #endregion
204 >
205 > // #region Implementation
206 >
207 > /** How long a `loadSdkRoot` failure latches before we try again. */
208 > const LOAD_FAILURE_NEGATIVE_CACHE_MS = 30_000;
209 >
210 > /**
211 > * Minimum gap between download-progress samples. A 70-95MB tarball over a fast
212 > * link produces thousands of chunks; without throttling we'd flood the progress
213 > * channel. ~250ms keeps the percentage visibly moving without spamming.
214 > */
215 > const PROGRESS_EMIT_THROTTLE_MS = 250;
216 >
217 > /**
218 > * Parses a `Content-Length` header into a positive integer byte count, or
219 > * `undefined` when the header is absent, an array, or not a clean integer.
220 > */
221 > function parseContentLength(header: string | string[] | undefined): number | undefined { diskFileSystemProvider.ts ×20
222 > if (typeof header !== 'string' || !/^\d+$/.test(header)) {
223 return undefined;
224 }
225 > const parsed = parseInt(header, 10); diskFileSystemProvider.ts ×20
226 > return parsed > 0 ? parsed : undefined;
227 > }
229 > export class AgentSdkDownloader extends Disposable implements IAgentSdkDownloader {
230 > declare readonly _serviceBrand: undefined;
231 >
232 > private readonly _onDidDownloadProgress = this._register(new Emitter<IAgentSdkDownloadProgress>());
233 > readonly onDidDownloadProgress: Event<IAgentSdkDownloadProgress> = this._onDidDownloadProgress.event;
234 >
235 > /**
236 > * In-flight downloads keyed by the destination `cacheDir` (which
237 > * already encodes `<pkg>/<sdkVersion>/<sdkTarget>`). Concurrent
238 > * `loadSdkRoot` calls in the same process share the same promise so
239 > * we never download the same tarball twice. Universal launches that
240 > * resolve to different targets get distinct entries because their
241 > * cacheDirs differ.
242 > */
243 > private readonly _pendingDownloads = new Map<string, Promise<string>>();
244 >
245 > /**
246 > * Negative cache: most recent failure per package id, with an expiry.
247 > * While within the window, `loadSdkRoot` re-throws the cached error
248 > * immediately instead of re-attempting the download. Without this, a
249 > * broken CDN causes every SDK method call (poll-driven UIs hit this
250 > * hard) to fire a fresh request.
251 > *
252 > * Keyed by `pkg.id` (not the finer cacheDir): CDN failures are
253 > * effectively global per SDK (DNS, proxy auth, 5xx) and per-target
254 > * latching wouldn't protect against the actual failure modes — the
255 > * broader latch is intentional.
256 > */
257 > private readonly _failureLatch = new Map<string, { error: Error; expiresAt: number }>();
258 >
259 > constructor(
260 > @INativeEnvironmentService private readonly _environmentService: INativeEnvironmentService, agentSdkDownloader.ts ×1
261 > @IProductService private readonly _productService: IProductService,
262 > @IRequestService private readonly _requestService: IRequestService,
263 > @IFileService private readonly _fileService: IFileService,
264 > @ILogService private readonly _logService: ILogService,
265 > ) {
266 > super();
267 > }
269 > isAvailable(pkg: IAgentSdkPackage): boolean {
270 > if (process.env[pkg.devOverrideEnvVar]) { agentSdkDownloader.ts ×2
271 > return true; agentSdkDownloader.ts ×1
272 > }
273 > return !!this._productService.agentSdks?.[pkg.id] && resolveSdkTarget(pkg) !== undefined; agentSdkDownloader.ts ×2
274 > }
276 > async isSdkResolvableWithoutDownload(pkg: IAgentSdkPackage): Promise<boolean> {
277 if (process.env[pkg.devOverrideEnvVar]) {
278 return true;
279 }
280 const config = this._productService.agentSdks?.[pkg.id];
281 if (!config) {
282 return false;
283 }
284 const sdkTarget = resolveSdkTarget(pkg);
285 if (!sdkTarget) {
286 return false;
287 }
288 const sentinel = URI.joinPath(URI.file(this._cacheDir(pkg.id, config.version, sdkTarget)), '.complete');
289 return this._fileService.exists(sentinel);
290 }
292 > async loadSdkRoot(pkg: IAgentSdkPackage, token: CancellationToken): Promise<string> {
293 > // 1. Dev override. agentSdkDownloader.ts ×3
294 > const override = process.env[pkg.devOverrideEnvVar];
295 > if (override) {
296 > this._logService.info(`[AgentSdkDownloader] ${pkg.id}: using dev override at ${override}`); agentSdkDownloader.ts ×1
297 > return override;
298 > }
300 > // 2. Negative cache: a recent failure short-circuits without I/O.
301 > const latched = this._failureLatch.get(pkg.id);
302 > if (latched && latched.expiresAt > Date.now()) { agentSdkDownloader.ts ×3
303 throw latched.error;
304 }
306 > try {
307 > const root = await this._resolveOrDownload(pkg, token);
308 > this._failureLatch.delete(pkg.id); agentSdkDownloader.ts ×1
309 > return root;
310 > } catch (err) { agentSdkDownloader.ts ×5
311 > if (token.isCancellationRequested) { agentSdkDownloader.ts ×2
312 > // Don't latch cancellations — user intent, not a real failure. agentSdkDownloader.ts ×3
313 > throw err;
314 > }
315 > const error = err instanceof Error ? err : new Error(String(err)); agentSdkDownloader.ts ×2
316 > this._failureLatch.set(pkg.id, {
317 > error,
318 > expiresAt: Date.now() + LOAD_FAILURE_NEGATIVE_CACHE_MS,
319 > });
320 > throw error;
321 > }
324 > private async _resolveOrDownload(pkg: IAgentSdkPackage, token: CancellationToken): Promise<string> {
325 > const config = this._productService.agentSdks?.[pkg.id]; agentSdkDownloader.ts ×5
326 > if (!config) {
327 > throw new Error( agentSdkDownloader.ts ×1
328 > `Cannot load ${pkg.id} SDK: no \`product.agentSdks.${pkg.id}\` configured and ` +
329 > `no ${pkg.devOverrideEnvVar} dev override set.`,
330 > );
331 > }
332 > const sdkTarget = resolveSdkTarget(pkg); agentSdkDownloader.ts ×2
333 > if (!sdkTarget) {
334 throw new Error(
335 `Cannot load ${pkg.id} SDK: no SDK target for this host ` +
336 `(${process.platform}/${process.arch}). ` +
337 `Set ${pkg.devOverrideEnvVar} to a local SDK root to bypass.`,
338 );
339 }
340 > const url = format2(config.urlTemplate, { sdkTarget }); agentSdkDownloader.ts ×2
341 > // `format2` leaves unknown `{placeholder}` segments untouched; catch
342 > // vscode-distro typos like `{sdkTaret}` here instead of letting the
343 > // CDN return a 404 against a clearly-broken URL.
344 > const stray = /{[^}]+}/.exec(url);
345 > if (stray) {
346 > throw new Error( agentSdkDownloader.ts ×1
347 > `Cannot load ${pkg.id} SDK: \`product.agentSdks.${pkg.id}.urlTemplate\` ` +
348 > `contains an unknown placeholder ${stray[0]} — only {sdkTarget} is substituted. ` +
349 > `Template: ${config.urlTemplate}`,
350 > );
351 > }
353 > const cacheDir = this._cacheDir(pkg.id, config.version, sdkTarget);
354 > const sentinel = URI.joinPath(URI.file(cacheDir), '.complete');
355 >
356 > // `.complete`'s mere presence is the integrity signal — extracts
357 > // that crashed mid-way never write it. See `_download` for why
358 > // the sentinel is written inside the tmp dir before the rename.
359 > if (await this._fileService.exists(sentinel)) {
360 > return cacheDir; agentSdkDownloader.ts ×1
361 > }
363 > // Download (deduped across concurrent callers in the same process).
364 > // cacheDir is already unique per (pkg, version, sdkTarget) — within
365 > // a single downloader instance userDataPath is fixed, so it serves
366 > // as the dedup key without an extra string allocation.
367 > let pending = this._pendingDownloads.get(cacheDir);
368 > if (!pending) {
369 > pending = this._download(pkg, url, cacheDir, sentinel, token).finally(() => {
370 > this._pendingDownloads.delete(cacheDir);
371 > });
372 > this._pendingDownloads.set(cacheDir, pending);
373 > }
374 > return pending;
377 > private _cacheDir(packageId: string, sdkVersion: string, sdkTarget: string): string {
378 > // `sdkTarget` is in the path so macOS Universal builds keep two agentSdkDownloader.ts ×2
379 > // independent caches — one per resolved target — instead of
380 > // thrashing a single shared one as launches alternate.
381 > return path.join(
382 > this._environmentService.userDataPath,
383 > 'agent-host',
384 > 'sdk-cache',
385 > packageId,
386 > sdkVersion,
387 > sdkTarget,
388 > );
389 > }
391 > private async _download(
392 > pkg: IAgentSdkPackage, diskFileSystemProvider.ts ×20
393 > url: string,
394 > cacheDir: string,
395 > sentinel: URI,
396 > token: CancellationToken,
397 > ): Promise<string> {
398 > this._logService.info(`[AgentSdkDownloader] ${pkg.id}: downloading from ${url}`);
399 > const start = Date.now();
400 > const parent = path.dirname(cacheDir);
401 > await this._fileService.createFolder(URI.file(parent));
402 >
403 > // Extract to a per-pid scratch dir alongside the final cache dir, then
404 > // rename into place. If two windows of the same install race, the loser
405 > // catches the `move`'s `FILE_MOVE_CONFLICT`, checks the existing
406 > // .complete sentinel, and uses that instead — see the rename-loser
407 > // path below.
408 > const tmpDir = `${cacheDir}.tmp.${process.pid}`;
409 > const tmpDirUri = URI.file(tmpDir);
410 > await this._delIgnoringMissing(tmpDirUri);
411 > await this._fileService.createFolder(tmpDirUri);
412 >
413 > // Fire the download lifecycle on the process-global event so a single
414 > // subscriber (the protocol server) can forward it to clients. One
415 > // `started`, throttled `progress` from `_fetch`, then a terminal frame.
416 > const downloadId = generateUuid();
417 > let lastReceived = 0;
418 > let lastTotal: number | undefined;
419 > this._fireProgress(pkg, downloadId, 'started', 0, undefined);
420 >
421 > try {
422 > const tarballPath = path.join(tmpDir, 'sdk.tgz');
423 > await this._fetch(url, tarballPath, token, (receivedBytes, totalBytes) => {
424 > lastReceived = receivedBytes;
425 > lastTotal = totalBytes;
426 > this._fireProgress(pkg, downloadId, 'progress', receivedBytes, totalBytes);
427 > });
428 > await this._extractTarGz(tarballPath, tmpDir); diskFileSystemProvider.ts ×11
429 > await this._fileService.del(URI.file(tarballPath));
430 >
431 > // Write the `.complete` sentinel inside the tmp dir BEFORE the
432 > // move so the move atomically publishes a directory that
433 > // already carries its sentinel — a crash between move and
434 > // sentinel-write can't leave a wedged, sentinel-less cacheDir
435 > // behind. Content is intentionally empty: only existence
436 > // matters, and the cache dir path already encodes
437 > // `<pkg>/<version>/<sdkTarget>` for debugging.
438 > await this._fileService.writeFile(
439 > URI.joinPath(tmpDirUri, '.complete'),
440 > VSBuffer.fromString(''),
441 > );
442 >
443 > // Atomic publish of the completed extraction.
444 > try {
445 > await this._fileService.move(tmpDirUri, URI.file(cacheDir));
446 > } catch (err) {
447 if (await this._handleRenameLoser(err, sentinel, tmpDirUri)) {
448 this._logService.info(`[AgentSdkDownloader] ${pkg.id}: lost rename race, using existing cache`);
449 this._fireProgress(pkg, downloadId, 'completed', lastReceived, lastTotal);
450 return cacheDir;
451 }
452 throw err;
453 }
455 > const elapsed = Math.round((Date.now() - start) / 1000);
456 > this._logService.info(`[AgentSdkDownloader] ${pkg.id}: downloaded in ${elapsed}s`);
457 > this._fireProgress(pkg, downloadId, 'completed', lastTotal ?? lastReceived, lastTotal); diskFileSystemProvider.ts ×20
458 > return cacheDir;
459 > } catch (err) {
460 > await this._delIgnoringMissing(tmpDirUri); agentSdkDownloader.ts ×3
461 > if (token.isCancellationRequested) {
462 > this._fireProgress(pkg, downloadId, 'failed', lastReceived, lastTotal, 'cancelled');
463 > throw new CancellationError();
464 > }
465 > const message = err instanceof Error ? err.message : String(err);
466 > this._fireProgress(pkg, downloadId, 'failed', lastReceived, lastTotal, message);
467 > throw new Error(
468 > `Failed to download ${pkg.id} SDK from ${url} ` +
469 > `(cache target: ${cacheDir}). ` +
470 > `Set ${pkg.devOverrideEnvVar} to a local SDK root to bypass. ` +
471 > `Cause: ${message}`,
472 > );
473 > }
476 > private _fireProgress(
477 > pkg: IAgentSdkPackage, diskFileSystemProvider.ts ×20
478 > downloadId: string,
479 > phase: AgentSdkDownloadPhase,
480 > receivedBytes: number,
481 > totalBytes: number | undefined,
482 > error?: string,
483 > ): void {
484 > this._onDidDownloadProgress.fire({
485 > downloadId,
486 > packageId: pkg.id,
487 > displayName: pkg.displayName,
488 > phase,
489 > receivedBytes,
490 > totalBytes,
491 > ...(error !== undefined ? { error } : {}),
492 > });
493 > }
495 > private async _handleRenameLoser(
496 err: unknown,
497 sentinel: URI,
498 tmpDirUri: URI,
499 ): Promise<boolean> {
500 // `IFileService.move` with default (overwrite: false) throws a
501 // FileOperationError with FILE_MOVE_CONFLICT when the target exists.
502 // Anything else is a real error.
503 if (!(err instanceof FileOperationError) || err.fileOperationResult !== FileOperationResult.FILE_MOVE_CONFLICT) {
504 return false;
505 }
506 if (!(await this._fileService.exists(sentinel))) {
507 return false;
508 }
509 // Winner already published a complete cache. Drop our scratch dir.
510 await this._delIgnoringMissing(tmpDirUri);
511 return true;
512 }
514 > private async _fetch(
515 > url: string, diskFileSystemProvider.ts ×20
516 > dest: string,
517 > token: CancellationToken,
518 > onBytes?: (receivedBytes: number, totalBytes: number | undefined) => void,
519 > ): Promise<void> {
520 > // Delegate to IRequestService (corporate proxy, strictSSL, kerberos,
521 > // retries, redirect follow). `fs.createWriteStream` (not
522 > // `IFileService.writeFile`) so that cancelling a multi-MB download
523 > // aborts promptly via destroy(). Manual pipe (not `stream.pipeline`)
524 > // because the source is a VSBufferReadableStream — not a Node
525 > // Readable — so node-stream utilities can't introspect it.
526 > if (token.isCancellationRequested) {
527 throw new CancellationError();
528 }
529 > const context: IRequestContext = await this._requestService.request({ diskFileSystemProvider.ts ×20
530 > url,
531 > type: 'GET',
532 > callSite: 'agentSdkDownloader',
533 > }, token);
534 > if (token.isCancellationRequested) {
535 context.stream.destroy();
536 throw new CancellationError();
537 }
539 > const statusCode = context.res.statusCode ?? 0;
540 > if (statusCode < 200 || statusCode >= 300) {
541 context.stream.destroy();
542 throw new Error(`HTTP ${statusCode} fetching ${url}`);
543 }
545 > // The CDN sends `Content-Length` for these static tarballs, which lets
546 > // us report determinate percentage progress. A missing/garbled header
547 > // degrades gracefully to an indeterminate (byte-count only) report.
548 > const totalBytes = parseContentLength(context.res.headers['content-length']);
549 >
550 > await new Promise<void>((resolve, reject) => {
551 > const out = fs.createWriteStream(dest);
552 > let settled = false;
553 > // Throttle progress so a fast link doesn't fire thousands of
554 > // samples. The first chunk always passes (lastEmit starts at 0)
555 > // and 'end' forces a final sample, so consumers see a start and a
556 > // 100% finish regardless of chunk timing.
557 > let receivedBytes = 0;
558 > let lastEmitTime = 0;
559 > const emitBytes = (force: boolean) => {
560 > if (!onBytes) {
561 return;
562 }
563 > const now = Date.now(); diskFileSystemProvider.ts ×20
564 > if (!force && now - lastEmitTime < PROGRESS_EMIT_THROTTLE_MS) {
565 return;
566 }
567 > lastEmitTime = now; diskFileSystemProvider.ts ×20
568 > onBytes(receivedBytes, totalBytes);
569 > };
570 > const settleResolve = () => {
571 > if (settled) { return; } diskFileSystemProvider.ts ×11
572 > settled = true;
573 > cancelSub.dispose();
574 > resolve();
575 > };
576 > const settleReject = (err: unknown) => { diskFileSystemProvider.ts ×20
577 > if (settled) { return; } agentSdkDownloader.ts ×3
578 > settled = true;
579 > cancelSub.dispose();
580 > context.stream.destroy();
581 > out.destroy();
582 > reject(err);
583 > };
584 > const cancelSub = token.onCancellationRequested(() => settleReject(new CancellationError())); diskFileSystemProvider.ts ×20
585 > out.on('error', settleReject);
586 > out.on('finish', settleResolve);
587 > // Backpressure: tarballs are 70-95MB; if the disk is slower
588 > // than the network (Windows AV scan, network home dir, …) an
589 > // unthrottled pipe buffers the whole thing in memory. Pause the
590 > // source when the sink's internal buffer hits highWaterMark and
591 > // resume on 'drain'.
592 > out.on('drain', () => context.stream.resume());
593 > context.stream.on('data', chunk => {
594 > receivedBytes += chunk.byteLength;
595 > emitBytes(false);
596 > if (!out.write(chunk.buffer)) {
597 context.stream.pause();
598 }
600 > context.stream.on('end', () => {
601 > emitBytes(true); diskFileSystemProvider.ts ×11
602 > out.end();
604 > context.stream.on('error', settleReject);
605 > });
606 > }
608 > private async _extractTarGz(tarball: string, dest: string): Promise<void> {
609 > // `tar` (node-tar) is pure JS — works on every platform the agent host diskFileSystemProvider.ts ×11
610 > // runs on without depending on a system `tar` binary.
611 > await tar.x({ file: tarball, cwd: dest });
612 > }
614 > private async _delIgnoringMissing(uri: URI): Promise<void> {
616 > await this._fileService.del(uri, { recursive: true });
617 > } catch (err) {
618 > // `force: true` behaviour: missing path is a no-op.
619 > if (toFileOperationResult(err as Error) !== FileOperationResult.FILE_NOT_FOUND) {
620 throw err;
621 }
623 > }
625 >
626 > // #endregion