Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -32,3 +32,8 @@ export const enum AccessibilitySupport {
}
export const CONTEXT_ACCESSIBILITY_MODE_ENABLED = new RawContextKey<boolean>('accessibilityModeEnabled', false);
export interface IAccessibilityInformation {
label: string;
role?: string;
}

View File

@@ -111,6 +111,7 @@ export class MenuId {
static readonly TunnelInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly ViewItemContext = new MenuId('ViewItemContext');
static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext');
static readonly ViewTitle = new MenuId('ViewTitle');
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
static readonly CommentThreadTitle = new MenuId('CommentThreadTitle');
@@ -483,7 +484,8 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable {
disposables.add(MenuRegistry.appendMenuItem(menu.id, { command, ...menu }));
}
if (f1) {
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command }));
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition }));
disposables.add(MenuRegistry.addCommand(command));
}
// keybinding

View File

@@ -17,11 +17,12 @@ export interface IUserDataSyncAuthToken {
export interface IAuthenticationTokenService {
_serviceBrand: undefined;
readonly token: IUserDataSyncAuthToken | undefined;
readonly onDidChangeToken: Event<IUserDataSyncAuthToken | undefined>;
readonly onTokenFailed: Event<void>;
getToken(): Promise<IUserDataSyncAuthToken | undefined>;
setToken(userDataSyncAuthToken: IUserDataSyncAuthToken | undefined): Promise<void>;
readonly onTokenFailed: Event<void>;
sendTokenFailed(): void;
}
@@ -29,21 +30,14 @@ export class AuthenticationTokenService extends Disposable implements IAuthentic
_serviceBrand: any;
private _token: IUserDataSyncAuthToken | undefined;
get token(): IUserDataSyncAuthToken | undefined { return this._token; }
private _onDidChangeToken = this._register(new Emitter<IUserDataSyncAuthToken | undefined>());
readonly onDidChangeToken = this._onDidChangeToken.event;
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
private _token: IUserDataSyncAuthToken | undefined;
constructor() {
super();
}
async getToken(): Promise<IUserDataSyncAuthToken | undefined> {
return this._token;
}
async setToken(token: IUserDataSyncAuthToken | undefined): Promise<void> {
if (token && this._token ? token.token !== this._token.token || token.authenticationProviderId !== this._token.authenticationProviderId : token !== this._token) {

View File

@@ -7,7 +7,6 @@ import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
export class AuthenticationTokenServiceChannel implements IServerChannel {
constructor(private readonly service: IAuthenticationTokenService) { }
@@ -22,7 +21,6 @@ export class AuthenticationTokenServiceChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'setToken': return this.service.setToken(args);
case 'getToken': return this.service.getToken();
}
throw new Error('Invalid call');
}

View File

@@ -17,7 +17,7 @@ import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/com
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources';
import { isEqual as areResourcesEquals, getComparisonKey } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
import { Schemas } from 'vs/base/common/network';
@@ -486,7 +486,7 @@ export class BackupMainService implements IBackupMainService {
// for backward compatibility, use the fspath as key
key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
} else {
key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString();
key = folderUri.toString().toLowerCase();
}
return crypto.createHash('md5').update(key).digest('hex');

View File

@@ -11,11 +11,15 @@ export class BrowserClipboardService implements IClipboardService {
_serviceBrand: undefined;
private _internalResourcesClipboard: URI[] | undefined;
private readonly mapTextToType = new Map<string, string>(); // unsupported in web (only in-memory)
async writeText(text: string, type?: string): Promise<void> {
// With type: only in-memory is supported
if (type) {
return; // TODO@sbatten support for writing a specific type into clipboard is unsupported
this.mapTextToType.set(type, text);
return;
}
// Guard access to navigator.clipboard with try/catch
@@ -52,8 +56,10 @@ export class BrowserClipboardService implements IClipboardService {
}
async readText(type?: string): Promise<string> {
// With type: only in-memory is supported
if (type) {
return ''; // TODO@sbatten support for reading a specific type from clipboard is unsupported
return this.mapTextToType.get(type) || '';
}
// Guard access to navigator.clipboard with try/catch
@@ -68,25 +74,42 @@ export class BrowserClipboardService implements IClipboardService {
}
}
private findText = ''; // unsupported in web (only in-memory)
async readFindText(): Promise<string> {
return this.findText;
}
async writeFindText(text: string): Promise<void> {
this.findText = text;
}
private resources: URI[] = []; // unsupported in web (only in-memory)
async writeResources(resources: URI[]): Promise<void> {
this.resources = resources;
}
async readResources(): Promise<URI[]> {
return this.resources;
}
async hasResources(): Promise<boolean> {
return this.resources.length > 0;
}
/** @deprecated */
readTextSync(): string | undefined {
return undefined;
}
readFindText(): string {
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
/** @deprecated */
readFindTextSync(): string {
return this.findText;
}
writeFindText(text: string): void { }
writeResources(resources: URI[]): void {
this._internalResourcesClipboard = resources;
}
readResources(): URI[] {
return this._internalResourcesClipboard || [];
}
hasResources(): boolean {
return this._internalResourcesClipboard !== undefined && this._internalResourcesClipboard.length > 0;
/** @deprecated */
writeFindTextSync(text: string): void {
this.findText = text;
}
}

View File

@@ -22,30 +22,38 @@ export interface IClipboardService {
*/
readText(type?: string): Promise<string>;
readTextSync(): string | undefined;
/**
* Reads text from the system find pasteboard.
*/
readFindText(): string;
readFindText(): Promise<string>;
/**
* Writes text to the system find pasteboard.
*/
writeFindText(text: string): void;
writeFindText(text: string): Promise<void>;
/**
* Writes resources to the system clipboard.
*/
writeResources(resources: URI[]): void;
writeResources(resources: URI[]): Promise<void>;
/**
* Reads resources from the system clipboard.
*/
readResources(): URI[];
readResources(): Promise<URI[]>;
/**
* Find out if resources are copied to the clipboard.
*/
hasResources(): boolean;
hasResources(): Promise<boolean>;
/** @deprecated */
readTextSync(): string | undefined;
/** @deprecated */
readFindTextSync(): string;
/** @deprecated */
writeFindTextSync(text: string): void;
}

View File

@@ -9,9 +9,9 @@
.context-view-block {
position: fixed;
cursor: initial;
left:0;
top:0;
z-index: -1;
width: 100%;
height: 100%;
}
}

View File

@@ -14,7 +14,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { EventType, $, removeNode } from 'vs/base/browser/dom';
import { EventType, $, removeNode, isHTMLElement } from 'vs/base/browser/dom';
import { attachMenuStyler } from 'vs/platform/theme/common/styler';
import { domEvent } from 'vs/base/browser/event';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@@ -50,6 +50,7 @@ export class ContextMenuHandler {
let menu: Menu | undefined;
const anchor = delegate.getAnchor();
this.contextViewService.showContextView({
getAnchor: () => delegate.getAnchor(),
canRelayout: false,
@@ -65,6 +66,7 @@ export class ContextMenuHandler {
// Render invisible div to block mouse interaction in the rest of the UI
if (this.options.blockMouse) {
this.block = container.appendChild($('.context-view-block'));
domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation());
}
const menuDisposables = new DisposableStore();
@@ -131,7 +133,7 @@ export class ContextMenuHandler {
this.focusToReturn.focus();
}
}
});
}, !!delegate.anchorAsContainer && isHTMLElement(anchor) ? anchor : undefined);
}
private onActionRun(e: IRunEvent): void {

View File

@@ -15,7 +15,7 @@ export interface IContextViewService extends IContextViewProvider {
_serviceBrand: undefined;
showContextView(delegate: IContextViewDelegate): void;
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void;
hideContextView(data?: any): void;
layout(): void;
anchorAlignment?: AnchorAlignment;

View File

@@ -12,13 +12,15 @@ export class ContextViewService extends Disposable implements IContextViewServic
_serviceBrand: undefined;
private contextView: ContextView;
private container: HTMLElement;
constructor(
@ILayoutService readonly layoutService: ILayoutService
) {
super();
this.contextView = this._register(new ContextView(layoutService.container));
this.container = layoutService.container;
this.contextView = this._register(new ContextView(this.container, false));
this.layout();
this._register(layoutService.onLayout(() => this.layout()));
@@ -26,11 +28,24 @@ export class ContextViewService extends Disposable implements IContextViewServic
// ContextView
setContainer(container: HTMLElement): void {
this.contextView.setContainer(container);
setContainer(container: HTMLElement, useFixedPosition?: boolean): void {
this.contextView.setContainer(container, !!useFixedPosition);
}
showContextView(delegate: IContextViewDelegate): void {
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void {
if (container) {
if (container !== this.container) {
this.container = container;
this.setContainer(container, true);
}
} else {
if (this.container !== this.layoutService.container) {
this.container = this.layoutService.container;
this.setContainer(this.container, false);
}
}
this.contextView.show(delegate);
}
@@ -41,4 +56,4 @@ export class ContextViewService extends Disposable implements IContextViewServic
hideContextView(data?: any): void {
this.contextView.hide(data);
}
}
}

View File

@@ -14,27 +14,27 @@ export class KeytarCredentialsService implements ICredentialsService {
private readonly _keytar = new IdleValue<Promise<KeytarModule>>(() => import('keytar'));
async getPassword(service: string, account: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
const keytar = await this._keytar.value;
return keytar.getPassword(service, account);
}
async setPassword(service: string, account: string, password: string): Promise<void> {
const keytar = await this._keytar.getValue();
const keytar = await this._keytar.value;
return keytar.setPassword(service, account, password);
}
async deletePassword(service: string, account: string): Promise<boolean> {
const keytar = await this._keytar.getValue();
const keytar = await this._keytar.value;
return keytar.deletePassword(service, account);
}
async findPassword(service: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
const keytar = await this._keytar.value;
return keytar.findPassword(service);
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
const keytar = await this._keytar.getValue();
const keytar = await this._keytar.value;
return keytar.findCredentials(service);
}
}

View File

@@ -272,3 +272,12 @@ export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI
message.push('');
return message.join('\n');
}
export interface INativeOpenDialogOptions {
forceNewWindow?: boolean;
defaultPath?: string;
telemetryEventName?: string;
telemetryExtraData?: ITelemetryData;
}

View File

@@ -11,7 +11,7 @@ import { isMacintosh } from 'vs/base/common/platform';
import { dirname } from 'vs/base/common/path';
import { normalizeNFC } from 'vs/base/common/normalization';
import { exists } from 'vs/base/node/pfs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';

View File

@@ -6,11 +6,11 @@
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import * as electron from 'electron';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { remote } from 'electron';
import { timeout } from 'vs/base/common/async';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
class WindowDriver extends BaseWindowDriver {
@@ -32,7 +32,7 @@ class WindowDriver extends BaseWindowDriver {
private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise<void> {
const { x, y } = await this._getElementXY(selector, offset);
const webContents: electron.WebContents = (electron as any).remote.getCurrentWebContents();
const webContents = remote.getCurrentWebContents();
webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
await timeout(10);

View File

@@ -4,19 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } from 'electron';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { INativeOpenWindowOptions, IOpenedWindow } from 'vs/platform/windows/node/window';
export const IElectronService = createDecorator<IElectronService>('electronService');
export interface IElectronService {
export interface ICommonElectronService {
_serviceBrand: undefined;
// Properties
readonly windowId: number;
// Events
readonly onWindowOpen: Event<number>;
@@ -32,7 +31,7 @@ export interface IElectronService {
getActiveWindowId(): Promise<number | undefined>;
openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
openWindow(toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise<void>;
openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;
toggleFullScreen(): Promise<void>;
@@ -61,6 +60,16 @@ export interface IElectronService {
setDocumentEdited(edited: boolean): Promise<void>;
openExternal(url: string): Promise<boolean>;
updateTouchBar(items: ISerializableCommandAction[][]): Promise<void>;
moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise<boolean>;
// clipboard
readClipboardText(type?: 'selection' | 'clipboard'): Promise<string>;
writeClipboardText(text: string, type?: 'selection' | 'clipboard'): Promise<void>;
readClipboardFindText(): Promise<string>;
writeClipboardFindText(text: string): Promise<void>;
writeClipboardBuffer(format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise<void>;
readClipboardBuffer(format: string): Promise<Uint8Array>;
hasClipboard(format: string, type?: 'selection' | 'clipboard'): Promise<boolean>;
// macOS Touchbar
newWindowTab(): Promise<void>;

View File

@@ -5,13 +5,13 @@
import { Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app } from 'electron';
import { INativeOpenWindowOptions, IOpenedWindow, OpenContext } from 'vs/platform/windows/node/window';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app, clipboard } from 'electron';
import { OpenContext } from 'vs/platform/windows/node/window';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { isMacintosh } from 'vs/base/common/platform';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { ICommonElectronService } from 'vs/platform/electron/common/electron';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
@@ -23,9 +23,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { ILogService } from 'vs/platform/log/common/log';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
export interface IElectronMainService extends AddFirstParameterToFunctions<IElectronService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
export interface IElectronMainService extends AddFirstParameterToFunctions<ICommonElectronService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
export const IElectronMainService = createDecorator<IElectronService>('electronMainService');
export const IElectronMainService = createDecorator<IElectronMainService>('electronMainService');
export class ElectronMainService implements IElectronMainService {
@@ -41,6 +41,12 @@ export class ElectronMainService implements IElectronMainService {
) {
}
//#region Properties
get windowId(): never { throw new Error('Not implemented in electron-main'); }
//#endregion
//#region Events
readonly onWindowOpen: Event<number> = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-created', (_, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
@@ -85,8 +91,8 @@ export class ElectronMainService implements IElectronMainService {
}
openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise<void>;
openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise<void>;
openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: INativeOpenWindowOptions): Promise<void> {
openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;
openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> {
if (Array.isArray(arg1)) {
return this.doOpenWindow(windowId, arg1, arg2);
}
@@ -94,7 +100,7 @@ export class ElectronMainService implements IElectronMainService {
return this.doOpenEmptyWindow(windowId, arg1);
}
private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: INativeOpenWindowOptions = Object.create(null)): Promise<void> {
private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: IOpenWindowOptions = Object.create(null)): Promise<void> {
if (toOpen.length > 0) {
this.windowsMainService.open({
context: OpenContext.API,
@@ -114,7 +120,10 @@ export class ElectronMainService implements IElectronMainService {
}
private async doOpenEmptyWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise<void> {
this.windowsMainService.openEmptyWindow(OpenContext.API, options);
this.windowsMainService.openEmptyWindow({
context: OpenContext.API,
contextWindowId: windowId
}, options);
}
async toggleFullScreen(windowId: number | undefined): Promise<void> {
@@ -290,6 +299,43 @@ export class ElectronMainService implements IElectronMainService {
}
}
async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<boolean> {
return shell.moveItemToTrash(fullPath);
}
//#endregion
//#region clipboard
async readClipboardText(windowId: number | undefined, type?: 'selection' | 'clipboard'): Promise<string> {
return clipboard.readText(type);
}
async writeClipboardText(windowId: number | undefined, text: string, type?: 'selection' | 'clipboard'): Promise<void> {
return clipboard.writeText(text, type);
}
async readClipboardFindText(windowId: number | undefined,): Promise<string> {
return clipboard.readFindText();
}
async writeClipboardFindText(windowId: number | undefined, text: string): Promise<void> {
return clipboard.writeFindText(text);
}
async writeClipboardBuffer(windowId: number | undefined, format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise<void> {
return clipboard.writeBuffer(format, buffer as Buffer, type);
}
async readClipboardBuffer(windowId: number | undefined, format: string): Promise<Uint8Array> {
return clipboard.readBuffer(format);
}
async hasClipboard(windowId: number | undefined, format: string, type?: 'selection' | 'clipboard'): Promise<boolean> {
return clipboard.has(format, type);
}
//#endregion
//#region macOS Touchbar

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonElectronService } from 'vs/platform/electron/common/electron';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
export const IElectronService = createDecorator<IElectronService>('electronService');
export interface IElectronService extends ICommonElectronService { }
export class ElectronService {
_serviceBrand: undefined;
constructor(
readonly windowId: number,
@IMainProcessService mainProcessService: IMainProcessService
) {
return createChannelSender<IElectronService>(mainProcessService.getChannel('electron'), {
context: windowId,
properties: (() => {
const properties = new Map<string, unknown>();
properties.set('windowId', windowId);
return properties;
})()
});
}
}

View File

@@ -72,6 +72,7 @@ export interface ParsedArgs {
remote?: string;
'disable-user-env-probe'?: boolean;
'force'?: boolean;
'donot-sync'?: boolean;
'force-user-env'?: boolean;
'sync'?: 'on' | 'off';
@@ -196,6 +197,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'file-chmod': { type: 'boolean' },
'driver-verbose': { type: 'boolean' },
'force': { type: 'boolean' },
'donot-sync': { type: 'boolean' },
'trace': { type: 'boolean' },
'trace-category-filter': { type: 'string' },
'trace-options': { type: 'string' },

View File

@@ -6,7 +6,7 @@
import * as assert from 'assert';
import { firstIndex } from 'vs/base/common/arrays';
import { localize } from 'vs/nls';
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/node/files';
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
import { parseArgs, ErrorReporter, OPTIONS, ParsedArgs } from 'vs/platform/environment/node/argv';
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs {

View File

@@ -415,10 +415,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
}
const { id, uuid } = <IExtensionIdentifier>arg1; // {{SQL CARBON EDIT}} @anthonydresser remove extension ? extension.identifier
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated)
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (uuid) {
query = query.withFilter(FilterType.ExtensionId, uuid);
@@ -479,8 +478,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
let query = new Query()
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (text) {
// Use category filter instead of "category:themes"
@@ -641,6 +639,11 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
}
private queryGallery(query: Query, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
// Always exclude non validated and unpublished extensions
query = query
.withFlags(query.flags, Flags.ExcludeNonValidated)
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
if (!this.isEnabled()) {
return Promise.reject(new Error('No extension gallery service configured.'));
}
@@ -768,10 +771,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise<IGalleryExtensionVersion[]> {
let query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated)
.withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (extension.identifier.uuid) {
query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid);

View File

@@ -96,7 +96,9 @@ export interface IGalleryMetadata {
export interface ILocalExtension extends IExtension {
readonly manifest: IExtensionManifest;
metadata: IGalleryMetadata;
isMachineScoped: boolean;
publisherId: string | null;
publisherDisplayName: string | null;
readmeUrl: URI | null;
changelogUrl: URI | null;
}
@@ -191,6 +193,12 @@ export const INSTALL_ERROR_NOT_SUPPORTED = 'notsupported';
export const INSTALL_ERROR_MALICIOUS = 'malicious';
export const INSTALL_ERROR_INCOMPATIBLE = 'incompatible';
export class ExtensionManagementError extends Error {
constructor(message: string, readonly code: string) {
super(message);
}
}
export interface IExtensionManagementService {
_serviceBrand: undefined;
@@ -200,10 +208,10 @@ export interface IExtensionManagementService {
onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
zip(extension: ILocalExtension): Promise<URI>;
unzip(zipLocation: URI, type: ExtensionType): Promise<IExtensionIdentifier>;
unzip(zipLocation: URI): Promise<IExtensionIdentifier>;
getManifest(vsix: URI): Promise<IExtensionManifest>;
install(vsix: URI): Promise<ILocalExtension>;
installFromGallery(extension: IGalleryExtension): Promise<ILocalExtension>;
install(vsix: URI, isMachineScoped?: boolean): Promise<ILocalExtension>;
installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise<ILocalExtension>;
uninstall(extension: ILocalExtension, force?: boolean): Promise<void>;
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>;

View File

@@ -60,7 +60,7 @@ export class ExtensionManagementChannel implements IServerChannel {
const uriTransformer: IURITransformer | null = this.getUriTransformer(context);
switch (command) {
case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer));
case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer), args[1]);
case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer));
case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer));
case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer));
case 'installFromGallery': return this.service.installFromGallery(args[0]);
@@ -92,8 +92,8 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(<UriComponents>result)));
}
unzip(zipLocation: URI, type: ExtensionType): Promise<IExtensionIdentifier> {
return Promise.resolve(this.channel.call('unzip', [zipLocation, type]));
unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
return Promise.resolve(this.channel.call('unzip', [zipLocation]));
}
install(vsix: URI): Promise<ILocalExtension> {

View File

@@ -69,12 +69,11 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
id: extension.identifier.id,
name: extension.manifest.name,
galleryId: null,
publisherId: extension.metadata ? extension.metadata.publisherId : null,
publisherId: extension.publisherId,
publisherName: extension.manifest.publisher,
publisherDisplayName: extension.metadata ? extension.metadata.publisherDisplayName : null,
publisherDisplayName: extension.publisherDisplayName,
dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0,
// {{SQL CARBON EDIT}}
extensionVersion: extension.manifest.version
extensionVersion: extension.manifest.version // {{SQL CARBON EDIT}}
};
}
@@ -120,4 +119,4 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<str
}
return result;
}
}

View File

@@ -76,8 +76,13 @@ export class ExtensionTipsService implements IExtensionTipsService {
});
}
});
const remotes = getDomainsOfRemotes(content.value.toString(), keys(recommendationByRemote));
remotes.forEach(remote => result.push(recommendationByRemote.get(remote)!));
const domains = getDomainsOfRemotes(content.value.toString(), keys(recommendationByRemote));
for (const domain of domains) {
const remote = recommendationByRemote.get(domain);
if (remote) {
result.push(remote);
}
}
} catch (error) { /* Ignore */ }
}
return result;

View File

@@ -8,8 +8,7 @@ import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { assign } from 'vs/base/common/objects';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { flatten/*, isNonEmptyArray*/ } from 'vs/base/common/arrays';
import { extract, ExtractError, zip, IFile } from 'vs/base/node/zip';
import { zip, IFile } from 'vs/base/node/zip';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
IGalleryExtension, IGalleryMetadata,
@@ -19,21 +18,20 @@ import {
IReportedExtension,
InstallOperation,
INSTALL_ERROR_MALICIOUS,
INSTALL_ERROR_INCOMPATIBLE
INSTALL_ERROR_INCOMPATIBLE,
ExtensionManagementError
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from '../common/extensionNls';
import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { Limiter, createCancelablePromise, CancelablePromise, Queue } from 'vs/base/common/async';
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import * as semver from 'semver-umd';
import { URI } from 'vs/base/common/uri';
import product from 'vs/platform/product/common/product';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { isMacintosh } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
@@ -43,79 +41,35 @@ import { IDownloadService } from 'vs/platform/download/common/download';
import { optional, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Schemas } from 'vs/base/common/network';
import { CancellationToken } from 'vs/base/common/cancellation';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsScanner, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
const INSTALL_ERROR_DOWNLOADING = 'downloading';
const INSTALL_ERROR_VALIDATING = 'validating';
const INSTALL_ERROR_LOCAL = 'local';
const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_RENAMING = 'renaming';
const INSTALL_ERROR_DELETING = 'deleting';
const ERROR_UNKNOWN = 'unknown';
export class ExtensionManagementError extends Error {
constructor(message: string, readonly code: string) {
super(message);
}
}
function parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);
const metadata = manifest.__metadata || null;
delete manifest.__metadata;
c({ manifest, metadata });
} catch (err) {
e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
}
});
}
function readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> {
const promises = [
pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => parseManifest(raw)),
pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];
return Promise.all(promises).then(([{ manifest, metadata }, translations]) => {
return {
manifest: localizeManifest(manifest, translations),
metadata
};
});
}
interface InstallableExtension {
zipPath: string;
identifierWithVersion: ExtensionIdentifierWithVersion;
metadata: IGalleryMetadata | null;
metadata?: IMetadata;
}
export class ExtensionManagementService extends Disposable implements IExtensionManagementService {
_serviceBrand: undefined;
private systemExtensionsPath: string;
private extensionsPath: string;
private uninstalledPath: string;
private uninstalledFileLimiter: Queue<any>;
private readonly extensionsScanner: ExtensionsScanner;
private reportedExtensions: Promise<IReportedExtension[]> | undefined;
private lastReportTimestamp = 0;
private readonly installingExtensions: Map<string, CancelablePromise<ILocalExtension>> = new Map<string, CancelablePromise<ILocalExtension>>();
private readonly uninstallingExtensions: Map<string, CancelablePromise<void>> = new Map<string, CancelablePromise<void>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionsDownloader: ExtensionsDownloader;
private readonly extensionLifecycle: ExtensionsLifecycle;
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
@@ -130,7 +84,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
constructor(
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IEnvironmentService environmentService: INativeEnvironmentService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
@optional(IDownloadService) private downloadService: IDownloadService,
@@ -138,13 +92,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
this.extensionsPath = environmentService.extensionsPath!;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
this.extensionLifecycle = this._register(new ExtensionsLifecycle(environmentService, this.logService));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
@@ -152,6 +102,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.installingExtensions.clear();
this.uninstallingExtensions.clear();
}));
const extensionLifecycle = this._register(new ExtensionsLifecycle(environmentService, this.logService));
this._register(this.extensionsScanner.onDidRemoveExtension(extension => extensionLifecycle.postUninstall(extension)));
}
zip(extension: ILocalExtension): Promise<URI> {
@@ -161,9 +114,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
.then<URI>(path => URI.file(path));
}
unzip(zipLocation: URI, type: ExtensionType): Promise<IExtensionIdentifier> {
unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString());
return this.install(zipLocation, type).then(local => local.identifier);
return this.install(zipLocation).then(local => local.identifier);
}
async getManifest(vsix: URI): Promise<IExtensionManifest> {
@@ -198,7 +151,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
install(vsix: URI, type: ExtensionType = ExtensionType.User): Promise<ILocalExtension> {
install(vsix: URI, isMachineScoped?: boolean): Promise<ILocalExtension> {
// {{SQL CARBON EDIT}}
let startTime = new Date().getTime();
@@ -220,10 +173,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
.then(installedExtensions => {
const existing = installedExtensions.filter(i => areSameExtensions(identifier, i.identifier))[0];
if (existing) {
// operation = InstallOperation.Update; {{SQL CARBON EDIT}} comment out for no unused
isMachineScoped = isMachineScoped || existing.isMachineScoped;
// operation = InstallOperation.Update;
if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) {
// {{SQL CARBON EDIT}} - Update VS Code product name
return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name))));
return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name))));
} else if (semver.gt(existing.manifest.version, manifest.version)) {
return this.uninstall(existing, true);
}
@@ -233,7 +186,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
return this.unsetUninstalledAndGetLocal(identifierWithVersion)
.then(existing => {
if (existing) {
return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name))));
return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name))));
}
return undefined;
});
@@ -245,7 +198,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
this._onInstallExtension.fire({ identifier, zipPath });
// {{SQL CARBON EDIT}}
// Until there's a gallery for SQL Ops Studio, skip retrieving the metadata from the gallery
return this.installExtension({ zipPath, identifierWithVersion, metadata: null }, type, token)
return this.installExtension({ zipPath, identifierWithVersion, metadata: { isMachineScoped } }, token)
.then(
local => {
this.reportTelemetry(this.getTelemetryEvent(InstallOperation.Install), getLocalExtensionTelemetryData(local), new Date().getTime() - startTime, void 0);
@@ -282,9 +235,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
return this.downloadService.download(vsix, URI.file(downloadedLocation)).then(() => URI.file(downloadedLocation));
}
/*private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IGalleryMetadata | null, type: ExtensionType, operation: InstallOperation, token: CancellationToken): Promise<ILocalExtension> { {{SQL CARBON EDIT}} comment out for no unused
return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, type, token)
.then(local => this.installDependenciesAndPackExtensions(local, null)
/*private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise<ILocalExtension> { {{SQL CARBON EDIT}}
return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, token)
.then(local => this.installDependenciesAndPackExtensions(local, undefined)
.then(
() => local,
error => {
@@ -302,7 +255,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
));
}*/
async installFromGallery(extension: IGalleryExtension): Promise<ILocalExtension> {
async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise<ILocalExtension> {
if (!this.galleryService.isEnabled()) {
return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")));
}
@@ -344,14 +297,17 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.installingExtensions.set(key, cancellablePromise);
try {
const installed = await this.getInstalled(ExtensionType.User);
const existingExtension = installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0];
const existingExtension = installed.find(i => areSameExtensions(i.identifier, extension.identifier));
if (existingExtension) {
operation = InstallOperation.Update;
}
this.downloadInstallableExtension(extension, operation)
.then(installableExtension => this.installExtension(installableExtension, ExtensionType.User, cancellationToken)
.then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local)))
.then(installableExtension => {
installableExtension.metadata.isMachineScoped = isMachineScoped || existingExtension?.isMachineScoped;
return this.installExtension(installableExtension, cancellationToken)
.then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local));
})
.then(local => this.installDependenciesAndPackExtensions(local, existingExtension)
.then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error))))
.then(
@@ -403,7 +359,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
.then(galleryExtension => {
if (galleryExtension) {
return this.setUninstalled(extension)
.then(() => this.removeUninstalledExtension(extension)
.then(() => this.extensionsScanner.removeUninstalledExtension(extension)
.then(
() => this.installFromGallery(galleryExtension).then(),
e => Promise.reject(new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e))))));
@@ -421,7 +377,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
.then(report => getMaliciousExtensionsSet(report).has(extension.identifier.id));
}
private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<InstallableExtension> {
private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<Required<InstallableExtension>> {
const metadata = <IGalleryMetadata>{
id: extension.identifier.uuid,
publisherId: extension.publisherId,
@@ -436,21 +392,21 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
return getManifest(zipPath)
.then(
manifest => (<InstallableExtension>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }),
manifest => (<Required<InstallableExtension>>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }),
error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING))
);
},
error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING)));
}
private installExtension(installableExtension: InstallableExtension, type: ExtensionType, token: CancellationToken): Promise<ILocalExtension> {
private installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
return this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion)
.then(
local => {
if (local) {
return local;
}
return this.extractAndInstall(installableExtension, type, token);
return this.extractAndInstall(installableExtension, token);
},
e => {
if (isMacintosh) {
@@ -477,63 +433,17 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, type: ExtensionType, token: CancellationToken): Promise<ILocalExtension> {
private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
const { identifier } = identifierWithVersion;
const location = type === ExtensionType.User ? this.extensionsPath : this.systemExtensionsPath;
const folderName = identifierWithVersion.key();
const tempPath = path.join(location, `.${folderName}`);
const extensionPath = path.join(location, folderName);
return pfs.rimraf(extensionPath)
.then(() => this.extractAndRename(identifier, zipPath, tempPath, extensionPath, token), e => Promise.reject(new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifier.id), INSTALL_ERROR_DELETING)))
.then(() => this.scanExtension(folderName, location, type))
.then(local => {
if (!local) {
return Promise.reject(nls.localize('cannot read', "Cannot read the extension from {0}", location));
}
this.logService.info('Installation completed.', identifier.id);
if (metadata) {
this.setMetadata(local, metadata);
return this.saveMetadataForLocalExtension(local);
}
return local;
}, error => pfs.rimraf(extensionPath).then(() => Promise.reject(error), () => Promise.reject(error)));
let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, token);
this.logService.info('Installation completed.', identifier.id);
if (metadata) {
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, metadata);
}
return local;
}
private extractAndRename(identifier: IExtensionIdentifier, zipPath: string, extractPath: string, renamePath: string, token: CancellationToken): Promise<void> {
return this.extract(identifier, zipPath, extractPath, token)
.then(() => this.rename(identifier, extractPath, renamePath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */)
.then(
() => this.logService.info('Renamed to', renamePath),
e => {
this.logService.info('Rename failed. Deleting from extracted location', extractPath);
return pfs.rimraf(extractPath).finally(() => { }).then(() => Promise.reject(e));
}));
}
private extract(identifier: IExtensionIdentifier, zipPath: string, extractPath: string, token: CancellationToken): Promise<void> {
this.logService.trace(`Started extracting the extension from ${zipPath} to ${extractPath}`);
return pfs.rimraf(extractPath)
.then(
() => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, token)
.then(
() => this.logService.info(`Extracted extension to ${extractPath}:`, identifier.id),
e => pfs.rimraf(extractPath).finally(() => { })
.then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))),
e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING)));
}
private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
return pfs.rename(extractPath, renamePath)
.then(undefined, error => {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
return this.rename(identifier, extractPath, renamePath, retryUntil);
}
return Promise.reject(new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING));
});
}
private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | null): Promise<void> {
private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined): Promise<void> {
if (this.galleryService.isEnabled()) {
const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || [];
if (installed.manifest.extensionPack) {
@@ -587,31 +497,16 @@ export class ExtensionManagementService extends Disposable implements IExtension
}));
}
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id);
local.metadata = metadata;
return this.saveMetadataForLocalExtension(local)
.then(localExtension => {
this.manifestCache.invalidate();
return localExtension;
});
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...metadata, isMachineScoped: local.isMachineScoped });
this.manifestCache.invalidate();
return local;
}
private saveMetadataForLocalExtension(local: ILocalExtension): Promise<ILocalExtension> {
if (!local.metadata) {
return Promise.resolve(local);
}
const manifestPath = path.join(local.location.fsPath, 'package.json');
return pfs.readFile(manifestPath, 'utf8')
.then(raw => parseManifest(raw))
.then(({ manifest }) => assign(manifest, { __metadata: local.metadata }))
.then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')))
.then(() => local);
}
/*private getMetadata(extensionName: string): Promise<IGalleryMetadata | null> { {{SQL CARBON EDIT}} comment out function for no unused
/*private getGalleryMetadata(extensionName: string): Promise<IGalleryMetadata | undefined> { {{SQL CARBON EDIT}}
return this.findGalleryExtensionByName(extensionName)
.then(galleryExtension => galleryExtension ? <IGalleryMetadata>{ id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : null);
.then(galleryExtension => galleryExtension ? <IGalleryMetadata>{ id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : undefined);
}*/
private findGalleryExtension(local: ILocalExtension): Promise<IGalleryExtension> {
@@ -724,7 +619,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
let promise = this.uninstallingExtensions.get(local.identifier.id);
if (!promise) {
// Set all versions of the extension as uninstalled
promise = createCancelablePromise(token => this.scanUserExtensions(false)
promise = createCancelablePromise(token => this.extensionsScanner.scanUserExtensions(false)
.then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier))))
.then(() => { this.uninstallingExtensions.delete(local.identifier.id); }));
this.uninstallingExtensions.set(local.identifier.id, promise);
@@ -748,142 +643,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
getInstalled(type: ExtensionType | null = null): Promise<ILocalExtension[]> {
const promises: Promise<ILocalExtension[]>[] = [];
if (type === null || type === ExtensionType.System) {
promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS))));
}
if (type === null || type === ExtensionType.User) {
promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS))));
}
return Promise.all<ILocalExtension[]>(promises).then(flatten, errors => Promise.reject(this.joinErrors(errors)));
return this.extensionsScanner.scanExtensions(type);
}
private scanSystemExtensions(): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning system extensions');
const systemExtensionsPromise = this.scanExtensions(this.systemExtensionsPath, ExtensionType.System)
.then(result => {
this.logService.trace('Scanned system extensions:', result.length);
return result;
});
if (this.environmentService.isBuilt) {
return systemExtensionsPromise;
}
// Scan other system extensions during development
const devSystemExtensionsPromise = this.getDevSystemExtensionsList()
.then(devSystemExtensionsList => {
if (devSystemExtensionsList.length) {
return this.scanExtensions(this.devSystemExtensionsPath, ExtensionType.System)
.then(result => {
this.logService.trace('Scanned dev system extensions:', result.length);
return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id })));
});
} else {
return [];
}
});
return Promise.all([systemExtensionsPromise, devSystemExtensionsPromise])
.then(([systemExtensions, devSystemExtensions]) => [...systemExtensions, ...devSystemExtensions]);
}
private scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning user extensions');
return Promise.all([this.getUninstalledExtensions(), this.scanExtensions(this.extensionsPath, ExtensionType.User)])
.then(([uninstalled, extensions]) => {
extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
if (excludeOutdated) {
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
}
this.logService.trace('Scanned user extensions:', extensions.length);
return extensions;
});
}
private scanExtensions(root: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
return pfs.readdir(root)
.then(extensionsFolders => Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, root, type)))))
.then(extensions => extensions.filter(e => e && e.identifier));
}
private scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return Promise.resolve(null);
}
const extensionPath = path.join(root, folderName);
return pfs.readdir(extensionPath)
.then(children => readManifest(extensionPath)
.then(({ manifest, metadata }) => {
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : null;
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : null;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl };
if (metadata) {
this.setMetadata(local, metadata);
}
return local;
}))
.then(undefined, () => null);
}
private setMetadata(local: ILocalExtension, metadata: IGalleryMetadata): void {
local.metadata = metadata;
local.identifier.uuid = metadata.id;
}
async removeDeprecatedExtensions(): Promise<void> {
await this.removeUninstalledExtensions();
await this.removeOutdatedExtensions();
}
private async removeUninstalledExtensions(): Promise<void> {
const uninstalled = await this.getUninstalledExtensions();
const extensions = await this.scanExtensions(this.extensionsPath, ExtensionType.User); // All user extensions
const installed: Set<string> = new Set<string>();
for (const e of extensions) {
if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) {
installed.add(e.identifier.id.toLowerCase());
}
}
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
await Promise.all(byExtension.map(async e => {
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
if (!installed.has(latest.identifier.id.toLowerCase())) {
await this.extensionLifecycle.postUninstall(latest);
}
}));
const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private removeOutdatedExtensions(): Promise<void> {
return this.scanExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
.then(extensions => {
const toRemove: ILocalExtension[] = [];
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
return Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}).then(() => undefined);
}
private removeUninstalledExtension(extension: ILocalExtension): Promise<void> {
return this.removeExtension(extension, 'uninstalled')
.then(() => this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]))
.then(() => undefined);
}
private removeExtension(extension: ILocalExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath));
removeDeprecatedExtensions(): Promise<void> {
return this.extensionsScanner.cleanUp();
}
private isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise<boolean> {
@@ -891,7 +655,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
return this.withUninstalledExtensions(allUninstalled => {
return this.extensionsScanner.withUninstalledExtensions(allUninstalled => {
const uninstalled: string[] = [];
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
@@ -904,34 +668,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
return this.withUninstalledExtensions(uninstalled => assign(uninstalled, ids.reduce((result, id) => { result[id.key()] = true; return result; }, {} as { [id: string]: boolean })));
return this.extensionsScanner.withUninstalledExtensions(uninstalled => assign(uninstalled, ids.reduce((result, id) => { result[id.key()] = true; return result; }, {} as { [id: string]: boolean })));
}
private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise<void> {
return this.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
}
private getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
return this.withUninstalledExtensions(uninstalled => uninstalled);
}
private async withUninstalledExtensions<T>(fn: (uninstalled: { [id: string]: boolean; }) => T): Promise<T> {
return await this.uninstalledFileLimiter.queue(() => {
let result: T | null = null;
return pfs.readFile(this.uninstalledPath, 'utf8')
.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))
.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
.then(uninstalled => { result = fn(uninstalled); return uninstalled; })
.then(uninstalled => {
if (Object.keys(uninstalled).length === 0) {
return pfs.rimraf(this.uninstalledPath);
} else {
const raw = JSON.stringify(uninstalled);
return pfs.writeFile(this.uninstalledPath, raw);
}
})
.then(() => result);
});
return this.extensionsScanner.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
}
getExtensionsReport(): Promise<IReportedExtension[]> {
@@ -958,18 +699,6 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private _devSystemExtensionsPath: string | null = null;
private get devSystemExtensionsPath(): string {
if (!this._devSystemExtensionsPath) {
this._devSystemExtensionsPath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions'));
}
return this._devSystemExtensionsPath;
}
private getDevSystemExtensionsList(): Promise<string[]> {
return Promise.resolve(product.builtInExtensions ? product.builtInExtensions.map(e => e.name) : []);
}
private toNonCancellablePromise<T>(promise: Promise<T>): Promise<T> {
return new Promise((c, e) => promise.then(result => c(result), error => e(error)));
}

View File

@@ -0,0 +1,351 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as semver from 'semver-umd';
import { Disposable } from 'vs/base/common/lifecycle';
import * as pfs from 'vs/base/node/pfs';
import * as path from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { ILocalExtension, IGalleryMetadata, ExtensionManagementError } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions, ExtensionIdentifierWithVersion, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { Limiter, Queue } from 'vs/base/common/async';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { CancellationToken } from 'vscode';
import { extract, ExtractError } from 'vs/base/node/zip';
import { isWindows } from 'vs/base/common/platform';
import { flatten } from 'vs/base/common/arrays';
import { Emitter } from 'vs/base/common/event';
import { assign } from 'vs/base/common/objects';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_RENAMING = 'renaming';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; }>;
export class ExtensionsScanner extends Disposable {
private readonly systemExtensionsPath: string;
private readonly extensionsPath: string;
private readonly uninstalledPath: string;
private readonly uninstalledFileLimiter: Queue<any>;
private _onDidRemoveExtension = new Emitter<ILocalExtension>();
readonly onDidRemoveExtension = this._onDidRemoveExtension.event;
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
this.extensionsPath = environmentService.extensionsPath!;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
}
async cleanUp(): Promise<void> {
await this.removeUninstalledExtensions();
await this.removeOutdatedExtensions();
}
async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> {
const promises: Promise<ILocalExtension[]>[] = [];
if (type === null || type === ExtensionType.System) {
promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS))));
}
if (type === null || type === ExtensionType.User) {
promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS))));
}
return Promise.all<ILocalExtension[]>(promises).then(flatten, errors => Promise.reject(this.joinErrors(errors)));
}
async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning user extensions');
let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions()]);
extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
if (excludeOutdated) {
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
}
this.logService.trace('Scanned user extensions:', extensions.length);
return extensions;
}
async scanAllUserExtensions(): Promise<ILocalExtension[]> {
return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User);
}
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
const { identifier } = identifierWithVersion;
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
const extensionPath = path.join(this.extensionsPath, folderName);
try {
await pfs.rimraf(extensionPath);
} catch (error) {
try {
await pfs.rimraf(extensionPath);
} catch (e) { /* ignore */ }
throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifier.id), INSTALL_ERROR_DELETING);
}
await this.extractAtLocation(identifier, zipPath, tempPath, token);
try {
await this.rename(identifier, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
try {
pfs.rimraf(tempPath);
} catch (e) { /* ignore */ }
throw error;
}
let local: ILocalExtension | null = null;
try {
local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User);
} catch (e) { /*ignore */ }
if (local) {
return local;
}
throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath));
}
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
// unset if false
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
assign(manifest, { __metadata: metadata });
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
return this.withUninstalledExtensions(uninstalled => uninstalled);
}
async withUninstalledExtensions<T>(fn: (uninstalled: { [id: string]: boolean; }) => T): Promise<T> {
return this.uninstalledFileLimiter.queue(async () => {
let result: T | null = null;
return pfs.readFile(this.uninstalledPath, 'utf8')
.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))
.then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
.then(uninstalled => { result = fn(uninstalled); return uninstalled; })
.then(uninstalled => {
if (Object.keys(uninstalled).length === 0) {
return pfs.rimraf(this.uninstalledPath);
} else {
const raw = JSON.stringify(uninstalled);
return pfs.writeFile(this.uninstalledPath, raw);
}
})
.then(() => result);
});
}
async removeExtension(extension: ILocalExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
await pfs.rimraf(extension.location.fsPath);
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
}
async removeUninstalledExtension(extension: ILocalExtension): Promise<void> {
await this.removeExtension(extension, 'uninstalled');
await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]);
}
private extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> {
this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`);
return pfs.rimraf(location)
.then(
() => extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token)
.then(
() => this.logService.info(`Extracted extension to ${location}:`, identifier.id),
e => pfs.rimraf(location).finally(() => { })
.then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))),
e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING)));
}
private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
return pfs.rename(extractPath, renamePath)
.then(undefined, error => {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
return this.rename(identifier, extractPath, renamePath, retryUntil);
}
return Promise.reject(new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING));
});
}
private async scanSystemExtensions(): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning system extensions');
const systemExtensionsPromise = this.scanDefaultSystemExtensions();
if (this.environmentService.isBuilt) {
return systemExtensionsPromise;
}
// Scan other system extensions during development
const devSystemExtensionsPromise = this.scanDevSystemExtensions();
const [systemExtensions, devSystemExtensions] = await Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]);
return [...systemExtensions, ...devSystemExtensions];
}
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
const extensionsFolders = await pfs.readdir(dir);
const extensions = await Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type))));
return extensions.filter(e => e && e.identifier);
}
private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
const extensionPath = path.join(root, folderName);
try {
const children = await pfs.readdir(extensionPath);
const { manifest, metadata } = await this.readManifest(extensionPath);
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : null;
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : null;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false };
if (metadata) {
this.setMetadata(local, metadata);
}
return local;
} catch (e) {
this.logService.trace(e);
return null;
}
}
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned system extensions:', result.length);
return result;
}
private async scanDevSystemExtensions(): Promise<ILocalExtension[]> {
const devSystemExtensionsList = this.getDevSystemExtensionsList();
if (devSystemExtensionsList.length) {
const result = await this.scanExtensionsInDir(this.devSystemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned dev system extensions:', result.length);
return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id })));
} else {
return [];
}
}
private setMetadata(local: ILocalExtension, metadata: IMetadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
}
private async removeUninstalledExtensions(): Promise<void> {
const uninstalled = await this.getUninstalledExtensions();
const extensions = await this.scanAllUserExtensions(); // All user extensions
const installed: Set<string> = new Set<string>();
for (const e of extensions) {
if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) {
installed.add(e.identifier.id.toLowerCase());
}
}
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
await Promise.all(byExtension.map(async e => {
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
if (!installed.has(latest.identifier.id.toLowerCase())) {
this._onDidRemoveExtension.fire(latest);
}
}));
const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private async removeOutdatedExtensions(): Promise<void> {
const extensions = await this.scanAllUserExtensions();
const toRemove: ILocalExtension[] = [];
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
await Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}
private getDevSystemExtensionsList(): string[] {
return (this.productService.builtInExtensions || []).map(e => e.name);
}
private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
if (errors.length === 1) {
return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
}
return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
}, new Error(''));
}
private _devSystemExtensionsPath: string | null = null;
private get devSystemExtensionsPath(): string {
if (!this._devSystemExtensionsPath) {
this._devSystemExtensionsPath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions'));
}
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
const promises = [
pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];
const [{ manifest, metadata }, translations] = await Promise.all(promises);
return {
manifest: localizeManifest(manifest, translations),
metadata
};
}
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);
const metadata = manifest.__metadata || null;
delete manifest.__metadata;
c({ manifest, metadata });
} catch (err) {
e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
}
});
}
}

View File

@@ -157,6 +157,7 @@ export interface IExtensionManifest {
readonly forceReload?: boolean; // {{ SQL CARBON EDIT }} add field
readonly description?: string;
readonly main?: string;
readonly browser?: string;
readonly icon?: string;
readonly categories?: string[];
readonly keywords?: string[];

View File

@@ -7,7 +7,7 @@ import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isAbsolutePath, dirname, basename, joinPath, isEqual, isEqualOrParent } from 'vs/base/common/resources';
import { isAbsolutePath, dirname, basename, joinPath, isEqual, ExtUri } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
@@ -674,15 +674,16 @@ export class FileService extends Disposable implements IFileService {
// Check if source is equal or parent to target (requires providers to be the same)
if (sourceProvider === targetProvider) {
const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
const extUri = new ExtUri(_ => !isPathCaseSensitive);
if (!isPathCaseSensitive) {
isSameResourceWithDifferentPathCase = isEqual(source, target, true /* ignore case */);
isSameResourceWithDifferentPathCase = extUri.isEqual(source, target);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target)));
}
if (!isSameResourceWithDifferentPathCase && isEqualOrParent(target, source, !isPathCaseSensitive)) {
if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) {
throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
}
}
@@ -700,7 +701,8 @@ export class FileService extends Disposable implements IFileService {
// it as it would delete the source as well. In this case we have to throw
if (sourceProvider === targetProvider) {
const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
if (isEqualOrParent(source, target, !isPathCaseSensitive)) {
const extUri = new ExtUri(_ => !isPathCaseSensitive);
if (extUri.isEqualOrParent(source, target)) {
throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));
}
}

View File

@@ -824,7 +824,6 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined
return stat.mtime.toString(29) + stat.size.toString(31);
}
export function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) {
return Promise.resolve();
@@ -838,3 +837,9 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr
});
});
}
/**
* Desktop only: limits for memory sizes
*/
export const MIN_MAX_MEMORY_SIZE_MB = 2048;
export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;

View File

@@ -3,15 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { shell } from 'electron';
import { DiskFileSystemProvider as NodeDiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { basename } from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
constructor(
logService: ILogService,
private electronService: IElectronService,
options?: IDiskFileSystemProviderOptions
) {
super(logService, options);
}
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
@@ -25,9 +34,9 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
return super.doDelete(filePath, opts);
}
const result = shell.moveItemToTrash(filePath);
const result = await this.electronService.moveItemToTrash(filePath);
if (!result) {
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
}
}
}
}

View File

@@ -7,7 +7,7 @@ import * as assert from 'assert';
import { tmpdir } from 'os';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { generateUuid } from 'vs/base/common/uuid';
import { join, basename, dirname, posix } from 'vs/base/common/path';

View File

@@ -219,7 +219,7 @@ export class InstantiationService implements IInstantiationService {
if (key in target) {
return target[key];
}
let obj = idle.getValue();
let obj = idle.value;
let prop = obj[key];
if (typeof prop !== 'function') {
return prop;
@@ -229,7 +229,7 @@ export class InstantiationService implements IInstantiationService {
return prop;
},
set(_target: T, p: PropertyKey, value: any): boolean {
idle.getValue()[p] = value;
idle.value[p] = value;
return true;
}
});

View File

@@ -3,10 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/electron-browser/ipc.electron-browser';
import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox';
import { Disposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');

View File

@@ -3,10 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IIssueService = createDecorator<IIssueService>('issueService');
// Since data sent through the service is serialized to JSON, functions will be lost, so Color objects
// should not be sent as their 'toString' method will be stripped. Instead convert to strings before sending.
export interface WindowStyles {
@@ -91,7 +87,7 @@ export interface ProcessExplorerData extends WindowData {
styles: ProcessExplorerStyles;
}
export interface IIssueService {
export interface ICommonIssueService {
_serviceBrand: undefined;
openReporter(data: IssueReporterData): Promise<void>;
openProcessExplorer(data: ProcessExplorerData): Promise<void>;

View File

@@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import * as objects from 'vs/base/common/objects';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/node/issue';
import { ICommonIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue';
import { BrowserWindow, ipcMain, screen, IpcMainEvent, Display, shell } from 'electron';
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
@@ -18,10 +18,16 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IWindowState } from 'vs/platform/windows/electron-main/windows';
import { listProcesses } from 'vs/base/node/ps';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
const DEFAULT_BACKGROUND_COLOR = '#1E1E1E';
export class IssueMainService implements IIssueService {
export const IIssueMainService = createDecorator<IIssueMainService>('issueMainService');
export interface IIssueMainService extends ICommonIssueService { }
export class IssueMainService implements ICommonIssueService {
_serviceBrand: undefined;
_issueWindow: BrowserWindow | null = null;
_issueParentWindow: BrowserWindow | null = null;
@@ -163,12 +169,11 @@ export class IssueMainService implements IIssueService {
}
});
ipcMain.on('windowsInfoRequest', (event: IpcMainEvent) => {
ipcMain.on('vscode:windowsInfoRequest', (event: IpcMainEvent) => {
this.launchMainService.getMainProcessInfo().then(info => {
event.sender.send('vscode:windowsInfoResponse', info.windows);
});
});
}
openReporter(data: IssueReporterData): Promise<void> {
@@ -189,7 +194,9 @@ export class IssueMainService implements IIssueService {
title: localize('issueReporter', "Issue Reporter"),
backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR,
webPreferences: {
nodeIntegration: true
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
nodeIntegration: true,
enableWebSQL: false
}
});
@@ -224,7 +231,7 @@ export class IssueMainService implements IIssueService {
if (!this._processExplorerWindow) {
this._processExplorerParentWindow = BrowserWindow.getFocusedWindow();
if (this._processExplorerParentWindow) {
const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 300);
const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500);
this._processExplorerWindow = new BrowserWindow({
skipTaskbar: true,
resizable: true,
@@ -238,7 +245,9 @@ export class IssueMainService implements IIssueService {
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer"),
webPreferences: {
nodeIntegration: true
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
nodeIntegration: true,
enableWebSQL: false
}
});

View File

@@ -3,13 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonIssueService } from 'vs/platform/issue/common/issue';
export interface INativeOpenDialogOptions {
forceNewWindow?: boolean;
export const IIssueService = createDecorator<IIssueService>('issueService');
defaultPath?: string;
telemetryEventName?: string;
telemetryExtraData?: ITelemetryData;
}
export interface IIssueService extends ICommonIssueService { }

View File

@@ -6,7 +6,6 @@
import { ILogService } from 'vs/platform/log/common/log';
import { IURLService } from 'vs/platform/url/common/url';
import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ParsedArgs } from 'vs/platform/environment/node/argv';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowSettings } from 'vs/platform/windows/common/windows';
@@ -56,7 +55,6 @@ export interface ILaunchMainService {
start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void>;
getMainProcessId(): Promise<number>;
getMainProcessInfo(): Promise<IMainProcessInfo>;
getLogsPath(): Promise<string>;
getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]>;
}
@@ -69,7 +67,6 @@ export class LaunchMainService implements ILaunchMainService {
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IURLService private readonly urlService: IURLService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IConfigurationService private readonly configurationService: IConfigurationService
) { }
@@ -84,7 +81,7 @@ export class LaunchMainService implements ILaunchMainService {
// Create a window if there is none
if (this.windowsMainService.getWindowCount() === 0) {
const window = this.windowsMainService.openEmptyWindow(OpenContext.DESKTOP)[0];
const window = this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP })[0];
whenWindowReady = window.ready();
}
@@ -234,12 +231,6 @@ export class LaunchMainService implements ILaunchMainService {
});
}
getLogsPath(): Promise<string> {
this.logService.trace('Received request for logs path from other instance.');
return Promise.resolve(this.environmentService.logsPath);
}
getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> {
const windows = this.windowsMainService.getWindows();
const promises: Promise<IDiagnosticInfo | IRemoteDiagnosticError | undefined>[] = windows.map(window => {

View File

@@ -129,6 +129,7 @@ export const keyboardNavigationSettingKey = 'workbench.list.keyboardNavigation';
export const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardNavigation';
const treeIndentKey = 'workbench.tree.indent';
const treeRenderIndentGuidesKey = 'workbench.tree.renderIndentGuides';
const listSmoothScrolling = 'workbench.list.smoothScrolling';
function getHorizontalScrollingSetting(configurationService: IConfigurationService): boolean {
return getMigratedSettingValue<boolean>(configurationService, horizontalScrollingKey, 'workbench.tree.horizontalScrolling');
@@ -658,6 +659,7 @@ abstract class ResourceNavigator<T> extends Disposable {
onDidChangeFocus: Event<{ browserEvent?: UIEvent }>,
onDidChangeSelection: Event<{ browserEvent?: UIEvent }>,
onDidOpen: Event<{ browserEvent?: UIEvent }>,
readonly openOnSingleClick?: boolean
},
options?: IResourceNavigatorOptions
) {
@@ -711,7 +713,7 @@ abstract class ResourceNavigator<T> extends Disposable {
!!(<SelectionKeyboardEvent>browserEvent).preserveFocus :
!isDoubleClick;
if (this.options.openOnSingleClick || isDoubleClick || isKeyboardEvent) {
if (this.options.openOnSingleClick || this.treeOrList.openOnSingleClick || isDoubleClick || isKeyboardEvent) {
const sideBySide = browserEvent instanceof MouseEvent && (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey);
this.open(preserveFocus, isDoubleClick || isMiddleClick, sideBySide, browserEvent);
}
@@ -738,8 +740,8 @@ export class ListResourceNavigator<T> extends ResourceNavigator<number> {
}
export class TreeResourceNavigator<T, TFilterData> extends ResourceNavigator<T> {
constructor(tree: WorkbenchObjectTree<T, TFilterData> | WorkbenchCompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<any, T, TFilterData> | WorkbenchAsyncDataTree<any, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<any, T, TFilterData>, options?: IResourceNavigatorOptions) {
super(tree, { openOnSingleClick: tree.openOnSingleClick, ...(options || {}) });
constructor(tree: WorkbenchObjectTree<T, TFilterData> | WorkbenchCompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<any, T, TFilterData> | WorkbenchAsyncDataTree<any, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<any, T, TFilterData>, options: IResourceNavigatorOptions = {}) {
super(tree, options);
}
}
@@ -1003,6 +1005,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
...workbenchListOptions,
indent: configurationService.getValue<number>(treeIndentKey),
renderIndentGuides: configurationService.getValue<RenderIndentGuides>(treeRenderIndentGuidesKey),
smoothScrolling: configurationService.getValue<boolean>(listSmoothScrolling),
automaticKeyboardNavigation: getAutomaticKeyboardNavigation(),
simpleKeyboardNavigation: keyboardNavigation === 'simple',
filterOnType: keyboardNavigation === 'filter',
@@ -1078,25 +1081,33 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0);
}),
configurationService.onDidChangeConfiguration(e => {
let options: any = {};
if (e.affectsConfiguration(openModeSettingKey)) {
tree.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) });
options = { ...options, openOnSingleClick: useSingleClickToOpen(configurationService) };
}
if (e.affectsConfiguration(multiSelectModifierSettingKey)) {
this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService);
}
if (e.affectsConfiguration(treeIndentKey)) {
const indent = configurationService.getValue<number>(treeIndentKey);
tree.updateOptions({ indent });
options = { ...options, indent };
}
if (e.affectsConfiguration(treeRenderIndentGuidesKey)) {
const renderIndentGuides = configurationService.getValue<RenderIndentGuides>(treeRenderIndentGuidesKey);
tree.updateOptions({ renderIndentGuides });
options = { ...options, renderIndentGuides };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
options = { ...options, smoothScrolling };
}
if (e.affectsConfiguration(keyboardNavigationSettingKey)) {
updateKeyboardNavigation();
}
if (e.affectsConfiguration(automaticKeyboardNavigationSettingKey)) {
tree.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() });
options = { ...options, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() };
}
if (Object.keys(options).length > 0) {
tree.updateOptions(options);
}
}),
this.contextKeyService.onDidChangeContext(e => {
@@ -1181,6 +1192,11 @@ configurationRegistry.registerConfiguration({
default: 'onHover',
description: localize('render tree indent guides', "Controls whether the tree should render indent guides.")
},
[listSmoothScrolling]: {
type: 'boolean',
default: false,
description: localize('list smoothScrolling setting', "Controls whether lists and trees have smooth scrolling."),
},
[keyboardNavigationSettingKey]: {
'type': 'string',
'enum': ['simple', 'highlight', 'filter'],

View File

@@ -3,14 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
export const IMenubarService = createDecorator<IMenubarService>('menubarService');
export interface IMenubarService {
_serviceBrand: undefined;
export interface ICommonMenubarService {
updateMenubar(windowId: number, menuData: IMenubarData): Promise<void>;
}

View File

@@ -18,7 +18,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels';
import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/node/menubar';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar';
import { URI } from 'vs/base/common/uri';
import { IStateService } from 'vs/platform/state/node/state';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
@@ -61,7 +61,7 @@ export class Menubar {
private keybindings: { [commandId: string]: IMenubarKeybinding };
private fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = {};
private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = Object.create(null);
constructor(
@IUpdateService private readonly updateService: IUpdateService,
@@ -113,8 +113,8 @@ export class Menubar {
private addFallbackHandlers(): void {
// File Menu Items
this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU);
this.fallbackMenuHandlers['workbench.action.newWindow'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU);
this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id });
this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id });
this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.electronMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.electronMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
@@ -267,7 +267,7 @@ export class Menubar {
this.appMenuInstalled = true;
const dockMenu = new Menu();
dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow(OpenContext.DOCK) }));
dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) }));
app.dock.setMenu(dockMenu);
}

View File

@@ -3,38 +3,42 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMenubarService, IMenubarData } from 'vs/platform/menubar/node/menubar';
import { ICommonMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar';
import { Menubar } from 'vs/platform/menubar/electron-main/menubar';
import { ILogService } from 'vs/platform/log/common/log';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
export class MenubarMainService implements IMenubarService {
export const IMenubarMainService = createDecorator<IMenubarMainService>('menubarMainService');
export interface IMenubarMainService extends ICommonMenubarService {
_serviceBrand: undefined;
}
export class MenubarMainService implements IMenubarMainService {
_serviceBrand: undefined;
private _menubar: Menubar | undefined;
private menubar: Promise<Menubar>;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@ILogService private readonly logService: ILogService
) {
// Install Menu
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
this._menubar = this.instantiationService.createInstance(Menubar);
});
this.menubar = this.installMenuBarAfterWindowOpen();
}
updateMenubar(windowId: number, menus: IMenubarData): Promise<void> {
return this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
this.logService.trace('menubarService#updateMenubar', windowId);
private async installMenuBarAfterWindowOpen(): Promise<Menubar> {
await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen);
if (this._menubar) {
this._menubar.updateMenu(menus, windowId);
}
return this.instantiationService.createInstance(Menubar);
}
return undefined;
});
async updateMenubar(windowId: number, menus: IMenubarData): Promise<void> {
this.logService.trace('menubarService#updateMenubar', windowId);
const menubar = await this.menubar;
menubar.updateMenu(menus, windowId);
}
}

View File

@@ -3,5 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const MIN_MAX_MEMORY_SIZE_MB = 2048;
export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonMenubarService } from 'vs/platform/menubar/common/menubar';
export const IMenubarService = createDecorator<IMenubarService>('menubarService');
export interface IMenubarService extends ICommonMenubarService {
_serviceBrand: undefined;
}

View File

@@ -21,7 +21,7 @@ if (isWeb) {
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.17.0-dev',
vscodeVersion: '1.45.0-dev',
vscodeVersion: '1.46.0-dev',
nameLong: 'Azure Data Studio Web Dev',
nameShort: 'Azure Data Studio Web Dev',
urlProtocol: 'azuredatastudio-oss'

View File

@@ -24,7 +24,7 @@ export interface IQuickAccessOptions {
itemActivation?: ItemActivation;
/**
* Wether to take the input value as is and not restore it
* Whether to take the input value as is and not restore it
* from any existing value if quick access is visible.
*/
preserveValue?: boolean;

View File

@@ -23,6 +23,11 @@ export interface ITelemetryData {
export interface ITelemetryService {
/**
* Whether error telemetry will get sent. If false, `publicLogError` will no-op.
*/
readonly sendErrorTelemetry: boolean;
_serviceBrand: undefined;
/**

View File

@@ -35,7 +35,7 @@ export class TelemetryService implements ITelemetryService {
private _piiPaths: string[];
private _userOptIn: boolean;
private _enabled: boolean;
private _sendErrorTelemetry: boolean;
public readonly sendErrorTelemetry: boolean;
private readonly _disposables = new DisposableStore();
private _cleanupPatterns: RegExp[] = [];
@@ -49,7 +49,7 @@ export class TelemetryService implements ITelemetryService {
this._piiPaths = config.piiPaths || [];
this._userOptIn = true;
this._enabled = true;
this._sendErrorTelemetry = !!config.sendErrorTelemetry;
this.sendErrorTelemetry = !!config.sendErrorTelemetry;
// static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information`
this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi];
@@ -148,7 +148,7 @@ export class TelemetryService implements ITelemetryService {
}
publicLogError(errorEventName: string, data?: ITelemetryData): Promise<any> {
if (!this._sendErrorTelemetry) {
if (!this.sendErrorTelemetry) {
return Promise.resolve(undefined);
}

View File

@@ -13,6 +13,8 @@ import { isObject } from 'vs/base/common/types';
export const NullTelemetryService = new class implements ITelemetryService {
_serviceBrand: undefined;
readonly sendErrorTelemetry = false;
publicLog(eventName: string, data?: ITelemetryData) {
return Promise.resolve(undefined);
}

View File

@@ -87,8 +87,11 @@ export interface ITokenStyle {
}
export interface IColorTheme {
readonly type: ThemeType;
readonly label: string;
/**
* Resolves the color of the given color identifier. If the theme does not
* specify the color, the default color is returned unless <code>useDefault</code> is set to false.

View File

@@ -515,7 +515,7 @@ function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry
registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]);
registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['support.type']]);
registerTokenType('struct', nls.localize('struct', "Style for structs."), [['storage.type.struct']]);
registerTokenType('struct', nls.localize('struct', "Style for structs."), [['entity.name.type.struct']]);
registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.type.class'], ['support.class']]);
registerTokenType('interface', nls.localize('interface', "Style for interfaces."), [['entity.name.type.interface']]);
registerTokenType('enum', nls.localize('enum', "Style for enums."), [['entity.name.type.enum']]);

View File

@@ -9,6 +9,8 @@ import { Color } from 'vs/base/common/color';
export class TestColorTheme implements IColorTheme {
public readonly label = 'test';
constructor(private colors: { [id: string]: string; } = {}, public type = DARK) {
}

View File

@@ -5,6 +5,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
@@ -28,6 +29,13 @@ export interface IWorkspaceUndoRedoElement {
undo(): Promise<void> | void;
redo(): Promise<void> | void;
split(): IResourceUndoRedoElement[];
/**
* If implemented, will be invoked before calling `undo()` or `redo()`.
* This is a good place to prepare everything such that the calls to `undo()` or `redo()` are synchronous.
* If a disposable is returned, it will be invoked to clean things up.
*/
prepareUndoRedo?(): Promise<IDisposable> | IDisposable | void;
}
export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement;
@@ -37,9 +45,18 @@ export interface IPastFutureElements {
future: IUndoRedoElement[];
}
export interface UriComparisonKeyComputer {
/**
* Return `null` if you don't own this URI.
*/
getComparisonKey(uri: URI): string | null;
}
export interface IUndoRedoService {
_serviceBrand: undefined;
registerUriComparisonKeyComputer(uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable;
/**
* Add a new element to the `undo` stack.
* This will destroy the `redo` stack.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { UndoRedoElementType, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { mock } from 'vs/base/test/common/mock';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
suite('UndoRedoService', () => {
function createUndoRedoService(dialogService: IDialogService = new TestDialogService()): UndoRedoService {
const notificationService = new TestNotificationService();
return new UndoRedoService(dialogService, notificationService);
}
test('simple single resource elements', () => {
const resource = URI.file('test.txt');
const service = createUndoRedoService();
assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), false);
assert.ok(service.getLastElement(resource) === null);
let undoCall1 = 0;
let redoCall1 = 0;
const element1: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 1',
undo: () => { undoCall1++; },
redo: () => { redoCall1++; }
};
service.pushElement(element1);
assert.equal(undoCall1, 0);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
service.redo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);
let undoCall2 = 0;
let redoCall2 = 0;
const element2: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 2',
undo: () => { undoCall2++; },
redo: () => { redoCall2++; }
};
service.pushElement(element2);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 0);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element2);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
let undoCall3 = 0;
let redoCall3 = 0;
const element3: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: resource,
label: 'typing 2',
undo: () => { undoCall3++; },
redo: () => { redoCall3++; }
};
service.pushElement(element3);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 0);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element3);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 1);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
});
test('multi resource elements', async () => {
const resource1 = URI.file('test1.txt');
const resource2 = URI.file('test2.txt');
const service = createUndoRedoService(new class extends mock<IDialogService>() {
async show() {
return {
choice: 0 // confirm!
};
}
});
let undoCall1 = 0, undoCall11 = 0, undoCall12 = 0;
let redoCall1 = 0, redoCall11 = 0, redoCall12 = 0;
const element1: IUndoRedoElement = {
type: UndoRedoElementType.Workspace,
resources: [resource1, resource2],
label: 'typing 1',
undo: () => { undoCall1++; },
redo: () => { redoCall1++; },
split: () => {
return [
{
type: UndoRedoElementType.Resource,
resource: resource1,
label: 'typing 1.1',
undo: () => { undoCall11++; },
redo: () => { redoCall11++; }
},
{
type: UndoRedoElementType.Resource,
resource: resource2,
label: 'typing 1.2',
undo: () => { undoCall12++; },
redo: () => { redoCall12++; }
}
];
}
};
service.pushElement(element1);
assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);
await service.undo(resource1);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource1), false);
assert.equal(service.canRedo(resource1), true);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === null);
assert.equal(service.canUndo(resource2), false);
assert.equal(service.canRedo(resource2), true);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === null);
await service.redo(resource2);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall11, 0);
assert.equal(redoCall11, 0);
assert.equal(undoCall12, 0);
assert.equal(redoCall12, 0);
assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);
});
});

View File

@@ -94,8 +94,8 @@ export class Win32UpdateService extends AbstractUpdateService {
protected buildUpdateFeedUrl(quality: string): string | undefined {
let platform = 'win32';
if (process.arch === 'x64') {
platform += '-x64';
if (process.arch !== 'ia32') {
platform += `-${process.arch}`;
}
if (getUpdateType() === UpdateType.Archive) {

View File

@@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { values } from 'vs/base/common/map';
import { first } from 'vs/base/common/async';
import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import product from 'vs/platform/product/common/product';
export abstract class AbstractURLService extends Disposable implements IURLService {
@@ -27,3 +28,16 @@ export abstract class AbstractURLService extends Disposable implements IURLServi
return toDisposable(() => this.handlers.delete(handler));
}
}
export class NativeURLService extends AbstractURLService {
create(options?: Partial<UriComponents>): URI {
let { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined };
if (authority && path && path.indexOf('/') !== 0) {
path = `/${path}`; // URI validation requires a path if there is an authority
}
return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment });
}
}

View File

@@ -1,21 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI, UriComponents } from 'vs/base/common/uri';
import product from 'vs/platform/product/common/product';
import { AbstractURLService } from 'vs/platform/url/common/urlService';
export class URLService extends AbstractURLService {
create(options?: Partial<UriComponents>): URI {
let { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined };
if (authority && path && path.indexOf('/') !== 0) {
path = `/${path}`; // URI validation requires a path if there is an authority
}
return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment });
}
}

View File

@@ -7,10 +7,10 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
import { CancelablePromise } from 'vs/base/common/async';
import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ParseError, parse } from 'vs/base/common/json';
@@ -21,6 +21,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { isString } from 'vs/base/common/types';
import { uppercaseFirstLetter } from 'vs/base/common/strings';
import { equals } from 'vs/base/common/arrays';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IStorageService } from 'vs/platform/storage/common/storage';
type SyncSourceClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -33,19 +35,34 @@ export interface IRemoteUserData {
export interface ISyncData {
version: number;
machineId?: string;
content: string;
}
function isSyncData(thing: any): thing is ISyncData {
return thing
&& (thing.version && typeof thing.version === 'number')
&& (thing.content && typeof thing.content === 'string')
&& Object.keys(thing).length === 2;
if (thing
&& (thing.version !== undefined && typeof thing.version === 'number')
&& (thing.content !== undefined && typeof thing.content === 'string')) {
// backward compatibility
if (Object.keys(thing).length === 2) {
return true;
}
if (Object.keys(thing).length === 3
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')) {
return true;
}
}
return false;
}
export abstract class AbstractSynchroniser extends Disposable {
protected readonly syncFolder: URI;
private readonly currentMachineIdPromise: Promise<string>;
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
@@ -57,7 +74,8 @@ export abstract class AbstractSynchroniser extends Disposable {
private _onDidChangeConflicts: Emitter<Conflict[]> = this._register(new Emitter<Conflict[]>());
readonly onDidChangeConflicts: Event<Conflict[]> = this._onDidChangeConflicts.event;
protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
protected readonly lastSyncResource: URI;
@@ -67,10 +85,11 @@ export abstract class AbstractSynchroniser extends Disposable {
readonly resource: SyncResource,
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
) {
@@ -78,6 +97,22 @@ export abstract class AbstractSynchroniser extends Disposable {
this.syncResourceLogLabel = uppercaseFirstLetter(this.resource);
this.syncFolder = joinPath(environmentService.userDataSyncHome, resource);
this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`);
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
protected async triggerLocalChange(): Promise<void> {
if (this.isEnabled()) {
this.localChangeTriggerScheduler.schedule();
}
}
protected async doTriggerLocalChange(): Promise<void> {
this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`);
const lastSyncUserData = await this.getLastSyncUserData();
const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData)).hasRemoteChanged : true;
if (hasRemoteChanged) {
this._onDidChangeLocal.fire();
}
}
protected setStatus(status: SyncStatus): void {
@@ -108,7 +143,7 @@ export abstract class AbstractSynchroniser extends Disposable {
protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); }
async sync(ref?: string): Promise<void> {
async sync(manifest: IUserDataManifest | null): Promise<void> {
if (!this.isEnabled()) {
if (this.status !== SyncStatus.Idle) {
await this.stop();
@@ -129,7 +164,7 @@ export abstract class AbstractSynchroniser extends Disposable {
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData);
const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData);
let status: SyncStatus = SyncStatus.Idle;
try {
@@ -144,6 +179,51 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
async replace(uri: URI): Promise<boolean> {
const content = await this.resolveContent(uri);
if (!content) {
return false;
}
const syncData = this.parseSyncData(content);
if (!syncData) {
return false;
}
await this.stop();
try {
this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
await this.performReplace(syncData, remoteUserData, lastSyncUserData);
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
} finally {
this.setStatus(SyncStatus.Idle);
}
return true;
}
private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
if (lastSyncUserData) {
const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined;
// Last time synced resource and latest resource on server are same
if (lastSyncUserData.ref === latestRef) {
return lastSyncUserData;
}
// There is no resource on server and last time it was synced with no resource
if (latestRef === undefined && lastSyncUserData.syncData === null) {
return lastSyncUserData;
}
}
return this.getRemoteUserData(lastSyncUserData);
}
async getSyncPreview(): Promise<ISyncPreviewResult> {
if (!this.isEnabled()) {
return { hasLocalChanged: false, hasRemoteChanged: false };
@@ -158,7 +238,7 @@ export abstract class AbstractSynchroniser extends Disposable {
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
// current version is not compatible with cloud version
this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource });
throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource);
}
try {
const status = await this.performSync(remoteUserData, lastSyncUserData);
@@ -166,7 +246,7 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.RemotePreconditionFailed:
case UserDataSyncErrorCode.PreconditionFailed:
// Rejected as there is a new remote version. Syncing again...
this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`);
@@ -207,6 +287,18 @@ export abstract class AbstractSynchroniser extends Disposable {
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` });
}
async getMachineId({ uri }: ISyncResourceHandle): Promise<string | undefined> {
const ref = basename(uri);
if (isEqual(uri, this.toRemoteBackupResource(ref))) {
const { content } = await this.getUserData(ref);
if (content) {
const syncData = this.parseSyncData(content);
return syncData?.machineId;
}
}
return undefined;
}
async resolveContent(uri: URI): Promise<string | null> {
const ref = basename(uri);
if (isEqual(uri, this.toRemoteBackupResource(ref))) {
@@ -225,18 +317,21 @@ export abstract class AbstractSynchroniser extends Disposable {
} catch (e) { /* ignore */ }
}
protected async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
async getLastSyncUserData<T extends IRemoteUserData>(): Promise<T | null> {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
const parsed = JSON.parse(content.value.toString());
let syncData: ISyncData = JSON.parse(parsed.content);
const userData: IUserData = parsed as IUserData;
if (userData.content === null) {
return { ref: parsed.ref, syncData: null } as T;
}
const syncData: ISyncData = JSON.parse(userData.content);
// Migration from old content to sync data
if (!isSyncData(syncData)) {
syncData = { version: this.version, content: parsed.content };
/* Check if syncData is of expected type. Return only if matches */
if (isSyncData(syncData)) {
return { ...parsed, ...{ syncData, content: undefined } };
}
return { ...parsed, ...{ syncData, content: undefined } };
} catch (error) {
if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) {
// log error always except when file does not exist
@@ -247,11 +342,11 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps };
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
}
protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise<IRemoteUserData> {
const { ref, content } = await this.getUserData(lastSyncData);
let syncData: ISyncData | null = null;
if (content !== null) {
@@ -260,20 +355,16 @@ export abstract class AbstractSynchroniser extends Disposable {
return { ref, syncData };
}
protected parseSyncData(content: string): ISyncData | null {
let syncData: ISyncData | null = null;
protected parseSyncData(content: string): ISyncData {
try {
syncData = <ISyncData>JSON.parse(content);
// Migration from old content to sync data
if (!isSyncData(syncData)) {
syncData = { version: this.version, content };
const syncData: ISyncData = JSON.parse(content);
if (isSyncData(syncData)) {
return syncData;
}
} catch (e) {
this.logService.error(e);
} catch (error) {
this.logService.error(error);
}
return syncData;
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.Incompatible, this.resource);
}
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
@@ -287,7 +378,8 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
const syncData: ISyncData = { version: this.version, content };
const machineId = await this.currentMachineIdPromise;
const syncData: ISyncData = { version: this.version, machineId, content };
ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref);
return { ref, syncData };
}
@@ -301,6 +393,7 @@ export abstract class AbstractSynchroniser extends Disposable {
protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void>;
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
}
@@ -321,6 +414,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
resource: SyncResource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -328,7 +422,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(file)));
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
}
@@ -403,7 +497,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
// Otherwise fire change event
else {
this._onDidChangeLocal.fire();
this.triggerLocalChange();
}
}
@@ -426,6 +520,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
resource: SyncResource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@@ -434,7 +529,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(file, resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
}
protected hasErrors(content: string): boolean {

View File

@@ -7,6 +7,10 @@ import { values, keys } from 'vs/base/common/map';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { startsWith } from 'vs/base/common/strings';
import { deepClone } from 'vs/base/common/objects';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { distinct } from 'vs/base/common/arrays';
export interface IMergeResult {
added: ISyncExtension[];
@@ -21,16 +25,15 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
const updated: ISyncExtension[] = [];
if (!remoteExtensions) {
const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()));
return {
added,
removed,
updated,
remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()))
remote: remote.length > 0 ? remote : null
};
}
// massage incoming extension - add disabled property
const massageIncomingExtension = (extension: ISyncExtension): ISyncExtension => ({ ...extension, ...{ disabled: !!extension.disabled } });
localExtensions = localExtensions.map(massageIncomingExtension);
remoteExtensions = remoteExtensions.map(massageIncomingExtension);
lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null;
@@ -53,7 +56,14 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
};
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const key = getKey(extension);
extension = deepClone(extension);
if (localExtensionsMap.get(key)?.installed) {
extension.installed = true;
}
return addExtensionToMap(map, extension);
}, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
@@ -62,90 +72,82 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}, new Set<string>());
const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { added: [], removed: [], updated: [], remote: null };
}
if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) {
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
// massage outgoing extension - remove disabled property
const massageOutgoingExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
};
if (extension.disabled) {
massagedExtension.disabled = true;
}
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
};
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
}
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
} else {
// Add to local
added.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
} else {
// Add only installed extension to local
const remoteExtension = remoteExtensionsMap.get(key)!;
if (remoteExtension.installed) {
added.push(massageOutgoingExtension(remoteExtension, key));
}
}
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.delete(key);
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// Update in local always
updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!);
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
const extension = deepClone(localExtensionsMap.get(key)!);
// Retain installed property
if (newRemoteExtensionsMap.get(key)?.installed) {
extension.installed = true;
}
newRemoteExtensionsMap.set(key, extension);
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
// Remove only if it is an installed extension
if (lastSyncExtensionsMap?.get(key)?.installed) {
newRemoteExtensionsMap.delete(key);
}
}
}
}
const remote: ISyncExtension[] = [];
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>());
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), { checkInstalledProperty: true });
if (remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0) {
newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key)));
}
@@ -153,7 +155,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
return { added, removed, updated, remote: remote.length ? remote : null };
}
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
@@ -169,6 +171,7 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
if (!toExtension
|| fromExtension.disabled !== toExtension.disabled
|| fromExtension.version !== toExtension.version
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
) {
updated.add(key);
}
@@ -176,3 +179,44 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
return { added, removed, updated };
}
// massage incoming extension - add optional properties
function massageIncomingExtension(extension: ISyncExtension): ISyncExtension {
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
}
// massage outgoing extension - remove optional properties
function massageOutgoingExtension(extension: ISyncExtension, key: string): ISyncExtension {
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
};
if (extension.disabled) {
massagedExtension.disabled = true;
}
if (extension.installed) {
massagedExtension.installed = true;
}
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
}
export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] {
const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase());
const value = (configurationService.getValue<string[]>('sync.ignoredExtensions') || []).map(id => id.toLowerCase());
const added: string[] = [], removed: string[] = [];
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
}
}
}
return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1));
}

View File

@@ -3,22 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, dirname, basename } from 'vs/base/common/resources';
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { compare } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
readonly localExtensions: ISyncExtension[];
@@ -35,14 +37,20 @@ interface ILastSyncUserData extends IRemoteUserData {
skippedExtensions: ISyncExtension[] | undefined;
}
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 2;
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
/*
Version 3 - Introduce installed property to skip installing built in extensions
*/
protected readonly version: number = 3;
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@@ -53,14 +61,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(
Event.debounce(
Event.any<any>(
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
this.extensionEnablementService.onDidChangeEnablement),
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
() => undefined, 500)(() => this.triggerLocalChange()));
}
async pull(): Promise<void> {
@@ -79,9 +87,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
if (remoteUserData.syncData !== null) {
const localExtensions = await this.getLocalExtensions();
const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
@@ -112,8 +122,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`);
this.setStatus(SyncStatus.Syncing);
const localExtensions = await this.getLocalExtensions();
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions);
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
await this.apply({
@@ -132,35 +144,58 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
async stop(): Promise<void> { }
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'extensions.json') }];
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
return this.format(localExtensions);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
switch (basename(uri)) {
case 'extensions.json':
const edits = format(syncData.content, undefined, {});
return applyEdits(syncData.content, edits);
return this.format(this.parseExtensions(syncData));
}
}
}
return null;
}
private format(extensions: ISyncExtension[]): string {
extensions.sort((e1, e2) => {
if (!e1.identifier.uuid && e2.identifier.uuid) {
return -1;
}
if (e1.identifier.uuid && !e2.identifier.uuid) {
return 1;
}
return compare(e1.identifier.id, e2.identifier.id);
});
const content = JSON.stringify(extensions);
const edits = format(content, undefined, {});
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
async hasLocalData(): Promise<boolean> {
try {
const localExtensions = await this.getLocalExtensions();
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
if (isNonEmptyArray(localExtensions)) {
return true;
}
@@ -176,12 +211,28 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
await this.apply({
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const localExtensions = await this.getLocalExtensions();
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
@@ -189,7 +240,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
}
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
return {
added,
@@ -205,10 +256,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
};
}
private getIgnoredExtensions() {
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
}
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
if (!hasLocalChanged && !hasRemoteChanged) {
@@ -216,9 +263,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
if (hasLocalChanged) {
// back up all disabled or market place extensions
const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid);
await this.backupLocal(JSON.stringify(backUpExtensions));
await this.backupLocal(JSON.stringify(localExtensions));
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
}
@@ -317,31 +362,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return newSkippedExtensions;
}
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
let extensions: ISyncExtension[] = JSON.parse(syncData.content);
if (syncData.version !== this.version) {
extensions = extensions.map(e => {
private async parseAndMigrateExtensions(syncData: ISyncData): Promise<ISyncExtension[]> {
const extensions = this.parseExtensions(syncData);
if (syncData.version === 1
|| syncData.version === 2
) {
const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System);
for (const extension of extensions) {
// #region Migration from v1 (enabled -> disabled)
if (!(<any>e).enabled) {
e.disabled = true;
if (syncData.version === 1) {
if ((<any>extension).enabled === false) {
extension.disabled = true;
}
delete (<any>extension).enabled;
}
delete (<any>e).enabled;
// #endregion
return e;
});
// #region Migration from v2 (set installed property on extension)
if (syncData.version === 2) {
if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) {
extension.installed = true;
}
}
// #endregion
}
}
return extensions;
}
private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled();
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
return JSON.parse(syncData.content);
}
private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtension[] {
const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
return installedExtensions
.map(({ identifier }) => {
.map(({ identifier, type }) => {
const syncExntesion: ISyncExtension = { identifier };
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
syncExntesion.disabled = true;
}
if (type === ExtensionType.User) {
syncExntesion.installed = true;
}
return syncExntesion;
});
}

View File

@@ -18,7 +18,7 @@ export interface IMergeResult {
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
if (!remoteStorage) {
return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] };
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
}
const localToRemote = compare(localStorage, remoteStorage);
@@ -40,7 +40,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
@@ -64,7 +64,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
@@ -82,7 +82,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
for (const key of values(baseToRemote.removed)) {
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
continue;
}
local.removed.push(key);
@@ -120,7 +120,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
// do not remove from remote if storage key is not found
if (!storageKey) {
skipped.push(key);
logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
continue;
}

View File

@@ -3,17 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { dirname, joinPath, basename } from 'vs/base/common/resources';
import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
import { IFileService } from 'vs/platform/files/common/files';
import { IStringDictionary } from 'vs/base/common/collections';
import { edit } from 'vs/platform/userDataSync/common/content';
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
import { parse } from 'vs/base/common/json';
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
@@ -41,6 +41,7 @@ interface ILastSyncUserData extends IRemoteUserData {
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
protected readonly version: number = 1;
constructor(
@@ -55,7 +56,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
@IStorageService private readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
this._register(
Event.any(
@@ -65,7 +66,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
/* Storage key registered */
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
)((() => this._onDidChangeLocal.fire()))
)((() => this.triggerLocalChange()))
);
}
@@ -139,28 +140,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
async stop(): Promise<void> { }
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
return [{ resource: joinPath(uri, 'globalState.json') }];
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
}
async resolveContent(uri: URI): Promise<string | null> {
if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
const localGlobalState = await this.getLocalGlobalState();
return this.format(localGlobalState);
}
let content = await super.resolveContent(uri);
if (content) {
return content;
}
content = await super.resolveContent(dirname(uri));
if (content) {
const syncData = this.parseSyncData(content);
if (syncData) {
switch (basename(uri)) {
case 'globalState.json':
const edits = format(syncData.content, undefined, {});
return applyEdits(syncData.content, edits);
return this.format(JSON.parse(syncData.content));
}
}
}
return null;
}
private format(globalState: IGlobalState): string {
const storageKeys = Object.keys(globalState.storage).sort();
const storage: IStringDictionary<IStorageValue> = {};
storageKeys.forEach(key => storage[key] = globalState.storage[key]);
globalState.storage = storage;
const content = JSON.stringify(globalState);
const edits = format(content, undefined, {});
return applyEdits(content, edits);
}
async acceptConflict(conflict: URI, content: string): Promise<void> {
throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`);
}
@@ -183,6 +200,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
return SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
const localUserData = await this.getLocalGlobalState();
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
await this.apply({
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
skippedStorageKeys: skipped,
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
hasRemoteChanged: true
});
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;

View File

@@ -16,10 +16,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISyncContent {
mac?: string;
@@ -42,10 +43,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
async pull(): Promise<void> {
@@ -212,6 +214,24 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
if (content !== null) {
const fileContent = await this.getLocalFileContent();
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasConflicts: false,
hasLocalChanged: true,
hasRemoteChanged: true,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
@@ -248,10 +268,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) {
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null);
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } });
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
@@ -315,7 +335,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {

View File

@@ -429,26 +429,34 @@ function getEditToInsertAtLocation(content: string, key: string, value: any, loc
if (location.insertAfter) {
const edits: Edit[] = [];
/* Insert after a setting */
if (node.setting) {
return [{ offset: node.endOffset, length: 0, content: ',' + newProperty }];
edits.push({ offset: node.endOffset, length: 0, content: ',' + newProperty });
}
/*
Insert after a comment and before a setting (or)
Insert between comments and there is a setting after
*/
if (tree[location.index + 1] &&
(tree[location.index + 1].setting || findNextSettingNode(location.index, tree))) {
return [{ offset: node.endOffset, length: 0, content: eol + newProperty + ',' }];
/* Insert after a comment */
else {
const nextSettingNode = findNextSettingNode(location.index, tree);
const previousSettingNode = findPreviousSettingNode(location.index, tree);
const previousSettingCommaOffset = previousSettingNode?.setting?.commaOffset;
/* If there is a previous setting and it does not has comma then add it */
if (previousSettingNode && previousSettingCommaOffset === undefined) {
edits.push({ offset: previousSettingNode.endOffset, length: 0, content: ',' });
}
const isPreviouisSettingIncludesComment = previousSettingCommaOffset !== undefined && previousSettingCommaOffset > node.endOffset;
edits.push({
offset: isPreviouisSettingIncludesComment ? previousSettingCommaOffset! + 1 : node.endOffset,
length: 0,
content: nextSettingNode ? eol + newProperty + ',' : eol + newProperty
});
}
/* Insert after the comment at the end */
const edits = [{ offset: node.endOffset, length: 0, content: eol + newProperty }];
const previousSettingNode = findPreviousSettingNode(location.index, tree);
if (previousSettingNode && !previousSettingNode.setting!.hasCommaSeparator) {
edits.splice(0, 0, { offset: previousSettingNode.endOffset, length: 0, content: ',' });
}
return edits;
}
@@ -516,7 +524,7 @@ interface INode {
readonly value: string;
readonly setting?: {
readonly key: string;
readonly hasCommaSeparator: boolean;
readonly commaOffset: number | undefined;
};
readonly comment?: string;
}
@@ -547,7 +555,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -564,7 +572,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -577,7 +585,7 @@ function parseSettings(content: string): INode[] {
value: content.substring(startOffset, offset + length),
setting: {
key,
hasCommaSeparator: false
commaOffset: undefined
}
});
}
@@ -585,15 +593,21 @@ function parseSettings(content: string): INode[] {
onSeparator: (sep: string, offset: number, length: number) => {
if (hierarchyLevel === 0) {
if (sep === ',') {
const node = nodes.pop();
let index = nodes.length - 1;
for (; index >= 0; index--) {
if (nodes[index].setting) {
break;
}
}
const node = nodes[index];
if (node) {
nodes.push({
nodes.splice(index, 1, {
startOffset: node.startOffset,
endOffset: node.endOffset,
value: node.value,
setting: {
key: node.setting!.key,
hasCommaSeparator: true
commaOffset: offset
}
});
}

View File

@@ -14,11 +14,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Edit } from 'vs/base/common/jsonFormatter';
import { setProperty, applyEdits } from 'vs/base/common/jsonEdit';
export interface ISettingsSyncContent {
settings: string;
@@ -41,6 +44,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
constructor(
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -50,7 +54,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
@ITelemetryService telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
}
protected setStatus(status: SyncStatus): void {
@@ -257,6 +261,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (settingsSyncContent) {
const fileContent = await this.getLocalFileContent();
const formatUtils = await this.getFormattingOptions();
const ignoredSettings = await this.getIgnoredSettings();
const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<IFileSyncPreviewResult>({
fileContent,
remoteUserData,
lastSyncUserData,
content,
hasLocalChanged: true,
hasRemoteChanged: true,
hasConflicts: false,
}));
await this.apply();
}
}
private async apply(forcePush?: boolean): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
@@ -357,7 +382,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null;
}
private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null {
try {
const parsed = <ISettingsSyncContent>JSON.parse(syncContent);
return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent };
@@ -391,4 +416,49 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
}
}
async recoverSettings(): Promise<void> {
try {
const fileContent = await this.getLocalFileContent();
if (!fileContent) {
return;
}
const syncData: ISyncData = JSON.parse(fileContent.value.toString());
if (!isSyncData(syncData)) {
return;
}
this.telemetryService.publicLog2('sync/settingsCorrupted');
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
if (!settingsSyncContent || !settingsSyncContent.settings) {
return;
}
let settings = settingsSyncContent.settings;
const formattingOptions = await this.getFormattingOptions();
for (const key in syncData) {
if (['version', 'content', 'machineId'].indexOf(key) === -1 && (syncData as any)[key] !== undefined) {
const edits: Edit[] = setProperty(settings, [key], (syncData as any)[key], formattingOptions);
if (edits.length) {
settings = applyEdits(settings, edits);
}
}
}
await this.fileService.writeFile(this.file, VSBuffer.fromString(settings));
} catch (e) {/* ignore */ }
}
}
function isSyncData(thing: any): thing is ISyncData {
if (thing
&& (thing.version !== undefined && typeof thing.version === 'number')
&& (thing.content !== undefined && typeof thing.content === 'string')
&& (thing.machineId !== undefined && typeof thing.machineId === 'string')
) {
return true;
}
return false;
}

View File

@@ -26,7 +26,7 @@ export function merge(local: IStringDictionary<string>, remote: IStringDictionar
removed: values(removed),
updated,
conflicts: [],
remote: local
remote: Object.keys(local).length > 0 ? local : null
};
}

View File

@@ -16,6 +16,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
interface ISinppetsSyncPreviewResult extends ISyncPreviewResult {
readonly local: IStringDictionary<IFileContent>;
@@ -39,6 +40,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -46,7 +48,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
this.snippetsFolder = environmentService.snippetsHome;
this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
@@ -70,7 +72,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
// Otherwise fire change event
else {
this._onDidChangeLocal.fire();
this.triggerLocalChange();
}
}
@@ -256,6 +258,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
}
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> {
const local = await this.getSnippetsFileContents();
const localSnippets = this.toSnippetsContents(local);
const snippets = this.parseSnippets(syncData);
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
hasRemoteChanged: true
}));
await this.apply();
}
protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
@@ -285,7 +300,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null;
if (remoteSnippets) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`);

View File

@@ -3,24 +3,29 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { timeout, Delayer } from 'vs/base/common/async';
import { Delayer, disposableTimeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
type AutoSyncTriggerClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
type AutoSyncClassification = {
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export const RESOURCE_ENABLEMENT_SOURCE = 'resourceEnablement';
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
_serviceBrand: any;
private enabled: boolean = false;
private readonly autoSync = this._register(new MutableDisposable<AutoSync>());
private successiveFailures: number = 0;
private readonly syncDelayer: Delayer<void>;
private lastSyncTriggerTime: number | undefined = undefined;
private readonly syncTriggerDelayer: Delayer<void>;
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
readonly onError: Event<UserDataSyncError> = this._onError.event;
@@ -31,100 +36,157 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.updateEnablement(false, true);
this.syncDelayer = this._register(new Delayer<void>(0));
this._register(Event.any<any>(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false)));
this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync(['resourceEnablement'])));
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
if (getUserDataSyncStore(this.productService, this.configurationService)) {
this.updateAutoSync();
this._register(Event.any(authTokenService.onDidChangeToken, this.userDataSyncEnablementService.onDidChangeEnablement)(() => this.updateAutoSync()));
this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerAutoSync([RESOURCE_ENABLEMENT_SOURCE])));
}
}
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
const { enabled, reason } = await this.isAutoSyncEnabled();
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Auto Sync: Started');
this.sync(true, auto);
return;
} else {
this.resetFailures();
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Auto Sync: stopped because', reason);
}
}
}
private async sync(loop: boolean, auto: boolean): Promise<void> {
if (this.enabled) {
try {
await this.userDataSyncService.sync();
this.resetFailures();
} catch (e) {
const error = UserDataSyncError.toUserDataSyncError(e);
if (error.code === UserDataSyncErrorCode.TurnedOff || error.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
this.logService.info('Auto Sync: Resetting the local sync state.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Completed resetting the local sync state.');
if (auto) {
this.userDataSyncEnablementService.setEnablement(false);
this._onError.fire(error);
return;
} else {
return this.sync(loop, auto);
}
private updateAutoSync(): void {
const { enabled, reason } = this.isAutoSyncEnabled();
if (enabled) {
if (this.autoSync.value === undefined) {
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService);
this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e)));
if (this.startAutoSync()) {
this.autoSync.value.start();
}
this.logService.error(error);
this.successiveFailures++;
this._onError.fire(error);
}
if (loop) {
await timeout(1000 * 60 * 5);
this.sync(loop, true);
}
} else {
this.logService.trace('Auto Sync: Not syncing as it is disabled.');
if (this.autoSync.value !== undefined) {
this.logService.info('Auto Sync: Disabled because', reason);
this.autoSync.clear();
}
}
}
private async isAutoSyncEnabled(): Promise<{ enabled: boolean, reason?: string }> {
// For tests purpose only
protected startAutoSync(): boolean { return true; }
private isAutoSyncEnabled(): { enabled: boolean, reason?: string } {
if (!this.userDataSyncEnablementService.isEnabled()) {
return { enabled: false, reason: 'sync is disabled' };
}
if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
return { enabled: false, reason: 'sync is not initialized' };
}
const token = await this.authTokenService.getToken();
if (!token) {
if (!this.authTokenService.token) {
return { enabled: false, reason: 'token is not avaialable' };
}
return { enabled: true };
}
private resetFailures(): void {
this.successiveFailures = 0;
private async onDidFinishSync(error: Error | undefined): Promise<void> {
if (!error) {
// Sync finished without errors
this.successiveFailures = 0;
return;
}
// Error while syncing
const userDataSyncError = UserDataSyncError.toUserDataSyncError(error);
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Sync is turned off in the cloud.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Did reset the local sync state.');
this.userDataSyncEnablementService.setEnablement(false);
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
} else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) {
this.userDataSyncEnablementService.setEnablement(false);
this.logService.info('Auto Sync: Turned off sync because of making too many requests to server');
} else {
this.logService.error(userDataSyncError);
this.successiveFailures++;
}
this._onError.fire(userDataSyncError);
}
private sources: string[] = [];
async triggerAutoSync(sources: string[]): Promise<void> {
sources.forEach(source => this.telemetryService.publicLog2<{ source: string }, AutoSyncTriggerClassification>('sync/triggerAutoSync', { source }));
if (this.enabled) {
return this.syncDelayer.trigger(() => {
this.logService.info('Auto Sync: Triggered.');
return this.sync(false, true);
}, this.successiveFailures
? 1000 * 1 * Math.min(this.successiveFailures, 60) /* Delay by number of seconds as number of failures up to 1 minute */
: 1000);
} else {
this.syncDelayer.cancel();
if (this.autoSync.value === undefined) {
return this.syncTriggerDelayer.cancel();
}
/*
If sync is not triggered by sync resource (triggered by other sources like window focus etc.,) or by resource enablement
then limit sync to once per 10s
*/
const hasToLimitSync = sources.indexOf(RESOURCE_ENABLEMENT_SOURCE) === -1 && ALL_SYNC_RESOURCES.every(syncResource => sources.indexOf(syncResource) === -1);
if (hasToLimitSync && this.lastSyncTriggerTime
&& Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) {
this.logService.debug('Auto Sync Skipped: Limited to once per 10 seconds.');
return;
}
this.sources.push(...sources);
return this.syncTriggerDelayer.trigger(async () => {
this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources });
this.sources = [];
if (this.autoSync.value) {
await this.autoSync.value.sync('Activity');
}
}, this.successiveFailures
? 1000 * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */
: 1000); /* Debounce for a second if there are no failures */
}
}
class AutoSync extends Disposable {
private static readonly INTERVAL_SYNCING = 'Interval';
private readonly intervalHandler = this._register(new MutableDisposable<IDisposable>());
private readonly _onDidStartSync = this._register(new Emitter<void>());
readonly onDidStartSync = this._onDidStartSync.event;
private readonly _onDidFinishSync = this._register(new Emitter<Error | undefined>());
readonly onDidFinishSync = this._onDidFinishSync.event;
constructor(
private readonly interval: number /* in milliseconds */,
private readonly userDataSyncService: IUserDataSyncService,
private readonly logService: IUserDataSyncLogService,
) {
super();
}
start(): void {
this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync()));
this._register(toDisposable(() => {
this.userDataSyncService.stop();
this.logService.info('Auto Sync: Stopped');
}));
this.logService.info('Auto Sync: Started');
this.sync(AutoSync.INTERVAL_SYNCING);
}
private waitUntilNextIntervalAndSync(): void {
this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval);
}
async sync(reason: string): Promise<void> {
this.logService.info(`Auto Sync: Triggered by ${reason}`);
this._onDidStartSync.fire();
let error: Error | undefined;
try {
await this.userDataSyncService.sync();
} catch (e) {
this.logService.error(e);
error = e;
}
this._onDidFinishSync.fire(error);
}
register<T extends IDisposable>(t: T): T {
return super._register(t);
}
}

View File

@@ -6,7 +6,6 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { localize } from 'vs/nls';
@@ -149,7 +148,7 @@ export const enum SyncResource {
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState];
export interface IUserDataManifest {
latest?: Record<SyncResource, string>
latest?: Record<ServerResource, string>
session: string;
}
@@ -159,16 +158,17 @@ export interface IResourceRefHandle {
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export type ServerResource = SyncResource | 'machines';
export interface IUserDataSyncStoreService {
_serviceBrand: undefined;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
read(resource: SyncResource, oldValue: IUserData | null): Promise<IUserData>;
write(resource: SyncResource, content: string, ref: string | null): Promise<string>;
read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData>;
write(resource: ServerResource, content: string, ref: string | null): Promise<string>;
manifest(): Promise<IUserDataManifest | null>;
clear(): Promise<void>;
getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: SyncResource, ref: string): Promise<string | null>;
delete(resource: SyncResource): Promise<void>;
getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]>;
resolveContent(resource: ServerResource, ref: string): Promise<string | null>;
delete(resource: ServerResource): Promise<void>;
}
export const IUserDataSyncBackupStoreService = createDecorator<IUserDataSyncBackupStoreService>('IUserDataSyncBackupStoreService');
@@ -184,17 +184,20 @@ export interface IUserDataSyncBackupStoreService {
// #region User Data Sync Error
export enum UserDataSyncErrorCode {
// Server Errors
Unauthorized = 'Unauthorized',
Forbidden = 'Forbidden',
// Client Errors (>= 400 )
Unauthorized = 'Unauthorized', /* 401 */
PreconditionFailed = 'PreconditionFailed', /* 412 */
TooLarge = 'TooLarge', /* 413 */
UpgradeRequired = 'UpgradeRequired', /* 426 */
PreconditionRequired = 'PreconditionRequired', /* 428 */
TooManyRequests = 'RemoteTooManyRequests', /* 429 */
// Local Errors
ConnectionRefused = 'ConnectionRefused',
RemotePreconditionFailed = 'RemotePreconditionFailed',
TooLarge = 'TooLarge',
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',
// Local Errors
LocalTooManyRequests = 'LocalTooManyRequests',
LocalPreconditionFailed = 'LocalPreconditionFailed',
LocalInvalidContent = 'LocalInvalidContent',
LocalError = 'LocalError',
@@ -223,7 +226,11 @@ export class UserDataSyncError extends Error {
}
export class UserDataSyncStoreError extends UserDataSyncError { }
export class UserDataSyncStoreError extends UserDataSyncError {
constructor(message: string, code: UserDataSyncErrorCode) {
super(message, code);
}
}
//#endregion
@@ -233,6 +240,7 @@ export interface ISyncExtension {
identifier: IExtensionIdentifier;
version?: string;
disabled?: boolean;
installed?: boolean;
}
export interface IStorageValue {
@@ -274,7 +282,8 @@ export interface IUserDataSynchroniser {
pull(): Promise<void>;
push(): Promise<void>;
sync(ref?: string): Promise<void>;
sync(manifest: IUserDataManifest | null): Promise<void>;
replace(uri: URI): Promise<boolean>;
stop(): Promise<void>;
getSyncPreview(): Promise<ISyncPreviewResult>
@@ -288,6 +297,7 @@ export interface IUserDataSynchroniser {
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getMachineId(syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}
//#endregion
@@ -330,6 +340,7 @@ export interface IUserDataSyncService {
pull(): Promise<void>;
sync(): Promise<void>;
stop(): Promise<void>;
replace(uri: URI): Promise<void>;
reset(): Promise<void>;
resetLocal(): Promise<void>;
@@ -340,6 +351,7 @@ export interface IUserDataSyncService {
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
}
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
@@ -369,9 +381,6 @@ export interface IConflictSetting {
//#endregion
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export const PREVIEW_DIR_NAME = 'preview';
export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined {
if (localPreview.scheme === USER_DATA_SYNC_SCHEME) {

View File

@@ -11,10 +11,12 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
export class UserDataSyncChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncService) { }
constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
@@ -27,12 +29,23 @@ export class UserDataSyncChannel implements IServerChannel {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
async call(context: any, command: string, args?: any): Promise<any> {
try {
const result = await this._call(context, command, args);
return result;
} catch (e) {
this.logService.error(e);
throw e;
}
}
private _call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]);
case 'pull': return this.service.pull();
case 'sync': return this.service.sync();
case 'stop': this.service.stop(); return Promise.resolve();
case 'replace': return this.service.replace(URI.revive(args[0]));
case 'reset': return this.service.reset();
case 'resetLocal': return this.service.resetLocal();
case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge();
@@ -41,6 +54,7 @@ export class UserDataSyncChannel implements IServerChannel {
case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]);
case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]);
case 'getAssociatedResources': return this.service.getAssociatedResources(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
case 'getMachineId': return this.service.getMachineId(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) });
}
throw new Error('Invalid call');
}
@@ -151,3 +165,24 @@ export class StorageKeysSyncRegistryChannelClient extends Disposable implements
}
}
export class UserDataSyncMachinesServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncMachinesService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
async call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getMachines': return this.service.getMachines();
case 'addCurrentMachine': return this.service.addCurrentMachine(args[0]);
case 'removeCurrentMachine': return this.service.removeCurrentMachine();
case 'renameMachine': return this.service.renameMachine(args[0], args[1]);
case 'disableMachine': return this.service.disableMachine(args[0]);
}
throw new Error('Invalid call');
}
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Disposable } from 'vs/base/common/lifecycle';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
interface IMachineData {
id: string;
name: string;
disabled?: boolean;
}
interface IMachinesData {
version: number;
machines: IMachineData[];
}
export type IUserDataSyncMachine = Readonly<IMachineData> & { readonly isCurrent: boolean };
export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachinesService>('IUserDataSyncMachinesService');
export interface IUserDataSyncMachinesService {
_serviceBrand: any;
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void>;
removeCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
renameMachine(machineId: string, name: string): Promise<void>;
disableMachine(machineId: string): Promise<void>
}
export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
private static readonly VERSION = 1;
private static readonly RESOURCE = 'machines';
_serviceBrand: any;
private readonly currentMachineIdPromise: Promise<string>;
private userData: IUserData | null = null;
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IProductService private readonly productService: IProductService,
) {
super();
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
}
async getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
return machineData.machines.map<IUserDataSyncMachine>(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } }));
}
async addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
let currentMachine = machineData.machines.find(({ id }) => id === currentMachineId);
if (currentMachine) {
currentMachine.name = name;
} else {
machineData.machines.push({ id: currentMachineId, name });
}
await this.writeMachinesData(machineData);
}
async removeCurrentMachine(manifest?: IUserDataManifest): Promise<void> {
const currentMachineId = await this.currentMachineIdPromise;
const machineData = await this.readMachinesData(manifest);
const updatedMachines = machineData.machines.filter(({ id }) => id !== currentMachineId);
if (updatedMachines.length !== machineData.machines.length) {
machineData.machines = updatedMachines;
await this.writeMachinesData(machineData);
}
}
async renameMachine(machineId: string, name: string, manifest?: IUserDataManifest): Promise<void> {
const machineData = await this.readMachinesData(manifest);
const currentMachine = machineData.machines.find(({ id }) => id === machineId);
if (currentMachine) {
currentMachine.name = name;
await this.writeMachinesData(machineData);
}
}
async disableMachine(machineId: string): Promise<void> {
const machineData = await this.readMachinesData();
const machine = machineData.machines.find(({ id }) => id === machineId);
if (machine) {
machine.disabled = true;
await this.writeMachinesData(machineData);
}
}
private async readMachinesData(manifest?: IUserDataManifest): Promise<IMachinesData> {
this.userData = await this.readUserData(manifest);
const machinesData = this.parse(this.userData);
if (machinesData.version !== UserDataSyncMachinesService.VERSION) {
throw new Error(localize('error incompatible', "Cannot read machines data as the current version is incompatible. Please update {0} and try again.", this.productService.nameLong));
}
return machinesData;
}
private async writeMachinesData(machinesData: IMachinesData): Promise<void> {
const content = JSON.stringify(machinesData);
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
this.userData = { ref, content };
}
private async readUserData(manifest?: IUserDataManifest): Promise<IUserData> {
if (this.userData) {
const latestRef = manifest && manifest.latest ? manifest.latest[UserDataSyncMachinesService.RESOURCE] : undefined;
// Last time synced resource and latest resource on server are same
if (this.userData.ref === latestRef) {
return this.userData;
}
// There is no resource on server and last time it was synced with no resource
if (latestRef === undefined && this.userData.content === null) {
return this.userData;
}
}
return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData);
}
private parse(userData: IUserData): IMachinesData {
if (userData.content !== null) {
try {
return JSON.parse(userData.content);
} catch (e) {
this.logService.error(e);
}
}
return {
version: UserDataSyncMachinesService.VERSION,
machines: []
};
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
@@ -19,9 +19,14 @@ import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
import { Throttler } from 'vs/base/common/async';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IProductService } from 'vs/platform/product/common/productService';
import { platform, PlatformToString } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
type SyncErrorClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
type SyncClassification = {
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const SESSION_ID_KEY = 'sync.sessionId';
@@ -31,6 +36,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
_serviceBrand: any;
private readonly syncThrottler: Throttler;
private readonly synchronisers: IUserDataSynchroniser[];
private _status: SyncStatus = SyncStatus.Uninitialized;
@@ -65,9 +71,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
@IProductService private readonly productService: IProductService
) {
super();
this.syncThrottler = new Throttler();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser));
@@ -87,31 +96,55 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async pull(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.pull();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}
async push(): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
try {
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.push();
} catch (e) {
this.handleSynchronizerError(e, synchroniser.resource);
}
}
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
}
this.updateLastSyncTime();
}
private recoveredSettings: boolean = false;
async sync(): Promise<void> {
await this.checkEnablement();
if (!this.recoveredSettings) {
await this.settingsSynchroniser.recoverSettings();
this.recoveredSettings = true;
}
await this.syncThrottler.queue(() => this.doSync());
}
private async doSync(): Promise<void> {
const startTime = new Date().getTime();
this._syncErrors = [];
try {
@@ -120,11 +153,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.setStatus(SyncStatus.Syncing);
}
this.telemetryService.publicLog2('sync/getmanifest');
let manifest = await this.userDataSyncStoreService.manifest();
// Server has no data but this machine was synced before
if (manifest === null && await this.hasPreviouslySynced()) {
// Sync was turned off from other machine
// Sync was turned off in the cloud
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
@@ -134,11 +168,22 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
const currentMachine = machines.find(machine => machine.isCurrent);
// Check if sync was turned off from other machine
if (currentMachine?.disabled) {
// Unset the current machine
await this.userDataSyncMachinesService.removeCurrentMachine(manifest || undefined);
// Throw TurnedOff error
throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff);
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resource] : undefined);
await synchroniser.sync(manifest);
} catch (e) {
this.handleSyncError(e, synchroniser.resource);
this.handleSynchronizerError(e, synchroniser.resource);
this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]);
}
}
@@ -153,15 +198,34 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
if (!currentMachine) {
const name = this.computeDefaultMachineName(machines);
await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined);
}
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
this.updateLastSyncTime();
} catch (error) {
if (error instanceof UserDataSyncError) {
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource });
}
throw error;
} finally {
this.updateStatus();
this._onSyncErrors.fire(this._syncErrors);
}
}
async replace(uri: URI): Promise<void> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.replace(uri)) {
return;
}
}
}
async stop(): Promise<void> {
await this.checkEnablement();
if (this.status === SyncStatus.Idle) {
@@ -209,6 +273,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
}
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined> {
return this.getSynchroniser(resource).getMachineId(syncResourceHandle);
}
async isFirstTimeSyncWithMerge(): Promise<boolean> {
await this.checkEnablement();
if (!await this.userDataSyncStoreService.manifest()) {
@@ -232,16 +300,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async reset(): Promise<void> {
await this.checkEnablement();
await this.resetRemote();
await this.resetLocal();
await this.resetLocal(true);
}
async resetLocal(): Promise<void> {
async resetLocal(donotUnsetMachine?: boolean): Promise<void> {
await this.checkEnablement();
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL);
if (!donotUnsetMachine) {
await this.userDataSyncMachinesService.removeCurrentMachine();
}
for (const synchroniser of this.synchronisers) {
try {
synchroniser.resetLocal();
await synchroniser.resetLocal();
} catch (e) {
this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`);
this.logService.error(e);
@@ -322,13 +393,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
private handleSyncError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncStoreError) {
private handleSynchronizerError(e: Error, source: SyncResource): void {
if (e instanceof UserDataSyncError) {
switch (e.code) {
case UserDataSyncErrorCode.TooLarge:
this.telemetryService.publicLog2<{ source: string }, SyncErrorClassification>('sync/errorTooLarge', { source });
case UserDataSyncErrorCode.TooManyRequests:
case UserDataSyncErrorCode.LocalTooManyRequests:
case UserDataSyncErrorCode.UpgradeRequired:
case UserDataSyncErrorCode.Incompatible:
throw e;
}
throw e;
}
this.logService.error(e);
this.logService.error(`${source}: ${toErrorMessage(e)}`);
@@ -339,6 +413,20 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
.map(s => ({ syncResource: s.resource, conflicts: s.conflicts }));
}
private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string {
const namePrefix = `${this.productService.nameLong} (${PlatformToString(platform)})`;
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
let nameIndex = 0;
for (const machine of machines) {
const matches = nameRegEx.exec(machine.name);
const index = matches ? parseInt(matches[1]) : 0;
nameIndex = index > nameIndex ? index : nameIndex;
}
return `${namePrefix} #${nameIndex + 1}`;
}
getSynchroniser(source: SyncResource): IUserDataSynchroniser {
return this.synchronisers.filter(s => s.resource === source)[0];
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
import { joinPath, relativePath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -15,9 +15,15 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { assign } from 'vs/base/common/objects';
import { generateUuid } from 'vs/base/common/uuid';
import { isWeb } from 'vs/base/common/platform';
const USER_SESSION_ID_KEY = 'sync.user-session-id';
const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id';
const REQUEST_SESSION_LIMIT = 100;
const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
@@ -25,6 +31,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
readonly userDataSyncStore: IUserDataSyncStore | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
private readonly session: RequestsSession;
constructor(
@IProductService productService: IProductService,
@@ -34,21 +41,28 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
.then(uuid => {
const headers: IHeaders = {
'X-Sync-Client-Id': productService.version,
'X-Client-Name': `${productService.applicationName}${isWeb ? '-web' : ''}`,
'X-Client-Version': productService.version,
'X-Machine-Id': uuid
};
headers['X-Sync-Machine-Id'] = uuid;
if (productService.commit) {
headers['X-Client-Commit'] = productService.commit;
}
return headers;
});
/* A requests session that limits requests per sessions */
this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService);
}
async getAllRefs(resource: SyncResource): Promise<IResourceRefHandle[]> {
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -56,17 +70,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const uri = joinPath(this.userDataSyncStore.url, 'resource', resource);
const headers: IHeaders = {};
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const result = await asJson<{ url: string, created: number }[]>(context) || [];
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
}
async resolveContent(resource: SyncResource, ref: string): Promise<string | null> {
async resolveContent(resource: ServerResource, ref: string): Promise<string | null> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -75,17 +89,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const headers: IHeaders = {};
headers['Cache-Control'] = 'no-cache';
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const content = await asText(context);
return content;
}
async delete(resource: SyncResource): Promise<void> {
async delete(resource: ServerResource): Promise<void> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -93,14 +107,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
const headers: IHeaders = {};
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
}
async read(resource: SyncResource, oldValue: IUserData | null): Promise<IUserData> {
async read(resource: ServerResource, oldValue: IUserData | null): Promise<IUserData> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -113,7 +127,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-None-Match'] = oldValue.ref;
}
const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (context.res.statusCode === 304) {
// There is no new value. Hence return the old value.
@@ -121,18 +135,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const ref = context.res.headers['etag'];
if (!ref) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
}
const content = await asText(context);
return { ref, content };
}
async write(resource: SyncResource, data: string, ref: string | null): Promise<string> {
async write(resource: ServerResource, data: string, ref: string | null): Promise<string> {
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
@@ -143,15 +157,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
headers['If-Match'] = ref;
}
const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None);
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource);
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
const newRef = context.res.headers['etag'];
if (!newRef) {
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource);
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef);
}
return newRef;
}
@@ -164,12 +178,30 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
const headers: IHeaders = { 'Content-Type': 'application/json' };
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
return asJson(context);
const manifest = await asJson<IUserDataManifest>(context);
const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
if (currentSessionId && manifest && currentSessionId !== manifest.session) {
// Server session is different from client session so clear cached session.
this.clearSession();
}
if (manifest === null && currentSessionId) {
// server session is cleared so clear cached session.
this.clearSession();
}
if (manifest) {
// update session
this.storageService.store(USER_SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
return manifest;
}
async clear(): Promise<void> {
@@ -180,17 +212,25 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
if (!isSuccess(context)) {
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
}
// clear cached session.
this.clearSession();
}
private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise<IRequestContext> {
const authToken = await this.authTokenService.getToken();
private clearSession(): void {
this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const authToken = this.authTokenService.token;
if (!authToken) {
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source);
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized);
}
const commonHeaders = await this.commonHeadersPromise;
@@ -199,34 +239,95 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
'authorization': `Bearer ${authToken.token}`,
});
// Add session headers
this.addSessionHeaders(options.headers);
this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } });
let context;
try {
context = await this.requestService.request(options, token);
context = await this.session.request(options, token);
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
} catch (e) {
throw new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
if (!(e instanceof UserDataSyncStoreError)) {
e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused);
}
throw e;
}
if (context.res.statusCode === 401) {
this.authTokenService.sendTokenFailed();
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
}
if (context.res.statusCode === 403) {
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source);
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized);
}
if (context.res.statusCode === 412) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.RemotePreconditionFailed, source);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed);
}
if (context.res.statusCode === 413) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source);
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge);
}
if (context.res.statusCode === 426) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired);
}
if (context.res.statusCode === 429) {
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests);
}
return context;
}
private addSessionHeaders(headers: IHeaders): void {
let machineSessionId = this.storageService.get(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
if (machineSessionId === undefined) {
machineSessionId = generateUuid();
this.storageService.store(MACHINE_SESSION_ID_KEY, machineSessionId, StorageScope.GLOBAL);
}
headers['X-Machine-Session-Id'] = machineSessionId;
const userSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
if (userSessionId !== undefined) {
headers['X-User-Session-Id'] = userSessionId;
}
}
}
export class RequestsSession {
private count: number = 0;
private startTime: Date | undefined = undefined;
constructor(
private readonly limit: number,
private readonly interval: number, /* in ms */
private readonly requestService: IRequestService,
) { }
request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
if (this.isExpired()) {
this.reset();
}
if (this.count >= this.limit) {
throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests);
}
this.startTime = this.startTime || new Date();
this.count++;
return this.requestService.request(options, token);
}
private isExpired(): boolean {
return this.startTime !== undefined && new Date().getTime() - this.startTime.getTime() > this.interval;
}
private reset(): void {
this.count = 0;
this.startTime = undefined;
}
}

View File

@@ -5,10 +5,12 @@
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@@ -19,8 +21,10 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IAuthenticationTokenService authTokenService: IAuthenticationTokenService,
@ITelemetryService telemetryService: ITelemetryService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService);
super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService, productService, configurationService);
this._register(Event.debounce<string, string[]>(Event.any<string>(
Event.map(electronService.onWindowFocus, () => 'windowFocus'),

View File

@@ -7,13 +7,13 @@ import * as assert from 'assert';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
suite('ExtensionsMerge - No Conflicts', () => {
suite('ExtensionsMerge', () => {
test('merge returns local extension if remote does not exist', async () => {
test('merge returns local extension if remote does not exist', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, null, null, [], []);
@@ -24,15 +24,15 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, localExtensions);
});
test('merge returns local extension if remote does not exist with ignored extensions', async () => {
test('merge returns local extension if remote does not exist with ignored extensions', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, null, null, [], ['a']);
@@ -43,15 +43,15 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', async () => {
test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, null, null, [], ['A']);
@@ -62,19 +62,19 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with skipped extensions', async () => {
test('merge returns local extension if remote does not exist with skipped extensions', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const skippedExtension: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, null, null, skippedExtension, []);
@@ -85,18 +85,18 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with skipped and ignored extensions', async () => {
test('merge returns local extension if remote does not exist with skipped and ignored extensions', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const skippedExtension: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, null, null, skippedExtension, ['a']);
@@ -107,180 +107,180 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when there is no base', async () => {
test('merge local and remote extensions when there is no base', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when there is no base and with ignored extensions', async () => {
test('merge local and remote extensions when there is no base and with ignored extensions', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], ['a']);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when remote is moved forwarded', async () => {
test('merge local and remote extensions when remote is moved forwarded', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote is moved forwarded with disabled extension', async () => {
test('merge local and remote extensions when remote is moved forwarded with disabled extension', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'd', uuid: 'd' }, disabled: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]);
assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true }]);
assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true }]);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote moved forwarded with ignored extensions', async () => {
test('merge local and remote extensions when remote moved forwarded with ignored extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote is moved forwarded with skipped extensions', async () => {
test('merge local and remote extensions when remote is moved forwarded with skipped extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', async () => {
test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']);
assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when local is moved forwarded', async () => {
test('merge local and remote extensions when local is moved forwarded', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
@@ -291,19 +291,19 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, localExtensions);
});
test('merge local and remote extensions when local is moved forwarded with disabled extensions', async () => {
test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, disabled: true },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, disabled: true, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
@@ -314,18 +314,18 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, localExtensions);
});
test('merge local and remote extensions when local is moved forwarded with ignored settings', async () => {
test('merge local and remote extensions when local is moved forwarded with ignored settings', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']);
@@ -334,30 +334,30 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, [
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
]);
});
test('merge local and remote extensions when local is moved forwarded with skipped extensions', async () => {
test('merge local and remote extensions when local is moved forwarded with skipped extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
@@ -368,25 +368,25 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', async () => {
test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']);
@@ -397,54 +397,54 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded', async () => {
test('merge local and remote extensions when both moved forwarded', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]);
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with ignored extensions', async () => {
test('merge local and remote extensions when both moved forwarded with ignored extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']);
@@ -455,58 +455,58 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with skipped extensions', async () => {
test('merge local and remote extensions when both moved forwarded with skipped extensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', async () => {
test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'e', uuid: 'e' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'e', uuid: 'e' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']);
@@ -517,30 +517,134 @@ suite('ExtensionsMerge - No Conflicts', () => {
assert.deepEqual(actual.remote, expected);
});
test('merge when remote extension has no uuid and different extension id case', async () => {
test('merge when remote extension has no uuid and different extension id case', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'A' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'A' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'A', uuid: 'a' } },
{ identifier: { id: 'd', uuid: 'd' } },
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'c', uuid: 'c' } },
{ identifier: { id: 'A', uuid: 'a' }, installed: true },
{ identifier: { id: 'd', uuid: 'd' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' }, installed: true },
{ identifier: { id: 'c', uuid: 'c' }, installed: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' } }]);
assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge when remote extension is not an installed extension', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
{ identifier: { id: 'b', uuid: 'b' } },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, null);
});
test('merge when remote extension is not an installed extension but is an installed extension locally', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, localExtensions);
});
test('merge when an extension is not an installed extension remotely and does not exist locally', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
{ identifier: { id: 'b', uuid: 'b' } },
];
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, null);
});
test('merge when an extension is an installed extension remotely but not locally and updated locally', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, disabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true },
];
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge when an extension is an installed extension remotely but not locally and updated remotely', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true },
];
const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, remoteExtensions);
assert.deepEqual(actual.remote, null);
});
test('merge not installed extensions', () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' } },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' } },
{ identifier: { id: 'a', uuid: 'a' } },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
});

View File

@@ -44,11 +44,58 @@ suite('GlobalStateSync', () => {
teardown(() => disposableStore.clear());
test('when global state does not exist', async () => {
assert.deepEqual(await testObject.getLastSyncUserData(), null);
let manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
]);
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(lastSyncUserData!.syncData, null);
manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
});
test('when global state is created after first sync', async () => {
await testObject.sync(await testClient.manifest());
updateStorage('a', 'value1', testClient);
let lastSyncUserData = await testObject.getLastSyncUserData();
const manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
]);
lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.deepEqual(JSON.parse(lastSyncUserData!.syncData!.content).storage, { 'a': { version: 1, value: 'value1' } });
});
test('first time sync - outgoing to server (no state)', async () => {
updateStorage('a', 'value1', testClient);
await updateLocale(testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -63,7 +110,7 @@ suite('GlobalStateSync', () => {
await updateLocale(client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -76,7 +123,7 @@ suite('GlobalStateSync', () => {
await client2.sync();
updateStorage('b', 'value2', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -94,7 +141,7 @@ suite('GlobalStateSync', () => {
await client2.sync();
updateStorage('a', 'value2', client2);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -109,10 +156,10 @@ suite('GlobalStateSync', () => {
test('sync adding a storage value', async () => {
updateStorage('a', 'value1', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
updateStorage('b', 'value2', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -127,10 +174,10 @@ suite('GlobalStateSync', () => {
test('sync updating a storage value', async () => {
updateStorage('a', 'value1', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
updateStorage('a', 'value2', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -145,10 +192,10 @@ suite('GlobalStateSync', () => {
test('sync removing a storage value', async () => {
updateStorage('a', 'value1', testClient);
updateStorage('b', 'value2', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
removeStorage('b', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
import { VSBuffer } from 'vs/base/common/buffer';
suite('KeybindingsSync', () => {
const disposableStore = new DisposableStore();
const server = new UserDataSyncTestServer();
let client: UserDataSyncClient;
let testObject: KeybindingsSynchroniser;
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp(true);
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Keybindings) as KeybindingsSynchroniser;
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
});
teardown(() => disposableStore.clear());
test('when keybindings file does not exist', async () => {
const fileService = client.instantiationService.get(IFileService);
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
assert.deepEqual(await testObject.getLastSyncUserData(), null);
let manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
]);
assert.ok(!await fileService.exists(keybindingsResource));
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(lastSyncUserData!.syncData, null);
manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
});
test('when keybindings file is created after first sync', async () => {
const fileService = client.instantiationService.get(IFileService);
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
await testObject.sync(await client.manifest());
await fileService.createFile(keybindingsResource, VSBuffer.fromString('[]'));
let lastSyncUserData = await testObject.getLastSyncUserData();
const manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
]);
lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]');
});
});

View File

@@ -1495,8 +1495,102 @@ suite('SettingsMerge - Add Setting', () => {
assert.equal(actual, sourceContent);
});
test('Insert after a comment with comma separator of previous setting and no next nodes ', () => {
const sourceContent = `
{
"a": 1
// this is comment for a
,
"b": 2
}`;
const targetContent = `
{
"a": 1
// this is comment for a
,
}`;
const expected = `
{
"a": 1
// this is comment for a
,
"b": 2
}`;
const actual = addSetting('b', sourceContent, targetContent, formattingOptions);
assert.equal(actual, expected);
});
test('Insert after a comment with comma separator of previous setting and there is a setting after ', () => {
const sourceContent = `
{
"a": 1
// this is comment for a
,
"b": 2,
"c": 3
}`;
const targetContent = `
{
"a": 1
// this is comment for a
,
"c": 3
}`;
const expected = `
{
"a": 1
// this is comment for a
,
"b": 2,
"c": 3
}`;
const actual = addSetting('b', sourceContent, targetContent, formattingOptions);
assert.equal(actual, expected);
});
test('Insert after a comment with comma separator of previous setting and there is a comment after ', () => {
const sourceContent = `
{
"a": 1
// this is comment for a
,
"b": 2
// this is a comment
}`;
const targetContent = `
{
"a": 1
// this is comment for a
,
// this is a comment
}`;
const expected = `
{
"a": 1
// this is comment for a
,
"b": 2
// this is a comment
}`;
const actual = addSetting('b', sourceContent, targetContent, formattingOptions);
assert.equal(actual, expected);
});
});
function stringify(value: any): string {
return JSON.stringify(value, null, '\t');
}

View File

@@ -15,6 +15,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { Event } from 'vs/base/common/event';
suite('SettingsSync', () => {
@@ -43,13 +44,67 @@ suite('SettingsSync', () => {
setup(async () => {
client = disposableStore.add(new UserDataSyncClient(server));
await client.setUp();
await client.setUp(true);
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser;
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
});
teardown(() => disposableStore.clear());
test('when settings file does not exist', async () => {
const fileService = client.instantiationService.get(IFileService);
const settingResource = client.instantiationService.get(IEnvironmentService).settingsResource;
assert.deepEqual(await testObject.getLastSyncUserData(), null);
let manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
]);
assert.ok(!await fileService.exists(settingResource));
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(lastSyncUserData!.syncData, null);
manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
});
test('when settings file is created after first sync', async () => {
const fileService = client.instantiationService.get(IFileService);
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
await testObject.sync(await client.manifest());
await fileService.createFile(settingsResource, VSBuffer.fromString('{}'));
let lastSyncUserData = await testObject.getLastSyncUserData();
const manifest = await client.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
]);
lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}');
});
test('sync for first time to the server', async () => {
const expected =
`{
@@ -75,7 +130,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(expected);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -99,7 +154,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -130,7 +185,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -161,7 +216,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -185,7 +240,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -203,7 +258,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -213,6 +268,24 @@ suite('SettingsSync', () => {
}`);
});
test('local change event is triggered when settings are changed', async () => {
const content =
`{
"files.autoSave": "afterDelay",
"files.simpleDialog.enable": true,
}`;
await updateSettings(content);
await testObject.sync(await client.manifest());
const promise = Event.toPromise(testObject.onDidChangeLocal);
await updateSettings(`{
"files.autoSave": "off",
"files.simpleDialog.enable": true,
}`);
await promise;
});
test('do not sync ignored settings', async () => {
const settingsContent =
`{
@@ -237,7 +310,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -285,7 +358,7 @@ suite('SettingsSync', () => {
}`;
await updateSettings(settingsContent);
await testObject.sync();
await testObject.sync(await client.manifest());
const { content } = await client.read(testObject.resource);
assert.ok(content !== null);
@@ -333,7 +406,7 @@ suite('SettingsSync', () => {
await updateSettings(expected);
try {
await testObject.sync();
await testObject.sync(await client.manifest());
assert.fail('should fail with invalid content error');
} catch (e) {
assert.ok(e instanceof UserDataSyncError);

View File

@@ -167,11 +167,62 @@ suite('SnippetsSync', () => {
teardown(() => disposableStore.clear());
test('when snippets does not exist', async () => {
const fileService = testClient.instantiationService.get(IFileService);
const snippetsResource = testClient.instantiationService.get(IEnvironmentService).snippetsHome;
assert.deepEqual(await testObject.getLastSyncUserData(), null);
let manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} },
]);
assert.ok(!await fileService.exists(snippetsResource));
const lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.equal(lastSyncUserData!.syncData, null);
manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, []);
});
test('when snippet is created after first sync', async () => {
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet1, testClient);
let lastSyncUserData = await testObject.getLastSyncUserData();
const manifest = await testClient.manifest();
server.reset();
await testObject.sync(manifest);
assert.deepEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } },
]);
lastSyncUserData = await testObject.getLastSyncUserData();
const remoteUserData = await testObject.getRemoteUserData(null);
assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref);
assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData);
assert.deepEqual(lastSyncUserData!.syncData!.content, JSON.stringify({ 'html.json': htmlSnippet1 }));
});
test('first time sync - outgoing to server (no snippets)', async () => {
await updateSnippet('html.json', htmlSnippet1, testClient);
await updateSnippet('typescript.json', tsSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -186,7 +237,7 @@ suite('SnippetsSync', () => {
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -201,7 +252,7 @@ suite('SnippetsSync', () => {
await client2.sync();
await updateSnippet('typescript.json', tsSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -221,7 +272,7 @@ suite('SnippetsSync', () => {
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
@@ -234,7 +285,7 @@ suite('SnippetsSync', () => {
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet1);
@@ -259,7 +310,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
@@ -278,7 +329,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
let conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
@@ -299,7 +350,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet2, testClient);
await updateSnippet('typescript.json', tsSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
const conflicts = testObject.conflicts;
await testObject.acceptConflict(conflicts[0].local, htmlSnippet2);
@@ -324,10 +375,10 @@ suite('SnippetsSync', () => {
test('sync adding a snippet', async () => {
await updateSnippet('html.json', htmlSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('typescript.json', tsSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -345,12 +396,12 @@ suite('SnippetsSync', () => {
test('sync adding a snippet - accept', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -362,10 +413,10 @@ suite('SnippetsSync', () => {
test('sync updating a snippet', async () => {
await updateSnippet('html.json', htmlSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -381,12 +432,12 @@ suite('SnippetsSync', () => {
test('sync updating a snippet - accept', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet2, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -397,13 +448,13 @@ suite('SnippetsSync', () => {
test('sync updating a snippet - conflict', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet2, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet3, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json');
@@ -413,13 +464,13 @@ suite('SnippetsSync', () => {
test('sync updating a snippet - resolve conflict', async () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet2, client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet3, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2);
assert.equal(testObject.status, SyncStatus.Idle);
@@ -437,10 +488,10 @@ suite('SnippetsSync', () => {
test('sync removing a snippet', async () => {
await updateSnippet('html.json', htmlSnippet1, testClient);
await updateSnippet('typescript.json', tsSnippet1, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await removeSnippet('html.json', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -459,12 +510,12 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await removeSnippet('html.json', client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -478,13 +529,13 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await updateSnippet('html.json', htmlSnippet2, client2);
await client2.sync();
await removeSnippet('html.json', testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -499,13 +550,13 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await removeSnippet('html.json', client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.HasConflicts);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
@@ -517,13 +568,13 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await removeSnippet('html.json', client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3);
assert.equal(testObject.status, SyncStatus.Idle);
@@ -544,13 +595,13 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
await removeSnippet('html.json', client2);
await client2.sync();
await updateSnippet('html.json', htmlSnippet2, testClient);
await testObject.sync();
await testObject.sync(await testClient.manifest());
await testObject.acceptConflict(testObject.conflicts[0].local, '');
assert.equal(testObject.status, SyncStatus.Idle);
@@ -601,7 +652,7 @@ suite('SnippetsSync', () => {
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);
@@ -622,7 +673,7 @@ suite('SnippetsSync', () => {
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
await testObject.sync();
await testObject.sync(await testClient.manifest());
assert.equal(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflicts, []);

View File

@@ -7,7 +7,7 @@ import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { Barrier } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
@@ -39,9 +39,11 @@ class TestSynchroniser extends AbstractSynchroniser {
return this.syncResult.status || SyncStatus.Idle;
}
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> { }
async apply(ref: string): Promise<void> {
ref = await this.userDataSyncStoreService.write(this.resource, '', ref);
await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } });
const remoteUserData = await this.updateRemoteUserData('', ref);
await this.updateLastSyncUserData(remoteUserData);
}
async stop(): Promise<void> {
@@ -49,6 +51,16 @@ class TestSynchroniser extends AbstractSynchroniser {
this.syncBarrier.open();
}
async triggerLocalChange(): Promise<void> {
super.triggerLocalChange();
}
onDidTriggerLocalChangeCall: Emitter<void> = this._register(new Emitter<void>());
protected async doTriggerLocalChange(): Promise<void> {
await super.doTriggerLocalChange();
this.onDidTriggerLocalChangeCall.fire();
}
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
return { hasLocalChanged: false, hasRemoteChanged: false };
}
@@ -79,7 +91,7 @@ suite('TestSynchronizer', () => {
const promise = Event.toPromise(testObject.onDoSyncCall.event);
testObject.sync();
testObject.sync(await client.manifest());
await promise;
assert.deepEqual(actual, [SyncStatus.Syncing]);
@@ -94,7 +106,7 @@ suite('TestSynchronizer', () => {
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync();
await testObject.sync(await client.manifest());
assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]);
assert.deepEqual(testObject.status, SyncStatus.Idle);
@@ -107,7 +119,7 @@ suite('TestSynchronizer', () => {
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync();
await testObject.sync(await client.manifest());
assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
@@ -122,7 +134,7 @@ suite('TestSynchronizer', () => {
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
try {
await testObject.sync();
await testObject.sync(await client.manifest());
assert.fail('Should fail');
} catch (e) {
assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]);
@@ -134,12 +146,12 @@ suite('TestSynchronizer', () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
const promise = Event.toPromise(testObject.onDoSyncCall.event);
testObject.sync();
testObject.sync(await client.manifest());
await promise;
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync();
await testObject.sync(await client.manifest());
assert.deepEqual(actual, []);
assert.deepEqual(testObject.status, SyncStatus.Syncing);
@@ -154,7 +166,7 @@ suite('TestSynchronizer', () => {
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync();
await testObject.sync(await client.manifest());
assert.deepEqual(actual, []);
assert.deepEqual(testObject.status, SyncStatus.Idle);
@@ -164,11 +176,11 @@ suite('TestSynchronizer', () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncResult = { status: SyncStatus.HasConflicts };
testObject.syncBarrier.open();
await testObject.sync();
await testObject.sync(await client.manifest());
const actual: SyncStatus[] = [];
disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status)));
await testObject.sync();
await testObject.sync(await client.manifest());
assert.deepEqual(actual, []);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
@@ -178,7 +190,7 @@ suite('TestSynchronizer', () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
// Sync once
testObject.syncBarrier.open();
await testObject.sync();
await testObject.sync(await client.manifest());
testObject.syncBarrier = new Barrier();
// update remote data before syncing so that 412 is thrown by server
@@ -190,8 +202,9 @@ suite('TestSynchronizer', () => {
});
// Start sycing
const { ref } = await userDataSyncStoreService.read(testObject.resource, null);
await testObject.sync(ref);
const manifest = await client.manifest();
const ref = manifest!.latest![testObject.resource];
await testObject.sync(await client.manifest());
assert.deepEqual(server.requests, [
{ type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } },
@@ -200,5 +213,18 @@ suite('TestSynchronizer', () => {
]);
});
test('no requests are made to server when local change is triggered', async () => {
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
testObject.syncBarrier.open();
await testObject.sync(await client.manifest());
server.reset();
const promise = Event.toPromise(testObject.onDidTriggerLocalChangeCall.event);
await testObject.triggerLocalChange();
await promise;
assert.deepEqual(server.requests, []);
});
});

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
import { IUserDataSyncService, SyncResource, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
class TestUserDataAutoSyncService extends UserDataAutoSyncService {
protected startAutoSync(): boolean { return false; }
}
suite('UserDataAutoSyncService', () => {
const disposableStore = new DisposableStore();
teardown(() => disposableStore.clear());
test('test auto sync with sync resource change triggers sync', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
target.reset();
client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true);
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
// Trigger auto sync with settings change
await testObject.triggerAutoSync([SyncResource.Settings]);
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
test('test auto sync with sync resource change triggers sync for every change', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
target.reset();
client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true);
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
// Trigger auto sync with settings change multiple times
for (let counter = 0; counter < 2; counter++) {
await testObject.triggerAutoSync([SyncResource.Settings]);
}
assert.deepEqual(target.requests, [
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }
]);
});
test('test auto sync with non sync resource change triggers sync', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
target.reset();
client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true);
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
// Trigger auto sync with window focus once
await testObject.triggerAutoSync(['windowFocus']);
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
test('test auto sync with non sync resource change does not trigger continuous syncs', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
// Sync once and reset requests
await client.instantiationService.get(IUserDataSyncService).sync();
target.reset();
client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true);
const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService);
// Trigger auto sync with window focus multiple times
for (let counter = 0; counter < 2; counter++) {
await testObject.triggerAutoSync(['windowFocus']);
}
// Make sure only one request is made
assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]);
});
});

View File

@@ -6,7 +6,7 @@
import { IRequestService } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource } from 'vs/platform/userDataSync/common/userDataSync';
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { generateUuid } from 'vs/base/common/uuid';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
@@ -37,6 +37,7 @@ import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
export class UserDataSyncClient extends Disposable {
@@ -83,12 +84,13 @@ export class UserDataSyncClient extends Disposable {
this.instantiationService.stub(IRequestService, this.testServer);
this.instantiationService.stub(IAuthenticationTokenService, <Partial<IAuthenticationTokenService>>{
onDidChangeToken: new Emitter<IUserDataSyncAuthToken | undefined>().event,
async getToken() { return { authenticationProviderId: 'id', token: 'token' }; }
token: { authenticationProviderId: 'id', token: 'token' }
});
this.instantiationService.stub(IUserDataSyncLogService, logService);
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService));
this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService));
this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService());
this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService));
@@ -124,22 +126,31 @@ export class UserDataSyncClient extends Disposable {
return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null);
}
manifest(): Promise<IUserDataManifest | null> {
return this.instantiationService.get(IUserDataSyncStoreService).manifest();
}
}
const ALL_SERVER_RESOURCES: ServerResource[] = [...ALL_SYNC_RESOURCES, 'machines'];
export class UserDataSyncTestServer implements IRequestService {
_serviceBrand: any;
readonly url: string = 'http://host:3000';
private session: string | null = null;
private readonly data: Map<SyncResource, IUserData> = new Map<SyncResource, IUserData>();
private readonly data: Map<ServerResource, IUserData> = new Map<SyncResource, IUserData>();
private _requests: { url: string, type: string, headers?: IHeaders }[] = [];
get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; }
private _requestsWithAllHeaders: { url: string, type: string, headers?: IHeaders }[] = [];
get requestsWithAllHeaders(): { url: string, type: string, headers?: IHeaders }[] { return this._requestsWithAllHeaders; }
private _responses: { status: number }[] = [];
get responses(): { status: number }[] { return this._responses; }
reset(): void { this._requests = []; this._responses = []; }
reset(): void { this._requests = []; this._responses = []; this._requestsWithAllHeaders = []; }
async resolveProxy(url: string): Promise<string | undefined> { return url; }
@@ -154,6 +165,7 @@ export class UserDataSyncTestServer implements IRequestService {
}
}
this._requests.push({ url: options.url!, type: options.type!, headers });
this._requestsWithAllHeaders.push({ url: options.url!, type: options.type!, headers: options.headers });
const requestContext = await this.doRequest(options);
this._responses.push({ status: requestContext.res.statusCode! });
return requestContext;
@@ -180,7 +192,7 @@ export class UserDataSyncTestServer implements IRequestService {
private async getManifest(headers?: IHeaders): Promise<IRequestContext> {
if (this.session) {
const latest: Record<SyncResource, string> = Object.create({});
const latest: Record<ServerResource, string> = Object.create({});
const manifest: IUserDataManifest = { session: this.session, latest };
this.data.forEach((value, key) => latest[key] = value.ref);
return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest));
@@ -189,7 +201,7 @@ export class UserDataSyncTestServer implements IRequestService {
}
private async getLatestData(resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource);
const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
if (resourceKey) {
const data = this.data.get(resourceKey);
if (!data) {
@@ -207,7 +219,7 @@ export class UserDataSyncTestServer implements IRequestService {
if (!this.session) {
this.session = generateUuid();
}
const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource);
const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource);
if (resourceKey) {
const data = this.data.get(resourceKey);
if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) {
@@ -220,7 +232,7 @@ export class UserDataSyncTestServer implements IRequestService {
return this.toResponse(204);
}
private async clear(headers?: IHeaders): Promise<IRequestContext> {
async clear(headers?: IHeaders): Promise<IRequestContext> {
this.data.clear();
this.session = null;
return this.toResponse(204);

View File

@@ -31,6 +31,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
@@ -45,9 +47,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});
@@ -65,21 +68,22 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
// Snippets
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});
@@ -183,11 +187,13 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
/* sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } },
]);
});
@@ -223,6 +229,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
/* first time sync */
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
@@ -231,6 +238,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } },
]);
});
@@ -370,6 +378,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
@@ -384,9 +394,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Machines
{ type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } },
]);
});

View File

@@ -0,0 +1,371 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { isWeb } from 'vs/base/common/platform';
import { RequestsSession } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestService } from 'vs/platform/request/common/request';
import { newWriteableBufferStream } from 'vs/base/common/buffer';
import { timeout } from 'vs/base/common/async';
suite('UserDataSyncStoreService', () => {
const disposableStore = new DisposableStore();
teardown(() => disposableStore.clear());
test('test read manifest for the first time', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
const productService = client.instantiationService.get(IProductService);
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Name'], `${productService.applicationName}${isWeb ? '-web' : ''}`);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Version'], productService.version);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test read manifest for the second time when session is not yet created', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test session id header is not set in the first manifest request after session is created', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
await testObject.write(SyncResource.Settings, 'some content', null);
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test session id header is set from the second manifest request after session is created', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test headers are send for write request', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
await testObject.manifest();
target.reset();
await testObject.write(SyncResource.Settings, 'some content', null);
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test headers are send for read request', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
await testObject.manifest();
target.reset();
await testObject.read(SyncResource.Settings, null);
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test headers are reset after session is cleared ', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
await testObject.manifest();
await testObject.clear();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test old headers are sent after session is changed on server ', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
await target.clear();
// client 2
const client2 = disposableStore.add(new UserDataSyncClient(target));
await client2.setUp();
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
await testObject2.write(SyncResource.Settings, 'some content', null);
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
});
test('test old headers are reset from second request after session is changed on server ', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
await target.clear();
// client 2
const client2 = disposableStore.add(new UserDataSyncClient(target));
await client2.setUp();
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
await testObject2.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
});
test('test old headers are sent after session is cleared from another server ', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
// client 2
const client2 = disposableStore.add(new UserDataSyncClient(target));
await client2.setUp();
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
await testObject2.clear();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
});
test('test headers are reset after session is cleared from another server ', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
// client 2
const client2 = disposableStore.add(new UserDataSyncClient(target));
await client2.setUp();
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
await testObject2.clear();
await testObject.manifest();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
test('test headers are reset after session is cleared from another server - started syncing again', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'];
const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'];
// client 2
const client2 = disposableStore.add(new UserDataSyncClient(target));
await client2.setUp();
const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService);
await testObject2.clear();
await testObject.manifest();
await testObject.write(SyncResource.Settings, 'some content', null);
await testObject.manifest();
target.reset();
await testObject.manifest();
assert.equal(target.requestsWithAllHeaders.length, 1);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId);
assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined);
});
});
suite('UserDataSyncRequestsSession', () => {
const requestService: IRequestService = {
_serviceBrand: undefined,
async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; },
async resolveProxy() { return undefined; }
};
test('too many requests are thrown when limit exceeded', async () => {
const testObject = new RequestsSession(1, 500, requestService);
await testObject.request({}, CancellationToken.None);
try {
await testObject.request({}, CancellationToken.None);
} catch (error) {
assert.ok(error instanceof UserDataSyncStoreError);
assert.equal((<UserDataSyncStoreError>error).code, UserDataSyncErrorCode.LocalTooManyRequests);
return;
}
assert.fail('Should fail with limit exceeded');
});
test('requests are handled after session is expired', async () => {
const testObject = new RequestsSession(1, 500, requestService);
await testObject.request({}, CancellationToken.None);
await timeout(600);
await testObject.request({}, CancellationToken.None);
});
test('too many requests are thrown after session is expired', async () => {
const testObject = new RequestsSession(1, 500, requestService);
await testObject.request({}, CancellationToken.None);
await timeout(600);
await testObject.request({}, CancellationToken.None);
try {
await testObject.request({}, CancellationToken.None);
} catch (error) {
assert.ok(error instanceof UserDataSyncStoreError);
assert.equal((<UserDataSyncStoreError>error).code, UserDataSyncErrorCode.LocalTooManyRequests);
return;
}
assert.fail('Should fail with limit exceeded');
});
});

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMediaMime, MIME_UNKNOWN } from 'vs/base/common/mime';
import { extname } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
const webviewMimeTypes = new Map([
['.svg', 'image/svg+xml'],
['.txt', 'text/plain'],
['.css', 'text/css'],
['.js', 'application/javascript'],
['.json', 'application/json'],
['.html', 'text/html'],
['.htm', 'text/html'],
['.xhtml', 'application/xhtml+xml'],
['.oft', 'font/otf'],
['.xml', 'application/xml'],
]);
export function getWebviewContentMimeType(resource: URI): string {
const ext = extname(resource.fsPath).toLowerCase();
return webviewMimeTypes.get(ext) || getMediaMime(resource.fsPath) || MIME_UNKNOWN;
}

View File

@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
import { isUNC } from 'vs/base/common/extpath';
import { Schemas } from 'vs/base/common/network';
import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
export namespace WebviewResourceResponse {
export enum Type { Success, Failed, AccessDenied }
export class StreamSuccess {
readonly type = Type.Success;
constructor(
public readonly stream: VSBufferReadableStream,
public readonly mimeType: string
) { }
}
export class BufferSuccess {
readonly type = Type.Success;
constructor(
public readonly buffer: VSBuffer,
public readonly mimeType: string
) { }
}
export const Failed = { type: Type.Failed } as const;
export const AccessDenied = { type: Type.AccessDenied } as const;
export type BufferResponse = BufferSuccess | typeof Failed | typeof AccessDenied;
export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied;
}
export async function loadLocalResource(
requestUri: URI,
fileService: IFileService,
extensionLocation: URI | undefined,
roots: ReadonlyArray<URI>
): Promise<WebviewResourceResponse.BufferResponse> {
const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots);
if (!resourceToLoad) {
return WebviewResourceResponse.AccessDenied;
}
try {
const data = await fileService.readFile(resourceToLoad);
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
return new WebviewResourceResponse.BufferSuccess(data.value, mime);
} catch (err) {
console.log(err);
return WebviewResourceResponse.Failed;
}
}
export async function loadLocalResourceStream(
requestUri: URI,
fileService: IFileService,
extensionLocation: URI | undefined,
roots: ReadonlyArray<URI>
): Promise<WebviewResourceResponse.StreamResponse> {
const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots);
if (!resourceToLoad) {
return WebviewResourceResponse.AccessDenied;
}
try {
const contents = await fileService.readFileStream(resourceToLoad);
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
return new WebviewResourceResponse.StreamSuccess(contents.value, mime);
} catch (err) {
console.log(err);
return WebviewResourceResponse.Failed;
}
}
function getResourceToLoad(
requestUri: URI,
extensionLocation: URI | undefined,
roots: ReadonlyArray<URI>
): URI | undefined {
const normalizedPath = normalizeRequestPath(requestUri);
for (const root of roots) {
if (!containsResource(root, normalizedPath)) {
continue;
}
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
return URI.from({
scheme: REMOTE_HOST_SCHEME,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: normalizedPath.path
})
});
} else {
return normalizedPath;
}
}
return undefined;
}
function normalizeRequestPath(requestUri: URI) {
if (requestUri.scheme !== Schemas.vscodeWebviewResource) {
return requestUri;
}
// The `vscode-webview-resource` scheme has the following format:
//
// vscode-webview-resource://id/scheme//authority?/path
//
const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)\/{1,2}/i, '$1://'));
return resourceUri.with({
query: requestUri.query,
fragment: requestUri.fragment
});
}
function containsResource(root: URI, resource: URI): boolean {
let rootPath = root.fsPath + (root.fsPath.endsWith(sep) ? '' : sep);
let resourceFsPath = resource.fsPath;
if (isUNC(root.fsPath) && isUNC(resource.fsPath)) {
rootPath = rootPath.toLowerCase();
resourceFsPath = resourceFsPath.toLowerCase();
}
return resourceFsPath.startsWith(rootPath);
}

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IWebviewManagerService = createDecorator<IWebviewManagerService>('webviewManagerService');
export interface IWebviewManagerService {
_serviceBrand: unknown;
registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise<void>;
unregisterWebview(id: string): Promise<void>;
updateLocalResourceRoots(id: string, roots: UriComponents[]): Promise<void>;
setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise<void>;
}
export interface RegisterWebviewMetadata {
readonly extensionLocation: UriComponents | undefined;
readonly localResourceRoots: readonly UriComponents[];
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { webContents } from 'electron';
import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
import { IFileService } from 'vs/platform/files/common/files';
import { UriComponents, URI } from 'vs/base/common/uri';
export class WebviewMainService implements IWebviewManagerService {
_serviceBrand: undefined;
private protocolProvider: WebviewProtocolProvider;
constructor(
@IFileService fileService: IFileService,
) {
this.protocolProvider = new WebviewProtocolProvider(fileService);
}
public async registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise<void> {
this.protocolProvider.registerWebview(id,
metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined,
metadata.localResourceRoots.map((x: UriComponents) => URI.from(x))
);
}
public async unregisterWebview(id: string): Promise<void> {
this.protocolProvider.unreigsterWebview(id);
}
public async updateLocalResourceRoots(id: string, roots: UriComponents[]): Promise<void> {
this.protocolProvider.updateLocalResourceRoots(id, roots.map((x: UriComponents) => URI.from(x)));
}
public async setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise<void> {
const contents = webContents.fromId(webContentsId);
if (!contents) {
throw new Error(`Invalid webContentsId: ${webContentsId}`);
}
if (!contents.isDestroyed()) {
contents.setIgnoreMenuShortcuts(enabled);
}
}
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { protocol } from 'electron';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { streamToNodeReadable } from 'vs/base/node/stream';
import { IFileService } from 'vs/platform/files/common/files';
import { loadLocalResourceStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
export class WebviewProtocolProvider extends Disposable {
private readonly webviewMetadata = new Map<string, {
readonly extensionLocation: URI | undefined;
readonly localResourceRoots: readonly URI[];
}>();
constructor(
@IFileService private readonly fileService: IFileService,
) {
super();
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
try {
const uri = URI.parse(request.url);
const id = uri.authority;
const metadata = this.webviewMetadata.get(id);
if (metadata) {
const result = await loadLocalResourceStream(uri, this.fileService, metadata.extensionLocation, metadata.localResourceRoots);
if (result.type === WebviewResourceResponse.Type.Success) {
return callback({
statusCode: 200,
data: streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
}
});
}
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ data: null, statusCode: 401 });
}
}
} catch {
// noop
}
return callback({ data: null, statusCode: 404 });
});
this._register(toDisposable(() => protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
}
public registerWebview(id: string, extensionLocation: URI | undefined, localResourceRoots: readonly URI[]): void {
this.webviewMetadata.set(id, { extensionLocation, localResourceRoots });
}
public unreigsterWebview(id: string): void {
this.webviewMetadata.delete(id);
}
public updateLocalResourceRoots(id: string, localResourceRoots: readonly URI[]) {
const entry = this.webviewMetadata.get(id);
if (entry) {
this.webviewMetadata.set(id, {
extensionLocation: entry.extensionLocation,
localResourceRoots: localResourceRoots,
});
}
}
}

View File

@@ -7,6 +7,8 @@ import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ThemeType } from 'vs/platform/theme/common/themeService';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export interface IBaseOpenWindowsOptions {
forceReuseWindow?: boolean;
@@ -17,6 +19,26 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions {
preferNewWindow?: boolean;
noRecentEntry?: boolean;
addMode?: boolean;
diffMode?: boolean;
gotoLineMode?: boolean;
waitMarkerFileURI?: URI;
}
export interface IAddFoldersRequest {
foldersToAdd: UriComponents[];
}
export interface IOpenedWindow {
id: number;
workspace?: IWorkspaceIdentifier;
folderUri?: ISingleFolderWorkspaceIdentifier;
title: string;
filename?: string;
dirty: boolean;
}
export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions {
@@ -157,6 +179,7 @@ export interface IWindowConfiguration {
remoteAuthority?: string;
highContrast?: boolean;
defaultThemeType?: ThemeType;
filesToOpenOrCreate?: IPath[];
filesToDiff?: IPath[];

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IElectronMainService } from 'vs/platform/electron/electron-main/electronMainService';
export class ActiveWindowManager extends Disposable {
private readonly disposables = this._register(new DisposableStore());
private firstActiveWindowIdPromise: CancelablePromise<number | undefined> | undefined;
private activeWindowId: number | undefined;
constructor(@IElectronMainService electronService: IElectronMainService) {
super();
// remember last active window id upon events
const onActiveWindowChange = Event.latch(Event.any(electronService.onWindowOpen, electronService.onWindowFocus));
onActiveWindowChange(this.setActiveWindow, this, this.disposables);
// resolve current active window
this.firstActiveWindowIdPromise = createCancelablePromise(() => electronService.getActiveWindowId(-1));
(async () => {
try {
const windowId = await this.firstActiveWindowIdPromise;
this.activeWindowId = (typeof this.activeWindowId === 'number') ? this.activeWindowId : windowId;
} finally {
this.firstActiveWindowIdPromise = undefined;
}
})();
}
private setActiveWindow(windowId: number | undefined) {
if (this.firstActiveWindowIdPromise) {
this.firstActiveWindowIdPromise.cancel();
this.firstActiveWindowIdPromise = undefined;
}
this.activeWindowId = windowId;
}
async getActiveClientId(): Promise<string | undefined> {
const id = this.firstActiveWindowIdPromise ? (await this.firstActiveWindowIdPromise) : this.activeWindowId;
return `window:${id}`;
}
}

View File

@@ -105,7 +105,7 @@ export interface IWindowsMainService {
readonly onWindowsCountChanged: Event<IWindowsCountChangedEvent>;
open(openConfig: IOpenConfiguration): ICodeWindow[];
openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[];
openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[];
openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[];
sendToFocused(channel: string, ...args: any[]): void;
@@ -118,9 +118,12 @@ export interface IWindowsMainService {
getWindowCount(): number;
}
export interface IOpenConfiguration {
export interface IBaseOpenConfiguration {
readonly context: OpenContext;
readonly contextWindowId?: number;
}
export interface IOpenConfiguration extends IBaseOpenConfiguration {
readonly cli: ParsedArgs;
readonly userEnv?: IProcessEnvironment;
readonly urisToOpen?: IWindowOpenable[];
@@ -136,3 +139,5 @@ export interface IOpenConfiguration {
readonly initialStartup?: boolean;
readonly noRecentEntry?: boolean;
}
export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { }

View File

@@ -16,15 +16,14 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm
import { IStateService } from 'vs/platform/state/node/state';
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { ipcMain as ipc, screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } from 'electron';
import { parseLineAndColumnAware } from 'vs/code/node/paths';
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IAddFoldersRequest, IPathsToWaitFor } from 'vs/platform/windows/node/window';
import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IPathsToWaitFor } from 'vs/platform/windows/node/window';
import { Emitter } from 'vs/base/common/event';
import product from 'vs/platform/product/common/product';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces';
@@ -39,7 +38,7 @@ import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
import { isWindowsDriveLetter, toSlashes } from 'vs/base/common/extpath';
import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath';
import { CharCode } from 'vs/base/common/charCode';
export interface IWindowState {
@@ -393,7 +392,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
};
}
openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[] {
openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] {
let cli = this.environmentService.args;
const remote = options?.remoteAuthority;
if (cli && (cli.remote !== remote)) {
@@ -403,7 +402,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const forceReuseWindow = options?.forceReuseWindow;
const forceNewWindow = !forceReuseWindow;
return this.open({ context, cli, forceEmpty: true, forceNewWindow, forceReuseWindow });
return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow });
}
open(openConfig: IOpenConfiguration): ICodeWindow[] {
@@ -474,7 +473,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// Make sure to pass focus to the most relevant of the windows if we open multiple
if (usedWindows.length > 1) {
const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length);
let focusLastOpened = true;
let focusLastWindow = true;
@@ -753,15 +751,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined);
for (let i = 0; i < emptyToOpen; i++) {
usedWindows.push(this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
remoteAuthority,
forceNewWindow: openFolderInNewWindow,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
fileInputs
}));
usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, fileInputs));
// Reset these because we handled them
fileInputs = undefined;
@@ -801,12 +791,29 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return window;
}
private doOpenEmpty(openConfig: IOpenConfiguration, forceNewWindow: boolean, remoteAuthority: string | undefined, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow {
if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') {
windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/97172
}
return this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
remoteAuthority,
forceNewWindow,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
fileInputs,
windowToUse
});
}
private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow {
if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') {
windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/Microsoft/vscode/issues/49587
}
const browserWindow = this.openInBrowserWindow({
return this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
@@ -818,8 +825,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
windowToUse
});
return browserWindow;
}
private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IOpenWindowOptions, IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows';
import { IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows';
import { URI, UriComponents } from 'vs/base/common/uri';
import * as platform from 'vs/base/common/platform';
import * as extpath from 'vs/base/common/extpath';
@@ -13,15 +13,6 @@ import { LogLevel } from 'vs/platform/log/common/log';
import { ExportData } from 'vs/base/common/performance';
import { ParsedArgs } from 'vs/platform/environment/node/argv';
export interface IOpenedWindow {
id: number;
workspace?: IWorkspaceIdentifier;
folderUri?: ISingleFolderWorkspaceIdentifier;
title: string;
filename?: string;
dirty: boolean;
}
export const enum OpenContext {
// opening when running from the command line
@@ -53,10 +44,6 @@ export interface IRunKeybindingInWindowRequest {
userSettingsLabel: string;
}
export interface IAddFoldersRequest {
foldersToAdd: UriComponents[];
}
export interface INativeWindowConfiguration extends IWindowConfiguration, ParsedArgs {
mainPid: number;
@@ -100,13 +87,6 @@ export interface IPathsToWaitForData {
waitMarkerFileUri: UriComponents;
}
export interface INativeOpenWindowOptions extends IOpenWindowOptions {
diffMode?: boolean;
addMode?: boolean;
gotoLineMode?: boolean;
waitMarkerFileURI?: URI;
}
export interface IWindowContext {
openedWorkspace?: IWorkspaceIdentifier;
openedFolderUri?: URI;

View File

@@ -19,7 +19,7 @@ import { isWindows } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { dirname, joinPath } from 'vs/base/common/resources';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';