Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop support for USB, HID and Serial device access #198047

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
348ec1c
Add Web-* device support in electron
thegecko Nov 10, 2023
7852383
Merge branch 'main' into electron-devices
thegecko Nov 11, 2023
324e57f
Add device picker UI
thegecko Nov 12, 2023
ad8cd09
Merge branch 'main' into electron-devices
thegecko Nov 12, 2023
4ebf7e8
Merge branch 'main' into electron-devices
thegecko Nov 13, 2023
e5feb19
Merge branch 'main' into electron-devices
thegecko Nov 14, 2023
9f26a12
Merge branch 'main' into electron-devices
thegecko Nov 14, 2023
c7932cd
Merge branch 'main' into electron-devices
thegecko Nov 15, 2023
449db4c
Merge branch 'main' into electron-devices
thegecko Nov 16, 2023
f9e4a7e
Merge branch 'main' into electron-devices
thegecko Nov 21, 2023
594fe80
Merge branch 'main' into electron-devices
thegecko Nov 22, 2023
b193f74
Merge branch 'main' into electron-devices
thegecko Dec 1, 2023
e96ee44
Merge branch 'main' into electron-devices
thegecko Dec 9, 2023
d6bdd5f
Merge branch 'main' into electron-devices
thegecko Dec 12, 2023
41dad90
Merge branch 'main' into electron-devices
thegecko Dec 17, 2023
a461ed2
Merge branch 'main' into electron-devices
thegecko Dec 18, 2023
1db8fd1
Merge branch 'main' into electron-devices
thegecko Dec 27, 2023
2ce8e0f
Merge branch 'main' into electron-devices
thegecko Jan 2, 2024
a922ec2
Merge branch 'main' into electron-devices
thegecko Jan 4, 2024
332c97b
Merge branch 'main' into electron-devices
thegecko Jan 25, 2024
2e4977a
Merge branch 'main' into electron-devices
thegecko Feb 2, 2024
81074b3
Merge branch 'main' into electron-devices
thegecko Feb 9, 2024
0b34d48
Merge branch 'main' into electron-devices
thegecko Mar 2, 2024
c29ad8a
Merge branch 'main' into electron-devices
thegecko Apr 22, 2024
3ca241c
Merge branch 'main' into electron-devices
thegecko May 11, 2024
a1464f1
Merge branch 'main' into electron-devices
thegecko May 17, 2024
ede38c6
Merge branch 'main' into electron-devices
thegecko May 21, 2024
d4e39bd
Merge branch 'main' into electron-devices
thegecko Jun 5, 2024
0b33b92
Merge branch 'main' into electron-devices
thegecko Jun 11, 2024
f5f88c6
Merge branch 'main' into electron-devices
thegecko Dec 24, 2024
81f370f
Use vscode: channels
thegecko Dec 24, 2024
0cfbc8c
Merge branch 'main' into electron-devices
thegecko Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/vs/base/parts/sandbox/electron-sandbox/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,13 +115,23 @@ export interface ISandboxContext {
resolveConfiguration(): Promise<ISandboxConfiguration>;
}

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;
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
Expand All @@ -134,6 +144,7 @@ export interface IMainWindowSandboxGlobals {
readonly process: ISandboxNodeProcess;
readonly context: ISandboxContext;
readonly webUtils: WebUtils;
readonly deviceAccess: IDeviceAccess;
}

/**
Expand All @@ -143,4 +154,5 @@ export interface IMainWindowSandboxGlobals {
export interface ISandboxGlobals {
readonly ipcRenderer: Pick<import('./electronTypes.js').IpcRenderer, 'send' | 'invoke'>;
readonly webFrame: import('./electronTypes.js').WebFrame;
readonly deviceAccess: IDeviceAccess;
}
13 changes: 12 additions & 1 deletion src/vs/base/parts/sandbox/electron-sandbox/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -90,6 +91,14 @@

//#endregion

//#region Device Access

const deviceAccess: IDeviceAccess = {
handleDeviceAccess: callback => ipcRenderer.on('vscode:device-access', callback),
};

//#endregion

//#region Globals Definition

// #######################################################################
Expand Down Expand Up @@ -243,7 +252,9 @@
async resolveConfiguration(): Promise<ISandboxConfiguration> {
return resolveConfiguration;
}
}
},

deviceAccess
};

// Use `contextBridge` APIs to expose globals to VSCode
Expand Down
18 changes: 16 additions & 2 deletions src/vs/platform/window/electron-sandbox/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 };
Expand All @@ -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<string | undefined>) {
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<T extends ISandboxConfiguration = ISandboxConfiguration> {
Expand Down
40 changes: 40 additions & 0 deletions src/vs/platform/windows/electron-main/windowImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> | undefined;
Expand Down
42 changes: 39 additions & 3 deletions src/vs/workbench/electron-sandbox/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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 {

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<UsbDeviceData | undefined> => {
return requestUsbDevice(options);
});

// Allow extensions to request Serial devices in Web
CommandsRegistry.registerCommand('workbench.experimental.requestSerialPort', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<SerialPortData | undefined> => {
return requestSerialPort(options);
});

// Allow extensions to request HID devices in Web
CommandsRegistry.registerCommand('workbench.experimental.requestHidDevice', async (_accessor: ServicesAccessor, options?: { filters?: unknown[] }): Promise<HidDeviceData | undefined> => {
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<number, ZoomStatusEntry>();
Expand Down