sussudio
Version:
An unofficial VS Code Internal API
430 lines (429 loc) • 22.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { BrowserWindow, contentTracing, screen } from 'electron';
import { validatedIpcMain } from "../../../base/parts/ipc/electron-main/ipcMain.mjs";
import { arch, release, type } from 'os';
import { mnemonicButtonLabel } from "../../../base/common/labels.mjs";
import { DisposableStore } from "../../../base/common/lifecycle.mjs";
import { FileAccess } from "../../../base/common/network.mjs";
import { isMacintosh } from "../../../base/common/platform.mjs";
import { listProcesses } from "../../../base/node/ps.mjs";
import { localize } from "../../../nls.mjs";
import { IDiagnosticsService, isRemoteDiagnosticError } from "../../diagnostics/common/diagnostics.mjs";
import { IDiagnosticsMainService } from "../../diagnostics/electron-main/diagnosticsMainService.mjs";
import { IDialogMainService } from "../../dialogs/electron-main/dialogMainService.mjs";
import { IEnvironmentMainService } from "../../environment/electron-main/environmentMainService.mjs";
import { createDecorator } from "../../instantiation/common/instantiation.mjs";
import { ILogService } from "../../log/common/log.mjs";
import { INativeHostMainService } from "../../native/electron-main/nativeHostMainService.mjs";
import product from "../../product/common/product.mjs";
import { IProductService } from "../../product/common/productService.mjs";
import { IProtocolMainService } from "../../protocol/electron-main/protocol.mjs";
import { zoomLevelToZoomFactor } from "../../window/common/window.mjs";
import { randomPath } from "../../../base/common/extpath.mjs";
import { withNullAsUndefined } from "../../../base/common/types.mjs";
import { IStateMainService } from "../../state/electron-main/state.mjs";
export const IIssueMainService = createDecorator('issueMainService');
const processExplorerWindowState = 'issue.processExplorerWindowState';
let IssueMainService = class IssueMainService {
userEnv;
environmentMainService;
logService;
diagnosticsService;
diagnosticsMainService;
dialogMainService;
nativeHostMainService;
protocolMainService;
productService;
stateMainService;
static DEFAULT_BACKGROUND_COLOR = '#1E1E1E';
issueReporterWindow = null;
issueReporterParentWindow = null;
processExplorerWindow = null;
processExplorerParentWindow = null;
constructor(userEnv, environmentMainService, logService, diagnosticsService, diagnosticsMainService, dialogMainService, nativeHostMainService, protocolMainService, productService, stateMainService) {
this.userEnv = userEnv;
this.environmentMainService = environmentMainService;
this.logService = logService;
this.diagnosticsService = diagnosticsService;
this.diagnosticsMainService = diagnosticsMainService;
this.dialogMainService = dialogMainService;
this.nativeHostMainService = nativeHostMainService;
this.protocolMainService = protocolMainService;
this.productService = productService;
this.stateMainService = stateMainService;
this.registerListeners();
}
registerListeners() {
validatedIpcMain.on('vscode:issueSystemInfoRequest', async (event) => {
const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]);
const msg = await this.diagnosticsService.getSystemInfo(info, remoteData);
this.safeSend(event, 'vscode:issueSystemInfoResponse', msg);
});
validatedIpcMain.on('vscode:listProcesses', async (event) => {
const processes = [];
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}`);
}
this.safeSend(event, 'vscode:listProcessesResponse', processes);
});
validatedIpcMain.on('vscode:issueReporterClipboard', async (event) => {
const messageOptions = {
title: this.productService.nameLong,
message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."),
type: 'warning',
buttons: [
mnemonicButtonLabel(localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK")),
mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")),
],
defaultId: 0,
cancelId: 1,
noLink: true
};
if (this.issueReporterWindow) {
const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow);
this.safeSend(event, 'vscode:issueReporterClipboardResponse', result.response === 0);
}
});
validatedIpcMain.on('vscode:issuePerformanceInfoRequest', async (event) => {
const performanceInfo = await this.getPerformanceInfo();
this.safeSend(event, 'vscode:issuePerformanceInfoResponse', performanceInfo);
});
validatedIpcMain.on('vscode:issueReporterConfirmClose', async () => {
const messageOptions = {
title: this.productService.nameLong,
message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"),
type: 'warning',
buttons: [
mnemonicButtonLabel(localize({ key: 'yes', comment: ['&& denotes a mnemonic'] }, "&&Yes")),
mnemonicButtonLabel(localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")),
],
defaultId: 0,
cancelId: 1,
noLink: true
};
if (this.issueReporterWindow) {
const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow);
if (result.response === 0) {
if (this.issueReporterWindow) {
this.issueReporterWindow.destroy();
this.issueReporterWindow = null;
}
}
}
});
validatedIpcMain.on('vscode:workbenchCommand', (_, commandInfo) => {
const { id, from, args } = commandInfo;
let parentWindow;
switch (from) {
case 'issueReporter':
parentWindow = this.issueReporterParentWindow;
break;
case 'processExplorer':
parentWindow = this.processExplorerParentWindow;
break;
default:
throw new Error(`Unexpected command source: ${from}`);
}
parentWindow?.webContents.send('vscode:runAction', { id, from, args });
});
validatedIpcMain.on('vscode:openExternal', (_, arg) => {
this.nativeHostMainService.openExternal(undefined, arg);
});
validatedIpcMain.on('vscode:closeIssueReporter', event => {
this.issueReporterWindow?.close();
});
validatedIpcMain.on('vscode:closeProcessExplorer', event => {
this.processExplorerWindow?.close();
});
validatedIpcMain.on('vscode:windowsInfoRequest', async (event) => {
const mainProcessInfo = await this.diagnosticsMainService.getMainDiagnostics();
this.safeSend(event, 'vscode:windowsInfoResponse', mainProcessInfo.windows);
});
}
safeSend(event, channel, ...args) {
if (!event.sender.isDestroyed()) {
event.sender.send(channel, ...args);
}
}
async openReporter(data) {
if (!this.issueReporterWindow) {
this.issueReporterParentWindow = BrowserWindow.getFocusedWindow();
if (this.issueReporterParentWindow) {
const issueReporterDisposables = new DisposableStore();
const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl());
const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800);
this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, {
backgroundColor: data.styles.backgroundColor,
title: localize('issueReporter', "Issue Reporter"),
zoomLevel: data.zoomLevel,
alwaysOnTop: false
}, 'issue-reporter');
// Store into config object URL
issueReporterWindowConfigUrl.update({
appRoot: this.environmentMainService.appRoot,
windowId: this.issueReporterWindow.id,
userEnv: this.userEnv,
data,
disableExtensions: !!this.environmentMainService.disableExtensions,
os: {
type: type(),
arch: arch(),
release: release(),
},
product
});
this.issueReporterWindow.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/issue/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));
this.issueReporterWindow.on('close', () => {
this.issueReporterWindow = null;
issueReporterDisposables.dispose();
});
this.issueReporterParentWindow.on('closed', () => {
if (this.issueReporterWindow) {
this.issueReporterWindow.close();
this.issueReporterWindow = null;
issueReporterDisposables.dispose();
}
});
}
}
if (this.issueReporterWindow) {
this.focusWindow(this.issueReporterWindow);
}
}
async openProcessExplorer(data) {
if (!this.processExplorerWindow) {
this.processExplorerParentWindow = BrowserWindow.getFocusedWindow();
if (this.processExplorerParentWindow) {
const processExplorerDisposables = new DisposableStore();
const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl());
const savedPosition = this.stateMainService.getItem(processExplorerWindowState, undefined);
const position = isStrictWindowState(savedPosition) ? savedPosition : this.getWindowPosition(this.processExplorerParentWindow, 800, 500);
// Correct dimensions to take scale/dpr into account
const displayToUse = screen.getDisplayNearestPoint({ x: position.x, y: position.y });
position.width /= displayToUse.scaleFactor;
position.height /= displayToUse.scaleFactor;
this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, {
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer"),
zoomLevel: data.zoomLevel,
alwaysOnTop: true
}, 'process-explorer');
// Store into config object URL
processExplorerWindowConfigUrl.update({
appRoot: this.environmentMainService.appRoot,
windowId: this.processExplorerWindow.id,
userEnv: this.userEnv,
data,
product
});
this.processExplorerWindow.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));
this.processExplorerWindow.on('close', () => {
this.processExplorerWindow = null;
processExplorerDisposables.dispose();
});
this.processExplorerParentWindow.on('close', () => {
if (this.processExplorerWindow) {
this.processExplorerWindow.close();
this.processExplorerWindow = null;
processExplorerDisposables.dispose();
}
});
const storeState = () => {
if (!this.processExplorerWindow) {
return;
}
const size = this.processExplorerWindow.getSize();
const position = this.processExplorerWindow.getPosition();
if (!size || !position) {
return;
}
const state = {
width: size[0],
height: size[1],
x: position[0],
y: position[1]
};
this.stateMainService.setItem(processExplorerWindowState, state);
};
this.processExplorerWindow.on('moved', storeState);
this.processExplorerWindow.on('resized', storeState);
}
}
if (this.processExplorerWindow) {
this.focusWindow(this.processExplorerWindow);
}
}
focusWindow(window) {
if (window.isMinimized()) {
window.restore();
}
window.focus();
}
createBrowserWindow(position, ipcObjectUrl, options, windowKind) {
const window = new BrowserWindow({
fullscreen: false,
skipTaskbar: false,
resizable: true,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title: options.title,
backgroundColor: options.backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR,
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js').fsPath,
additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`, `--vscode-window-kind=${windowKind}`],
v8CacheOptions: this.environmentMainService.useCodeCache ? 'bypassHeatCheck' : 'none',
enableWebSQL: false,
spellcheck: false,
zoomFactor: zoomLevelToZoomFactor(options.zoomLevel),
sandbox: true,
contextIsolation: true
},
alwaysOnTop: options.alwaysOnTop,
experimentalDarkMode: true
});
window.setMenuBarVisibility(false);
return window;
}
async getSystemStatus() {
const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]);
return this.diagnosticsService.getDiagnostics(info, remoteData);
}
getWindowPosition(parentWindow, defaultWidth, defaultHeight) {
// We want the new window to open on the same display that the parent is in
let displayToUse;
const displays = screen.getAllDisplays();
// Single Display
if (displays.length === 1) {
displayToUse = displays[0];
}
// Multi Display
else {
// on mac there is 1 menu per window so we need to use the monitor where the cursor currently is
if (isMacintosh) {
const cursorPoint = screen.getCursorScreenPoint();
displayToUse = screen.getDisplayNearestPoint(cursorPoint);
}
// if we have a last active window, use that display for the new window
if (!displayToUse && parentWindow) {
displayToUse = screen.getDisplayMatching(parentWindow.getBounds());
}
// fallback to primary display or first display
if (!displayToUse) {
displayToUse = screen.getPrimaryDisplay() || displays[0];
}
}
const displayBounds = displayToUse.bounds;
const state = {
width: defaultWidth,
height: defaultHeight,
x: displayBounds.x + (displayBounds.width / 2) - (defaultWidth / 2),
y: displayBounds.y + (displayBounds.height / 2) - (defaultHeight / 2)
};
if (displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) {
if (state.x < displayBounds.x) {
state.x = displayBounds.x; // prevent window from falling out of the screen to the left
}
if (state.y < displayBounds.y) {
state.y = displayBounds.y; // prevent window from falling out of the screen to the top
}
if (state.x > (displayBounds.x + displayBounds.width)) {
state.x = displayBounds.x; // prevent window from falling out of the screen to the right
}
if (state.y > (displayBounds.y + displayBounds.height)) {
state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom
}
if (state.width > displayBounds.width) {
state.width = displayBounds.width; // prevent window from exceeding display bounds width
}
if (state.height > displayBounds.height) {
state.height = displayBounds.height; // prevent window from exceeding display bounds height
}
}
return state;
}
async getPerformanceInfo() {
try {
const [info, remoteData] = await Promise.all([this.diagnosticsMainService.getMainDiagnostics(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]);
return await this.diagnosticsService.getPerformanceInfo(info, remoteData);
}
catch (error) {
this.logService.warn('issueService#getPerformanceInfo ', error.message);
throw error;
}
}
async stopTracing() {
if (!this.environmentMainService.args.trace) {
return; // requires tracing to be on
}
const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`);
// Inform user to report an issue
await this.dialogMainService.showMessageBox({
title: this.productService.nameLong,
type: 'info',
message: localize('trace.message', "Successfully created the trace file"),
detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path),
buttons: [mnemonicButtonLabel(localize({ key: 'trace.ok', comment: ['&& denotes a mnemonic'] }, "&&OK"))],
defaultId: 0,
noLink: true
}, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
// Show item in explorer
this.nativeHostMainService.showItemInFolder(undefined, path);
}
};
IssueMainService = __decorate([
__param(1, IEnvironmentMainService),
__param(2, ILogService),
__param(3, IDiagnosticsService),
__param(4, IDiagnosticsMainService),
__param(5, IDialogMainService),
__param(6, INativeHostMainService),
__param(7, IProtocolMainService),
__param(8, IProductService),
__param(9, IStateMainService)
], IssueMainService);
export { IssueMainService };
function isStrictWindowState(obj) {
if (typeof obj !== 'object' || obj === null) {
return false;
}
return ('x' in obj &&
'y' in obj &&
'width' in obj &&
'height' in obj);
}