From 348ec1c391875af0c4114477cf182ffd095b9471 Mon Sep 17 00:00:00 2001 From: thegecko Date: Fri, 10 Nov 2023 14:51:52 +0000 Subject: [PATCH 1/2] Add Web-* device support in electron --- .../windows/electron-main/windowImpl.ts | 23 +++++++++++++++++++ src/vs/workbench/electron-sandbox/window.ts | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 5fec8f9520ba3..2dc40cc266045 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -654,6 +654,29 @@ 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(); + // ToDo: Show picker of devices instead of returning first + callback(details.deviceList?.[0]?.deviceId); + }); + + this._win.webContents.session.on('select-hid-device', (event, details, callback) => { + event.preventDefault(); + // ToDo: Show picker of devices instead of returning first + callback(details.deviceList?.[0]?.deviceId); + }); + + this._win.webContents.session.on('select-serial-port', (event, portList, _webContents, callback) => { + event.preventDefault(); + // ToDo: Show picker of devices instead of returning first + callback(portList[0]?.portId); + }); } private marketplaceHeadersPromise: Promise | undefined; diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 5623d25571ba3..12ea2a7d553dc 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -72,6 +72,7 @@ import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/uti import { registerWindowDriver } from 'vs/workbench/services/driver/electron-sandbox/driver'; import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { mainWindow } from 'vs/base/browser/window'; +import { HidDeviceData, SerialPortData, UsbDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice } from 'vs/base/browser/deviceAccess'; export class NativeWindow extends Disposable { @@ -682,6 +683,9 @@ export class NativeWindow extends Disposable { // Touchbar menu (if enabled) this.updateTouchbarMenu(); + // Commands + this.registerCommands(); + // Smoke Test Driver if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); @@ -1034,4 +1038,22 @@ export class NativeWindow extends Disposable { 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); + }); + } } From 324e57fa0e48a8ba0437cb6bc5fbdafc0a587dcb Mon Sep 17 00:00:00 2001 From: thegecko Date: Sun, 12 Nov 2023 16:56:58 +0000 Subject: [PATCH 2/2] Add device picker UI --- .../parts/sandbox/electron-sandbox/globals.ts | 13 ++++++++- .../parts/sandbox/electron-sandbox/preload.js | 8 +++++ .../window/electron-sandbox/window.ts | 16 ++++++++-- .../windows/electron-main/windowImpl.ts | 29 ++++++++++++++----- src/vs/workbench/electron-sandbox/window.ts | 18 ++++++++++-- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 44a54904e26fa..9b0db52b06628 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 'vs/base/common/platform'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; -import { IpcRenderer, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { IpcRenderer, IpcRendererEvent, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; /** * In Electron renderers we cannot expose all of the `process` global of node.js @@ -115,12 +115,22 @@ export interface ISandboxContext { resolveConfiguration(): Promise; } +export interface IDevice { + id: string; + label: string; +} + +export interface IDeviceAccess { + handleDeviceAccess: (callback: (event: IpcRendererEvent, type: string, devices: IDevice[]) => void) => void; +} + const vscodeGlobal = (globalThis as any).vscode; export const ipcRenderer: IpcRenderer = vscodeGlobal.ipcRenderer; export const ipcMessagePort: IpcMessagePort = vscodeGlobal.ipcMessagePort; export const webFrame: WebFrame = vscodeGlobal.webFrame; export const process: ISandboxNodeProcess = vscodeGlobal.process; export const context: ISandboxContext = vscodeGlobal.context; +export const deviceAccess: IDeviceAccess = vscodeGlobal.deviceAccess; /** * A set of globals that are available in all windows that either @@ -129,4 +139,5 @@ export const context: ISandboxContext = vscodeGlobal.context; export interface ISandboxGlobals { readonly ipcRenderer: Pick; readonly webFrame: import('vs/base/parts/sandbox/electron-sandbox/electronTypes').WebFrame; + readonly deviceAccess: IDeviceAccess; } diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.js b/src/vs/base/parts/sandbox/electron-sandbox/preload.js index 90ac940861fb4..9e7925ec298b2 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.js +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.js @@ -316,6 +316,14 @@ async resolveConfiguration() { return resolveConfiguration; } + }, + + /** + * + * @type {import('./globals').IDeviceAccess} + */ + deviceAccess: { + handleDeviceAccess: callback => ipcRenderer.on('device-access', callback), } }; diff --git a/src/vs/platform/window/electron-sandbox/window.ts b/src/vs/platform/window/electron-sandbox/window.ts index 55c34942677e9..01fe7891c408c 100644 --- a/src/vs/platform/window/electron-sandbox/window.ts +++ b/src/vs/platform/window/electron-sandbox/window.ts @@ -6,7 +6,8 @@ import { getZoomLevel, setZoomFactor, setZoomLevel } from 'vs/base/browser/browser'; import { getWindows } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; -import { ISandboxGlobals, ipcRenderer, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; +import { IDevice, ISandboxGlobals, deviceAccess, ipcRenderer, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; /** @@ -24,7 +25,7 @@ export function applyZoom(zoomLevel: number): void { 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 }; @@ -43,3 +44,14 @@ export function zoomIn(): void { export function zoomOut(): void { applyZoom(getZoomLevel() - 1); } + +export function registerDeviceAccessHandler(handler: (devices: IDevice[], type: string) => Promise) { + const asyncHandler = async (event: IpcRendererEvent, type: string, devices: IDevice[]) => { + const id = await handler(devices, type); + event.sender.send(type, id); + }; + + for (const { window } of getWindows()) { + getGlobals(window)?.deviceAccess.handleDeviceAccess(asyncHandler); + } +} diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 2dc40cc266045..bbdac7fa48b1e 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, Display, Event as ElectronEvent, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; +import { app, BrowserWindow, Display, Event as ElectronEvent, ipcMain, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; import { DeferredPromise, RunOnceScheduler, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -662,20 +662,35 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this._win.webContents.session.on('select-usb-device', (event, details, callback) => { event.preventDefault(); - // ToDo: Show picker of devices instead of returning first - callback(details.deviceList?.[0]?.deviceId); + const type = 'select-usb-device'; + const items = details.deviceList.map(device => ({ + id: device.deviceId, + label: device.productName || device.serialNumber || `${device.vendorId}:${device.productId}` + })); + ipcMain.once(type, (_event, value) => callback(value)); + this._win.webContents.send('device-access', type, items); }); this._win.webContents.session.on('select-hid-device', (event, details, callback) => { event.preventDefault(); - // ToDo: Show picker of devices instead of returning first - callback(details.deviceList?.[0]?.deviceId); + const type = 'select-hid-device'; + const items = details.deviceList.map(device => ({ + id: device.deviceId, + label: device.name + })); + ipcMain.once(type, (_event, value) => callback(value)); + this._win.webContents.send('device-access', type, items); }); this._win.webContents.session.on('select-serial-port', (event, portList, _webContents, callback) => { event.preventDefault(); - // ToDo: Show picker of devices instead of returning first - callback(portList[0]?.portId); + const type = 'select-serial-port'; + const items = portList.map(device => ({ + id: device.portId, + label: device.displayName || device.portName + })); + ipcMain.once(type, (_event, value) => callback(value)); + this._win.webContents.send('device-access', type, items); }); } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 12ea2a7d553dc..20d1240933b7f 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WindowMinimumSize, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/window/common/window'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; +import { applyZoom, registerDeviceAccessHandler } from 'vs/platform/window/electron-sandbox/window'; import { setFullscreen, getZoomLevel } from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -73,6 +73,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/electron-sand import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { mainWindow } from 'vs/base/browser/window'; import { HidDeviceData, SerialPortData, UsbDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice } from 'vs/base/browser/deviceAccess'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class NativeWindow extends Disposable { @@ -128,7 +129,8 @@ export class NativeWindow extends Disposable { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, - @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); @@ -686,6 +688,9 @@ export class NativeWindow extends Disposable { // Commands this.registerCommands(); + // Handlers + this.registerHandlers(); + // Smoke Test Driver if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); @@ -1056,4 +1061,13 @@ export class NativeWindow extends Disposable { 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; + }); + } }