process - convert explorer to editor (#248120)

This commit is contained in:
Benjamin Pasero 2025-05-06 17:21:02 +02:00 committed by GitHub
parent 5f5d2c13f0
commit 708b6aa379
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 870 additions and 26 deletions

View File

@ -94,8 +94,9 @@ export interface IWorkspaceInformation extends IWorkspace {
rendererSessionId: string;
}
export function isRemoteDiagnosticError(x: any): x is IRemoteDiagnosticError {
return !!x.hostName && !!x.errorMessage;
export function isRemoteDiagnosticError(x: unknown): x is IRemoteDiagnosticError {
const candidate = x as IRemoteDiagnosticError | undefined;
return !!candidate?.hostName && !!candidate?.errorMessage;
}
export class NullDiagnosticsService implements IDiagnosticsService {

View File

@ -486,7 +486,7 @@ export class DiagnosticsService implements IDiagnosticsService {
// Format name with indent
let name: string;
if (isRoot) {
name = item.pid === mainPid ? `${this.productService.applicationName} main` : 'remote agent';
name = item.pid === mainPid ? this.productService.applicationName : 'remote-server';
} else {
if (mapProcessToName.has(item.pid)) {
name = mapProcessToName.get(item.pid)!;

View File

@ -7,6 +7,7 @@ import { equals } from '../../../base/common/arrays.js';
import { IDisposable } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IRectangle } from '../../window/common/window.js';
export interface IResolvableEditorModel extends IDisposable {
@ -300,12 +301,25 @@ export interface IEditorOptions {
transient?: boolean;
/**
* A hint that the editor should have compact chrome when showing if possible.
*
* Note: this currently is only working if AUX_GROUP is specified as target to
* open the editor in a floating window.
* Options that only apply when `AUX_WINDOW_GROUP` is used for opening.
*/
compact?: boolean;
auxiliary?: {
/**
* Define the bounds of the editor window.
*/
bounds?: Partial<IRectangle>;
/**
* Show editor compact, hiding unnecessary elements.
*/
compact?: boolean;
/**
* Show the editor always on top of other windows.
*/
alwaysOnTop?: boolean;
};
}
export interface ITextEditorSelection {

View File

@ -108,6 +108,7 @@ export class ExtensionHostStarter extends Disposable implements IDisposable, IEx
extHost.start({
...opts,
type: 'extensionHost',
name: 'extension-host',
entryPoint: 'vs/workbench/api/node/extensionHostProcess',
args: ['--skipWorkspaceStorageLock'],
execArgv: opts.execArgv,

View File

@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProcessItem } from '../../../base/common/processes.js';
import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes.js';
import { PerformanceInfo, SystemInfo } from '../../diagnostics/common/diagnostics.js';
import { IRemoteDiagnosticError, PerformanceInfo, SystemInfo } from '../../diagnostics/common/diagnostics.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
// Since data sent through the service is serialized to JSON, functions will be lost, so Color objects
@ -57,12 +58,21 @@ export interface ProcessExplorerWindowConfiguration extends ISandboxConfiguratio
export const IProcessMainService = createDecorator<IProcessMainService>('processService');
export interface IResolvedProcessInformation {
readonly pidToNames: [number, string][];
readonly processes: { name: string; rootProcess: ProcessItem | IRemoteDiagnosticError }[];
}
export interface IProcessMainService {
readonly _serviceBrand: undefined;
getSystemStatus(): Promise<string>;
stopTracing(): Promise<void>;
openProcessExplorer(data: ProcessExplorerData): Promise<void>;
resolve(): Promise<IResolvedProcessInformation>;
// Used by the process explorer
$getSystemInfo(): Promise<SystemInfo>;
$getPerformanceInfo(): Promise<PerformanceInfo>;

View File

@ -11,12 +11,12 @@ import { IProcessEnvironment, isMacintosh } from '../../../base/common/platform.
import { listProcesses } from '../../../base/node/ps.js';
import { validatedIpcMain } from '../../../base/parts/ipc/electron-main/ipcMain.js';
import { getNLSLanguage, getNLSMessages, localize } from '../../../nls.js';
import { IDiagnosticsService, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from '../../diagnostics/common/diagnostics.js';
import { IDiagnosticsService, IRemoteDiagnosticError, isRemoteDiagnosticError, PerformanceInfo, SystemInfo } from '../../diagnostics/common/diagnostics.js';
import { IDiagnosticsMainService } from '../../diagnostics/electron-main/diagnosticsMainService.js';
import { IDialogMainService } from '../../dialogs/electron-main/dialogMainService.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { ICSSDevelopmentService } from '../../cssDev/node/cssDevService.js';
import { IProcessMainService, ProcessExplorerData, ProcessExplorerWindowConfiguration } from '../common/process.js';
import { IProcessMainService, IResolvedProcessInformation, ProcessExplorerData, ProcessExplorerWindowConfiguration } from '../common/process.js';
import { ILogService } from '../../log/common/log.js';
import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js';
import product from '../../product/common/product.js';
@ -26,6 +26,7 @@ import { IStateService } from '../../state/node/state.js';
import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js';
import { zoomLevelToZoomFactor } from '../../window/common/window.js';
import { IWindowState } from '../../window/electron-main/window.js';
import { ProcessItem } from '../../../base/common/processes.js';
const processExplorerWindowState = 'issue.processExplorerWindowState';
@ -62,6 +63,46 @@ export class ProcessMainService implements IProcessMainService {
this.registerListeners();
}
async resolve(): Promise<IResolvedProcessInformation> {
const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics();
const pidToNames: [number, string][] = [];
for (const window of mainProcessInfo.windows) {
pidToNames.push([window.pid, `window [${window.id}] (${window.title})`]);
}
for (const { pid, name } of UtilityProcess.getAll()) {
pidToNames.push([pid, name]);
}
const processes: { name: string; rootProcess: ProcessItem | IRemoteDiagnosticError }[] = [];
try {
processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(process.pid) });
const remoteDiagnostics = await this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true });
remoteDiagnostics.forEach(data => {
if (isRemoteDiagnosticError(data)) {
processes.push({
name: data.hostName,
rootProcess: data
});
} else {
if (data.processes) {
processes.push({
name: data.hostName,
rootProcess: data.processes
});
}
}
});
} catch (e) {
this.logService.error(`Listing processes failed: ${e}`);
}
return { pidToNames, processes };
}
//#region Register Listeners
private registerListeners(): void {

View File

@ -169,6 +169,7 @@ export class SharedProcess extends Disposable {
this.utilityProcess.start({
type: 'shared-process',
name: 'shared-process',
entryPoint: 'vs/code/electron-utility/sharedProcess/sharedProcessMain',
payload: this.createSharedProcessConfiguration(),
respondToAuthRequestsFromMainProcess: true,

View File

@ -57,6 +57,7 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte
this.utilityProcess.start({
type: 'ptyHost',
name: 'pty-host',
entryPoint: 'vs/platform/terminal/node/ptyHostMain',
execArgv,
args: ['--logsPath', this._environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath],

View File

@ -15,6 +15,11 @@ export interface IUtilityProcessWorkerProcess {
* forked process to identify it easier.
*/
readonly type: string;
/**
* A human-readable name for the utility process.
*/
readonly name: string;
}
export interface IOnDidTerminateUtilityrocessWorkerProcess {

View File

@ -26,6 +26,11 @@ export interface IUtilityProcessConfiguration {
*/
readonly type: string;
/**
* A human-readable name for the utility process.
*/
readonly name: string;
/**
* The entry point to load in the utility process.
*/
@ -306,7 +311,7 @@ export class UtilityProcess extends Disposable {
this.processPid = process.pid;
if (typeof process.pid === 'number') {
UtilityProcess.all.set(process.pid, { pid: process.pid, name: isWindowUtilityProcessConfiguration(configuration) ? `${configuration.type} [${configuration.responseWindowId}]` : configuration.type });
UtilityProcess.all.set(process.pid, { pid: process.pid, name: isWindowUtilityProcessConfiguration(configuration) ? `${configuration.name} [${configuration.responseWindowId}]` : configuration.name });
}
this.log('successfully created', Severity.Info);

View File

@ -126,6 +126,7 @@ class UtilityProcessWorker extends Disposable {
return this.utilityProcess.start({
type: this.configuration.process.type,
name: this.configuration.process.name,
entryPoint: this.configuration.process.moduleId,
parentLifecycleBound: windowPid,
windowLifecycleBound: true,

View File

@ -429,4 +429,3 @@ export function zoomLevelToZoomFactor(zoomLevel = 0): number {
export const DEFAULT_WINDOW_SIZE = { width: 1200, height: 800 } as const;
export const DEFAULT_AUX_WINDOW_SIZE = { width: 1024, height: 768 } as const;
export const DEFAULT_COMPACT_AUX_WINDOW_SIZE = { width: 640, height: 640 } as const;

View File

@ -101,7 +101,7 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew
const widget = (_sessionId ? widgetService.getWidgetBySessionId(_sessionId) : undefined)
?? widgetService.lastFocusedWidget;
if (!widget || !widget.viewModel || widget.location !== ChatAgentLocation.Panel) {
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true, compact: moveTo === MoveToNewLocation.Window } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP);
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP);
return;
}
@ -111,7 +111,7 @@ async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNew
widget.clear();
await widget.waitForReady();
const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState, compact: moveTo === MoveToNewLocation.Window };
const options: IChatEditorOptions = { target: { sessionId }, pinned: true, viewState, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } };
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP);
}

View File

@ -248,13 +248,13 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {
const msgContainer = append(desc, $('div.msg'));
const actionbar = new ActionBar(desc);
actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));
const listener = actionbar.onDidRun(({ error }) => error && this._notificationService.error(error));
const timeContainer = append(element, $('.time'));
const activationTime = append(timeContainer, $('div.activation-time'));
const profileTime = append(timeContainer, $('div.profile-time'));
const disposables = [actionbar];
const disposables = [actionbar, listener];
return {
root,
@ -468,7 +468,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {
this._list.splice(0, this._list.length, this._elements || undefined);
this._list.onContextMenu((e) => {
this._register(this._list.onContextMenu((e) => {
if (!e.element) {
return;
}
@ -504,7 +504,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane {
getAnchor: () => e.anchor,
getActions: () => actions
});
});
}));
}
public layout(dimension: Dimension): void {

View File

@ -16,6 +16,9 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres
import { IProcessMainService } from '../../../../platform/process/common/process.js';
import './processService.js';
import './processMainService.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
import { ProcessExplorerEditorInput } from '../../processExplorer/electron-sandbox/processExplorerEditoInput.js';
//#region Commands
@ -34,8 +37,14 @@ class OpenProcessExplorer extends Action2 {
override async run(accessor: ServicesAccessor): Promise<void> {
const processService = accessor.get(IWorkbenchProcessService);
const configurationService = accessor.get(IConfigurationService);
const editorService = accessor.get(IEditorService);
return processService.openProcessExplorer();
if (configurationService.getValue('application.useNewProcessExplorer') !== true) {
return processService.openProcessExplorer();
}
editorService.openEditor({ resource: ProcessExplorerEditorInput.RESOURCE, options: { pinned: true, auxiliary: { compact: true, bounds: { width: 800, height: 500 }, alwaysOnTop: true } } }, AUX_WINDOW_GROUP);
}
}
registerAction2(OpenProcessExplorer);

View File

@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.process-explorer .row {
display: flex;
}
.process-explorer .row .cell:not(:first-of-type) {
padding-left: 10px;
}
.process-explorer .row .cell:not(:last-of-type) {
padding-right: 10px;
}
.process-explorer .row:not(.header) .cell {
border-right: 1px solid var(--vscode-tree-tableColumnsBorder);
}
.process-explorer .row.header {
font-weight: 600;
border-bottom: 1px solid var(--vscode-tree-tableColumnsBorder);
}
.process-explorer .row .cell.name {
text-align: left;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.process-explorer .row .cell.cpu {
flex: 0 0 60px;
}
.process-explorer .row .cell.memory {
flex: 0 0 90px;
}
.process-explorer .row .cell.pid {
flex: 0 0 50px;
}
.mac:not(.fullscreen) .process-explorer .monaco-list:focus::before {
/* Rounded corners to make focus outline appear properly (unless fullscreen) */
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
.mac:not(.fullscreen).macos-bigsur-or-newer .process-explorer .monaco-list:focus::before {
/* macOS Big Sur increased rounded corners size */
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
}
.process-explorer .monaco-list-row:first-of-type {
border-bottom: 1px solid var(--vscode-tree-tableColumnsBorder);
}

View File

@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IEditorSerializer, EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js';
import { ProcessExplorerEditorInput } from './processExplorerEditoInput.js';
import { ProcessExplorerEditor } from './processExplorerEditor.js';
class ProcessExplorerEditorContribution implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.processExplorerEditor';
constructor(
@IEditorResolverService editorResolverService: IEditorResolverService,
@IInstantiationService instantiationService: IInstantiationService
) {
editorResolverService.registerEditor(
`${ProcessExplorerEditorInput.RESOURCE.scheme}:**/**`,
{
id: ProcessExplorerEditorInput.ID,
label: localize('promptOpenWith.processExplorer.displayName', "Process Explorer"),
priority: RegisteredEditorPriority.exclusive
},
{
singlePerResource: true,
canSupportResource: resource => resource.scheme === ProcessExplorerEditorInput.RESOURCE.scheme
},
{
createEditorInput: () => {
return {
editor: instantiationService.createInstance(ProcessExplorerEditorInput),
options: {
pinned: true
}
};
}
}
);
}
}
registerWorkbenchContribution2(ProcessExplorerEditorContribution.ID, ProcessExplorerEditorContribution, WorkbenchPhase.BlockStartup);
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
EditorPaneDescriptor.create(ProcessExplorerEditor, ProcessExplorerEditor.ID, localize('processExplorer', "Process Explorer")),
[new SyncDescriptor(ProcessExplorerEditorInput)]
);
class ProcessExplorerEditorInputSerializer implements IEditorSerializer {
canSerialize(editorInput: EditorInput): boolean {
return true;
}
serialize(editorInput: EditorInput): string {
return '';
}
deserialize(instantiationService: IInstantiationService): EditorInput {
return ProcessExplorerEditorInput.instance;
}
}
Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(ProcessExplorerEditorInput.ID, ProcessExplorerEditorInputSerializer);

View File

@ -0,0 +1,515 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/processExplorer.css';
import { localize } from '../../../../nls.js';
import { INativeHostService } from '../../../../platform/native/common/native.js';
import { $, append, Dimension, getDocument } from '../../../../base/browser/dom.js';
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
import { IDataSource, ITreeRenderer, ITreeNode, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';
import { ProcessItem } from '../../../../base/common/processes.js';
import { IRemoteDiagnosticError, isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';
import { ByteSize } from '../../../../platform/files/common/files.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { WorkbenchDataTree } from '../../../../platform/list/browser/listService.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IAction, Separator, toAction } from '../../../../base/common/actions.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { RenderIndentGuides } from '../../../../base/browser/ui/tree/abstractTree.js';
import { isWindows } from '../../../../base/common/platform.js';
import { IProcessMainService } from '../../../../platform/process/common/process.js';
import { Delayer } from '../../../../base/common/async.js';
const DEBUG_FLAGS_PATTERN = /\s--inspect(?:-brk|port)?=(?<port>\d+)?/;
const DEBUG_PORT_PATTERN = /\s--inspect-port=(?<port>\d+)/;
//#region --- process explorer tree
interface IProcessTree {
readonly processes: IProcessInformation;
}
interface IProcessInformation {
readonly processRoots: IMachineProcessInformation[];
}
interface IMachineProcessInformation {
readonly name: string;
readonly rootProcess: ProcessItem | IRemoteDiagnosticError;
}
function isMachineProcessInformation(item: unknown): item is IMachineProcessInformation {
const candidate = item as IMachineProcessInformation | undefined;
return !!candidate?.name && !!candidate?.rootProcess;
}
function isProcessInformation(item: unknown): item is IProcessInformation {
const candidate = item as IProcessInformation | undefined;
return !!candidate?.processRoots;
}
function isProcessItem(item: unknown): item is ProcessItem {
const candidate = item as ProcessItem | undefined;
return typeof candidate?.pid === 'number';
}
class ProcessListDelegate implements IListVirtualDelegate<IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
getHeight() {
return 22;
}
getTemplateId(element: IProcessInformation | IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError) {
if (isProcessItem(element)) {
return 'process';
}
if (isMachineProcessInformation(element)) {
return 'machine';
}
if (isRemoteDiagnosticError(element)) {
return 'error';
}
if (isProcessInformation(element)) {
return 'header';
}
return '';
}
}
class ProcessTreeDataSource implements IDataSource<IProcessTree, IProcessInformation | IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
hasChildren(element: IProcessTree | IProcessInformation | IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError): boolean {
if (isRemoteDiagnosticError(element)) {
return false;
}
if (isProcessItem(element)) {
return !!element.children?.length;
}
return true;
}
getChildren(element: IProcessTree | IProcessInformation | IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError) {
if (isProcessItem(element)) {
return element.children ?? [];
}
if (isRemoteDiagnosticError(element)) {
return [];
}
if (isProcessInformation(element)) {
if (element.processRoots.length > 1) {
return element.processRoots; // If there are multiple process roots, return these, otherwise go directly to the root process
}
if (element.processRoots.length > 0) {
return [element.processRoots[0].rootProcess];
}
return [];
}
if (isMachineProcessInformation(element)) {
return [element.rootProcess];
}
return element.processes ? [element.processes] : [];
}
}
function createRow(container: HTMLElement) {
const row = append(container, $('.row'));
const name = append(row, $('.cell.name'));
const cpu = append(row, $('.cell.cpu'));
const memory = append(row, $('.cell.memory'));
const pid = append(row, $('.cell.pid'));
return { name, cpu, memory, pid };
}
interface IProcessRowTemplateData {
readonly name: HTMLElement;
}
interface IProcessItemTemplateData extends IProcessRowTemplateData {
readonly cpu: HTMLElement;
readonly memory: HTMLElement;
readonly pid: HTMLElement;
}
class ProcessHeaderTreeRenderer implements ITreeRenderer<IProcessInformation, void, IProcessItemTemplateData> {
readonly templateId: string = 'header';
renderTemplate(container: HTMLElement): IProcessItemTemplateData {
return createRow(container);
}
renderElement(node: ITreeNode<IProcessInformation, void>, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void {
templateData.name.textContent = localize('processName', "Process Name");
templateData.cpu.textContent = localize('processCpu', "CPU (%)");
templateData.pid.textContent = localize('processPid', "PID");
templateData.memory.textContent = localize('processMemory', "Memory (MB)");
}
renderTwistie(element: IProcessInformation, twistieElement: HTMLElement): boolean {
return false;
}
disposeTemplate(templateData: unknown): void {
// Nothing to do
}
}
class MachineRenderer implements ITreeRenderer<IMachineProcessInformation, void, IProcessRowTemplateData> {
readonly templateId: string = 'machine';
renderTemplate(container: HTMLElement): IProcessRowTemplateData {
return createRow(container);
}
renderElement(node: ITreeNode<IMachineProcessInformation, void>, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void {
templateData.name.textContent = node.element.name;
}
disposeTemplate(templateData: IProcessRowTemplateData): void {
// Nothing to do
}
}
class ErrorRenderer implements ITreeRenderer<IRemoteDiagnosticError, void, IProcessRowTemplateData> {
readonly templateId: string = 'error';
renderTemplate(container: HTMLElement): IProcessRowTemplateData {
return createRow(container);
}
renderElement(node: ITreeNode<IRemoteDiagnosticError, void>, index: number, templateData: IProcessRowTemplateData, height: number | undefined): void {
templateData.name.textContent = node.element.errorMessage;
}
disposeTemplate(templateData: IProcessRowTemplateData): void {
// Nothing to do
}
}
class ProcessRenderer implements ITreeRenderer<ProcessItem, void, IProcessItemTemplateData> {
readonly templateId: string = 'process';
constructor(private totalMem: number, private model: ProcessExplorerModel) { }
renderTemplate(container: HTMLElement): IProcessItemTemplateData {
return createRow(container);
}
renderElement(node: ITreeNode<ProcessItem, void>, index: number, templateData: IProcessItemTemplateData, height: number | undefined): void {
const { element } = node;
const pid = element.pid.toFixed(0);
templateData.name.textContent = this.model.getName(element.pid, element.name);
templateData.name.title = element.cmd;
templateData.cpu.textContent = element.load.toFixed(0);
templateData.pid.textContent = pid;
templateData.pid.parentElement!.id = `pid-${pid}`;
const memory = isWindows ? element.mem : (this.totalMem * (element.mem / 100));
templateData.memory.textContent = (memory / ByteSize.MB).toFixed(0);
}
disposeTemplate(templateData: IProcessItemTemplateData): void {
// Nothing to do
}
}
class ProcessAccessibilityProvider implements IListAccessibilityProvider<IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
getWidgetAriaLabel(): string {
return localize('processExplorer', "Process Explorer");
}
getAriaLabel(element: IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError): string | null {
if (isProcessItem(element) || isMachineProcessInformation(element)) {
return element.name;
}
if (isRemoteDiagnosticError(element)) {
return element.hostName;
}
return null;
}
}
class ProcessIdentityProvider implements IIdentityProvider<IMachineProcessInformation | ProcessItem | IRemoteDiagnosticError> {
getId(element: IRemoteDiagnosticError | ProcessItem | IMachineProcessInformation): { toString(): string } {
if (isProcessItem(element)) {
return element.pid.toString();
}
if (isRemoteDiagnosticError(element)) {
return element.hostName;
}
if (isProcessInformation(element)) {
return 'processes';
}
if (isMachineProcessInformation(element)) {
return element.name;
}
return 'header';
}
}
//#endregion
export class ProcessExplorerControl extends Disposable {
private dimensions: Dimension | undefined = undefined;
private readonly model: ProcessExplorerModel;
private tree: WorkbenchDataTree<IProcessTree, IProcessTree | IMachineProcessInformation | ProcessItem | IProcessInformation | IRemoteDiagnosticError> | undefined;
private readonly delayer = this._register(new Delayer(1000));
constructor(
container: HTMLElement,
@INativeHostService private readonly nativeHostService: INativeHostService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IProductService private readonly productService: IProductService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ICommandService private readonly commandService: ICommandService,
@IProcessMainService private readonly processMainService: IProcessMainService
) {
super();
this.model = new ProcessExplorerModel(this.productService);
this.create(container);
}
private async create(container: HTMLElement): Promise<void> {
const { totalmem } = await this.nativeHostService.getOSStatistics();
this.createProcessTree(container, totalmem);
this.update();
}
private createProcessTree(container: HTMLElement, totalmem: number): void {
container.classList.add('process-explorer');
container.id = 'process-explorer';
const renderers = [
new ProcessRenderer(totalmem, this.model),
new ProcessHeaderTreeRenderer(),
new MachineRenderer(),
new ErrorRenderer()
];
this.tree = this._register(this.instantiationService.createInstance(
WorkbenchDataTree<IProcessTree, IProcessTree | IMachineProcessInformation | ProcessItem | IProcessInformation | IRemoteDiagnosticError>,
'processExplorer',
container,
new ProcessListDelegate(),
renderers,
new ProcessTreeDataSource(),
{
accessibilityProvider: new ProcessAccessibilityProvider(),
identityProvider: new ProcessIdentityProvider(),
expandOnlyOnTwistieClick: true,
renderIndentGuides: RenderIndentGuides.OnHover
}));
this._register(this.tree.onKeyDown(e => this.onTreeKeyDown(e)));
this._register(this.tree.onContextMenu(e => this.onTreeContextMenu(container, e)));
this.tree.setInput(this.model);
this.layoutTree();
}
private async onTreeKeyDown(e: KeyboardEvent): Promise<void> {
const event = new StandardKeyboardEvent(e);
if (event.keyCode === KeyCode.KeyE && event.altKey) {
const selectionPids = this.getSelectedPids();
await Promise.all(selectionPids.map(pid => this.nativeHostService.killProcess(pid, 'SIGTERM')));
}
}
private onTreeContextMenu(container: HTMLElement, e: ITreeContextMenuEvent<IProcessTree | IMachineProcessInformation | ProcessItem | IProcessInformation | IRemoteDiagnosticError | null>): void {
if (!isProcessItem(e.element)) {
return;
}
const item = e.element;
const pid = Number(item.pid);
const actions: IAction[] = [];
actions.push(toAction({ id: 'killProcess', label: localize('killProcess', "Kill Process"), run: () => this.nativeHostService.killProcess(pid, 'SIGTERM') }));
actions.push(toAction({ id: 'forceKillProcess', label: localize('forceKillProcess', "Force Kill Process"), run: () => this.nativeHostService.killProcess(pid, 'SIGKILL') }));
actions.push(new Separator());
actions.push(toAction({
id: 'copy',
label: localize('copy', "Copy"),
run: () => {
const selectionPids = this.getSelectedPids();
if (!selectionPids?.includes(pid)) {
selectionPids.length = 0; // If the selection does not contain the right clicked item, copy the right clicked item only.
selectionPids.push(pid);
}
const rows = selectionPids?.map(e => getDocument(container).getElementById(`pid-${e}`)).filter(e => !!e);
if (rows) {
const text = rows.map(e => e.innerText).filter(e => !!e);
this.nativeHostService.writeClipboardText(text.join('\n'));
}
}
}));
actions.push(toAction({
id: 'copyAll',
label: localize('copyAll', "Copy All"),
run: () => {
const processList = getDocument(container).getElementById('process-explorer');
if (processList) {
this.nativeHostService.writeClipboardText(processList.innerText);
}
}
}));
if (this.isDebuggable(item.cmd)) {
actions.push(new Separator());
actions.push(toAction({ id: 'debug', label: localize('debug', "Debug"), run: () => this.attachTo(item) }));
}
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions
});
}
private isDebuggable(cmd: string): boolean {
const matches = DEBUG_FLAGS_PATTERN.exec(cmd);
return (matches && matches.groups!.port !== '0') || cmd.indexOf('node ') >= 0 || cmd.indexOf('node.exe') >= 0;
}
private attachTo(item: ProcessItem): void {
const config: { type: string; request: string; name: string; port?: number; processId?: string } = {
type: 'node',
request: 'attach',
name: `process ${item.pid}`
};
let matches = DEBUG_FLAGS_PATTERN.exec(item.cmd);
if (matches) {
config.port = Number(matches.groups!.port);
} else {
config.processId = String(item.pid); // no port -> try to attach via pid (send SIGUSR1)
}
// a debug-port=n or inspect-port=n overrides the port
matches = DEBUG_PORT_PATTERN.exec(item.cmd);
if (matches) {
config.port = Number(matches.groups!.port); // override port
}
this.commandService.executeCommand('debug.startFromConfig', config);
}
private getSelectedPids(): number[] {
return coalesce(this.tree?.getSelection()?.map(e => {
if (!isProcessItem(e)) {
return undefined;
}
return e.pid;
}) ?? []);
}
private async update(): Promise<void> {
const { processes, pidToNames } = await this.processMainService.resolve();
this.model.update(processes, pidToNames);
this.tree?.updateChildren();
this.layoutTree();
this.delayer.trigger(() => this.update());
}
focus(): void {
this.tree?.domFocus();
}
layout(dimension: Dimension): void {
this.dimensions = dimension;
this.layoutTree();
}
private layoutTree(): void {
if (this.dimensions && this.tree) {
this.tree.layout(this.dimensions.height, this.dimensions.width);
}
}
}
class ProcessExplorerModel implements IProcessTree {
processes: IProcessInformation = { processRoots: [] };
private readonly mapPidToName = new Map<number, string>();
constructor(@IProductService private productService: IProductService) { }
update(processRoots: IMachineProcessInformation[], pidToNames: [number, string][]): void {
// PID to Names
this.mapPidToName.clear();
for (const [pid, name] of pidToNames) {
this.mapPidToName.set(pid, name);
}
// Processes
processRoots.forEach((info, index) => {
if (isProcessItem(info.rootProcess)) {
info.rootProcess.name = index === 0 ? this.productService.applicationName : 'remote-server';
}
});
this.processes = { processRoots };
}
getName(pid: number, fallback: string): string {
return this.mapPidToName.get(pid) ?? fallback;
}
}

View File

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
const processExplorerEditorIcon = registerIcon('process-explorer-editor-label-icon', Codicon.serverProcess, localize('processExplorerEditorLabelIcon', 'Icon of the process explorer editor label.'));
export class ProcessExplorerEditorInput extends EditorInput {
static readonly ID = 'workbench.editors.processEditorInput';
static readonly RESOURCE = URI.from({
scheme: 'process-explorer',
path: 'default'
});
private static _instance: ProcessExplorerEditorInput;
static get instance() {
if (!ProcessExplorerEditorInput._instance || ProcessExplorerEditorInput._instance.isDisposed()) {
ProcessExplorerEditorInput._instance = new ProcessExplorerEditorInput();
}
return ProcessExplorerEditorInput._instance;
}
override get typeId(): string { return ProcessExplorerEditorInput.ID; }
override get capabilities(): EditorInputCapabilities { return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; }
readonly resource = ProcessExplorerEditorInput.RESOURCE;
override getName(): string {
return localize('processExplorerInputName', "Process Explorer");
}
override getIcon(): ThemeIcon {
return processExplorerEditorIcon;
}
override matches(other: EditorInput | IUntypedEditorInput): boolean {
if (super.matches(other)) {
return true;
}
return other instanceof ProcessExplorerEditorInput;
}
}

View File

@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension } from '../../../../base/browser/dom.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
import { ProcessExplorerControl } from './processExplorerControl.js';
export class ProcessExplorerEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.processExplorer';
private processExplorerControl: ProcessExplorerControl | undefined = undefined;
constructor(
group: IEditorGroup,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(ProcessExplorerEditor.ID, group, telemetryService, themeService, storageService);
}
protected override createEditor(parent: HTMLElement): void {
this.processExplorerControl = this._register(this.instantiationService.createInstance(ProcessExplorerControl, parent));
}
override focus(): void {
this.processExplorerControl?.focus();
}
override layout(dimension: Dimension): void {
this.processExplorerControl?.layout(dimension);
}
}

View File

@ -30,6 +30,7 @@ import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-sandbox/window.js';
import { DefaultAccountManagementContribution } from '../services/accounts/common/defaultAccount.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contributions.js';
import product from '../../platform/product/common/product.js';
// Actions
(function registerActions(): void {
@ -147,7 +148,12 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../common/contri
'included': !isWindows,
'scope': ConfigurationScope.APPLICATION,
'markdownDescription': localize('application.shellEnvironmentResolutionTimeout', "Controls the timeout in seconds before giving up resolving the shell environment when the application is not already launched from a terminal. See our [documentation](https://go.microsoft.com/fwlink/?linkid=2149667) for more information.")
}
},
'application.useNewProcessExplorer': {
'type': 'boolean',
'default': product.quality !== 'stable', // TODO@bpasero decide on a default
'description': localize('useNewProcessExplorer', "Controls whether a the process explorer opens in a floating window."),
},
}
});

View File

@ -21,7 +21,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { DEFAULT_AUX_WINDOW_SIZE, DEFAULT_COMPACT_AUX_WINDOW_SIZE, IRectangle, WindowMinimumSize } from '../../../../platform/window/common/window.js';
import { DEFAULT_AUX_WINDOW_SIZE, IRectangle, WindowMinimumSize } from '../../../../platform/window/common/window.js';
import { BaseWindow } from '../../../browser/window.js';
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
import { IHostService } from '../../host/browser/host.js';
@ -319,7 +319,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
height: activeWindow.outerHeight
};
const defaultSize = options?.compact ? DEFAULT_COMPACT_AUX_WINDOW_SIZE : DEFAULT_AUX_WINDOW_SIZE;
const defaultSize = DEFAULT_AUX_WINDOW_SIZE;
const width = Math.max(options?.bounds?.width ?? defaultSize.width, WindowMinimumSize.WIDTH);
const height = Math.max(options?.bounds?.height ?? defaultSize.height, WindowMinimumSize.HEIGHT);

View File

@ -92,7 +92,11 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer
// Group: Aux Window
else if (preferredGroup === AUX_WINDOW_GROUP) {
group = editorGroupService.createAuxiliaryEditorPart({ compact: options?.compact }).then(group => group.activeGroup);
group = editorGroupService.createAuxiliaryEditorPart({
bounds: options?.auxiliary?.bounds,
compact: options?.auxiliary?.compact,
alwaysOnTop: options?.auxiliary?.alwaysOnTop
}).then(group => group.activeGroup);
}
// Group: Unspecified without a specific index to open

View File

@ -565,7 +565,7 @@ export interface IEditorGroupsService extends IEditorGroupsContainer {
* Opens a new window with a full editor part instantiated
* in there at the optional position and size on screen.
*/
createAuxiliaryEditorPart(options?: { bounds?: Partial<IRectangle>; compact?: boolean }): Promise<IAuxiliaryEditorPart>;
createAuxiliaryEditorPart(options?: { bounds?: Partial<IRectangle>; compact?: boolean; alwaysOnTop?: boolean }): Promise<IAuxiliaryEditorPart>;
/**
* Returns the instantiation service that is scoped to the

View File

@ -35,7 +35,8 @@ export class UniversalWatcherClient extends AbstractUniversalWatcherClient {
// the process automatically when the window closes or reloads.
const { client, onDidTerminate } = disposables.add(await this.utilityProcessWorkerWorkbenchService.createWorker({
moduleId: 'vs/platform/files/node/watcher/watcherMain',
type: 'fileWatcher'
type: 'fileWatcher',
name: 'file-watcher'
}));
// React on unexpected termination of the watcher process

View File

@ -122,6 +122,7 @@ import './contrib/issue/electron-sandbox/issue.contribution.js';
// Process
import './contrib/issue/electron-sandbox/process.contribution.js';
import './contrib/processExplorer/electron-sandbox/processExplorer.contribution.js';
// Remote
import './contrib/remote/electron-sandbox/remote.contribution.js';