mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 02:58:31 -05:00
Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
.context-view-block {
|
||||
position: fixed;
|
||||
cursor: initial;
|
||||
left:0;
|
||||
top:0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
|
||||
33
src/vs/platform/electron/electron-sandbox/electron.ts
Normal file
33
src/vs/platform/electron/electron-sandbox/electron.ts
Normal 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;
|
||||
})()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
351
src/vs/platform/extensionManagement/node/extensionsScanner.ts
Normal file
351
src/vs/platform/extensionManagement/node/extensionsScanner.ts
Normal 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.")));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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']]);
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -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
211
src/vs/platform/undoRedo/test/common/undoRedoService.test.ts
Normal file
211
src/vs/platform/undoRedo/test/common/undoRedoService.test.ts
Normal 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);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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...`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
158
src/vs/platform/userDataSync/common/userDataSyncMachines.ts
Normal file
158
src/vs/platform/userDataSync/common/userDataSyncMachines.ts
Normal 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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
|
||||
@@ -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!), '[]');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
|
||||
@@ -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, []);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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: {} }]);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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' } },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
26
src/vs/platform/webview/common/mimeTypes.ts
Normal file
26
src/vs/platform/webview/common/mimeTypes.ts
Normal 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;
|
||||
}
|
||||
141
src/vs/platform/webview/common/resourceLoader.ts
Normal file
141
src/vs/platform/webview/common/resourceLoader.ts
Normal 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);
|
||||
}
|
||||
24
src/vs/platform/webview/common/webviewManagerService.ts
Normal file
24
src/vs/platform/webview/common/webviewManagerService.ts
Normal 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[];
|
||||
}
|
||||
48
src/vs/platform/webview/electron-main/webviewMainService.ts
Normal file
48
src/vs/platform/webview/electron-main/webviewMainService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
51
src/vs/platform/windows/electron-main/windowTracker.ts
Normal file
51
src/vs/platform/windows/electron-main/windowTracker.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user