mirror of
https://github.com/coder/code-server.git
synced 2026-05-05 20:15:19 +02:00
chore(vscode): update to 1.55.2
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 });
|
||||
30
lib/vscode/src/vs/platform/checksum/node/checksumService.ts
Normal file
30
lib/vscode/src/vs/platform/checksum/node/checksumService.ts
Normal 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(/=+$/, ''))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
1135
lib/vscode/src/vs/platform/checksum/test/node/fixtures/lorem.txt
Normal file
1135
lib/vscode/src/vs/platform/checksum/test/node/fixtures/lorem.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
14
lib/vscode/src/vs/platform/environment/node/userDataPath.d.ts
vendored
Normal file
14
lib/vscode/src/vs/platform/environment/node/userDataPath.d.ts
vendored
Normal 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;
|
||||
99
lib/vscode/src/vs/platform/environment/node/userDataPath.js
Normal file
99
lib/vscode/src/vs/platform/environment/node/userDataPath.js
Normal 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');
|
||||
}
|
||||
}());
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -104,6 +104,7 @@ export interface ILocalExtension extends IExtension {
|
||||
isMachineScoped: boolean;
|
||||
publisherId: string | null;
|
||||
publisherDisplayName: string | null;
|
||||
installedTimestamp?: number;
|
||||
}
|
||||
|
||||
export const enum SortBy {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
197
lib/vscode/src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal file
197
lib/vscode/src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal 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]));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
|
||||
disposables.add(userdataFileProvider);
|
||||
};
|
||||
|
||||
setup(async () => {
|
||||
setup(async function () {
|
||||
this.timeout(15000);
|
||||
await reload();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,8 +25,6 @@ export interface ILocalizationsService {
|
||||
|
||||
readonly onDidLanguagesChange: Event<void>;
|
||||
getLanguageIds(): Promise<string[]>;
|
||||
|
||||
update(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export function isValidLocalization(localization: ILocalization): boolean {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.)")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -251,4 +251,8 @@ export class SharedProcess extends Disposable implements ISharedProcess {
|
||||
this.window.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return this.window?.isVisible() ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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> {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 });
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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> {
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
166
lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts
Normal file
166
lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user