diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index d124479b68d3e326090a8de85163904ab25a11cd..2779685fddc85c0d4b06a5b62922f3b1580331e5 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -17,7 +17,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { parseArgs } from 'vs/platform/environment/node/argv'; import product from 'vs/platform/node/product'; -import pkg from 'vs/platform/node/package'; import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -26,6 +25,7 @@ import { IWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/worksp import { IBackupMainService } from 'vs/platform/backup/common/backup'; import { ICommandAction } from 'vs/platform/actions/common/actions'; import { mark, exportEntries } from 'vs/base/common/performance'; +import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService'; export interface IWindowState { width?: number; @@ -97,6 +97,8 @@ export class CodeWindow implements ICodeWindow { private currentConfig: IWindowConfiguration; private pendingLoadConfig: IWindowConfiguration; + private marketplaceHeadersPromise: TPromise; + private touchBarGroups: Electron.TouchBarSegmentedControl[]; constructor( @@ -123,6 +125,9 @@ export class CodeWindow implements ICodeWindow { // macOS: touch bar support this.createTouchBar(); + // Request handling + this.handleMarketplaceRequests(); + // Eventing this.registerListeners(); } @@ -330,17 +335,21 @@ export class CodeWindow implements ICodeWindow { return this._readyState; } - private registerListeners(): void { - const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - const headers = { - 'X-Market-Client-Id': `VSCode ${pkg.version}`, - 'User-Agent': `VSCode ${pkg.version}`, - 'X-Market-User-Id': this.environmentService.machineUUID - }; + private handleMarketplaceRequests(): void { + + // Resolve marketplace headers + this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.environmentService); + // Inject headers when requests are incoming + const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => { - cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) }); + this.marketplaceHeadersPromise.done(headers => { + cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) }); + }); }); + } + + private registerListeners(): void { // Prevent loading of svgs this._win.webContents.session.webRequest.onBeforeRequest(null, (details, callback) => { diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index ef5353ab958c9f265f1ae412cd3e385ff33b2da8..d712c6c2eeddafaef95e1998fc1bb9801ea50833 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -78,7 +78,7 @@ export interface IEnvironmentService { appSettingsHome: string; appSettingsPath: string; appKeybindingsPath: string; - machineUUID: string; + settingsSearchBuildId: number; settingsSearchUrl: string; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index da69f155eca0b7a700a5df10db5d4626d46b727b..2f14fdb9a3b1b0996b3c44e9b4bc91d3394b9e14 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -8,9 +8,7 @@ import * as crypto from 'crypto'; import * as paths from 'vs/base/node/paths'; import * as os from 'os'; import * as path from 'path'; -import * as fs from 'fs'; import URI from 'vs/base/common/uri'; -import { generateUuid, isUUID } from 'vs/base/common/uuid'; import { memoize } from 'vs/base/common/decorators'; import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; @@ -131,27 +129,7 @@ export class EnvironmentService implements IEnvironmentService { get disableUpdates(): boolean { return !!this._args['disable-updates']; } get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; } - readonly machineUUID: string; - - constructor(private _args: ParsedArgs, private _execPath: string) { - const machineIdPath = path.join(this.userDataPath, 'machineid'); - - try { - this.machineUUID = fs.readFileSync(machineIdPath, 'utf8'); - - if (!isUUID(this.machineUUID)) { - throw new Error('Not a UUID'); - } - } catch (err) { - this.machineUUID = generateUuid(); - - try { - fs.writeFileSync(machineIdPath, this.machineUUID, 'utf8'); - } catch (err) { - // noop - } - } - } + constructor(private _args: ParsedArgs, private _execPath: string) { } } export function parseExtensionHostPort(args: ParsedArgs, isBuild: boolean): IExtensionHostDebugParams { diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index 1c25ef50b083d3bd94303d2d00495c91f11e34bd..e79adfe2cbce59d883b549b91e1285999d95a9cf 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { tmpdir } from 'os'; import * as path from 'path'; import { TPromise } from 'vs/base/common/winjs.base'; -import * as uuid from 'vs/base/common/uuid'; import { distinct } from 'vs/base/common/arrays'; import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -21,6 +20,9 @@ import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; import { isVersionValid } from 'vs/platform/extensions/node/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { readFile } from 'vs/base/node/pfs'; +import { writeFileAndFlushSync } from 'vs/base/node/extfs'; +import { generateUuid, isUUID } from 'vs/base/common/uuid'; interface IRawGalleryExtensionFile { assetType: string; @@ -313,7 +315,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { private extensionsGalleryUrl: string; - private readonly commonHTTPHeaders: { [key: string]: string; }; + private readonly commonHeadersPromise: TPromise<{ [key: string]: string; }>; constructor( @IRequestService private requestService: IRequestService, @@ -322,11 +324,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { ) { const config = product.extensionsGallery; this.extensionsGalleryUrl = config && config.serviceUrl; - this.commonHTTPHeaders = { - 'X-Market-Client-Id': `VSCode ${pkg.version}`, - 'User-Agent': `VSCode ${pkg.version}`, - 'X-Market-User-Id': this.environmentService.machineUUID - }; + this.commonHeadersPromise = resolveMarketplaceHeaders(this.environmentService); } private api(path = ''): string { @@ -412,33 +410,34 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } private queryGallery(query: Query): TPromise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { - const commonHeaders = this.commonHTTPHeaders; - const data = JSON.stringify(query.raw); - const headers = assign({}, commonHeaders, { - 'Content-Type': 'application/json', - 'Accept': 'application/json;api-version=3.0-preview.1', - 'Accept-Encoding': 'gzip', - 'Content-Length': data.length - }); + return this.commonHeadersPromise.then(commonHeaders => { + const data = JSON.stringify(query.raw); + const headers = assign({}, commonHeaders, { + 'Content-Type': 'application/json', + 'Accept': 'application/json;api-version=3.0-preview.1', + 'Accept-Encoding': 'gzip', + 'Content-Length': data.length + }); - return this.requestService.request({ - type: 'POST', - url: this.api('/extensionquery'), - data, - headers - }).then(context => { + return this.requestService.request({ + type: 'POST', + url: this.api('/extensionquery'), + data, + headers + }).then(context => { - if (context.res.statusCode >= 400 && context.res.statusCode < 500) { - return { galleryExtensions: [], total: 0 }; - } + if (context.res.statusCode >= 400 && context.res.statusCode < 500) { + return { galleryExtensions: [], total: 0 }; + } - return asJson(context).then(result => { - const r = result.results[0]; - const galleryExtensions = r.extensions; - const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; - const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; + return asJson(context).then(result => { + const r = result.results[0]; + const galleryExtensions = r.extensions; + const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; + const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; - return { galleryExtensions, total }; + return { galleryExtensions, total }; + }); }); }); } @@ -448,13 +447,15 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return TPromise.as(null); } - const headers = { ...this.commonHTTPHeaders, Accept: '*/*;api-version=4.0-preview.1' }; + return this.commonHeadersPromise.then(commonHeaders => { + const headers = { ...commonHeaders, Accept: '*/*;api-version=4.0-preview.1' }; - return this.requestService.request({ - type: 'POST', - url: this.api(`/publishers/${publisher}/extensions/${name}/${version}/stats?statType=${type}`), - headers - }).then(null, () => null); + return this.requestService.request({ + type: 'POST', + url: this.api(`/publishers/${publisher}/extensions/${name}/${version}/stats?statType=${type}`), + headers + }).then(null, () => null); + }); } download(extension: IGalleryExtension): TPromise { @@ -463,7 +464,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!extension) { return TPromise.wrapError(new Error(localize('notCompatibleDownload', "Unable to download because the extension compatible with current version '{0}' of VS Code is not found.", pkg.version))); } - const zipPath = path.join(tmpdir(), uuid.generateUuid()); + const zipPath = path.join(tmpdir(), generateUuid()); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); /* __GDPR__ @@ -591,47 +592,25 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } private getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}): TPromise { - const baseOptions = { type: 'GET' }; - const headers = assign({}, this.commonHTTPHeaders, options.headers || {}); - options = assign({}, options, baseOptions, { headers }); - - const url = asset.uri; - const fallbackUrl = asset.fallbackUri; - const firstOptions = assign({}, options, { url }); - - return this.requestService.request(firstOptions) - .then(context => { - if (context.res.statusCode === 200) { - return TPromise.as(context); - } - - return asText(context) - .then(message => TPromise.wrapError(new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`))); - }) - .then(null, err => { - if (isPromiseCanceledError(err)) { - return TPromise.wrapError(err); - } - - const message = getErrorMessage(err); - /* __GDPR__ - "galleryService:requestError" : { - "url" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "cdn": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('galleryService:requestError', { url, cdn: true, message }); - /* __GDPR__ - "galleryService:cdnFallback" : { - "url" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + return this.commonHeadersPromise.then(commonHeaders => { + const baseOptions = { type: 'GET' }; + const headers = assign({}, commonHeaders, options.headers || {}); + options = assign({}, options, baseOptions, { headers }); + + const url = asset.uri; + const fallbackUrl = asset.fallbackUri; + const firstOptions = assign({}, options, { url }); + + return this.requestService.request(firstOptions) + .then(context => { + if (context.res.statusCode === 200) { + return TPromise.as(context); } - */ - this.telemetryService.publicLog('galleryService:cdnFallback', { url, message }); - const fallbackOptions = assign({}, options, { url: fallbackUrl }); - return this.requestService.request(fallbackOptions).then(null, err => { + return asText(context) + .then(message => TPromise.wrapError(new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`))); + }) + .then(null, err => { if (isPromiseCanceledError(err)) { return TPromise.wrapError(err); } @@ -644,10 +623,34 @@ export class ExtensionGalleryService implements IExtensionGalleryService { "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.telemetryService.publicLog('galleryService:requestError', { url: fallbackUrl, cdn: false, message }); - return TPromise.wrapError(err); + this.telemetryService.publicLog('galleryService:requestError', { url, cdn: true, message }); + /* __GDPR__ + "galleryService:cdnFallback" : { + "url" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('galleryService:cdnFallback', { url, message }); + + const fallbackOptions = assign({}, options, { url: fallbackUrl }); + return this.requestService.request(fallbackOptions).then(null, err => { + if (isPromiseCanceledError(err)) { + return TPromise.wrapError(err); + } + + const message = getErrorMessage(err); + /* __GDPR__ + "galleryService:requestError" : { + "url" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "cdn": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "message": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('galleryService:requestError', { url: fallbackUrl, cdn: false, message }); + return TPromise.wrapError(err); + }); }); - }); + }); } private getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): TPromise { @@ -709,3 +712,33 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return false; } } + +export function resolveMarketplaceHeaders(environmentService: IEnvironmentService): TPromise<{ [key: string]: string; }> { + const marketplaceMachineIdFile = path.join(environmentService.userDataPath, 'machineid'); + + return readFile(marketplaceMachineIdFile, 'utf8').then(contents => { + if (isUUID(contents)) { + return contents; + } + + return TPromise.wrap(null); // invalid marketplace UUID + }, error => { + return TPromise.wrap(null); // error reading ID file + }).then(uuid => { + if (!uuid) { + uuid = generateUuid(); + + try { + writeFileAndFlushSync(marketplaceMachineIdFile, uuid); + } catch (error) { + //noop + } + } + + return { + 'X-Market-Client-Id': `VSCode ${pkg.version}`, + 'User-Agent': `VSCode ${pkg.version}`, + 'X-Market-User-Id': uuid + }; + }); +} \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..63d4e31cb4d205711d7f9dbd91d9934de1fa0f78 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionGalleryService.test.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import * as os from 'os'; +import extfs = require('vs/base/node/extfs'); +import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { parseArgs } from 'vs/platform/environment/node/argv'; +import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; +import { join } from 'path'; +import { mkdirp } from 'vs/base/node/pfs'; +import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService'; +import { isUUID } from 'vs/base/common/uuid'; + +suite('Extension Gallery Service', () => { + const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice'); + const marketplaceHome = join(parentDir, 'Marketplace'); + + setup(done => { + + // Delete any existing backups completely and then re-create it. + extfs.del(marketplaceHome, os.tmpdir(), () => { + mkdirp(marketplaceHome).then(() => { + done(); + }); + }); + }); + + teardown(done => { + extfs.del(marketplaceHome, os.tmpdir(), done); + }); + + test('marketplace machine id', done => { + const args = ['--user-data-dir', marketplaceHome]; + const environmentService = new EnvironmentService(parseArgs(args), process.execPath); + + return resolveMarketplaceHeaders(environmentService).then(headers => { + assert.ok(isUUID(headers['X-Market-User-Id'])); + + return resolveMarketplaceHeaders(environmentService).then(headers2 => { + assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']); + + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index 04cbddcf857d9b67edaa8341ea80c52d47508482..86fd42f3913c116689a66b4cc45958726162c816 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -18,10 +18,14 @@ export class FileStorage { constructor(private dbPath: string, private verbose?: boolean) { } - public getItem(key: string, defaultValue?: T): T { + private ensureLoaded(): void { if (!this.database) { - this.database = this.load(); + this.database = this.loadSync(); } + } + + public getItem(key: string, defaultValue?: T): T { + this.ensureLoaded(); const res = this.database[key]; if (isUndefinedOrNull(res)) { @@ -32,9 +36,7 @@ export class FileStorage { } public setItem(key: string, data: any): void { - if (!this.database) { - this.database = this.load(); - } + this.ensureLoaded(); // Remove an item when it is undefined or null if (isUndefinedOrNull(data)) { @@ -49,22 +51,20 @@ export class FileStorage { } this.database[key] = data; - this.save(); + this.saveSync(); } public removeItem(key: string): void { - if (!this.database) { - this.database = this.load(); - } + this.ensureLoaded(); // Only update if the key is actually present (not undefined) if (!isUndefined(this.database[key])) { this.database[key] = void 0; - this.save(); + this.saveSync(); } } - private load(): object { + private loadSync(): object { try { return JSON.parse(fs.readFileSync(this.dbPath).toString()); // invalid JSON or permission issue can happen here } catch (error) { @@ -76,7 +76,7 @@ export class FileStorage { } } - private save(): void { + private saveSync(): void { try { writeFileAndFlushSync(this.dbPath, JSON.stringify(this.database, null, 4)); // permission issue can happen here } catch (error) {