mirror of
https://github.com/coder/code-server.git
synced 2026-05-05 20:15:19 +02:00
chore(vscode): update to 1.54.2
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -115,9 +115,6 @@ export interface INativeEnvironmentService extends IEnvironmentService {
|
||||
machineSettingsResource: URI;
|
||||
installSourcePath: string;
|
||||
|
||||
// --- IPC Handles
|
||||
sharedIPCHandle: string;
|
||||
|
||||
// --- Extensions
|
||||
extensionsPath: string;
|
||||
extensionsDownloadPath: string;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
138
lib/vscode/src/vs/platform/environment/node/shellEnv.ts
Normal file
138
lib/vscode/src/vs/platform/environment/node/shellEnv.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IDiskFileChange {
|
||||
}
|
||||
|
||||
export interface ILogMessage {
|
||||
type: 'trace' | 'warn' | 'error';
|
||||
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>][] {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
77
lib/vscode/src/vs/platform/ipc/electron-sandbox/services.ts
Normal file
77
lib/vscode/src/vs/platform/ipc/electron-sandbox/services.ts
Normal 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
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ suite('AbstractKeybindingService', () => {
|
||||
contextMatchesRules: undefined!,
|
||||
getContextKeyValue: undefined!,
|
||||
createScoped: undefined!,
|
||||
createOverlay: undefined!,
|
||||
getContext: (target: IContextKeyServiceTarget): any => {
|
||||
return currentContextValue;
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
120
lib/vscode/src/vs/platform/log/node/spdlogLog.ts
Normal file
120
lib/vscode/src/vs/platform/log/node/spdlogLog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ export interface ISharedProcessConfiguration {
|
||||
|
||||
readonly userEnv: NodeJS.ProcessEnv;
|
||||
|
||||
readonly sharedIPCHandle: string;
|
||||
|
||||
readonly args: NativeParsedArgs;
|
||||
|
||||
readonly logLevel: LogLevel;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
136
lib/vscode/src/vs/platform/storage/common/storageIpc.ts
Normal file
136
lib/vscode/src/vs/platform/storage/common/storageIpc.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
136
lib/vscode/src/vs/platform/storage/electron-main/storageIpc.ts
Normal file
136
lib/vscode/src/vs/platform/storage/electron-main/storageIpc.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
307
lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts
Normal file
307
lib/vscode/src/vs/platform/storage/electron-main/storageMain.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => { }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ITelemetryInfo {
|
||||
sessionId: string;
|
||||
machineId: string;
|
||||
instanceId: string;
|
||||
firstSessionDate: string;
|
||||
msftInternal?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user