From 2fd6a33c04f0cc6aca76ffaa6c72ca06798392ba Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 5 Feb 2026 12:46:27 +0100 Subject: [PATCH] Surface power API --- eslint.config.js | 5 + extensions/vscode-api-tests/package.json | 1 + .../src/singlefolder-tests/env.power.test.ts | 81 +++++++++ .../common/extensionsApiProposals.ts | 3 + src/vs/platform/native/common/native.ts | 32 ++++ .../electron-main/nativeHostMainService.ts | 71 +++++++- .../api/browser/extensionHost.contribution.ts | 1 + .../workbench/api/browser/mainThreadPower.ts | 62 +++++++ .../workbench/api/common/extHost.api.impl.ts | 56 ++++++ .../api/common/extHost.common.services.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 27 +++ src/vs/workbench/api/common/extHostPower.ts | 124 +++++++++++++ .../services/power/browser/powerService.ts | 59 +++++++ .../services/power/common/powerService.ts | 52 ++++++ .../power/electron-browser/powerService.ts | 73 ++++++++ .../electron-browser/workbenchTestServices.ts | 18 +- src/vs/workbench/workbench.desktop.main.ts | 1 + src/vs/workbench/workbench.web.main.ts | 1 + .../vscode.proposed.environmentPower.d.ts | 166 ++++++++++++++++++ 19 files changed, 832 insertions(+), 3 deletions(-) create mode 100644 extensions/vscode-api-tests/src/singlefolder-tests/env.power.test.ts create mode 100644 src/vs/workbench/api/browser/mainThreadPower.ts create mode 100644 src/vs/workbench/api/common/extHostPower.ts create mode 100644 src/vs/workbench/services/power/browser/powerService.ts create mode 100644 src/vs/workbench/services/power/common/powerService.ts create mode 100644 src/vs/workbench/services/power/electron-browser/powerService.ts create mode 100644 src/vscode-dts/vscode.proposed.environmentPower.d.ts diff --git a/eslint.config.js b/eslint.config.js index e6555bd7eeb..4118fd0759a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -895,6 +895,11 @@ export default tseslint.config( 'collapse', 'create', 'delete', + 'lock', + 'resume', + 'shutdown', + 'suspend', + 'unlock', 'discover', 'dispose', 'drop', diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index c3c08b17c5b..fe160d2c76c 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,6 +7,7 @@ "enabledApiProposals": [ "activeComment", "authSession", + "environmentPower", "chatParticipantPrivate", "chatProvider", "contribStatusBarItems", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/env.power.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/env.power.test.ts new file mode 100644 index 00000000000..a15032b86c9 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/env.power.test.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; + +suite('vscode API - env.power', () => { + + test('isOnBatteryPower returns a boolean', async () => { + const result = await vscode.env.power.isOnBatteryPower(); + assert.strictEqual(typeof result, 'boolean'); + }); + + test('getSystemIdleState returns valid state', async () => { + const state = await vscode.env.power.getSystemIdleState(60); + assert.ok(['active', 'idle', 'locked', 'unknown'].includes(state)); + }); + + test('getSystemIdleTime returns a number', async () => { + const idleTime = await vscode.env.power.getSystemIdleTime(); + assert.strictEqual(typeof idleTime, 'number'); + assert.ok(idleTime >= 0); + }); + + test('getCurrentThermalState returns valid state', async () => { + const state = await vscode.env.power.getCurrentThermalState(); + assert.ok(['unknown', 'nominal', 'fair', 'serious', 'critical'].includes(state)); + }); + + test('power save blocker can be started and disposed', async () => { + const blocker = await vscode.env.power.startPowerSaveBlocker('prevent-app-suspension'); + assert.strictEqual(typeof blocker.id, 'number'); + // Power save blocker is not supported in browser (id === -1), so isStarted will be false + const isSupported = blocker.id >= 0; + assert.strictEqual(blocker.isStarted, isSupported); + + blocker.dispose(); + assert.strictEqual(blocker.isStarted, false); + }); + + test('power save blocker with prevent-display-sleep type', async () => { + const blocker = await vscode.env.power.startPowerSaveBlocker('prevent-display-sleep'); + assert.strictEqual(typeof blocker.id, 'number'); + // Power save blocker is not supported in browser (id === -1), so isStarted will be false + const isSupported = blocker.id >= 0; + assert.strictEqual(blocker.isStarted, isSupported); + + blocker.dispose(); + assert.strictEqual(blocker.isStarted, false); + }); + + test('events are defined', () => { + assert.ok(vscode.env.power.onDidSuspend); + assert.ok(vscode.env.power.onDidResume); + assert.ok(vscode.env.power.onDidChangeOnBatteryPower); + assert.ok(vscode.env.power.onDidChangeThermalState); + assert.ok(vscode.env.power.onDidChangeSpeedLimit); + assert.ok(vscode.env.power.onWillShutdown); + assert.ok(vscode.env.power.onDidLockScreen); + assert.ok(vscode.env.power.onDidUnlockScreen); + }); + + test('event listeners can be registered and disposed', () => { + const disposables: vscode.Disposable[] = []; + + disposables.push(vscode.env.power.onDidSuspend(() => { })); + disposables.push(vscode.env.power.onDidResume(() => { })); + disposables.push(vscode.env.power.onDidChangeOnBatteryPower(() => { })); + disposables.push(vscode.env.power.onDidChangeThermalState(() => { })); + disposables.push(vscode.env.power.onDidChangeSpeedLimit(() => { })); + disposables.push(vscode.env.power.onWillShutdown(() => { })); + disposables.push(vscode.env.power.onDidLockScreen(() => { })); + disposables.push(vscode.env.power.onDidUnlockScreen(() => { })); + + // Dispose all listeners + disposables.forEach(d => d.dispose()); + }); +}); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ab2b5407d60..3c7108b9869 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -233,6 +233,9 @@ const _allApiProposals = { envIsAppPortable: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts', }, + environmentPower: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.environmentPower.d.ts', + }, extensionAffinity: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionAffinity.d.ts', }, diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index f408bf6fbd3..585585003d3 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -102,8 +102,16 @@ export interface ICommonNativeHostService { readonly onDidChangeDisplay: Event; + readonly onDidSuspendOS: Event; readonly onDidResumeOS: Event; + readonly onDidChangeOnBatteryPower: Event; + readonly onDidChangeThermalState: Event; + readonly onDidChangeSpeedLimit: Event; + readonly onWillShutdownOS: Event; + readonly onDidLockScreen: Event; + readonly onDidUnlockScreen: Event; + readonly onDidChangeColorScheme: Event; readonly onDidChangePassword: Event<{ readonly service: string; readonly account: string }>; @@ -262,8 +270,32 @@ export interface ICommonNativeHostService { * @param files An array of file entries to include in the zip, each with a relative path and string contents. */ createZipFile(zipPath: URI, files: { path: string; contents: string }[]): Promise; + + // Power + getSystemIdleState(idleThreshold: number): Promise; + getSystemIdleTime(): Promise; + getCurrentThermalState(): Promise; + isOnBatteryPower(): Promise; + startPowerSaveBlocker(type: PowerSaveBlockerType): Promise; + stopPowerSaveBlocker(id: number): Promise; + isPowerSaveBlockerStarted(id: number): Promise; } +/** + * Represents the system's idle state. + */ +export type SystemIdleState = 'active' | 'idle' | 'locked' | 'unknown'; + +/** + * Represents the system's thermal state. + */ +export type ThermalState = 'unknown' | 'nominal' | 'fair' | 'serious' | 'critical'; + +/** + * The type of power save blocker. + */ +export type PowerSaveBlockerType = 'prevent-app-suspension' | 'prevent-display-sleep'; + export const INativeHostService = createDecorator('nativeHostService'); /** diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 7a9aad58e54..22ad671ed56 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import { exec } from 'child_process'; -import { app, BrowserWindow, clipboard, contentTracing, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, Notification, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; +import { app, BrowserWindow, clipboard, contentTracing, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, Notification, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, powerSaveBlocker, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os'; import { promisify } from 'util'; import { memoize } from '../../../base/common/decorators.js'; @@ -27,7 +27,7 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; -import { FocusMode, ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IToastOptions, IToastResult } from '../common/native.js'; +import { FocusMode, ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IToastOptions, IToastResult, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../common/native.js'; import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; @@ -118,8 +118,34 @@ export class NativeHostMainService extends Disposable implements INativeHostMain Event.map(Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-focus', (event, window: BrowserWindow) => this.auxiliaryWindowsMainService.getWindowByWebContents(window.webContents)), window => !!window), window => window!.id) ); + this.onDidSuspendOS = Event.fromNodeEventEmitter(powerMonitor, 'suspend'); this.onDidResumeOS = Event.fromNodeEventEmitter(powerMonitor, 'resume'); + // Battery power events (macOS and Windows only) + this.onDidChangeOnBatteryPower = Event.any( + Event.map(Event.fromNodeEventEmitter(powerMonitor, 'on-ac'), () => false), + Event.map(Event.fromNodeEventEmitter(powerMonitor, 'on-battery'), () => true) + ); + + // Thermal state events (macOS only) + this.onDidChangeThermalState = Event.map( + Event.fromNodeEventEmitter<{ state: ThermalState }>(powerMonitor, 'thermal-state-change'), + e => e.state + ); + + // Speed limit events (macOS and Windows only) + this.onDidChangeSpeedLimit = Event.map( + Event.fromNodeEventEmitter<{ limit: number }>(powerMonitor, 'speed-limit-change'), + e => e.limit + ); + + // Shutdown event (Linux and macOS only) + this.onWillShutdownOS = Event.fromNodeEventEmitter(powerMonitor, 'shutdown'); + + // Screen lock events (macOS and Windows only) + this.onDidLockScreen = Event.fromNodeEventEmitter(powerMonitor, 'lock-screen'); + this.onDidUnlockScreen = Event.fromNodeEventEmitter(powerMonitor, 'unlock-screen'); + this.onDidChangeColorScheme = this.themeMainService.onDidChangeColorScheme; this.onDidChangeDisplay = Event.debounce(Event.any( @@ -162,8 +188,16 @@ export class NativeHostMainService extends Disposable implements INativeHostMain readonly onDidChangeWindowAlwaysOnTop: Event<{ readonly windowId: number; readonly alwaysOnTop: boolean }>; + readonly onDidSuspendOS: Event; readonly onDidResumeOS: Event; + readonly onDidChangeOnBatteryPower: Event; + readonly onDidChangeThermalState: Event; + readonly onDidChangeSpeedLimit: Event; + readonly onWillShutdownOS: Event; + readonly onDidLockScreen: Event; + readonly onDidUnlockScreen: Event; + readonly onDidChangeColorScheme: Event; private readonly _onDidChangePassword = this._register(new Emitter<{ account: string; service: string }>()); @@ -1236,6 +1270,39 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#endregion + + //#region Power + + async getSystemIdleState(windowId: number | undefined, idleThreshold: number): Promise { + return powerMonitor.getSystemIdleState(idleThreshold); + } + + async getSystemIdleTime(windowId: number | undefined): Promise { + return powerMonitor.getSystemIdleTime(); + } + + async getCurrentThermalState(windowId: number | undefined): Promise { + return powerMonitor.getCurrentThermalState(); + } + + async isOnBatteryPower(windowId: number | undefined): Promise { + return powerMonitor.isOnBatteryPower(); + } + + async startPowerSaveBlocker(windowId: number | undefined, type: PowerSaveBlockerType): Promise { + return powerSaveBlocker.start(type); + } + + async stopPowerSaveBlocker(windowId: number | undefined, id: number): Promise { + return powerSaveBlocker.stop(id); + } + + async isPowerSaveBlockerStarted(windowId: number | undefined, id: number): Promise { + return powerSaveBlocker.isStarted(id); + } + + //#endregion + private windowById(windowId: number | undefined, fallbackCodeWindowId?: number): ICodeWindow | IAuxiliaryWindow | undefined { return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId) ?? this.codeWindowById(fallbackCodeWindowId); } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index de3f735424e..6f3d0b86673 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -69,6 +69,7 @@ import './mainThreadDownloadService.js'; import './mainThreadUrls.js'; import './mainThreadUriOpeners.js'; import './mainThreadWindow.js'; +import './mainThreadPower.js'; import './mainThreadWebviewManager.js'; import './mainThreadWorkspace.js'; import './mainThreadComments.js'; diff --git a/src/vs/workbench/api/browser/mainThreadPower.ts b/src/vs/workbench/api/browser/mainThreadPower.ts new file mode 100644 index 00000000000..dbd8078aa19 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadPower.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostContext, ExtHostPowerShape, MainContext, MainThreadPowerShape, PowerSaveBlockerType, PowerSystemIdleState, PowerThermalState } from '../common/extHost.protocol.js'; +import { IPowerService } from '../../services/power/common/powerService.js'; + +@extHostNamedCustomer(MainContext.MainThreadPower) +export class MainThreadPower extends Disposable implements MainThreadPowerShape { + + private readonly proxy: ExtHostPowerShape; + private readonly disposables = this._register(new DisposableStore()); + + constructor( + extHostContext: IExtHostContext, + @IPowerService private readonly powerService: IPowerService, + ) { + super(); + this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostPower); + + // Forward power events to extension host + this.powerService.onDidSuspend(this.proxy.$onDidSuspend, this.proxy, this.disposables); + this.powerService.onDidResume(this.proxy.$onDidResume, this.proxy, this.disposables); + this.powerService.onDidChangeOnBatteryPower(this.proxy.$onDidChangeOnBatteryPower, this.proxy, this.disposables); + this.powerService.onDidChangeThermalState((state: PowerThermalState) => this.proxy.$onDidChangeThermalState(state), this, this.disposables); + this.powerService.onDidChangeSpeedLimit(this.proxy.$onDidChangeSpeedLimit, this.proxy, this.disposables); + this.powerService.onWillShutdown(this.proxy.$onWillShutdown, this.proxy, this.disposables); + this.powerService.onDidLockScreen(this.proxy.$onDidLockScreen, this.proxy, this.disposables); + this.powerService.onDidUnlockScreen(this.proxy.$onDidUnlockScreen, this.proxy, this.disposables); + } + + async $getSystemIdleState(idleThreshold: number): Promise { + return this.powerService.getSystemIdleState(idleThreshold); + } + + async $getSystemIdleTime(): Promise { + return this.powerService.getSystemIdleTime(); + } + + async $getCurrentThermalState(): Promise { + return this.powerService.getCurrentThermalState(); + } + + async $isOnBatteryPower(): Promise { + return this.powerService.isOnBatteryPower(); + } + + async $startPowerSaveBlocker(type: PowerSaveBlockerType): Promise { + return this.powerService.startPowerSaveBlocker(type); + } + + async $stopPowerSaveBlocker(id: number): Promise { + return this.powerService.stopPowerSaveBlocker(id); + } + + async $isPowerSaveBlockerStarted(id: number): Promise { + return this.powerService.isPowerSaveBlockerStarted(id); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a193a61de27..fccab633ec5 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -115,6 +115,7 @@ import { ExtHostWebviews } from './extHostWebview.js'; import { ExtHostWebviewPanels } from './extHostWebviewPanels.js'; import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; +import { IExtHostPower } from './extHostPower.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; import { ExtHostChatContext } from './extHostChatContext.js'; @@ -149,6 +150,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); + const extHostPower = accessor.get(IExtHostPower); const extHostUrls = accessor.get(IExtHostUrlsService); const extHostSecretState = accessor.get(IExtHostSecretState); const extHostEditorTabs = accessor.get(IExtHostEditorTabs); @@ -169,6 +171,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService); rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); + rpcProtocol.set(ExtHostContext.ExtHostPower, extHostPower); rpcProtocol.set(ExtHostContext.ExtHostUrls, extHostUrls); rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry); @@ -480,6 +483,59 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getDataChannel(channelId: string): vscode.DataChannel { checkProposedApiEnabled(extension, 'dataChannels'); return extHostDataChannels.createDataChannel(extension, channelId); + }, + get power(): typeof vscode.env.power { + checkProposedApiEnabled(extension, 'environmentPower'); + return { + get onDidSuspend() { + return _asExtensionEvent(extHostPower.onDidSuspend); + }, + get onDidResume() { + return _asExtensionEvent(extHostPower.onDidResume); + }, + get onDidChangeOnBatteryPower() { + return _asExtensionEvent(extHostPower.onDidChangeOnBatteryPower); + }, + get onDidChangeThermalState() { + return _asExtensionEvent(extHostPower.onDidChangeThermalState); + }, + get onDidChangeSpeedLimit() { + return _asExtensionEvent(extHostPower.onDidChangeSpeedLimit); + }, + get onWillShutdown() { + return _asExtensionEvent(extHostPower.onWillShutdown); + }, + get onDidLockScreen() { + return _asExtensionEvent(extHostPower.onDidLockScreen); + }, + get onDidUnlockScreen() { + return _asExtensionEvent(extHostPower.onDidUnlockScreen); + }, + getSystemIdleState(idleThresholdSeconds: number) { + return extHostPower.getSystemIdleState(idleThresholdSeconds); + }, + getSystemIdleTime() { + return extHostPower.getSystemIdleTime(); + }, + getCurrentThermalState() { + return extHostPower.getCurrentThermalState(); + }, + isOnBatteryPower() { + return extHostPower.isOnBatteryPower(); + }, + async startPowerSaveBlocker(type: vscode.env.power.PowerSaveBlockerType): Promise { + const blocker = await extHostPower.startPowerSaveBlocker(type); + return { + id: blocker.id, + get isStarted() { + return blocker.isStarted; + }, + dispose() { + blocker.dispose(); + } + }; + } + }; } }; if (!initData.environment.extensionTestsLocationURI) { diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index c63ea8ed77e..d139ccf0c73 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -18,6 +18,7 @@ import { IExtHostStorage, ExtHostStorage } from './extHostStorage.js'; import { IExtHostTunnelService, ExtHostTunnelService } from './extHostTunnelService.js'; import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from './extHostApiDeprecationService.js'; import { IExtHostWindow, ExtHostWindow } from './extHostWindow.js'; +import { IExtHostPower, ExtHostPower } from './extHostPower.js'; import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from './extHostFileSystemConsumer.js'; import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from './extHostFileSystemInfo.js'; import { IExtHostSecretState, ExtHostSecretState } from './extHostSecretState.js'; @@ -57,6 +58,7 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService, Instant registerSingleton(IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration, InstantiationType.Eager); registerSingleton(IExtHostTunnelService, ExtHostTunnelService, InstantiationType.Eager); registerSingleton(IExtHostWindow, ExtHostWindow, InstantiationType.Eager); +registerSingleton(IExtHostPower, ExtHostPower, InstantiationType.Eager); registerSingleton(IExtHostUrlsService, ExtHostUrls, InstantiationType.Eager); registerSingleton(IExtHostWorkspace, ExtHostWorkspace, InstantiationType.Eager); registerSingleton(IExtHostSecretState, ExtHostSecretState, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 50250361f9a..e233de77dd8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2913,6 +2913,31 @@ export interface ExtHostWindowShape { $onDidChangeActiveNativeWindowHandle(handle: string | undefined): void; } +export type PowerSystemIdleState = 'active' | 'idle' | 'locked' | 'unknown'; +export type PowerThermalState = 'unknown' | 'nominal' | 'fair' | 'serious' | 'critical'; +export type PowerSaveBlockerType = 'prevent-app-suspension' | 'prevent-display-sleep'; + +export interface MainThreadPowerShape extends IDisposable { + $getSystemIdleState(idleThreshold: number): Promise; + $getSystemIdleTime(): Promise; + $getCurrentThermalState(): Promise; + $isOnBatteryPower(): Promise; + $startPowerSaveBlocker(type: PowerSaveBlockerType): Promise; + $stopPowerSaveBlocker(id: number): Promise; + $isPowerSaveBlockerStarted(id: number): Promise; +} + +export interface ExtHostPowerShape { + $onDidSuspend(): void; + $onDidResume(): void; + $onDidChangeOnBatteryPower(isOnBattery: boolean): void; + $onDidChangeThermalState(state: PowerThermalState): void; + $onDidChangeSpeedLimit(limit: number): void; + $onWillShutdown(): void; + $onDidLockScreen(): void; + $onDidUnlockScreen(): void; +} + export interface ExtHostLogLevelServiceShape { $setLogLevel(level: LogLevel, resource?: UriComponents): void; } @@ -3478,6 +3503,7 @@ export const MainContext = { MainThreadShare: createProxyIdentifier('MainThreadShare'), MainThreadTask: createProxyIdentifier('MainThreadTask'), MainThreadWindow: createProxyIdentifier('MainThreadWindow'), + MainThreadPower: createProxyIdentifier('MainThreadPower'), MainThreadLabelService: createProxyIdentifier('MainThreadLabelService'), MainThreadNotebook: createProxyIdentifier('MainThreadNotebook'), MainThreadNotebookDocuments: createProxyIdentifier('MainThreadNotebookDocumentsShape'), @@ -3534,6 +3560,7 @@ export const ExtHostContext = { ExtHostTask: createProxyIdentifier('ExtHostTask'), ExtHostWorkspace: createProxyIdentifier('ExtHostWorkspace'), ExtHostWindow: createProxyIdentifier('ExtHostWindow'), + ExtHostPower: createProxyIdentifier('ExtHostPower'), ExtHostWebviews: createProxyIdentifier('ExtHostWebviews'), ExtHostWebviewPanels: createProxyIdentifier('ExtHostWebviewPanels'), ExtHostCustomEditors: createProxyIdentifier('ExtHostCustomEditors'), diff --git a/src/vs/workbench/api/common/extHostPower.ts b/src/vs/workbench/api/common/extHostPower.ts new file mode 100644 index 00000000000..a4bd6eadd26 --- /dev/null +++ b/src/vs/workbench/api/common/extHostPower.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; +import { IExtHostRpcService } from './extHostRpcService.js'; +import { ExtHostPowerShape, MainContext, MainThreadPowerShape, PowerSaveBlockerType, PowerSystemIdleState, PowerThermalState } from './extHost.protocol.js'; + +export class ExtHostPower extends Disposable implements ExtHostPowerShape { + + declare _serviceBrand: undefined; + + private readonly _proxy: MainThreadPowerShape; + + // Events + private readonly _onDidSuspend = this._register(new Emitter()); + readonly onDidSuspend: Event = this._onDidSuspend.event; + + private readonly _onDidResume = this._register(new Emitter()); + readonly onDidResume: Event = this._onDidResume.event; + + private readonly _onDidChangeOnBatteryPower = this._register(new Emitter()); + readonly onDidChangeOnBatteryPower: Event = this._onDidChangeOnBatteryPower.event; + + private readonly _onDidChangeThermalState = this._register(new Emitter()); + readonly onDidChangeThermalState: Event = this._onDidChangeThermalState.event; + + private readonly _onDidChangeSpeedLimit = this._register(new Emitter()); + readonly onDidChangeSpeedLimit: Event = this._onDidChangeSpeedLimit.event; + + private readonly _onWillShutdown = this._register(new Emitter()); + readonly onWillShutdown: Event = this._onWillShutdown.event; + + private readonly _onDidLockScreen = this._register(new Emitter()); + readonly onDidLockScreen: Event = this._onDidLockScreen.event; + + private readonly _onDidUnlockScreen = this._register(new Emitter()); + readonly onDidUnlockScreen: Event = this._onDidUnlockScreen.event; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + ) { + super(); + this._proxy = extHostRpc.getProxy(MainContext.MainThreadPower); + } + + // === Proxy callbacks (called by MainThread) === + + $onDidSuspend(): void { + this._onDidSuspend.fire(); + } + + $onDidResume(): void { + this._onDidResume.fire(); + } + + $onDidChangeOnBatteryPower(isOnBattery: boolean): void { + this._onDidChangeOnBatteryPower.fire(isOnBattery); + } + + $onDidChangeThermalState(state: PowerThermalState): void { + this._onDidChangeThermalState.fire(state); + } + + $onDidChangeSpeedLimit(limit: number): void { + this._onDidChangeSpeedLimit.fire(limit); + } + + $onWillShutdown(): void { + this._onWillShutdown.fire(); + } + + $onDidLockScreen(): void { + this._onDidLockScreen.fire(); + } + + $onDidUnlockScreen(): void { + this._onDidUnlockScreen.fire(); + } + + // === API for extensions === + + getSystemIdleState(idleThresholdSeconds: number): Promise { + return this._proxy.$getSystemIdleState(idleThresholdSeconds); + } + + getSystemIdleTime(): Promise { + return this._proxy.$getSystemIdleTime(); + } + + getCurrentThermalState(): Promise { + return this._proxy.$getCurrentThermalState(); + } + + isOnBatteryPower(): Promise { + return this._proxy.$isOnBatteryPower(); + } + + async startPowerSaveBlocker(type: PowerSaveBlockerType): Promise<{ id: number; isStarted: boolean; dispose: () => void }> { + const id = await this._proxy.$startPowerSaveBlocker(type); + const proxy = this._proxy; + const isSupported = id >= 0; + let disposed = false; + + return { + id, + get isStarted(): boolean { + return isSupported && !disposed; + }, + dispose: () => { + if (isSupported && !disposed) { + disposed = true; + proxy.$stopPowerSaveBlocker(id); + } + } + }; + } +} + +export const IExtHostPower = createDecorator('IExtHostPower'); +export interface IExtHostPower extends ExtHostPower, ExtHostPowerShape { } diff --git a/src/vs/workbench/services/power/browser/powerService.ts b/src/vs/workbench/services/power/browser/powerService.ts new file mode 100644 index 00000000000..06a3a43695c --- /dev/null +++ b/src/vs/workbench/services/power/browser/powerService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IPowerService, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../common/powerService.js'; + +/** + * Browser stub implementation of IPowerService. + * Power APIs are not available in web environments. + */ +export class BrowserPowerService extends Disposable implements IPowerService { + + declare readonly _serviceBrand: undefined; + + // Events never fire in browser + readonly onDidSuspend = Event.None; + readonly onDidResume = Event.None; + readonly onDidChangeOnBatteryPower = Event.None; + readonly onDidChangeThermalState = Event.None; + readonly onDidChangeSpeedLimit = Event.None; + readonly onWillShutdown = Event.None; + readonly onDidLockScreen = Event.None; + readonly onDidUnlockScreen = Event.None; + + async getSystemIdleState(_idleThreshold: number): Promise { + return 'unknown'; + } + + async getSystemIdleTime(): Promise { + return 0; + } + + async getCurrentThermalState(): Promise { + return 'unknown'; + } + + async isOnBatteryPower(): Promise { + return false; + } + + async startPowerSaveBlocker(_type: PowerSaveBlockerType): Promise { + // Return a fake ID (no-op in browser) + return -1; + } + + async stopPowerSaveBlocker(_id: number): Promise { + return false; + } + + async isPowerSaveBlockerStarted(_id: number): Promise { + return false; + } +} + +registerSingleton(IPowerService, BrowserPowerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/power/common/powerService.ts b/src/vs/workbench/services/power/common/powerService.ts new file mode 100644 index 00000000000..4902579d9f0 --- /dev/null +++ b/src/vs/workbench/services/power/common/powerService.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +/** + * Represents the system's idle state. + */ +export type SystemIdleState = 'active' | 'idle' | 'locked' | 'unknown'; + +/** + * Represents the system's thermal state. + */ +export type ThermalState = 'unknown' | 'nominal' | 'fair' | 'serious' | 'critical'; + +/** + * The type of power save blocker. + */ +export type PowerSaveBlockerType = 'prevent-app-suspension' | 'prevent-display-sleep'; + +export const IPowerService = createDecorator('powerService'); + +/** + * A service for monitoring power state and preventing system sleep. + * Only fully functional in desktop environments. Web/remote returns stub values. + */ +export interface IPowerService { + + readonly _serviceBrand: undefined; + + // Events + readonly onDidSuspend: Event; + readonly onDidResume: Event; + readonly onDidChangeOnBatteryPower: Event; + readonly onDidChangeThermalState: Event; + readonly onDidChangeSpeedLimit: Event; + readonly onWillShutdown: Event; + readonly onDidLockScreen: Event; + readonly onDidUnlockScreen: Event; + + // Methods + getSystemIdleState(idleThreshold: number): Promise; + getSystemIdleTime(): Promise; + getCurrentThermalState(): Promise; + isOnBatteryPower(): Promise; + startPowerSaveBlocker(type: PowerSaveBlockerType): Promise; + stopPowerSaveBlocker(id: number): Promise; + isPowerSaveBlockerStarted(id: number): Promise; +} diff --git a/src/vs/workbench/services/power/electron-browser/powerService.ts b/src/vs/workbench/services/power/electron-browser/powerService.ts new file mode 100644 index 00000000000..86797a925b0 --- /dev/null +++ b/src/vs/workbench/services/power/electron-browser/powerService.ts @@ -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 { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IPowerService, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../common/powerService.js'; +import { Event } from '../../../../base/common/event.js'; + +/** + * Desktop implementation of IPowerService using Electron's powerMonitor. + */ +export class NativePowerService extends Disposable implements IPowerService { + + declare readonly _serviceBrand: undefined; + + readonly onDidSuspend: Event; + readonly onDidResume: Event; + readonly onDidChangeOnBatteryPower: Event; + readonly onDidChangeThermalState: Event; + readonly onDidChangeSpeedLimit: Event; + readonly onWillShutdown: Event; + readonly onDidLockScreen: Event; + readonly onDidUnlockScreen: Event; + + constructor( + @INativeHostService private readonly nativeHostService: INativeHostService, + ) { + super(); + + // Forward events from native host service + this.onDidSuspend = nativeHostService.onDidSuspendOS; + this.onDidResume = Event.map(nativeHostService.onDidResumeOS, () => undefined); + this.onDidChangeOnBatteryPower = nativeHostService.onDidChangeOnBatteryPower; + this.onDidChangeThermalState = nativeHostService.onDidChangeThermalState; + this.onDidChangeSpeedLimit = nativeHostService.onDidChangeSpeedLimit; + this.onWillShutdown = nativeHostService.onWillShutdownOS; + this.onDidLockScreen = nativeHostService.onDidLockScreen; + this.onDidUnlockScreen = nativeHostService.onDidUnlockScreen; + } + + async getSystemIdleState(idleThreshold: number): Promise { + return this.nativeHostService.getSystemIdleState(idleThreshold); + } + + async getSystemIdleTime(): Promise { + return this.nativeHostService.getSystemIdleTime(); + } + + async getCurrentThermalState(): Promise { + return this.nativeHostService.getCurrentThermalState(); + } + + async isOnBatteryPower(): Promise { + return this.nativeHostService.isOnBatteryPower(); + } + + async startPowerSaveBlocker(type: PowerSaveBlockerType): Promise { + return this.nativeHostService.startPowerSaveBlocker(type); + } + + async stopPowerSaveBlocker(id: number): Promise { + return this.nativeHostService.stopPowerSaveBlocker(id); + } + + async isPowerSaveBlockerStarted(id: number): Promise { + return this.nativeHostService.isPowerSaveBlockerStarted(id); + } +} + +registerSingleton(IPowerService, NativePowerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index d6ba59284db..702928e173a 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -25,7 +25,7 @@ import { InMemoryFileSystemProvider } from '../../../platform/files/common/inMem import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ISharedProcessService } from '../../../platform/ipc/electron-browser/services.js'; import { NullLogService } from '../../../platform/log/common/log.js'; -import { INativeHostOptions, INativeHostService, IOSProperties, IOSStatistics, IToastOptions, IToastResult } from '../../../platform/native/common/native.js'; +import { INativeHostOptions, INativeHostService, IOSProperties, IOSStatistics, IToastOptions, IToastResult, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../../../platform/native/common/native.js'; import { IProductService } from '../../../platform/product/common/productService.js'; import { AuthInfo, Credentials } from '../../../platform/request/common/request.js'; import { IStorageService } from '../../../platform/storage/common/storage.js'; @@ -74,7 +74,14 @@ export class TestNativeHostService implements INativeHostService { readonly onDidBlurMainWindow: Event = Event.None; readonly onDidFocusMainOrAuxiliaryWindow: Event = Event.None; readonly onDidBlurMainOrAuxiliaryWindow: Event = Event.None; + readonly onDidSuspendOS: Event = Event.None; readonly onDidResumeOS: Event = Event.None; + readonly onDidChangeOnBatteryPower: Event = Event.None; + readonly onDidChangeThermalState: Event = Event.None; + readonly onDidChangeSpeedLimit: Event = Event.None; + readonly onWillShutdownOS: Event = Event.None; + readonly onDidLockScreen: Event = Event.None; + readonly onDidUnlockScreen: Event = Event.None; onDidChangeColorScheme = Event.None; onDidChangePassword = Event.None; readonly onDidTriggerWindowSystemContextMenu: Event<{ windowId: number; x: number; y: number }> = Event.None; @@ -179,6 +186,15 @@ export class TestNativeHostService implements INativeHostService { async showToast(options: IToastOptions): Promise { return { supported: false, clicked: false }; } async clearToast(id: string): Promise { } async clearToasts(): Promise { } + + // Power APIs + async getSystemIdleState(idleThreshold: number): Promise { return 'unknown'; } + async getSystemIdleTime(): Promise { return 0; } + async getCurrentThermalState(): Promise { return 'unknown'; } + async isOnBatteryPower(): Promise { return false; } + async startPowerSaveBlocker(type: PowerSaveBlockerType): Promise { return -1; } + async stopPowerSaveBlocker(id: number): Promise { return false; } + async isPowerSaveBlockerStarted(id: number): Promise { return false; } } export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index f242c4bd8ee..44c5621cf7d 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -91,6 +91,7 @@ import './services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js'; import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js'; import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js'; import './services/process/electron-browser/processService.js'; +import './services/power/electron-browser/powerService.js'; import { registerSingleton } from '../platform/instantiation/common/extensions.js'; import { IUserDataInitializationService, UserDataInitializationService } from './services/userData/browser/userDataInit.js'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 342ad97659b..0138c8fe280 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -70,6 +70,7 @@ import './services/configurationResolver/browser/configurationResolverService.js import '../platform/extensionResourceLoader/browser/extensionResourceLoaderService.js'; import './services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import './services/browserElements/browser/webBrowserElementsService.js'; +import './services/power/browser/powerService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js'; diff --git a/src/vscode-dts/vscode.proposed.environmentPower.d.ts b/src/vscode-dts/vscode.proposed.environmentPower.d.ts new file mode 100644 index 00000000000..12ebd9bf2f2 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.environmentPower.d.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace env { + + /** + * Namespace for power-related APIs including monitoring system power state + * and preventing the system from entering low-power modes. + * + * Note: These APIs are only fully functional in the desktop version of the editor. + * In web or remote scenarios, events will not fire and queries return default values. + */ + export namespace power { + + // === Events === + + /** + * Fires when the system is suspending (going to sleep). + */ + export const onDidSuspend: Event; + + /** + * Fires when the system is resuming from sleep. + */ + export const onDidResume: Event; + + /** + * Fires when the system's battery power state changes. + * The event value is `true` when on battery power, `false` when on AC power. + * + * Note: Only available on macOS and Windows. + */ + export const onDidChangeOnBatteryPower: Event; + + /** + * Fires when the system's thermal state changes. + * + * Apps may react to the new state by reducing expensive computing tasks + * (e.g., video encoding), or notifying the user. + * + * Note: Only available on macOS. + */ + export const onDidChangeThermalState: Event; + + /** + * Fires when the operating system's advertised CPU speed limit changes. + * The event value is the speed limit in percent (values below 100 indicate + * the system is impairing processing power due to thermal management). + * + * Note: Only available on macOS and Windows. + */ + export const onDidChangeSpeedLimit: Event; + + /** + * Fires when the system is about to shut down or reboot. + * + * Note: Only available on Linux and macOS. + */ + export const onWillShutdown: Event; + + /** + * Fires when the system screen is about to be locked. + * + * Note: Only available on macOS and Windows. + */ + export const onDidLockScreen: Event; + + /** + * Fires when the system screen is unlocked. + * + * Note: Only available on macOS and Windows. + */ + export const onDidUnlockScreen: Event; + + // === Methods === + + /** + * Gets the system's current idle state. + * + * @param idleThresholdSeconds The amount of time (in seconds) before the system + * is considered idle. + * @returns The system's current idle state. + */ + export function getSystemIdleState(idleThresholdSeconds: number): Thenable; + + /** + * Gets the system's idle time in seconds. + * + * @returns The number of seconds the system has been idle. + */ + export function getSystemIdleTime(): Thenable; + + /** + * Gets the system's current thermal state. + * + * Note: Only available on macOS. Returns `'unknown'` on other platforms. + * + * @returns The system's current thermal state. + */ + export function getCurrentThermalState(): Thenable; + + /** + * Checks whether the system is currently on battery power. + * + * @returns `true` if the system is on battery power, `false` otherwise. + */ + export function isOnBatteryPower(): Thenable; + + // === Power Save Blocker === + + /** + * Starts preventing the system from entering lower-power mode. + * + * @param type The type of power save blocker: + * - `'prevent-app-suspension'`: Prevents the application from being suspended. + * Keeps the system active but allows the screen to turn off. + * Example use cases: downloading a file or playing audio. + * - `'prevent-display-sleep'`: Prevents the display from going to sleep. + * Keeps the system and screen active. + * Example use case: playing video. + * + * Note: `'prevent-display-sleep'` has higher precedence over `'prevent-app-suspension'`. + * + * @returns A {@link PowerSaveBlocker} that can be disposed to stop blocking. + */ + export function startPowerSaveBlocker(type: PowerSaveBlockerType): Thenable; + + // === Types === + + /** + * Represents the system's idle state. + */ + export type SystemIdleState = 'active' | 'idle' | 'locked' | 'unknown'; + + /** + * Represents the system's thermal state. + */ + export type ThermalState = 'unknown' | 'nominal' | 'fair' | 'serious' | 'critical'; + + /** + * The type of power save blocker. + */ + export type PowerSaveBlockerType = 'prevent-app-suspension' | 'prevent-display-sleep'; + + /** + * A power save blocker that prevents the system from entering low-power mode. + * Dispose to stop blocking. + */ + export interface PowerSaveBlocker extends Disposable { + /** + * The unique identifier for this power save blocker. + */ + readonly id: number; + + /** + * Whether this power save blocker is currently active. + */ + readonly isStarted: boolean; + } + } + } +}