nativeHostMainService.ts 24.9 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { Emitter, Event } from 'vs/base/common/event';
7
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
8
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron';
9
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
10
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
11
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
12
import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform';
13
import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native';
14
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
15
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
16
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
17
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
18 19 20 21
import { dirExists } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
22
import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
23 24
import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
25 26 27 28
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 { memoize } from 'vs/base/common/decorators';
29
import { Disposable } from 'vs/base/common/lifecycle';
30
import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess';
31

32
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
33

34
export const INativeHostMainService = createDecorator<INativeHostMainService>('nativeHostMainService');
35

36 37 38 39 40
interface ChunkedPassword {
	content: string;
	hasNextChunk: boolean;
}

41
export class NativeHostMainService extends Disposable implements INativeHostMainService {
42

43
	declare readonly _serviceBrand: undefined;
44 45

	constructor(
46
		private sharedProcess: ISharedProcess,
47
		@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
48
		@IDialogMainService private readonly dialogMainService: IDialogMainService,
49
		@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
50
		@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
51 52
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@ILogService private readonly logService: ILogService
53
	) {
54 55
		super();

56
		this.registerListeners();
57 58
	}

59 60 61 62
	private registerListeners(): void {

		// Color Scheme changes
		nativeTheme.on('updated', () => {
63
			this._onDidChangeColorScheme.fire({
64 65 66
				highContrast: nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors,
				dark: nativeTheme.shouldUseDarkColors
			});
67 68 69 70
		});
	}


71 72 73 74 75 76
	//#region Properties

	get windowId(): never { throw new Error('Not implemented in electron-main'); }

	//#endregion

77 78
	//#region Events

79
	readonly onDidOpenWindow = Event.map(this.windowsMainService.onWindowOpened, window => window.id);
80

81 82
	readonly onDidMaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-maximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
	readonly onDidUnmaximizeWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-unmaximize', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
83

84 85
	readonly onDidBlurWindow = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-blur', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId));
	readonly onDidFocusWindow = Event.any(
86
		Event.map(Event.filter(Event.map(this.windowsMainService.onWindowsCountChanged, () => this.windowsMainService.getLastActiveWindow()), window => !!window), window => window!.id),
87
		Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId))
88 89
	);

90
	readonly onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume');
91

92 93
	private readonly _onDidChangeColorScheme = this._register(new Emitter<IColorScheme>());
	readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event;
94

95
	private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>());
96 97
	readonly onDidChangePassword = this._onDidChangePassword.event;

98 99
	//#endregion

100 101
	//#region Window

102 103 104 105 106 107 108 109
	async getWindows(): Promise<IOpenedWindow[]> {
		const windows = this.windowsMainService.getWindows();

		return windows.map(window => ({
			id: window.id,
			workspace: window.openedWorkspace,
			folderUri: window.openedFolderUri,
			title: window.win.getTitle(),
110 111
			filename: window.getRepresentedFilename(),
			dirty: window.isDocumentEdited()
112 113 114
		}));
	}

115
	async getWindowCount(windowId: number | undefined): Promise<number> {
116 117 118
		return this.windowsMainService.getWindowCount();
	}

119
	async getActiveWindowId(windowId: number | undefined): Promise<number | undefined> {
120
		const activeWindow = BrowserWindow.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
121 122 123 124 125 126 127
		if (activeWindow) {
			return activeWindow.id;
		}

		return undefined;
	}

128
	openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise<void>;
129 130
	openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void>;
	openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> {
131 132 133 134 135
		if (Array.isArray(arg1)) {
			return this.doOpenWindow(windowId, arg1, arg2);
		}

		return this.doOpenEmptyWindow(windowId, arg1);
136 137
	}

138
	private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: IOpenWindowOptions = Object.create(null)): Promise<void> {
139 140 141 142 143
		if (toOpen.length > 0) {
			this.windowsMainService.open({
				context: OpenContext.API,
				contextWindowId: windowId,
				urisToOpen: toOpen,
B
Benjamin Pasero 已提交
144
				cli: this.environmentService.args,
145 146
				forceNewWindow: options.forceNewWindow,
				forceReuseWindow: options.forceReuseWindow,
147
				preferNewWindow: options.preferNewWindow,
148 149 150 151 152 153 154 155 156
				diffMode: options.diffMode,
				addMode: options.addMode,
				gotoLineMode: options.gotoLineMode,
				noRecentEntry: options.noRecentEntry,
				waitMarkerFileURI: options.waitMarkerFileURI
			});
		}
	}

157
	private async doOpenEmptyWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise<void> {
158 159 160 161
		this.windowsMainService.openEmptyWindow({
			context: OpenContext.API,
			contextWindowId: windowId
		}, options);
162 163
	}

164 165
	async toggleFullScreen(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
166 167 168 169 170
		if (window) {
			window.toggleFullScreen();
		}
	}

171 172
	async handleTitleDoubleClick(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
173 174 175 176 177
		if (window) {
			window.handleTitleDoubleClick();
		}
	}

178 179
	async isMaximized(windowId: number | undefined): Promise<boolean> {
		const window = this.windowById(windowId);
180 181 182 183 184 185 186
		if (window) {
			return window.win.isMaximized();
		}

		return false;
	}

187 188
	async maximizeWindow(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
189 190 191 192 193
		if (window) {
			window.win.maximize();
		}
	}

194 195
	async unmaximizeWindow(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
196 197 198 199 200
		if (window) {
			window.win.unmaximize();
		}
	}

201 202
	async minimizeWindow(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
203 204 205 206 207
		if (window) {
			window.win.minimize();
		}
	}

208
	async focusWindow(windowId: number | undefined, options?: { windowId?: number; force?: boolean; }): Promise<void> {
209 210 211 212
		if (options && typeof options.windowId === 'number') {
			windowId = options.windowId;
		}

213
		const window = this.windowById(windowId);
214
		if (window) {
215
			window.focus({ force: options?.force ?? false });
216 217 218
		}
	}

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
	async setMinimumSize(windowId: number | undefined, width: number | undefined, height: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
		if (window) {
			const [windowWidth, windowHeight] = window.win.getSize();
			const [minWindowWidth, minWindowHeight] = window.win.getMinimumSize();
			const [newMinWindowWidth, newMinWindowHeight] = [width ?? minWindowWidth, height ?? minWindowHeight];
			const [newWindowWidth, newWindowHeight] = [Math.max(windowWidth, newMinWindowWidth), Math.max(windowHeight, newMinWindowHeight)];

			if (minWindowWidth !== newMinWindowWidth || minWindowHeight !== newMinWindowHeight) {
				window.win.setMinimumSize(newMinWindowWidth, newMinWindowHeight);
			}
			if (windowWidth !== newWindowWidth || windowHeight !== newWindowHeight) {
				window.win.setSize(newWindowWidth, newWindowHeight);
			}
		}
	}

236 237
	//#endregion

238 239
	//#region Dialog

240
	async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
241
		return this.dialogMainService.showMessageBox(options, this.toBrowserWindow(windowId));
242 243
	}

244
	async showSaveDialog(windowId: number | undefined, options: SaveDialogOptions): Promise<SaveDialogReturnValue> {
245
		return this.dialogMainService.showSaveDialog(options, this.toBrowserWindow(windowId));
246
	}
247

248
	async showOpenDialog(windowId: number | undefined, options: OpenDialogOptions): Promise<OpenDialogReturnValue> {
249 250 251
		return this.dialogMainService.showOpenDialog(options, this.toBrowserWindow(windowId));
	}

252 253
	private toBrowserWindow(windowId: number | undefined): BrowserWindow | undefined {
		const window = this.windowById(windowId);
254 255 256 257 258
		if (window) {
			return window.win;
		}

		return undefined;
259
	}
260

261
	async pickFileFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> {
262 263 264 265 266
		const paths = await this.dialogMainService.pickFileFolder(options);
		if (paths) {
			this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData);
			this.doOpenPicked(await Promise.all(paths.map(async path => (await dirExists(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId);
		}
267 268
	}

269
	async pickFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> {
270 271 272 273 274
		const paths = await this.dialogMainService.pickFolder(options);
		if (paths) {
			this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFolder', options.telemetryExtraData);
			this.doOpenPicked(paths.map(path => ({ folderUri: URI.file(path) })), options, windowId);
		}
275 276
	}

277
	async pickFileAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> {
278 279 280 281 282
		const paths = await this.dialogMainService.pickFile(options);
		if (paths) {
			this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFile', options.telemetryExtraData);
			this.doOpenPicked(paths.map(path => ({ fileUri: URI.file(path) })), options, windowId);
		}
283 284
	}

285
	async pickWorkspaceAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise<void> {
286 287 288 289 290 291 292
		const paths = await this.dialogMainService.pickWorkspace(options);
		if (paths) {
			this.sendPickerTelemetry(paths, options.telemetryEventName || 'openWorkspace', options.telemetryExtraData);
			this.doOpenPicked(paths.map(path => ({ workspaceUri: URI.file(path) })), options, windowId);
		}
	}

293
	private doOpenPicked(openable: IWindowOpenable[], options: INativeOpenDialogOptions, windowId: number | undefined): void {
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
		this.windowsMainService.open({
			context: OpenContext.DIALOG,
			contextWindowId: windowId,
			cli: this.environmentService.args,
			urisToOpen: openable,
			forceNewWindow: options.forceNewWindow
		});
	}

	private sendPickerTelemetry(paths: string[], telemetryEventName: string, telemetryExtraData?: ITelemetryData) {
		const numberOfPaths = paths ? paths.length : 0;

		// Telemetry
		// __GDPR__TODO__ Dynamic event names and dynamic properties. Can not be registered statically.
		this.telemetryService.publicLog(telemetryEventName, {
			...telemetryExtraData,
			outcome: numberOfPaths ? 'success' : 'canceled',
			numberOfPaths
		});
313 314
	}

315 316
	//#endregion

317 318
	//#region OS

319
	async showItemInFolder(windowId: number | undefined, path: string): Promise<void> {
320 321
		shell.showItemInFolder(path);
	}
322

323 324
	async setRepresentedFilename(windowId: number | undefined, path: string): Promise<void> {
		const window = this.windowById(windowId);
325 326 327 328 329
		if (window) {
			window.setRepresentedFilename(path);
		}
	}

330 331
	async setDocumentEdited(windowId: number | undefined, edited: boolean): Promise<void> {
		const window = this.windowById(windowId);
332
		if (window) {
333
			window.setDocumentEdited(edited);
334 335 336
		}
	}

337
	async openExternal(windowId: number | undefined, url: string): Promise<boolean> {
338 339
		if (isLinuxSnap) {
			this.safeSnapOpenExternal(url);
340 341 342
		} else {
			shell.openExternal(url);
		}
343 344

		return true;
345 346
	}

347 348 349
	private safeSnapOpenExternal(url: string): void {

		// Remove some environment variables before opening to avoid issues...
350 351 352 353 354 355 356
		const gdkPixbufModuleFile = process.env['GDK_PIXBUF_MODULE_FILE'];
		const gdkPixbufModuleDir = process.env['GDK_PIXBUF_MODULEDIR'];
		delete process.env['GDK_PIXBUF_MODULE_FILE'];
		delete process.env['GDK_PIXBUF_MODULEDIR'];

		shell.openExternal(url);

357
		// ...but restore them after
358 359 360 361
		process.env['GDK_PIXBUF_MODULE_FILE'] = gdkPixbufModuleFile;
		process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir;
	}

362 363
	async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<boolean> {
		return shell.moveItemToTrash(fullPath);
364 365
	}

366 367 368 369 370
	async isAdmin(): Promise<boolean> {
		let isAdmin: boolean;
		if (isWindows) {
			isAdmin = (await import('native-is-elevated'))();
		} else {
371
			isAdmin = process.getuid() === 0;
372 373 374 375 376
		}

		return isAdmin;
	}

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
	async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { overwriteReadonly?: boolean }): Promise<void> {
		const sudoPrompt = await import('sudo-prompt');

		return new Promise<void>((resolve, reject) => {
			const sudoCommand: string[] = [`"${this.cliPath}"`];
			if (options?.overwriteReadonly) {
				sudoCommand.push('--file-chmod');
			}

			sudoCommand.push('--file-write', `"${source.fsPath}"`, `"${target.fsPath}"`);

			const promptOptions = {
				name: product.nameLong.replace('-', ''),
				icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined
			};

			sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => {
				if (stdout) {
					this.logService.trace(`[sudo-prompt] received stdout: ${stdout}`);
				}

				if (stderr) {
					this.logService.trace(`[sudo-prompt] received stderr: ${stderr}`);
				}

				if (error) {
					reject(error);
				} else {
					resolve(undefined);
				}
			});
		});
	}

	@memoize
	private get cliPath(): string {

		// Windows
		if (isWindows) {
			if (this.environmentService.isBuilt) {
				return join(dirname(process.execPath), 'bin', `${product.applicationName}.cmd`);
			}

			return join(this.environmentService.appRoot, 'scripts', 'code-cli.bat');
		}

		// Linux
		if (isLinux) {
			if (this.environmentService.isBuilt) {
				return join(dirname(process.execPath), 'bin', `${product.applicationName}`);
			}

			return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh');
		}

		// macOS
		if (this.environmentService.isBuilt) {
			return join(this.environmentService.appRoot, 'bin', 'code');
		}

		return join(this.environmentService.appRoot, 'scripts', 'code-cli.sh');
	}

440 441 442 443 444 445
	async getOSStatistics(): Promise<IOSStatistics> {
		return {
			totalmem: totalmem(),
			freemem: freemem(),
			loadavg: loadavg()
		};
446 447
	}

448
	async getOSProperties(): Promise<IOSProperties> {
449 450 451 452
		return {
			arch: arch(),
			platform: platform(),
			release: release(),
453 454
			type: type(),
			cpus: cpus()
455 456 457
		};
	}

458 459 460 461
	async getOSVirtualMachineHint(): Promise<number> {
		return virtualMachineHint.value();
	}

462 463 464 465 466 467 468 469 470
	//#endregion


	//#region Process

	async killProcess(windowId: number | undefined, pid: number, code: string): Promise<void> {
		process.kill(pid, code);
	}

471 472
	//#endregion

473

474
	//#region Clipboard
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492

	async readClipboardText(windowId: number | undefined, type?: 'selection' | 'clipboard'): Promise<string> {
		return clipboard.readText(type);
	}

	async writeClipboardText(windowId: number | undefined, text: string, type?: 'selection' | 'clipboard'): Promise<void> {
		return clipboard.writeText(text, type);
	}

	async readClipboardFindText(windowId: number | undefined,): Promise<string> {
		return clipboard.readFindText();
	}

	async writeClipboardFindText(windowId: number | undefined, text: string): Promise<void> {
		return clipboard.writeFindText(text);
	}

	async writeClipboardBuffer(windowId: number | undefined, format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise<void> {
B
Benjamin Pasero 已提交
493
		return clipboard.writeBuffer(format, Buffer.from(buffer), type);
494 495 496 497 498 499 500 501 502 503 504 505
	}

	async readClipboardBuffer(windowId: number | undefined, format: string): Promise<Uint8Array> {
		return clipboard.readBuffer(format);
	}

	async hasClipboard(windowId: number | undefined, format: string, type?: 'selection' | 'clipboard'): Promise<boolean> {
		return clipboard.has(format, type);
	}

	//#endregion

506 507 508
	//#region macOS Touchbar

	async newWindowTab(): Promise<void> {
509
		this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true });
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
	}

	async showPreviousWindowTab(): Promise<void> {
		Menu.sendActionToFirstResponder('selectPreviousTab:');
	}

	async showNextWindowTab(): Promise<void> {
		Menu.sendActionToFirstResponder('selectNextTab:');
	}

	async moveWindowTabToNewWindow(): Promise<void> {
		Menu.sendActionToFirstResponder('moveTabToNewWindow:');
	}

	async mergeAllWindowTabs(): Promise<void> {
		Menu.sendActionToFirstResponder('mergeAllWindows:');
	}

	async toggleWindowTabsBar(): Promise<void> {
		Menu.sendActionToFirstResponder('toggleTabBar:');
	}

532 533 534 535 536 537 538
	async updateTouchBar(windowId: number | undefined, items: ISerializableCommandAction[][]): Promise<void> {
		const window = this.windowById(windowId);
		if (window) {
			window.updateTouchBar(items);
		}
	}

539 540
	//#endregion

541 542
	//#region Lifecycle

543 544 545 546 547 548 549
	async notifyReady(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
		if (window) {
			window.setReady();
		}
	}

550
	async relaunch(windowId: number | undefined, options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void> {
551 552 553
		return this.lifecycleMainService.relaunch(options);
	}

554 555
	async reload(windowId: number | undefined, options?: { disableExtensions?: boolean }): Promise<void> {
		const window = this.windowById(windowId);
556
		if (window) {
557
			return this.lifecycleMainService.reload(window, options?.disableExtensions !== undefined ? { _: [], 'disable-extensions': options?.disableExtensions } : undefined);
558 559 560
		}
	}

561
	async closeWindow(windowId: number | undefined): Promise<void> {
562 563 564 565 566
		this.closeWindowById(windowId, windowId);
	}

	async closeWindowById(currentWindowId: number | undefined, targetWindowId?: number | undefined): Promise<void> {
		const window = this.windowById(targetWindowId);
567 568 569 570 571
		if (window) {
			return window.win.close();
		}
	}

572 573 574 575 576
	async quit(windowId: number | undefined): Promise<void> {

		// If the user selected to exit from an extension development host window, do not quit, but just
		// close the window unless this is the last window that is opened.
		const window = this.windowsMainService.getLastActiveWindow();
B
Benjamin Pasero 已提交
577
		if (window?.isExtensionDevelopmentHost && this.windowsMainService.getWindowCount() > 1) {
578 579 580 581 582 583 584 585 586
			window.win.close();
		}

		// Otherwise: normal quit
		else {
			setTimeout(() => {
				this.lifecycleMainService.quit();
			}, 10 /* delay to unwind callback stack (IPC) */);
		}
B
Benjamin Pasero 已提交
587 588
	}

589 590 591 592
	async exit(windowId: number | undefined, code: number): Promise<void> {
		await this.lifecycleMainService.kill(code);
	}

593 594
	//#endregion

595 596
	//#region Connectivity

597
	async resolveProxy(windowId: number | undefined, url: string): Promise<string | undefined> {
R
Robo 已提交
598 599 600 601 602 603 604
		const window = this.windowById(windowId);
		const session = window?.win?.webContents?.session;
		if (session) {
			return session.resolveProxy(url);
		} else {
			return undefined;
		}
605 606 607 608
	}

	//#endregion

609 610
	//#region Development

611 612
	async openDevTools(windowId: number | undefined, options?: OpenDevToolsOptions): Promise<void> {
		const window = this.windowById(windowId);
613 614 615 616 617
		if (window) {
			window.win.webContents.openDevTools(options);
		}
	}

618 619
	async toggleDevTools(windowId: number | undefined): Promise<void> {
		const window = this.windowById(windowId);
620 621
		if (window) {
			const contents = window.win.webContents;
R
Robo 已提交
622
			contents.toggleDevTools();
623 624
		}
	}
625

626
	async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise<void> {
627 628 629 630 631 632
		const window = this.windowById(windowId);
		if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
			window.win.webContents.sendInputEvent(event);
		}
	}

633 634 635 636
	async toggleSharedProcessWindow(): Promise<void> {
		return this.sharedProcess.toggle();
	}

637
	//#endregion
638

639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
	//#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> {
		if (!isWindows) {
			return undefined;
		}

		const Registry = await import('vscode-windows-registry');
		try {
			return Registry.GetStringRegKey(hive, path, name);
		} catch {
			return undefined;
		}
	}

654 655 656 657
	//#endregion

	//#region Credentials

658 659 660
	private static readonly MAX_PASSWORD_LENGTH = 2500;
	private static readonly PASSWORD_CHUNK_SIZE = NativeHostMainService.MAX_PASSWORD_LENGTH - 100;

661 662 663
	async getPassword(windowId: number | undefined, service: string, account: string): Promise<string | null> {
		const keytar = await import('keytar');

664 665 666 667
		const password = await keytar.getPassword(service, account);
		if (password) {
			try {
				let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password);
668 669 670 671
				if (!content || !hasNextChunk) {
					return password;
				}

672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
				let index = 1;
				while (hasNextChunk) {
					const nextChunk = await keytar.getPassword(service, `${account}-${index}`);
					const result: ChunkedPassword = JSON.parse(nextChunk!);
					content += result.content;
					hasNextChunk = result.hasNextChunk;
				}

				return content;
			} catch {
				return password;
			}
		}

		return password;
687 688 689 690
	}

	async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise<void> {
		const keytar = await import('keytar');
691

692
		if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) {
693 694 695 696
			let index = 0;
			let chunk = 0;
			let hasNextChunk = true;
			while (hasNextChunk) {
697 698
				const passwordChunk = password.substring(index, index + NativeHostMainService.PASSWORD_CHUNK_SIZE);
				index += NativeHostMainService.PASSWORD_CHUNK_SIZE;
699
				hasNextChunk = password.length - index > 0;
700

701 702 703 704 705 706 707 708 709 710 711 712 713
				const content: ChunkedPassword = {
					content: passwordChunk,
					hasNextChunk: hasNextChunk
				};

				await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
				chunk++;
			}

		} else {
			await keytar.setPassword(service, account, password);
		}

714
		this._onDidChangePassword.fire({ service, account });
715 716 717 718
	}

	async deletePassword(windowId: number | undefined, service: string, account: string): Promise<boolean> {
		const keytar = await import('keytar');
719

720 721
		const didDelete = await keytar.deletePassword(service, account);
		if (didDelete) {
722
			this._onDidChangePassword.fire({ service, account });
723
		}
724

725
		return didDelete;
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
	}

	async findPassword(windowId: number | undefined, service: string): Promise<string | null> {
		const keytar = await import('keytar');

		return keytar.findPassword(service);
	}

	async findCredentials(windowId: number | undefined, service: string): Promise<Array<{ account: string, password: string }>> {
		const keytar = await import('keytar');

		return keytar.findCredentials(service);
	}

	//#endregion

742 743 744 745 746 747 748
	private windowById(windowId: number | undefined): ICodeWindow | undefined {
		if (typeof windowId !== 'number') {
			return undefined;
		}

		return this.windowsMainService.getWindowById(windowId);
	}
749
}