From a57d55d3b7b809800a2eb03abb8b29f54a07c9dd Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 4 Dec 2017 20:59:54 +0100 Subject: [PATCH] Validate extension manifest cache lazily --- .../electron-browser/extensionPoints.ts | 12 +- .../electron-browser/extensionService.ts | 115 ++++++++++++++---- 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts index ea753d7c504..d0582c6dc86 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionPoints.ts @@ -359,9 +359,15 @@ export class ExtensionScanner { } const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - const extensionDescriptions = await TPromise.join(folders.map(f => this.scanExtension(input.ourVersion, log, join(absoluteFolderPath, f), isBuiltin, nlsConfig))); - return extensionDescriptions.filter(item => item !== null); - + let extensionDescriptions = await TPromise.join(folders.map(f => this.scanExtension(input.ourVersion, log, join(absoluteFolderPath, f), isBuiltin, nlsConfig))); + extensionDescriptions = extensionDescriptions.filter(item => item !== null); + extensionDescriptions.sort((a, b) => { + if (a.extensionFolderPath < b.extensionFolderPath) { + return -1; + } + return 1; + }); + return extensionDescriptions; } catch (err) { log.error(absoluteFolderPath, err); return []; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index ba24ef48185..861416abc24 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -6,6 +6,7 @@ import * as nls from 'vs/nls'; import * as errors from 'vs/base/common/errors'; +import * as objects from 'vs/base/common/objects'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import pkg from 'vs/platform/node/package'; @@ -19,7 +20,7 @@ import { IExtensionEnablementService, IExtensionIdentifier, EnablementState } fr import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/platform/extensions/common/extensionsRegistry'; import { ExtensionScanner, ILog, ExtensionScannerInput } from 'vs/workbench/services/extensions/electron-browser/extensionPoints'; -import { IMessageService } from 'vs/platform/message/common/message'; +import { IMessageService, CloseAction } from 'vs/platform/message/common/message'; import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadService'; import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -38,6 +39,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/ import { Barrier } from 'vs/base/common/async'; import Event, { Emitter } from 'vs/base/common/event'; import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler'; +import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions')); @@ -380,7 +382,7 @@ export class ExtensionService extends Disposable implements IExtensionService { this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message); }); - return ExtensionService._scanInstalledExtensions(this._environmentService, log) + return ExtensionService._scanInstalledExtensions(this._instantiationService, this._messageService, this._environmentService, log) .then(({ system, user, development }) => { this._extensionEnablementService.migrateToIdentifiers(user); // TODO: @sandy Remove it after couple of milestones return this._extensionEnablementService.getDisabledExtensions() @@ -468,46 +470,104 @@ export class ExtensionService extends Disposable implements IExtensionService { } } - private static async _scanExtensionsCached(environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise { + private static async _validateExtensionsCache(instantiationService: IInstantiationService, messageService: IMessageService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise { + const cacheFolder = path.join(environmentService.userDataPath, 'CachedExtensions'); + const cacheFile = path.join(cacheFolder, cacheKey); + + const expected = await ExtensionScanner.scanExtensions(input, new NullLogger()); + + const cacheContents = await this._readExtensionCache(environmentService, cacheKey); + const actual = cacheContents.result; + + if (objects.equals(expected, actual)) { + // Cache is valid and running with it is perfectly fine... + return; + } + + try { + await pfs.del(cacheFile); + } catch (err) { + errors.onUnexpectedError(err); + console.error(err); + } + + let message = nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."); + messageService.show(Severity.Info, { + message: message, + actions: [ + instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, ReloadWindowAction.LABEL), + CloseAction + ] + }); + } + + private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise { const cacheFolder = path.join(environmentService.userDataPath, 'CachedExtensions'); const cacheFile = path.join(cacheFolder, cacheKey); try { const cacheRawContents = await pfs.readFile(cacheFile, 'utf8'); - const cacheContents: IExtensionCacheData = JSON.parse(cacheRawContents); - if (ExtensionScannerInput.equals(cacheContents.input, input)) { - // TODO: async validate cache!!! - return cacheContents.result; - } + return JSON.parse(cacheRawContents); } catch (err) { // That's ok... } + return null; + } + + private static async _writeExtensionCache(environmentService: IEnvironmentService, cacheKey: string, cacheContents: IExtensionCacheData): TPromise { + const cacheFolder = path.join(environmentService.userDataPath, 'CachedExtensions'); + const cacheFile = path.join(cacheFolder, cacheKey); + + try { + await pfs.mkdirp(cacheFolder); + } catch (err) { + // That's ok... + } + + try { + await pfs.writeFile(cacheFile, JSON.stringify(cacheContents)); + } catch (err) { + // That's ok... + } + } + + private static async _scanExtensionsWithCache(instantiationService: IInstantiationService, messageService: IMessageService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise { + const cacheContents = await this._readExtensionCache(environmentService, cacheKey); + if (cacheContents && ExtensionScannerInput.equals(cacheContents.input, input)) { + // Validate the cache asynchronously after 5s + setTimeout(async () => { + try { + await this._validateExtensionsCache(instantiationService, messageService, environmentService, cacheKey, input); + } catch (err) { + errors.onUnexpectedError(err); + } + }, 5000); + return cacheContents.result; + } + const counterLogger = new CounterLogger(log); const result = await ExtensionScanner.scanExtensions(input, counterLogger); if (!true && counterLogger.errorCnt === 0) { // Nothing bad happened => cache the result - try { - const cacheContents: IExtensionCacheData = { - input: input, - result: result - }; - await pfs.mkdirp(cacheFolder); - await pfs.writeFile(cacheFile, JSON.stringify(cacheContents)); - } catch (err) { - // That's ok... - } + const cacheContents: IExtensionCacheData = { + input: input, + result: result + }; + await this._writeExtensionCache(environmentService, cacheKey, cacheContents); } return result; } - private static _scanInstalledExtensions(environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { + private static _scanInstalledExtensions(instantiationService: IInstantiationService, messageService: IMessageService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> { const version = pkg.version; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.locale; - const builtinExtensions = this._scanExtensionsCached( + const builtinExtensions = this._scanExtensionsWithCache( + instantiationService, + messageService, environmentService, 'builtin', new ExtensionScannerInput(version, locale, devMode, SystemExtensionsRoot, true), @@ -517,7 +577,9 @@ export class ExtensionService extends Disposable implements IExtensionService { const userExtensions = ( environmentService.disableExtensions || !environmentService.extensionsPath ? TPromise.as([]) - : this._scanExtensionsCached( + : this._scanExtensionsWithCache( + instantiationService, + messageService, environmentService, 'user', new ExtensionScannerInput(version, locale, devMode, environmentService.extensionsPath, false), @@ -647,7 +709,7 @@ export class Logger implements ILog { } } -export class CounterLogger implements ILog { +class CounterLogger implements ILog { public errorCnt = 0; public warnCnt = 0; @@ -668,3 +730,12 @@ export class CounterLogger implements ILog { this._actual.info(source, message); } } + +class NullLogger implements ILog { + public error(source: string, message: string): void { + } + public warn(source: string, message: string): void { + } + public info(source: string, message: string): void { + } +}