UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

501 lines (500 loc) 18.8 kB
/*--------------------------------------------------------------------------------------------- * 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 '@sussudio/base/parts/ipc/electron-main/ipcMain.mjs'; import { arch, release, type } from 'os'; import { mnemonicButtonLabel } from '@sussudio/base/common/labels.mjs'; import { DisposableStore } from '@sussudio/base/common/lifecycle.mjs'; import { FileAccess } from '@sussudio/base/common/network.mjs'; import { isMacintosh } from '@sussudio/base/common/platform.mjs'; import { listProcesses } from '@sussudio/base/node/ps.mjs'; import { localize } from 'vscode-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 '@sussudio/base/common/extpath.mjs'; import { withNullAsUndefined } from '@sussudio/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; }