提交 bc6e4f52 编写于 作者: B Benjamin Pasero

Environment service: sync read access to UUID file on startup (fixes #39034)

上级 383ee752
......@@ -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<object>;
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) => {
......
......@@ -78,7 +78,7 @@ export interface IEnvironmentService {
appSettingsHome: string;
appSettingsPath: string;
appKeybindingsPath: string;
machineUUID: string;
settingsSearchBuildId: number;
settingsSearchUrl: string;
......
......@@ -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 {
......
......@@ -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<IRawGalleryQueryResult>(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<IRawGalleryQueryResult>(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<string> {
......@@ -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<IRequestContext> {
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<IRequestContext>(new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`)));
})
.then(null, err => {
if (isPromiseCanceledError(err)) {
return TPromise.wrapError<IRequestContext>(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<IRequestContext>(new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`)));
})
.then(null, err => {
if (isPromiseCanceledError(err)) {
return TPromise.wrapError<IRequestContext>(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<IRequestContext>(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<IRequestContext>(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<IRequestContext>(err);
});
});
});
});
}
private getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): TPromise<IRawGalleryExtensionVersion> {
......@@ -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
/*---------------------------------------------------------------------------------------------
* 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
......@@ -18,10 +18,14 @@ export class FileStorage {
constructor(private dbPath: string, private verbose?: boolean) { }
public getItem<T>(key: string, defaultValue?: T): T {
private ensureLoaded(): void {
if (!this.database) {
this.database = this.load();
this.database = this.loadSync();
}
}
public getItem<T>(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) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册