chore(vscode): update to 1.54.2

This commit is contained in:
Joe Previte
2021-03-11 10:27:10 -07:00
1459 changed files with 53404 additions and 51004 deletions

View File

@@ -5,7 +5,6 @@
.monaco-action-bar .action-item.menu-entry .action-label {
background-image: var(--menu-entry-icon-light);
display: inline-flex;
}
.vs-dark .monaco-action-bar .action-item.menu-entry .action-label,

View File

@@ -6,7 +6,7 @@
import 'vs/css!./menuEntryActionViewItem';
import { asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { IAction, Separator } from 'vs/base/common/actions';
import { IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions';
@@ -27,10 +27,10 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct
return asDisposable(groups);
}
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean, primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean): IDisposable {
const groups = menu.getActions(options);
// Action bars handle alternative actions on their own so the alternative actions should be ignored
fillInActions(groups, target, false, isPrimaryGroup);
fillInActions(groups, target, false, isPrimaryGroup, primaryMaxCount, shouldInlineSubmenu);
return asDisposable(groups);
}
@@ -44,25 +44,68 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
return disposables;
}
function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
for (let [group, actions] of groups) {
if (useAlternativeActions) {
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
}
function fillInActions(
groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; },
useAlternativeActions: boolean,
isPrimaryGroup: (group: string) => boolean = group => group === 'navigation',
primaryMaxCount: number = Number.MAX_SAFE_INTEGER,
shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false
): void {
let primaryBucket: IAction[];
let secondaryBucket: IAction[];
if (Array.isArray(target)) {
primaryBucket = target;
secondaryBucket = target;
} else {
primaryBucket = target.primary;
secondaryBucket = target.secondary;
}
const submenuInfo = new Set<{ group: string, action: SubmenuAction, index: number }>();
for (const [group, actions] of groups) {
let target: IAction[];
if (isPrimaryGroup(group)) {
const to = Array.isArray(target) ? target : target.primary;
to.unshift(...actions);
target = primaryBucket;
} else {
const to = Array.isArray(target) ? target : target.secondary;
if (to.length > 0) {
to.push(new Separator());
target = secondaryBucket;
if (target.length > 0) {
target.push(new Separator());
}
to.push(...actions);
}
for (let action of actions) {
if (useAlternativeActions) {
action = action instanceof MenuItemAction && action.alt ? action.alt : action;
}
const newLen = target.push(action);
// keep submenu info for later inlining
if (action instanceof SubmenuAction) {
submenuInfo.add({ group, action, index: newLen - 1 });
}
}
}
// ask the outside if submenu should be inlined or not. only ask when
// there would be enough space
for (const { group, action, index } of submenuInfo) {
const target = isPrimaryGroup(group) ? primaryBucket : secondaryBucket;
// inlining submenus with length 0 or 1 is easy,
// larger submenus need to be checked with the overall limit
const submenuActions = action.actions;
if ((submenuActions.length <= 1 || target.length + submenuActions.length - 2 <= primaryMaxCount) && shouldInlineSubmenu(action, group, target.length)) {
target.splice(index, 1, ...submenuActions);
}
}
// overflow items from the primary group into the secondary bucket
if (primaryBucket !== secondaryBucket && primaryBucket.length > primaryMaxCount) {
const overflow = primaryBucket.splice(primaryMaxCount, primaryBucket.length - primaryMaxCount);
secondaryBucket.unshift(...overflow, new Separator());
}
}

View File

@@ -89,6 +89,7 @@ export class MenuId {
static readonly EditorContext = new MenuId('EditorContext');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorTitle = new MenuId('EditorTitle');
static readonly EditorTitleRun = new MenuId('EditorTitleRun');
static readonly EditorTitleContext = new MenuId('EditorTitleContext');
static readonly EmptyEditorGroupContext = new MenuId('EmptyEditorGroupContext');
static readonly ExplorerContext = new MenuId('ExplorerContext');
@@ -120,11 +121,14 @@ export class MenuId {
static readonly SCMTitle = new MenuId('SCMTitle');
static readonly SearchContext = new MenuId('SearchContext');
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly TestItem = new MenuId('TestItem');
static readonly TouchBarContext = new MenuId('TouchBarContext');
static readonly TitleBarContext = new MenuId('TitleBarContext');
static readonly TunnelContext = new MenuId('TunnelContext');
static readonly TunnelInline = new MenuId('TunnelInline');
static readonly TunnelPortInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline');
static readonly TunnelOriginInline = new MenuId('TunnelOriginInline');
static readonly ViewItemContext = new MenuId('ViewItemContext');
static readonly ViewContainerTitle = new MenuId('ViewContainerTitle');
static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext');
@@ -148,6 +152,7 @@ export class MenuId {
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly AccountsContext = new MenuId('AccountsContext');
static readonly PanelTitle = new MenuId('PanelTitle');
static readonly TerminalContext = new MenuId('TerminalContext');
readonly id: number;
readonly _debugName: string;
@@ -174,7 +179,7 @@ export interface IMenuService {
readonly _serviceBrand: undefined;
createMenu(id: MenuId, scopedKeybindingService: IContextKeyService): IMenu;
createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges?: boolean): IMenu;
}
export type ICommandsMap = Map<string, ICommandAction>;
@@ -319,34 +324,30 @@ export class ExecuteCommandAction extends Action {
export class SubmenuItemAction extends SubmenuAction {
readonly item: ISubmenuItem;
constructor(
item: ISubmenuItem,
menuService: IMenuService,
contextKeyService: IContextKeyService,
options?: IMenuActionOptions
readonly item: ISubmenuItem,
private readonly _menuService: IMenuService,
private readonly _contextKeyService: IContextKeyService,
private readonly _options?: IMenuActionOptions
) {
super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, [], 'submenu');
}
get actions(): readonly IAction[] {
const result: IAction[] = [];
const menu = menuService.createMenu(item.submenu, contextKeyService);
const groups = menu.getActions(options);
const menu = this._menuService.createMenu(this.item.submenu, this._contextKeyService);
const groups = menu.getActions(this._options);
menu.dispose();
for (let group of groups) {
const [, actions] = group;
for (const [, actions] of groups) {
if (actions.length > 0) {
result.push(...actions);
result.push(new Separator());
}
}
if (result.length) {
result.pop(); // remove last separator
}
super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, result, 'submenu');
this.item = item;
return result;
}
}

View File

@@ -20,8 +20,14 @@ export class MenuService implements IMenuService {
//
}
createMenu(id: MenuId, contextKeyService: IContextKeyService): IMenu {
return new Menu(id, this._commandService, contextKeyService, this);
/**
* Create a new menu for the given menu identifier. A menu sends events when it's entries
* have changed (placement, enablement, checked-state). By default it does send events for
* sub menu entries. That is more expensive and must be explicitly enabled with the
* `emitEventsForSubmenuChanges` flag.
*/
createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges: boolean = false): IMenu {
return new Menu(id, emitEventsForSubmenuChanges, this._commandService, contextKeyService, this);
}
}
@@ -40,6 +46,7 @@ class Menu implements IMenu {
constructor(
private readonly _id: MenuId,
private readonly _fireEventsForSubmenuChanges: boolean,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IMenuService private readonly _menuService: IMenuService
@@ -93,23 +100,33 @@ class Menu implements IMenu {
group![1].push(item);
// keep keys for eventing
Menu._fillInKbExprKeys(item.when, this._contextKeys);
if (isIMenuItem(item)) {
// keep precondition keys for event if applicable
if (item.command.precondition) {
Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
}
// keep toggled keys for event if applicable
if (item.command.toggled) {
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
}
this._collectContextKeys(item);
}
this._onDidChange.fire(this);
}
private _collectContextKeys(item: IMenuItem | ISubmenuItem): void {
Menu._fillInKbExprKeys(item.when, this._contextKeys);
if (isIMenuItem(item)) {
// keep precondition keys for event if applicable
if (item.command.precondition) {
Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
}
// keep toggled keys for event if applicable
if (item.command.toggled) {
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
} else if (this._fireEventsForSubmenuChanges) {
// recursively collect context keys from submenus so that this
// menu fires events when context key changes affect submenus
MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this);
}
}
getActions(options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
const result: [string, Array<MenuItemAction | SubmenuItemAction>][] = [];
for (let group of this._menuGroups) {

View File

@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
import { createHash } from 'crypto';
import { join } from 'vs/base/common/path';
import { isLinux } from 'vs/base/common/platform';
import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
@@ -35,21 +35,21 @@ export class BackupMainService implements IBackupMainService {
// - ignore path casing on Windows/macOS
// - respect path casing on Linux
private readonly backupUriComparer = extUriBiasedIgnorePathCase;
private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !platform.isLinux) };
private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) };
constructor(
@IEnvironmentMainService environmentService: IEnvironmentMainService,
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
this.backupHome = environmentMainService.backupHome;
this.workspacesJsonPath = environmentMainService.backupWorkspacesPath;
}
async initialize(): Promise<void> {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(await readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
} catch (error) {
backups = Object.create(null);
}
@@ -151,8 +151,8 @@ export class BackupMainService implements IBackupMainService {
if (fs.existsSync(moveFromPath)) {
try {
fs.renameSync(moveFromPath, backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
} catch (error) {
this.logService.error(`Backup: Could not move backup folder to new location: ${error.toString()}`);
}
}
}
@@ -204,7 +204,7 @@ export class BackupMainService implements IBackupMainService {
}
private getBackupPath(oldFolderHash: string): string {
return path.join(this.backupHome, oldFolderHash);
return join(this.backupHome, oldFolderHash);
}
private async validateWorkspaces(rootWorkspaces: IWorkspaceBackupInfo[]): Promise<IWorkspaceBackupInfo[]> {
@@ -312,8 +312,8 @@ export class BackupMainService implements IBackupMainService {
if (await exists(backupPath)) {
await rimraf(backupPath, RimRafMode.MOVE);
}
} catch (ex) {
this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
} catch (error) {
this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`);
}
}
@@ -328,9 +328,9 @@ export class BackupMainService implements IBackupMainService {
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
await rename(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
await fs.promises.rename(backupPath, newEmptyWindowBackupPath);
} catch (error) {
this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`);
return false;
}
this.emptyWindows.push({ backupFolder: newBackupFolder });
@@ -350,8 +350,8 @@ export class BackupMainService implements IBackupMainService {
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
fs.renameSync(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
} catch (error) {
this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`);
return false;
}
this.emptyWindows.push({ backupFolder: newBackupFolder });
@@ -406,7 +406,7 @@ export class BackupMainService implements IBackupMainService {
for (const backupSchema of backupSchemas) {
try {
const backupSchemaChildren = await readdir(path.join(backupPath, backupSchema));
const backupSchemaChildren = await readdir(join(backupPath, backupSchema));
if (backupSchemaChildren.length > 0) {
return true;
}
@@ -424,16 +424,16 @@ export class BackupMainService implements IBackupMainService {
private saveSync(): void {
try {
writeFileSync(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
} catch (error) {
this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`);
}
}
private async save(): Promise<void> {
try {
await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
} catch (error) {
this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`);
}
}
@@ -454,11 +454,11 @@ export class BackupMainService implements IBackupMainService {
if (folderUri.scheme === Schemas.file) {
// for backward compatibility, use the fspath as key
key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
key = isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
} else {
key = folderUri.toString().toLowerCase();
}
return crypto.createHash('md5').update(key).digest('hex');
return createHash('md5').update(key).digest('hex');
}
}

View File

@@ -17,7 +17,7 @@ import { IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, ISerializedWorkspace } from 'vs/platform/backup/node/backup';
import { HotExitConfiguration } from 'vs/platform/files/common/files';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ConsoleLogMainService } from 'vs/platform/log/common/log';
import { ConsoleMainLogger, LogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { createHash } from 'crypto';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
@@ -106,12 +106,12 @@ flakySuite('BackupMainService', () => {
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
await pfs.mkdirp(backupHome);
await fs.promises.mkdir(backupHome, { recursive: true });
configService = new TestConfigurationService();
service = new class TestBackupMainService extends BackupMainService {
constructor() {
super(environmentService, configService, new ConsoleLogMainService());
super(environmentService, configService, new LogService(new ConsoleMainLogger()));
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
@@ -445,7 +445,7 @@ flakySuite('BackupMainService', () => {
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -461,7 +461,7 @@ flakySuite('BackupMainService', () => {
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -483,7 +483,7 @@ flakySuite('BackupMainService', () => {
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1);
if (platform.isLinux) {
@@ -499,7 +499,7 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
});
@@ -514,7 +514,7 @@ flakySuite('BackupMainService', () => {
assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]);
@@ -527,7 +527,7 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
@@ -537,7 +537,7 @@ flakySuite('BackupMainService', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
@@ -548,12 +548,12 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(barFile);
service.unregisterFolderBackupSync(fooFile);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.folderURIWorkspaces, []);
});
@@ -565,12 +565,12 @@ flakySuite('BackupMainService', () => {
service.registerWorkspaceBackupSync(ws2);
service.unregisterWorkspaceBackupSync(ws1.workspace);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.rootURIWorkspaces, []);
});
@@ -580,12 +580,12 @@ flakySuite('BackupMainService', () => {
service.registerEmptyWindowBackupSync('bar');
service.unregisterEmptyWindowBackupSync('foo');
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
service.unregisterEmptyWindowBackupSync('bar');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.emptyWorkspaceInfos, []);
});
@@ -599,7 +599,7 @@ flakySuite('BackupMainService', () => {
await service.initialize();
service.unregisterFolderBackupSync(barFile);
service.unregisterEmptyWindowBackupSync('test');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -669,8 +669,8 @@ flakySuite('BackupMainService', () => {
assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0);
try {
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true });
await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true });
} catch (error) {
// ignore - folder might exist already
}

View File

@@ -3,14 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { distinct } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression, RawContextKey, ContextKeyInfo } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
@@ -287,6 +288,13 @@ export abstract class AbstractContextKeyService implements IContextKeyService {
return new ScopedContextKeyService(this, domNode);
}
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new OverlayContextKeyService(this, overlay);
}
public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
@@ -405,27 +413,25 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon
class ScopedContextKeyService extends AbstractContextKeyService {
private _parent: AbstractContextKeyService;
private _domNode: IContextKeyServiceTarget | undefined;
private _domNode: IContextKeyServiceTarget;
private readonly _parentChangeListener = new MutableDisposable();
constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) {
constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {
super(parent.createChildContext());
this._parent = parent;
this._updateParentChangeListener();
if (domNode) {
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
let extraInfo = '';
if ((this._domNode as HTMLElement).classList) {
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');
}
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
let extraInfo = '';
if ((this._domNode as HTMLElement).classList) {
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
private _updateParentChangeListener(): void {
@@ -434,14 +440,15 @@ class ScopedContextKeyService extends AbstractContextKeyService {
}
public dispose(): void {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._parent.disposeContext(this._myContextId);
this._parentChangeListener?.dispose();
if (this._domNode) {
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._domNode = undefined;
if (this._isDisposed) {
return;
}
this._onDidChangeContext.dispose();
this._parent.disposeContext(this._myContextId);
this._parentChangeListener.dispose();
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._isDisposed = true;
}
public getContextValuesContainer(contextId: number): Context {
@@ -484,6 +491,76 @@ class ScopedContextKeyService extends AbstractContextKeyService {
}
}
class OverlayContext implements IContext {
constructor(private parent: IContext, private overlay: ReadonlyMap<string, any>) { }
getValue<T>(key: string): T | undefined {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue(key);
}
}
class OverlayContextKeyService implements IContextKeyService {
declare _serviceBrand: undefined;
private overlay: Map<string, any>;
get contextId(): number {
return this.parent.contextId;
}
get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this.parent.onDidChangeContext;
}
constructor(private parent: AbstractContextKeyService | OverlayContextKeyService, overlay: Iterable<[string, any]>) {
this.overlay = new Map(overlay);
}
bufferChangeEvents(callback: Function): void {
this.parent.bufferChangeEvents(callback);
}
createKey<T>(): IContextKey<T> {
throw new Error('Not supported.');
}
getContext(target: IContextKeyServiceTarget | null): IContext {
return new OverlayContext(this.parent.getContext(target), this.overlay);
}
getContextValuesContainer(contextId: number): IContext {
const parentContext = this.parent.getContextValuesContainer(contextId);
return new OverlayContext(parentContext, this.overlay);
}
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
const context = this.getContextValuesContainer(this.contextId);
const result = KeybindingResolver.contextMatchesRules(context, rules);
return result;
}
getContextKeyValue<T>(key: string): T | undefined {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);
}
createScoped(): IContextKeyService {
throw new Error('Not supported.');
}
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
return new OverlayContextKeyService(this, overlay);
}
updateParent(): void {
throw new Error('Not supported.');
}
dispose(): void {
// noop
}
}
function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
while (domNode) {
if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
@@ -501,3 +578,27 @@ function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
CommandsRegistry.registerCommand(SET_CONTEXT_COMMAND_ID, function (accessor, contextKey: any, contextValue: any) {
accessor.get(IContextKeyService).createKey(String(contextKey), contextValue);
});
CommandsRegistry.registerCommand({
id: 'getContextKeyInfo',
handler() {
return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));
},
description: {
description: localize('getContextKeyInfo', "A command that returns information about context keys"),
args: []
}
});
CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {
const result: ContextKeyInfo[] = [];
const seen = new Set<string>();
for (let info of RawContextKey.all()) {
if (!seen.has(info.key)) {
seen.add(info.key);
result.push(info);
}
}
result.sort((a, b) => a.key.localeCompare(b.key));
console.log(JSON.stringify(result, undefined, 2));
});

View File

@@ -1257,13 +1257,32 @@ export class ContextKeyOrExpr implements IContextKeyExpression {
}
}
export interface ContextKeyInfo {
readonly key: string;
readonly type?: string;
readonly description?: string;
}
export class RawContextKey<T> extends ContextKeyDefinedExpr {
private static _info: ContextKeyInfo[] = [];
static all(): IterableIterator<ContextKeyInfo> {
return RawContextKey._info.values();
}
private readonly _defaultValue: T | undefined;
constructor(key: string, defaultValue: T | undefined) {
constructor(readonly key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string, description: string }) {
super(key);
this._defaultValue = defaultValue;
// collect all context keys into a central place
if (typeof metaOrHide === 'object') {
RawContextKey._info.push({ ...metaOrHide, key });
} else if (metaOrHide !== true) {
RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined });
}
}
public bindTo(target: IContextKeyService): IContextKey<T> {
@@ -1326,7 +1345,8 @@ export interface IContextKeyService {
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;
getContextKeyValue<T>(key: string): T | undefined;
createScoped(target?: IContextKeyServiceTarget): IContextKeyService;
createScoped(target: IContextKeyServiceTarget): IContextKeyService;
createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;
getContext(target: IContextKeyServiceTarget | null): IContext;
updateParent(parentContextKeyService: IContextKeyService): void;

View File

@@ -3,17 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
export const IsMacContext = new RawContextKey<boolean>('isMac', isMacintosh);
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux);
export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindows);
export const IsMacContext = new RawContextKey<boolean>('isMac', isMacintosh, localize('isMac', "Whether the operating system is macOS"));
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux, localize('isLinux', "Whether the operating system is Linux"));
export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindows, localize('isWindows', "Whether the operating system is Windows"));
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb);
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb);
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser"));
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform"));
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false);
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true);
export const InputFocusedContextKey = 'inputFocus';
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false);
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false, localize('inputFocus', "Whether keyboard focus is inside an input box"));

View File

@@ -24,7 +24,7 @@ export class ContextViewService extends Disposable implements IContextViewServic
this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE));
this.layout();
this._register(layoutService.onLayout(() => this.layout()));
this._register(layoutService.onDidLayout(() => this.layout()));
}
// ContextView

View File

@@ -43,7 +43,12 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
return {};
}
const debug = codeWindow.win.webContents.debugger;
const win = codeWindow.win;
if (!win) {
return {};
}
const debug = win.webContents.debugger;
let listeners = debug.isAttached() ? Infinity : 0;
const server = createServer(listener => {
@@ -61,7 +66,7 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) =>
writeMessage(({ method, params, sessionId }));
codeWindow.win.on('close', () => {
win.on('close', () => {
debug.removeListener('message', onMessage);
listener.end();
closed = true;
@@ -103,7 +108,7 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
});
await new Promise<void>(r => server.listen(0, r));
codeWindow.win.on('close', () => server.close());
win.on('close', () => server.close());
return { rendererDebugPort: (server.address() as AddressInfo).port };
}

View File

@@ -7,6 +7,20 @@ import { UriComponents } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
import { IStringDictionary } from 'vs/base/common/collections';
import { IMainProcessInfo } from 'vs/platform/launch/common/launch';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
export interface IDiagnosticsService {
readonly _serviceBrand: undefined;
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
}
export interface IMachineInfo {
os: string;

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as osLib from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { readdir, exists, readFile } from 'fs';
import { IDiagnosticsService, IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { exists, readFile } from 'fs';
import { join, basename } from 'vs/base/common/path';
import { parse, ParseError, getNodeType } from 'vs/base/common/json';
import { listProcesses } from 'vs/base/node/ps';
@@ -13,24 +13,12 @@ import product from 'vs/platform/product/common/product';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IMainProcessInfo } from 'vs/platform/launch/node/launch';
import { IMainProcessInfo } from 'vs/platform/launch/common/launch';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Iterable } from 'vs/base/common/iterator';
import { Schemas } from 'vs/base/common/network';
import { ByteSize } from 'vs/platform/files/common/files';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
export interface IDiagnosticsService {
readonly _serviceBrand: undefined;
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
}
import { IDirent, readdir } from 'vs/base/node/pfs';
export interface VersionInfo {
vscodeVersion: string;
@@ -79,68 +67,69 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P
function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise<void> {
const relativePath = dir.substring(root.length + 1);
return new Promise(resolve => {
readdir(dir, { withFileTypes: true }, async (err, files) => {
return new Promise(async resolve => {
let files: IDirent[];
try {
files = await readdir(dir, { withFileTypes: true });
} catch (error) {
// Ignore folders that can't be read
if (err) {
resolve();
return;
}
if (token.count >= MAX_FILES) {
token.count += files.length;
token.maxReached = true;
resolve();
return;
}
let pending = files.length;
if (pending === 0) {
resolve();
return;
}
let filesToRead = files;
if (token.count + files.length > MAX_FILES) {
token.maxReached = true;
pending = MAX_FILES - token.count;
filesToRead = files.slice(0, pending);
}
resolve();
return;
}
if (token.count >= MAX_FILES) {
token.count += files.length;
token.maxReached = true;
resolve();
return;
}
for (const file of filesToRead) {
if (file.isDirectory()) {
if (!filter.includes(file.name)) {
await collect(root, join(dir, file.name), filter, token);
}
let pending = files.length;
if (pending === 0) {
resolve();
return;
}
if (--pending === 0) {
resolve();
return;
}
} else {
const index = file.name.lastIndexOf('.');
if (index >= 0) {
const fileType = file.name.substring(index + 1);
if (fileType) {
fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);
}
}
let filesToRead = files;
if (token.count + files.length > MAX_FILES) {
token.maxReached = true;
pending = MAX_FILES - token.count;
filesToRead = files.slice(0, pending);
}
for (const configFile of configFilePatterns) {
if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) {
configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);
}
}
token.count += files.length;
if (--pending === 0) {
resolve();
return;
for (const file of filesToRead) {
if (file.isDirectory()) {
if (!filter.includes(file.name)) {
await collect(root, join(dir, file.name), filter, token);
}
if (--pending === 0) {
resolve();
return;
}
} else {
const index = file.name.lastIndexOf('.');
if (index >= 0) {
const fileType = file.name.substring(index + 1);
if (fileType) {
fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);
}
}
for (const configFile of configFilePatterns) {
if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) {
configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);
}
}
if (--pending === 0) {
resolve();
return;
}
}
});
}
});
}

View File

@@ -16,6 +16,8 @@ import { withNullAsUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { hash } from 'vs/base/common/hash';
export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');
@@ -48,14 +50,13 @@ export class DialogMainService implements IDialogMainService {
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
private readonly mapWindowToDialogQueue: Map<number, Queue<void>>;
private readonly noWindowDialogQueue: Queue<void>;
private readonly windowFileDialogLocks = new Map<number, Set<number>>();
private readonly windowDialogQueues = new Map<number, Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>>();
private readonly noWindowDialogueQueue = new Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>();
constructor(
@IStateService private readonly stateService: IStateService
) {
this.mapWindowToDialogQueue = new Map<number, Queue<void>>();
this.noWindowDialogQueue = new Queue<void>();
}
pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
@@ -123,22 +124,25 @@ export class DialogMainService implements IDialogMainService {
return;
}
private getDialogQueue(window?: BrowserWindow): Queue<any> {
if (!window) {
return this.noWindowDialogQueue;
}
private getWindowDialogQueue<T extends MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>(window?: BrowserWindow): Queue<T> {
let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id);
if (!windowDialogQueue) {
windowDialogQueue = new Queue<any>();
this.mapWindowToDialogQueue.set(window.id, windowDialogQueue);
}
// Queue message box requests per window so that one can show
// after the other.
if (window) {
let windowDialogQueue = this.windowDialogQueues.get(window.id);
if (!windowDialogQueue) {
windowDialogQueue = new Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>();
this.windowDialogQueues.set(window.id, windowDialogQueue);
}
return windowDialogQueue;
return windowDialogQueue as unknown as Queue<T>;
} else {
return this.noWindowDialogueQueue as unknown as Queue<T>;
}
}
showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise<MessageBoxReturnValue> {
return this.getDialogQueue(window).queue(async () => {
return this.getWindowDialogQueue<MessageBoxReturnValue>(window).queue(async () => {
if (window) {
return dialog.showMessageBox(window, options);
}
@@ -147,61 +151,115 @@ export class DialogMainService implements IDialogMainService {
});
}
showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> {
async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> {
function normalizePath(path: string | undefined): string | undefined {
if (path && isMacintosh) {
path = normalizeNFC(path); // normalize paths returned from the OS
}
return path;
// prevent duplicates of the same dialog queueing at the same time
const fileDialogLock = this.acquireFileDialogLock(options, window);
if (!fileDialogLock) {
throw new Error('A file save dialog is already or will be showing for the window with the same configuration');
}
return this.getDialogQueue(window).queue(async () => {
let result: SaveDialogReturnValue;
if (window) {
result = await dialog.showSaveDialog(window, options);
} else {
result = await dialog.showSaveDialog(options);
}
try {
return await this.getWindowDialogQueue<SaveDialogReturnValue>(window).queue(async () => {
let result: SaveDialogReturnValue;
if (window) {
result = await dialog.showSaveDialog(window, options);
} else {
result = await dialog.showSaveDialog(options);
}
result.filePath = normalizePath(result.filePath);
result.filePath = this.normalizePath(result.filePath);
return result;
});
return result;
});
} finally {
dispose(fileDialogLock);
}
}
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
function normalizePaths(paths: string[]): string[] {
if (paths && paths.length > 0 && isMacintosh) {
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
}
return paths;
private normalizePath(path: string): string;
private normalizePath(path: string | undefined): string | undefined;
private normalizePath(path: string | undefined): string | undefined {
if (path && isMacintosh) {
path = normalizeNFC(path); // macOS only: normalize paths to NFC form
}
return this.getDialogQueue(window).queue(async () => {
return path;
}
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
private normalizePaths(paths: string[]): string[] {
return paths.map(path => this.normalizePath(path));
}
async showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
}
}
// prevent duplicates of the same dialog queueing at the same time
const fileDialogLock = this.acquireFileDialogLock(options, window);
if (!fileDialogLock) {
throw new Error('A file open dialog is already or will be showing for the window with the same configuration');
}
try {
return await this.getWindowDialogQueue<OpenDialogReturnValue>(window).queue(async () => {
let result: OpenDialogReturnValue;
if (window) {
result = await dialog.showOpenDialog(window, options);
} else {
result = await dialog.showOpenDialog(options);
}
result.filePaths = this.normalizePaths(result.filePaths);
return result;
});
} finally {
dispose(fileDialogLock);
}
}
private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined {
// if no window is provided, allow as many dialogs as
// needed since we consider them not modal per window
if (!window) {
return Disposable.None;
}
// if a window is provided, only allow a single dialog
// at the same time because dialogs are modal and we
// do not want to open one dialog after the other
// (https://github.com/microsoft/vscode/issues/114432)
// we figure this out by `hashing` the configuration
// options for the dialog to prevent duplicates
let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id);
if (!windowFileDialogLocks) {
windowFileDialogLocks = new Set();
this.windowFileDialogLocks.set(window.id, windowFileDialogLocks);
}
const optionsHash = hash(options);
if (windowFileDialogLocks.has(optionsHash)) {
return undefined; // prevent duplicates, return
}
windowFileDialogLocks.add(optionsHash);
return toDisposable(() => {
windowFileDialogLocks?.delete(optionsHash);
// if the window has no more dialog locks, delete it from the set of locks
if (windowFileDialogLocks?.size === 0) {
this.windowFileDialogLocks.delete(window.id);
}
// Show dialog
let result: OpenDialogReturnValue;
if (window) {
result = await dialog.showOpenDialog(window, options);
} else {
result = await dialog.showOpenDialog(options);
}
result.filePaths = normalizePaths(result.filePaths);
return result;
});
}
}

View File

@@ -6,7 +6,7 @@
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-sandbox/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { timeout } from 'vs/base/common/async';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';

View File

@@ -62,7 +62,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
await this.whenUnfrozen(windowId);
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
if (!window?.win) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
@@ -101,7 +101,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
}
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
if (!window?.win) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
@@ -207,10 +207,10 @@ export class Driver implements IDriver, IWindowDriverRegistry {
export async function serve(
windowServer: IPCServer,
handle: string,
environmentService: IEnvironmentMainService,
environmentMainService: IEnvironmentMainService,
instantiationService: IInstantiationService
): Promise<IDisposable> {
const verbose = environmentService.driverVerbose;
const verbose = environmentMainService.driverVerbose;
const driver = instantiationService.createInstance(Driver, windowServer, { verbose });
const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver);

View File

@@ -111,6 +111,19 @@ export enum EditorActivation {
PRESERVE
}
export enum EditorOverride {
/**
* Displays a picker and allows the user to decide which editor to use
*/
PICK = 1,
/**
* Disables overrides
*/
DISABLED
}
export enum EditorOpenContext {
/**
@@ -204,10 +217,10 @@ export interface IEditorOptions {
/**
* Allows to override the editor that should be used to display the input:
* - `undefined`: let the editor decide for itself
* - `false`: disable overrides
* - `string`: specific override by id
* - `EditorOverride`: specific override handling
*/
readonly override?: false | string;
readonly override?: string | EditorOverride;
/**
* A optional hint to signal in which context the editor opens.

View File

@@ -65,6 +65,7 @@ export interface NativeParsedArgs {
'export-default-configuration'?: string;
'install-source'?: string;
'disable-updates'?: boolean;
'disable-keytar'?: boolean;
'disable-crash-reporter'?: boolean;
'crash-reporter-directory'?: string;
'crash-reporter-id'?: string;

View File

@@ -115,9 +115,6 @@ export interface INativeEnvironmentService extends IEnvironmentService {
machineSettingsResource: URI;
installSourcePath: string;
// --- IPC Handles
sharedIPCHandle: string;
// --- Extensions
extensionsPath: string;
extensionsDownloadPath: string;

View File

@@ -36,6 +36,7 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
sandbox: boolean;
driverVerbose: boolean;
disableUpdates: boolean;
disableKeytar: boolean;
}
export class EnvironmentMainService extends NativeEnvironmentService implements IEnvironmentMainService {
@@ -61,6 +62,9 @@ export class EnvironmentMainService extends NativeEnvironmentService implements
@memoize
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
@memoize
get disableKeytar(): boolean { return !!this._args['disable-keytar']; }
@memoize
get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; }
}

View File

@@ -97,6 +97,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'skip-release-notes': { type: 'boolean' },
'disable-telemetry': { type: 'boolean' },
'disable-updates': { type: 'boolean' },
'disable-keytar': { type: 'boolean' },
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },

View File

@@ -3,18 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { homedir, tmpdir } from 'os';
import product from 'vs/platform/product/common/product';
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import * as paths from 'vs/base/node/paths';
import * as path from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { getDefaultUserDataPath } from 'vs/base/node/userDataPath';
import { dirname, join, normalize, resolve } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { memoize } from 'vs/base/common/decorators';
import product from 'vs/platform/product/common/product';
import { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
export class NativeEnvironmentService implements INativeEnvironmentService {
@@ -23,46 +22,46 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
get args(): NativeParsedArgs { return this._args; }
@memoize
get appRoot(): string { return path.dirname(FileAccess.asFileUri('', require).fsPath); }
get appRoot(): string { return dirname(FileAccess.asFileUri('', require).fsPath); }
readonly logsPath: string;
@memoize
get userHome(): URI { return URI.file(os.homedir()); }
get userHome(): URI { return URI.file(homedir()); }
@memoize
get userDataPath(): string {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'user-data');
return join(vscodePortable, 'user-data');
}
return parseUserDataDir(this._args, process);
}
@memoize
get appSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'User')); }
get appSettingsHome(): URI { return URI.file(join(this.userDataPath, 'User')); }
@memoize
get tmpDir(): URI { return URI.file(os.tmpdir()); }
get tmpDir(): URI { return URI.file(tmpdir()); }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); }
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); }
get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); }
@memoize
get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); }
get userDataSyncLogResource(): URI { return URI.file(join(this.logsPath, 'userDataSync.log')); }
@memoize
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
@memoize
get machineSettingsResource(): URI { return resources.joinPath(URI.file(path.join(this.userDataPath, 'Machine')), 'settings.json'); }
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
@@ -71,32 +70,32 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
@memoize
get keybindingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keybindings.json'); }
get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get argvResource(): URI {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(path.join(vscodePortable, 'argv.json'));
return URI.file(join(vscodePortable, 'argv.json'));
}
return resources.joinPath(this.userHome, product.dataFolderName, 'argv.json');
return joinPath(this.userHome, product.dataFolderName, 'argv.json');
}
@memoize
get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); }
get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize
get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; }
@memoize
get untitledWorkspacesHome(): URI { return URI.file(path.join(this.userDataPath, 'Workspaces')); }
get untitledWorkspacesHome(): URI { return URI.file(join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); }
get installSourcePath(): string { return join(this.userDataPath, 'installSource'); }
@memoize
get builtinExtensionsPath(): string {
@@ -104,7 +103,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
if (fromArgs) {
return fromArgs;
} else {
return path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
}
@@ -113,7 +112,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
if (fromArgs) {
return fromArgs;
} else {
return path.join(this.userDataPath, 'CachedExtensionVSIXs');
return join(this.userDataPath, 'CachedExtensionVSIXs');
}
}
@@ -132,10 +131,10 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'extensions');
return join(vscodePortable, 'extensions');
}
return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath;
return joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath;
}
@memoize get extraExtensionPaths(): string[] {
@@ -153,7 +152,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
if (/^[^:/?#]+?:\/\//.test(p)) {
return URI.parse(p);
}
return URI.file(path.normalize(p));
return URI.file(normalize(p));
});
}
return undefined;
@@ -166,7 +165,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
if (/^[^:/?#]+?:\/\//.test(s)) {
return URI.parse(s);
}
return URI.file(path.normalize(s));
return URI.file(normalize(s));
}
return undefined;
}
@@ -196,10 +195,7 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
get logLevel(): string | undefined { return this._args.log; }
@memoize
get sharedIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'shared', product.version); }
@memoize
get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); }
get serviceMachineIdResource(): URI { return joinPath(URI.file(this.userDataPath), 'machineid'); }
get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; }
get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; }
@@ -207,13 +203,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
get driverHandle(): string | undefined { return this._args['driver']; }
@memoize
get telemetryLogResource(): URI { return URI.file(path.join(this.logsPath, 'telemetry.log')); }
get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; }
constructor(protected _args: NativeParsedArgs) {
if (!_args.logsPath) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
_args.logsPath = path.join(this.userDataPath, 'logs', key);
_args.logsPath = join(this.userDataPath, 'logs', key);
}
this.logsPath = _args.logsPath;
}
@@ -242,15 +238,15 @@ export function parsePathArg(arg: string | undefined, process: NodeJS.Process):
// Determine if the arg is relative or absolute, if relative use the original CWD
// (VSCODE_CWD), not the potentially overridden one (process.cwd()).
const resolved = path.resolve(arg);
const resolved = resolve(arg);
if (path.normalize(arg) === resolved) {
if (normalize(arg) === resolved) {
return resolved;
}
return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg);
return resolve(process.env['VSCODE_CWD'] || process.cwd(), arg);
}
export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string {
return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath());
return parsePathArg(args['user-data-dir'], process) || resolve(getDefaultUserDataPath());
}

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { spawn } from 'child_process';
import { generateUuid } from 'vs/base/common/uuid';
import { isWindows, platform } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { getSystemShell } from 'vs/base/node/shell';
/**
* We need to get the environment from a user's shell.
* This should only be done when Code itself is not launched
* from within a shell.
*/
export async function resolveShellEnv(logService: ILogService, args: NativeParsedArgs, env: NodeJS.ProcessEnv): Promise<typeof process.env> {
// Skip if --force-disable-user-env
if (args['force-disable-user-env']) {
logService.trace('resolveShellEnv(): skipped (--force-disable-user-env)');
return {};
}
// Skip on windows
else if (isWindows) {
logService.trace('resolveShellEnv(): skipped (Windows)');
return {};
}
// Skip if running from CLI already
else if (isLaunchedFromCli(env) && !args['force-user-env']) {
logService.trace('resolveShellEnv(): skipped (VSCODE_CLI is set)');
return {};
}
// Otherwise resolve (macOS, Linux)
else {
if (isLaunchedFromCli(env)) {
logService.trace('resolveShellEnv(): running (--force-user-env)');
} else {
logService.trace('resolveShellEnv(): running (macOS/Linux)');
}
if (!unixShellEnvPromise) {
unixShellEnvPromise = doResolveUnixShellEnv(logService);
}
return unixShellEnvPromise;
}
}
let unixShellEnvPromise: Promise<typeof process.env> | undefined = undefined;
async function doResolveUnixShellEnv(logService: ILogService): Promise<typeof process.env> {
const promise = new Promise<typeof process.env>(async (resolve, reject) => {
const runAsNode = process.env['ELECTRON_RUN_AS_NODE'];
logService.trace('getUnixShellEnvironment#runAsNode', runAsNode);
const noAttach = process.env['ELECTRON_NO_ATTACH_CONSOLE'];
logService.trace('getUnixShellEnvironment#noAttach', noAttach);
const mark = generateUuid().replace(/-/g, '').substr(0, 12);
const regex = new RegExp(mark + '(.*)' + mark);
const env = {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
};
const command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
logService.trace('getUnixShellEnvironment#env', env);
logService.trace('getUnixShellEnvironment#spawn', command);
const systemShellUnix = await getSystemShell(platform);
const child = spawn(systemShellUnix, ['-ilc', command], {
detached: true,
stdio: ['ignore', 'pipe', process.stderr],
env
});
const buffers: Buffer[] = [];
child.on('error', () => resolve({}));
child.stdout.on('data', b => buffers.push(b));
child.on('close', code => {
if (code !== 0) {
return reject(new Error('Failed to get environment'));
}
const raw = Buffer.concat(buffers).toString('utf8');
logService.trace('getUnixShellEnvironment#raw', raw);
const match = regex.exec(raw);
const rawStripped = match ? match[1] : '{}';
try {
const env = JSON.parse(rawStripped);
if (runAsNode) {
env['ELECTRON_RUN_AS_NODE'] = runAsNode;
} else {
delete env['ELECTRON_RUN_AS_NODE'];
}
if (noAttach) {
env['ELECTRON_NO_ATTACH_CONSOLE'] = noAttach;
} else {
delete env['ELECTRON_NO_ATTACH_CONSOLE'];
}
// https://github.com/microsoft/vscode/issues/22593#issuecomment-336050758
delete env['XDG_RUNTIME_DIR'];
logService.trace('getUnixShellEnvironment#result', env);
resolve(env);
} catch (err) {
logService.error('getUnixShellEnvironment#error', err);
reject(err);
}
});
});
try {
return await promise;
} catch (error) {
logService.error('getUnixShellEnvironment#error', toErrorMessage(error));
return {}; // ignore any errors
}
}

View File

@@ -3,15 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small.
*/
import * as path from 'vs/base/common/path';
import * as os from 'os';
import * as fs from 'fs';
import { tmpdir } from 'os';
import { join } from 'vs/base/common/path';
export function createWaitMarkerFile(verbose?: boolean): string | undefined {
const randomWaitMarkerPath = path.join(os.tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
const randomWaitMarkerPath = join(tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
try {
fs.writeFileSync(randomWaitMarkerPath, ''); // use built-in fs to avoid dragging in more dependencies

View File

@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { Disposable } from 'vs/base/common/lifecycle';
import { rename } from 'vs/base/node/pfs';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -15,6 +15,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import * as semver from 'vs/base/common/semver/semver';
import { isWindows } from 'vs/base/common/platform';
import { Promises } from 'vs/base/common/async';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
@@ -62,7 +63,7 @@ export class ExtensionsDownloader extends Disposable {
private async rename(from: URI, to: URI, retryUntil: number): Promise<void> {
try {
await rename(from.fsPath, to.fsPath);
await promises.rename(from.fsPath, to.fsPath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`);
@@ -97,7 +98,7 @@ export class ExtensionsDownloader extends Disposable {
}
distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
await Promise.all(toDelete.map(resource => {
await Promises.settled(toDelete.map(resource => {
this.logService.trace('Deleting vsix from cache', resource.path);
return this.fileService.del(resource);
}));

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
@@ -25,7 +26,7 @@ import {
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
import { createCancelablePromise, CancelablePromise, Promises } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import * as semver from 'vs/base/common/semver/semver';
import { URI } from 'vs/base/common/uri';
@@ -130,7 +131,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
const collectFilesFromDirectory = async (dir: string): Promise<string[]> => {
let entries = await pfs.readdir(dir);
entries = entries.map(e => path.join(dir, e));
const stats = await Promise.all(entries.map(e => pfs.stat(e)));
const stats = await Promise.all(entries.map(e => fs.promises.stat(e)));
let promise: Promise<string[]> = Promise.resolve([]);
stats.forEach((stat, index) => {
const entry = entries[index];
@@ -459,7 +460,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None);
const extensionsToInstall = galleryResult.firstPage;
try {
await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e, options)));
await Promises.settled(extensionsToInstall.map(e => this.installFromGallery(e, options)));
} catch (error) {
try { await this.rollback(extensionsToInstall); } catch (e) { /* ignore */ }
throw error;
@@ -471,7 +472,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private async rollback(extensions: IGalleryExtension[]): Promise<void> {
const installed = await this.getInstalled(ExtensionType.User);
const extensionsToUninstall = installed.filter(local => extensions.some(galleryExtension => new ExtensionIdentifierWithVersion(local.identifier, local.manifest.version).equals(new ExtensionIdentifierWithVersion(galleryExtension.identifier, galleryExtension.version)))); // Check with version because we want to rollback the exact version
await Promise.all(extensionsToUninstall.map(local => this.uninstall(local)));
await Promises.settled(extensionsToUninstall.map(local => this.uninstall(local)));
}
async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise<void> {
@@ -555,7 +556,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.checkForDependents(e, extensionsToUninstall, installed, extension);
}
}
await Promise.all([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]);
await Promises.settled([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]);
}
private checkForDependents(extension: ILocalExtension, extensionsToUninstall: ILocalExtension[], installed: ILocalExtension[], extensionToUninstall: ILocalExtension): void {

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as semver from 'vs/base/common/semver/semver';
import { Disposable } from 'vs/base/common/lifecycle';
import * as pfs from 'vs/base/node/pfs';
@@ -11,7 +12,7 @@ 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 { Limiter, Promises, Queue } from 'vs/base/common/async';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
@@ -138,7 +139,7 @@ export class ExtensionsScanner extends Disposable {
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
metadata.isBuiltin = metadata.isBuiltin || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.readFile(manifestPath, 'utf8');
const raw = await fs.promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = metadata;
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
@@ -153,7 +154,7 @@ export class ExtensionsScanner extends Disposable {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
raw = await pfs.readFile(this.uninstalledPath, 'utf8');
raw = await fs.promises.readFile(this.uninstalledPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
@@ -211,7 +212,7 @@ export class ExtensionsScanner extends Disposable {
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
try {
await pfs.rename(extractPath, renamePath);
await fs.promises.rename(extractPath, renamePath);
} catch (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);
@@ -306,14 +307,14 @@ export class ExtensionsScanner extends Disposable {
}
}
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
await Promise.all(byExtension.map(async e => {
await Promises.settled(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.beforeRemovingExtension(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)));
await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private async removeOutdatedExtensions(): Promise<void> {
@@ -324,7 +325,7 @@ export class ExtensionsScanner extends Disposable {
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')));
await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}
private getDevSystemExtensionsList(): string[] {
@@ -351,9 +352,9 @@ export class ExtensionsScanner extends Disposable {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
const promises = [
pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8')
fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];

View File

@@ -113,6 +113,15 @@ export interface IAuthenticationContribution {
readonly label: string;
}
export interface IGettingStartedContent {
readonly id: string;
readonly title: string;
readonly description: string;
readonly button: { title: string } & ({ command?: never, link: string } | { command: string, link?: never }),
readonly media: { path: string | { hc: string, light: string, dark: string }, altText: string },
readonly when?: string;
}
export interface IExtensionContributions {
commands?: ICommand[];
configuration?: IConfiguration | IConfiguration[];
@@ -132,10 +141,13 @@ export interface IExtensionContributions {
readonly customEditors?: readonly IWebviewEditor[];
readonly codeActions?: readonly ICodeActionContribution[];
authentication?: IAuthenticationContribution[];
gettingStarted?: IGettingStartedContent[];
}
export type ExtensionKind = 'ui' | 'workspace' | 'web';
export type ExtensionWorkspaceTrustRequirement = false | 'onStart' | 'onDemand';
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing
&& typeof thing === 'object'
@@ -162,8 +174,8 @@ export const EXTENSION_CATEGORIES = [
'Programming Languages',
'SCM Providers',
'Snippets',
'Themes',
'Testing',
'Themes',
'Visualization',
'Other',
];
@@ -190,6 +202,7 @@ export interface IExtensionManifest {
readonly enableProposedApi?: boolean;
readonly api?: string;
readonly scripts?: { [key: string]: string; };
readonly requiresWorkspaceTrust?: ExtensionWorkspaceTrustRequirement;
}
export const enum ExtensionType {

View File

@@ -8,22 +8,22 @@ import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isVal
suite('Extension Version Validator', () => {
test('isValidVersionStr', () => {
assert.equal(isValidVersionStr('0.10.0-dev'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.1'), true);
assert.equal(isValidVersionStr('0.10.100'), true);
assert.equal(isValidVersionStr('0.11.0'), true);
assert.strictEqual(isValidVersionStr('0.10.0-dev'), true);
assert.strictEqual(isValidVersionStr('0.10.0'), true);
assert.strictEqual(isValidVersionStr('0.10.1'), true);
assert.strictEqual(isValidVersionStr('0.10.100'), true);
assert.strictEqual(isValidVersionStr('0.11.0'), true);
assert.equal(isValidVersionStr('x.x.x'), true);
assert.equal(isValidVersionStr('0.x.x'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.x'), true);
assert.equal(isValidVersionStr('^0.10.0'), true);
assert.equal(isValidVersionStr('*'), true);
assert.strictEqual(isValidVersionStr('x.x.x'), true);
assert.strictEqual(isValidVersionStr('0.x.x'), true);
assert.strictEqual(isValidVersionStr('0.10.0'), true);
assert.strictEqual(isValidVersionStr('0.10.x'), true);
assert.strictEqual(isValidVersionStr('^0.10.0'), true);
assert.strictEqual(isValidVersionStr('*'), true);
assert.equal(isValidVersionStr('0.x.x.x'), false);
assert.equal(isValidVersionStr('0.10'), false);
assert.equal(isValidVersionStr('0.10.'), false);
assert.strictEqual(isValidVersionStr('0.x.x.x'), false);
assert.strictEqual(isValidVersionStr('0.10'), false);
assert.strictEqual(isValidVersionStr('0.10.'), false);
});
test('parseVersion', () => {
@@ -31,7 +31,7 @@ suite('Extension Version Validator', () => {
const actual = parseVersion(version);
const expected: IParsedVersion = { hasCaret, hasGreaterEquals, majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, preRelease };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version);
}
assertParseVersion('0.10.0-dev', false, false, 0, true, 10, true, 0, true, '-dev');
@@ -56,7 +56,7 @@ suite('Extension Version Validator', () => {
function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void {
const actual = normalizeVersion(parseVersion(version));
const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version);
}
assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false);
@@ -80,7 +80,7 @@ suite('Extension Version Validator', () => {
test('isValidVersion', () => {
function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void {
let actual = isValidVersion(version, desiredVersion);
assert.equal(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
}
testIsValidVersion('0.10.0-dev', 'x.x.x', true);
@@ -213,7 +213,7 @@ suite('Extension Version Validator', () => {
let reasons: string[] = [];
let actual = isValidExtensionVersion(version, desc, reasons);
assert.equal(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
}
function testIsInvalidExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean): void {

View File

@@ -10,7 +10,6 @@ import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { Throttler } from 'vs/base/common/async';
import { localize } from 'vs/nls';
import * as browser from 'vs/base/browser/browser';
import { joinPath } from 'vs/base/common/resources';
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
@@ -48,9 +47,6 @@ export class IndexedDB {
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdgeLegacy) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
const request = window.indexedDB.open(name, version);
request.onerror = (err) => e(request.error);

View File

@@ -14,8 +14,8 @@ import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream';
import { Queue } from 'vs/base/common/async';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, listenStream, consumeStream } from 'vs/base/common/stream';
import { Promises, Queue } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { readFileIntoStream } from 'vs/platform/files/common/io';
@@ -91,7 +91,7 @@ export class FileService extends Disposable implements IFileService {
// If the provider is not yet there, make sure to join on the listeners assuming
// that it takes a bit longer to register the file system provider.
await Promise.all(joiners);
await Promises.settled(joiners);
}
canHandleResource(resource: URI): boolean {
@@ -239,7 +239,7 @@ export class FileService extends Disposable implements IFileService {
if (fileStat.isDirectory && recurse(fileStat, siblings)) {
try {
const entries = await provider.readdir(resource);
const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => {
const resolvedEntries = await Promises.settled(entries.map(async ([name, type]) => {
try {
const childResource = providerExtUri.joinPath(resource, name);
const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
@@ -269,7 +269,7 @@ export class FileService extends Disposable implements IFileService {
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]>;
async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise<IResolveFileResultWithMetadata[]>;
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(async entry => {
return Promises.settled(toResolve.map(async entry => {
try {
return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
} catch (error) {
@@ -454,20 +454,17 @@ export class FileService extends Disposable implements IFileService {
throw error;
});
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
let fileStream: VSBufferReadableStream | undefined = undefined;
try {
// if the etag is provided, we await the result of the validation
// due to the likelyhood of hitting a NOT_MODIFIED_SINCE result.
// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
// otherwise, we let it run in parallel to the file reading for
// optimal startup performance.
if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED) {
await statPromise;
}
let fileStream: VSBufferReadableStream | undefined = undefined;
// read unbuffered (only if either preferred, or the provider has no buffered read capability)
if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) {
fileStream = this.readFileUnbuffered(provider, resource, options);
@@ -483,9 +480,6 @@ export class FileService extends Disposable implements IFileService {
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
}
// observe the stream for the error case below
fileStreamObserver = observe(fileStream);
const fileStat = await statPromise;
return {
@@ -497,8 +491,8 @@ export class FileService extends Disposable implements IFileService {
// Await the stream to finish so that we exit this method
// in a consistent state with file handles closed
// (https://github.com/microsoft/vscode/issues/114024)
if (fileStreamObserver) {
await fileStreamObserver.errorOrEnd();
if (fileStream) {
await consumeStream(fileStream);
}
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@@ -552,6 +546,7 @@ export class FileService extends Disposable implements IFileService {
stream.end(VSBuffer.wrap(buffer));
} catch (err) {
stream.error(err);
stream.end();
}
})();
@@ -738,7 +733,7 @@ export class FileService extends Disposable implements IFileService {
// create children in target
if (Array.isArray(sourceFolder.children)) {
await Promise.all(sourceFolder.children.map(async sourceChild => {
await Promises.settled(sourceFolder.children.map(async sourceChild => {
const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);
if (sourceChild.isDirectory) {
return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);
@@ -1065,28 +1060,29 @@ export class FileService extends Disposable implements IFileService {
return new Promise(async (resolve, reject) => {
stream.on('data', async chunk => {
listenStream(stream, {
onData: async chunk => {
// pause stream to perform async write operation
stream.pause();
// pause stream to perform async write operation
stream.pause();
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
posInFile += chunk.byteLength;
posInFile += chunk.byteLength;
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
},
onError: error => reject(error),
onEnd: () => resolve()
});
stream.on('error', error => reject(error));
stream.on('end', () => resolve());
});
}
@@ -1170,7 +1166,7 @@ export class FileService extends Disposable implements IFileService {
} catch (error) {
throw ensureFileSystemProviderError(error);
} finally {
await Promise.all([
await Promises.settled([
typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(),
typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(),
]);

View File

@@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs';
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs';
import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
@@ -30,7 +30,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
usePolling: boolean | string[];
}
export interface IDiskFileSystemProviderOptions {
@@ -80,7 +80,7 @@ export class DiskFileSystemProvider extends Disposable implements
async stat(resource: URI): Promise<IStat> {
try {
const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
return {
type: this.toType(stat, symbolicLink),
@@ -95,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdirWithFileTypes(this.toFilePath(resource));
const children = await readdir(this.toFilePath(resource), { withFileTypes: true });
const result: [string, FileType][] = [];
await Promise.all(children.map(async child => {
@@ -119,7 +119,7 @@ export class DiskFileSystemProvider extends Disposable implements
}
}
private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType {
private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType {
// Signal file type by checking for file / directory, except:
// - symbolic links pointing to non-existing files are FileType.Unknown
@@ -151,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements
try {
const filePath = this.toFilePath(resource);
return await readFile(filePath);
return await promises.readFile(filePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -212,18 +212,20 @@ export class DiskFileSystemProvider extends Disposable implements
let flags: string | undefined = undefined;
if (opts.create) {
if (isWindows && await exists(filePath)) {
if (isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/microsoft/vscode/issues/6363)
await truncate(filePath, 0);
await promises.truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
} catch (error) {
this.logService.trace(error);
if (error.code !== 'ENOENT') {
this.logService.trace(error);
}
}
}
@@ -398,7 +400,7 @@ export class DiskFileSystemProvider extends Disposable implements
async mkdir(resource: URI): Promise<void> {
try {
await promisify(mkdir)(this.toFilePath(resource));
await promises.mkdir(this.toFilePath(resource));
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -418,7 +420,7 @@ export class DiskFileSystemProvider extends Disposable implements
if (opts.recursive) {
await rimraf(filePath, RimRafMode.MOVE);
} else {
await unlink(filePath);
await promises.unlink(filePath);
}
}
@@ -463,7 +465,7 @@ export class DiskFileSystemProvider extends Disposable implements
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
// Copy
await copy(fromFilePath, toFilePath);
await copy(fromFilePath, toFilePath, { preserveSymlinks: true });
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks

View File

@@ -5,7 +5,7 @@
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { statLink } from 'vs/base/node/pfs';
import { SymlinkSupport } from 'vs/base/node/pfs';
import { realpath } from 'vs/base/node/extpath';
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
@@ -35,7 +35,7 @@ export class FileWatcher extends Disposable {
private async startWatching(): Promise<void> {
try {
const { stat, symbolicLink } = await statLink(this.path);
const { stat, symbolicLink } = await SymlinkSupport.stat(this.path);
if (this.isDisposed) {
return;
@@ -47,6 +47,10 @@ export class FileWatcher extends Disposable {
pathToWatch = await realpath(pathToWatch);
} catch (error) {
this.onError(error);
if (symbolicLink.dangling) {
return; // give up if symbolic link is dangling
}
}
}
@@ -70,7 +74,9 @@ export class FileWatcher extends Disposable {
}, error => this.onError(error)));
}
} catch (error) {
this.onError(error);
if (error.code !== 'ENOENT') {
this.onError(error);
}
}
}

View File

@@ -61,9 +61,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
});
// Logging
if (this.verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
this.debug(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
// Stop watching some roots
rootsToStopWatching.forEach(root => {
@@ -133,9 +131,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}
}
if (this.verboseLogging) {
this.log(`Start watching with nsfw: ${request.path}`);
}
this.debug(`Start watching with nsfw: ${request.path}`);
nsfw(request.path, events => {
for (const e of events) {
@@ -249,4 +245,8 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
}
}

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new NsfwWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -61,7 +61,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);

View File

@@ -49,7 +49,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
get wacherCount() { return this._watcherCount; }
private pollingInterval?: number;
private usePolling?: boolean;
private usePolling?: boolean | string[];
private verboseLogging: boolean | undefined;
private spamCheckStartTime: number | undefined;
@@ -101,7 +101,11 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
const pollingInterval = this.pollingInterval || 5000;
const usePolling = this.usePolling;
let usePolling = this.usePolling; // boolean or a list of path patterns
if (Array.isArray(usePolling)) {
// switch to polling if one of the paths matches with a watched path
usePolling = usePolling.some(pattern => requests.some(r => glob.match(pattern, r.path)));
}
const watcherOpts: chokidar.WatchOptions = {
ignoreInitial: true,
@@ -142,9 +146,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
if (this.verboseLogging) {
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
}
this.debug(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
this._watcherCount++;
@@ -297,6 +299,10 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
}

View File

@@ -13,7 +13,7 @@ export interface IWatcherRequest {
export interface IWatcherOptions {
pollingInterval?: number;
usePolling?: boolean;
usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling
verboseLogging?: boolean;
}

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new ChokidarWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -62,7 +62,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));

View File

@@ -13,7 +13,7 @@ export interface IDiskFileChange {
}
export interface ILogMessage {
type: 'trace' | 'warn' | 'error';
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
message: string;
}

View File

@@ -6,11 +6,13 @@
import * as assert from 'assert';
import { FileService } from 'vs/platform/files/common/fileService';
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent, FileOpenOptions, FileReadStreamOptions, IStat, FileType } from 'vs/platform/files/common/files';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NullLogService } from 'vs/platform/log/common/log';
import { timeout } from 'vs/base/common/async';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
suite('File Service', () => {
@@ -74,6 +76,8 @@ suite('File Service', () => {
assert.strictEqual(registrations.length, 2);
assert.strictEqual(registrations[1].scheme, 'test');
assert.strictEqual(registrations[1].added, false);
service.dispose();
});
test('watch', async () => {
@@ -121,5 +125,85 @@ suite('File Service', () => {
assert.strictEqual(disposeCounter, 1);
watcher3Disposable2.dispose();
assert.strictEqual(disposeCounter, 2);
service.dispose();
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
testReadErrorBubbles(true);
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060)', async () => {
testReadErrorBubbles(false);
});
async function testReadErrorBubbles(async: boolean) {
const service = new FileService(new NullLogService());
const provider = new class extends NullFileSystemProvider {
async stat(resource: URI): Promise<IStat> {
return {
mtime: Date.now(),
ctime: Date.now(),
size: 100,
type: FileType.File
};
}
readFile(resource: URI): Promise<Uint8Array> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
open(resource: URI, opts: FileOpenOptions): Promise<number> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
if (async) {
const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);
timeout(5).then(() => stream.error(new Error('failed')));
return stream;
}
throw new Error('failed');
}
};
const disposable = service.registerProvider('test', provider);
for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) {
provider.setCapabilities(capabilities);
let e1;
try {
await service.readFile(URI.parse('test://foo/bar'));
} catch (error) {
e1 = error;
}
assert.ok(e1);
let e2;
try {
const stream = await service.readFileStream(URI.parse('test://foo/bar'));
await consumeStream(stream.value, chunk => chunk[0]);
} catch (error) {
e2 = error;
}
assert.ok(e2);
}
disposable.dispose();
}
});

View File

@@ -11,9 +11,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { join, basename, dirname, posix } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { copy, rimraf, symlink, rimrafSync } from 'vs/base/node/pfs';
import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux, isWindows } from 'vs/base/common/platform';
@@ -147,7 +147,7 @@ flakySuite('Disk File Service', function () {
const sourceDir = getPathFromAmdModule(require, './fixtures/service');
await copy(sourceDir, testDir);
await copy(sourceDir, testDir, { preserveSymlinks: false });
});
teardown(() => {
@@ -408,7 +408,7 @@ flakySuite('Disk File Service', function () {
test('resolve - folder symbolic link', async () => {
const link = URI.file(join(testDir, 'deep-link'));
await symlink(join(testDir, 'deep'), link.fsPath, 'junction');
await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction');
const resolved = await service.resolve(link);
assert.strictEqual(resolved.children!.length, 4);
@@ -418,7 +418,7 @@ flakySuite('Disk File Service', function () {
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => {
const link = URI.file(join(testDir, 'lorem.txt-linked'));
await symlink(join(testDir, 'lorem.txt'), link.fsPath);
await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath);
const resolved = await service.resolve(link);
assert.strictEqual(resolved.isDirectory, false);
@@ -426,7 +426,7 @@ flakySuite('Disk File Service', function () {
});
test('resolve - symbolic link pointing to non-existing file does not break', async () => {
await symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction');
await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction');
const resolved = await service.resolve(URI.file(testDir));
assert.strictEqual(resolved.isDirectory, true);
@@ -477,7 +477,7 @@ flakySuite('Disk File Service', function () {
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => {
const target = URI.file(join(testDir, 'lorem.txt'));
const link = URI.file(join(testDir, 'lorem.txt-linked'));
await symlink(target.fsPath, link.fsPath);
await promises.symlink(target.fsPath, link.fsPath);
const source = await service.resolve(link);
@@ -499,7 +499,7 @@ flakySuite('Disk File Service', function () {
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => {
const target = URI.file(join(testDir, 'foo'));
const link = URI.file(join(testDir, 'bar'));
await symlink(target.fsPath, link.fsPath);
await promises.symlink(target.fsPath, link.fsPath);
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
@@ -1586,7 +1586,7 @@ flakySuite('Disk File Service', function () {
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => {
const link = URI.file(join(testDir, 'small.js-link'));
await symlink(join(testDir, 'small.js'), link.fsPath);
await promises.symlink(join(testDir, 'small.js'), link.fsPath);
let error: FileOperationError | undefined = undefined;
try {
@@ -2024,7 +2024,7 @@ flakySuite('Disk File Service', function () {
(runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => {
const toWatch = URI.file(join(testDir, 'lorem.txt-linked'));
await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath);
await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath);
const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50);
@@ -2152,7 +2152,7 @@ flakySuite('Disk File Service', function () {
(runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => {
const watchDir = URI.file(join(testDir, 'deep-link'));
await symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction');
await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction');
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');

View File

@@ -8,8 +8,14 @@ import { ServiceIdentifier, BrandedService } from './instantiation';
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation?: boolean): void {
_registry.push([id, new SyncDescriptor<T>(ctor as new (...args: any[]) => T, [], supportsDelayedInstantiation)]);
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation?: boolean): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, descriptor: SyncDescriptor<any>): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], supportsDelayedInstantiation);
}
_registry.push([id, ctorOrDescriptor]);
}
export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
/**
* An implementation of `IMainProcessService` that leverages MessagePorts.
*/
export class MessagePortMainProcessService implements IMainProcessService {
declare readonly _serviceBrand: undefined;
constructor(
private server: MessagePortServer,
private router: StaticRouter
) { }
getChannel(channelName: string): IChannel {
return this.server.getChannel(channelName, this.router);
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.server.registerChannel(channelName, channel);
}
}

View File

@@ -1,68 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { generateUuid } from 'vs/base/common/uuid';
import { ILogService } from 'vs/platform/log/common/log';
import { Disposable } from 'vs/base/common/lifecycle';
export const ISharedProcessService = createDecorator<ISharedProcessService>('sharedProcessService');
export interface ISharedProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
export class SharedProcessService extends Disposable implements ISharedProcessService {
declare readonly _serviceBrand: undefined;
private readonly withSharedProcessConnection: Promise<MessagePortClient>;
constructor(
@INativeHostService private readonly nativeHostService: INativeHostService,
@ILogService private readonly logService: ILogService
) {
super();
this.withSharedProcessConnection = this.connect();
}
private async connect(): Promise<MessagePortClient> {
this.logService.trace('Renderer->SharedProcess#connect');
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce);
// Wait until the main side has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
this.logService.trace('Renderer->SharedProcess#connect: connection established');
return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`));
}
getChannel(channelName: string): IChannel {
return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName)));
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel));
}
}

View File

@@ -3,22 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron';
import { Disposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
export interface IMainProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
/**
* An implementation of `IMainProcessService` that leverages Electron's IPC.
@@ -45,24 +33,3 @@ export class ElectronIPCMainProcessService extends Disposable implements IMainPr
this.mainProcessConnection.registerChannel(channelName, channel);
}
}
/**
* An implementation of `IMainProcessService` that leverages MessagePorts.
*/
export class MessagePortMainProcessService implements IMainProcessService {
declare readonly _serviceBrand: undefined;
constructor(
private server: MessagePortServer,
private router: StaticRouter
) { }
getChannel(channelName: string): IChannel {
return this.server.getChannel(channelName, this.router);
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.server.registerChannel(channelName, channel);
}
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
type ChannelClientCtor<T> = { new(channel: IChannel): T };
type Remote = { getChannel(channelName: string): IChannel; };
abstract class RemoteServiceStub<T> {
constructor(
channelName: string,
channelClientCtor: ChannelClientCtor<T> | undefined,
remote: Remote
) {
const channel = remote.getChannel(channelName);
if (channelClientCtor) {
return new channelClientCtor(channel);
} else {
return ProxyChannel.toService(channel);
}
}
}
export interface IRemoteServiceOptions<T> {
readonly channelClientCtor?: ChannelClientCtor<T>;
readonly supportsDelayedInstantiation?: boolean;
}
//#region Main Process
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
export interface IMainProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
class MainProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, channelClientCtor: ChannelClientCtor<T> | undefined, @IMainProcessService ipcService: IMainProcessService) {
super(channelName, channelClientCtor, ipcService);
}
}
export function registerMainProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options: IRemoteServiceOptions<T> = {}): void {
registerSingleton(id, new SyncDescriptor(MainProcessRemoteServiceStub, [channelName, options.channelClientCtor], options.supportsDelayedInstantiation));
}
//#endregion
//#region Shared Process
export const ISharedProcessService = createDecorator<ISharedProcessService>('sharedProcessService');
export interface ISharedProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
class SharedProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, channelClientCtor: ChannelClientCtor<T> | undefined, @ISharedProcessService ipcService: ISharedProcessService) {
super(channelName, channelClientCtor, ipcService);
}
}
export function registerSharedProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options: IRemoteServiceOptions<T> = {}): void {
registerSingleton(id, new SyncDescriptor(SharedProcessRemoteServiceStub, [channelName, options.channelClientCtor], options.supportsDelayedInstantiation));
}
//#endregion

View File

@@ -10,8 +10,7 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { ICommonIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue';
import { BrowserWindow, ipcMain, screen, IpcMainEvent, Display } from 'electron';
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
import { IDiagnosticsService, PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
@@ -39,7 +38,7 @@ export class IssueMainService implements ICommonIssueService {
constructor(
private machineId: string,
private userEnv: IProcessEnvironment,
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ILaunchMainService private readonly launchMainService: ILaunchMainService,
@ILogService private readonly logService: ILogService,
@IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService,
@@ -272,7 +271,7 @@ export class IssueMainService implements ICommonIssueService {
this._processExplorerWindow.setMenuBarVisibility(false);
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
appRoot: this.environmentMainService.appRoot,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
@@ -398,13 +397,13 @@ export class IssueMainService implements ICommonIssueService {
}
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
appRoot: this.environmentMainService.appRoot,
windowId: this._issueWindow.id,
machineId: this.machineId,
userEnv: this.userEnv,
data,
features,
disableExtensions: this.environmentService.disableExtensions,
disableExtensions: this.environmentMainService.disableExtensions,
os: {
type: os.type(),
arch: os.arch(),

View File

@@ -5,7 +5,7 @@
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { IntervalTimer } from 'vs/base/common/async';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode, Keybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
@@ -35,6 +35,9 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
private _currentChord: CurrentChord | null;
private _currentChordChecker: IntervalTimer;
private _currentChordStatusMessage: IDisposable | null;
private _currentSingleModifier: null | string;
private _currentSingleModifierClearTimeout: TimeoutTimer;
protected _logging: boolean;
public get inChordMode(): boolean {
@@ -53,6 +56,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
this._currentChord = null;
this._currentChordChecker = new IntervalTimer();
this._currentChordStatusMessage = null;
this._currentSingleModifier = null;
this._currentSingleModifierClearTimeout = new TimeoutTimer();
this._logging = false;
}
@@ -166,30 +171,69 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
public dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void {
const keybindings = this.resolveUserBinding(userSettingsLabel);
if (keybindings.length >= 1) {
this._doDispatch(keybindings[0], target);
this._doDispatch(keybindings[0], target, /*isSingleModiferChord*/false);
}
}
protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
return this._doDispatch(this.resolveKeyboardEvent(e), target);
return this._doDispatch(this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/false);
}
private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget): boolean {
protected _singleModifierDispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
const keybinding = this.resolveKeyboardEvent(e);
const [singleModifier,] = keybinding.getSingleModifierDispatchParts();
if (singleModifier !== null && this._currentSingleModifier === null) {
// we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms
this._log(`+ Storing single modifier for possible chord ${singleModifier}.`);
this._currentSingleModifier = singleModifier;
this._currentSingleModifierClearTimeout.cancelAndSet(() => {
this._log(`+ Clearing single modifier due to 300ms elapsed.`);
this._currentSingleModifier = null;
}, 300);
return false;
}
if (singleModifier !== null && singleModifier === this._currentSingleModifier) {
// bingo!
this._log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`);
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return this._doDispatch(keybinding, target, /*isSingleModiferChord*/true);
}
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
private _doDispatch(keybinding: ResolvedKeybinding, target: IContextKeyServiceTarget, isSingleModiferChord = false): boolean {
let shouldPreventDefault = false;
if (keybinding.isChord()) {
console.warn('Unexpected keyboard event mapped to a chord');
return false;
}
const [firstPart,] = keybinding.getDispatchParts();
let firstPart: string | null = null; // the first keybinding i.e. Ctrl+K
let currentChord: string | null = null;// the "second" keybinding i.e. Ctrl+K "Ctrl+D"
if (isSingleModiferChord) {
const [dispatchKeyname,] = keybinding.getSingleModifierDispatchParts();
firstPart = dispatchKeyname;
currentChord = dispatchKeyname;
} else {
[firstPart,] = keybinding.getDispatchParts();
currentChord = this._currentChord ? this._currentChord.keypress : null;
}
if (firstPart === null) {
this._log(`\\ Keyboard event cannot be dispatched.`);
this._log(`\\ Keyboard event cannot be dispatched in keydown phase.`);
// cannot be dispatched, probably only modifier keys
return shouldPreventDefault;
}
const contextValue = this._contextKeyService.getContext(target);
const currentChord = this._currentChord ? this._currentChord.keypress : null;
const keypressLabel = keybinding.getLabel();
const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart);

View File

@@ -69,10 +69,15 @@ export abstract class BaseResolvedKeybinding<T extends Modifiers> extends Resolv
return this._parts.map((keybinding) => this._getDispatchPart(keybinding));
}
public getSingleModifierDispatchParts(): (string | null)[] {
return this._parts.map((keybinding) => this._getSingleModifierDispatchPart(keybinding));
}
protected abstract _getLabel(keybinding: T): string | null;
protected abstract _getAriaLabel(keybinding: T): string | null;
protected abstract _getElectronAccelerator(keybinding: T): string | null;
protected abstract _getUserSettingsLabel(keybinding: T): string | null;
protected abstract _isWYSIWYG(keybinding: T): boolean;
protected abstract _getDispatchPart(keybinding: T): string | null;
protected abstract _getSingleModifierDispatchPart(keybinding: T): string | null;
}

View File

@@ -23,6 +23,10 @@ export class ResolvedKeybindingItem {
constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null, isBuiltinExtension: boolean) {
this.resolvedKeybinding = resolvedKeybinding;
this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : [];
if (resolvedKeybinding && this.keypressParts.length === 0) {
// handle possible single modifier chord keybindings
this.keypressParts = removeElementsAfterNulls(resolvedKeybinding.getSingleModifierDispatchParts());
}
this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false);
this.command = this.bubble ? command!.substr(1) : command;
this.commandArgs = commandArgs;

View File

@@ -111,4 +111,20 @@ export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding<SimpleKey
return result;
}
protected _getSingleModifierDispatchPart(keybinding: SimpleKeybinding): string | null {
if (keybinding.keyCode === KeyCode.Ctrl && !keybinding.shiftKey && !keybinding.altKey && !keybinding.metaKey) {
return 'ctrl';
}
if (keybinding.keyCode === KeyCode.Shift && !keybinding.ctrlKey && !keybinding.altKey && !keybinding.metaKey) {
return 'shift';
}
if (keybinding.keyCode === KeyCode.Alt && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.metaKey) {
return 'alt';
}
if (keybinding.keyCode === KeyCode.Meta && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.altKey) {
return 'meta';
}
return null;
}
}

View File

@@ -118,6 +118,7 @@ suite('AbstractKeybindingService', () => {
contextMatchesRules: undefined!,
getContextKeyValue: undefined!,
createScoped: undefined!,
createOverlay: undefined!,
getContext: (target: IContextKeyServiceTarget): any => {
return currentContextValue;
},

View File

@@ -66,6 +66,9 @@ export class MockContextKeyService implements IContextKeyService {
public createScoped(domNode: HTMLElement): IContextKeyService {
return this;
}
public createOverlay(): IContextKeyService {
return this;
}
updateParent(_parentContextKeyService: IContextKeyService): void {
// no-op
}

View File

@@ -3,11 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nativeKeymap from 'native-keymap';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IKeyboardLayoutData, IKeyboardLayoutMainService as ICommonKeyboardLayoutMainService } from 'vs/platform/keyboardLayout/common/keyboardLayoutMainService';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import * as nativeKeymap from 'native-keymap';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
export const IKeyboardLayoutMainService = createDecorator<IKeyboardLayoutMainService>('keyboardLayoutMainService');
@@ -23,10 +24,17 @@ export class KeyboardLayoutMainService extends Disposable implements ICommonKeyb
private _initPromise: Promise<void> | null;
private _keyboardLayoutData: IKeyboardLayoutData | null;
constructor() {
constructor(
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
) {
super();
this._initPromise = null;
this._keyboardLayoutData = null;
// perf: automatically trigger initialize after windows
// have opened so that we can do this work in parallel
// to the window load.
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this._initialize());
}
private _initialize(): Promise<void> {

View File

@@ -17,10 +17,11 @@ import { URI } from 'vs/base/common/uri';
import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron';
import { coalesce } from 'vs/base/common/arrays';
import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/launch';
import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/common/launch';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { assertIsDefined } from 'vs/base/common/types';
export const ID = 'launchMainService';
export const ILaunchMainService = createDecorator<ILaunchMainService>(ID);
@@ -293,8 +294,9 @@ export class LaunchMainService implements ILaunchMainService {
private codeWindowToInfo(window: ICodeWindow): IWindowInfo {
const folderURIs = this.getFolderURIs(window);
const win = assertIsDefined(window.win);
return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority);
return this.browserWindowToInfo(win, folderURIs, window.remoteAuthority);
}
private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo {

View File

@@ -32,7 +32,7 @@ export interface ILayoutService {
* An event that is emitted when the container is layed out. The
* event carries the dimensions of the container as part of it.
*/
readonly onLayout: Event<IDimension>;
readonly onDidLayout: Event<IDimension>;
/**
* Focus the primary component of the container.

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isThenable } from 'vs/base/common/async';
import { Promises, isThenable } from 'vs/base/common/async';
// Shared veto handling across main and renderer
export function handleVetos(vetos: (boolean | Promise<boolean>)[], onError: (error: Error) => void): Promise<boolean /* veto */> {
@@ -33,5 +33,5 @@ export function handleVetos(vetos: (boolean | Promise<boolean>)[], onError: (err
}
}
return Promise.all(promises).then(() => lazyValue);
return Promises.settled(promises).then(() => lazyValue);
}

View File

@@ -11,9 +11,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { Disposable } from 'vs/base/common/lifecycle';
import { Barrier, timeout } from 'vs/base/common/async';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Promises, Barrier, timeout } from 'vs/base/common/async';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { assertIsDefined } from 'vs/base/common/types';
export const ILifecycleMainService = createDecorator<ILifecycleMainService>('lifecycleMainService');
@@ -24,6 +26,11 @@ export const enum UnloadReason {
LOAD = 4
}
export interface IWindowLoadEvent {
window: ICodeWindow;
workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined;
}
export interface IWindowUnloadEvent {
window: ICodeWindow;
reason: UnloadReason;
@@ -72,16 +79,27 @@ export interface ILifecycleMainService {
readonly onWillShutdown: Event<ShutdownEvent>;
/**
* An event that fires before a window closes. This event is fired after any veto has been dealt
* with so that listeners know for sure that the window will close without veto.
* An event that fires when a window is loading. This can either be a window opening for the
* first time or a window reloading or changing to another URL.
*/
readonly onBeforeWindowClose: Event<ICodeWindow>;
readonly onWillLoadWindow: Event<IWindowLoadEvent>;
/**
* An event that fires before a window is about to unload. Listeners can veto this event to prevent
* the window from unloading.
*/
readonly onBeforeWindowUnload: Event<IWindowUnloadEvent>;
readonly onBeforeUnloadWindow: Event<IWindowUnloadEvent>;
/**
* An event that fires before a window closes. This event is fired after any veto has been dealt
* with so that listeners know for sure that the window will close without veto.
*/
readonly onBeforeCloseWindow: Event<ICodeWindow>;
/**
* Make a `ICodeWindow` known to the lifecycle main service.
*/
registerWindow(window: ICodeWindow): void;
/**
* Reload a window. All lifecycle event handlers are triggered.
@@ -147,11 +165,14 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
private readonly _onWillShutdown = this._register(new Emitter<ShutdownEvent>());
readonly onWillShutdown = this._onWillShutdown.event;
private readonly _onBeforeWindowClose = this._register(new Emitter<ICodeWindow>());
readonly onBeforeWindowClose = this._onBeforeWindowClose.event;
private readonly _onWillLoadWindow = this._register(new Emitter<IWindowLoadEvent>());
readonly onWillLoadWindow = this._onWillLoadWindow.event;
private readonly _onBeforeWindowUnload = this._register(new Emitter<IWindowUnloadEvent>());
readonly onBeforeWindowUnload = this._onBeforeWindowUnload.event;
private readonly _onBeforeCloseWindow = this._register(new Emitter<ICodeWindow>());
readonly onBeforeCloseWindow = this._onBeforeCloseWindow.event;
private readonly _onBeforeUnloadWindow = this._register(new Emitter<IWindowUnloadEvent>());
readonly onBeforeUnloadWindow = this._onBeforeUnloadWindow.event;
private _quitRequested = false;
get quitRequested(): boolean { return this._quitRequested; }
@@ -274,7 +295,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
}
});
this.pendingWillShutdownPromise = Promise.all(joiners).then(() => undefined, err => this.logService.error(err));
this.pendingWillShutdownPromise = Promises.settled(joiners).then(() => undefined, err => this.logService.error(err));
return this.pendingWillShutdownPromise;
}
@@ -314,12 +335,17 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
}
registerWindow(window: ICodeWindow): void {
const windowListeners = new DisposableStore();
// track window count
this.windowCounter++;
// Window Will Load
windowListeners.add(window.onWillLoad(e => this._onWillLoadWindow.fire({ window, workspace: e.workspace })));
// Window Before Closing: Main -> Renderer
window.win.on('close', e => {
const win = assertIsDefined(window.win);
win.on('close', e => {
// The window already acknowledged to be closed
const windowId = window.id;
@@ -341,9 +367,9 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
this.windowToCloseRequest.add(windowId);
// Fire onBeforeWindowClose before actually closing
this.logService.trace(`Lifecycle#onBeforeWindowClose.fire() - window ID ${windowId}`);
this._onBeforeWindowClose.fire(window);
// Fire onBeforeCloseWindow before actually closing
this.logService.trace(`Lifecycle#onBeforeCloseWindow.fire() - window ID ${windowId}`);
this._onBeforeCloseWindow.fire(window);
// No veto, close window now
window.close();
@@ -351,12 +377,15 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
});
// Window After Closing
window.win.on('closed', () => {
win.on('closed', () => {
this.logService.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`);
// update window count
this.windowCounter--;
// clear window listeners
windowListeners.dispose();
// if there are no more code windows opened, fire the onWillShutdown event, unless
// we are on macOS where it is perfectly fine to close the last window and
// the application continues running (unless quit was actually requested)
@@ -452,7 +481,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
private onBeforeUnloadWindowInMain(window: ICodeWindow, reason: UnloadReason): Promise<boolean /* veto */> {
const vetos: (boolean | Promise<boolean>)[] = [];
this._onBeforeWindowUnload.fire({
this._onBeforeUnloadWindow.fire({
reason,
window,
veto(value) {

View File

@@ -26,9 +26,11 @@ import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeC
import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree';
import { IKeyboardNavigationEventFilter, IAbstractTreeOptions, RenderIndentGuides, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ITableOptions, ITableOptionsUpdate, Table } from 'vs/base/browser/ui/table/tableWidget';
import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table';
export type ListWidget = List<any> | PagedList<any> | ObjectTree<any, any> | DataTree<any, any, any> | AsyncDataTree<any, any, any>;
export type WorkbenchListWidget = WorkbenchList<any> | WorkbenchPagedList<any> | WorkbenchObjectTree<any, any> | WorkbenchCompressibleObjectTree<any, any> | WorkbenchDataTree<any, any, any> | WorkbenchAsyncDataTree<any, any, any> | WorkbenchCompressibleAsyncDataTree<any, any, any>;
export type ListWidget = List<any> | PagedList<any> | ObjectTree<any, any> | DataTree<any, any, any> | AsyncDataTree<any, any, any> | Table<any>;
export type WorkbenchListWidget = WorkbenchList<any> | WorkbenchPagedList<any> | WorkbenchObjectTree<any, any> | WorkbenchCompressibleObjectTree<any, any> | WorkbenchDataTree<any, any, any> | WorkbenchAsyncDataTree<any, any, any> | WorkbenchCompressibleAsyncDataTree<any, any, any> | WorkbenchTable<any>;
export const IListService = createDecorator<IListService>('listService');
@@ -189,22 +191,20 @@ export interface IWorkbenchListOptionsUpdate extends IListOptionsUpdate {
readonly overrideStyles?: IColorMapping;
}
export interface IWorkbenchListOptions<T> extends IWorkbenchListOptionsUpdate, IListOptions<T> {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
}
export interface IWorkbenchListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IListOptions<T> { }
export class WorkbenchList<T> extends List<T> {
readonly contextKeyService: IContextKeyService;
private readonly themeService: IThemeService;
private listHasSelectionOrFocus: IContextKey<boolean>;
private listDoubleSelection: IContextKey<boolean>;
private listMultiSelection: IContextKey<boolean>;
private horizontalScrolling: boolean | undefined;
private _styler: IDisposable | undefined;
private _useAltAsMultipleSelectionModifier: boolean;
private navigator: ListResourceNavigator<T>;
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.navigator.onDidOpen; }
constructor(
user: string,
@@ -287,6 +287,9 @@ export class WorkbenchList<T> extends List<T> {
this.updateOptions(options);
}
}));
this.navigator = new ListResourceNavigator(this, { configurationService, ...options });
this.disposables.add(this.navigator);
}
updateOptions(options: IWorkbenchListOptionsUpdate): void {
@@ -297,38 +300,33 @@ export class WorkbenchList<T> extends List<T> {
}
}
dispose(): void {
super.dispose();
if (this._styler) {
this._styler.dispose();
}
}
private updateStyles(styles: IColorMapping): void {
if (this._styler) {
this._styler.dispose();
}
this._styler?.dispose();
this._styler = attachListStyler(this, this.themeService, styles);
}
get useAltAsMultipleSelectionModifier(): boolean {
return this._useAltAsMultipleSelectionModifier;
}
dispose(): void {
this._styler?.dispose();
super.dispose();
}
}
export interface IWorkbenchPagedListOptions<T> extends IWorkbenchListOptionsUpdate, IPagedListOptions<T> {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
}
export interface IWorkbenchPagedListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IPagedListOptions<T> { }
export class WorkbenchPagedList<T> extends PagedList<T> {
readonly contextKeyService: IContextKeyService;
private readonly themeService: IThemeService;
private readonly disposables: DisposableStore;
private _useAltAsMultipleSelectionModifier: boolean;
private horizontalScrolling: boolean | undefined;
private _styler: IDisposable | undefined;
private navigator: ListResourceNavigator<T>;
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.navigator.onDidOpen; }
constructor(
user: string,
@@ -357,6 +355,8 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
this.disposables.add(workbenchListOptionsDisposable);
this.contextKeyService = createScopedContextKeyService(contextKeyService, this);
this.themeService = themeService;
this.horizontalScrolling = options.horizontalScrolling;
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
@@ -367,6 +367,10 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
this.disposables.add(this.contextKeyService);
this.disposables.add((listService as ListService).register(this));
if (options.overrideStyles) {
this.updateStyles(options.overrideStyles);
}
if (options.overrideStyles) {
this.disposables.add(attachListStyler(this, themeService, options.overrideStyles));
}
@@ -390,6 +394,22 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
this.updateOptions(options);
}
}));
this.navigator = new ListResourceNavigator(this, { configurationService, ...options });
this.disposables.add(this.navigator);
}
updateOptions(options: IWorkbenchListOptionsUpdate): void {
super.updateOptions(options);
if (options.overrideStyles) {
this.updateStyles(options.overrideStyles);
}
}
private updateStyles(styles: IColorMapping): void {
this._styler?.dispose();
this._styler = attachListStyler(this, this.themeService, styles);
}
get useAltAsMultipleSelectionModifier(): boolean {
@@ -397,9 +417,141 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
}
dispose(): void {
super.dispose();
this._styler?.dispose();
this.disposables.dispose();
super.dispose();
}
}
export interface IWorkbenchTableOptionsUpdate extends ITableOptionsUpdate {
readonly overrideStyles?: IColorMapping;
}
export interface IWorkbenchTableOptions<T> extends IWorkbenchTableOptionsUpdate, ITableOptions<T> { }
export class WorkbenchTable<TRow> extends Table<TRow> {
readonly contextKeyService: IContextKeyService;
private readonly themeService: IThemeService;
private listHasSelectionOrFocus: IContextKey<boolean>;
private listDoubleSelection: IContextKey<boolean>;
private listMultiSelection: IContextKey<boolean>;
private horizontalScrolling: boolean | undefined;
private _styler: IDisposable | undefined;
private _useAltAsMultipleSelectionModifier: boolean;
private readonly disposables: DisposableStore;
private navigator: TableResourceNavigator<TRow>;
get onDidOpen(): Event<IOpenEvent<TRow | undefined>> { return this.navigator.onDidOpen; }
constructor(
user: string,
container: HTMLElement,
delegate: ITableVirtualDelegate<TRow>,
columns: ITableColumn<TRow, any>[],
renderers: ITableRenderer<TRow, any>[],
options: IWorkbenchTableOptions<TRow>,
@IContextKeyService contextKeyService: IContextKeyService,
@IListService listService: IListService,
@IThemeService themeService: IThemeService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService
) {
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
super(user, container, delegate, columns, renderers,
{
keyboardSupport: false,
...computeStyles(themeService.getColorTheme(), defaultListStyles),
...workbenchListOptions,
horizontalScrolling
}
);
this.disposables = new DisposableStore();
this.disposables.add(workbenchListOptionsDisposable);
this.contextKeyService = createScopedContextKeyService(contextKeyService, this);
this.themeService = themeService;
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);
this.horizontalScrolling = options.horizontalScrolling;
this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService);
this.disposables.add(this.contextKeyService);
this.disposables.add((listService as ListService).register(this));
if (options.overrideStyles) {
this.updateStyles(options.overrideStyles);
}
this.disposables.add(this.onDidChangeSelection(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.contextKeyService.bufferChangeEvents(() => {
this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0);
this.listMultiSelection.set(selection.length > 1);
this.listDoubleSelection.set(selection.length === 2);
});
}));
this.disposables.add(this.onDidChangeFocus(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0);
}));
this.disposables.add(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(multiSelectModifierSettingKey)) {
this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService);
}
let options: IListOptionsUpdate = {};
if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
options = { ...options, horizontalScrolling };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
options = { ...options, smoothScrolling };
}
if (Object.keys(options).length > 0) {
this.updateOptions(options);
}
}));
this.navigator = new TableResourceNavigator(this, { configurationService, ...options });
this.disposables.add(this.navigator);
}
updateOptions(options: IWorkbenchTableOptionsUpdate): void {
super.updateOptions(options);
if (options.overrideStyles) {
this.updateStyles(options.overrideStyles);
}
}
private updateStyles(styles: IColorMapping): void {
this._styler?.dispose();
this._styler = attachListStyler(this, this.themeService, styles);
}
get useAltAsMultipleSelectionModifier(): boolean {
return this._useAltAsMultipleSelectionModifier;
}
dispose(): void {
this._styler?.dispose();
this.disposables.dispose();
super.dispose();
}
}
@@ -552,11 +704,11 @@ abstract class ResourceNavigator<T> extends Disposable {
abstract getSelectedElement(): T | undefined;
}
export class ListResourceNavigator<T> extends ResourceNavigator<T> {
class ListResourceNavigator<T> extends ResourceNavigator<T> {
constructor(
protected readonly widget: List<T> | PagedList<T>,
options?: IResourceNavigatorOptions
options: IResourceNavigatorOptions
) {
super(widget, options);
}
@@ -566,6 +718,20 @@ export class ListResourceNavigator<T> extends ResourceNavigator<T> {
}
}
class TableResourceNavigator<TRow> extends ResourceNavigator<TRow> {
constructor(
protected readonly widget: Table<TRow>,
options: IResourceNavigatorOptions
) {
super(widget, options);
}
getSelectedElement(): TRow | undefined {
return this.widget.getSelectedElements()[0];
}
}
class TreeResourceNavigator<T, TFilterData> extends ResourceNavigator<T> {
constructor(
@@ -852,7 +1018,6 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService),
additionalScrollHeight,
hideTwistiesOfChildlessElements: options.hideTwistiesOfChildlessElements,
expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick',
expandOnlyOnTwistieClick: options.expandOnlyOnTwistieClick ?? (configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick')
} as TOptions
};
@@ -952,9 +1117,6 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
newOptions = { ...newOptions, horizontalScrolling };
}
if (e.affectsConfiguration(openModeSettingKey)) {
newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' };
}
if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) {
newOptions = { ...newOptions, expandOnlyOnTwistieClick: configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick' };
}
@@ -1021,7 +1183,7 @@ configurationRegistry.registerConfiguration({
'description': localize({
key: 'openModeModifier',
comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.']
}, "Controls how to open items in trees and lists using the mouse (if supported). For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ")
}, "Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.")
},
[horizontalScrollingKey]: {
'type': 'boolean',
@@ -1066,7 +1228,7 @@ configurationRegistry.registerConfiguration({
type: 'string',
enum: ['singleClick', 'doubleClick'],
default: 'singleClick',
description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names."),
description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names. Note that some trees and lists might choose to ignore this setting if it is not applicable."),
}
}
});

View File

@@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as pfs from 'vs/base/node/pfs';
import { writeFile } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { createHash } from 'crypto';
import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -73,10 +74,10 @@ export class LocalizationsService extends Disposable implements ILocalizationsSe
});
}
update(): Promise<boolean> {
return Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()])
.then(([current, installed]) => this.cache.update(installed)
.then(updated => !equals(Object.keys(current), Object.keys(updated))));
async update(): Promise<boolean> {
const [current, installed] = await Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()]);
const updated = await this.cache.update(installed);
return !equals(Object.keys(current), Object.keys(updated));
}
}
@@ -157,7 +158,7 @@ class LanguagePacksCache extends Disposable {
private withLanguagePacks<T>(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise<T> {
return this.languagePacksFileLimiter.queue(() => {
let result: T | null = null;
return pfs.readFile(this.languagePacksFilePath, 'utf8')
return promises.readFile(this.languagePacksFilePath, 'utf8')
.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))
.then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
.then(languagePacks => { result = fn(languagePacks); return languagePacks; })
@@ -171,7 +172,7 @@ class LanguagePacksCache extends Disposable {
this.initializedCache = true;
const raw = JSON.stringify(this.languagePacks);
this.logService.debug('Writing language packs', raw);
return pfs.writeFile(this.languagePacksFilePath, raw);
return writeFile(this.languagePacksFilePath, raw);
})
.then(() => result, error => this.logService.error(error));
});

View File

@@ -3,25 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogService, DEFAULT_LOG_LEVEL, LogLevel, LogServiceAdapter } from 'vs/platform/log/common/log';
import { DEFAULT_LOG_LEVEL, LogLevel, AdapterLogger, ILogger } from 'vs/platform/log/common/log';
interface IAutomatedWindow {
codeAutomationLog(type: string, args: any[]): void;
}
function logLevelToString(level: LogLevel): string {
switch (level) {
case LogLevel.Trace: return 'trace';
case LogLevel.Debug: return 'debug';
case LogLevel.Info: return 'info';
case LogLevel.Warning: return 'warn';
case LogLevel.Error: return 'error';
case LogLevel.Critical: return 'critical';
}
return 'info';
}
/**
* A logger that is used when VSCode is running in the web with
* an automation such as playwright. We expect a global codeAutomationLog
* to be defined that we can use to log to.
*/
export class ConsoleLogInAutomationService extends LogServiceAdapter implements ILogService {
export class ConsoleLogInAutomationLogger extends AdapterLogger implements ILogger {
declare codeAutomationLog: any;
declare readonly _serviceBrand: undefined;
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super({ consoleLog: (type, args) => this.consoleLog(type, args) }, logLevel);
super({ log: (level, args) => this.consoleLog(logLevelToString(level), args) }, logLevel);
}
private consoleLog(type: string, args: any[]): void {

View File

@@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogService, LogLevel, AbstractLogService, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log';
import { ILogService, LogLevel, AbstractLogger, DEFAULT_LOG_LEVEL, ILogger } from 'vs/platform/log/common/log';
interface ILog {
level: LogLevel;
args: any[];
}
function getLogFunction(logger: ILogService, level: LogLevel): Function {
function getLogFunction(logger: ILogger, level: LogLevel): Function {
switch (level) {
case LogLevel.Trace: return logger.trace;
case LogLevel.Debug: return logger.debug;
@@ -22,11 +22,11 @@ function getLogFunction(logger: ILogService, level: LogLevel): Function {
}
}
export class BufferLogService extends AbstractLogService implements ILogService {
export class BufferLogService extends AbstractLogger implements ILogService {
declare readonly _serviceBrand: undefined;
private buffer: ILog[] = [];
private _logger: ILogService | undefined = undefined;
private _logger: ILogger | undefined = undefined;
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
@@ -38,7 +38,7 @@ export class BufferLogService extends AbstractLogService implements ILogService
}));
}
set logger(logger: ILogService) {
set logger(logger: ILogger) {
this._logger = logger;
for (const { level, args } of this.buffer) {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogService, LogLevel, AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log';
import { ILogService, LogLevel, AbstractLogger, ILoggerService, ILogger } from 'vs/platform/log/common/log';
import { URI } from 'vs/base/common/uri';
import { ByteSize, FileOperationError, FileOperationResult, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files';
import { Queue } from 'vs/base/common/async';
@@ -15,9 +15,7 @@ import { BufferLogService } from 'vs/platform/log/common/bufferLog';
const MAX_FILE_SIZE = 5 * ByteSize.MB;
export class FileLogService extends AbstractLogService implements ILogService {
declare readonly _serviceBrand: undefined;
export class FileLogger extends AbstractLogger implements ILogger {
private readonly initializePromise: Promise<void>;
private readonly queue: Queue<void>;
@@ -176,12 +174,12 @@ export class FileLoggerService extends Disposable implements ILoggerService {
this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level))));
}
getLogger(resource: URI): ILogger {
createLogger(resource: URI): ILogger {
let logger = this.loggers.get(resource.toString());
if (!logger) {
logger = new BufferLogService(this.logService.getLevel());
this.loggers.set(resource.toString(), logger);
whenProviderRegistered(resource, this.fileService).then(() => (<BufferLogService>logger).logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel()));
whenProviderRegistered(resource, this.fileService).then(() => (<BufferLogService>logger).logger = this.instantiationService.createInstance(FileLogger, basename(resource), resource, this.logService.getLevel()));
}
return logger;
}

View File

@@ -8,7 +8,6 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { isWindows } from 'vs/base/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { LoggerChannelClient } from 'vs/platform/log/common/logIpc';
import { URI } from 'vs/base/common/uri';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -53,13 +52,39 @@ export interface ILogService extends ILogger {
readonly _serviceBrand: undefined;
}
export interface ILoggerOptions {
/**
* Name of the logger.
*/
name?: string;
/**
* Do not create rotating files if max size exceeds.
*/
donotRotate?: boolean;
/**
* Do not use formatters.
*/
donotUseFormatters?: boolean;
/**
* If set, logger logs the message always.
*/
always?: boolean;
}
export interface ILoggerService {
readonly _serviceBrand: undefined;
getLogger(file: URI): ILogger;
/**
* Creates a logger
*/
createLogger(file: URI, options?: ILoggerOptions): ILogger;
}
export abstract class AbstractLogService extends Disposable {
export abstract class AbstractLogger extends Disposable {
private level: LogLevel = DEFAULT_LOG_LEVEL;
private readonly _onDidChangeLogLevel: Emitter<LogLevel> = this._register(new Emitter<LogLevel>());
@@ -78,9 +103,85 @@ export abstract class AbstractLogService extends Disposable {
}
export class ConsoleLogMainService extends AbstractLogService implements ILogService {
export abstract class AbstractMessageLogger extends AbstractLogger implements ILogger {
protected abstract log(level: LogLevel, message: string): void;
constructor(private readonly logAlways?: boolean) {
super();
}
private checkLogLevel(level: LogLevel): boolean {
return this.logAlways || this.getLevel() <= level;
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
this.log(LogLevel.Trace, this.format([message, ...args]));
}
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
this.log(LogLevel.Debug, this.format([message, ...args]));
}
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
this.log(LogLevel.Info, this.format([message, ...args]));
}
}
warn(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
this.log(LogLevel.Warning, this.format([message, ...args]));
}
}
error(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (message instanceof Error) {
const array = Array.prototype.slice.call(arguments) as any[];
array[0] = message.stack;
this.log(LogLevel.Error, this.format(array));
} else {
this.log(LogLevel.Error, this.format([message, ...args]));
}
}
}
critical(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Critical)) {
this.log(LogLevel.Critical, this.format([message, ...args]));
}
}
flush(): void { }
private format(args: any): string {
let result = '';
for (let i = 0; i < args.length; i++) {
let a = args[i];
if (typeof a === 'object') {
try {
a = JSON.stringify(a);
} catch (e) { }
}
result += (i > 0 ? ' ' : '') + a;
}
return result;
}
}
export class ConsoleMainLogger extends AbstractLogger implements ILogger {
declare readonly _serviceBrand: undefined;
private useColors: boolean;
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
@@ -159,9 +260,7 @@ export class ConsoleLogMainService extends AbstractLogService implements ILogSer
}
export class ConsoleLogService extends AbstractLogService implements ILogService {
declare readonly _serviceBrand: undefined;
export class ConsoleLogger extends AbstractLogger implements ILogger {
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
@@ -213,48 +312,46 @@ export class ConsoleLogService extends AbstractLogService implements ILogService
}
}
export class LogServiceAdapter extends AbstractLogService implements ILogService {
export class AdapterLogger extends AbstractLogger implements ILogger {
declare readonly _serviceBrand: undefined;
constructor(private readonly adapter: { consoleLog: (type: string, args: any[]) => void }, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
constructor(private readonly adapter: { log: (logLevel: LogLevel, args: any[]) => void }, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
this.setLevel(logLevel);
}
trace(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Trace) {
this.adapter.consoleLog('trace', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Trace, [this.extractMessage(message), ...args]);
}
}
debug(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Debug) {
this.adapter.consoleLog('debug', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Debug, [this.extractMessage(message), ...args]);
}
}
info(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Info) {
this.adapter.consoleLog('info', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Info, [this.extractMessage(message), ...args]);
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Warning) {
this.adapter.consoleLog('warn', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Warning, [this.extractMessage(message), ...args]);
}
}
error(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Error) {
this.adapter.consoleLog('error', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Error, [this.extractMessage(message), ...args]);
}
}
critical(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Critical) {
this.adapter.consoleLog('critical', [this.extractMessage(message), ...args]);
this.adapter.log(LogLevel.Critical, [this.extractMessage(message), ...args]);
}
}
@@ -275,19 +372,10 @@ export class LogServiceAdapter extends AbstractLogService implements ILogService
}
}
export class ConsoleLogInMainService extends LogServiceAdapter implements ILogService {
export class MultiplexLogService extends AbstractLogger implements ILogService {
declare readonly _serviceBrand: undefined;
constructor(client: LoggerChannelClient, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super({ consoleLog: (type, args) => client.consoleLog(type, args) }, logLevel);
}
}
export class MultiplexLogService extends AbstractLogService implements ILogService {
declare readonly _serviceBrand: undefined;
constructor(private readonly logServices: ReadonlyArray<ILogService>) {
constructor(private readonly logServices: ReadonlyArray<ILogger>) {
super();
if (logServices.length) {
this.setLevel(logServices[0].getLevel());
@@ -350,52 +438,52 @@ export class MultiplexLogService extends AbstractLogService implements ILogServi
}
}
export class DelegatedLogService extends Disposable implements ILogService {
export class LogService extends Disposable implements ILogService {
declare readonly _serviceBrand: undefined;
constructor(private logService: ILogService) {
constructor(private logger: ILogger) {
super();
this._register(logService);
this._register(logger);
}
get onDidChangeLogLevel(): Event<LogLevel> {
return this.logService.onDidChangeLogLevel;
return this.logger.onDidChangeLogLevel;
}
setLevel(level: LogLevel): void {
this.logService.setLevel(level);
this.logger.setLevel(level);
}
getLevel(): LogLevel {
return this.logService.getLevel();
return this.logger.getLevel();
}
trace(message: string, ...args: any[]): void {
this.logService.trace(message, ...args);
this.logger.trace(message, ...args);
}
debug(message: string, ...args: any[]): void {
this.logService.debug(message, ...args);
this.logger.debug(message, ...args);
}
info(message: string, ...args: any[]): void {
this.logService.info(message, ...args);
this.logger.info(message, ...args);
}
warn(message: string, ...args: any[]): void {
this.logService.warn(message, ...args);
this.logger.warn(message, ...args);
}
error(message: string | Error, ...args: any[]): void {
this.logService.error(message, ...args);
this.logger.error(message, ...args);
}
critical(message: string | Error, ...args: any[]): void {
this.logService.critical(message, ...args);
this.logger.critical(message, ...args);
}
flush(): void {
this.logService.flush();
this.logger.flush();
}
}

View File

@@ -4,10 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { LogLevel, ILogService, DelegatedLogService } from 'vs/platform/log/common/log';
import { LogLevel, ILogService, LogService, ILoggerService, ILogger, AbstractMessageLogger, ILoggerOptions, AdapterLogger } from 'vs/platform/log/common/log';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
export class LoggerChannel implements IServerChannel {
export class LogLevelChannel implements IServerChannel {
onDidChangeLogLevel: Event<LogLevel>;
@@ -23,35 +24,17 @@ export class LoggerChannel implements IServerChannel {
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
async call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'setLevel': this.service.setLevel(arg); return Promise.resolve();
case 'consoleLog': this.consoleLog(arg[0], arg[1]); return Promise.resolve();
case 'setLevel': return this.service.setLevel(arg);
}
throw new Error(`Call not found: ${command}`);
}
private consoleLog(severity: string, args: string[]): void {
let consoleFn = console.log;
switch (severity) {
case 'error':
consoleFn = console.error;
break;
case 'warn':
consoleFn = console.warn;
break;
case 'info':
consoleFn = console.info;
break;
}
consoleFn.call(console, ...args);
}
}
export class LoggerChannelClient {
export class LogLevelChannelClient {
constructor(private channel: IChannel) { }
@@ -60,22 +43,131 @@ export class LoggerChannelClient {
}
setLevel(level: LogLevel): void {
LoggerChannelClient.setLevel(this.channel, level);
LogLevelChannelClient.setLevel(this.channel, level);
}
public static setLevel(channel: IChannel, level: LogLevel): Promise<void> {
return channel.call('setLevel', level);
}
consoleLog(severity: string, args: string[]): void {
this.channel.call('consoleLog', [severity, args]);
}
export class LoggerChannel implements IServerChannel {
private readonly loggers = new Map<string, ILogger>();
constructor(private readonly loggerService: ILoggerService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
async call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'createLogger': this.createLogger(URI.revive(arg[0]), arg[1]); return;
case 'log': return this.log(URI.revive(arg[0]), arg[1]);
case 'consoleLog': return this.consoleLog(arg[0], arg[1]);
}
throw new Error(`Call not found: ${command}`);
}
private createLogger(file: URI, options: ILoggerOptions): void {
this.loggers.set(file.toString(), this.loggerService.createLogger(file, options));
}
private consoleLog(level: LogLevel, args: any[]): void {
let consoleFn = console.log;
switch (level) {
case LogLevel.Error:
consoleFn = console.error;
break;
case LogLevel.Warning:
consoleFn = console.warn;
break;
case LogLevel.Info:
consoleFn = console.info;
break;
}
consoleFn.call(console, ...args);
}
private log(file: URI, messages: [LogLevel, string][]): void {
const logger = this.loggers.get(file.toString());
if (!logger) {
throw new Error('Create the logger before logging');
}
for (const [level, message] of messages) {
switch (level) {
case LogLevel.Trace: logger.trace(message); break;
case LogLevel.Debug: logger.debug(message); break;
case LogLevel.Info: logger.info(message); break;
case LogLevel.Warning: logger.warn(message); break;
case LogLevel.Error: logger.error(message); break;
case LogLevel.Critical: logger.critical(message); break;
default: throw new Error('Invalid log level');
}
}
}
}
export class FollowerLogService extends DelegatedLogService implements ILogService {
export class LoggerChannelClient implements ILoggerService {
declare readonly _serviceBrand: undefined;
constructor(private parent: LoggerChannelClient, logService: ILogService) {
constructor(private readonly channel: IChannel) { }
createConsoleMainLogger(): ILogger {
return new AdapterLogger({
log: (level: LogLevel, args: any[]) => {
this.channel.call('consoleLog', [level, args]);
}
});
}
createLogger(file: URI, options?: ILoggerOptions): ILogger {
return new Logger(this.channel, file, options);
}
}
class Logger extends AbstractMessageLogger {
private isLoggerCreated: boolean = false;
private buffer: [LogLevel, string][] = [];
constructor(
private readonly channel: IChannel,
private readonly file: URI,
loggerOptions?: ILoggerOptions,
) {
super(loggerOptions?.always);
this.channel.call('createLogger', [file, loggerOptions])
.then(() => {
this._log(this.buffer);
this.isLoggerCreated = true;
});
}
protected log(level: LogLevel, message: string) {
this._log([[level, message]]);
}
private _log(messages: [LogLevel, string][]) {
if (this.isLoggerCreated) {
this.channel.call('log', [this.file, messages]);
} else {
this.buffer.push(...messages);
}
}
}
export class FollowerLogService extends LogService implements ILogService {
declare readonly _serviceBrand: undefined;
constructor(private parent: LogLevelChannelClient, logService: ILogService) {
super(logService);
this._register(parent.onDidChangeLogLevel(level => logService.setLevel(level)));
}

View File

@@ -3,45 +3,54 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogService, ILoggerService, ILogger } from 'vs/platform/log/common/log';
import { ILogService, ILoggerService, ILogger, ILoggerOptions, LogLevel } from 'vs/platform/log/common/log';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { basename, extname, dirname } from 'vs/base/common/resources';
import { basename } from 'vs/base/common/resources';
import { Schemas } from 'vs/base/common/network';
import { FileLogService } from 'vs/platform/log/common/fileLogService';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { FileLogger } from 'vs/platform/log/common/fileLog';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import { IFileService } from 'vs/platform/files/common/files';
import { generateUuid } from 'vs/base/common/uuid';
export class LoggerService extends Disposable implements ILoggerService {
declare readonly _serviceBrand: undefined;
private readonly loggers = new Map<string, ILogger>();
private readonly logLevelChangeableLoggers: ILogger[] = [];
constructor(
@ILogService private logService: ILogService,
@IFileService private fileService: IFileService
) {
super();
this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level))));
this._register(logService.onDidChangeLogLevel(level => this.logLevelChangeableLoggers.forEach(logger => logger.setLevel(level))));
}
getLogger(resource: URI): ILogger {
createLogger(resource: URI, options?: ILoggerOptions): ILogger {
let logger = this.loggers.get(resource.toString());
if (!logger) {
if (resource.scheme === Schemas.file) {
const baseName = basename(resource);
const ext = extname(resource);
logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel());
logger = new SpdLogLogger(options?.name || generateUuid(), resource.fsPath, !options?.donotRotate, this.logService.getLevel());
if (options?.donotUseFormatters) {
(<SpdLogLogger>logger).clearFormatters();
}
} else {
logger = new FileLogService(basename(resource), resource, this.logService.getLevel(), this.fileService);
logger = new FileLogger(options?.name ?? basename(resource), resource, this.logService.getLevel(), this.fileService);
}
this.loggers.set(resource.toString(), logger);
if (options?.always) {
logger.setLevel(LogLevel.Trace);
} else {
this.logLevelChangeableLoggers.push(logger);
}
}
return logger;
}
dispose(): void {
this.logLevelChangeableLoggers.splice(0, this.logLevelChangeableLoggers.length);
this.loggers.forEach(logger => logger.dispose());
this.loggers.clear();
super.dispose();

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LogLevel, ILogger, AbstractMessageLogger } from 'vs/platform/log/common/log';
import * as spdlog from 'spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise<spdlog.RotatingLogger | null> {
// Do not crash if spdlog cannot be loaded
try {
const _spdlog = await import('spdlog');
_spdlog.setAsyncMode(8192, 500);
return _spdlog.createRotatingLoggerAsync(name, logfilePath, filesize, filecount);
} catch (e) {
console.error(e);
}
return null;
}
export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger {
const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog');
return _spdlog.createRotatingLogger(name, filename, filesize, filecount);
}
interface ILog {
level: LogLevel;
message: string;
}
function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void {
switch (level) {
case LogLevel.Trace: logger.trace(message); break;
case LogLevel.Debug: logger.debug(message); break;
case LogLevel.Info: logger.info(message); break;
case LogLevel.Warning: logger.warn(message); break;
case LogLevel.Error: logger.error(message); break;
case LogLevel.Critical: logger.critical(message); break;
default: throw new Error('Invalid log level');
}
}
export class SpdLogLogger extends AbstractMessageLogger implements ILogger {
private buffer: ILog[] = [];
private readonly _loggerCreationPromise: Promise<void>;
private _logger: spdlog.RotatingLogger | undefined;
constructor(
private readonly name: string,
private readonly filepath: string,
private readonly rotating: boolean,
level: LogLevel
) {
super();
this.setLevel(level);
this._loggerCreationPromise = this._createSpdLogLogger();
this._register(this.onDidChangeLogLevel(level => {
if (this._logger) {
this._logger.setLevel(level);
}
}));
}
private _createSpdLogLogger(): Promise<void> {
const filecount = this.rotating ? 6 : 1;
const filesize = (30 / filecount) * ByteSize.MB;
return createSpdLogLogger(this.name, this.filepath, filesize, filecount)
.then(logger => {
if (logger) {
this._logger = logger;
this._logger.setLevel(this.getLevel());
for (const { level, message } of this.buffer) {
log(this._logger, level, message);
}
this.buffer = [];
}
});
}
protected log(level: LogLevel, message: string): void {
if (this._logger) {
log(this._logger, level, message);
} else if (this.getLevel() <= level) {
this.buffer.push({ level, message });
}
}
clearFormatters(): void {
if (this._logger) {
this._logger.clearFormatters();
} else {
this._loggerCreationPromise.then(() => this.clearFormatters());
}
}
flush(): void {
if (this._logger) {
this._logger.flush();
} else {
this._loggerCreationPromise.then(() => this.flush());
}
}
dispose(): void {
if (this._logger) {
this.disposeLogger();
} else {
this._loggerCreationPromise.then(() => this.disposeLogger());
}
}
private disposeLogger(): void {
if (this._logger) {
this._logger.drop();
this._logger = undefined;
}
}
}

View File

@@ -1,174 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log';
import * as spdlog from 'spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
async function createSpdLogLogger(processName: string, logsFolder: string): Promise<spdlog.RotatingLogger | null> {
// Do not crash if spdlog cannot be loaded
try {
const _spdlog = await import('spdlog');
_spdlog.setAsyncMode(8192, 500);
const logfilePath = path.join(logsFolder, `${processName}.log`);
return _spdlog.createRotatingLoggerAsync(processName, logfilePath, 5 * ByteSize.MB, 6);
} catch (e) {
console.error(e);
}
return null;
}
export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger {
const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog');
return _spdlog.createRotatingLogger(name, filename, filesize, filecount);
}
interface ILog {
level: LogLevel;
message: string;
}
function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void {
switch (level) {
case LogLevel.Trace: logger.trace(message); break;
case LogLevel.Debug: logger.debug(message); break;
case LogLevel.Info: logger.info(message); break;
case LogLevel.Warning: logger.warn(message); break;
case LogLevel.Error: logger.error(message); break;
case LogLevel.Critical: logger.critical(message); break;
default: throw new Error('Invalid log level');
}
}
export class SpdLogService extends AbstractLogService implements ILogService {
declare readonly _serviceBrand: undefined;
private buffer: ILog[] = [];
private _loggerCreationPromise: Promise<void> | undefined = undefined;
private _logger: spdlog.RotatingLogger | undefined;
constructor(private readonly name: string, private readonly logsFolder: string, level: LogLevel) {
super();
this.setLevel(level);
this._createSpdLogLogger();
this._register(this.onDidChangeLogLevel(level => {
if (this._logger) {
this._logger.setLevel(level);
}
}));
}
private _createSpdLogLogger(): Promise<void> {
if (!this._loggerCreationPromise) {
this._loggerCreationPromise = createSpdLogLogger(this.name, this.logsFolder)
.then(logger => {
if (logger) {
this._logger = logger;
this._logger.setLevel(this.getLevel());
for (const { level, message } of this.buffer) {
log(this._logger, level, message);
}
this.buffer = [];
}
});
}
return this._loggerCreationPromise;
}
private _log(level: LogLevel, message: string): void {
if (this._logger) {
log(this._logger, level, message);
} else if (this.getLevel() <= level) {
this.buffer.push({ level, message });
}
}
trace(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Trace) {
this._log(LogLevel.Trace, this.format([message, ...args]));
}
}
debug(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Debug) {
this._log(LogLevel.Debug, this.format([message, ...args]));
}
}
info(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Info) {
this._log(LogLevel.Info, this.format([message, ...args]));
}
}
warn(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Warning) {
this._log(LogLevel.Warning, this.format([message, ...args]));
}
}
error(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Error) {
if (message instanceof Error) {
const array = Array.prototype.slice.call(arguments) as any[];
array[0] = message.stack;
this._log(LogLevel.Error, this.format(array));
} else {
this._log(LogLevel.Error, this.format([message, ...args]));
}
}
}
critical(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Critical) {
this._log(LogLevel.Critical, this.format([message, ...args]));
}
}
flush(): void {
if (this._logger) {
this._logger.flush();
} else if (this._loggerCreationPromise) {
this._loggerCreationPromise.then(() => this.flush());
}
}
dispose(): void {
if (this._logger) {
this.disposeLogger();
} else if (this._loggerCreationPromise) {
this._loggerCreationPromise.then(() => this.disposeLogger());
}
this._loggerCreationPromise = undefined;
}
private disposeLogger(): void {
if (this._logger) {
this._logger.drop();
this._logger = undefined;
}
}
private format(args: any): string {
let result = '';
for (let i = 0; i < args.length; i++) {
let a = args[i];
if (typeof a === 'object') {
try {
a = JSON.stringify(a);
} catch (e) { }
}
result += (i > 0 ? ' ' : '') + a;
}
return result;
}
}

View File

@@ -67,7 +67,7 @@ export class Menubar {
@IUpdateService private readonly updateService: IUpdateService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IStateService private readonly stateService: IStateService,
@@ -168,9 +168,9 @@ export class Menubar {
this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true);
// // Listen to some events from window service to update menu
this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e));
this.nativeHostMainService.onDidBlurWindow(() => this.onWindowFocusChange());
this.nativeHostMainService.onDidFocusWindow(() => this.onWindowFocusChange());
this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e));
this.nativeHostMainService.onDidBlurWindow(() => this.onDidChangeWindowFocus());
this.nativeHostMainService.onDidFocusWindow(() => this.onDidChangeWindowFocus());
}
private get currentEnableMenuBarMnemonics(): boolean {
@@ -225,7 +225,7 @@ export class Menubar {
}
}
private onWindowsCountChanged(e: IWindowsCountChangedEvent): void {
private onDidChangeWindowsCount(e: IWindowsCountChangedEvent): void {
if (!isMacintosh) {
return;
}
@@ -237,7 +237,7 @@ export class Menubar {
}
}
private onWindowFocusChange(): void {
private onDidChangeWindowFocus(): void {
if (!isMacintosh) {
return;
}
@@ -499,7 +499,7 @@ export class Menubar {
const openInNewWindow = this.isOptionClick(event);
const success = this.windowsMainService.open({
context: OpenContext.MENU,
cli: this.environmentService.args,
cli: this.environmentMainService.args,
urisToOpen: [openable],
forceNewWindow: openInNewWindow,
gotoLineMode: false
@@ -716,7 +716,7 @@ export class Menubar {
if (activeWindow) {
this.logService.trace('menubar#runActionInRenderer', invocation);
if (isMacintosh && !this.environmentService.isBuilt && !activeWindow.isReady) {
if (isMacintosh && !this.environmentMainService.isBuilt && !activeWindow.isReady) {
if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) {
// prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719)
// we already register a keybinding in bootstrap-window.js for opening developer tools in case something

View File

@@ -15,7 +15,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
import { dirExists } from 'vs/base/node/pfs';
import { SymlinkSupport } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -47,7 +47,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService
) {
@@ -76,14 +76,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#region Events
readonly onDidOpenWindow = Event.map(this.windowsMainService.onWindowOpened, window => window.id);
readonly onDidOpenWindow = Event.map(this.windowsMainService.onDidOpenWindow, window => window.id);
readonly onDidMaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-maximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
readonly onDidUnmaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-unmaximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
readonly onDidBlurWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
readonly onDidFocusWindow = Event.any(
Event.map(Event.filter(Event.map(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id),
Event.map(Event.filter(Event.map(this.windowsMainService.onDidChangeWindowsCount, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id),
Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId))
);
@@ -105,7 +105,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return windows.map(window => ({
id: window.id,
workspace: window.openedWorkspace,
title: window.win.getTitle(),
title: window.win?.getTitle() ?? '',
filename: window.getRepresentedFilename(),
dirty: window.isDocumentEdited()
}));
@@ -140,7 +140,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
context: OpenContext.API,
contextWindowId: windowId,
urisToOpen: toOpen,
cli: this.environmentService.args,
cli: this.environmentMainService.args,
forceNewWindow: options.forceNewWindow,
forceReuseWindow: options.forceReuseWindow,
preferNewWindow: options.preferNewWindow,
@@ -176,7 +176,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async isMaximized(windowId: number | undefined): Promise<boolean> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
return window.win.isMaximized();
}
@@ -185,21 +185,21 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async maximizeWindow(windowId: number | undefined): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
window.win.maximize();
}
}
async unmaximizeWindow(windowId: number | undefined): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
window.win.unmaximize();
}
}
async minimizeWindow(windowId: number | undefined): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
window.win.minimize();
}
}
@@ -217,7 +217,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async setMinimumSize(windowId: number | undefined, width: number | undefined, height: number | undefined): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
const [windowWidth, windowHeight] = window.win.getSize();
const [minWindowWidth, minWindowHeight] = window.win.getMinimumSize();
const [newMinWindowWidth, newMinWindowHeight] = [width ?? minWindowWidth, height ?? minWindowHeight];
@@ -250,7 +250,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
private toBrowserWindow(windowId: number | undefined): BrowserWindow | undefined {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
return window.win;
}
@@ -261,7 +261,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
const paths = await this.dialogMainService.pickFileFolder(options);
if (paths) {
this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData);
this.doOpenPicked(await Promise.all(paths.map(async path => (await dirExists(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId);
this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId);
}
}
@@ -293,7 +293,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
this.windowsMainService.open({
context: OpenContext.DIALOG,
contextWindowId: windowId,
cli: this.environmentService.args,
cli: this.environmentMainService.args,
urisToOpen: openable,
forceNewWindow: options.forceNewWindow
});
@@ -386,7 +386,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
const promptOptions = {
name: product.nameLong.replace('-', ''),
icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined
icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${product.nameShort}.icns`) : undefined
};
sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => {
@@ -412,28 +412,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// Windows
if (isWindows) {
if (this.environmentService.isBuilt) {
if (this.environmentMainService.isBuilt) {
return join(dirname(process.execPath), 'bin', `${product.applicationName}.cmd`);
}
return join(this.environmentService.appRoot, 'scripts', 'code-cli.bat');
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.bat');
}
// Linux
if (isLinux) {
if (this.environmentService.isBuilt) {
if (this.environmentMainService.isBuilt) {
return join(dirname(process.execPath), 'bin', `${product.applicationName}`);
}
return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh');
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
}
// macOS
if (this.environmentService.isBuilt) {
return join(this.environmentService.appRoot, 'bin', 'code');
if (this.environmentMainService.isBuilt) {
return join(this.environmentMainService.appRoot, 'bin', 'code');
}
return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh');
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
}
async getOSStatistics(): Promise<IOSStatistics> {
@@ -505,7 +505,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#region macOS Touchbar
async newWindowTab(): Promise<void> {
this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true });
this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, forceEmpty: true });
}
async showPreviousWindowTab(): Promise<void> {
@@ -563,7 +563,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async closeWindowById(currentWindowId: number | undefined, targetWindowId?: number | undefined): Promise<void> {
const window = this.windowById(targetWindowId);
if (window) {
if (window?.win) {
return window.win.close();
}
}
@@ -573,7 +573,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// If the user selected to exit from an extension development host window, do not quit, but just
// close the window unless this is the last window that is opened.
const window = this.windowsMainService.getLastActiveWindow();
if (window?.isExtensionDevelopmentHost && this.windowsMainService.getWindowCount() > 1) {
if (window?.isExtensionDevelopmentHost && this.windowsMainService.getWindowCount() > 1 && window.win) {
window.win.close();
}
@@ -609,14 +609,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async openDevTools(windowId: number | undefined, options?: OpenDevToolsOptions): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
window.win.webContents.openDevTools(options);
}
}
async toggleDevTools(windowId: number | undefined): Promise<void> {
const window = this.windowById(windowId);
if (window) {
if (window?.win) {
const contents = window.win.webContents;
contents.toggleDevTools();
}
@@ -624,7 +624,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise<void> {
const window = this.windowById(windowId);
if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
if (window?.win && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
window.win.webContents.sendInputEvent(event);
}
}
@@ -658,7 +658,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
private static readonly PASSWORD_CHUNK_SIZE = NativeHostMainService.MAX_PASSWORD_LENGTH - 100;
async getPassword(windowId: number | undefined, service: string, account: string): Promise<string | null> {
const keytar = await import('keytar');
const keytar = await this.withKeytar();
const password = await keytar.getPassword(service, account);
if (password) {
@@ -686,7 +686,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise<void> {
const keytar = await import('keytar');
const keytar = await this.withKeytar();
if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) {
let index = 0;
@@ -714,7 +714,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
async deletePassword(windowId: number | undefined, service: string, account: string): Promise<boolean> {
const keytar = await import('keytar');
const keytar = await this.withKeytar();
const didDelete = await keytar.deletePassword(service, account);
if (didDelete) {
@@ -725,17 +725,25 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
async findPassword(windowId: number | undefined, service: string): Promise<string | null> {
const keytar = await import('keytar');
const keytar = await this.withKeytar();
return keytar.findPassword(service);
}
async findCredentials(windowId: number | undefined, service: string): Promise<Array<{ account: string, password: string }>> {
const keytar = await import('keytar');
const keytar = await this.withKeytar();
return keytar.findCredentials(service);
}
private async withKeytar(): Promise<typeof import('keytar')> {
if (this.environmentMainService.disableKeytar) {
throw new Error('keytar has been disabled via --disable-keytar option');
}
return await import('keytar');
}
//#endregion
private windowById(windowId: number | undefined): ICodeWindow | undefined {

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
// @ts-ignore: interface is implemented via proxy
export class NativeHostService implements INativeHostService {
@@ -16,7 +16,7 @@ export class NativeHostService implements INativeHostService {
readonly windowId: number,
@IMainProcessService mainProcessService: IMainProcessService
) {
return createChannelSender<INativeHostService>(mainProcessService.getChannel('nativeHost'), {
return ProxyChannel.toService<INativeHostService>(mainProcessService.getChannel('nativeHost'), {
context: windowId,
properties: (() => {
const properties = new Map<string, unknown>();

View File

@@ -20,7 +20,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
// Running out of sources
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.53.0-dev',
version: '1.54.0-dev',
nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
applicationName: 'code-oss',
@@ -28,7 +28,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
urlProtocol: 'code-oss',
reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new',
licenseName: 'MIT',
licenseUrl: 'https://github.com/microsoft/vscode/blob/master/LICENSE.txt',
licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt',
extensionAllowedProposedApi: [
'ms-vscode.vscode-js-profile-flame',
'ms-vscode.vscode-js-profile-table',

View File

@@ -23,7 +23,6 @@ export interface IBuiltInExtension {
}
export type ConfigurationSyncStore = {
web?: Partial<Omit<ConfigurationSyncStore, 'web'>>,
url: string,
insidersUrl: string,
stableUrl: string,
@@ -47,7 +46,7 @@ export interface IProductConfiguration {
readonly applicationName: string;
readonly urlProtocol: string;
readonly dataFolderName: string;
readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders)
readonly builtInExtensions?: IBuiltInExtension[];
@@ -125,6 +124,7 @@ export interface IProductConfiguration {
readonly portable?: string;
readonly extensionKind?: { readonly [extensionId: string]: ExtensionKind[]; };
readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; };
readonly extensionAllowedProposedApi?: readonly string[];
readonly msftInternalDomains?: string[];

View File

@@ -7,7 +7,7 @@ import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuick
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, listFocusBackground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground } from 'vs/platform/theme/common/colorRegistry';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground } from 'vs/platform/theme/common/colorRegistry';
import { CancellationToken } from 'vs/base/common/cancellation';
import { computeStyles } from 'vs/platform/theme/common/styler';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
@@ -87,7 +87,7 @@ export class QuickInputService extends Themable implements IQuickInputService {
controller.layout(host.dimension, host.offset?.top ?? 0);
// Layout changes
this._register(host.onLayout(dimension => controller.layout(dimension, host.offset?.top ?? 0)));
this._register(host.onDidLayout(dimension => controller.layout(dimension, host.offset?.top ?? 0)));
// Context keys
this._register(controller.onShow(() => this.resetContextKeys()));
@@ -213,7 +213,7 @@ export class QuickInputService extends Themable implements IQuickInputService {
listBackground: quickInputBackground,
// Look like focused when inactive.
listInactiveFocusForeground: listFocusForeground,
listInactiveFocusBackground: listFocusBackground,
listInactiveFocusBackground: quickInputListFocusBackground,
listFocusOutline: activeContrastBorder,
listInactiveFocusOutline: activeContrastBorder,
pickerGroupBorder,

View File

@@ -2,46 +2,47 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as Platform from 'vs/platform/registry/common/platform';
import * as Types from 'vs/base/common/types';
import { Registry } from 'vs/platform/registry/common/platform';
import { isFunction } from 'vs/base/common/types';
suite('Platform / Registry', () => {
test('registry - api', function () {
assert.ok(Types.isFunction(Platform.Registry.add));
assert.ok(Types.isFunction(Platform.Registry.as));
assert.ok(Types.isFunction(Platform.Registry.knows));
assert.ok(isFunction(Registry.add));
assert.ok(isFunction(Registry.as));
assert.ok(isFunction(Registry.knows));
});
test('registry - mixin', function () {
Platform.Registry.add('foo', { bar: true });
Registry.add('foo', { bar: true });
assert.ok(Platform.Registry.knows('foo'));
assert.ok(Platform.Registry.as<any>('foo').bar);
assert.equal(Platform.Registry.as<any>('foo').bar, true);
assert.ok(Registry.knows('foo'));
assert.ok(Registry.as<any>('foo').bar);
assert.equal(Registry.as<any>('foo').bar, true);
});
test('registry - knows, as', function () {
let ext = {};
Platform.Registry.add('knows,as', ext);
Registry.add('knows,as', ext);
assert.ok(Platform.Registry.knows('knows,as'));
assert.ok(!Platform.Registry.knows('knows,as1234'));
assert.ok(Registry.knows('knows,as'));
assert.ok(!Registry.knows('knows,as1234'));
assert.ok(Platform.Registry.as('knows,as') === ext);
assert.ok(Platform.Registry.as('knows,as1234') === null);
assert.ok(Registry.as('knows,as') === ext);
assert.ok(Registry.as('knows,as1234') === null);
});
test('registry - mixin, fails on duplicate ids', function () {
Platform.Registry.add('foo-dup', { bar: true });
Registry.add('foo-dup', { bar: true });
try {
Platform.Registry.add('foo-dup', { bar: false });
Registry.add('foo-dup', { bar: false });
assert.ok(false);
} catch (e) {
assert.ok(true);

View File

@@ -6,15 +6,16 @@
import { Client, PersistentProtocol, ISocket, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net';
import { generateUuid } from 'vs/base/common/uuid';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { ISignService } from 'vs/platform/sign/common/sign';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
const INITIAL_CONNECT_TIMEOUT = 120 * 1000 /* 120s */;
const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */;
@@ -86,59 +87,152 @@ export interface ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
}
async function readOneControlMessage<T>(protocol: PersistentProtocol): Promise<T> {
const raw = await Event.toPromise(protocol.onControlMessage);
const msg = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
throw error;
function createTimeoutCancellation(millis: number): CancellationToken {
const source = new CancellationTokenSource();
setTimeout(() => source.cancel(), millis);
return source.token;
}
function combineTimeoutCancellation(a: CancellationToken, b: CancellationToken): CancellationToken {
if (a.isCancellationRequested || b.isCancellationRequested) {
return CancellationToken.Cancelled;
}
return msg;
const source = new CancellationTokenSource();
a.onCancellationRequested(() => source.cancel());
b.onCancellationRequested(() => source.cancel());
return source.token;
}
function waitWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutToken = setTimeout(() => {
const error: any = new Error('Timeout');
error.code = 'ETIMEDOUT';
error.syscall = 'connect';
reject(error);
}, timeout);
class PromiseWithTimeout<T> {
promise.then(
(result) => {
clearTimeout(timeoutToken);
resolve(result);
},
(error) => {
clearTimeout(timeoutToken);
reject(error);
}
);
});
}
private _state: 'pending' | 'resolved' | 'rejected' | 'timedout';
private readonly _disposables: DisposableStore;
public readonly promise: Promise<T>;
private _resolvePromise!: (value: T) => void;
private _rejectPromise!: (err: any) => void;
function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise<ISocket> {
return new Promise<ISocket>((resolve, reject) => {
socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => {
if (err || !socket) {
return reject(err);
}
resolve(socket);
public get didTimeout(): boolean {
return (this._state === 'timedout');
}
constructor(timeoutCancellationToken: CancellationToken) {
this._state = 'pending';
this._disposables = new DisposableStore();
this.promise = new Promise<T>((resolve, reject) => {
this._resolvePromise = resolve;
this._rejectPromise = reject;
});
});
if (timeoutCancellationToken.isCancellationRequested) {
this._timeout();
} else {
this._disposables.add(timeoutCancellationToken.onCancellationRequested(() => this._timeout()));
}
}
public registerDisposable(disposable: IDisposable): void {
if (this._state === 'pending') {
this._disposables.add(disposable);
} else {
disposable.dispose();
}
}
private _timeout(): void {
if (this._state !== 'pending') {
return;
}
this._disposables.dispose();
this._state = 'timedout';
this._rejectPromise(this._createTimeoutError());
}
private _createTimeoutError(): Error {
const err: any = new Error('Time limit reached');
err.code = 'ETIMEDOUT';
err.syscall = 'connect';
return err;
}
public resolve(value: T): void {
if (this._state !== 'pending') {
return;
}
this._disposables.dispose();
this._state = 'resolved';
this._resolvePromise(value);
}
public reject(err: any): void {
if (this._state !== 'pending') {
return;
}
this._disposables.dispose();
this._state = 'rejected';
this._rejectPromise(err);
}
}
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
function readOneControlMessage<T>(protocol: PersistentProtocol, timeoutCancellationToken: CancellationToken): Promise<T> {
const result = new PromiseWithTimeout<T>(timeoutCancellationToken);
result.registerDisposable(protocol.onControlMessage(raw => {
const msg: T = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
result.reject(error);
} else {
result.resolve(msg);
}
}));
return result.promise;
}
function createSocket(logService: ILogService, socketFactory: ISocketFactory, host: string, port: number, query: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
const result = new PromiseWithTimeout<ISocket>(timeoutCancellationToken);
socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => {
if (result.didTimeout) {
if (err) {
logService.error(err);
}
socket?.dispose();
} else {
if (err || !socket) {
result.reject(err);
} else {
result.resolve(socket);
}
}
});
return result.promise;
}
function raceWithTimeoutCancellation<T>(promise: Promise<T>, timeoutCancellationToken: CancellationToken): Promise<T> {
const result = new PromiseWithTimeout<T>(timeoutCancellationToken);
promise.then(
(res) => {
if (!result.didTimeout) {
result.resolve(res);
}
},
(err) => {
if (!result.didTimeout) {
result.reject(err);
}
}
);
return result.promise;
}
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
const logPrefix = connectLogPrefix(options, connectionType);
options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
let socket: ISocket;
try {
socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`);
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, timeoutCancellationToken);
} catch (error) {
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`);
options.logService.error(error);
throw error;
}
@@ -164,7 +258,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest)));
try {
const msg = await waitWithTimeout(readOneControlMessage<HandshakeMessage>(protocol), 10000);
const msg = await readOneControlMessage<HandshakeMessage>(protocol, combineTimeoutCancellation(timeoutCancellationToken, createTimeoutCancellation(10000)));
if (msg.type !== 'sign' || typeof msg.data !== 'string') {
const error: any = new Error('Unexpected handshake message');
@@ -174,7 +268,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`);
const signed = await options.signService.sign(msg.data);
const signed = await raceWithTimeoutCancellation(options.signService.sign(msg.data), timeoutCancellationToken);
const connTypeRequest: ConnectionTypeRequest = {
type: 'connectionType',
commit: options.commit,
@@ -192,7 +286,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
} catch (error) {
if (error && error.code === 'ETIMEDOUT') {
options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`);
options.logService.error(`${logPrefix} the handshake timed out. Error:`);
options.logService.error(error);
}
if (error && error.code === 'VSCODE_CONNECTION_ERROR') {
@@ -210,34 +304,34 @@ interface IManagementConnectionResult {
protocol: PersistentProtocol;
}
async function connectToRemoteExtensionHostAgentAndReadOneMessage(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; firstMessage: any }> {
async function connectToRemoteExtensionHostAgentAndReadOneMessage<T>(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; firstMessage: T; }> {
const startTime = Date.now();
const logPrefix = connectLogPrefix(options, connectionType);
const { protocol, ownsProtocol } = await connectToRemoteExtensionHostAgent(options, connectionType, args);
return new Promise<{ protocol: PersistentProtocol; firstMessage: any }>((c, e) => {
const registration = protocol.onControlMessage(raw => {
registration.dispose();
const msg = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
return e(error);
const { protocol, ownsProtocol } = await connectToRemoteExtensionHostAgent(options, connectionType, args, timeoutCancellationToken);
const result = new PromiseWithTimeout<{ protocol: PersistentProtocol; firstMessage: T; }>(timeoutCancellationToken);
result.registerDisposable(protocol.onControlMessage(raw => {
const msg: T = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
result.reject(error);
} else {
if (options.reconnectionProtocol) {
options.reconnectionProtocol.endAcceptReconnection();
}
options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
c({ protocol, firstMessage: msg });
});
});
result.resolve({ protocol, firstMessage: msg });
}
}));
return result.promise;
}
async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise<IManagementConnectionResult> {
const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Management, undefined);
async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<IManagementConnectionResult> {
const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Management, undefined, timeoutCancellationToken);
return { protocol };
}
@@ -254,8 +348,8 @@ interface IExtensionHostConnectionResult {
debugPort?: number;
}
async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<IExtensionHostConnectionResult> {
const { protocol, firstMessage } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.ExtensionHost, startArguments);
async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams, timeoutCancellationToken: CancellationToken): Promise<IExtensionHostConnectionResult> {
const { protocol, firstMessage } = await connectToRemoteExtensionHostAgentAndReadOneMessage<{ debugPort?: number; }>(options, ConnectionType.ExtensionHost, startArguments, timeoutCancellationToken);
const debugPort = firstMessage && firstMessage.debugPort;
return { protocol, debugPort };
}
@@ -264,10 +358,10 @@ export interface ITunnelConnectionStartParams {
port: number;
}
async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams): Promise<PersistentProtocol> {
async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams, timeoutCancellationToken: CancellationToken): Promise<PersistentProtocol> {
const startTime = Date.now();
const logPrefix = connectLogPrefix(options, ConnectionType.Tunnel);
const { protocol } = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams);
const { protocol } = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams, timeoutCancellationToken);
options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
return protocol;
}
@@ -310,7 +404,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions,
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), INITIAL_CONNECT_TIMEOUT);
const { protocol } = await doConnectRemoteAgentManagement(simpleOptions, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol);
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
@@ -324,7 +418,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), INITIAL_CONNECT_TIMEOUT);
const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort);
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
@@ -336,7 +430,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise<PersistentProtocol> {
const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null);
const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), INITIAL_CONNECT_TIMEOUT);
const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
return protocol;
}
@@ -497,7 +591,7 @@ abstract class PersistentConnection extends Disposable {
this._options.logService.info(`${logPrefix} resolving connection...`);
const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol);
this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`);
await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT);
await this._reconnect(simpleOptions, createTimeoutCancellation(RECONNECT_TIMEOUT));
this._options.logService.info(`${logPrefix} reconnected!`);
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
@@ -552,7 +646,7 @@ abstract class PersistentConnection extends Disposable {
safeDisposeProtocolAndSocket(this.protocol);
}
protected abstract _reconnect(options: ISimpleConnectionOptions): Promise<void>;
protected abstract _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void>;
}
export class ManagementPersistentConnection extends PersistentConnection {
@@ -567,8 +661,8 @@ export class ManagementPersistentConnection extends PersistentConnection {
}, options.ipcLogger));
}
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
await doConnectRemoteAgentManagement(options);
protected async _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void> {
await doConnectRemoteAgentManagement(options, timeoutCancellationToken);
}
}
@@ -583,31 +677,11 @@ export class ExtensionHostPersistentConnection extends PersistentConnection {
this.debugPort = debugPort;
}
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
await doConnectRemoteAgentExtensionHost(options, this._startArguments);
protected async _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void> {
await doConnectRemoteAgentExtensionHost(options, this._startArguments, timeoutCancellationToken);
}
}
function connectWithTimeLimit<T>(logService: ILogService, p: Promise<T>, timeLimit: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
let timeout = setTimeout(() => {
const err: any = new Error('Time limit reached');
err.code = 'ETIMEDOUT';
err.syscall = 'connect';
logService.error(`[remote-connection] The time limit has been reached for a connection. Error:`);
logService.error(err);
reject(err);
}, timeLimit);
p.then((value) => {
clearTimeout(timeout);
resolve(value);
}, (err) => {
clearTimeout(timeout);
reject(err);
});
});
}
function safeDisposeProtocolAndSocket(protocol: PersistentProtocol): void {
try {
protocol.acceptDisconnect();

View File

@@ -99,10 +99,6 @@ export function isAllInterfaces(host: string): boolean {
return ALL_INTERFACES_ADDRESSES.indexOf(host) >= 0;
}
function getOtherLocalhost(host: string): string | undefined {
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
}
export function isPortPrivileged(port: number, os?: OperatingSystem): boolean {
if (os) {
return os !== OperatingSystem.Windows && (port < 1024);
@@ -282,20 +278,25 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel | undefined> } | undefined {
const otherLocalhost = getOtherLocalhost(remoteHost);
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }> | undefined;
if (otherLocalhost) {
const firstMap = this._tunnels.get(remoteHost);
const secondMap = this._tunnels.get(otherLocalhost);
if (firstMap && secondMap) {
portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]);
} else {
portMap = firstMap ?? secondMap;
}
} else {
portMap = this._tunnels.get(remoteHost);
let hosts = [remoteHost];
// Order matters. We want the original host to be first.
if (isLocalhost(remoteHost)) {
hosts.push(...LOCALHOST_ADDRESSES);
// For localhost, we add the all interfaces hosts because if the tunnel is already available at all interfaces,
// then of course it is available at localhost.
hosts.push(...ALL_INTERFACES_ADDRESSES);
} else if (isAllInterfaces(remoteHost)) {
hosts.push(...ALL_INTERFACES_ADDRESSES);
}
return portMap ? portMap.get(remotePort) : undefined;
const existingPortMaps = hosts.map(host => this._tunnels.get(host));
for (const map of existingPortMaps) {
const existingTunnel = map?.get(remotePort);
if (existingTunnel) {
return existingTunnel;
}
}
return undefined;
}
canTunnel(uri: URI): boolean {

View File

@@ -0,0 +1,254 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BrowserWindow, ipcMain, Event as ElectronEvent, MessagePortMain, IpcMainEvent, RenderProcessGoneDetails } from 'electron';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { Barrier } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
import { FileAccess } from 'vs/base/common/network';
import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform';
import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess';
import { Disposable } from 'vs/base/common/lifecycle';
import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp';
import { assertIsDefined } from 'vs/base/common/types';
import { Emitter, Event } from 'vs/base/common/event';
import { WindowError } from 'vs/platform/windows/electron-main/windows';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
export class SharedProcess extends Disposable implements ISharedProcess {
private readonly firstWindowConnectionBarrier = new Barrier();
private window: BrowserWindow | undefined = undefined;
private windowCloseListener: ((event: ElectronEvent) => void) | undefined = undefined;
private readonly _onDidError = this._register(new Emitter<{ type: WindowError, details: string | RenderProcessGoneDetails }>());
readonly onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener!
constructor(
private readonly machineId: string,
private userEnv: NodeJS.ProcessEnv,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@ILogService private readonly logService: ILogService,
@IThemeMainService private readonly themeMainService: IThemeMainService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
// Lifecycle
this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown()));
// Shared process connections from workbench windows
ipcMain.on('vscode:createSharedProcessMessageChannel', async (e, nonce: string) => this.onWindowConnection(e, nonce));
}
private async onWindowConnection(e: IpcMainEvent, nonce: string): Promise<void> {
this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel');
// release barrier if this is the first window connection
if (!this.firstWindowConnectionBarrier.isOpen()) {
this.firstWindowConnectionBarrier.open();
}
// await the shared process to be overall ready
// we do not just wait for IPC ready because the
// workbench window will communicate directly
await this.whenReady();
// connect to the shared process window
const port = await this.connect();
// Check back if the requesting window meanwhile closed
// Since shared process is delayed on startup there is
// a chance that the window close before the shared process
// was ready for a connection.
if (e.sender.isDestroyed()) {
return port.close();
}
// send the port back to the requesting window
e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]);
}
private onWillShutdown(): void {
const window = this.window;
if (!window) {
return; // possibly too early before created
}
// Signal exit to shared process when shutting down
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
window.webContents.send('vscode:electron-main->shared-process=exit');
}
// Shut the shared process down when we are quitting
//
// Note: because we veto the window close, we must first remove our veto.
// Otherwise the application would never quit because the shared process
// window is refusing to close!
//
if (this.windowCloseListener) {
window.removeListener('close', this.windowCloseListener);
this.windowCloseListener = undefined;
}
// Electron seems to crash on Windows without this setTimeout :|
setTimeout(() => {
try {
window.close();
} catch (err) {
// ignore, as electron is already shutting down
}
this.window = undefined;
}, 0);
}
private _whenReady: Promise<void> | undefined = undefined;
whenReady(): Promise<void> {
if (!this._whenReady) {
// Overall signal that the shared process window was loaded and
// all services within have been created.
this._whenReady = new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => {
this.logService.trace('SharedProcess: Overall ready');
resolve();
}));
}
return this._whenReady;
}
private _whenIpcReady: Promise<void> | undefined = undefined;
private get whenIpcReady() {
if (!this._whenIpcReady) {
this._whenIpcReady = (async () => {
// Always wait for first window asking for connection
await this.firstWindowConnectionBarrier.wait();
// Resolve shell environment
this.userEnv = { ...this.userEnv, ...(await resolveShellEnv(this.logService, this.environmentMainService.args, process.env)) };
// Create window for shared process
this.createWindow();
// Listeners
this.registerWindowListeners();
// Wait for window indicating that IPC connections are accepted
await new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => {
this.logService.trace('SharedProcess: IPC ready');
resolve();
}));
})();
}
return this._whenIpcReady;
}
private createWindow(): void {
// shared process is a hidden window by default
this.window = new BrowserWindow({
show: false,
backgroundColor: this.themeMainService.getBackgroundColor(),
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
v8CacheOptions: browserCodeLoadingCacheStrategy,
nodeIntegration: true,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
images: false,
webgl: false,
disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer
}
});
const config: ISharedProcessConfiguration = {
machineId: this.machineId,
windowId: this.window.id,
appRoot: this.environmentMainService.appRoot,
nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir,
backupWorkspacesPath: this.environmentMainService.backupWorkspacesPath,
userEnv: this.userEnv,
args: this.environmentMainService.args,
logLevel: this.logService.getLevel()
};
// Load with config
this.window.loadURL(FileAccess
.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true)
);
}
private registerWindowListeners(): void {
if (!this.window) {
return;
}
// Prevent the window from closing
this.windowCloseListener = (e: ElectronEvent) => {
this.logService.trace('SharedProcess#close prevented');
// We never allow to close the shared process unless we get explicitly disposed()
e.preventDefault();
// Still hide the window though if visible
if (this.window?.isVisible()) {
this.window.hide();
}
};
this.window.on('close', this.windowCloseListener);
// Crashes & Unrsponsive & Failed to load
// We use `onUnexpectedError` explicitly because the error handler
// will send the error to the active window to log in devtools too
this.window.webContents.on('render-process-gone', (event, details) => this._onDidError.fire({ type: WindowError.CRASHED, details }));
this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE, details: 'SharedProcess: detected unresponsive window' }));
this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this._onDidError.fire({ type: WindowError.LOAD, details: `SharedProcess: failed to load: ${errorDescription}` }));
}
async connect(): Promise<MessagePortMain> {
// Wait for shared process being ready to accept connection
await this.whenIpcReady;
// Connect and return message port
const window = assertIsDefined(this.window);
return connectMessagePort(window);
}
async toggle(): Promise<void> {
// wait for window to be created
await this.whenIpcReady;
if (!this.window) {
return; // possibly disposed already
}
if (this.window.isVisible()) {
this.window.webContents.closeDevTools();
this.window.hide();
} else {
this.window.show();
this.window.webContents.openDevTools();
}
}
}

View File

@@ -23,8 +23,6 @@ export interface ISharedProcessConfiguration {
readonly userEnv: NodeJS.ProcessEnv;
readonly sharedIPCHandle: string;
readonly args: NativeParsedArgs;
readonly logLevel: LogLevel;

View File

@@ -6,7 +6,7 @@
import * as path from 'vs/base/common/path';
import * as fs from 'fs';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { writeFileSync, readFile } from 'vs/base/node/pfs';
import { writeFileSync } from 'vs/base/node/pfs';
import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types';
import { IStateService } from 'vs/platform/state/node/state';
import { ILogService } from 'vs/platform/log/common/log';
@@ -58,7 +58,7 @@ export class FileStorage {
private async loadAsync(): Promise<StorageDatabase> {
try {
this.lastFlushedSerializedDatabase = (await readFile(this.dbPath)).toString();
this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString();
return JSON.parse(this.lastFlushedSerializedDatabase);
} catch (error) {

View File

@@ -5,10 +5,11 @@
import * as assert from 'assert';
import { tmpdir } from 'os';
import { promises } from 'fs';
import { join } from 'vs/base/common/path';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileStorage } from 'vs/platform/state/node/stateService';
import { mkdirp, rimraf, writeFileSync } from 'vs/base/node/pfs';
import { rimraf, writeFileSync } from 'vs/base/node/pfs';
flakySuite('StateService', () => {
@@ -17,7 +18,7 @@ flakySuite('StateService', () => {
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice');
return mkdirp(testDir);
return promises.mkdir(testDir, { recursive: true });
});
teardown(() => {

View File

@@ -3,21 +3,22 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { StorageScope, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
import { Promises } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
export class BrowserStorageService extends AbstractStorageService {
private static BROWSER_DEFAULT_FLUSH_INTERVAL = 5 * 1000; // every 5s because async operations are not permitted on shutdown
private globalStorage: IStorage | undefined;
private workspaceStorage: IStorage | undefined;
@@ -27,38 +28,26 @@ export class BrowserStorageService extends AbstractStorageService {
private globalStorageFile: URI | undefined;
private workspaceStorageFile: URI | undefined;
private initializePromise: Promise<void> | undefined;
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */));
private runWhenIdleDisposable: IDisposable | undefined = undefined;
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
}
constructor(
private readonly payload: IWorkspaceInitializationPayload,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super();
super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL });
}
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize(payload);
}
return this.initializePromise;
}
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
protected async doInitialize(): Promise<void> {
// Ensure state folder exists
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
await this.fileService.createFolder(stateRoot);
// Workspace Storage
this.workspaceStorageFile = joinPath(stateRoot, `${payload.id}.json`);
this.workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
@@ -71,7 +60,7 @@ export class BrowserStorageService extends AbstractStorageService {
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
// Init both
await Promise.all([
await Promises.settled([
this.workspaceStorage.init(),
this.globalStorage.init()
]);
@@ -91,91 +80,36 @@ export class BrowserStorageService extends AbstractStorageService {
} else if (firstWorkspaceOpen) {
this.workspaceStorage.set(IS_NEW_KEY, false);
}
// In the browser we do not have support for long running unload sequences. As such,
// we cannot ask for saving state in that moment, because that would result in a
// long running operation.
// Instead, periodically ask customers to save save. The library will be clever enough
// to only save state that has actually changed.
this.periodicFlushScheduler.schedule();
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope).get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope).getBoolean(key, fallbackValue);
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope): number | undefined;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope).getNumber(key, fallbackValue);
}
<<<<<<< HEAD
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
return this.getStorage(scope).set(key, value);
=======
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
}
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
private getStorage(scope: StorageScope): IStorage {
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
}
async logStorage(): Promise<void> {
const [globalStorage, workspaceStorage, globalStorageFile, workspaceStorageFile] = assertAllDefined(this.globalStorage, this.workspaceStorage, this.globalStorageFile, this.workspaceStorageFile);
const result = await Promise.all([
globalStorage.items,
workspaceStorage.items
]);
return logStorage(result[0], result[1], globalStorageFile.toString(), workspaceStorageFile.toString());
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString();
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
throw new Error('Migrating storage is currently unsupported in Web');
}
protected async doFlush(): Promise<void> {
await Promise.all([
this.getStorage(StorageScope.GLOBAL).whenFlushed(),
this.getStorage(StorageScope.WORKSPACE).whenFlushed()
]);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
dispose(this.runWhenIdleDisposable);
// Run when idle
this.runWhenIdleDisposable = runWhenIdle(() => {
// this event will potentially cause new state to be stored
// since new state will only be created while the document
// has focus, one optimization is to not run this when the
// document has no focus, assuming that state has not changed
//
// another optimization is to not collect more state if we
// have a pending update already running which indicates
// that the connection is either slow or disconnected and
// thus unhealthy.
if (document.hasFocus() && !this.hasPendingUpdate) {
this.flush();
}
// repeat
this.periodicFlushScheduler.schedule();
});
protected shouldFlushWhenIdle(): boolean {
// this flush() will potentially cause new state to be stored
// since new state will only be created while the document
// has focus, one optimization is to not run this when the
// document has no focus, assuming that state has not changed
//
// another optimization is to not collect more state if we
// have a pending update already running which indicates
// that the connection is either slow or disconnected and
// thus unhealthy.
return document.hasFocus() && !this.hasPendingUpdate;
}
close(): void {
@@ -189,13 +123,6 @@ export class BrowserStorageService extends AbstractStorageService {
// get triggered in this phase.
this.dispose();
}
dispose(): void {
dispose(this.runWhenIdleDisposable);
this.runWhenIdleDisposable = undefined;
super.dispose();
}
}
export class FileStorageDatabase extends Disposable implements IStorageDatabase {

View File

@@ -5,9 +5,11 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, dispose, MutableDisposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { InMemoryStorageDatabase, IStorage, Storage } from 'vs/base/parts/storage/common/storage';
import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
export const IS_NEW_KEY = '__$__isNewStorageMarker';
const TARGET_KEY = '__$__targetStorageMarker';
@@ -218,10 +220,16 @@ interface IKeyTargets {
[key: string]: StorageTarget
}
export interface IStorageServiceOptions {
flushInterval: number;
}
export abstract class AbstractStorageService extends Disposable implements IStorageService {
declare readonly _serviceBrand: undefined;
private static DEFAULT_FLUSH_INTERVAL = 60 * 1000; // every minute
private readonly _onDidChangeValue = this._register(new PauseableEmitter<IStorageValueChangeEvent>());
readonly onDidChangeValue = this._onDidChangeValue.event;
@@ -231,6 +239,56 @@ export abstract class AbstractStorageService extends Disposable implements IStor
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
readonly onWillSaveState = this._onWillSaveState.event;
private initializationPromise: Promise<void> | undefined;
private readonly flushWhenIdleScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), this.options.flushInterval));
private readonly runFlushWhenIdle = this._register(new MutableDisposable());
constructor(private options: IStorageServiceOptions = { flushInterval: AbstractStorageService.DEFAULT_FLUSH_INTERVAL }) {
super();
}
private doFlushWhenIdle(): void {
this.runFlushWhenIdle.value = runWhenIdle(() => {
if (this.shouldFlushWhenIdle()) {
this.flush();
}
// repeat
this.flushWhenIdleScheduler.schedule();
});
}
protected shouldFlushWhenIdle(): boolean {
return true;
}
protected stopFlushWhenIdle(): void {
dispose([this.runFlushWhenIdle, this.flushWhenIdleScheduler]);
}
initialize(): Promise<void> {
if (!this.initializationPromise) {
this.initializationPromise = (async () => {
// Ask subclasses to initialize storage
await this.doInitialize();
// On some OS we do not get enough time to persist state on shutdown (e.g. when
// Windows restarts after applying updates). In other cases, VSCode might crash,
// so we periodically save state to reduce the chance of loosing any state.
// In the browser we do not have support for long running unload sequences. As such,
// we cannot ask for saving state in that moment, because that would result in a
// long running operation.
// Instead, periodically ask customers to save save. The library will be clever enough
// to only save state that has actually changed.
this.flushWhenIdleScheduler.schedule();
})();
}
return this.initializationPromise;
}
protected emitDidChangeValue(scope: StorageScope, key: string): void {
// Specially handle `TARGET_KEY`
@@ -257,6 +315,24 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this._onWillSaveState.fire({ reason });
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
return this.getStorage(scope)?.get(key, fallbackValue);
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope): boolean | undefined;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
return this.getStorage(scope)?.getBoolean(key, fallbackValue);
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope): number | undefined;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
return this.getStorage(scope)?.getNumber(key, fallbackValue);
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void {
// We remove the key for undefined/null values
@@ -272,7 +348,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this.updateKeyTarget(key, scope, target);
// Store actual value
this.doStore(key, value, scope);
this.getStorage(scope)?.set(key, value);
});
}
@@ -285,7 +361,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
this.updateKeyTarget(key, scope, undefined);
// Remove actual key
this.doRemove(key, scope);
this.getStorage(scope)?.delete(key);
});
}
@@ -326,7 +402,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
if (typeof target === 'number') {
if (keyTargets[key] !== target) {
keyTargets[key] = target;
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
}
}
@@ -334,7 +410,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
else {
if (typeof keyTargets[key] === 'number') {
delete keyTargets[key];
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
this.getStorage(scope)?.set(TARGET_KEY, JSON.stringify(keyTargets));
}
}
}
@@ -378,118 +454,70 @@ export abstract class AbstractStorageService extends Disposable implements IStor
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
flush(): Promise<void> {
async flush(): Promise<void> {
// Signal event to collect changes
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
// Await flush
return this.doFlush();
await Promises.settled([
this.getStorage(StorageScope.GLOBAL)?.whenFlushed() ?? Promise.resolve(),
this.getStorage(StorageScope.WORKSPACE)?.whenFlushed() ?? Promise.resolve()
]);
}
async logStorage(): Promise<void> {
const globalItems = this.getStorage(StorageScope.GLOBAL)?.items ?? new Map<string, string>();
const workspaceItems = this.getStorage(StorageScope.WORKSPACE)?.items ?? new Map<string, string>();
return logStorage(
globalItems,
workspaceItems,
this.getLogDetails(StorageScope.GLOBAL) ?? '',
this.getLogDetails(StorageScope.WORKSPACE) ?? ''
);
}
// --- abstract
abstract get(key: string, scope: StorageScope, fallbackValue: string): string;
abstract get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
abstract getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
abstract getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
<<<<<<< HEAD
protected abstract doStore(key: string, value: string | boolean | number, scope: StorageScope): Promise<void> | void;
=======
protected abstract doInitialize(): Promise<void>;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
protected abstract doRemove(key: string, scope: StorageScope): void;
protected abstract getStorage(scope: StorageScope): IStorage | undefined;
protected abstract doFlush(): Promise<void>;
protected abstract getLogDetails(scope: StorageScope): string | undefined;
abstract migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
abstract logStorage(): void;
}
export class InMemoryStorageService extends AbstractStorageService {
private readonly globalCache = new Map<string, string>();
private readonly workspaceCache = new Map<string, string>();
private globalStorage = new Storage(new InMemoryStorageDatabase());
private workspaceStorage = new Storage(new InMemoryStorageDatabase());
private getCache(scope: StorageScope): Map<string, string> {
return scope === StorageScope.GLOBAL ? this.globalCache : this.workspaceCache;
constructor() {
super();
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
protected getStorage(scope: StorageScope): IStorage {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? 'inMemory (global)' : 'inMemory (workspace)';
}
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
const value = this.getCache(scope).get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
protected doStore(key: string, value: string | boolean | number, scope: StorageScope): void {
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.getCache(scope).get(key);
if (currentValue === valueStr) {
return;
}
// Update in cache
this.getCache(scope).set(key, valueStr);
// Events
this.emitDidChangeValue(scope, key);
}
protected doRemove(key: string, scope: StorageScope): void {
const wasDeleted = this.getCache(scope).delete(key);
if (!wasDeleted) {
return; // Return early if value already deleted
}
// Events
this.emitDidChangeValue(scope, key);
}
logStorage(): void {
logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory');
}
protected async doInitialize(): Promise<void> { }
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
// not supported
}
async doFlush(): Promise<void> { }
async close(): Promise<void> { }
}
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { IEmptyWorkspaceIdentifier, ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export type Key = string;
export type Value = string;
export type Item = [Key, Value];
export interface IBaseSerializableStorageRequest {
readonly workspace: ISerializedWorkspaceIdentifier | ISerializedSingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined
}
export interface ISerializableUpdateRequest extends IBaseSerializableStorageRequest {
insert?: Item[];
delete?: Key[];
}
export interface ISerializableItemsChangeEvent {
readonly changed?: Item[];
readonly deleted?: Key[];
}
abstract class BaseStorageDatabaseClient extends Disposable implements IStorageDatabase {
abstract readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
constructor(protected channel: IChannel, protected workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined) {
super();
}
async getItems(): Promise<Map<string, string>> {
const serializableRequest: IBaseSerializableStorageRequest = { workspace: this.workspace };
const items: Item[] = await this.channel.call('getItems', serializableRequest);
return new Map(items);
}
updateItems(request: IUpdateRequest): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace };
if (request.insert) {
serializableRequest.insert = Array.from(request.insert.entries());
}
if (request.delete) {
serializableRequest.delete = Array.from(request.delete.values());
}
return this.channel.call('updateItems', serializableRequest);
}
abstract close(): Promise<void>;
}
class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase {
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
constructor(channel: IChannel) {
super(channel, undefined);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.channel.listen<ISerializableItemsChangeEvent>('onDidChangeGlobalStorage')((e: ISerializableItemsChangeEvent) => this.onDidChangeGlobalStorage(e)));
}
private onDidChangeGlobalStorage(e: ISerializableItemsChangeEvent): void {
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
this._onDidChangeItemsExternal.fire({
changed: e.changed ? new Map(e.changed) : undefined,
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
});
}
}
async close(): Promise<void> {
// The global storage database is shared across all instances so
// we do not await it. However we dispose the listener for external
// changes because we no longer interested int it.
this.dispose();
}
}
class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implements IStorageDatabase {
readonly onDidChangeItemsExternal = Event.None; // unsupported for workspace storage because we only ever write from one window
constructor(channel: IChannel, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier) {
super(channel, workspace);
}
async close(): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace };
return this.channel.call('close', serializableRequest);
}
}
export class StorageDatabaseChannelClient extends Disposable {
private _globalStorage: GlobalStorageDatabaseClient | undefined = undefined;
get globalStorage() {
if (!this._globalStorage) {
this._globalStorage = new GlobalStorageDatabaseClient(this.channel);
}
return this._globalStorage;
}
private _workspaceStorage: WorkspaceStorageDatabaseClient | undefined = undefined;
get workspaceStorage() {
if (!this._workspaceStorage && this.workspace) {
this._workspaceStorage = new WorkspaceStorageDatabaseClient(this.channel, this.workspace);
}
return this._workspaceStorage;
}
constructor(
private channel: IChannel,
private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined
) {
super();
}
}

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from 'vs/platform/log/common/log';
import { ISerializableItemsChangeEvent, ISerializableUpdateRequest, IBaseSerializableStorageRequest, Key, Value } from 'vs/platform/storage/common/storageIpc';
import { IStorageChangeEvent, IStorageMain } from 'vs/platform/storage/electron-main/storageMain';
import { IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces';
export class StorageDatabaseChannel extends Disposable implements IServerChannel {
private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100;
private readonly _onDidChangeGlobalStorage = this._register(new Emitter<ISerializableItemsChangeEvent>());
private readonly onDidChangeGlobalStorage = this._onDidChangeGlobalStorage.event;
constructor(
private logService: ILogService,
private storageMainService: IStorageMainService
) {
super();
this.registerGlobalStorageListeners();
}
//#region Global Storage Change Events
private registerGlobalStorageListeners(): void {
// Listen for changes in global storage to send to listeners
// that are listening. Use a debouncer to reduce IPC traffic.
this._register(Event.debounce(this.storageMainService.globalStorage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => {
if (!prev) {
prev = [cur];
} else {
prev.push(cur);
}
return prev;
}, StorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => {
if (events.length) {
this._onDidChangeGlobalStorage.fire(this.serializeGlobalStorageEvents(events));
}
}));
}
private serializeGlobalStorageEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent {
const changed = new Map<Key, Value>();
const deleted = new Set<Key>();
events.forEach(event => {
const existing = this.storageMainService.globalStorage.get(event.key);
if (typeof existing === 'string') {
changed.set(event.key, existing);
} else {
deleted.add(event.key);
}
});
return {
changed: Array.from(changed.entries()),
deleted: Array.from(deleted.values())
};
}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeGlobalStorage': return this.onDidChangeGlobalStorage;
}
throw new Error(`Event not found: ${event}`);
}
//#endregion
async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise<any> {
const workspace = reviveIdentifier(arg.workspace);
// Get storage to be ready
const storage = await this.withStorageInitialized(workspace);
// handle call
switch (command) {
case 'getItems': {
return Array.from(storage.items.entries());
}
case 'updateItems': {
const items: ISerializableUpdateRequest = arg;
if (items.insert) {
for (const [key, value] of items.insert) {
storage.set(key, value);
}
}
if (items.delete) {
items.delete.forEach(key => storage.delete(key));
}
break;
}
case 'close': {
// We only allow to close workspace scoped storage because
// global storage is shared across all windows and closes
// only on shutdown.
if (workspace) {
return storage.close();
}
break;
}
default:
throw new Error(`Call not found: ${command}`);
}
}
private async withStorageInitialized(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): Promise<IStorageMain> {
const storage = workspace ? this.storageMainService.workspaceStorage(workspace) : this.storageMainService.globalStorage;
try {
await storage.init();
} catch (error) {
this.logService.error(`StorageIPC#init: Unable to init ${workspace ? 'workspace' : 'global'} storage due to ${error}`);
}
return storage;
}
}

View File

@@ -0,0 +1,307 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { exists, writeFile } from 'vs/base/node/pfs';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, InMemoryStorageDatabase, StorageHint, IStorage } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
import { generateUuid } from 'vs/base/common/uuid';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export interface IStorageMainOptions {
/**
* If enabled, storage will not persist to disk
* but into memory.
*/
useInMemoryStorage?: boolean;
}
/**
* Provides access to global and workspace storage from the
* electron-main side that is the owner of all storage connections.
*/
export interface IStorageMain extends IDisposable {
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
/**
* Emitted when the storage is closed.
*/
readonly onDidCloseStorage: Event<void>;
/**
* Access to all cached items of this storage service.
*/
readonly items: Map<string, string>;
/**
* Required call to ensure the service can be used.
*/
init(): Promise<void>;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*/
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*/
set(key: string, value: string | boolean | number | undefined | null): void;
/**
* Delete an element stored under the provided key from storage.
*/
delete(key: string): void;
/**
* Close the storage connection.
*/
close(): Promise<void>;
}
export interface IStorageChangeEvent {
key: string;
}
abstract class BaseStorageMain extends Disposable implements IStorageMain {
protected readonly _onDidChangeStorage = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onDidCloseStorage = this._register(new Emitter<void>());
readonly onDidCloseStorage = this._onDidCloseStorage.event;
private storage: IStorage = new Storage(new InMemoryStorageDatabase()); // storage is in-memory until initialized
private initializePromise: Promise<void> | undefined = undefined;
constructor(
protected readonly logService: ILogService
) {
super();
}
init(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = (async () => {
try {
// Create storage via subclasses
const storage = await this.doCreate();
// Replace our in-memory storage with the real
// once as soon as possible without awaiting
// the init call.
this.storage.dispose();
this.storage = storage;
// Re-emit storage changes via event
this._register(storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key })));
// Await storage init
await this.doInit(storage);
// Ensure we track wether storage is new or not
const isNewStorage = storage.getBoolean(IS_NEW_KEY);
if (isNewStorage === undefined) {
storage.set(IS_NEW_KEY, true);
} else if (isNewStorage) {
storage.set(IS_NEW_KEY, false);
}
} catch (error) {
this.logService.error(`StorageMain#initialize(): Unable to init storage due to ${error}`);
}
})();
}
return this.initializePromise;
}
protected createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions {
return {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
logError: error => this.logService.error(error)
};
}
protected doInit(storage: IStorage): Promise<void> {
return storage.init();
}
protected abstract doCreate(): Promise<IStorage>;
get items(): Map<string, string> { return this.storage.items; }
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
return this.storage.get(key, fallbackValue);
}
set(key: string, value: string | boolean | number | undefined | null): Promise<void> {
return this.storage.set(key, value);
}
delete(key: string): Promise<void> {
return this.storage.delete(key);
}
async close(): Promise<void> {
// Ensure we are not accidentally leaving
// a pending initialized storage behind in
// case close() was called before init()
// finishes
if (this.initializePromise) {
await this.initializePromise;
}
// Propagate to storage lib
await this.storage.close();
// Signal as event
this._onDidCloseStorage.fire();
}
}
export class GlobalStorageMain extends BaseStorageMain implements IStorageMain {
private static readonly STORAGE_NAME = 'state.vscdb';
constructor(
private readonly options: IStorageMainOptions,
logService: ILogService,
private readonly environmentService: IEnvironmentService
) {
super(logService);
}
protected async doCreate(): Promise<IStorage> {
let storagePath: string;
if (this.options.useInMemoryStorage) {
storagePath = SQLiteStorageDatabase.IN_MEMORY_PATH;
} else {
storagePath = join(this.environmentService.globalStorageHome.fsPath, GlobalStorageMain.STORAGE_NAME);
}
return new Storage(new SQLiteStorageDatabase(storagePath, {
logging: this.createLogginOptions()
}));
}
protected async doInit(storage: IStorage): Promise<void> {
await super.doInit(storage);
// Apply global telemetry values as part of the initialization
this.updateTelemetryState(storage);
}
private updateTelemetryState(storage: IStorage): void {
// Instance UUID (once)
const instanceId = storage.get(instanceStorageKey, undefined);
if (instanceId === undefined) {
storage.set(instanceStorageKey, generateUuid());
}
// First session date (once)
const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined);
if (firstSessionDate === undefined) {
storage.set(firstSessionDateStorageKey, new Date().toUTCString());
}
// Last / current session (always)
// previous session date was the "current" one at that time
// current session date is "now"
const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined);
const currentSessionDate = new Date().toUTCString();
storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate);
storage.set(currentSessionDateStorageKey, currentSessionDate);
}
}
export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMain {
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static readonly WORKSPACE_META_NAME = 'workspace.json';
constructor(
private workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier,
private readonly options: IStorageMainOptions,
logService: ILogService,
private readonly environmentService: IEnvironmentService
) {
super(logService);
}
protected async doCreate(): Promise<IStorage> {
const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder();
return new Storage(new SQLiteStorageDatabase(storageFilePath, {
logging: this.createLogginOptions()
}), { hint: wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined });
}
private async prepareWorkspaceStorageFolder(): Promise<{ storageFilePath: string, wasCreated: boolean }> {
// Return early if using inMemory storage
if (this.options.useInMemoryStorage) {
return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true };
}
// Otherwise, ensure the storage folder exists on disk
const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.fsPath, this.workspace.id);
const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME);
const storageExists = await exists(workspaceStorageFolderPath);
if (storageExists) {
return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false };
}
// Ensure storage folder exists
await promises.mkdir(workspaceStorageFolderPath, { recursive: true });
// Write metadata into folder (but do not await)
this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath);
return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true };
}
private async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath: string): Promise<void> {
let meta: object | undefined = undefined;
if (isSingleFolderWorkspaceIdentifier(this.workspace)) {
meta = { folder: this.workspace.uri.toString() };
} else if (isWorkspaceIdentifier(this.workspace)) {
meta = { workspace: this.workspace.configPath.toString() };
}
if (meta) {
try {
const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME);
const storageExists = await exists(workspaceStorageMetaPath);
if (!storageExists) {
await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2));
}
} catch (error) {
this.logService.error(`StorageMain#ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`);
}
}
}
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { ILogService } from 'vs/platform/log/common/log';
import { GlobalStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain';
import { IWindowSettings } from 'vs/platform/windows/common/windows';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
export interface IStorageMainService {
readonly _serviceBrand: undefined;
/**
* Provides access to the global storage shared across all windows.
*/
readonly globalStorage: IStorageMain;
/**
* Provides access to the workspace storage specific to a single window.
*/
workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain;
}
export class StorageMainService extends Disposable implements IStorageMainService {
declare readonly _serviceBrand: undefined;
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.registerListeners();
}
protected getStorageOptions(): IStorageMainOptions {
return {
useInMemoryStorage: !!this.environmentService.extensionTestsLocationURI // no storage during extension tests!
};
}
protected enableMainWorkspaceStorage(): boolean {
return !!(this.configurationService.getValue<IWindowSettings | undefined>('window')?.enableExperimentalMainProcessWorkspaceStorage);
}
private registerListeners(): void {
// Global Storage: Warmup when any window opens
(async () => {
await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen);
this.globalStorage.init();
})();
// Workspace Storage: Warmup when related window with workspace loads
if (this.enableMainWorkspaceStorage()) {
this._register(this.lifecycleMainService.onWillLoadWindow(async e => {
if (e.workspace) {
this.workspaceStorage(e.workspace).init();
}
}));
}
// All Storage: Close when shutting down
this._register(this.lifecycleMainService.onWillShutdown(e => {
// Global Storage
e.join(this.globalStorage.close());
// Workspace Storage(s)
for (const [, storage] of this.mapWorkspaceToStorage) {
e.join(storage.close());
}
}));
}
//#region Global Storage
readonly globalStorage = this.createGlobalStorage();
private createGlobalStorage(): IStorageMain {
if (this.globalStorage) {
return this.globalStorage; // only once
}
this.logService.trace(`StorageMainService: creating global storage`);
const globalStorage = new GlobalStorageMain(this.getStorageOptions(), this.logService, this.environmentService);
once(globalStorage.onDidCloseStorage)(() => {
this.logService.trace(`StorageMainService: closed global storage`);
});
return globalStorage;
}
//#endregion
//#region Workspace Storage
private readonly mapWorkspaceToStorage = new Map<string, IStorageMain>();
private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain {
const workspaceStorage = new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService);
return workspaceStorage;
}
workspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain {
let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id);
if (!workspaceStorage) {
this.logService.trace(`StorageMainService: creating workspace storage (${workspace.id})`);
workspaceStorage = this.createWorkspaceStorage(workspace);
this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage);
once(workspaceStorage.onDidCloseStorage)(() => {
this.logService.trace(`StorageMainService: closed workspace storage (${workspace.id})`);
this.mapWorkspaceToStorage.delete(workspace.id);
});
}
return workspaceStorage;
}
//#endregion
}

View File

@@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MutableDisposable } from 'vs/base/common/lifecycle';
import { StorageScope, WillSaveStateReason, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { Storage, IStorage } from 'vs/base/parts/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { Promises } from 'vs/base/common/async';
import { mark } from 'vs/base/common/performance';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { StorageDatabaseChannelClient } from 'vs/platform/storage/common/storageIpc';
import { joinPath } from 'vs/base/common/resources';
export class NativeStorageService2 extends AbstractStorageService {
// Global Storage is readonly and shared across windows
private readonly globalStorage: IStorage;
// Workspace Storage is scoped to a window but can change
// in the current window, when entering a workspace!
private workspaceStorage: IStorage | undefined = undefined;
private workspaceStorageId: string | undefined = undefined;
private workspaceStorageDisposable = this._register(new MutableDisposable());
constructor(
workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined,
private readonly mainProcessService: IMainProcessService,
private readonly environmentService: IEnvironmentService
) {
super();
this.globalStorage = this.createGlobalStorage();
this.workspaceStorage = this.createWorkspaceStorage(workspace);
}
private createGlobalStorage(): IStorage {
const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), undefined);
const globalStorage = new Storage(storageDataBaseClient.globalStorage);
this._register(globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
return globalStorage;
}
private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorage;
private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IStorage | undefined;
private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined): IStorage | undefined {
const storageDataBaseClient = new StorageDatabaseChannelClient(this.mainProcessService.getChannel('storage'), workspace);
if (storageDataBaseClient.workspaceStorage) {
const workspaceStorage = new Storage(storageDataBaseClient.workspaceStorage);
this.workspaceStorageDisposable.value = workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key));
this.workspaceStorageId = workspace?.id;
return workspaceStorage;
} else {
this.workspaceStorageDisposable.clear();
this.workspaceStorageId = undefined;
return undefined;
}
}
protected async doInitialize(): Promise<void> {
// Init all storage locations
mark('code/willInitStorage');
try {
await Promises.settled([
this.globalStorage.init(),
this.workspaceStorage?.init() ?? Promise.resolve()
]);
} finally {
mark('code/didInitStorage');
}
}
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStorageId ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspaceStorageId, 'state.vscdb').fsPath} [!!! Experimental Main Storage !!!]` : undefined;
}
async close(): Promise<void> {
// Stop periodic scheduler and idle runner as we now collect state normally
this.stopFlushWhenIdle();
// Signal as event so that clients can still store data
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
// Do it
await Promises.settled([
this.globalStorage.close(),
this.workspaceStorage?.close() ?? Promise.resolve()
]);
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
// Keep current workspace storage items around to restore
const oldWorkspaceStorage = this.workspaceStorage;
const oldItems = oldWorkspaceStorage?.items ?? new Map();
// Close current which will change to new workspace storage
if (oldWorkspaceStorage) {
await oldWorkspaceStorage.close();
oldWorkspaceStorage.dispose();
}
// Create new workspace storage & init
this.workspaceStorage = this.createWorkspaceStorage(toWorkspace);
await this.workspaceStorage.init();
// Copy over previous keys
for (const [key, value] of oldItems) {
this.workspaceStorage.set(key, value);
}
}
}

View File

@@ -1,214 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService';
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
type Key = string;
type Value = string;
type Item = [Key, Value];
interface ISerializableUpdateRequest {
insert?: Item[];
delete?: Key[];
}
interface ISerializableItemsChangeEvent {
readonly changed?: Item[];
readonly deleted?: Key[];
}
export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel {
private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100;
private readonly _onDidChangeItems = this._register(new Emitter<ISerializableItemsChangeEvent>());
readonly onDidChangeItems = this._onDidChangeItems.event;
private readonly whenReady = this.init();
constructor(
private logService: ILogService,
private storageMainService: IStorageMainService
) {
super();
}
private async init(): Promise<void> {
try {
await this.storageMainService.initialize();
} catch (error) {
this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`);
}
// Apply global telemetry values as part of the initialization
// These are global across all windows and thereby should be
// written from the main process once.
this.initTelemetry();
// Setup storage change listeners
this.registerListeners();
}
private initTelemetry(): void {
const instanceId = this.storageMainService.get(instanceStorageKey, undefined);
if (instanceId === undefined) {
this.storageMainService.store(instanceStorageKey, generateUuid());
}
const firstSessionDate = this.storageMainService.get(firstSessionDateStorageKey, undefined);
if (firstSessionDate === undefined) {
this.storageMainService.store(firstSessionDateStorageKey, new Date().toUTCString());
}
const lastSessionDate = this.storageMainService.get(currentSessionDateStorageKey, undefined); // previous session date was the "current" one at that time
const currentSessionDate = new Date().toUTCString(); // current session date is "now"
this.storageMainService.store(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate);
this.storageMainService.store(currentSessionDateStorageKey, currentSessionDate);
}
private registerListeners(): void {
// Listen for changes in global storage to send to listeners
// that are listening. Use a debouncer to reduce IPC traffic.
this._register(Event.debounce(this.storageMainService.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => {
if (!prev) {
prev = [cur];
} else {
prev.push(cur);
}
return prev;
}, GlobalStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => {
if (events.length) {
this._onDidChangeItems.fire(this.serializeEvents(events));
}
}));
}
private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent {
const changed = new Map<Key, Value>();
const deleted = new Set<Key>();
events.forEach(event => {
const existing = this.storageMainService.get(event.key);
if (typeof existing === 'string') {
changed.set(event.key, existing);
} else {
deleted.add(event.key);
}
});
return {
changed: Array.from(changed.entries()),
deleted: Array.from(deleted.values())
};
}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeItems': return this.onDidChangeItems;
}
throw new Error(`Event not found: ${event}`);
}
async call(_: unknown, command: string, arg?: any): Promise<any> {
// ensure to always wait for ready
await this.whenReady;
// handle call
switch (command) {
case 'getItems': {
return Array.from(this.storageMainService.items.entries());
}
case 'updateItems': {
const items: ISerializableUpdateRequest = arg;
if (items.insert) {
for (const [key, value] of items.insert) {
this.storageMainService.store(key, value);
}
}
if (items.delete) {
items.delete.forEach(key => this.storageMainService.remove(key));
}
break;
}
default:
throw new Error(`Call not found: ${command}`);
}
}
}
export class GlobalStorageDatabaseChannelClient extends Disposable implements IStorageDatabase {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
private onDidChangeItemsOnMainListener: IDisposable | undefined;
constructor(private channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this.onDidChangeItemsOnMainListener = this.channel.listen<ISerializableItemsChangeEvent>('onDidChangeItems')((e: ISerializableItemsChangeEvent) => this.onDidChangeItemsOnMain(e));
}
private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void {
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
this._onDidChangeItemsExternal.fire({
changed: e.changed ? new Map(e.changed) : undefined,
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
});
}
}
async getItems(): Promise<Map<string, string>> {
const items: Item[] = await this.channel.call('getItems');
return new Map(items);
}
updateItems(request: IUpdateRequest): Promise<void> {
const serializableRequest: ISerializableUpdateRequest = Object.create(null);
if (request.insert) {
serializableRequest.insert = Array.from(request.insert.entries());
}
if (request.delete) {
serializableRequest.delete = Array.from(request.delete.values());
}
return this.channel.call('updateItems', serializableRequest);
}
async close(): Promise<void> {
// when we are about to close, we start to ignore main-side changes since we close anyway
dispose(this.onDidChangeItemsOnMainListener);
}
dispose(): void {
super.dispose();
dispose(this.onDidChangeItemsOnMainListener);
}
}

View File

@@ -1,191 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
export interface IStorageMainService {
readonly _serviceBrand: undefined;
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IStorageChangeEvent>;
/**
* Emitted when the storage is about to persist. This is the right time
* to persist data to ensure it is stored before the application shuts
* down.
*
* Note: this event may be fired many times, not only on shutdown to prevent
* loss of state in situations where the shutdown is not sufficient to
* persist the data properly.
*/
readonly onWillSaveState: Event<void>;
/**
* Access to all cached items of this storage service.
*/
readonly items: Map<string, string>;
/**
* Required call to ensure the service can be used.
*/
initialize(): Promise<void>;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
*/
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
*/
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
*/
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
/**
* Store a string value under the given key to storage. The value will
* be converted to a string.
*/
store(key: string, value: string | boolean | number | undefined | null): void;
/**
* Delete an element stored under the provided key from storage.
*/
remove(key: string): void;
}
export interface IStorageChangeEvent {
key: string;
}
export class StorageMainService extends Disposable implements IStorageMainService {
declare readonly _serviceBrand: undefined;
private static readonly STORAGE_NAME = 'state.vscdb';
private readonly _onDidChangeStorage = this._register(new Emitter<IStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onWillSaveState = this._register(new Emitter<void>());
readonly onWillSaveState = this._onWillSaveState.event;
get items(): Map<string, string> { return this.storage.items; }
private storage: IStorage;
private initializePromise: Promise<void> | undefined;
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
super();
// Until the storage has been initialized, it can only be in memory
this.storage = new Storage(new InMemoryStorageDatabase());
}
private get storagePath(): string {
if (!!this.environmentService.extensionTestsLocationURI) {
return SQLiteStorageDatabase.IN_MEMORY_PATH; // no storage during extension tests!
}
return join(this.environmentService.globalStorageHome.fsPath, StorageMainService.STORAGE_NAME);
}
private createLogginOptions(): ISQLiteStorageDatabaseLoggingOptions {
return {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
logError: error => this.logService.error(error)
};
}
initialize(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize();
}
return this.initializePromise;
}
private async doInitialize(): Promise<void> {
this.storage.dispose();
this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, {
logging: this.createLogginOptions()
}));
this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key })));
await this.storage.init();
// Check to see if this is the first time we are "opening" the application
const firstOpen = this.storage.getBoolean(IS_NEW_KEY);
if (firstOpen === undefined) {
this.storage.set(IS_NEW_KEY, true);
} else if (firstOpen) {
this.storage.set(IS_NEW_KEY, false);
}
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
return this.storage.get(key, fallbackValue);
}
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
return this.storage.getBoolean(key, fallbackValue);
}
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
getNumber(key: string, fallbackValue?: number): number | undefined {
return this.storage.getNumber(key, fallbackValue);
}
store(key: string, value: string | boolean | number | undefined | null): Promise<void> {
return this.storage.set(key, value);
}
remove(key: string): Promise<void> {
return this.storage.delete(key);
}
close(): Promise<void> {
// Signal as event so that clients can still store data
this._onWillSaveState.fire();
// Do it
return this.storage.close();
}
}

View File

@@ -3,18 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { StorageScope, WillSaveStateReason, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
import { mark } from 'vs/base/common/performance';
import { join } from 'vs/base/common/path';
import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs';
import { copy, exists, writeFile } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { assertIsDefined } from 'vs/base/common/types';
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
import { Promises } from 'vs/base/common/async';
export class NativeStorageService extends AbstractStorageService {
@@ -27,13 +28,9 @@ export class NativeStorageService extends AbstractStorageService {
private workspaceStorage: IStorage | undefined;
private workspaceStorageListener: IDisposable | undefined;
private initializePromise: Promise<void> | undefined;
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60000 /* every minute */));
private runWhenIdleDisposable: IDisposable | undefined = undefined;
constructor(
private globalStorageDatabase: IStorageDatabase,
private payload: IWorkspaceInitializationPayload | undefined,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
@@ -48,26 +45,13 @@ export class NativeStorageService extends AbstractStorageService {
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.doInitialize(payload);
}
return this.initializePromise;
}
private async doInitialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
protected async doInitialize(): Promise<void> {
// Init all storage locations
await Promise.all([
await Promises.settled([
this.initializeGlobalStorage(),
payload ? this.initializeWorkspaceStorage(payload) : Promise.resolve()
this.payload ? this.initializeWorkspaceStorage(this.payload) : Promise.resolve()
]);
// On some OS we do not get enough time to persist state on shutdown (e.g. when
// Windows restarts after applying updates). In other cases, VSCode might crash,
// so we periodically save state to reduce the chance of loosing any state.
this.periodicFlushScheduler.schedule();
}
private initializeGlobalStorage(): Promise<void> {
@@ -138,7 +122,7 @@ export class NativeStorageService extends AbstractStorageService {
return { path: workspaceStorageFolderPath, wasCreated: false };
}
await mkdirp(workspaceStorageFolderPath);
await promises.mkdir(workspaceStorageFolderPath, { recursive: true });
// Write metadata into folder
this.ensureWorkspaceStorageFolderMeta(payload);
@@ -169,6 +153,7 @@ export class NativeStorageService extends AbstractStorageService {
}
}
<<<<<<< HEAD
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope): string | undefined;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
@@ -193,73 +178,38 @@ export class NativeStorageService extends AbstractStorageService {
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
=======
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
>>>>>>> e8cd17a97d8c58fffcbac05394b3ee2b3c72d384
}
private getStorage(scope: StorageScope): IStorage {
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
}
protected async doFlush(): Promise<void> {
const promises: Promise<unknown>[] = [];
if (this.globalStorage) {
promises.push(this.globalStorage.whenFlushed());
}
if (this.workspaceStorage) {
promises.push(this.workspaceStorage.whenFlushed());
}
await Promise.all(promises);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
dispose(this.runWhenIdleDisposable);
// Run when idle
this.runWhenIdleDisposable = runWhenIdle(() => {
// send event to collect state
this.flush();
// repeat
this.periodicFlushScheduler.schedule();
});
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStoragePath;
}
async close(): Promise<void> {
// Stop periodic scheduler and idle runner as we now collect state normally
this.periodicFlushScheduler.dispose();
dispose(this.runWhenIdleDisposable);
this.runWhenIdleDisposable = undefined;
this.stopFlushWhenIdle();
// Signal as event so that clients can still store data
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
// Do it
await Promise.all([
await Promises.settled([
this.globalStorage.close(),
this.workspaceStorage ? this.workspaceStorage.close() : Promise.resolve()
]);
}
async logStorage(): Promise<void> {
return logStorage(
this.globalStorage.items,
this.workspaceStorage ? this.workspaceStorage.items : new Map<string, string>(), // Shared process storage does not has workspace storage
this.environmentService.globalStorageHome.fsPath,
this.workspaceStoragePath || '');
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) {
return; // no migration needed if running in memory
}
// Close workspace DB to be able to copy
await this.getStorage(StorageScope.WORKSPACE).close();
await this.workspaceStorage?.close();
// Prepare new workspace storage folder
const result = await this.prepareWorkspaceStorageFolder(toWorkspace);
@@ -267,7 +217,7 @@ export class NativeStorageService extends AbstractStorageService {
const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME);
// Copy current storage over to new workspace storage
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath);
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath, { preserveSymlinks: false });
// Recreate and init workspace storage
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();

View File

@@ -4,49 +4,67 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual } from 'assert';
import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { rimraf } from 'vs/base/node/pfs';
import { BrowserStorageService, FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { NullLogService } from 'vs/platform/log/common/log';
import { Storage } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { FileService } from 'vs/platform/files/common/fileService';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { createSuite } from 'vs/platform/storage/test/common/storageService.test';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
suite('Storage', () => {
suite('StorageService (browser)', function () {
let testDir: string;
const disposables = new DisposableStore();
let storageService: BrowserStorageService;
createSuite<BrowserStorageService>({
setup: async () => {
const logService = new NullLogService();
const fileService = disposables.add(new FileService(logService));
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
storageService = disposables.add(new BrowserStorageService({ id: String(Date.now()) }, { userRoamingDataHome: URI.file('/User').with({ scheme: Schemas.userData }) } as unknown as IEnvironmentService, fileService));
await storageService.initialize();
return storageService;
},
teardown: async storage => {
await storageService.flush();
disposables.clear();
}
});
});
suite('FileStorageDatabase (browser)', () => {
let fileService: FileService;
let fileProvider: DiskFileSystemProvider;
const disposables = new DisposableStore();
setup(async () => {
const logService = new NullLogService();
fileService = new FileService(logService);
disposables.add(fileService);
fileService = disposables.add(new FileService(logService));
fileProvider = new DiskFileSystemProvider(logService);
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
disposables.add(fileProvider);
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
const userDataProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider));
});
teardown(() => {
disposables.clear();
return rimraf(testDir);
});
test('File Based Storage', async () => {
let storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
test('Basics', async () => {
const testDir = URI.file('/User/storage.json').with({ scheme: Schemas.userData });
let storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();
@@ -60,7 +78,7 @@ suite('Storage', () => {
await storage.close();
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();
@@ -78,7 +96,7 @@ suite('Storage', () => {
await storage.close();
storage = new Storage(new FileStorageDatabase(URI.file(join(testDir, 'storage.json')), false, fileService));
storage = new Storage(new FileStorageDatabase(testDir, false, fileService));
await storage.init();

View File

@@ -4,98 +4,102 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok } from 'assert';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent, IStorageService } from 'vs/platform/storage/common/storage';
suite('StorageService', function () {
export function createSuite<T extends IStorageService>(params: { setup: () => Promise<T>, teardown: (service: T) => Promise<void> }): void {
test('Get Data, Integer, Boolean (global, in-memory)', () => {
let storageService: T;
setup(async () => {
storageService = await params.setup();
});
teardown(() => {
return params.teardown(storageService);
});
test('Get Data, Integer, Boolean (global)', () => {
storeData(StorageScope.GLOBAL);
});
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
test('Get Data, Integer, Boolean (workspace)', () => {
storeData(StorageScope.WORKSPACE);
});
function storeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
storageService.onDidChangeValue(e => storageValueChangeEvents.push(e));
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('test.get', scope, ''), '');
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
strictEqual(storageService.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storageService.get('test.get', scope, ''), '');
strictEqual(storageService.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storageService.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storageService.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storageService.getBoolean('test.getBoolean', scope, false), false);
storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
storageService.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storageService.get('test.get', scope, (undefined)!), 'foobar');
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.get');
storageValueChangeEvents = [];
storage.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storageService.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storageService.get('test.get', scope, (undefined)!), '');
storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
strictEqual(storageValueChangeEvent!.scope, scope);
strictEqual(storageValueChangeEvent!.key, 'test.get');
storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
storageService.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 5);
storage.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
storageService.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
strictEqual(storageService.getNumber('test.getNumber', scope, (undefined)!), 0);
storage.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
storageService.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), true);
storage.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
storageService.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
strictEqual(storageService.getBoolean('test.getBoolean', scope, (undefined)!), false);
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
strictEqual(storageService.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storageService.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storageService.getBoolean('test.getBooleanDefault', scope, true), true);
}
test('Remove Data (global, in-memory)', () => {
test('Remove Data (global)', () => {
removeData(StorageScope.GLOBAL);
});
test('Remove Data (workspace, in-memory)', () => {
test('Remove Data (workspace)', () => {
removeData(StorageScope.WORKSPACE);
});
function removeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
storageService.onDidChangeValue(e => storageValueChangeEvents.push(e));
storage.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
storageService.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
strictEqual('foobar', storageService.get('test.remove', scope, (undefined)!));
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
storageService.remove('test.remove', scope);
ok(!storageService.get('test.remove', scope, (undefined)!));
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.remove');
}
test('Keys (in-memory)', () => {
const storage = new InMemoryStorageService();
let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined;
storage.onDidChangeTarget(e => storageTargetEvent = e);
storageService.onDidChangeTarget(e => storageTargetEvent = e);
let storageValueChangeEvent: IStorageValueChangeEvent | undefined = undefined;
storage.onDidChangeValue(e => storageValueChangeEvent = e);
storageService.onDidChangeValue(e => storageValueChangeEvent = e);
// Empty
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
strictEqual(storage.keys(scope, target).length, 0);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
@@ -105,8 +109,8 @@ suite('StorageService', function () {
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
@@ -115,33 +119,33 @@ suite('StorageService', function () {
storageTargetEvent = undefined;
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'otherValue1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'otherValue1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
strictEqual(storageTargetEvent, undefined);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.target, target);
storage.store('test.target2', 'value2', scope, target);
storage.store('test.target3', 'value3', scope, target);
storageService.store('test.target2', 'value2', scope, target);
storageService.store('test.target3', 'value3', scope, target);
strictEqual(storage.keys(scope, target).length, 3);
strictEqual(storageService.keys(scope, target).length, 3);
}
}
// Remove values
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keysLength = storage.keys(scope, target).length;
const keysLength = storageService.keys(scope, target).length;
storage.store('test.target4', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, keysLength + 1);
storageService.store('test.target4', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, keysLength + 1);
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.remove('test.target4', scope);
strictEqual(storage.keys(scope, target).length, keysLength);
storageService.remove('test.target4', scope);
strictEqual(storageService.keys(scope, target).length, keysLength);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target4');
strictEqual(storageValueChangeEvent?.scope, scope);
@@ -151,48 +155,55 @@ suite('StorageService', function () {
// Remove all
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keys = storage.keys(scope, target);
const keys = storageService.keys(scope, target);
for (const key of keys) {
storage.remove(key, scope);
storageService.remove(key, scope);
}
strictEqual(storage.keys(scope, target).length, 0);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
// Adding undefined or null removes value
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', 'value1', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
storageTargetEvent = Object.create(null);
storage.store('test.target1', undefined, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
storageService.store('test.target1', undefined, scope, target);
strictEqual(storageService.keys(scope, target).length, 0);
strictEqual(storageTargetEvent?.scope, scope);
storage.store('test.target1', '', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageService.store('test.target1', '', scope, target);
strictEqual(storageService.keys(scope, target).length, 1);
storage.store('test.target1', null, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
storageService.store('test.target1', null, scope, target);
strictEqual(storageService.keys(scope, target).length, 0);
}
}
// Target change
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
storageService.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(!storageTargetEvent); // no change in target
});
}
suite('StorageService (in-memory)', function () {
createSuite<InMemoryStorageService>({
setup: async () => new InMemoryStorageService(),
teardown: async () => { }
});
});

View File

@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { notStrictEqual, strictEqual } from 'assert';
import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { NullLogService } from 'vs/platform/log/common/log';
import { StorageMainService } from 'vs/platform/storage/electron-main/storageMainService';
import { currentSessionDateStorageKey, firstSessionDateStorageKey, instanceStorageKey } from 'vs/platform/telemetry/common/telemetry';
import { IStorageChangeEvent, IStorageMain, IStorageMainOptions } from 'vs/platform/storage/electron-main/storageMain';
import { generateUuid } from 'vs/base/common/uuid';
import { IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { ILifecycleMainService, LifecycleMainPhase, ShutdownEvent, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { Emitter, Event } from 'vs/base/common/event';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { Promises } from 'vs/base/common/async';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('StorageMainService', function () {
class TestStorageMainService extends StorageMainService {
protected getStorageOptions(): IStorageMainOptions {
return {
useInMemoryStorage: true
};
}
protected enableMainWorkspaceStorage(): boolean {
return true;
}
}
class StorageTestLifecycleMainService implements ILifecycleMainService {
_serviceBrand: undefined;
onBeforeShutdown = Event.None;
private readonly _onWillShutdown = new Emitter<ShutdownEvent>();
readonly onWillShutdown = this._onWillShutdown.event;
async fireOnWillShutdown(): Promise<void> {
const joiners: Promise<void>[] = [];
this._onWillShutdown.fire({
join(promise) {
if (promise) {
joiners.push(promise);
}
}
});
await Promises.settled(joiners);
}
onWillLoadWindow = Event.None;
onBeforeCloseWindow = Event.None;
onBeforeUnloadWindow = Event.None;
wasRestarted = false;
quitRequested = false;
phase = LifecycleMainPhase.Ready;
registerWindow(window: ICodeWindow): void { }
async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise<void> { }
async unload(window: ICodeWindow, reason: UnloadReason): Promise<boolean> { return true; }
relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined; }): void { }
async quit(fromUpdate?: boolean): Promise<boolean> { return true; }
async kill(code?: number): Promise<void> { }
async when(phase: LifecycleMainPhase): Promise<void> { }
}
async function testStorage(storage: IStorageMain, isGlobal: boolean): Promise<void> {
// Telemetry: added after init
if (isGlobal) {
strictEqual(storage.items.size, 0);
strictEqual(storage.get(instanceStorageKey), undefined);
await storage.init();
strictEqual(typeof storage.get(instanceStorageKey), 'string');
strictEqual(typeof storage.get(firstSessionDateStorageKey), 'string');
strictEqual(typeof storage.get(currentSessionDateStorageKey), 'string');
} else {
await storage.init();
}
let storageChangeEvent: IStorageChangeEvent | undefined = undefined;
const storageChangeListener = storage.onDidChangeStorage(e => {
storageChangeEvent = e;
});
let storageDidClose = false;
const storageCloseListener = storage.onDidCloseStorage(() => storageDidClose = true);
// Basic store/get/remove
const size = storage.items.size;
storage.set('bar', 'foo');
strictEqual(storageChangeEvent!.key, 'bar');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
strictEqual(storage.items.size, size + 3);
storage.delete('bar');
strictEqual(storage.get('bar'), undefined);
strictEqual(storage.items.size, size + 2);
// IS_NEW
strictEqual(storage.get(IS_NEW_KEY), 'true');
// Close
await storage.close();
strictEqual(storageDidClose, true);
storageChangeListener.dispose();
storageCloseListener.dispose();
}
test('basics (global)', function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
return testStorage(storageMainService.globalStorage, true);
});
test('basics (workspace)', function () {
const workspace = { id: generateUuid() };
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
return testStorage(storageMainService.workspaceStorage(workspace), false);
});
test('storage closed onWillShutdown', async function () {
const lifecycleMainService = new StorageTestLifecycleMainService();
const workspace = { id: generateUuid() };
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), lifecycleMainService, new TestConfigurationService());
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
strictEqual(workspaceStorage, storageMainService.workspaceStorage(workspace)); // same instance as long as not closed
await globalStorage.init();
await workspaceStorage.init();
await lifecycleMainService.fireOnWillShutdown();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
let storage2 = storageMainService.workspaceStorage(workspace);
notStrictEqual(workspaceStorage, storage2);
return storage2.close();
});
test('storage closed before init works', async function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
await globalStorage.close();
await workspaceStorage.close();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
});
test('storage closed before init awaits works', async function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
workspaceStorage.onDidCloseStorage(() => {
didCloseWorkspaceStorage = true;
});
let globalStorage = storageMainService.globalStorage;
let didCloseGlobalStorage = false;
globalStorage.onDidCloseStorage(() => {
didCloseGlobalStorage = true;
});
globalStorage.init();
workspaceStorage.init();
await globalStorage.close();
await workspaceStorage.close();
strictEqual(didCloseGlobalStorage, true);
strictEqual(didCloseWorkspaceStorage, true);
});
});

View File

@@ -7,47 +7,56 @@ import { strictEqual } from 'assert';
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
import { tmpdir } from 'os';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { rimraf } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { createSuite } from 'vs/platform/storage/test/common/storageService.test';
flakySuite('NativeStorageService', function () {
flakySuite('StorageService (native)', function () {
class StorageTestEnvironmentService extends NativeEnvironmentService {
constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) {
super(parseArgs(process.argv, OPTIONS));
}
get workspaceStorageHome(): URI {
return this.workspaceStorageFolderPath;
}
get extensionsPath(): string {
return this._extensionsPath;
}
}
let testDir: string;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
createSuite<NativeStorageService>({
setup: async () => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
return mkdirp(testDir);
});
await promises.mkdir(testDir, { recursive: true });
teardown(() => {
return rimraf(testDir);
const storageService = new NativeStorageService(new InMemoryStorageDatabase(), { id: String(Date.now()) }, new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storageService.initialize();
return storageService;
},
teardown: async storageService => {
await storageService.close();
return rimraf(testDir);
}
});
test('Migrate Data', async function () {
class StorageTestEnvironmentService extends NativeEnvironmentService {
constructor(private workspaceStorageFolderPath: URI, private _extensionsPath: string) {
super(parseArgs(process.argv, OPTIONS));
}
get workspaceStorageHome(): URI {
return this.workspaceStorageFolderPath;
}
get extensionsPath(): string {
return this._extensionsPath;
}
}
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storage.initialize({ id: String(Date.now()) });
const storage = new NativeStorageService(new InMemoryStorageDatabase(), { id: String(Date.now()) }, new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storage.initialize();
storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE);

View File

@@ -12,6 +12,7 @@ export interface ITelemetryInfo {
sessionId: string;
machineId: string;
instanceId: string;
firstSessionDate: string;
msftInternal?: boolean;
}

View File

@@ -17,7 +17,7 @@ export class TelemetryLogAppender extends Disposable implements ITelemetryAppend
@IEnvironmentService environmentService: IEnvironmentService
) {
super();
this.logger = this._register(loggerService.getLogger(environmentService.telemetryLogResource));
this.logger = this._register(loggerService.createLogger(environmentService.telemetryLogResource));
this.logger.info('The below are logs for every telemetry event sent from VS Code once the log level is set to trace.');
this.logger.info('===========================================================');
}

Some files were not shown because too many files have changed in this diff Show More