From 348ec1c391875af0c4114477cf182ffd095b9471 Mon Sep 17 00:00:00 2001 From: thegecko Date: Fri, 10 Nov 2023 14:51:52 +0000 Subject: [PATCH 1/3] 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/3] 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; + }); + } } From 81f370f0e7f85dd1180a0b6928c1039e6331b366 Mon Sep 17 00:00:00 2001 From: robmor01 Date: Tue, 24 Dec 2024 16:05:15 +0000 Subject: [PATCH 3/3] Use vscode: channels --- .../sandbox/electron-sandbox/electronTypes.ts | 9 --------- .../parts/sandbox/electron-sandbox/globals.ts | 12 ++++++++++- .../parts/sandbox/electron-sandbox/preload.ts | 4 ++-- .../window/electron-sandbox/window.ts | 20 ++++++++++--------- .../windows/electron-main/windowImpl.ts | 20 ++++++++++--------- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index 4291ec20bdc7c..a132d7d6eb41d 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -188,12 +188,3 @@ export interface WebUtils { */ getPathForFile(file: File): string; } - -export interface IDevice { - id: string; - label: string; -} - -export interface IDeviceAccess { - handleDeviceAccess: (callback: (event: IpcRendererEvent, type: string, devices: IDevice[]) => void) => void; -} diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index a19b59f71add5..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, IDeviceAccess } 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; @@ -135,6 +144,7 @@ export interface IMainWindowSandboxGlobals { readonly process: ISandboxNodeProcess; readonly context: ISandboxContext; readonly webUtils: WebUtils; + 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 7bcfbec882136..ea35285fcf0b3 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.ts @@ -10,7 +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/electronTypes.js').IDeviceAccess; + type IDeviceAccess = import('vs/base/parts/sandbox/electron-sandbox/globals.js').IDeviceAccess; //#region Utilities @@ -94,7 +94,7 @@ //#region Device Access const deviceAccess: IDeviceAccess = { - handleDeviceAccess: callback => ipcRenderer.on('device-access', callback), + handleDeviceAccess: callback => ipcRenderer.on('vscode:device-access', callback), }; //#endregion diff --git a/src/vs/platform/window/electron-sandbox/window.ts b/src/vs/platform/window/electron-sandbox/window.ts index 362409bee8142..6c4171bbf4043 100644 --- a/src/vs/platform/window/electron-sandbox/window.ts +++ b/src/vs/platform/window/electron-sandbox/window.ts @@ -7,9 +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, deviceAccess } 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, IDevice } from '../../../base/parts/sandbox/electron-sandbox/electronTypes.js'; +import { IpcRendererEvent } from '../../../base/parts/sandbox/electron-sandbox/electronTypes.js'; export enum ApplyZoomTarget { ACTIVE_WINDOW = 1, @@ -65,14 +65,16 @@ export function zoomOut(target: ApplyZoomTarget | Window): void { applyZoom(getZoomLevel(typeof target === 'number' ? getActiveWindow() : target) - 1, target); } -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); - }; - +export function registerDeviceAccessHandler(handler: (devices: IDevice[]) => Promise) { for (const { window } of getWindows()) { - getGlobals(window)?.deviceAccess.handleDeviceAccess(asyncHandler); + 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); + }); + } } } diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 43eead4882561..aa2f7fde3ac99 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -714,36 +714,38 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this._win.webContents.session.on('select-usb-device', (event, details, callback) => { event.preventDefault(); - const type = 'select-usb-device'; const items = details.deviceList.map(device => ({ id: device.deviceId, label: device.productName || device.serialNumber || `${device.vendorId}:${device.productId}` })); - Electron.ipcMain.once(type, (_event, value) => callback(value)); - this._win.webContents.send('device-access', type, items); + handleDeviceSelect(items, callback); }); this._win.webContents.session.on('select-hid-device', (event, details, callback) => { event.preventDefault(); - const type = 'select-hid-device'; const items = details.deviceList.map(device => ({ id: device.deviceId, label: device.name })); - Electron.ipcMain.once(type, (_event, value) => callback(value)); - this._win.webContents.send('device-access', type, items); + handleDeviceSelect(items, callback); }); this._win.webContents.session.on('select-serial-port', (event, portList, _webContents, callback) => { event.preventDefault(); - const type = 'select-serial-port'; const items = portList.map(device => ({ id: device.portId, label: device.displayName || device.portName })); - Electron.ipcMain.once(type, (_event, value) => callback(value)); - this._win.webContents.send('device-access', type, items); + 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;