chore(vscode): update to 1.55.2

This commit is contained in:
Akash Satheesan
2021-04-09 11:32:27 +05:30
1102 changed files with 39988 additions and 23544 deletions

View File

@@ -12,25 +12,26 @@ import { localize } from 'vs/nls';
import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { isWindows, isLinux, OS } from 'vs/base/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string): IDisposable {
const groups = menu.getActions(options);
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey);
fillInActions(groups, target, useAlternativeActions, isPrimaryGroup);
fillInActions(groups, target, useAlternativeActions, primaryGroup);
return asDisposable(groups);
}
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 {
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string, 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, primaryMaxCount, shouldInlineSubmenu);
fillInActions(groups, target, false, primaryGroup, primaryMaxCount, shouldInlineSubmenu);
return asDisposable(groups);
}
@@ -48,7 +49,7 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
function fillInActions(
groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; },
useAlternativeActions: boolean,
isPrimaryGroup: (group: string) => boolean = group => group === 'navigation',
primaryGroup = 'navigation',
primaryMaxCount: number = Number.MAX_SAFE_INTEGER,
shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false
): void {
@@ -68,7 +69,7 @@ function fillInActions(
for (const [group, actions] of groups) {
let target: IAction[];
if (isPrimaryGroup(group)) {
if (group === primaryGroup) {
target = primaryBucket;
} else {
target = secondaryBucket;
@@ -92,7 +93,7 @@ function fillInActions(
// 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;
const target = group === primaryGroup ? primaryBucket : secondaryBucket;
// inlining submenus with length 0 or 1 is easy,
// larger submenus need to be checked with the overall limit
@@ -187,9 +188,19 @@ export class MenuEntryActionViewItem extends ActionViewItem {
const keybindingLabel = keybinding && keybinding.getLabel();
const tooltip = this._commandAction.tooltip || this._commandAction.label;
this.label.title = keybindingLabel
let title = keybindingLabel
? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel)
: tooltip;
if (!this._wantsAltCommand && this._action.alt) {
const altTooltip = this._action.alt.tooltip || this._action.alt.label;
const altKeybinding = this._keybindingService.lookupKeybinding(this._action.alt.id);
const altKeybindingLabel = altKeybinding && altKeybinding.getLabel();
const altTitleSection = altKeybindingLabel
? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel)
: altTooltip;
title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`;
}
this.label.title = title;
}
}
@@ -221,10 +232,10 @@ export class MenuEntryActionViewItem extends ActionViewItem {
if (ThemeIcon.isThemeIcon(icon)) {
// theme icons
const iconClass = ThemeIcon.asClassName(icon);
label.classList.add(...iconClass.split(' '));
const iconClasses = ThemeIcon.asClassNameArray(icon);
label.classList.add(...iconClasses);
this._itemClassDispose.value = toDisposable(() => {
label.classList.remove(...iconClass.split(' '));
label.classList.remove(...iconClasses);
});
} else {

View File

@@ -87,6 +87,7 @@ export class MenuId {
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
static readonly DebugToolBar = new MenuId('DebugToolBar');
static readonly EditorContext = new MenuId('EditorContext');
static readonly EditorContextCopy = new MenuId('EditorContextCopy');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorTitle = new MenuId('EditorTitle');
static readonly EditorTitleRun = new MenuId('EditorTitleRun');
@@ -98,6 +99,7 @@ export class MenuId {
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
static readonly MenubarEditMenu = new MenuId('MenubarEditMenu');
static readonly MenubarCopy = new MenuId('MenubarCopy');
static readonly MenubarFileMenu = new MenuId('MenubarFileMenu');
static readonly MenubarGoMenu = new MenuId('MenubarGoMenu');
static readonly MenubarHelpMenu = new MenuId('MenubarHelpMenu');
@@ -138,10 +140,12 @@ export class MenuId {
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
static readonly CommentTitle = new MenuId('CommentTitle');
static readonly CommentActions = new MenuId('CommentActions');
static readonly NotebookToolbar = new MenuId('NotebookToolbar');
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
static readonly NotebookCellListTop = new MenuId('NotebookCellTop');
static readonly NotebookCellExecute = new MenuId('NotebookCellExecute');
static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle');
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');

View File

@@ -23,6 +23,7 @@ import { createHash } from 'crypto';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import product from 'vs/platform/product/common/product';
flakySuite('BackupMainService', () => {
@@ -104,7 +105,7 @@ flakySuite('BackupMainService', () => {
backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
existingTestFolder1 = URI.file(path.join(testDir, 'folder1'));
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product });
await fs.promises.mkdir(backupHome, { recursive: true });

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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 { URI } from 'vs/base/common/uri';
export const IChecksumService = createDecorator<IChecksumService>('checksumService');
export interface IChecksumService {
readonly _serviceBrand: undefined;
/**
* Computes the checksum of the contents of the resource.
*/
checksum(resource: URI): Promise<string>;
}

View File

@@ -3,9 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { IChecksumService } from 'vs/platform/checksum/common/checksumService';
export interface IDisplayMainService {
readonly _serviceBrand: undefined;
readonly onDidDisplayChanged: Event<void>;
}
registerSharedProcessRemoteService(IChecksumService, 'checksum', { supportsDelayedInstantiation: true });

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'crypto';
import { listenStream } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { IChecksumService } from 'vs/platform/checksum/common/checksumService';
import { IFileService } from 'vs/platform/files/common/files';
export class ChecksumService implements IChecksumService {
declare readonly _serviceBrand: undefined;
constructor(@IFileService private readonly fileService: IFileService) { }
checksum(resource: URI): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
const hash = createHash('md5');
const stream = (await this.fileService.readFileStream(resource)).value;
listenStream(stream, {
onData: data => hash.update(data.buffer),
onError: error => reject(error),
onEnd: () => resolve(hash.digest('base64').replace(/=+$/, ''))
});
});
}
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 { getPathFromAmdModule } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ChecksumService } from 'vs/platform/checksum/node/checksumService';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
suite('Checksum Service', () => {
let fileService: IFileService;
setup(() => {
const logService = new NullLogService();
fileService = new FileService(logService);
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
});
teardown(() => {
fileService.dispose();
});
test('checksum', async () => {
const checksumService = new ChecksumService(fileService);
const checksum = await checksumService.checksum(URI.file(getPathFromAmdModule(require, './fixtures/lorem.txt')));
assert.ok(checksum === '8mi5KF8kcb817zmlal1kZA' || checksum === 'DnUKbJ1bHPPNZoHgHV25sg'); // depends on line endings git config
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,7 @@ export interface ICommandHandlerDescription {
readonly description: string;
readonly args: ReadonlyArray<{
readonly name: string;
readonly isOptional?: boolean;
readonly description?: string;
readonly constraint?: TypeConstraint;
readonly schema?: IJSONSchema;

View File

@@ -354,6 +354,8 @@ export class UserSettings extends Disposable {
super();
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource)));
// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134
this._register(this.fileService.watch(this.userSettingsResource));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics';
registerSharedProcessRemoteService(IDiagnosticsService, 'diagnostics', { supportsDelayedInstantiation: true });

View File

@@ -9,7 +9,7 @@ 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';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
@@ -213,7 +213,10 @@ export class DiagnosticsService implements IDiagnosticsService {
declare readonly _serviceBrand: undefined;
constructor(@ITelemetryService private readonly telemetryService: ITelemetryService) { }
constructor(
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService
) { }
private formatMachineInfo(info: IMachineInfo): string {
const output: string[] = [];
@@ -227,7 +230,7 @@ export class DiagnosticsService implements IDiagnosticsService {
private formatEnvironment(info: IMainProcessInfo): string {
const output: string[] = [];
output.push(`Version: ${product.nameShort} ${product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`);
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
@@ -481,7 +484,7 @@ export class DiagnosticsService implements IDiagnosticsService {
// Format name with indent
let name: string;
if (isRoot) {
name = item.pid === mainPid ? `${product.applicationName} main` : 'remote agent';
name = item.pid === mainPid ? `${this.productService.applicationName} main` : 'remote agent';
} else {
name = `${' '.repeat(indent)} ${item.name}`;

View File

@@ -1,47 +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 { IDisplayMainService as ICommonDisplayMainService } from 'vs/platform/display/common/displayMainService';
import { Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { app, Display, screen } from 'electron';
import { RunOnceScheduler } from 'vs/base/common/async';
export const IDisplayMainService = createDecorator<IDisplayMainService>('displayMainService');
export interface IDisplayMainService extends ICommonDisplayMainService { }
export class DisplayMainService extends Disposable implements ICommonDisplayMainService {
declare readonly _serviceBrand: undefined;
private readonly _onDidDisplayChanged = this._register(new Emitter<void>());
readonly onDidDisplayChanged = this._onDidDisplayChanged.event;
constructor() {
super();
const displayChangedScheduler = this._register(new RunOnceScheduler(() => {
this._onDidDisplayChanged.fire();
}, 100));
app.whenReady().then(() => {
const displayChangedListener = (event: Event, display: Display, changedMetrics?: string[]) => {
displayChangedScheduler.schedule();
};
screen.on('display-metrics-changed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-metrics-changed', displayChangedListener)));
screen.on('display-added', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-added', displayChangedListener)));
screen.on('display-removed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-removed', displayChangedListener)));
});
}
}

View File

@@ -43,6 +43,7 @@ export interface NativeParsedArgs {
'builtin-extensions-dir'?: string;
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
extensionDevelopmentKind?: string[];
'inspect-extensions'?: string;
'inspect-brk-extensions'?: string;
debugId?: string;

View File

@@ -3,12 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
export const INativeEnvironmentService = createDecorator<INativeEnvironmentService>('nativeEnvironmentService');
export const INativeEnvironmentService = refineServiceDecorator<IEnvironmentService, INativeEnvironmentService>(IEnvironmentService);
export interface IDebugParams {
port: number | null;
@@ -62,6 +63,7 @@ export interface IEnvironmentService {
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
extensionDevelopmentLocationURI?: URI[];
extensionDevelopmentKind?: ExtensionKind[];
extensionTestsLocationURI?: URI;
// --- logging
@@ -106,7 +108,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
// --- CLI Arguments
args: NativeParsedArgs;
// --- paths
// --- data paths
appRoot: string;
userHome: URI;
appSettingsHome: URI;
@@ -115,14 +117,14 @@ export interface INativeEnvironmentService extends IEnvironmentService {
machineSettingsResource: URI;
installSourcePath: string;
// --- Extensions
// --- extensions
extensionsPath: string;
extensionsDownloadPath: string;
builtinExtensionsPath: string;
extraExtensionPaths: string[]
extraBuiltinExtensionPaths: string[]
// --- Smoke test support
// --- smoke test support
driverHandle?: string;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View File

@@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProductService } from 'vs/platform/product/common/productService';
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
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 { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
import { env } from 'vs/base/common/process';
export interface INativeEnvironmentPaths {
/**
* The user data directory to use for anything that should be
* persisted except for the content that is meant for the `homeDir`.
*
* Only one instance of VSCode can use the same `userDataDir`.
*/
userDataDir: string
/**
* The user home directory mainly used for persisting extensions
* and global configuration that should be shared across all
* versions.
*/
homeDir: string;
/**
* OS tmp dir.
*/
tmpDir: string,
}
export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService {
declare readonly _serviceBrand: undefined;
@memoize
get appRoot(): string { return dirname(FileAccess.asFileUri('', require).fsPath); }
@memoize
get userHome(): URI { return URI.file(this.paths.homeDir); }
@memoize
get userDataPath(): string { return this.paths.userDataDir; }
@memoize
get appSettingsHome(): URI { return URI.file(join(this.userDataPath, 'User')); }
@memoize
get tmpDir(): URI { return URI.file(this.paths.tmpDir); }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); }
get logsPath(): string {
if (!this.args.logsPath) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
this.args.logsPath = join(this.userDataPath, 'logs', key);
}
return this.args.logsPath;
}
@memoize
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 joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
@memoize
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
@memoize
get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get argvResource(): URI {
const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(join(vscodePortable, 'argv.json'));
}
return joinPath(this.userHome, this.productService.dataFolderName, 'argv.json');
}
@memoize
get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize
get isExtensionDevelopment(): boolean { return !!this.args.extensionDevelopmentPath; }
@memoize
get untitledWorkspacesHome(): URI { return URI.file(join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return join(this.userDataPath, 'installSource'); }
@memoize
get builtinExtensionsPath(): string {
const cliBuiltinExtensionsDir = this.args['builtin-extensions-dir'];
if (cliBuiltinExtensionsDir) {
return resolve(cliBuiltinExtensionsDir);
}
return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
get extensionsDownloadPath(): string {
const cliExtensionsDownloadDir = this.args['extensions-download-dir'];
if (cliExtensionsDownloadDir) {
return resolve(cliExtensionsDownloadDir);
}
return join(this.userDataPath, 'CachedExtensionVSIXs');
}
@memoize
get extensionsPath(): string {
const cliExtensionsDir = this.args['extensions-dir'];
if (cliExtensionsDir) {
return resolve(cliExtensionsDir);
}
const vscodeExtensions = env['VSCODE_EXTENSIONS'];
if (vscodeExtensions) {
return vscodeExtensions;
}
const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return join(vscodePortable, 'extensions');
}
return joinPath(this.userHome, this.productService.dataFolderName, 'extensions').fsPath;
}
@memoize
get extensionDevelopmentLocationURI(): URI[] | undefined {
const extensionDevelopmentPaths = this.args.extensionDevelopmentPath;
if (Array.isArray(extensionDevelopmentPaths)) {
return extensionDevelopmentPaths.map(extensionDevelopmentPath => {
if (/^[^:/?#]+?:\/\//.test(extensionDevelopmentPath)) {
return URI.parse(extensionDevelopmentPath);
}
return URI.file(normalize(extensionDevelopmentPath));
});
}
return undefined;
}
@memoize
get extensionDevelopmentKind(): ExtensionKind[] | undefined {
return this.args.extensionDevelopmentKind?.map(kind => kind === 'ui' || kind === 'workspace' || kind === 'web' ? kind : 'workspace');
}
@memoize
get extensionTestsLocationURI(): URI | undefined {
const extensionTestsPath = this.args.extensionTestsPath;
if (extensionTestsPath) {
if (/^[^:/?#]+?:\/\//.test(extensionTestsPath)) {
return URI.parse(extensionTestsPath);
}
return URI.file(normalize(extensionTestsPath));
}
return undefined;
}
get disableExtensions(): boolean | string[] {
if (this.args['disable-extensions']) {
return true;
}
const disableExtensions = this.args['disable-extension'];
if (disableExtensions) {
if (typeof disableExtensions === 'string') {
return [disableExtensions];
}
if (Array.isArray(disableExtensions) && disableExtensions.length > 0) {
return disableExtensions;
}
}
return false;
}
@memoize
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this.args, this.isBuilt); }
get debugRenderer(): boolean { return !!this.args.debugRenderer; }
get isBuilt(): boolean { return !env['VSCODE_DEV']; }
get verbose(): boolean { return !!this.args.verbose; }
get logLevel(): string | undefined { return this.args.log; }
@memoize
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']; }
get driverHandle(): string | undefined { return this.args['driver']; }
@memoize
get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; }
get args(): NativeParsedArgs { return this._args; }
constructor(
private readonly _args: NativeParsedArgs,
private readonly paths: INativeEnvironmentPaths,
protected readonly productService: IProductService
) { }
}
export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams {
return parseDebugPort(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId);
}
export function parseSearchPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams {
return parseDebugPort(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild);
}
function parseDebugPort(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string): IExtensionHostDebugParams {
const portStr = debugBrkArg || debugArg;
const port = Number(portStr) || (!isBuild ? defaultBuildPort : null);
const brk = port ? Boolean(!!debugBrkArg) : false;
return { port, break: brk, debugId };
}

View File

@@ -5,13 +5,12 @@
import { join } from 'vs/base/common/path';
import { memoize } from 'vs/base/common/decorators';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
import product from 'vs/platform/product/common/product';
export const IEnvironmentMainService = createDecorator<IEnvironmentMainService>('nativeEnvironmentService');
export const IEnvironmentMainService = refineServiceDecorator<IEnvironmentService, IEnvironmentMainService>(IEnvironmentService);
/**
* A subclass of the `INativeEnvironmentService` to be used only in electron-main
@@ -51,19 +50,19 @@ export class EnvironmentMainService extends NativeEnvironmentService implements
get backupWorkspacesPath(): string { return join(this.backupHome, 'workspaces.json'); }
@memoize
get mainIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'main', product.version); }
get mainIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'main', this.productService.version); }
@memoize
get sandbox(): boolean { return !!this._args['__sandbox']; }
get sandbox(): boolean { return !!this.args['__sandbox']; }
@memoize
get driverVerbose(): boolean { return !!this._args['driver-verbose']; }
get driverVerbose(): boolean { return !!this.args['driver-verbose']; }
@memoize
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
get disableUpdates(): boolean { return !!this.args['disable-updates']; }
@memoize
get disableKeytar(): boolean { return !!this._args['disable-keytar']; }
get disableKeytar(): boolean { return !!this.args['disable-keytar']; }
@memoize
get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; }

View File

@@ -85,6 +85,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'locate-extension': { type: 'string[]' },
'extensionDevelopmentPath': { type: 'string[]' },
'extensionDevelopmentKind': { type: 'string[]' },
'extensionTestsPath': { type: 'string' },
'debugId': { type: 'string' },
'debugRenderer': { type: 'boolean' },
@@ -263,7 +264,7 @@ export function formatOptions(options: OptionDescriptions<any>, columns: number)
}
function indent(count: number): string {
return (<any>' ').repeat(count);
return ' '.repeat(count);
}
function wrapText(text: string, columns: number): string[] {

View File

@@ -4,40 +4,21 @@
*--------------------------------------------------------------------------------------------*/
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 { 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 { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { getUserDataPath } from 'vs/platform/environment/node/userDataPath';
import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService';
import { IProductService } from 'vs/platform/product/common/productService';
export class NativeEnvironmentService implements INativeEnvironmentService {
export class NativeEnvironmentService extends AbstractNativeEnvironmentService {
declare readonly _serviceBrand: undefined;
get args(): NativeParsedArgs { return this._args; }
@memoize
get appRoot(): string { return dirname(FileAccess.asFileUri('', require).fsPath); }
readonly logsPath: string;
@memoize
get userHome(): URI { return URI.file(homedir()); }
@memoize
get userDataPath(): string {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return join(vscodePortable, 'user-data');
}
return parseUserDataDir(this._args, process);
constructor(args: NativeParsedArgs, productService: IProductService) {
super(args, {
homeDir: homedir(),
tmpDir: tmpdir(),
userDataDir: getUserDataPath(args)
}, productService);
}
<<<<<<< HEAD
@memoize
get appSettingsHome(): URI { return URI.file(join(this.userDataPath, 'User')); }
@@ -249,4 +230,6 @@ export function parsePathArg(arg: string | undefined, process: NodeJS.Process):
export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string {
return parsePathArg(args['user-data-dir'], process) || resolve(getDefaultUserDataPath());
=======
>>>>>>> 801aed93200dc0ccf325a09089c911e8e2b612d0
}

View File

@@ -79,7 +79,9 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise<typeof pr
logService.trace('getUnixShellEnvironment#env', env);
logService.trace('getUnixShellEnvironment#spawn', command);
const systemShellUnix = await getSystemShell(platform);
const systemShellUnix = await getSystemShell(platform, env);
logService.trace('getUnixShellEnvironment#shell', systemShellUnix);
const child = spawn(systemShellUnix, ['-ilc', command], {
detached: true,
stdio: ['ignore', 'pipe', process.stderr],

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
/**
* Returns the user data path to use with some rules:
* - respect portable mode
* - respect --user-data-dir CLI argument
* - respect VSCODE_APPDATA environment variable
*/
export function getUserDataPath(args: NativeParsedArgs): string;

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path="../../../../typings/require.d.ts" />
//@ts-check
(function () {
'use strict';
/**
* @param {typeof import('path')} path
* @param {typeof import('os')} os
* @param {string} productName
*/
function factory(path, os, productName) {
/**
* @param {import('../../environment/common/argv').NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function getUserDataPath(cliArgs) {
return path.resolve(doGetUserDataPath(cliArgs));
}
/**
* @param {import('../../environment/common/argv').NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function doGetUserDataPath(cliArgs) {
// 1. Support portable mode
const portablePath = process.env['VSCODE_PORTABLE'];
if (portablePath) {
return path.join(portablePath, 'user-data');
}
// 2. Support explicit --user-data-dir
const cliPath = cliArgs['user-data-dir'];
if (cliPath) {
return cliPath;
}
// 3. Support global VSCODE_APPDATA environment variable
let appDataPath = process.env['VSCODE_APPDATA'];
// 4. Otherwise check per platform
if (!appDataPath) {
switch (process.platform) {
case 'win32':
appDataPath = process.env['APPDATA'];
if (!appDataPath) {
const userProfile = process.env['USERPROFILE'];
if (typeof userProfile !== 'string') {
throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable');
}
appDataPath = path.join(userProfile, 'AppData', 'Roaming');
}
break;
case 'darwin':
appDataPath = path.join(os.homedir(), 'Library', 'Application Support');
break;
case 'linux':
appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
break;
default:
throw new Error('Platform not supported');
}
}
return path.join(appDataPath, productName);
}
return {
getUserDataPath
};
}
if (typeof define === 'function') {
define(['require', 'path', 'os', 'vs/base/common/network', 'vs/base/common/resources'], function (require, /** @type {typeof import('path')} */ path, /** @type {typeof import('os')} */ os, /** @type {typeof import('../../../base/common/network')} */ network, /** @type {typeof import("../../../base/common/resources")} */ resources) {
const rootPath = resources.dirname(network.FileAccess.asFileUri('', require));
const pkg = require.__$__nodeRequire(resources.joinPath(rootPath, 'package.json').fsPath);
return factory(path, os, pkg.name);
}); // amd
} else if (typeof module === 'object' && typeof module.exports === 'object') {
const pkg = require('../../../../../package.json');
const path = require('path');
const os = require('os');
module.exports = factory(path, os, pkg.name); // commonjs
} else {
throw new Error('Unknown context');
}
}());

View File

@@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'vs/base/common/path';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { parseExtensionHostPort, parseUserDataDir } from 'vs/platform/environment/node/environmentService';
import { parseExtensionHostPort } from 'vs/platform/environment/common/environmentService';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import product from 'vs/platform/product/common/product';
suite('EnvironmentService', () => {
@@ -44,15 +45,6 @@ suite('EnvironmentService', () => {
assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('userDataPath', () => {
const parse = (a: string[], b: { cwd: () => string, env: { [key: string]: string } }) => parseUserDataDir(parseArgs(a, OPTIONS), <any>b);
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: {} }), path.resolve('/foo/dir'),
'should use cwd when --user-data-dir is specified');
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: { 'VSCODE_CWD': '/bar' } }), path.resolve('/bar/dir'),
'should use VSCODE_CWD as the cwd when --user-data-dir is specified');
});
// https://github.com/microsoft/vscode/issues/78440
test('careful with boolean file names', function () {
let actual = parseArgs(['-r', 'arg.txt'], OPTIONS);
@@ -63,4 +55,15 @@ suite('EnvironmentService', () => {
assert(actual['reuse-window']);
assert.deepStrictEqual(actual._, ['true.txt']);
});
test('userDataDir', () => {
const service1 = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product });
assert.ok(service1.userDataPath.length > 0);
const args = parseArgs(process.argv, OPTIONS);
args['user-data-dir'] = '/userDataDir/folder';
const service2 = new NativeEnvironmentService(args, { _serviceBrand: undefined, ...product });
assert.notStrictEqual(service1.userDataPath, service2.userDataPath);
});
});

View File

@@ -37,9 +37,9 @@ suite('Native Modules (all platforms)', () => {
assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog'));
});
test('vscode-nsfw', async () => {
const nsfWatcher = await import('vscode-nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw'));
test('nsfw', async () => {
const nsfWatcher = await import('nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('nsfw'));
});
test('vscode-sqlite3', async () => {

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* 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 { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
import { getUserDataPath } from 'vs/platform/environment/node/userDataPath';
suite('User data path', () => {
test('getUserDataPath - default', () => {
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.length > 0);
});
test('getUserDataPath - portable mode', () => {
const origPortable = process.env['VSCODE_PORTABLE'];
try {
const portableDir = 'portable-dir';
process.env['VSCODE_PORTABLE'] = portableDir;
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.includes(portableDir));
} finally {
if (typeof origPortable === 'string') {
process.env['VSCODE_PORTABLE'] = origPortable;
} else {
delete process.env['VSCODE_PORTABLE'];
}
}
});
test('getUserDataPath - --user-data-dir', () => {
const cliUserDataDir = 'cli-data-dir';
const args = parseArgs(process.argv, OPTIONS);
args['user-data-dir'] = cliUserDataDir;
const path = getUserDataPath(args);
assert.ok(path.includes(cliUserDataDir));
});
test('getUserDataPath - VSCODE_APPDATA', () => {
const origAppData = process.env['VSCODE_APPDATA'];
try {
const appDataDir = 'appdata-dir';
process.env['VSCODE_APPDATA'] = appDataDir;
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.includes(appDataDir));
} finally {
if (typeof origAppData === 'string') {
process.env['VSCODE_APPDATA'] = origAppData;
} else {
delete process.env['VSCODE_APPDATA'];
}
}
});
});

View File

@@ -104,6 +104,7 @@ export interface ILocalExtension extends IExtension {
isMachineScoped: boolean;
publisherId: string | null;
publisherDisplayName: string | null;
installedTimestamp?: number;
}
export const enum SortBy {

View File

@@ -10,11 +10,10 @@ import { URI } from 'vs/base/common/uri';
import { gt } from 'vs/base/common/semver/semver';
import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { getBaseLabel } from 'vs/base/common/labels';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);
const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');
@@ -47,8 +46,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
constructor(
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ILocalizationsService private readonly localizationsService: ILocalizationsService
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService
) { }
protected get location(): string | undefined {
@@ -170,10 +168,6 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) {
await this.updateLocalizationsCache();
}
if (failed.length) {
throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));
}
@@ -315,10 +309,6 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
}
if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) {
await this.updateLocalizationsCache();
}
}
public async locateExtension(extensions: string[], output: CLIOutput = console): Promise<void> {
@@ -335,11 +325,6 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
});
}
private updateLocalizationsCache(): Promise<boolean> {
return this.localizationsService.update();
}
private notInstalled(id: string) {
return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);
}

View File

@@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri';
import { basename, join, } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { env } from 'vs/base/common/process';
import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
@@ -294,11 +294,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
const exePaths: string[] = [];
if (isWindows) {
if (extensionTip.windowsPath) {
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', process.env['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', process.env['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', process.env['ProgramFiles']!)
.replace('%APPDATA%', process.env['APPDATA']!)
.replace('%WINDIR%', process.env['WINDIR']!));
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', env['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', env['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', env['ProgramFiles']!)
.replace('%APPDATA%', env['APPDATA']!)
.replace('%WINDIR%', env['WINDIR']!));
}
} else {
exePaths.push(join('/usr/local/bin', exeName));

View File

@@ -16,6 +16,7 @@ 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';
import { getErrorMessage } from 'vs/base/common/errors';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
@@ -45,13 +46,26 @@ export class ExtensionsDownloader extends Disposable {
// Download only if vsix does not exist
if (!await this.fileService.exists(location)) {
// Download to temporary location first only if vsix does not exist
const tempLocation = joinPath(this.extensionsDownloadDir, `.${vsixName}`);
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
if (!await this.fileService.exists(tempLocation)) {
await this.extensionGalleryService.download(extension, tempLocation, operation);
}
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
try {
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
} catch (error) {
try {
await this.fileService.del(tempLocation);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path);
throw error;
}
}
}
return location;

View File

@@ -48,6 +48,8 @@ import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
import { IFileService } from 'vs/platform/files/common/files';
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
const INSTALL_ERROR_DOWNLOADING = 'downloading';
@@ -92,12 +94,19 @@ export class ExtensionManagementService extends Disposable implements IExtension
@optional(IDownloadService) private downloadService: IDownloadService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IFileService fileService: IFileService,
) {
super();
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService));
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
added.forEach(extension => this._onDidInstallExtension.fire({ identifier: extension.identifier, operation: InstallOperation.None, local: extension }));
removed.forEach(extension => this._onDidUninstallExtension.fire({ identifier: extension }));
}));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
@@ -309,7 +318,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) {
await this.setUninstalled(existingExtension);
await this.extensionsScanner.setUninstalled(existingExtension);
}
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
@@ -353,7 +362,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
}
await this.setUninstalled(extension);
await this.extensionsScanner.setUninstalled(extension);
try {
await this.extensionsScanner.removeUninstalledExtension(extension);
} catch (e) {
@@ -421,11 +430,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
await this.unsetUninstalled(identifierWithVersion);
const local = await this.extensionsScanner.setInstalled(identifierWithVersion);
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
const installed = await this.getInstalled(ExtensionType.User);
return installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
return local;
}
private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
@@ -645,7 +653,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
// Set all versions of the extension as uninstalled
promise = createCancelablePromise(async () => {
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
await this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
await this.extensionsScanner.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
});
this.uninstallingExtensions.set(local.identifier.id, promise);
promise.finally(() => this.uninstallingExtensions.delete(local.identifier.id));
@@ -683,28 +691,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
return uninstalled.length === 1;
}
private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
return this.extensionsScanner.withUninstalledExtensions(allUninstalled => {
const uninstalled: string[] = [];
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
uninstalled.push(identifier.key());
}
private async filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
const uninstalled: string[] = [];
const allUninstalled = await this.extensionsScanner.getUninstalledExtensions();
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
uninstalled.push(identifier.key());
}
return uninstalled;
});
}
private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
return this.extensionsScanner.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
return uninstalled;
});
}
private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise<void> {
return this.extensionsScanner.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
}
return uninstalled;
}
getExtensionsReport(): Promise<IReportedExtension[]> {

View File

@@ -24,6 +24,10 @@ import { isWindows } from 'vs/base/common/platform';
import { flatten } from 'vs/base/common/arrays';
import { IStringDictionary } from 'vs/base/common/collections';
import { FileAccess } from 'vs/base/common/network';
import { IFileService } from 'vs/platform/files/common/files';
import { basename } from 'vs/base/common/resources';
import { generateUuid } from 'vs/base/common/uuid';
import { getErrorMessage } from 'vs/base/common/errors';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
@@ -31,7 +35,8 @@ const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_RENAMING = 'renaming';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean }>;
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
@@ -44,6 +49,7 @@ export class ExtensionsScanner extends Disposable {
constructor(
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
@@ -97,7 +103,7 @@ export class ExtensionsScanner extends Disposable {
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsPath, folderName);
try {
@@ -110,20 +116,29 @@ export class ExtensionsScanner extends Disposable {
}
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User);
if (!local) {
throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath));
}
await this.storeMetadata(local, { installedTimestamp: Date.now() });
try {
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
try {
pfs.rimraf(tempPath);
await pfs.rimraf(tempPath);
} catch (e) { /* ignore */ }
throw error;
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
throw error;
}
}
let local: ILocalExtension | null = null;
try {
local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User);
local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User);
} catch (e) { /*ignore */ }
if (local) {
@@ -134,23 +149,46 @@ export class ExtensionsScanner extends Disposable {
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
return local;
}
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
// unset if false
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
metadata.isBuiltin = metadata.isBuiltin || undefined;
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await fs.promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = metadata;
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
return this.withUninstalledExtensions(uninstalled => uninstalled);
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
return this.withUninstalledExtensions();
}
async withUninstalledExtensions<T>(fn: (uninstalled: IStringDictionary<boolean>) => T): Promise<T> {
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
await this.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
});
}
async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]);
const installed = await this.scanExtensions(ExtensionType.User);
const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
if (!localExtension) {
return null;
}
await this.storeMetadata(localExtension, { installedTimestamp: Date.now() });
return this.scanExtension(localExtension.location, ExtensionType.User);
}
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
@@ -168,15 +206,16 @@ export class ExtensionsScanner extends Disposable {
} catch (e) { /* ignore */ }
}
const result = fn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
if (updateFn) {
updateFn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
}
}
return result;
return uninstalled;
});
}
@@ -237,6 +276,7 @@ export class ExtensionsScanner extends Disposable {
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
<<<<<<< HEAD
const extensionsFolders = await pfs.readdir(dir)
.catch((error) => {
if (error.code !== 'ENOENT') {
@@ -246,30 +286,43 @@ export class ExtensionsScanner extends Disposable {
});
const extensions = await Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type))));
return extensions.filter(e => e && e.identifier);
=======
const stat = await this.fileService.resolve(URI.file(dir));
if (stat.children) {
const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory)
.map(c => limiter.queue(async () => {
if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
return this.scanExtension(c.resource, type);
})));
return extensions.filter(e => e && e.identifier);
}
return [];
>>>>>>> 801aed93200dc0ccf325a09089c911e8e2b612d0
}
private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
const extensionPath = path.join(root, folderName);
private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise<ILocalExtension | null> {
try {
const children = await pfs.readdir(extensionPath);
const { manifest, metadata } = await this.readManifest(extensionPath);
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : undefined;
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : undefined;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
const stat = await this.fileService.resolve(extensionLocation);
if (stat.children) {
const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath);
const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
local.installedTimestamp = metadata.installedTimestamp;
}
return local;
}
return local;
} catch (e) {
this.logService.trace(e);
return null;
if (type !== ExtensionType.System) {
this.logService.trace(e);
}
}
return null;
}
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
@@ -350,7 +403,7 @@ export class ExtensionsScanner extends Disposable {
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
const promises = [
fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Emitter, Event } from 'vs/base/common/event';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { FileChangeType, FileSystemProviderCapabilities, IFileChange, IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtUri } from 'vs/base/common/resources';
import { ILogService } from 'vs/platform/log/common/log';
export class ExtensionsWatcher extends Disposable {
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[], removed: IExtensionIdentifier[] }>());
readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
private startTimestamp = 0;
private installingExtensions: IExtensionIdentifier[] = [];
private installedExtensions: IExtensionIdentifier[] | undefined;
constructor(
private readonly extensionsManagementService: IExtensionManagementService,
@IFileService fileService: IFileService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => {
this.installedExtensions = extensions.map(e => e.identifier);
this.startTimestamp = Date.now();
});
this._register(extensionsManagementService.onInstallExtension(e => this.onInstallExtension(e)));
this._register(extensionsManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
const extensionsResource = URI.file(environmentService.extensionsPath);
const extUri = new ExtUri(resource => !fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive));
this._register(fileService.watch(extensionsResource));
this._register(Event.filter(fileService.onDidFilesChange, e => e.changes.some(change => this.doesChangeAffects(change, extensionsResource, extUri)))(() => this.onDidChange()));
}
private doesChangeAffects(change: IFileChange, extensionsResource: URI, extUri: ExtUri): boolean {
// Is not immediate child of extensions resource
if (!extUri.isEqual(extUri.dirname(change.resource), extensionsResource)) {
return false;
}
// .obsolete file changed
if (extUri.isEqual(change.resource, extUri.joinPath(extensionsResource, '.obsolete'))) {
return true;
}
// Only interested in added/deleted changes
if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) {
return false;
}
// Ingore changes to files starting with `.`
if (extUri.basename(change.resource).startsWith('.')) {
return false;
}
return true;
}
private onInstallExtension(e: InstallExtensionEvent): void {
this.addInstallingExtension(e.identifier);
}
private onDidInstallExtension(e: DidInstallExtensionEvent): void {
this.removeInstallingExtension(e.identifier);
if (!e.error) {
this.addInstalledExtension(e.identifier);
}
}
private onDidUninstallExtension(e: DidUninstallExtensionEvent): void {
if (!e.error) {
this.removeInstalledExtension(e.identifier);
}
}
private addInstallingExtension(extension: IExtensionIdentifier) {
this.removeInstallingExtension(extension);
this.installingExtensions.push(extension);
}
private removeInstallingExtension(identifier: IExtensionIdentifier) {
this.installingExtensions = this.installingExtensions.filter(e => !areSameExtensions(e, identifier));
}
private addInstalledExtension(extension: IExtensionIdentifier): void {
if (this.installedExtensions) {
this.removeInstalledExtension(extension);
this.installedExtensions.push(extension);
}
}
private removeInstalledExtension(identifier: IExtensionIdentifier): void {
if (this.installedExtensions) {
this.installedExtensions = this.installedExtensions.filter(e => !areSameExtensions(e, identifier));
}
}
private async onDidChange(): Promise<void> {
if (this.installedExtensions) {
const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User);
const added = extensions.filter(e => {
if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) {
return false;
}
if (e.installedTimestamp && e.installedTimestamp > this.startTimestamp) {
this.logService.info('Detected extension installed from another source', e.identifier.id);
return true;
} else {
this.logService.info('Ignored extension installed by another source because of invalid timestamp', e.identifier.id);
return false;
}
});
const removed = this.installedExtensions.filter(identifier => {
// Extension being installed
if (this.installingExtensions.some(installingExtension => areSameExtensions(installingExtension, identifier))) {
return false;
}
if (extensions.every(e => !areSameExtensions(e.identifier, identifier))) {
this.logService.info('Detected extension removed from another source', identifier.id);
return true;
}
return false;
});
this.installedExtensions = extensions.map(e => e.identifier);
if (added.length || removed.length) {
this._onDidChangeExtensionsByAnotherSource.fire({ added, removed });
}
}
}
}

View File

@@ -113,12 +113,24 @@ export interface IAuthenticationContribution {
readonly label: string;
}
export interface IGettingStartedContent {
export interface IWalkthroughTask {
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 button:
| { title: string, link: string, command?: never }
| { title: string, command: string, link?: never },
readonly media: { path: string, altText: string },
readonly doneOn?: { command: string };
readonly when?: string;
}
export interface IWalkthrough {
readonly id: string,
readonly title: string;
readonly description: string;
readonly tasks: IWalkthroughTask[];
readonly primary?: boolean;
readonly when?: string;
}
@@ -134,6 +146,7 @@ export interface IExtensionContributions {
snippets?: ISnippet[];
themes?: ITheme[];
iconThemes?: ITheme[];
productIconThemes?: ITheme[];
viewsContainers?: { [location: string]: IViewContainer[] };
views?: { [location: string]: IView[] };
colors?: IColor[];
@@ -141,12 +154,12 @@ export interface IExtensionContributions {
readonly customEditors?: readonly IWebviewEditor[];
readonly codeActions?: readonly ICodeActionContribution[];
authentication?: IAuthenticationContribution[];
gettingStarted?: IGettingStartedContent[];
walkthroughs?: IWalkthrough[];
}
export type ExtensionKind = 'ui' | 'workspace' | 'web';
export type ExtensionWorkspaceTrustRequirement = false | 'onStart' | 'onDemand';
export type ExtensionWorkspaceTrust = { required: ExtensionWorkspaceTrustRequirement, description?: string };
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing
@@ -165,6 +178,7 @@ export const EXTENSION_CATEGORIES = [
'Data Science',
'Debuggers',
'Extension Packs',
'Education',
'Formatters',
'Keymaps',
'Language Packs',
@@ -185,7 +199,7 @@ export interface IExtensionManifest {
readonly displayName?: string;
readonly publisher: string;
readonly version: string;
readonly engines: { vscode: string };
readonly engines: { readonly vscode: string };
readonly description?: string;
readonly main?: string;
readonly browser?: string;
@@ -202,7 +216,7 @@ export interface IExtensionManifest {
readonly enableProposedApi?: boolean;
readonly api?: string;
readonly scripts?: { [key: string]: string; };
readonly requiresWorkspaceTrust?: ExtensionWorkspaceTrustRequirement;
readonly workspaceTrust?: ExtensionWorkspaceTrust;
}
export const enum ExtensionType {

View File

@@ -6,16 +6,16 @@
import { localize } from 'vs/nls';
import { mark } from 'vs/base/common/performance';
import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
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, listenStream, consumeStream } from 'vs/base/common/stream';
import { Promises, Queue } from 'vs/base/common/async';
import { Promises, ResourceQueue } 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';
@@ -79,9 +79,7 @@ export class FileService extends Disposable implements IFileService {
this._onWillActivateFileSystemProvider.fire({
scheme,
join(promise) {
if (promise) {
joiners.push(promise);
}
joiners.push(promise);
},
});
@@ -364,12 +362,12 @@ export class FileService extends Disposable implements IFileService {
// write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream);
}
// write file: buffered
else {
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@@ -379,6 +377,14 @@ export class FileService extends Disposable implements IFileService {
}
private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise<IStat | undefined> {
// Validate unlock support
const unlock = !!options?.unlock;
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
// Validate via file stat meta data
let stat: IStat | undefined = undefined;
try {
stat = await provider.stat(resource);
@@ -386,7 +392,7 @@ export class FileService extends Disposable implements IFileService {
return undefined; // file might not exist
}
// file cannot be directory
// File cannot be directory
if ((stat.type & FileType.Directory) !== 0) {
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
@@ -417,7 +423,28 @@ export class FileService extends Disposable implements IFileService {
async readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const provider = await this.withReadProvider(resource);
const stream = await this.doReadAsFileStream(provider, resource, {
if (options?.atomic) {
return this.doReadFileAtomic(provider, resource, options);
}
return this.doReadFile(provider, resource, options);
}
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
return new Promise<IFileContent>((resolve, reject) => {
this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
try {
const content = await this.doReadFile(provider, resource, options);
resolve(content);
} catch (error) {
reject(error);
}
});
});
}
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const stream = await this.doReadFileStream(provider, resource, {
...options,
// optimization: since we know that the caller does not
// care about buffering, we indicate this to the reader.
@@ -433,13 +460,13 @@ export class FileService extends Disposable implements IFileService {
};
}
async readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent> {
async readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent> {
const provider = await this.withReadProvider(resource);
return this.doReadAsFileStream(provider, resource, options);
return this.doReadFileStream(provider, resource, options);
}
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileStreamOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
@@ -499,7 +526,7 @@ export class FileService extends Disposable implements IFileService {
}
}
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const fileStream = provider.readFileStream(resource, options, token);
return transform(fileStream, {
@@ -508,7 +535,7 @@ export class FileService extends Disposable implements IFileService {
}, data => VSBuffer.concat(data));
}
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const stream = newWriteableBufferStream();
readFileIntoStream(provider, resource, stream, data => data, {
@@ -520,7 +547,7 @@ export class FileService extends Disposable implements IFileService {
return stream;
}
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileStreamOptions): VSBufferReadableStream {
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
// Read the file into the stream async but do not wait for
@@ -553,7 +580,7 @@ export class FileService extends Disposable implements IFileService {
return stream;
}
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
const stat = await this.resolve(resource, { resolveMetadata: true });
// Throw if resource is a directory
@@ -572,7 +599,7 @@ export class FileService extends Disposable implements IFileService {
return stat;
}
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileOptions): void {
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
if (options?.limits) {
let tooLargeErrorResult: FileOperationResult | undefined = undefined;
@@ -861,7 +888,7 @@ export class FileService extends Disposable implements IFileService {
}
}
async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<Error | true> {
async canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true> {
try {
await this.doValidateDelete(resource, options);
} catch (error) {
@@ -871,7 +898,7 @@ export class FileService extends Disposable implements IFileService {
return true;
}
private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<IFileSystemProvider> {
private async doValidateDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<IFileSystemProvider> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
// Validate trash support
@@ -898,7 +925,7 @@ export class FileService extends Disposable implements IFileService {
return provider;
}
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
async del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void> {
const provider = await this.doValidateDelete(resource, options);
const useTrash = !!options?.useTrash;
@@ -984,35 +1011,13 @@ export class FileService extends Disposable implements IFileService {
//#region Helpers
private readonly writeQueues: Map<string, Queue<void>> = new Map();
private readonly writeQueue = this._register(new ResourceQueue());
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
const { providerExtUri } = this.getExtUri(provider);
const queueKey = providerExtUri.getComparisonKey(resource);
// ensure to never write to the same resource without finishing
// the one write. this ensures a write finishes consistently
// (even with error) before another write is done.
let writeQueue = this.writeQueues.get(queueKey);
if (!writeQueue) {
writeQueue = new Queue<void>();
this.writeQueues.set(queueKey, writeQueue);
const onFinish = Event.once(writeQueue.onFinished);
onFinish(() => {
this.writeQueues.delete(queueKey);
dispose(writeQueue);
});
}
return writeQueue;
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(async () => {
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
// open handle
const handle = await provider.open(resource, { create: true });
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
// write into handle until all bytes from buffer have been written
try {
@@ -1107,11 +1112,11 @@ export class FileService extends Disposable implements IFileService {
}
}
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStreamOrBufferedStream));
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream));
}
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
let buffer: VSBuffer;
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
buffer = bufferOrReadableOrStreamOrBufferedStream;
@@ -1124,11 +1129,11 @@ export class FileService extends Disposable implements IFileService {
}
// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@@ -1139,7 +1144,7 @@ export class FileService extends Disposable implements IFileService {
// Open handles
sourceHandle = await sourceProvider.open(source, { create: false });
targetHandle = await targetProvider.open(target, { create: true });
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
@@ -1174,21 +1179,21 @@ export class FileService extends Disposable implements IFileService {
}
private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true });
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
// Open handle
const targetHandle = await targetProvider.open(target, { create: true });
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
// Read entire buffer from source and write buffered
try {
@@ -1207,7 +1212,7 @@ export class FileService extends Disposable implements IFileService {
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
// Write buffer into target at once
await this.doWriteUnbuffered(targetProvider, target, buffer);
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
}
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {

View File

@@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import * as glob from 'vs/base/common/glob';
import { IExpression } from 'vs/base/common/glob';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { startsWithIgnoreCase } from 'vs/base/common/strings';
@@ -17,6 +17,8 @@ import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { TernarySearchTree } from 'vs/base/common/map';
//#region file service & providers
export const IFileService = createDecorator<IFileService>('fileService');
export interface IFileService {
@@ -112,7 +114,7 @@ export interface IFileService {
/**
* Read the contents of the provided resource buffered as stream.
*/
readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent>;
readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent>;
/**
* Updates the content replacing its previous value.
@@ -170,13 +172,13 @@ export interface IFileService {
* move the file to trash. The optional recursive parameter allows to delete
* non-empty folders recursively.
*/
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void>;
/**
* Find out if a delete operation is possible given the arguments. No changes on disk will
* be performed. Returns an Error if the operation cannot be done.
*/
canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<Error | true>;
canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true>;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
@@ -192,9 +194,24 @@ export interface IFileService {
}
export interface FileOverwriteOptions {
/**
* Set to `true` to overwrite a file if it exists. Will
* throw an error otherwise if the file does exist.
*/
overwrite: boolean;
}
export interface FileUnlockOptions {
/**
* Set to `true` to try to remove any write locks the file might
* have. A file that is write locked will throw an error for any
* attempt to write to unless `unlock: true` is provided.
*/
unlock: boolean;
}
export interface FileReadStreamOptions {
/**
@@ -218,28 +235,86 @@ export interface FileReadStreamOptions {
};
}
export interface FileWriteOptions {
overwrite: boolean;
export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOptions {
/**
* Set to `true` to create a file when it does not exist. Will
* throw an error otherwise if the file does not exist.
*/
create: boolean;
}
export interface FileOpenOptions {
create: boolean;
export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions;
export function isFileOpenForWriteOptions(options: FileOpenOptions): options is FileOpenForWriteOptions {
return options.create === true;
}
export interface FileOpenForReadOptions {
/**
* A hint that the file should be opened for reading only.
*/
create: false;
}
export interface FileOpenForWriteOptions extends FileUnlockOptions {
/**
* A hint that the file should be opened for reading and writing.
*/
create: true;
}
export interface FileDeleteOptions {
/**
* Set to `true` to recursively delete any children of the file. This
* only applies to folders and can lead to an error unless provided
* if the folder is not empty.
*/
recursive: boolean;
/**
* Set to `true` to attempt to move the file to trash
* instead of deleting it permanently from disk. This
* option maybe not be supported on all providers.
*/
useTrash: boolean;
}
export enum FileType {
/**
* File is unknown (neither file, directory nor symbolic link).
*/
Unknown = 0,
/**
* File is a normal file.
*/
File = 1,
/**
* File is a directory.
*/
Directory = 2,
/**
* File is a symbolic link.
*
* Note: even when the file is a symbolic link, you can test for
* `FileType.File` and `FileType.Directory` to know the type of
* the target the link points to.
*/
SymbolicLink = 64
}
export interface IStat {
/**
* The file type.
*/
type: FileType;
/**
@@ -252,25 +327,67 @@ export interface IStat {
*/
ctime: number;
/**
* The size of the file in bytes.
*/
size: number;
}
export interface IWatchOptions {
/**
* Set to `true` to watch for changes recursively in a folder
* and all of its children.
*/
recursive: boolean;
/**
* A set of paths to exclude from watching.
*/
excludes: string[];
}
export const enum FileSystemProviderCapabilities {
/**
* Provider supports unbuffered read/write.
*/
FileReadWrite = 1 << 1,
/**
* Provider supports open/read/write/close low level file operations.
*/
FileOpenReadWriteClose = 1 << 2,
/**
* Provider supports stream based reading.
*/
FileReadStream = 1 << 4,
/**
* Provider supports copy operation.
*/
FileFolderCopy = 1 << 3,
/**
* Provider is path case sensitive.
*/
PathCaseSensitive = 1 << 10,
/**
* All files of the provider are readonly.
*/
Readonly = 1 << 11,
Trash = 1 << 12
/**
* Provider supports to delete via trash.
*/
Trash = 1 << 12,
/**
* Provider support to unlock files for writing.
*/
FileWriteUnlock = 1 << 13
}
export interface IFileSystemProvider {
@@ -345,6 +462,7 @@ export enum FileSystemProviderErrorCode {
FileIsADirectory = 'EntryIsADirectory',
FileExceedsMemoryLimit = 'EntryExceedsMemoryLimit',
FileTooLarge = 'EntryTooLarge',
FileWriteLocked = 'EntryWriteLocked',
NoPermissions = 'NoPermissions',
Unavailable = 'Unavailable',
Unknown = 'Unknown'
@@ -404,6 +522,7 @@ export function toFileSystemProviderErrorCode(error: Error | undefined | null):
case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
case FileSystemProviderErrorCode.FileExceedsMemoryLimit: return FileSystemProviderErrorCode.FileExceedsMemoryLimit;
case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
}
@@ -426,6 +545,8 @@ export function toFileOperationResult(error: Error): FileOperationResult {
return FileOperationResult.FILE_IS_DIRECTORY;
case FileSystemProviderErrorCode.FileNotADirectory:
return FileOperationResult.FILE_NOT_DIRECTORY;
case FileSystemProviderErrorCode.FileWriteLocked:
return FileOperationResult.FILE_WRITE_LOCKED;
case FileSystemProviderErrorCode.NoPermissions:
return FileOperationResult.FILE_PERMISSION_DENIED;
case FileSystemProviderErrorCode.FileExists:
@@ -479,9 +600,9 @@ export class FileOperationEvent {
* Possible changes that can occur to a file.
*/
export const enum FileChangeType {
UPDATED = 0,
ADDED = 1,
DELETED = 2
UPDATED,
ADDED,
DELETED
}
/**
@@ -744,12 +865,7 @@ interface IBaseStat {
etag?: string;
}
export interface IBaseStatWithMetadata extends IBaseStat {
mtime: number;
ctime: number;
etag: string;
size: number;
}
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
/**
* A file resource with meta information.
@@ -767,7 +883,10 @@ export interface IFileStat extends IBaseStat {
isDirectory: boolean;
/**
* The resource is a symbolic link.
* The resource is a symbolic link. Note: even when the
* file is a symbolic link, you can test for `FileType.File`
* and `FileType.Directory` to know the type of the target
* the link points to.
*/
isSymbolicLink: boolean;
@@ -810,7 +929,7 @@ export interface IFileStreamContent extends IBaseStatWithMetadata {
value: VSBufferReadableStream;
}
export interface IReadFileOptions extends FileReadStreamOptions {
export interface IBaseReadFileOptions extends FileReadStreamOptions {
/**
* The optional etag parameter allows to return early from resolving the resource if
@@ -821,6 +940,28 @@ export interface IReadFileOptions extends FileReadStreamOptions {
readonly etag?: string;
}
export interface IReadFileStreamOptions extends IBaseReadFileOptions { }
export interface IReadFileOptions extends IBaseReadFileOptions {
/**
* The optional `atomic` flag can be used to make sure
* the `readFile` method is not running in parallel with
* any `write` operations in the same process.
*
* Typically you should not need to use this flag but if
* for example you are quickly reading a file right after
* a file event occured and the file changes a lot, there
* is a chance that a read returns an empty or partial file
* because a pending write has not finished yet.
*
* Note: this does not prevent the file from being written
* to from a different process. If you need such atomic
* operations, you better use a real database as storage.
*/
readonly atomic?: boolean;
}
export interface IWriteFileOptions {
/**
@@ -832,6 +973,11 @@ export interface IWriteFileOptions {
* The etag of the file. This can be used to prevent dirty writes.
*/
readonly etag?: string;
/**
* Whether to attempt to unlock a file before writing.
*/
readonly unlock?: boolean;
}
export interface IResolveFileOptions {
@@ -883,7 +1029,7 @@ export const enum FileOperationResult {
FILE_NOT_MODIFIED_SINCE,
FILE_MODIFIED_SINCE,
FILE_MOVE_CONFLICT,
FILE_READ_ONLY,
FILE_WRITE_LOCKED,
FILE_PERMISSION_DENIED,
FILE_TOO_LARGE,
FILE_INVALID_PATH,
@@ -892,6 +1038,10 @@ export const enum FileOperationResult {
FILE_OTHER_ERROR
}
//#endregion
//#region Settings
export const AutoSaveConfiguration = {
OFF: 'off',
AFTER_DELAY: 'afterDelay',
@@ -911,7 +1061,7 @@ export const FILES_EXCLUDE_CONFIG = 'files.exclude';
export interface IFilesConfiguration {
files: {
associations: { [filepattern: string]: string };
exclude: glob.IExpression;
exclude: IExpression;
watcherExclude: { [filepattern: string]: boolean };
encoding: string;
autoGuessEncoding: boolean;
@@ -926,6 +1076,10 @@ export interface IFilesConfiguration {
};
}
//#endregion
//#region Utilities
export enum FileKind {
FILE,
FOLDER,
@@ -1001,3 +1155,24 @@ export class ByteSize {
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
}
}
// Native only: Arch limits
export interface IArchLimits {
maxFileSize: number;
maxHeapSize: number;
}
export const enum Arch {
IA32,
OTHER
}
export function getPlatformLimits(arch: Arch): IArchLimits {
return {
maxFileSize: arch === Arch.IA32 ? 300 * ByteSize.MB : 16 * ByteSize.GB, // https://github.com/microsoft/vscode/issues/30180
maxHeapSize: arch === Arch.IA32 ? 700 * ByteSize.MB : 2 * 700 * ByteSize.MB, // https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
};
}
//#endregion

View File

@@ -46,7 +46,11 @@ export async function readFileIntoStream<T>(
error = options.errorTransformer(error);
}
target.end(error);
if (typeof error !== 'undefined') {
target.error(error);
}
target.end();
}
}

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, IFileSystemProviderWithFileReadWriteCapability, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, FileReadStreamOptions, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { toErrorMessage } from 'vs/base/common/errorMessage';
interface IFileChangeDto {
resource: UriComponents;
type: FileChangeType;
}
/**
* An abstract file system provider that delegates all calls to a provided
* `IChannel` via IPC communication.
*/
export abstract class IPCFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {
private readonly session: string = generateUuid();
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChange.event;
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
private _capabilities = FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.FileOpenReadWriteClose
| FileSystemProviderCapabilities.FileReadStream
| FileSystemProviderCapabilities.FileFolderCopy
| FileSystemProviderCapabilities.FileWriteUnlock;
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
constructor(private readonly channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(eventsOrError => {
if (Array.isArray(eventsOrError)) {
const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
} else {
const error = eventsOrError;
this._onDidWatchErrorOccur.fire(error);
}
}));
}
protected setCaseSensitive(isCaseSensitive: boolean) {
if (isCaseSensitive) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
} else {
this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive;
}
this._onDidChangeCapabilities.fire(undefined);
}
// --- forwarding calls
stat(resource: URI): Promise<IStat> {
return this.channel.call('stat', [resource]);
}
open(resource: URI, opts: FileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}
close(fd: number): Promise<void> {
return this.channel.call('close', [fd]);
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
// copy back the data that was written into the buffer on the remote
// side. we need to do this because buffers are not referenced by
// pointer, but only by value and as such cannot be directly written
// to from the other process.
data.set(bytes.buffer.slice(0, bytesRead), offset);
return bytesRead;
}
async readFile(resource: URI): Promise<Uint8Array> {
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
return buff.buffer;
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
// Reading as file stream goes through an event to the remote side
const listener = this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
// data
if (dataOrErrorOrEnd instanceof VSBuffer) {
stream.write(dataOrErrorOrEnd.buffer);
}
// end or error
else {
if (dataOrErrorOrEnd === 'end') {
stream.end();
} else {
// Since we receive data through a IPC channel, it is likely
// that the error was not serialized, or only partially. To
// ensure our API use is correct, we convert the data to an
// error here to forward it properly.
let error = dataOrErrorOrEnd;
if (!(error instanceof Error)) {
error = new Error(toErrorMessage(error));
}
stream.error(error);
stream.end();
}
// Signal to the remote side that we no longer listen
listener.dispose();
}
});
// Support cancellation
token.onCancellationRequested(() => {
// Ensure to end the stream properly with an error
// to indicate the cancellation.
stream.error(canceled());
stream.end();
// Ensure to dispose the listener upon cancellation. This will
// bubble through the remote side as event and allows to stop
// reading the file.
listener.dispose();
});
return stream;
}
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.channel.call('delete', [resource, opts]);
}
mkdir(resource: URI): Promise<void> {
return this.channel.call('mkdir', [resource]);
}
readdir(resource: URI): Promise<[string, FileType][]> {
return this.channel.call('readdir', [resource]);
}
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('rename', [resource, target, opts]);
}
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('copy', [resource, target, opts]);
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
const req = Math.random();
this.channel.call('watch', [this.session, req, resource, opts]);
return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
}
}

View File

@@ -6,7 +6,7 @@
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 { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } 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';
@@ -64,7 +64,8 @@ export class DiskFileSystemProvider extends Disposable implements
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
@@ -188,12 +189,12 @@ export class DiskFileSystemProvider extends Disposable implements
}
// Open
handle = await this.open(resource, { create: true });
handle = await this.open(resource, { create: true, unlock: opts.unlock });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(resource, error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
@@ -203,15 +204,28 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private readonly writeHandles = new Map<number, URI>();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
// Determine wether to unlock the file (write only)
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
try {
const { stat } = await SymlinkSupport.stat(filePath);
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
await promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
}
}
// Determine file flags for opening (read vs write)
let flags: string | undefined = undefined;
if (opts.create) {
if (isFileOpenForWriteOptions(opts)) {
if (isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
@@ -252,13 +266,17 @@ export class DiskFileSystemProvider extends Disposable implements
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
if (isFileOpenForWriteOptions(opts)) {
this.writeHandles.set(handle, resource);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
if (isFileOpenForWriteOptions(opts)) {
throw await this.toFileSystemProviderWriteError(resource, error);
} else {
throw this.toFileSystemProviderError(error);
}
}
}
@@ -388,7 +406,7 @@ export class DiskFileSystemProvider extends Disposable implements
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
@@ -690,6 +708,26 @@ export class DiskFileSystemProvider extends Disposable implements
return createFileSystemProviderError(error, code);
}
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
// If the write error signals permission issues, we try
// to read the file's mode to see if the file is write
// locked.
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
try {
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
}
} catch (error) {
this.logService.trace(error); // ignore - return original error
}
}
return fileSystemProviderWriteError;
}
//#endregion
dispose(): void {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nsfw from 'vscode-nsfw';
import * as nsfw from 'nsfw';
import * as glob from 'vs/base/common/glob';
import { join } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
@@ -247,6 +247,6 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message });
}
}

View File

@@ -10,7 +10,7 @@ import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
suite('NSFW Watcher Service', async () => {
// Load `nsfwWatcherService` within the suite to prevent all tests
// from failing to start if `vscode-nsfw` was not properly installed
// from failing to start if `nsfw` was not properly installed
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
class TestNsfwWatcherService extends NsfwWatcherService {

View File

@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
disposables.add(userdataFileProvider);
};
setup(async () => {
setup(async function () {
this.timeout(15000);
await reload();
});

View File

@@ -8,13 +8,12 @@ import { tmpdir } from 'os';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { flakySuite, getRandomTestPath, getPathFromAmdModule } 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, rimrafSync } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
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 { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { DisposableStore } from 'vs/base/common/lifecycle';
@@ -66,6 +65,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.Trash |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
@@ -1181,8 +1181,14 @@ flakySuite('Disk File Service', function () {
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
});
async function testReadFile(resource: URI): Promise<void> {
const content = await service.readFile(resource);
test('readFile - atomic', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream);
return testReadFile(URI.file(join(testDir, 'lorem.txt')), { atomic: true });
});
async function testReadFile(resource: URI, options?: IReadFileOptions): Promise<void> {
const content = await service.readFile(resource, options);
assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString());
}
@@ -1754,19 +1760,23 @@ flakySuite('Disk File Service', function () {
assert.ok(error!);
}
test('writeFile (large file) - multiple parallel writes queue up', async () => {
test('writeFile (large file) - multiple parallel writes queue up and atomic read support', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const writePromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent));
assert.strictEqual(fileStat.name, 'lorem.txt');
}));
const fileContent = readFileSync(resource.fsPath).toString();
assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent));
const readPromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async () => {
const fileContent = await service.readFile(resource, { atomic: true });
assert.ok(fileContent.value.byteLength > 0); // `atomic: true` ensures we never read a truncated file
}));
await Promise.all([writePromises, readPromises]);
});
test('writeFile (readable) - default', async () => {
@@ -1889,6 +1899,63 @@ flakySuite('Disk File Service', function () {
assert.strictEqual(readFileSync(resource.fsPath).toString(), content);
});
test('writeFile - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile (stream) - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testLockedFiles(true);
});
test('writeFile (stream) - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testLockedFiles(true);
});
async function testLockedFiles(expectError: boolean) {
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
const stats = await promises.stat(lockedFile.fsPath);
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
let error;
const newContent = 'Updates to locked file';
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent));
} catch (e) {
error = e;
}
assert.ok(error);
error = undefined;
if (expectError) {
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
} catch (e) {
error = e;
}
assert.ok(error);
} else {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
}
}
test('writeFile (error when folder is encountered)', async () => {
const resource = URI.file(testDir);
@@ -2286,7 +2353,7 @@ flakySuite('Disk File Service', function () {
const resource = URI.file(join(testDir, 'lorem.txt'));
const buffer = VSBuffer.alloc(1024);
const fdWrite = await fileProvider.open(resource, { create: true });
const fdWrite = await fileProvider.open(resource, { create: true, unlock: false });
const fdRead = await fileProvider.open(resource, { create: false });
let posInFileWrite = 0;

View File

@@ -155,6 +155,10 @@ export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
return id;
}
export function refineServiceDecorator<T1, T extends T1>(serviceIdentifier: ServiceIdentifier<T1>): ServiceIdentifier<T> {
return <ServiceIdentifier<T>>serviceIdentifier;
}
/**
* Mark a service dependency as optional.
*/

View File

@@ -14,24 +14,37 @@ type Remote = { getChannel(channelName: string): IChannel; };
abstract class RemoteServiceStub<T> {
constructor(
channelName: string,
channelClientCtor: ChannelClientCtor<T> | undefined,
options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined,
remote: Remote
) {
const channel = remote.getChannel(channelName);
if (channelClientCtor) {
return new channelClientCtor(channel);
} else {
return ProxyChannel.toService(channel);
if (isRemoteServiceWithChannelClientOptions(options)) {
return new options.channelClientCtor(channel);
}
return ProxyChannel.toService(channel, options?.proxyOptions);
}
}
export interface IRemoteServiceOptions<T> {
readonly channelClientCtor?: ChannelClientCtor<T>;
export interface IBaseRemoteServiceOptions {
readonly supportsDelayedInstantiation?: boolean;
}
export interface IRemoteServiceWithChannelClientOptions<T> extends IBaseRemoteServiceOptions {
readonly channelClientCtor: ChannelClientCtor<T>;
}
export interface IRemoteServiceWithProxyOptions extends IBaseRemoteServiceOptions {
readonly proxyOptions?: ProxyChannel.ICreateProxyServiceOptions;
}
function isRemoteServiceWithChannelClientOptions<T>(obj: unknown): obj is IRemoteServiceWithChannelClientOptions<T> {
const candidate = obj as IRemoteServiceWithChannelClientOptions<T> | undefined;
return !!candidate?.channelClientCtor;
}
//#region Main Process
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
@@ -43,13 +56,13 @@ export interface IMainProcessService {
}
class MainProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, channelClientCtor: ChannelClientCtor<T> | undefined, @IMainProcessService ipcService: IMainProcessService) {
super(channelName, channelClientCtor, ipcService);
constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @IMainProcessService ipcService: IMainProcessService) {
super(channelName, options, ipcService);
}
}
export function registerMainProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options: IRemoteServiceOptions<T> = {}): void {
registerSingleton(id, new SyncDescriptor(MainProcessRemoteServiceStub, [channelName, options.channelClientCtor], options.supportsDelayedInstantiation));
export function registerMainProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options?: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions): void {
registerSingleton(id, new SyncDescriptor(MainProcessRemoteServiceStub, [channelName, options], options?.supportsDelayedInstantiation));
}
//#endregion
@@ -65,13 +78,13 @@ export interface ISharedProcessService {
}
class SharedProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, channelClientCtor: ChannelClientCtor<T> | undefined, @ISharedProcessService ipcService: ISharedProcessService) {
super(channelName, channelClientCtor, ipcService);
constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @ISharedProcessService ipcService: ISharedProcessService) {
super(channelName, options, ipcService);
}
}
export function registerSharedProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options: IRemoteServiceOptions<T> = {}): void {
registerSingleton(id, new SyncDescriptor(SharedProcessRemoteServiceStub, [channelName, options.channelClientCtor], options.supportsDelayedInstantiation));
export function registerSharedProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options?: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions): void {
registerSingleton(id, new SyncDescriptor(SharedProcessRemoteServiceStub, [channelName, options], options?.supportsDelayedInstantiation));
}
//#endregion

View File

@@ -40,7 +40,7 @@ export interface IssueReporterStyles extends WindowStyles {
export interface IssueReporterExtensionData {
name: string;
publisher: string;
publisher: string | undefined;
version: string;
id: string;
isTheme: boolean;
@@ -78,7 +78,7 @@ export interface ProcessExplorerStyles extends WindowStyles {
export interface ProcessExplorerData extends WindowData {
pid: number;
styles: ProcessExplorerStyles;
platform: 'win32' | 'darwin' | 'linux';
platform: string;
applicationName: string;
}

View File

@@ -5,7 +5,7 @@
import { localize } from 'vs/nls';
import * as os from 'os';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
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';
@@ -43,7 +43,8 @@ export class IssueMainService implements ICommonIssueService {
@ILogService private readonly logService: ILogService,
@IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService,
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService private readonly productService: IProductService
) {
this.registerListeners();
}
@@ -410,11 +411,12 @@ export class IssueMainService implements ICommonIssueService {
release: os.release(),
},
product: {
nameShort: product.nameShort,
version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version,
commit: product.commit,
date: product.date,
reportIssueUrl: product.reportIssueUrl
nameShort: this.productService.nameShort,
version: !!this.productService.darwinUniversalAssetId ? `${this.productService.version} (Universal)` : this.productService.version,
commit: this.productService.commit,
date: this.productService.date,
reportIssueUrl: this.productService.reportIssueUrl,
reportMarketplaceIssueUrl: this.productService.reportMarketplaceIssueUrl
}
};

View File

@@ -140,6 +140,8 @@ suite('AbstractKeybindingService', () => {
let notificationService: INotificationService = {
_serviceBrand: undefined,
onDidAddNotification: undefined!,
onDidRemoveNotification: undefined!,
notify: (notification: INotification) => {
showMessageCalls.push({ sev: notification.severity, message: notification.message });
return new NoOpNotification();

View File

@@ -11,7 +11,7 @@ export interface IKeyboardLayoutData {
keyboardMapping: IKeyboardMapping;
}
export interface IKeyboardLayoutMainService {
export interface INativeKeyboardLayoutService {
readonly _serviceBrand: undefined;
readonly onDidChangeKeyboardLayout: Event<IKeyboardLayoutData>;
getKeyboardLayoutData(): Promise<IKeyboardLayoutData>;

View File

@@ -5,16 +5,16 @@
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 { IKeyboardLayoutData, INativeKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayoutService';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
export const IKeyboardLayoutMainService = createDecorator<IKeyboardLayoutMainService>('keyboardLayoutMainService');
export interface IKeyboardLayoutMainService extends ICommonKeyboardLayoutMainService { }
export interface IKeyboardLayoutMainService extends INativeKeyboardLayoutService { }
export class KeyboardLayoutMainService extends Disposable implements ICommonKeyboardLayoutMainService {
export class KeyboardLayoutMainService extends Disposable implements INativeKeyboardLayoutService {
declare readonly _serviceBrand: undefined;

View File

@@ -119,10 +119,11 @@ export class LaunchMainService implements ILaunchMainService {
let usedWindows: ICodeWindow[] = [];
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
const remoteAuthority = args.remote || undefined;
// Special case extension development
if (!!args.extensionDevelopmentPath) {
this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv, waitMarkerFileURI });
this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, { context, cli: args, userEnv, waitMarkerFileURI, remoteAuthority });
}
// Start without file/folder arguments
@@ -163,7 +164,8 @@ export class LaunchMainService implements ILaunchMainService {
userEnv,
forceNewWindow: true,
forceEmpty: true,
waitMarkerFileURI
waitMarkerFileURI,
remoteAuthority
});
}
@@ -175,7 +177,7 @@ export class LaunchMainService implements ILaunchMainService {
usedWindows = [lastActive];
} else {
usedWindows = this.windowsMainService.open({ context, cli: args, forceEmpty: true });
usedWindows = this.windowsMainService.open({ context, cli: args, forceEmpty: true, remoteAuthority });
}
}
}
@@ -193,7 +195,8 @@ export class LaunchMainService implements ILaunchMainService {
addMode: args.add,
noRecentEntry: !!args['skip-add-to-recently-opened'],
waitMarkerFileURI,
gotoLineMode: args.goto
gotoLineMode: args.goto,
remoteAuthority
});
}

View File

@@ -16,6 +16,7 @@ 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';
import { cwd } from 'vs/base/common/process';
export const ILifecycleMainService = createDecorator<ILifecycleMainService>('lifecycleMainService');
@@ -289,9 +290,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
this._onWillShutdown.fire({
join(promise) {
if (promise) {
joiners.push(promise);
}
joiners.push(promise);
}
});
@@ -555,13 +554,13 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
// Windows: we are about to restart and as such we need to restore the original
// current working directory we had on startup to get the exact same startup
// behaviour. As such, we briefly change back to the VSCODE_CWD and then when
// behaviour. As such, we briefly change back to that directory and then when
// Code starts it will set it back to the installation directory again.
try {
if (isWindows) {
const vscodeCwd = process.env['VSCODE_CWD'];
if (vscodeCwd) {
process.chdir(vscodeCwd);
const currentWorkingDir = cwd();
if (currentWorkingDir !== process.cwd()) {
process.chdir(currentWorkingDir);
}
}
} catch (err) {

View File

@@ -109,6 +109,7 @@ export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListF
export const WorkbenchListHasSelectionOrFocus = new RawContextKey<boolean>('listHasSelectionOrFocus', false);
export const WorkbenchListDoubleSelection = new RawContextKey<boolean>('listDoubleSelection', false);
export const WorkbenchListMultiSelection = new RawContextKey<boolean>('listMultiSelection', false);
export const WorkbenchListSelectionNavigation = new RawContextKey<boolean>('listSelectionNavigation', false);
export const WorkbenchListSupportsKeyboardNavigation = new RawContextKey<boolean>('listSupportsKeyboardNavigation', true);
export const WorkbenchListAutomaticKeyboardNavigationKey = 'listAutomaticKeyboardNavigation';
export const WorkbenchListAutomaticKeyboardNavigation = new RawContextKey<boolean>(WorkbenchListAutomaticKeyboardNavigationKey, true);
@@ -191,7 +192,9 @@ export interface IWorkbenchListOptionsUpdate extends IListOptionsUpdate {
readonly overrideStyles?: IColorMapping;
}
export interface IWorkbenchListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IListOptions<T> { }
export interface IWorkbenchListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IListOptions<T> {
readonly selectionNavigation?: boolean;
}
export class WorkbenchList<T> extends List<T> {
@@ -238,6 +241,9 @@ export class WorkbenchList<T> extends List<T> {
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
const listSelectionNavigation = WorkbenchListSelectionNavigation.bindTo(this.contextKeyService);
listSelectionNavigation.set(Boolean(options.selectionNavigation));
this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);
@@ -315,7 +321,9 @@ export class WorkbenchList<T> extends List<T> {
}
}
export interface IWorkbenchPagedListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IPagedListOptions<T> { }
export interface IWorkbenchPagedListOptions<T> extends IWorkbenchListOptionsUpdate, IResourceNavigatorOptions, IPagedListOptions<T> {
readonly selectionNavigation?: boolean;
}
export class WorkbenchPagedList<T> extends PagedList<T> {
@@ -362,6 +370,9 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
const listSelectionNavigation = WorkbenchListSelectionNavigation.bindTo(this.contextKeyService);
listSelectionNavigation.set(Boolean(options.selectionNavigation));
this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService);
this.disposables.add(this.contextKeyService);
@@ -427,7 +438,9 @@ export interface IWorkbenchTableOptionsUpdate extends ITableOptionsUpdate {
readonly overrideStyles?: IColorMapping;
}
export interface IWorkbenchTableOptions<T> extends IWorkbenchTableOptionsUpdate, ITableOptions<T> { }
export interface IWorkbenchTableOptions<T> extends IWorkbenchTableOptionsUpdate, IResourceNavigatorOptions, ITableOptions<T> {
readonly selectionNavigation?: boolean;
}
export class WorkbenchTable<TRow> extends Table<TRow> {
@@ -477,6 +490,9 @@ export class WorkbenchTable<TRow> extends Table<TRow> {
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
const listSelectionNavigation = WorkbenchListSelectionNavigation.bindTo(this.contextKeyService);
listSelectionNavigation.set(Boolean(options.selectionNavigation));
this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);
@@ -562,10 +578,6 @@ export interface IOpenResourceOptions {
payload: any;
}
export interface IResourceResultsNavigationOptions {
openOnFocus: boolean;
}
export interface IOpenEvent<T> {
editorOptions: IEditorOptions;
sideBySide: boolean;
@@ -575,7 +587,6 @@ export interface IOpenEvent<T> {
export interface IResourceNavigatorOptions {
readonly configurationService?: IConfigurationService;
readonly openOnFocus?: boolean;
readonly openOnSingleClick?: boolean;
}
@@ -596,7 +607,6 @@ export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: b
abstract class ResourceNavigator<T> extends Disposable {
private readonly openOnFocus: boolean;
private openOnSingleClick: boolean;
private readonly _onDidOpen = this._register(new Emitter<IOpenEvent<T | undefined>>());
@@ -608,16 +618,10 @@ abstract class ResourceNavigator<T> extends Disposable {
) {
super();
this.openOnFocus = options?.openOnFocus ?? false;
this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e)));
this._register(this.widget.onPointer((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onPointer(e.element, e.browserEvent)));
this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onMouseDblClick(e.element, e.browserEvent)));
if (this.openOnFocus) {
this._register(Event.filter(this.widget.onDidChangeFocus, e => e.browserEvent instanceof KeyboardEvent)(e => this.onFocusFromKeyboard(e)));
}
if (typeof options?.openOnSingleClick !== 'boolean' && options?.configurationService) {
this.openOnSingleClick = options?.configurationService!.getValue(openModeSettingKey) !== 'doubleClick';
this._register(options?.configurationService.onDidChangeConfiguration(() => {
@@ -628,18 +632,6 @@ abstract class ResourceNavigator<T> extends Disposable {
}
}
private onFocusFromKeyboard(event: ITreeEvent<any>): void {
const focus = this.widget.getFocus();
this.widget.setSelection(focus, event.browserEvent);
const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent;
const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true;
const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus;
const sideBySide = false;
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
}
private onSelectionFromKeyboard(event: ITreeEvent<any>): void {
if (event.elements.length !== 1) {
return;
@@ -770,6 +762,7 @@ function createKeyboardNavigationEventFilter(container: HTMLElement, keybindingS
export interface IWorkbenchObjectTreeOptions<T, TFilterData> extends IObjectTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
readonly overrideStyles?: IColorMapping;
readonly selectionNavigation?: boolean;
}
export class WorkbenchObjectTree<T extends NonNullable<any>, TFilterData = void> extends ObjectTree<T, TFilterData> {
@@ -806,6 +799,7 @@ export interface IWorkbenchCompressibleObjectTreeOptionsUpdate extends ICompress
export interface IWorkbenchCompressibleObjectTreeOptions<T, TFilterData> extends IWorkbenchCompressibleObjectTreeOptionsUpdate, ICompressibleObjectTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
readonly selectionNavigation?: boolean;
}
export class WorkbenchCompressibleObjectTree<T extends NonNullable<any>, TFilterData = void> extends CompressibleObjectTree<T, TFilterData> {
@@ -850,6 +844,7 @@ export interface IWorkbenchDataTreeOptionsUpdate extends IAbstractTreeOptionsUpd
export interface IWorkbenchDataTreeOptions<T, TFilterData> extends IWorkbenchDataTreeOptionsUpdate, IDataTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
readonly selectionNavigation?: boolean;
}
export class WorkbenchDataTree<TInput, T, TFilterData = void> extends DataTree<TInput, T, TFilterData> {
@@ -895,6 +890,7 @@ export interface IWorkbenchAsyncDataTreeOptionsUpdate extends IAsyncDataTreeOpti
export interface IWorkbenchAsyncDataTreeOptions<T, TFilterData> extends IWorkbenchAsyncDataTreeOptionsUpdate, IAsyncDataTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
readonly selectionNavigation?: boolean;
}
export class WorkbenchAsyncDataTree<TInput, T, TFilterData = void> extends AsyncDataTree<TInput, T, TFilterData> {
@@ -937,6 +933,7 @@ export class WorkbenchAsyncDataTree<TInput, T, TFilterData = void> extends Async
export interface IWorkbenchCompressibleAsyncDataTreeOptions<T, TFilterData> extends ICompressibleAsyncDataTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
readonly accessibilityProvider: IListAccessibilityProvider<T>;
readonly overrideStyles?: IColorMapping;
readonly selectionNavigation?: boolean;
}
export class WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData = void> extends CompressibleAsyncDataTree<TInput, T, TFilterData> {
@@ -1052,6 +1049,9 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
const listSelectionNavigation = WorkbenchListSelectionNavigation.bindTo(this.contextKeyService);
listSelectionNavigation.set(Boolean(options.selectionNavigation));
this.hasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.hasDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.hasMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);

View File

@@ -25,8 +25,6 @@ export interface ILocalizationsService {
readonly onDidLanguagesChange: Event<void>;
getLanguageIds(): Promise<string[]>;
update(): Promise<boolean>;
}
export function isValidLocalization(localization: ILocalization): boolean {

View File

@@ -31,10 +31,11 @@ export interface IMenubarMenuItemAction {
enabled?: boolean; // Assumed true if missing
}
export interface IMenubarMenuUriItemAction {
export interface IMenubarMenuRecentItemAction {
id: string;
label: string;
uri: URI;
remoteAuthority?: string;
enabled?: boolean;
}
@@ -48,7 +49,7 @@ export interface IMenubarMenuItemSeparator {
id: 'vscode.menubar.separator';
}
export type MenubarMenuItem = IMenubarMenuItemAction | IMenubarMenuItemSubmenu | IMenubarMenuItemSeparator | IMenubarMenuUriItemAction;
export type MenubarMenuItem = IMenubarMenuItemAction | IMenubarMenuItemSubmenu | IMenubarMenuItemSeparator | IMenubarMenuRecentItemAction;
export function isMenubarMenuItemSubmenu(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemSubmenu {
return (<IMenubarMenuItemSubmenu>menuItem).submenu !== undefined;
@@ -58,10 +59,10 @@ export function isMenubarMenuItemSeparator(menuItem: MenubarMenuItem): menuItem
return (<IMenubarMenuItemSeparator>menuItem).id === 'vscode.menubar.separator';
}
export function isMenubarMenuItemUriAction(menuItem: MenubarMenuItem): menuItem is IMenubarMenuUriItemAction {
return (<IMenubarMenuUriItemAction>menuItem).uri !== undefined;
export function isMenubarMenuItemRecentAction(menuItem: MenubarMenuItem): menuItem is IMenubarMenuRecentItemAction {
return (<IMenubarMenuRecentItemAction>menuItem).uri !== undefined;
}
export function isMenubarMenuItemAction(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemAction {
return !isMenubarMenuItemSubmenu(menuItem) && !isMenubarMenuItemSeparator(menuItem) && !isMenubarMenuItemUriAction(menuItem);
return !isMenubarMenuItemSubmenu(menuItem) && !isMenubarMenuItemSeparator(menuItem) && !isMenubarMenuItemRecentAction(menuItem);
}

View File

@@ -11,13 +11,13 @@ import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybinding
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUpdateService, StateType } from 'vs/platform/update/common/update';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { RunOnceScheduler } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { mnemonicMenuLabel } from 'vs/base/common/labels';
import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemRecentAction, IMenubarMenuRecentItemAction } from 'vs/platform/menubar/common/menubar';
import { URI } from 'vs/base/common/uri';
import { IStateService } from 'vs/platform/state/node/state';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
@@ -73,7 +73,8 @@ export class Menubar {
@IStateService private readonly stateService: IStateService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@ILogService private readonly logService: ILogService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService private readonly productService: IProductService
) {
this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0);
@@ -123,22 +124,22 @@ export class Menubar {
this.fallbackMenuHandlers['workbench.action.clearRecentFiles'] = () => this.workspacesHistoryMainService.clearRecentlyOpened();
// Help Menu Items
const twitterUrl = product.twitterUrl;
const twitterUrl = this.productService.twitterUrl;
if (twitterUrl) {
this.fallbackMenuHandlers['workbench.action.openTwitterUrl'] = () => this.openUrl(twitterUrl, 'openTwitterUrl');
}
const requestFeatureUrl = product.requestFeatureUrl;
const requestFeatureUrl = this.productService.requestFeatureUrl;
if (requestFeatureUrl) {
this.fallbackMenuHandlers['workbench.action.openRequestFeatureUrl'] = () => this.openUrl(requestFeatureUrl, 'openUserVoiceUrl');
}
const reportIssueUrl = product.reportIssueUrl;
const reportIssueUrl = this.productService.reportIssueUrl;
if (reportIssueUrl) {
this.fallbackMenuHandlers['workbench.action.openIssueReporter'] = () => this.openUrl(reportIssueUrl, 'openReportIssues');
}
const licenseUrl = product.licenseUrl;
const licenseUrl = this.productService.licenseUrl;
if (licenseUrl) {
this.fallbackMenuHandlers['workbench.action.openLicenseUrl'] = () => {
if (language) {
@@ -150,7 +151,7 @@ export class Menubar {
};
}
const privacyStatementUrl = product.privacyStatementUrl;
const privacyStatementUrl = this.productService.privacyStatementUrl;
if (privacyStatementUrl && licenseUrl) {
this.fallbackMenuHandlers['workbench.action.openPrivacyStatementUrl'] = () => {
if (language) {
@@ -268,7 +269,7 @@ export class Menubar {
let macApplicationMenuItem: MenuItem;
if (isMacintosh) {
const applicationMenu = new Menu();
macApplicationMenuItem = new MenuItem({ label: product.nameShort, submenu: applicationMenu });
macApplicationMenuItem = new MenuItem({ label: this.productService.nameShort, submenu: applicationMenu });
this.setMacApplicationMenu(applicationMenu);
menubar.append(macApplicationMenuItem);
}
@@ -362,7 +363,7 @@ export class Menubar {
}
private setMacApplicationMenu(macApplicationMenu: Menu): void {
const about = this.createMenuItem(nls.localize('mAbout', "About {0}", product.nameLong), 'workbench.action.showAboutDialog');
const about = this.createMenuItem(nls.localize('mAbout', "About {0}", this.productService.nameLong), 'workbench.action.showAboutDialog');
const checkForUpdates = this.getUpdateMenuItems();
let preferences;
@@ -374,11 +375,11 @@ export class Menubar {
const servicesMenu = new Menu();
const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });
const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", product.nameLong), role: 'hide', accelerator: 'Command+H' });
const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", this.productService.nameLong), role: 'hide', accelerator: 'Command+H' });
const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideOthers', accelerator: 'Command+Alt+H' });
const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
const quit = new MenuItem(this.likeAction('workbench.action.quit', {
label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => {
label: nls.localize('miQuit', "Quit {0}", this.productService.nameLong), click: () => {
const lastActiveWindow = this.windowsMainService.getLastActiveWindow();
if (
this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open
@@ -447,8 +448,8 @@ export class Menubar {
const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu });
this.setMenu(submenu, item.submenu.items);
menu.append(submenuItem);
} else if (isMenubarMenuItemUriAction(item)) {
menu.append(this.createOpenRecentMenuItem(item.uri, item.label, item.id));
} else if (isMenubarMenuItemRecentAction(item)) {
menu.append(this.createOpenRecentMenuItem(item));
} else if (isMenubarMenuItemAction(item)) {
if (item.id === 'workbench.action.showAboutDialog') {
this.insertCheckForUpdatesItems(menu);
@@ -487,14 +488,15 @@ export class Menubar {
}
}
private createOpenRecentMenuItem(uri: URI, label: string, commandId: string): MenuItem {
const revivedUri = URI.revive(uri);
private createOpenRecentMenuItem(item: IMenubarMenuRecentItemAction): MenuItem {
const revivedUri = URI.revive(item.uri);
const commandId = item.id;
const openable: IWindowOpenable =
(commandId === 'openRecentFile') ? { fileUri: revivedUri } :
(commandId === 'openRecentWorkspace') ? { workspaceUri: revivedUri } : { folderUri: revivedUri };
return new MenuItem(this.likeAction(commandId, {
label,
label: item.label,
click: (menuItem, win, event) => {
const openInNewWindow = this.isOptionClick(event);
const success = this.windowsMainService.open({
@@ -502,7 +504,8 @@ export class Menubar {
cli: this.environmentMainService.args,
urisToOpen: [openable],
forceNewWindow: openInNewWindow,
gotoLineMode: false
gotoLineMode: false,
remoteAuthority: item.remoteAuthority
}).length > 0;
if (!success) {

View File

@@ -45,6 +45,8 @@ export interface ICommonNativeHostService {
readonly onDidFocusWindow: Event<number>;
readonly onDidBlurWindow: Event<number>;
readonly onDidChangeDisplay: Event<void>;
readonly onDidResumeOS: Event<unknown>;
readonly onDidChangeColorScheme: Event<IColorScheme>;
@@ -98,7 +100,7 @@ export interface ICommonNativeHostService {
moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise<boolean>;
isAdmin(): Promise<boolean>;
writeElevated(source: URI, target: URI, options?: { overwriteReadonly?: boolean }): Promise<void>;
writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise<void>;
getOSProperties(): Promise<IOSProperties>;
getOSStatistics(): Promise<IOSStatistics>;

View File

@@ -5,7 +5,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme, screen, Display } from 'electron';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
@@ -24,7 +24,7 @@ import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from
import { virtualMachineHint } from 'vs/base/node/id';
import { ILogService } from 'vs/platform/log/common/log';
import { dirname, join } from 'vs/base/common/path';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { memoize } from 'vs/base/common/decorators';
import { Disposable } from 'vs/base/common/lifecycle';
import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess';
@@ -49,7 +49,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IProductService private readonly productService: IProductService
) {
super();
@@ -74,6 +75,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Events
readonly onDidOpenWindow = Event.map(this.windowsMainService.onDidOpenWindow, window => window.id);
@@ -95,8 +97,20 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>());
readonly onDidChangePassword = this._onDidChangePassword.event;
readonly onDidChangeDisplay = Event.debounce(Event.any(
Event.filter(Event.fromNodeEventEmitter(screen, 'display-metrics-changed', (event: Electron.Event, display: Display, changedMetrics?: string[]) => changedMetrics), changedMetrics => {
// Electron will emit 'display-metrics-changed' events even when actually
// going fullscreen, because the dock hides. However, we do not want to
// react on this event as there is no change in display bounds.
return !(Array.isArray(changedMetrics) && changedMetrics.length === 1 && changedMetrics[0] === 'workArea');
}),
Event.fromNodeEventEmitter(screen, 'display-added'),
Event.fromNodeEventEmitter(screen, 'display-removed')
), () => { }, 100);
//#endregion
//#region Window
async getWindows(): Promise<IOpenedWindow[]> {
@@ -148,7 +162,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
addMode: options.addMode,
gotoLineMode: options.gotoLineMode,
noRecentEntry: options.noRecentEntry,
waitMarkerFileURI: options.waitMarkerFileURI
waitMarkerFileURI: options.waitMarkerFileURI,
remoteAuthority: options.remoteAuthority || undefined
});
}
}
@@ -234,6 +249,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Dialog
async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
@@ -295,7 +311,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
contextWindowId: windowId,
cli: this.environmentMainService.args,
urisToOpen: openable,
forceNewWindow: options.forceNewWindow
forceNewWindow: options.forceNewWindow,
/* remoteAuthority will be determined based on openable */
});
}
@@ -313,6 +330,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region OS
async showItemInFolder(windowId: number | undefined, path: string): Promise<void> {
@@ -373,20 +391,20 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return isAdmin;
}
async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { overwriteReadonly?: boolean }): Promise<void> {
async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { unlock?: boolean }): Promise<void> {
const sudoPrompt = await import('sudo-prompt');
return new Promise<void>((resolve, reject) => {
const sudoCommand: string[] = [`"${this.cliPath}"`];
if (options?.overwriteReadonly) {
if (options?.unlock) {
sudoCommand.push('--file-chmod');
}
sudoCommand.push('--file-write', `"${source.fsPath}"`, `"${target.fsPath}"`);
const promptOptions = {
name: product.nameLong.replace('-', ''),
icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${product.nameShort}.icns`) : undefined
name: this.productService.nameLong.replace('-', ''),
icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${this.productService.nameShort}.icns`) : undefined
};
sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => {
@@ -413,7 +431,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// Windows
if (isWindows) {
if (this.environmentMainService.isBuilt) {
return join(dirname(process.execPath), 'bin', `${product.applicationName}.cmd`);
return join(dirname(process.execPath), 'bin', `${this.productService.applicationName}.cmd`);
}
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.bat');
@@ -422,7 +440,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// Linux
if (isLinux) {
if (this.environmentMainService.isBuilt) {
return join(dirname(process.execPath), 'bin', `${product.applicationName}`);
return join(dirname(process.execPath), 'bin', `${this.productService.applicationName}`);
}
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
@@ -502,10 +520,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region macOS Touchbar
async newWindowTab(): Promise<void> {
this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, forceEmpty: true });
this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentMainService.args, forceNewTabbedWindow: true, forceEmpty: true, remoteAuthority: this.environmentMainService.args.remote || undefined });
}
async showPreviousWindowTab(): Promise<void> {
@@ -537,6 +556,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Lifecycle
async notifyReady(windowId: number | undefined): Promise<void> {
@@ -591,6 +611,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Connectivity
async resolveProxy(windowId: number | undefined, url: string): Promise<string | undefined> {
@@ -605,6 +626,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Development
async openDevTools(windowId: number | undefined, options?: OpenDevToolsOptions): Promise<void> {
@@ -635,6 +657,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Registry (windows)
async windowsGetStringRegKey(windowId: number | undefined, hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> {
@@ -652,6 +675,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#endregion
//#region Credentials
private static readonly MAX_PASSWORD_LENGTH = 2500;

View File

@@ -86,7 +86,7 @@ export interface INotification extends INotificationProperties {
/**
* The source of the notification appears as additional information.
*/
readonly source?: string;
readonly source?: string | { label: string; id: string; };
/**
* Actions to show as part of the notification. Primary actions show up as
@@ -304,6 +304,16 @@ export interface INotificationService {
readonly _serviceBrand: undefined;
/**
* Emitted when a new notification is added.
*/
readonly onDidAddNotification: Event<INotification>;
/**
* Emitted when a notification is removed.
*/
readonly onDidRemoveNotification: Event<INotification>;
/**
* Show the provided notification to the user. The returned `INotificationHandle`
* can be used to control the notification afterwards.

View File

@@ -5,9 +5,14 @@
import { INotificationService, INotificationHandle, NoOpNotification, Severity, INotification, IPromptChoice, IPromptOptions, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
export class TestNotificationService implements INotificationService {
readonly onDidAddNotification: Event<INotification> = Event.None;
readonly onDidRemoveNotification: Event<INotification> = Event.None;
declare readonly _serviceBrand: undefined;
private static readonly NO_OP: INotificationHandle = new NoOpNotification();

View File

@@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProductConfiguration } from 'vs/platform/product/common/productService';
import { FileAccess } from 'vs/base/common/network';
import { isWeb } from 'vs/base/common/platform';
import { env } from 'vs/base/common/process';
import { FileAccess } from 'vs/base/common/network';
import { dirname, joinPath } from 'vs/base/common/resources';
import { IProductConfiguration } from 'vs/platform/product/common/productService';
let product: IProductConfiguration;
@@ -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.54.0-dev',
version: '1.55.0-dev',
nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
applicationName: 'code-oss',
@@ -32,7 +32,9 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
extensionAllowedProposedApi: [
'ms-vscode.vscode-js-profile-flame',
'ms-vscode.vscode-js-profile-table',
'ms-vscode.github-browser'
'ms-vscode.github-browser',
'ms-vscode.remotehub',
'ms-vscode.remotehub-insiders'
],
});
}

View File

@@ -109,11 +109,13 @@ export interface IProductConfiguration {
readonly twitterUrl?: string;
readonly requestFeatureUrl?: string;
readonly reportIssueUrl?: string;
readonly reportMarketplaceIssueUrl?: string;
readonly licenseUrl?: string;
readonly privacyStatementUrl?: string;
readonly telemetryOptOutUrl?: string;
readonly npsSurveyUrl?: string;
readonly cesSurveyUrl?: string;
readonly surveys?: readonly ISurveyData[];
readonly checksums?: { [path: string]: string; };

View File

@@ -7,6 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
import { IAction } from 'vs/base/common/actions';
import { DeferredPromise } from 'vs/base/common/async';
export const IProgressService = createDecorator<IProgressService>('progressService');
@@ -51,7 +52,7 @@ export const enum ProgressLocation {
export interface IProgressOptions {
readonly location: ProgressLocation | string;
readonly title?: string;
readonly source?: string;
readonly source?: string | { label: string; id: string; };
readonly total?: number;
readonly cancellable?: boolean;
readonly buttons?: string[];
@@ -123,6 +124,42 @@ export interface IOperation {
stop(): void;
}
/**
* RAII-style progress instance that allows imperative reporting and hides
* once `dispose()` is called.
*/
export class UnmanagedProgress extends Disposable {
private readonly deferred = new DeferredPromise<void>();
private reporter?: IProgress<IProgressStep>;
private lastStep?: IProgressStep;
constructor(
options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
@IProgressService progressService: IProgressService,
) {
super();
progressService.withProgress(options, reporter => {
this.reporter = reporter;
if (this.lastStep) {
reporter.report(this.lastStep);
}
return this.deferred.p;
});
this._register(toDisposable(() => this.deferred.complete()));
}
report(step: IProgressStep) {
if (this.reporter) {
this.reporter.report(step);
} else {
this.lastStep = step;
}
}
}
export class LongRunningOperation extends Disposable {
private currentOperationId = 0;
private readonly currentOperationDisposables = this._register(new DisposableStore());

View File

@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions, DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { Registry } from 'vs/platform/registry/common/platform';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { once } from 'vs/base/common/functional';
@@ -100,13 +100,27 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
}
// Register listeners
const cancellationToken = this.registerPickerListeners(picker, provider, descriptor, value, disposables);
disposables.add(this.registerPickerListeners(picker, provider, descriptor, value));
// Ask provider to fill the picker as needed if we have one
// and pass over a cancellation token that will indicate when
// the picker is hiding without a pick being made.
const cts = disposables.add(new CancellationTokenSource());
if (provider) {
disposables.add(provider.provide(picker, cancellationToken));
disposables.add(provider.provide(picker, cts.token));
}
// Finally, trigger disposal and cancellation when the picker
// hides depending on items selected or not.
once(picker.onDidHide)(() => {
if (picker.selectedItems.length === 0) {
cts.cancel();
}
// Start to dispose once picker hides
disposables.dispose();
});
// Finally, show the picker. This is important because a provider
// may not call this and then our disposables would leak that rely
// on the onDidHide event.
@@ -129,7 +143,8 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
picker.valueSelection = valueSelection;
}
private registerPickerListeners(picker: IQuickPick<IQuickPickItem>, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, disposables: DisposableStore): CancellationToken {
private registerPickerListeners(picker: IQuickPick<IQuickPickItem>, provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string): IDisposable {
const disposables = new DisposableStore();
// Remember as last visible picker and clean up once picker get's disposed
const visibleQuickAccess = this.visibleQuickAccess = { picker, descriptor, value };
@@ -157,19 +172,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
}));
}
// Create a cancellation token source that is valid as long as the
// picker has not been closed without picking an item
const cts = disposables.add(new CancellationTokenSource());
once(picker.onDidHide)(() => {
if (picker.selectedItems.length === 0) {
cts.cancel();
}
// Start to dispose once picker hides
disposables.dispose();
});
return cts.token;
return disposables;
}
private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] {

View File

@@ -17,7 +17,6 @@ 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 */;
export const enum ConnectionType {
@@ -404,7 +403,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions,
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol } = await doConnectRemoteAgentManagement(simpleOptions, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
const { protocol } = await doConnectRemoteAgentManagement(simpleOptions, CancellationToken.None);
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:`);
@@ -418,7 +417,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments, CancellationToken.None);
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:`);
@@ -430,7 +429,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 doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }, createTimeoutCancellation(INITIAL_CONNECT_TIMEOUT));
const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }, CancellationToken.None);
return protocol;
}

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isWindows, OperatingSystem } from 'vs/base/common/platform';
@@ -42,6 +43,23 @@ export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
}
export enum ProvidedOnAutoForward {
Notify = 1,
OpenBrowser = 2,
OpenPreview = 3,
Silent = 4,
Ignore = 5
}
export interface ProvidedPortAttributes {
port: number;
autoForwardAction: ProvidedOnAutoForward;
}
export interface PortAttributesProvider {
providePortAttributes(ports: number[], pid: number | undefined, commandLine: string | undefined, token: CancellationToken): Promise<ProvidedPortAttributes[]>;
}
export interface ITunnel {
remoteAddress: { port: number, host: string };
@@ -180,7 +198,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
this.logService.trace(`ForwardedPorts: (TunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
if (!addressProvider) {
return undefined;
}
@@ -191,20 +209,20 @@ export abstract class AbstractTunnelService implements ITunnelService {
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
if (!resolvedTunnel) {
this.logService.trace(`Tunnel was not created.`);
this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel was not created.`);
return resolvedTunnel;
}
return resolvedTunnel.then(tunnel => {
if (!tunnel) {
this.logService.trace('New tunnel is undefined.');
this.logService.trace('ForwardedPorts: (TunnelService) New tunnel is undefined.');
this.removeEmptyTunnelFromMap(remoteHost!, remotePort);
return undefined;
}
this.logService.trace('New tunnel established.');
this.logService.trace('ForwardedPorts: (TunnelService) New tunnel established.');
const newTunnel = this.makeTunnel(tunnel);
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
this.logService.warn('ForwardedPorts: (TunnelService) Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
}
this._onTunnelOpened.fire(newTunnel);
return newTunnel;
@@ -219,6 +237,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
localAddress: tunnel.localAddress,
public: tunnel.public,
dispose: async () => {
this.logService.trace(`ForwardedPorts: (TunnelService) dispose request for ${tunnel.tunnelRemotePort} `);
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
if (existingHost) {
const existing = existingHost.get(tunnel.tunnelRemotePort);
@@ -233,7 +252,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }): Promise<void> {
if (tunnel.refcount <= 0) {
this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`);
this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel is being disposed ${remoteHost}:${remotePort}.`);
const disposePromise: Promise<void> = tunnel.value.then(async (tunnel) => {
if (tunnel) {
await tunnel.dispose(true);
@@ -248,6 +267,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
async closeTunnel(remoteHost: string, remotePort: number): Promise<void> {
this.logService.trace(`ForwardedPorts: (TunnelService) close request for ${remotePort} `);
const portMap = this._tunnels.get(remoteHost);
if (portMap && portMap.has(remotePort)) {
const value = portMap.get(remotePort)!;
@@ -306,13 +326,13 @@ export abstract class AbstractTunnelService implements ITunnelService {
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined;
protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic };
const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo);
this.logService.trace('Tunnel created by provider.');
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created by provider.');
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}

View File

@@ -150,7 +150,7 @@ export class BaseTunnelService extends AbstractTunnelService {
if (this._tunnelProvider) {
return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
} else {
this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const options: IConnectionOptions = {
commit: this.productService.commit,
socketFactory: this.socketFactory,
@@ -161,7 +161,7 @@ export class BaseTunnelService extends AbstractTunnelService {
};
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
this.logService.trace('Tunnel created without provider.');
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
}

View File

@@ -102,7 +102,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration)
'http.systemCertificates': {
type: 'boolean',
default: true,
description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS a reload of the window is required after turning this off.)")
description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS, a reload of the window is required after turning this off.)")
}
}
});

View File

@@ -8,11 +8,11 @@ import { isBoolean } from 'vs/base/common/types';
export type Agent = any;
function getSystemProxyURI(requestURL: Url): string | null {
function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | null {
if (requestURL.protocol === 'http:') {
return process.env.HTTP_PROXY || process.env.http_proxy || null;
return env.HTTP_PROXY || env.http_proxy || null;
} else if (requestURL.protocol === 'https:') {
return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null;
return env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || null;
}
return null;
@@ -23,9 +23,9 @@ export interface IOptions {
strictSSL?: boolean;
}
export async function getProxyAgent(rawRequestURL: string, options: IOptions = {}): Promise<Agent> {
export async function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: IOptions = {}): Promise<Agent> {
const requestURL = parseUrl(rawRequestURL);
const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL);
const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL, env);
if (!proxyURL) {
return null;

View File

@@ -18,6 +18,8 @@ import { getProxyAgent, Agent } from 'vs/platform/request/node/proxy';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { streamToBufferReadableStream } from 'vs/base/common/buffer';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
export interface IRawRequestFunction {
(options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
@@ -43,6 +45,7 @@ export class RequestService extends Disposable implements IRequestService {
constructor(
@IConfigurationService configurationService: IConfigurationService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService
) {
super();
@@ -60,7 +63,11 @@ export class RequestService extends Disposable implements IRequestService {
this.logService.trace('RequestService#request', options.url);
const { proxyUrl, strictSSL } = this;
const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', { proxyUrl, strictSSL });
const env = {
...process.env,
...(await resolveShellEnv(this.logService, this.environmentService.args, process.env)),
};
const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL });
options.agent = agent;
options.strictSSL = strictSSL;

View File

@@ -251,4 +251,8 @@ export class SharedProcess extends Disposable implements ISharedProcess {
this.window.webContents.openDevTools();
}
}
isVisible(): boolean {
return this.window?.isVisible() ?? false;
}
}

View File

@@ -5,13 +5,11 @@
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');
@@ -38,8 +36,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic
constructor(
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IConfigurationService private readonly configurationService: IConfigurationService
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService
) {
super();
@@ -52,10 +49,6 @@ export class StorageMainService extends Disposable implements IStorageMainServic
};
}
protected enableMainWorkspaceStorage(): boolean {
return !!(this.configurationService.getValue<IWindowSettings | undefined>('window')?.enableExperimentalMainProcessWorkspaceStorage);
}
private registerListeners(): void {
// Global Storage: Warmup when any window opens
@@ -66,13 +59,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic
})();
// 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();
}
}));
}
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 => {

View File

@@ -14,7 +14,7 @@ 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 {
export class NativeStorageService extends AbstractStorageService {
// Global Storage is readonly and shared across windows
private readonly globalStorage: IStorage;
@@ -85,7 +85,7 @@ export class NativeStorageService2 extends AbstractStorageService {
}
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;
return scope === StorageScope.GLOBAL ? this.environmentService.globalStorageHome.fsPath : this.workspaceStorageId ? `${joinPath(this.environmentService.workspaceStorageHome, this.workspaceStorageId, 'state.vscdb').fsPath}` : undefined;
}
async close(): Promise<void> {

View File

@@ -1,198 +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 { promises } from 'fs';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
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, 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 { Promises } from 'vs/base/common/async';
export class NativeStorageService extends AbstractStorageService {
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static readonly WORKSPACE_META_NAME = 'workspace.json';
private readonly globalStorage = new Storage(this.globalStorageDatabase);
private workspaceStoragePath: string | undefined;
private workspaceStorage: IStorage | undefined;
private workspaceStorageListener: IDisposable | undefined;
constructor(
private globalStorageDatabase: IStorageDatabase,
private payload: IWorkspaceInitializationPayload | undefined,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
// Global Storage change events
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
protected async doInitialize(): Promise<void> {
// Init all storage locations
await Promises.settled([
this.initializeGlobalStorage(),
this.payload ? this.initializeWorkspaceStorage(this.payload) : Promise.resolve()
]);
}
private initializeGlobalStorage(): Promise<void> {
return this.globalStorage.init();
}
private async initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Promise<void> {
// Prepare workspace storage folder for DB
try {
const result = await this.prepareWorkspaceStorageFolder(payload);
const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests!
// Create workspace storage and initialize
mark('code/willInitWorkspaceStorage');
try {
const workspaceStorage = this.createWorkspaceStorage(
useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME),
result.wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined
);
await workspaceStorage.init();
// Check to see if this is the first time we are "opening" this workspace
const firstWorkspaceOpen = workspaceStorage.getBoolean(IS_NEW_KEY);
if (firstWorkspaceOpen === undefined) {
workspaceStorage.set(IS_NEW_KEY, result.wasCreated);
} else if (firstWorkspaceOpen) {
workspaceStorage.set(IS_NEW_KEY, false);
}
} finally {
mark('code/didInitWorkspaceStorage');
}
} catch (error) {
this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`);
}
}
private createWorkspaceStorage(workspaceStoragePath: string, hint?: StorageHint): IStorage {
// Logger for workspace storage
const workspaceLoggingOptions: ISQLiteStorageDatabaseLoggingOptions = {
logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined,
logError: error => this.logService.error(error)
};
// Dispose old (if any)
dispose(this.workspaceStorage);
dispose(this.workspaceStorageListener);
// Create new
this.workspaceStoragePath = workspaceStoragePath;
this.workspaceStorage = new Storage(new SQLiteStorageDatabase(workspaceStoragePath, { logging: workspaceLoggingOptions }), { hint });
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key));
return this.workspaceStorage;
}
private getWorkspaceStorageFolderPath(payload: IWorkspaceInitializationPayload): string {
return join(this.environmentService.workspaceStorageHome.fsPath, payload.id); // workspace home + workspace id;
}
private async prepareWorkspaceStorageFolder(payload: IWorkspaceInitializationPayload): Promise<{ path: string, wasCreated: boolean }> {
const workspaceStorageFolderPath = this.getWorkspaceStorageFolderPath(payload);
const storageExists = await exists(workspaceStorageFolderPath);
if (storageExists) {
return { path: workspaceStorageFolderPath, wasCreated: false };
}
await promises.mkdir(workspaceStorageFolderPath, { recursive: true });
// Write metadata into folder
this.ensureWorkspaceStorageFolderMeta(payload);
return { path: workspaceStorageFolderPath, wasCreated: true };
}
private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void {
let meta: object | undefined = undefined;
if (isSingleFolderWorkspaceIdentifier(payload)) {
meta = { folder: payload.uri.toString() };
} else if (isWorkspaceIdentifier(payload)) {
meta = { workspace: payload.configPath.toString() };
}
if (meta) {
(async () => {
try {
const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME);
const storageExists = await exists(workspaceStorageMetaPath);
if (!storageExists) {
await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2));
}
} catch (error) {
this.logService.error(error);
}
})();
}
}
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.workspaceStoragePath;
}
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 ? this.workspaceStorage.close() : Promise.resolve()
]);
}
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.workspaceStorage?.close();
// Prepare new workspace storage folder
const result = await this.prepareWorkspaceStorageFolder(toWorkspace);
const newWorkspaceStoragePath = join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME);
// Copy current storage over to new workspace storage
await copy(assertIsDefined(this.workspaceStoragePath), newWorkspaceStoragePath, { preserveSymlinks: false });
// Recreate and init workspace storage
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
}
}

View File

@@ -17,10 +17,13 @@ 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';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
suite('StorageMainService', function () {
const productService: IProductService = { _serviceBrand: undefined, ...product };
class TestStorageMainService extends StorageMainService {
protected getStorageOptions(): IStorageMainOptions {
@@ -28,10 +31,6 @@ suite('StorageMainService', function () {
useInMemoryStorage: true
};
}
protected enableMainWorkspaceStorage(): boolean {
return true;
}
}
class StorageTestLifecycleMainService implements ILifecycleMainService {
@@ -48,9 +47,7 @@ suite('StorageMainService', function () {
this._onWillShutdown.fire({
join(promise) {
if (promise) {
joiners.push(promise);
}
joiners.push(promise);
}
});
@@ -129,14 +126,14 @@ suite('StorageMainService', function () {
}
test('basics (global)', function () {
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS)), new StorageTestLifecycleMainService(), new TestConfigurationService());
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService), new StorageTestLifecycleMainService());
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());
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService), new StorageTestLifecycleMainService());
return testStorage(storageMainService.workspaceStorage(workspace), false);
});
@@ -144,7 +141,7 @@ suite('StorageMainService', function () {
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());
const storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService), lifecycleMainService);
let workspaceStorage = storageMainService.workspaceStorage(workspace);
let didCloseWorkspaceStorage = false;
@@ -175,7 +172,7 @@ suite('StorageMainService', function () {
});
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 storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService), new StorageTestLifecycleMainService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);
@@ -198,7 +195,7 @@ suite('StorageMainService', function () {
});
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 storageMainService = new TestStorageMainService(new NullLogService(), new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService), new StorageTestLifecycleMainService());
const workspace = { id: generateUuid() };
let workspaceStorage = storageMainService.workspaceStorage(workspace);

View File

@@ -1,77 +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 { 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 { 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('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;
createSuite<NativeStorageService>({
setup: async () => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
await promises.mkdir(testDir, { recursive: true });
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 () {
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);
storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE);
strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo');
strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.migrate({ id: String(Date.now() + 100) });
strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo');
strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.close();
});
});

View File

@@ -52,6 +52,21 @@ export interface ITelemetryService {
isOptedIn: boolean;
}
export interface ITelemetryEndpoint {
id: string;
aiKey: string;
sendErrorTelemetry: boolean;
}
export const ICustomEndpointTelemetryService = createDecorator<ICustomEndpointTelemetryService>('customEndpointTelemetryService');
export interface ICustomEndpointTelemetryService {
readonly _serviceBrand: undefined;
publicLog(endpoint: ITelemetryEndpoint, eventName: string, data?: ITelemetryData): Promise<void>;
publicLogError(endpoint: ITelemetryEndpoint, errorEventName: string, data?: ITelemetryData): Promise<void>;
}
// Keys
export const instanceStorageKey = 'telemetry.instanceId';
export const currentSessionDateStorageKey = 'telemetry.currentSessionDate';

View File

@@ -5,7 +5,7 @@
import { IDisposable } from 'vs/base/common/lifecycle';
import { IConfigurationService, ConfigurationTarget, ConfigurationTargetToString } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryService, ITelemetryInfo, ITelemetryData, ICustomEndpointTelemetryService, ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry';
import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
import { safeStringify } from 'vs/base/common/objects';
import { isObject } from 'vs/base/common/types';
@@ -41,6 +41,18 @@ export const NullTelemetryService = new class implements ITelemetryService {
}
};
export class NullEndpointTelemetryService implements ICustomEndpointTelemetryService {
_serviceBrand: undefined;
async publicLog(_endpoint: ITelemetryEndpoint, _eventName: string, _data?: ITelemetryData): Promise<void> {
// noop
}
async publicLogError(_endpoint: ITelemetryEndpoint, _errorEventName: string, _data?: ITelemetryData): Promise<void> {
// noop
}
}
export interface ITelemetryAppender {
log(eventName: string, data: any): void;
flush(): Promise<any>;

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { ICustomEndpointTelemetryService } from 'vs/platform/telemetry/common/telemetry';
registerSharedProcessRemoteService(ICustomEndpointTelemetryService, 'customEndpointTelemetry', { supportsDelayedInstantiation: true });

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp';
import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc';
import { FileAccess } from 'vs/base/common/network';
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export class CustomEndpointTelemetryService implements ICustomEndpointTelemetryService {
declare readonly _serviceBrand: undefined;
private customTelemetryServices = new Map<string, ITelemetryService>();
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@ITelemetryService private readonly telemetryService: ITelemetryService
) { }
private async getCustomTelemetryService(endpoint: ITelemetryEndpoint): Promise<ITelemetryService> {
if (!this.customTelemetryServices.has(endpoint.id)) {
const { machineId, sessionId } = await this.telemetryService.getTelemetryInfo();
const telemetryInfo: { [key: string]: string } = Object.create(null);
telemetryInfo['common.vscodemachineid'] = machineId;
telemetryInfo['common.vscodesessionid'] = sessionId;
const args = [endpoint.id, JSON.stringify(telemetryInfo), endpoint.aiKey];
const client = new TelemetryClient(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'Debug Telemetry',
timeout: 1000 * 60 * 5,
args,
env: {
ELECTRON_RUN_AS_NODE: 1,
VSCODE_PIPE_LOGGING: 'true',
VSCODE_AMD_ENTRYPOINT: 'vs/workbench/contrib/debug/node/telemetryApp'
}
}
);
const channel = client.getChannel('telemetryAppender');
const appender = new TelemetryAppenderClient(channel);
this.customTelemetryServices.set(endpoint.id, new TelemetryService({
appender,
sendErrorTelemetry: endpoint.sendErrorTelemetry
}, this.configurationService));
}
return this.customTelemetryServices.get(endpoint.id)!;
}
async publicLog(telemetryEndpoint: ITelemetryEndpoint, eventName: string, data?: ITelemetryData): Promise<void> {
const customTelemetryService = await this.getCustomTelemetryService(telemetryEndpoint);
await customTelemetryService.publicLog(eventName, data);
}
async publicLogError(telemetryEndpoint: ITelemetryEndpoint, errorEventName: string, data?: ITelemetryData): Promise<void> {
const customTelemetryService = await this.getCustomTelemetryService(telemetryEndpoint);
await customTelemetryService.publicLogError(errorEventName, data);
}
}

View File

@@ -3,11 +3,19 @@
* 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 { IProcessEnvironment } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
export enum WindowsShellType {
CommandPrompt = 'cmd',
PowerShell = 'pwsh',
Wsl = 'wsl',
GitBash = 'gitbash'
}
export type TerminalShellType = WindowsShellType | undefined;
export interface IRawTerminalInstanceLayoutInfo<T> {
relativeSize: number;
terminal: T;
@@ -17,7 +25,7 @@ export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo<IPtyHos
export interface IRawTerminalTabLayoutInfo<T> {
isActive: boolean;
activePersistentTerminalId: number | undefined;
activePersistentProcessId: number | undefined;
terminals: IRawTerminalInstanceLayoutInfo<T>[];
}
@@ -65,6 +73,37 @@ export enum TerminalIpcChannels {
Heartbeat = 'heartbeat'
}
export interface IOffProcessTerminalService {
readonly _serviceBrand: undefined;
/**
* Fired when the ptyHost process becomes non-responsive, this should disable stdin for all
* terminals using this pty host connection and mark them as disconnected.
*/
onPtyHostUnresponsive: Event<void>;
/**
* Fired when the ptyHost process becomes responsive after being non-responsive. Allowing
* previously disconnected terminals to reconnect.
*/
onPtyHostResponsive: Event<void>;
/**
* Fired when the ptyHost has been restarted, this is used as a signal for listening terminals
* that its pty has been lost and will remain disconnected.
*/
onPtyHostRestart: Event<void>;
attachToProcess(id: number): Promise<ITerminalChildProcess | undefined>;
listProcesses(reduceGraceTime?: boolean): Promise<IProcessDetails[]>;
setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void>;
getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
}
export const ILocalTerminalService = createDecorator<ILocalTerminalService>('localTerminalService');
export interface ILocalTerminalService extends IOffProcessTerminalService {
createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess>;
}
export const IPtyService = createDecorator<IPtyService>('ptyService');
export interface IPtyService {
readonly _serviceBrand: undefined;
@@ -77,9 +116,11 @@ export interface IPtyService {
readonly onProcessExit: Event<{ id: number, event: number | undefined }>;
readonly onProcessReady: Event<{ id: number, event: { pid: number, cwd: string } }>;
readonly onProcessTitleChanged: Event<{ id: number, event: string }>;
readonly onProcessShellTypeChanged: Event<{ id: number, event: TerminalShellType }>;
readonly onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>;
readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>;
readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>;
readonly onProcessOrphanQuestion: Event<{ id: number }>;
restartPtyHost?(): Promise<void>;
shutdownAll?(): Promise<void>;
@@ -99,6 +140,13 @@ export interface IPtyService {
attachToProcess(id: number): Promise<void>;
detachFromProcess(id: number): Promise<void>;
/**
* Lists all orphaned processes, ie. those without a connected frontend.
* @param reduceGraceTime Whether to reduce the reconnection grace time for all orphaned
* terminals.
*/
listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]>;
start(id: number): Promise<ITerminalLaunchError | undefined>;
shutdown(id: number, immediate: boolean): Promise<void>;
input(id: number, data: string): Promise<void>;
@@ -107,8 +155,10 @@ export interface IPtyService {
getCwd(id: number): Promise<string>;
getLatency(id: number): Promise<number>;
acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
/** Confirm the process is _not_ an orphan. */
orphanQuestionReply(id: number): Promise<void>;
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void;
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void>;
getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined>;
}
@@ -190,7 +240,7 @@ export interface IShellLaunchConfig {
/**
* Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`.
*/
isExtensionTerminal?: boolean;
isExtensionCustomPtyTerminal?: boolean;
/**
* A UUID generated by the extension host process for terminals created on the extension host process.
@@ -200,7 +250,7 @@ export interface IShellLaunchConfig {
/**
* This is a terminal that attaches to an already running terminal.
*/
attachPersistentTerminal?: { id: number; pid: number; title: string; cwd: string; };
attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; };
/**
* Whether the terminal process environment should be exactly as provided in
@@ -226,11 +276,6 @@ export interface IShellLaunchConfig {
*/
isFeatureTerminal?: boolean;
/**
* Whether flow control is enabled for this terminal.
*/
flowControl?: boolean;
/**
* Whether this terminal was created by an extension.
*/
@@ -269,6 +314,7 @@ export interface ITerminalChildProcess {
onProcessTitleChanged: Event<string>;
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined>;
onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig>;
onProcessShellTypeChanged: Event<TerminalShellType>;
/**
* Starts the process.
@@ -310,7 +356,7 @@ export const enum LocalReconnectConstants {
/**
* If there is no reconnection within this time-frame, consider the connection permanently closed...
*/
ReconnectionGraceTime = 30000, // 30 seconds
ReconnectionGraceTime = 60000, // 60 seconds
/**
* Maximal grace time between the first and the last reconnection...
*/
@@ -349,15 +395,15 @@ export interface ITerminalDimensions {
/**
* The columns of the terminal.
*/
readonly cols: number;
cols: number;
/**
* The rows of the terminal.
*/
readonly rows: number;
rows: number;
}
export interface ITerminalDimensionsOverride extends ITerminalDimensions {
export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimensions> {
/**
* indicate that xterm must receive these exact dimensions, even if they overflow the ui!
*/

View File

@@ -31,17 +31,16 @@ export class TerminalDataBufferer implements IDisposable {
let buffer = this._terminalBufferMap.get(id);
if (buffer) {
buffer.data.push(data);
return;
}
const timeoutId = setTimeout(() => this._flushBuffer(id), throttleBy);
const timeoutId = setTimeout(() => this.flushBuffer(id), throttleBy);
buffer = {
data: [data],
timeoutId: timeoutId,
dispose: () => {
clearTimeout(timeoutId);
this._flushBuffer(id);
this.flushBuffer(id);
disposable.dispose();
}
};
@@ -57,7 +56,7 @@ export class TerminalDataBufferer implements IDisposable {
}
}
private _flushBuffer(id: number): void {
flushBuffer(id: number): void {
const buffer = this._terminalBufferMap.get(id);
if (buffer) {
this._terminalBufferMap.delete(id);

View File

@@ -38,7 +38,6 @@ export interface ICompleteTerminalConfiguration {
'terminal.integrated.inheritEnv': boolean;
'terminal.integrated.cwd': string;
'terminal.integrated.detectLocale': 'auto' | 'off' | 'on';
'terminal.flowControl': boolean;
}
export type ITerminalEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][];
@@ -58,7 +57,7 @@ export interface IGetTerminalLayoutInfoArgs {
workspaceId: string;
}
export interface IPtyHostDescriptionDto {
export interface IProcessDetails {
id: number;
pid: number;
title: string;
@@ -68,7 +67,7 @@ export interface IPtyHostDescriptionDto {
isOrphan: boolean;
}
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IPtyHostDescriptionDto>;
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;
export interface ReplayEntry { cols: number; rows: number; data: string; }
export interface IPtyHostProcessReplayEvent {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { IPtyHostProcessReplayEvent, ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
const MAX_RECORDER_DATA_SIZE = 1024 * 1024; // 1MB
@@ -13,8 +13,6 @@ interface RecorderEntry {
data: string[];
}
export interface ReplayEntry { cols: number; rows: number; data: string; }
export interface IRemoteTerminalProcessReplayEvent {
events: ReplayEntry[];
}
@@ -22,11 +20,10 @@ export interface IRemoteTerminalProcessReplayEvent {
export class TerminalRecorder {
private _entries: RecorderEntry[];
private _totalDataLength: number;
private _totalDataLength: number = 0;
constructor(cols: number, rows: number) {
this._entries = [{ cols, rows, data: [] }];
this._totalDataLength = 0;
}
public recordResize(cols: number, rows: number): void {

View File

@@ -7,5 +7,4 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IPtyService } from 'vs/platform/terminal/common/terminal';
export const ILocalPtyService = createDecorator<ILocalPtyService>('localPtyService');
export interface ILocalPtyService extends IPtyService { }

View File

@@ -5,14 +5,14 @@
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants } from 'vs/platform/terminal/common/terminal';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { FileAccess } from 'vs/base/common/network';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { Emitter } from 'vs/base/common/event';
import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc';
import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
enum Constants {
MaxRestarts = 5
@@ -24,7 +24,11 @@ enum Constants {
*/
let lastPtyId = 0;
export class LocalPtyService extends Disposable implements IPtyService {
/**
* This service implements IPtyService by launching a pty host process, forwarding messages to and
* from the pty host process and manages the connection.
*/
export class PtyHostService extends Disposable implements IPtyService {
declare readonly _serviceBrand: undefined;
private _client: Client;
@@ -56,10 +60,14 @@ export class LocalPtyService extends Disposable implements IPtyService {
readonly onProcessReplay = this._onProcessReplay.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event;
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>());
readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event;
private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>());
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
constructor(
@ILogService private readonly _logService: ILogService
@@ -72,7 +80,7 @@ export class LocalPtyService extends Disposable implements IPtyService {
}
private _startPtyHost(): [Client, IPtyService] {
const client = this._register(new Client(
const client = new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'Pty Host',
@@ -84,11 +92,13 @@ export class LocalPtyService extends Disposable implements IPtyService {
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
);
this._onPtyHostStart.fire();
// Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts
const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
heartbeatService.onBeat(() => this._handleHeartbeat());
this._handleHeartbeat();
// Handle exit
this._register(client.onDidProcessExit(e => {
@@ -116,9 +126,11 @@ export class LocalPtyService extends Disposable implements IPtyService {
this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e)));
this._register(proxy.onProcessReady(e => this._onProcessReady.fire(e)));
this._register(proxy.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e)));
this._register(proxy.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
this._register(proxy.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e)));
this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e)));
this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e)));
this._register(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e)));
return [client, proxy];
}
@@ -141,6 +153,9 @@ export class LocalPtyService extends Disposable implements IPtyService {
detachFromProcess(id: number): Promise<void> {
return this._proxy.detachFromProcess(id);
}
listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> {
return this._proxy.listProcesses(reduceGraceTime);
}
start(id: number): Promise<ITerminalLaunchError | undefined> {
return this._proxy.start(id);
@@ -166,7 +181,11 @@ export class LocalPtyService extends Disposable implements IPtyService {
getLatency(id: number): Promise<number> {
return this._proxy.getLatency(id);
}
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void {
orphanQuestionReply(id: number): Promise<void> {
return this._proxy.orphanQuestionReply(id);
}
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
return this._proxy.setTerminalLayoutInfo(args);
}
async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {

View File

@@ -5,13 +5,14 @@
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById } from 'vs/platform/terminal/common/terminal';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess';
import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IPtyHostDescriptionDto, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { ILogService } from 'vs/platform/log/common/log';
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
type WorkspaceId = string;
@@ -34,10 +35,14 @@ export class PtyService extends Disposable implements IPtyService {
readonly onProcessReady = this._onProcessReady.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event;
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>());
readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event;
private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>());
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
constructor(
private _lastPtyId: number,
@@ -69,7 +74,7 @@ export class PtyService extends Disposable implements IPtyService {
workspaceId: string,
workspaceName: string
): Promise<number> {
if (shellLaunchConfig.attachPersistentTerminal) {
if (shellLaunchConfig.attachPersistentProcess) {
throw new Error('Attempt to create a process when attach object was provided');
}
const id = ++this._lastPtyId;
@@ -82,24 +87,26 @@ export class PtyService extends Disposable implements IPtyService {
if (process.onProcessResolvedShellLaunchConfig) {
process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event }));
}
const persistentTerminalProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
process.onProcessExit(() => {
persistentTerminalProcess.dispose();
persistentProcess.dispose();
this._ptys.delete(id);
});
persistentTerminalProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event }));
persistentTerminalProcess.onProcessReady(event => this._onProcessReady.fire({ id, event }));
persistentTerminalProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event }));
this._ptys.set(id, persistentTerminalProcess);
persistentProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event }));
persistentProcess.onProcessReady(event => this._onProcessReady.fire({ id, event }));
persistentProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event }));
persistentProcess.onProcessShellTypeChanged(event => this._onProcessShellTypeChanged.fire({ id, event }));
persistentProcess.onProcessOrphanQuestion(() => this._onProcessOrphanQuestion.fire({ id }));
this._ptys.set(id, persistentProcess);
return id;
}
async attachToProcess(id: number): Promise<void> {
try {
this._throwIfNoPty(id).attach();
this._logService.trace(`Persistent terminal reconnection "${id}"`);
this._logService.trace(`Persistent process reconnection "${id}"`);
} catch (e) {
this._logService.trace(`Persistent terminal reconnection "${id}" failed`, e.message);
this._logService.trace(`Persistent process reconnection "${id}" failed`, e.message);
}
}
@@ -107,6 +114,21 @@ export class PtyService extends Disposable implements IPtyService {
this._throwIfNoPty(id).detach();
}
async listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> {
if (reduceGraceTime) {
for (const pty of this._ptys.values()) {
pty.reduceGraceTime();
}
}
const persistentProcesses = Array.from(this._ptys.entries()).filter(([_, pty]) => pty.shouldPersistTerminal);
this._logService.info(`Listing ${persistentProcesses.length} persistent terminals, ${this._ptys.size} total terminals`);
const promises = persistentProcesses.map(async ([id, terminalProcessData]) => this._buildProcessDetails(id, terminalProcessData));
const allTerminals = await Promise.all(promises);
return allTerminals.filter(entry => entry.isOrphan);
}
async start(id: number): Promise<ITerminalLaunchError | undefined> {
return this._throwIfNoPty(id).start();
}
@@ -131,6 +153,9 @@ export class PtyService extends Disposable implements IPtyService {
async getLatency(id: number): Promise<number> {
return 0;
}
async orphanQuestionReply(id: number): Promise<void> {
return this._throwIfNoPty(id).orphanQuestionReply();
}
async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
this._workspaceLayoutInfos.set(args.workspaceId, args);
@@ -150,20 +175,20 @@ export class PtyService extends Disposable implements IPtyService {
private async _expandTerminalTab(tab: ITerminalTabLayoutInfoById): Promise<ITerminalTabLayoutInfoDto> {
const expandedTerminals = (await Promise.all(tab.terminals.map(t => this._expandTerminalInstance(t))));
const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto>[];
const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo<IProcessDetails>[];
return {
isActive: tab.isActive,
activePersistentTerminalId: tab.activePersistentTerminalId,
activePersistentProcessId: tab.activePersistentProcessId,
terminals: filtered
};
}
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto | null>> {
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IProcessDetails | null>> {
try {
const persistentTerminalProcess = this._throwIfNoPty(t.terminal);
const termDto = persistentTerminalProcess && await this._terminalToDto(t.terminal, persistentTerminalProcess);
const persistentProcess = this._throwIfNoPty(t.terminal);
const processDetails = persistentProcess && await this._buildProcessDetails(t.terminal, persistentProcess);
return {
terminal: termDto ?? null,
terminal: processDetails ?? null,
relativeSize: t.relativeSize
};
} catch (e) {
@@ -176,14 +201,14 @@ export class PtyService extends Disposable implements IPtyService {
}
}
private async _terminalToDto(id: number, persistentTerminalProcess: PersistentTerminalProcess): Promise<IPtyHostDescriptionDto> {
const [cwd, isOrphan] = await Promise.all([persistentTerminalProcess.getCwd(), persistentTerminalProcess.isOrphaned()]);
private async _buildProcessDetails(id: number, persistentProcess: PersistentTerminalProcess): Promise<IProcessDetails> {
const [cwd, isOrphan] = await Promise.all([persistentProcess.getCwd(), persistentProcess.isOrphaned()]);
return {
id,
title: persistentTerminalProcess.title,
pid: persistentTerminalProcess.pid,
workspaceId: persistentTerminalProcess.workspaceId,
workspaceName: persistentTerminalProcess.workspaceName,
title: persistentProcess.title,
pid: persistentProcess.pid,
workspaceId: persistentProcess.workspaceId,
workspaceName: persistentProcess.workspaceName,
cwd,
isOrphan
};
@@ -200,7 +225,7 @@ export class PtyService extends Disposable implements IPtyService {
export class PersistentTerminalProcess extends Disposable {
// private readonly _bufferer: TerminalDataBufferer;
private readonly _bufferer: TerminalDataBufferer;
private readonly _pendingCommands = new Map<number, { resolve: (data: any) => void; reject: (err: any) => void; }>();
@@ -219,10 +244,14 @@ export class PersistentTerminalProcess extends Disposable {
readonly onProcessReady = this._onProcessReady.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>());
readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event;
private readonly _onProcessData = this._register(new Emitter<IProcessDataEvent>());
readonly onProcessData = this._onProcessData.event;
private readonly _onProcessOrphanQuestion = this._register(new Emitter<void>());
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
private _inReplay = false;
@@ -233,7 +262,7 @@ export class PersistentTerminalProcess extends Disposable {
get title(): string { return this._terminalProcess.currentTitle; }
constructor(
private _persistentTerminalId: number,
private _persistentProcessId: number,
private readonly _terminalProcess: TerminalProcess,
public readonly workspaceId: string,
public readonly workspaceName: string,
@@ -246,36 +275,29 @@ export class PersistentTerminalProcess extends Disposable {
this._orphanQuestionBarrier = null;
this._orphanQuestionReplyTime = 0;
this._disconnectRunner1 = this._register(new RunOnceScheduler(() => {
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, shutting down pid "${this._pid}"`);
this.shutdown(true);
}, LocalReconnectConstants.ReconnectionGraceTime));
this._disconnectRunner2 = this._register(new RunOnceScheduler(() => {
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, shutting down pid ${this._pid}`);
this.shutdown(true);
}, LocalReconnectConstants.ReconnectionShortGraceTime));
// TODO: Bring back bufferer
// this._bufferer = new TerminalDataBufferer((id, data) => {
// const ev: IPtyHostProcessDataEvent = {
// type: 'data',
// data: data
// };
// this._events.fire(ev);
// });
this._register(this._terminalProcess.onProcessReady(e => {
this._pid = e.pid;
this._cwd = e.cwd;
this._onProcessReady.fire(e);
}));
this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e)));
this._register(this._terminalProcess.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
// Buffer data events to reduce the amount of messages going to the renderer
// this._register(this._bufferer.startBuffering(this._persistentTerminalId, this._terminalProcess.onProcessData));
this._register(this._terminalProcess.onProcessData(e => this._recorder.recordData(e)));
this._register(this._terminalProcess.onProcessExit(exitCode => {
// this._bufferer.stopBuffering(this._persistentTerminalId);
}));
// Data buffering to reduce the amount of messages going to the renderer
this._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire({ data: data, sync: true }));
this._register(this._bufferer.startBuffering(this._persistentProcessId, this._terminalProcess.onProcessData));
this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId)));
// Data recording for reconnect
this._register(this.onProcessData(e => this._recorder.recordData(e.data)));
}
attach(): void {
@@ -301,6 +323,7 @@ export class PersistentTerminalProcess extends Disposable {
} else {
this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd });
this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle);
this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType);
this.triggerReplay();
}
return undefined;
@@ -319,6 +342,9 @@ export class PersistentTerminalProcess extends Disposable {
return;
}
this._recorder.recordResize(cols, rows);
// Buffered events should flush when a resize occurs
this._bufferer.flushBuffer(this._persistentProcessId);
return this._terminalProcess.resize(cols, rows);
}
acknowledgeDataEvent(charCount: number): void {
@@ -344,7 +370,7 @@ export class PersistentTerminalProcess extends Disposable {
dataLength += e.data.length;
}
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
this._logService.info(`Persistent process "${this._persistentProcessId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
this._onProcessReplay.fire(ev);
this._terminalProcess.clearUnacknowledgedChars();
}
@@ -357,7 +383,7 @@ export class PersistentTerminalProcess extends Disposable {
this._pendingCommands.delete(reqId);
}
async orphanQuestionReply(): Promise<void> {
orphanQuestionReply(): void {
this._orphanQuestionReplyTime = Date.now();
if (this._orphanQuestionBarrier) {
const barrier = this._orphanQuestionBarrier;
@@ -382,19 +408,17 @@ export class PersistentTerminalProcess extends Disposable {
}
private async _isOrphaned(): Promise<boolean> {
// The process is already known to be orphaned
if (this._disconnectRunner1.isScheduled() || this._disconnectRunner2.isScheduled()) {
return true;
}
// Ask whether the renderer(s) whether the process is orphaned and await the reply
if (!this._orphanQuestionBarrier) {
// the barrier opens after 4 seconds with or without a reply
this._orphanQuestionBarrier = new AutoOpenBarrier(4000);
this._orphanQuestionReplyTime = 0;
// TODO: Fire?
// const ev: IPtyHostProcessOrphanQuestionEvent = {
// type: 'orphan?'
// };
// this._events.fire(ev);
this._onProcessOrphanQuestion.fire();
}
await this._orphanQuestionBarrier.wait();

View File

@@ -5,6 +5,7 @@
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as process from 'vs/base/common/process';
import { exists } from 'vs/base/node/pfs';
import { isString } from 'vs/base/common/types';
import { getCaseInsensitive } from 'vs/base/common/objects';

View File

@@ -10,12 +10,13 @@ import * as fs from 'fs';
import * as os from 'os';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride } from 'vs/platform/terminal/common/terminal';
import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { exec } from 'child_process';
import { ILogService } from 'vs/platform/log/common/log';
import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper';
// Writing large amounts of data can be corrupted for some reason, after looking into this is
// appears to be a race condition around writing to the FD which may be based on how powerful the
@@ -26,7 +27,7 @@ const WRITE_INTERVAL_MS = 5;
const enum ShutdownConstants {
/**
* The amount of time that must pass between data events after exit is queued before the actual
* The amount of ms that must pass between data events after exit is queued before the actual
* kill call is triggered. This data flush mechanism works around an [issue in node-pty][1]
* where not all data is flushed which causes problems for task problem matchers. Additionally
* on Windows under conpty, killing a process while data is being output will cause the [conhost
@@ -38,7 +39,7 @@ const enum ShutdownConstants {
*/
DataFlushTimeout = 250,
/**
* The maximum time to allow after dispose is called because forcefully killing the process.
* The maximum ms to allow after dispose is called because forcefully killing the process.
*/
MaximumShutdownTime = 5000
}
@@ -54,6 +55,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
private _currentTitle: string = '';
private _processStartupComplete: Promise<void> | undefined;
private _isDisposed: boolean = false;
private _windowsShellHelper: WindowsShellHelper | undefined;
private _titleInterval: NodeJS.Timer | null = null;
private _writeQueue: string[] = [];
private _writeTimeout: NodeJS.Timeout | undefined;
@@ -63,9 +65,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
private _isPtyPaused: boolean = false;
private _unacknowledgedCharCount: number = 0;
public get exitMessage(): string | undefined { return this._exitMessage; }
public get currentTitle(): string { return this._currentTitle; }
public get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; }
public get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; }
private readonly _onProcessData = this._register(new Emitter<string>());
public get onProcessData(): Event<string> { return this._onProcessData.event; }
@@ -75,6 +78,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; }
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>());
public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
constructor(
private readonly _shellLaunchConfig: IShellLaunchConfig,
@@ -111,15 +116,23 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
conptyInheritCursor: useConpty && !!_shellLaunchConfig.initialText
};
// Delay resizes to avoid conpty not respecting very early resize calls
if (platform.isWindows && useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) {
this._delayedResizer = new DelayedResizer();
this._register(this._delayedResizer.onTrigger(dimensions => {
this._delayedResizer?.dispose();
this._delayedResizer = undefined;
if (dimensions.cols && dimensions.rows) {
this.resize(dimensions.cols, dimensions.rows);
}
}));
if (platform.isWindows) {
if (useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) {
this._delayedResizer = new DelayedResizer();
this._register(this._delayedResizer.onTrigger(dimensions => {
this._delayedResizer?.dispose();
this._delayedResizer = undefined;
if (dimensions.cols && dimensions.rows) {
this.resize(dimensions.cols, dimensions.rows);
}
}));
}
// WindowsShellHelper is used to fetch the process title and shell type
this.onProcessReady(e => {
this._windowsShellHelper = this._register(new WindowsShellHelper(e.pid));
this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
this._register(this._windowsShellHelper.onShellNameChanged(e => this._onProcessTitleChanged.fire(e)));
});
}
}
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined> | undefined;
@@ -188,19 +201,22 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
this.onProcessReady(() => c());
});
ptyProcess.onData(data => {
if (this._shellLaunchConfig.flowControl) {
this._unacknowledgedCharCount += data.length;
if (!this._isPtyPaused && this._unacknowledgedCharCount > FlowControlConstants.HighWatermarkChars) {
this._logService.trace(`Flow control: Pause (${this._unacknowledgedCharCount} > ${FlowControlConstants.HighWatermarkChars})`);
this._isPtyPaused = true;
ptyProcess.pause();
}
// Handle flow control
this._unacknowledgedCharCount += data.length;
if (!this._isPtyPaused && this._unacknowledgedCharCount > FlowControlConstants.HighWatermarkChars) {
this._logService.trace(`Flow control: Pause (${this._unacknowledgedCharCount} > ${FlowControlConstants.HighWatermarkChars})`);
this._isPtyPaused = true;
ptyProcess.pause();
}
// Refire the data event
this._onProcessData.fire(data);
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
this._queueProcessExit();
}
this._windowsShellHelper?.checkShell();
});
ptyProcess.onExit(e => {
this._exitCode = e.exitCode;
@@ -216,10 +232,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
clearInterval(this._titleInterval);
}
this._titleInterval = null;
this._onProcessData.dispose();
this._onProcessExit.dispose();
this._onProcessReady.dispose();
this._onProcessTitleChanged.dispose();
super.dispose();
}
@@ -369,9 +381,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
}
public acknowledgeDataEvent(charCount: number): void {
if (!this._shellLaunchConfig.flowControl) {
return;
}
// Prevent lower than 0 to heal from errors
this._unacknowledgedCharCount = Math.max(this._unacknowledgedCharCount - charCount, 0);
this._logService.trace(`Flow control: Ack ${charCount} chars (unacknowledged: ${this._unacknowledgedCharCount})`);
@@ -383,10 +392,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
}
public clearUnacknowledgedChars(): void {
if (!this._shellLaunchConfig.flowControl) {
return;
}
this._unacknowledgedCharCount = 0;
this._logService.trace(`Flow control: Cleared all unacknowledged chars, forcing resume`);
if (this._isPtyPaused) {

View File

@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import type * as WindowsProcessTreeType from 'windows-process-tree';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { debounce } from 'vs/base/common/decorators';
import { timeout } from 'vs/base/common/async';
export interface IWindowsShellHelper extends IDisposable {
readonly onShellNameChanged: Event<string>;
readonly onShellTypeChanged: Event<TerminalShellType>;
getShellType(title: string): TerminalShellType;
getShellName(): Promise<string>;
}
const SHELL_EXECUTABLES = [
'cmd.exe',
'powershell.exe',
'pwsh.exe',
'bash.exe',
'wsl.exe',
'ubuntu.exe',
'ubuntu1804.exe',
'kali.exe',
'debian.exe',
'opensuse-42.exe',
'sles-12.exe'
];
let windowsProcessTree: typeof WindowsProcessTreeType;
export class WindowsShellHelper extends Disposable implements IWindowsShellHelper {
private _isDisposed: boolean;
private _currentRequest: Promise<string> | undefined;
private _shellType: TerminalShellType | undefined;
public get shellType(): TerminalShellType | undefined { return this._shellType; }
private _shellTitle: string = '';
public get shellTitle(): string { return this._shellTitle; }
private readonly _onShellNameChanged = new Emitter<string>();
public get onShellNameChanged(): Event<string> { return this._onShellNameChanged.event; }
private readonly _onShellTypeChanged = new Emitter<TerminalShellType>();
public get onShellTypeChanged(): Event<TerminalShellType> { return this._onShellTypeChanged.event; }
public constructor(
private _rootProcessId: number
) {
super();
if (!platform.isWindows) {
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`);
}
this._isDisposed = false;
this._startMonitoringShell();
}
private async _startMonitoringShell(): Promise<void> {
if (this._isDisposed) {
return;
}
this.checkShell();
}
@debounce(500)
async checkShell(): Promise<void> {
if (platform.isWindows) {
// Wait to give the shell some time to actually launch a process, this
// could lead to a race condition but it would be recovered from when
// data stops and should cover the majority of cases
await timeout(300);
this.getShellName().then(title => {
const type = this.getShellType(title);
if (type !== this._shellType) {
this._onShellTypeChanged.fire(type);
this._onShellNameChanged.fire(title);
this._shellType = type;
this._shellTitle = title;
}
});
}
}
private traverseTree(tree: any): string {
if (!tree) {
return '';
}
if (SHELL_EXECUTABLES.indexOf(tree.name) === -1) {
return tree.name;
}
if (!tree.children || tree.children.length === 0) {
return tree.name;
}
let favouriteChild = 0;
for (; favouriteChild < tree.children.length; favouriteChild++) {
const child = tree.children[favouriteChild];
if (!child.children || child.children.length === 0) {
break;
}
if (child.children[0].name !== 'conhost.exe') {
break;
}
}
if (favouriteChild >= tree.children.length) {
return tree.name;
}
return this.traverseTree(tree.children[favouriteChild]);
}
public dispose(): void {
this._isDisposed = true;
super.dispose();
}
/**
* Returns the innermost shell executable running in the terminal
*/
public getShellName(): Promise<string> {
if (this._isDisposed) {
return Promise.resolve('');
}
// Prevent multiple requests at once, instead return current request
if (this._currentRequest) {
return this._currentRequest;
}
this._currentRequest = new Promise<string>(async resolve => {
if (!windowsProcessTree) {
windowsProcessTree = await import('windows-process-tree');
}
windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => {
const name = this.traverseTree(tree);
this._currentRequest = undefined;
resolve(name);
});
});
return this._currentRequest;
}
public getShellType(executable: string): TerminalShellType {
switch (executable.toLowerCase()) {
case 'cmd.exe':
return WindowsShellType.CommandPrompt;
case 'powershell.exe':
case 'pwsh.exe':
return WindowsShellType.PowerShell;
case 'bash.exe':
case 'git-cmd.exe':
return WindowsShellType.GitBash;
case 'wsl.exe':
case 'ubuntu.exe':
case 'ubuntu1804.exe':
case 'kali.exe':
case 'debian.exe':
case 'opensuse-42.exe':
case 'sles-12.exe':
return WindowsShellType.Wsl;
default:
return undefined;
}
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
function eventsEqual(recorder: TerminalRecorder, expected: ReplayEntry[]) {
const actual = recorder.generateReplayEvent().events;
for (let i = 0; i < expected.length; i++) {
assert.deepStrictEqual(actual[i], expected[i]);
}
}
suite('TerminalRecorder', () => {
test('should record dimensions', () => {
const recorder = new TerminalRecorder(1, 2);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: '' }
]);
recorder.recordData('a');
recorder.recordResize(3, 4);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: 'a' },
{ cols: 3, rows: 4, data: '' }
]);
});
test('should ignore resize events without data', () => {
const recorder = new TerminalRecorder(1, 2);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: '' }
]);
recorder.recordResize(3, 4);
eventsEqual(recorder, [
{ cols: 3, rows: 4, data: '' }
]);
});
test('should record data and combine it into the previous resize event', () => {
const recorder = new TerminalRecorder(1, 2);
recorder.recordData('a');
recorder.recordData('b');
recorder.recordResize(3, 4);
recorder.recordData('c');
recorder.recordData('d');
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: 'ab' },
{ cols: 3, rows: 4, data: 'cd' }
]);
});
});

View File

@@ -36,7 +36,7 @@ export interface ColorDefaults {
}
/**
* A Color Value is either a color literal, a refence to other color or a derived color
* A Color Value is either a color literal, a reference to an other color or a derived color
*/
export type ColorValue = Color | string | ColorIdentifier | ColorFunction;

View File

@@ -41,7 +41,7 @@ configurationRegistry.registerConfiguration({
default: true,
scope: ConfigurationScope.APPLICATION,
title: localize('enableWindowsBackgroundUpdatesTitle', "Enable Background Updates on Windows"),
description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code Versions in the background on Windows"),
description: localize('enableWindowsBackgroundUpdates', "Enable to download and install new VS Code versions in the background on Windows."),
included: isWindows && !isWeb
},
'update.showReleaseNotes': {

View File

@@ -7,15 +7,15 @@ import { Event, Emitter } from 'vs/base/common/event';
import { timeout } from 'vs/base/common/async';
import { IConfigurationService, getMigratedSettingValue } from 'vs/platform/configuration/common/configuration';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { IUpdateService, State, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { ILogService } from 'vs/platform/log/common/log';
import { IRequestService } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
export function createUpdateURL(platform: string, quality: string): string {
return `${product.updateUrl}/api/update/${platform}/${quality}/${product.commit}`;
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
}
export type UpdateNotAvailableClassification = {
@@ -49,6 +49,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@IRequestService protected requestService: IRequestService,
@ILogService protected logService: ILogService,
@IProductService protected readonly productService: IProductService
) { }
/**
@@ -66,7 +67,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
return;
}
if (!product.updateUrl || !product.commit) {
if (!this.productService.updateUrl || !this.productService.commit) {
this.logService.info('update#ctor - updates are disabled as there is no update URL');
return;
}
@@ -104,7 +105,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
}
private getProductQuality(updateMode: string): string | undefined {
return updateMode === 'none' ? undefined : product.quality;
return updateMode === 'none' ? undefined : this.productService.quality;
}
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {

View File

@@ -15,7 +15,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e
import { ILogService } from 'vs/platform/log/common/log';
import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService';
import { IRequestService } from 'vs/platform/request/common/request';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
export class DarwinUpdateService extends AbstractUpdateService {
@@ -34,9 +34,10 @@ export class DarwinUpdateService extends AbstractUpdateService {
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService
@ILogService logService: ILogService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
initialize(): void {
@@ -58,12 +59,12 @@ export class DarwinUpdateService extends AbstractUpdateService {
protected buildUpdateFeedUrl(quality: string): string | undefined {
let assetID: string;
if (!product.darwinUniversalAssetId) {
if (!this.productService.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
} else {
assetID = product.darwinUniversalAssetId;
assetID = this.productService.darwinUniversalAssetId;
}
const url = createUpdateURL(assetID, quality);
const url = createUpdateURL(assetID, quality, this.productService);
try {
electron.autoUpdater.setFeedURL({ url });
} catch (e) {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { State, IUpdate, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
@@ -26,13 +26,14 @@ export class LinuxUpdateService extends AbstractUpdateService {
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
protected buildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality);
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
}
protected doCheckForUpdates(context: any): void {
@@ -64,8 +65,8 @@ export class LinuxUpdateService extends AbstractUpdateService {
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
// Use the download URL if available as we don't currently detect the package type that was
// installed and the website download page is more useful than the tarball generally.
if (product.downloadUrl && product.downloadUrl.length > 0) {
this.nativeHostMainService.openExternal(undefined, product.downloadUrl);
if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) {
this.nativeHostMainService.openExternal(undefined, this.productService.downloadUrl);
} else if (state.update.url) {
this.nativeHostMainService.openExternal(undefined, state.update.url);
}

View File

@@ -9,7 +9,7 @@ import * as pfs from 'vs/base/node/pfs';
import { memoize } from 'vs/base/common/decorators';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { State, IUpdate, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
@@ -55,7 +55,7 @@ export class Win32UpdateService extends AbstractUpdateService {
@memoize
get cachePath(): Promise<string> {
const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`);
const result = path.join(tmpdir(), `vscode-update-${this.productService.target}-${process.arch}`);
return fs.promises.mkdir(result, { recursive: true }).then(() => result);
}
@@ -67,9 +67,10 @@ export class Win32UpdateService extends AbstractUpdateService {
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@IFileService private readonly fileService: IFileService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProductService productService: IProductService
) {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService);
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
initialize(): void {
@@ -86,7 +87,7 @@ export class Win32UpdateService extends AbstractUpdateService {
"target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('update:win32SetupTarget', { target: product.target });
this.telemetryService.publicLog('update:win32SetupTarget', { target: this.productService.target });
}
}
@@ -99,11 +100,11 @@ export class Win32UpdateService extends AbstractUpdateService {
if (getUpdateType() === UpdateType.Archive) {
platform += '-archive';
} else if (product.target === 'user') {
} else if (this.productService.target === 'user') {
platform += '-user';
}
return createUpdateURL(platform, quality);
return createUpdateURL(platform, quality, this.productService);
}
protected doCheckForUpdates(context: any): void {
@@ -155,7 +156,7 @@ export class Win32UpdateService extends AbstractUpdateService {
this.availableUpdate = { packagePath };
if (fastUpdatesEnabled && update.supportsFastUpdate) {
if (product.target === 'user') {
if (this.productService.target === 'user') {
this.doApplyUpdate();
} else {
this.setState(State.Downloaded(update));
@@ -185,11 +186,11 @@ export class Win32UpdateService extends AbstractUpdateService {
private async getUpdatePackagePath(version: string): Promise<string> {
const cachePath = await this.cachePath;
return path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`);
return path.join(cachePath, `CodeSetup-${this.productService.quality}-${version}.exe`);
}
private async cleanup(exceptVersion: string | null = null): Promise<any> {
const filter = exceptVersion ? (one: string) => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true;
const filter = exceptVersion ? (one: string) => !(new RegExp(`${this.productService.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true;
const cachePath = await this.cachePath;
const versions = await pfs.readdir(cachePath);
@@ -219,7 +220,7 @@ export class Win32UpdateService extends AbstractUpdateService {
const cachePath = await this.cachePath;
this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${update.version}.flag`);
this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`);
await pfs.writeFile(this.availableUpdate.updateFilePath, 'flag');
const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
@@ -233,7 +234,7 @@ export class Win32UpdateService extends AbstractUpdateService {
this.setState(State.Idle(getUpdateType()));
});
const readyMutexName = `${product.win32MutexName}-ready`;
const readyMutexName = `${this.productService.win32MutexName}-ready`;
const mutex = await import('windows-mutex');
// poll for mutex-ready

View File

@@ -7,7 +7,7 @@ import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/commo
import { URI, UriComponents } from 'vs/base/common/uri';
import { first } from 'vs/base/common/async';
import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
export abstract class AbstractURLService extends Disposable implements IURLService {
@@ -30,6 +30,12 @@ export abstract class AbstractURLService extends Disposable implements IURLServi
export class NativeURLService extends AbstractURLService {
constructor(
@IProductService protected readonly productService: IProductService
) {
super();
}
create(options?: Partial<UriComponents>): URI {
let { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined };
@@ -37,6 +43,6 @@ export class NativeURLService extends AbstractURLService {
path = `/${path}`; // URI validation requires a path if there is an authority
}
return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment });
return URI.from({ scheme: this.productService.urlProtocol, authority, path, query, fragment });
}
}

View File

@@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { IURLService } from 'vs/platform/url/common/url';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { app, Event as ElectronEvent } from 'electron';
import { URI } from 'vs/base/common/uri';
import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
@@ -43,7 +43,8 @@ export class ElectronURLListener {
initialUrisToHandle: { uri: URI, url: string }[],
private readonly urlService: IURLService,
windowsMainService: IWindowsMainService,
environmentMainService: IEnvironmentMainService
environmentMainService: IEnvironmentMainService,
productService: IProductService
) {
// the initial set of URIs we need to handle once the window is ready
@@ -53,7 +54,7 @@ export class ElectronURLListener {
if (isWindows) {
const windowsParameters = environmentMainService.isBuilt ? [] : [`"${environmentMainService.appRoot}"`];
windowsParameters.push('--open-url', '--');
app.setAsDefaultProtocolClient(product.urlProtocol, process.execPath, windowsParameters);
app.setAsDefaultProtocolClient(productService.urlProtocol, process.execPath, windowsParameters);
}
// macOS: listen to `open-url` events from here on to handle

View File

@@ -118,6 +118,7 @@ export abstract class AbstractSynchroniser extends Disposable {
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
protected readonly lastSyncResource: URI;
private hasSyncResourceStateVersionChanged: boolean = false;
protected readonly syncResourceLogLabel: string;
private syncHeaders: IHeaders = {};
@@ -566,8 +567,7 @@ export abstract class AbstractSynchroniser extends Disposable {
const machineId = await this.currentMachineIdPromise;
const isLastSyncFromCurrentMachine = !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId;
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine && !this.hasSyncResourceStateVersionChanged ? remoteUserData : lastSyncUserData;
const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
const resourcePreviews: IEditableResourcePreview[] = [];
@@ -616,6 +616,14 @@ export abstract class AbstractSynchroniser extends Disposable {
try {
const content = await this.fileService.readFile(this.lastSyncResource);
const parsed = JSON.parse(content.value.toString());
const resourceSyncStateVersion = this.userDataSyncResourceEnablementService.getResourceSyncStateVersion(this.resource);
this.hasSyncResourceStateVersionChanged = parsed.version && resourceSyncStateVersion && parsed.version !== resourceSyncStateVersion;
if (this.hasSyncResourceStateVersionChanged) {
this.logService.info(`${this.syncResourceLogLabel}: Reset last sync state because last sync state version ${parsed.version} is not compatible with current sync state version ${resourceSyncStateVersion}.`);
await this.resetLocal();
return null;
}
const userData: IUserData = parsed as IUserData;
if (userData.content === null) {
return { ref: parsed.ref, syncData: null } as T;
@@ -637,7 +645,12 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary<any> = {}): Promise<void> {
const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps };
if (additionalProps['ref'] || additionalProps['content'] || additionalProps['version']) {
throw new Error('Cannot have core properties as additional');
}
const version = this.userDataSyncResourceEnablementService.getResourceSyncStateVersion(this.resource);
const lastSyncUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, version, ...additionalProps };
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
}

View File

@@ -408,6 +408,8 @@ export interface IUserDataSyncResourceEnablementService {
readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]>;
isResourceEnabled(resource: SyncResource): boolean;
setResourceEnablement(resource: SyncResource, enabled: boolean): void;
getResourceSyncStateVersion(resource: SyncResource): string | undefined;
}
export interface ISyncTask {

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