Path: blob/main/extensions/copilot/src/platform/networking/vscode-node/fetcherServiceImpl.ts
13400 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter } from '../../../util/vs/base/common/event';6import { Disposable } from '../../../util/vs/base/common/lifecycle';7import { Config, ConfigKey, ExperimentBasedConfig, ExperimentBasedConfigType, IConfigurationService } from '../../configuration/common/configurationService';8import { INTEGRATION_ID } from '../../endpoint/common/licenseAgreement';9import { IEnvService } from '../../env/common/envService';10import { ILogService } from '../../log/common/logService';11import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';12import { ITelemetryService } from '../../telemetry/common/telemetry';13import { FetchEvent, FetchOptions, FetchTelemetryEvent, IAbortController, IFetcherService, NO_FETCH_TELEMETRY, PaginationOptions, ReportFetchEvent, Response, safeGetHostname, WebSocketConnection, WebSocketConnectOptions } from '../common/fetcherService';14import { IFetcher } from '../common/networking';15import { fetchWithFallbacks } from '../node/fetcherFallback';16import { NodeFetcher } from '../node/nodeFetcher';17import { createWebSocket, NodeFetchFetcher } from '../node/nodeFetchFetcher';18import { ElectronFetcher } from './electronFetcher';1920export class FetcherService extends Disposable implements IFetcherService {2122declare readonly _serviceBrand: undefined;23private _availableFetchers: readonly IFetcher[] | undefined;24private _knownBadFetchers = new Set<string>();25private _experimentationService: IExperimentationService | undefined;26private _telemetryService: ITelemetryService | undefined;27private readonly _onDidFetch = this._register(new Emitter<FetchEvent>());28readonly onDidFetch = this._onDidFetch.event;29private readonly _onDidCompleteFetch = this._register(new Emitter<FetchTelemetryEvent>());30readonly onDidCompleteFetch = this._onDidCompleteFetch.event;3132constructor(33fetcher: IFetcher | undefined,34@ILogService private readonly _logService: ILogService,35@IEnvService private readonly _envService: IEnvService,36@IConfigurationService private readonly _configurationService: IConfigurationService,37) {38super();39this._availableFetchers = fetcher ? [fetcher] : undefined;40}4142async fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {43const items: T[] = [];44const pageSize = options.pageSize ?? 20;45let page = options.startPage ?? 1;46let hasNextPage = false;4748do {49const url = options.buildUrl(baseUrl, pageSize, page);50const response = await this.fetch(url, options);5152if (!response.ok) {53// Return what we've collected so far if request fails54return items;55}5657const data = await response.json();58const pageItems = options.getItemsFromResponse(data);59items.push(...pageItems);6061hasNextPage = pageItems.length === pageSize;62page++;63} while (hasNextPage);6465return items;66}6768setExperimentationService(experimentationService: IExperimentationService) {69this._experimentationService = experimentationService;70}7172setTelemetryService(telemetryService: ITelemetryService) {73this._telemetryService = telemetryService;74}7576private _getAvailableFetchers(): readonly IFetcher[] {77if (!this._availableFetchers) {78if (!this._experimentationService) {79this._logService.info('FetcherService: Experimentation service not available yet, using default fetcher configuration.');80} else {81this._logService.debug('FetcherService: Using experimentation service to determine fetcher configuration.');82}83this._availableFetchers = this._getFetchers(this._configurationService, this._experimentationService, this._envService);84}85return this._availableFetchers;86}8788private _getFetchers(configurationService: IConfigurationService, experimentationService: IExperimentationService | undefined, envService: IEnvService): IFetcher[] {89const reportEvent: ReportFetchEvent = e => this._onDidFetch.fire(e);90const useElectronFetcher = getShadowedConfig<boolean>(configurationService, experimentationService, ConfigKey.Shared.DebugUseElectronFetcher, ConfigKey.TeamInternal.DebugExpUseElectronFetcher);91const electronFetcher = ElectronFetcher.create(envService, reportEvent);92const useNodeFetcher = !(useElectronFetcher && electronFetcher) && getShadowedConfig<boolean>(configurationService, experimentationService, ConfigKey.Shared.DebugUseNodeFetcher, ConfigKey.TeamInternal.DebugExpUseNodeFetcher); // Node https wins over Node fetch. (historical order)93const useNodeFetchFetcher = !(useElectronFetcher && electronFetcher) && !useNodeFetcher && getShadowedConfig<boolean>(configurationService, experimentationService, ConfigKey.Shared.DebugUseNodeFetchFetcher, ConfigKey.TeamInternal.DebugExpUseNodeFetchFetcher);9495const fetchers = [];96if (electronFetcher) {97fetchers.push(electronFetcher);98}99if (useElectronFetcher) {100if (electronFetcher) {101this._logService.info(`Using the Electron fetcher.`);102} else {103this._logService.info(`Can't use the Electron fetcher in this environment.`);104}105}106107// Node fetch preferred over Node https in fallbacks. (HTTP2 support)108const nodeFetchFetcher = new NodeFetchFetcher(envService, reportEvent);109if (useNodeFetchFetcher) {110this._logService.info(`Using the Node fetch fetcher.`);111fetchers.unshift(nodeFetchFetcher);112} else {113fetchers.push(nodeFetchFetcher);114}115116const nodeFetcher = new NodeFetcher(envService, reportEvent);117if (useNodeFetcher || (!(useElectronFetcher && electronFetcher) && !useNodeFetchFetcher)) { // Node https used when none is configured. (historical)118this._logService.info(`Using the Node fetcher.`);119fetchers.unshift(nodeFetcher);120} else {121fetchers.push(nodeFetcher);122}123124return fetchers;125}126127getUserAgentLibrary(): string {128return this._getAvailableFetchers()[0].getUserAgentLibrary();129}130131createWebSocket(url: string, options?: WebSocketConnectOptions): WebSocketConnection {132if (options?.headers) {133delete options.headers['Request-Hmac'];134options.headers['Copilot-Integration-Id'] = INTEGRATION_ID;135}136return createWebSocket(url, options);137}138139async fetch(url: string, options: FetchOptions): Promise<Response> {140const start = Date.now();141try {142const { response: res, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(this._getAvailableFetchers(), url, options, this._knownBadFetchers, this._configurationService, this._logService, this._telemetryService, this._experimentationService);143if (updatedFetchers) {144this._availableFetchers = updatedFetchers;145}146if (updatedKnownBadFetchers) {147this._knownBadFetchers = updatedKnownBadFetchers;148}149if (options.callSite !== NO_FETCH_TELEMETRY) {150this._onDidCompleteFetch.fire({151callSite: options.callSite,152hostname: safeGetHostname(url),153latencyMs: Date.now() - start,154statusCode: res.status,155success: res.ok,156});157}158return res;159} catch (err) {160// Apply fetcher demotion if fetchWithFallbacks detected a network process crash161const demotion = (err as any)?._fetcherDemotion;162if (demotion) {163if (demotion.updatedFetchers) {164this._availableFetchers = demotion.updatedFetchers;165}166if (demotion.updatedKnownBadFetchers) {167this._knownBadFetchers = demotion.updatedKnownBadFetchers;168}169}170if (options.callSite !== NO_FETCH_TELEMETRY) {171this._onDidCompleteFetch.fire({172callSite: options.callSite,173hostname: safeGetHostname(url),174latencyMs: Date.now() - start,175statusCode: undefined,176success: false,177});178}179throw err;180}181}182183disconnectAll(): Promise<unknown> {184return this._getAvailableFetchers()[0].disconnectAll();185}186makeAbortController(): IAbortController {187return this._getAvailableFetchers()[0].makeAbortController();188}189isAbortError(e: any): boolean {190return this._getAvailableFetchers()[0].isAbortError(e);191}192isInternetDisconnectedError(e: any): boolean {193return this._getAvailableFetchers()[0].isInternetDisconnectedError(e);194}195isFetcherError(e: any): boolean {196return !!e?.fetcherId || this._getAvailableFetchers().some(f => f.isFetcherError(e));197}198isNetworkProcessCrashedError(e: any): boolean {199return this._getAvailableFetchers().some(f => f.isNetworkProcessCrashedError(e));200}201getUserMessageForFetcherError(err: any): string {202// Use the fetcher that recognizes the error, falling back to the primary203const recognizing = this._getAvailableFetchers().find(f => f.isFetcherError(err));204return (recognizing ?? this._getAvailableFetchers()[0]).getUserMessageForFetcherError(err);205}206}207208export function getShadowedConfig<T extends ExperimentBasedConfigType>(configurationService: IConfigurationService, experimentationService: IExperimentationService | undefined, configKey: Config<T>, expKey: ExperimentBasedConfig<T | undefined>): T {209if (!experimentationService) {210return configurationService.getConfig<T>(configKey);211}212213const inspect = configurationService.inspectConfig<T>(configKey);214if (inspect?.globalValue !== undefined) {215return inspect.globalValue;216}217const expValue = configurationService.getExperimentBasedConfig(expKey, experimentationService);218if (expValue !== undefined) {219return expValue;220}221return configurationService.getConfig<T>(configKey);222}223224225