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 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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
13 changes: 12 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 'vs/base/common/platform';
import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes';
import { IpcRenderer, ProcessMemoryInfo, WebFrame, WebUtils } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
import { IpcRenderer, IpcRendererEvent, ProcessMemoryInfo, WebFrame, WebUtils } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';

/**
* 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, 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 webUtils: WebUtils = vscodeGlobal.webUtils;
export const deviceAccess: IDeviceAccess = vscodeGlobal.deviceAccess;

/**
* A set of globals that are available in all windows that either
Expand All @@ -130,4 +140,5 @@ export const webUtils: WebUtils = vscodeGlobal.webUtils;
export interface ISandboxGlobals {
readonly ipcRenderer: Pick<import('vs/base/parts/sandbox/electron-sandbox/electronTypes').IpcRenderer, 'send' | 'invoke'>;
readonly webFrame: import('vs/base/parts/sandbox/electron-sandbox/electronTypes').WebFrame;
readonly deviceAccess: IDeviceAccess;
}
8 changes: 8 additions & 0 deletions src/vs/base/parts/sandbox/electron-sandbox/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@
async resolveConfiguration() {
return resolveConfiguration;
}
},

/**
*
* @type {import('./globals').IDeviceAccess}
*/
deviceAccess: {
handleDeviceAccess: callback => ipcRenderer.on('device-access', callback),
}
};

Expand Down
16 changes: 14 additions & 2 deletions src/vs/platform/window/electron-sandbox/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import { getZoomLevel, setZoomFactor, setZoomLevel } from 'vs/base/browser/browser';
import { getActiveWindow, 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';

export enum ApplyZoomTarget {
Expand Down Expand Up @@ -43,7 +44,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 @@ -62,3 +63,14 @@ export function zoomIn(target: ApplyZoomTarget | Window): void {
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<string | undefined>) {
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);
}
}
86 changes: 85 additions & 1 deletion src/vs/platform/windows/electron-main/windowImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { app, BrowserWindow, Display, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl, WebContents, Event as ElectronEvent } from 'electron';
import { app, BrowserWindow, Display, Event as ElectronEvent, ipcMain, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl, WebContents, Event as ElectronEvent } 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';
Expand Down Expand Up @@ -741,6 +741,90 @@ 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 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();
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();
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);
});

// Windows Custom System Context Menu
// See https://github.com/electron/electron/issues/24893
//
// The purpose of this is to allow for the context menu in the Windows Title Bar
//
// Currently, all mouse events in the title bar are captured by the OS
// thus we need to capture them here with a window hook specific to Windows
// and then forward them to the correct window.
const useCustomTitleStyle = getTitleBarStyle(this.configurationService) === 'custom';
if (isWindows && useCustomTitleStyle) {
const WM_INITMENU = 0x0116; // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-initmenu

// This sets up a listener for the window hook. This is a Windows-only API provided by electron.
this._win.hookWindowMessage(WM_INITMENU, () => {
const [x, y] = this._win.getPosition();
const cursorPos = screen.getCursorScreenPoint();
const cx = cursorPos.x - x;
const cy = cursorPos.y - y;

// In some cases, show the default system context menu
// 1) The mouse position is not within the title bar
// 2) The mouse position is within the title bar, but over the app icon
// We do not know the exact title bar height but we make an estimate based on window height
const shouldTriggerDefaultSystemContextMenu = () => {
// Use the custom context menu when over the title bar, but not over the app icon
// The app icon is estimated to be 30px wide
// The title bar is estimated to be the max of 35px and 15% of the window height
if (cx > 30 && cy >= 0 && cy <= Math.max(this._win.getBounds().height * 0.15, 35)) {
return false;
}

return true;
};

if (!shouldTriggerDefaultSystemContextMenu()) {
// This is necessary to make sure the native system context menu does not show up.
this._win.setEnabled(false);
this._win.setEnabled(true);

this._onDidTriggerSystemContextMenu.fire({ x: cx, y: cy });
}

return 0;
});
}
}

private marketplaceHeadersPromise: Promise<object> | undefined;
Expand Down
44 changes: 40 additions & 4 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 'vs/platform/telemetry/common/telemetry';
import { WindowMinimumSize, IOpenFileRequest, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from 'vs/platform/window/common/window';
import { ITitleService } from 'vs/workbench/services/title/browser/titleService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ApplyZoomTarget, applyZoom } from 'vs/platform/window/electron-sandbox/window';
import { ApplyZoomTarget, applyZoom, registerDeviceAccessHandler } from 'vs/platform/window/electron-sandbox/window';
import { setFullscreen, getZoomLevel, onDidChangeZoomLevel, getZoomFactor } from 'vs/base/browser/browser';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
Expand Down Expand Up @@ -72,6 +72,8 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService';
import { registerWindowDriver } from 'vs/workbench/services/driver/electron-sandbox/driver';
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';
import { BaseWindow } from 'vs/workbench/browser/window';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IStatusbarService, ShowTooltipCommand, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
Expand Down Expand Up @@ -129,7 +131,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 @@ -674,7 +677,13 @@ export class NativeWindow extends BaseWindow {
// Touchbar menu (if enabled)
this.updateTouchbarMenu();

// Zoom status
// Commands
this.registerCommands();

// Handlers
this.registerHandlers();

// Zoom status
for (const { window, disposables } of getWindows()) {
this.createWindowZoomStatusEntry(this.instantiationService, window.vscodeWindowId, disposables);
}
Expand Down Expand Up @@ -1055,7 +1064,34 @@ export class NativeWindow extends BaseWindow {
return this.editorService.openEditors(editors, undefined, { validateTrust: true });
}

//#region Window Zoom
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