Path: blob/main/extensions/json-language-features/client/src/node/jsonClientMain.ts
3322 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 { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode';6import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient';7import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';89import { promises as fs } from 'fs';10import * as path from 'path';11import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-light';1213import TelemetryReporter from '@vscode/extension-telemetry';14import { JSONSchemaCache } from './schemaCache';1516let client: AsyncDisposable | undefined;1718// this method is called when vs code is activated19export async function activate(context: ExtensionContext) {20const clientPackageJSON = await getPackageInfo(context);21const telemetry = new TelemetryReporter(clientPackageJSON.aiKey);22context.subscriptions.push(telemetry);2324const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true });25context.subscriptions.push(logOutputChannel);2627const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`;28const serverModule = context.asAbsolutePath(serverMain);2930// The debug options for the server31const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (6000 + Math.round(Math.random() * 999))] };3233// If the extension is launch in debug mode the debug server options are use34// Otherwise the run options are used35const serverOptions: ServerOptions = {36run: { module: serverModule, transport: TransportKind.ipc },37debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }38};3940const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {41return new LanguageClient(id, name, serverOptions, clientOptions);42};4344const timer = {45setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {46const handle = setTimeout(callback, ms, ...args);47return { dispose: () => clearTimeout(handle) };48}49};5051// pass the location of the localization bundle to the server52process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? '';5354const schemaRequests = await getSchemaRequestService(context, logOutputChannel);5556client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel });57}5859export async function deactivate(): Promise<any> {60if (client) {61await client.dispose();62client = undefined;63}64}6566interface IPackageInfo {67name: string;68version: string;69aiKey: string;70main: string;71}7273async function getPackageInfo(context: ExtensionContext): Promise<IPackageInfo> {74const location = context.asAbsolutePath('./package.json');75try {76return JSON.parse((await fs.readFile(location)).toString());77} catch (e) {78console.log(`Problems reading ${location}: ${e}`);79return { name: '', version: '', aiKey: '', main: '' };80}81}8283const retryTimeoutInHours = 2 * 24; // 2 days8485async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise<SchemaRequestService> {86let cache: JSONSchemaCache | undefined = undefined;87const globalStorage = context.globalStorageUri;8889let clearCache: (() => Promise<string[]>) | undefined;90if (globalStorage.scheme === 'file') {91const schemaCacheLocation = path.join(globalStorage.fsPath, 'json-schema-cache');92await fs.mkdir(schemaCacheLocation, { recursive: true });9394const schemaCache = new JSONSchemaCache(schemaCacheLocation, context.globalState);95log.trace(`[json schema cache] initial state: ${JSON.stringify(schemaCache.getCacheInfo(), null, ' ')}`);96cache = schemaCache;97clearCache = async () => {98const cachedSchemas = await schemaCache.clearCache();99log.trace(`[json schema cache] cache cleared. Previously cached schemas: ${cachedSchemas.join(', ')}`);100return cachedSchemas;101};102}103104105const isXHRResponse = (error: any): error is XHRResponse => typeof error?.status === 'number';106107const request = async (uri: string, etag?: string): Promise<string> => {108const headers: Headers = {109'Accept-Encoding': 'gzip, deflate',110'User-Agent': `${env.appName} (${env.appHost})`111};112if (etag) {113headers['If-None-Match'] = etag;114}115try {116log.trace(`[json schema cache] Requesting schema ${uri} etag ${etag}...`);117118const response = await xhr({ url: uri, followRedirects: 5, headers });119if (cache) {120const etag = response.headers['etag'];121if (typeof etag === 'string') {122log.trace(`[json schema cache] Storing schema ${uri} etag ${etag} in cache`);123await cache.putSchema(uri, etag, response.responseText);124} else {125log.trace(`[json schema cache] Response: schema ${uri} no etag`);126}127}128return response.responseText;129} catch (error: unknown) {130if (isXHRResponse(error)) {131if (error.status === 304 && etag && cache) {132133log.trace(`[json schema cache] Response: schema ${uri} unchanged etag ${etag}`);134135const content = await cache.getSchema(uri, etag, true);136if (content) {137log.trace(`[json schema cache] Get schema ${uri} etag ${etag} from cache`);138return content;139}140return request(uri);141}142143let status = getErrorStatusDescription(error.status);144if (status && error.responseText) {145status = `${status}\n${error.responseText.substring(0, 200)}`;146}147if (!status) {148status = error.toString();149}150log.trace(`[json schema cache] Respond schema ${uri} error ${status}`);151152throw status;153}154throw error;155}156};157158return {159getContent: async (uri: string) => {160if (cache && /^https?:\/\/(json|www)\.schemastore\.org\//.test(uri)) {161const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours);162if (content) {163if (log.logLevel === LogLevel.Trace) {164log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`);165}166167return content;168}169}170return request(uri, cache?.getETag(uri));171},172clearCache173};174}175176177