diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index cd1e53878649c..b0a07f70a1424 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -5,7 +5,7 @@ import { INodeProcess, IProcessEnvironment } from '../../../common/platform.js'; import { ISandboxConfiguration } from '../common/sandboxTypes.js'; -import { IpcRenderer, ProcessMemoryInfo, WebFrame, WebUtils } from './electronTypes.js'; +import { IpcRenderer, ProcessMemoryInfo, WebFrame, WebUtils, IpcRendererEvent } from './electronTypes.js'; /** * In Electron renderers we cannot expose all of the `process` global of node.js @@ -115,6 +115,15 @@ export interface ISandboxContext { resolveConfiguration(): Promise; } +export interface IDevice { + id: string; + label: string; +} + +export interface IDeviceAccess { + handleDeviceAccess: (callback: (event: IpcRendererEvent, devices: IDevice[]) => void) => void; +} + const vscodeGlobal = (globalThis as any).vscode; export const ipcRenderer: IpcRenderer = vscodeGlobal.ipcRenderer; export const ipcMessagePort: IpcMessagePort = vscodeGlobal.ipcMessagePort; @@ -122,6 +131,7 @@ export const webFrame: WebFrame = vscodeGlobal.webFrame; export const process: ISandboxNodeProcess = vscodeGlobal.process; export const context: ISandboxContext = vscodeGlobal.context; export const webUtils: WebUtils = vscodeGlobal.webUtils; +export const deviceAccess: IDeviceAccess = vscodeGlobal.deviceAccess; /** * A set of globals only available to main windows that depend @@ -134,6 +144,7 @@ export interface IMainWindowSandboxGlobals { readonly process: ISandboxNodeProcess; readonly context: ISandboxContext; readonly webUtils: WebUtils; + readonly deviceAccess: IDeviceAccess; } /** @@ -143,4 +154,5 @@ export interface IMainWindowSandboxGlobals { export interface ISandboxGlobals { readonly ipcRenderer: Pick; readonly webFrame: import('./electronTypes.js').WebFrame; + readonly deviceAccess: IDeviceAccess; } diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.ts b/src/vs/base/parts/sandbox/electron-sandbox/preload.ts index ba5476ffb2c3c..ea35285fcf0b3 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.ts @@ -10,6 +10,7 @@ const { ipcRenderer, webFrame, contextBridge, webUtils } = require('electron'); type ISandboxConfiguration = import('vs/base/parts/sandbox/common/sandboxTypes.js').ISandboxConfiguration; + type IDeviceAccess = import('vs/base/parts/sandbox/electron-sandbox/globals.js').IDeviceAccess; //#region Utilities @@ -90,6 +91,14 @@ //#endregion + //#region Device Access + + const deviceAccess: IDeviceAccess = { + handleDeviceAccess: callback => ipcRenderer.on('vscode:device-access', callback), + }; + + //#endregion + //#region Globals Definition // ####################################################################### @@ -243,7 +252,9 @@ async resolveConfiguration(): Promise { return resolveConfiguration; } - } + }, + + deviceAccess }; // Use `contextBridge` APIs to expose globals to VSCode diff --git a/src/vs/platform/window/electron-sandbox/window.ts b/src/vs/platform/window/electron-sandbox/window.ts index c9914e2117e55..6c4171bbf4043 100644 --- a/src/vs/platform/window/electron-sandbox/window.ts +++ b/src/vs/platform/window/electron-sandbox/window.ts @@ -7,8 +7,9 @@ import { getZoomLevel, setZoomFactor, setZoomLevel } from '../../../base/browser import { getActiveWindow, getWindows } from '../../../base/browser/dom.js'; import { mainWindow } from '../../../base/browser/window.js'; import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes.js'; -import { ISandboxGlobals, ipcRenderer, webFrame } from '../../../base/parts/sandbox/electron-sandbox/globals.js'; +import { ISandboxGlobals, ipcRenderer, webFrame, IDevice, deviceAccess } from '../../../base/parts/sandbox/electron-sandbox/globals.js'; import { zoomLevelToZoomFactor } from '../common/window.js'; +import { IpcRendererEvent } from '../../../base/parts/sandbox/electron-sandbox/electronTypes.js'; export enum ApplyZoomTarget { ACTIVE_WINDOW = 1, @@ -44,7 +45,7 @@ export function applyZoom(zoomLevel: number, target: ApplyZoomTarget | Window): function getGlobals(win: Window): ISandboxGlobals | undefined { if (win === mainWindow) { // main window - return { ipcRenderer, webFrame }; + return { ipcRenderer, webFrame, deviceAccess }; } else { // auxiliary window const auxiliaryWindow = win as unknown as { vscode: ISandboxGlobals }; @@ -64,6 +65,19 @@ export function zoomOut(target: ApplyZoomTarget | Window): void { applyZoom(getZoomLevel(typeof target === 'number' ? getActiveWindow() : target) - 1, target); } +export function registerDeviceAccessHandler(handler: (devices: IDevice[]) => Promise) { + for (const { window } of getWindows()) { + const globals = getGlobals(window); + + if (globals) { + globals.deviceAccess.handleDeviceAccess(async (_event: IpcRendererEvent, devices: IDevice[]) => { + const id = await handler(devices); + globals.ipcRenderer.send('vscode:device-access', id); + }); + } + } +} + //#region Bootstrap Window export interface ILoadOptions { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index ab629987eae88..aa2f7fde3ac99 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -706,6 +706,46 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }); }); + + // Enable WebUSB, WebHID and WebSerial device access + this._win.webContents.session.setPermissionCheckHandler((_webContents, permission, _requestingOrigin, _details) => { + return permission === 'usb' || permission === 'serial' || permission === 'hid'; + }); + + this._win.webContents.session.on('select-usb-device', (event, details, callback) => { + event.preventDefault(); + const items = details.deviceList.map(device => ({ + id: device.deviceId, + label: device.productName || device.serialNumber || `${device.vendorId}:${device.productId}` + })); + handleDeviceSelect(items, callback); + }); + + this._win.webContents.session.on('select-hid-device', (event, details, callback) => { + event.preventDefault(); + const items = details.deviceList.map(device => ({ + id: device.deviceId, + label: device.name + })); + handleDeviceSelect(items, callback); + }); + + this._win.webContents.session.on('select-serial-port', (event, portList, _webContents, callback) => { + event.preventDefault(); + const items = portList.map(device => ({ + id: device.portId, + label: device.displayName || device.portName + })); + handleDeviceSelect(items, callback); + }); + + const handleDeviceSelect = (items: { id: string; label: string }[], callback: (id: string) => void) => { + // Listen to callback from renderer + electron.ipcMain.once('vscode:device-access', (_event, value) => callback(value)); + + // Send details of list to be picked from + this.send('vscode:device-access', items); + }; } private marketplaceHeadersPromise: Promise | undefined; diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 4babd90fe708c..1f81d49400160 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -17,7 +17,7 @@ import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js' import { WindowMinimumSize, IOpenFileRequest, IAddRemoveFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js'; import { ITitleService } from '../services/title/browser/titleService.js'; import { IWorkbenchThemeService } from '../services/themes/common/workbenchThemeService.js'; -import { ApplyZoomTarget, applyZoom } from '../../platform/window/electron-sandbox/window.js'; +import { ApplyZoomTarget, applyZoom, registerDeviceAccessHandler } from '../../platform/window/electron-sandbox/window.js'; import { setFullscreen, getZoomLevel, onDidChangeZoomLevel, getZoomFactor } from '../../base/browser/browser.js'; import { ICommandService, CommandsRegistry } from '../../platform/commands/common/commands.js'; import { IResourceEditorInput } from '../../platform/editor/common/editor.js'; @@ -57,7 +57,7 @@ import { IEditorGroupsService, IEditorPart } from '../services/editor/common/edi import { IDialogService } from '../../platform/dialogs/common/dialogs.js'; import { AuthInfo } from '../../base/parts/sandbox/electron-sandbox/electronTypes.js'; import { ILogService } from '../../platform/log/common/log.js'; -import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../platform/instantiation/common/instantiation.js'; import { whenEditorClosed } from '../browser/editor.js'; import { ISharedProcessService } from '../../platform/ipc/electron-sandbox/services.js'; import { IProgressService, ProgressLocation } from '../../platform/progress/common/progress.js'; @@ -79,6 +79,8 @@ import { ThemeIcon } from '../../base/common/themables.js'; import { getWorkbenchContribution } from '../common/contributions.js'; import { DynamicWorkbenchSecurityConfiguration } from '../common/configuration.js'; import { nativeHoverDelegate } from '../../platform/hover/browser/hover.js'; +import { HidDeviceData, SerialPortData, UsbDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice } from '../../base/browser/deviceAccess.js'; +import { IQuickInputService } from '../../platform/quickinput/common/quickInput.js'; export class NativeWindow extends BaseWindow { @@ -127,7 +129,8 @@ export class NativeWindow extends BaseWindow { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(mainWindow, undefined, hostService, nativeEnvironmentService); @@ -684,6 +687,12 @@ export class NativeWindow extends BaseWindow { // Touchbar menu (if enabled) this.updateTouchbarMenu(); + // Commands + this.registerCommands(); + + // Handlers + this.registerHandlers(); + // Smoke Test Driver if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); @@ -1054,6 +1063,33 @@ export class NativeWindow extends BaseWindow { return this.editorService.openEditors(editors, undefined, { validateTrust: true }); } + private registerCommands(): void { + + // Allow extensions to request USB devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestUsbDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise => { + return requestUsbDevice(options); + }); + + // Allow extensions to request Serial devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestSerialPort', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise => { + return requestSerialPort(options); + }); + + // Allow extensions to request HID devices in Web + CommandsRegistry.registerCommand('workbench.experimental.requestHidDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise => { + return requestHidDevice(options); + }); + } + + private registerHandlers(): void { + + // Show a picker when a device is requested + registerDeviceAccessHandler(async devices => { + const device = await this.quickInputService.pick(devices, { title: `${this.productService.nameShort} wants to connect` }); + return device?.id; + }); + } + //#region Window Zoom private readonly mapWindowIdToZoomStatusEntry = new Map();