Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Commit

Permalink
feat: add support for WebUSB (electron#36289)
Browse files Browse the repository at this point in the history
* feat: add support for WebUSB

* fixup for gn check

* fixup gn check on Windows

* Apply review feedback

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: address review feedback

* chore: removed unneeded code

* Migrate non-default ScopedObservation<> instantiations to ScopedObservationTraits<> in chrome/browser/

https://chromium-review.googlesource.com/c/chromium/src/+/4016595

Co-authored-by: Charles Kerr <charles@charleskerr.com>
  • Loading branch information
2 people authored and khalwa committed Feb 22, 2023
1 parent cf78ff1 commit 53f8915
Show file tree
Hide file tree
Showing 29 changed files with 1,772 additions and 23 deletions.
120 changes: 117 additions & 3 deletions docs/api/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ app.whenReady().then(() => {
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
callback(selectedPort?.deviceId)
callback(selectedDevice?.deviceId)
})
})
```
Expand Down Expand Up @@ -429,6 +429,118 @@ const portConnect = async () => {
}
```
#### Event: 'select-usb-device'
Returns:
* `event` Event
* `details` Object
* `deviceList` [USBDevice[]](structures/usb-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `callback` Function
* `deviceId` string (optional)
Emitted when a USB device needs to be selected when a call to
`navigator.usb.requestDevice` is made. `callback` should be called with
`deviceId` to be selected; passing no arguments to `callback` will
cancel the request. Additionally, permissioning on `navigator.usb` can
be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).

```javascript
const { app, BrowserWindow } = require('electron')
let win = null
app.whenReady().then(() => {
win = new BrowserWindow()
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'usb') {
// Add logic here to determine if permission should be given to allow USB selection
return true
}
return false
})
// Optionally, retrieve previously persisted devices from a persistent store (fetchGrantedDevices needs to be implemented by developer to fetch persisted permissions)
const grantedDevices = fetchGrantedDevices()
win.webContents.session.setDevicePermissionHandler((details) => {
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'usb') {
if (details.device.vendorId === 123 && details.device.productId === 345) {
// Always allow this type of device (this allows skipping the call to `navigator.usb.requestDevice` first)
return true
}
// Search through the list of devices that have previously been granted permission
return grantedDevices.some((grantedDevice) => {
return grantedDevice.vendorId === details.device.vendorId &&
grantedDevice.productId === details.device.productId &&
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
})
}
return false
})
win.webContents.session.on('select-usb-device', (event, details, callback) => {
event.preventDefault()
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
if (selectedDevice) {
// Optionally, add this to the persisted devices (updateGrantedDevices needs to be implemented by developer to persist permissions)
grantedDevices.push(selectedDevice)
updateGrantedDevices(grantedDevices)
}
callback(selectedDevice?.deviceId)
})
})
```

#### Event: 'usb-device-added'

Returns:

* `event` Event
* `details` Object
* `device` [USBDevice](structures/usb-device.md)
* `frame` [WebFrameMain](web-frame-main.md)

Emitted after `navigator.usb.requestDevice` has been called and
`select-usb-device` has fired if a new device becomes available before
the callback from `select-usb-device` is called. This event is intended for
use when using a UI to ask users to pick a device so that the UI can be updated
with the newly added device.

#### Event: 'usb-device-removed'

Returns:

* `event` Event
* `details` Object
* `device` [USBDevice](structures/usb-device.md)
* `frame` [WebFrameMain](web-frame-main.md)

Emitted after `navigator.usb.requestDevice` has been called and
`select-usb-device` has fired if a device has been removed before the callback
from `select-usb-device` is called. This event is intended for use when using
a UI to ask users to pick a device so that the UI can be updated to remove the
specified device.

#### Event: 'usb-device-revoked'

Returns:

* `event` Event
* `details` Object
* `device` [USBDevice[]](structures/usb-device.md)
* `origin` string (optional) - The origin that the device has been revoked from.

Emitted after `USBDevice.forget()` has been called. This event can be used
to help maintain persistent storage of permissions when
`setDevicePermissionHandler` is used.

### Instance Methods

The following methods are available on instances of `Session`:
Expand Down Expand Up @@ -714,7 +826,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
* `handler` Function\<boolean> | null
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
* `permission` string - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
* `permission` string - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, `serial`, or `usb`.
* `requestingOrigin` string - The origin URL of the permission check
* `details` Object - Some properties are only available on certain permission types.
* `embeddingOrigin` string (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
Expand Down Expand Up @@ -800,7 +912,7 @@ Passing `null` instead of a function resets the handler to its default state.
* `handler` Function\<boolean> | null
* `details` Object
* `deviceType` string - The type of device that permission is being requested on, can be `hid` or `serial`.
* `deviceType` string - The type of device that permission is being requested on, can be `hid`, `serial`, or `usb`.
* `origin` string - The origin URL of the device permission check.
* `device` [HIDDevice](structures/hid-device.md) | [SerialPort](structures/serial-port.md)- the device that permission is being requested for.
Expand Down Expand Up @@ -828,6 +940,8 @@ app.whenReady().then(() => {
return true
} else if (permission === 'serial') {
// Add logic here to determine if permission should be given to allow serial port selection
} else if (permission === 'usb') {
// Add logic here to determine if permission should be given to allow USB device selection
}
return false
})
Expand Down
17 changes: 17 additions & 0 deletions docs/api/structures/usb-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# USBDevice Object

* `deviceId` string - Unique identifier for the device.
* `vendorId` Integer - The USB vendor ID.
* `productId` Integer - The USB product ID.
* `productName` string (optional) - Name of the device.
* `serialNumber` string (optional) - The USB device serial number.
* `manufacturerName` string (optional) - The manufacturer name of the device.
* `usbVersionMajor` Integer - The USB protocol major version supported by the device
* `usbVersionMinor` Integer - The USB protocol minor version supported by the device
* `usbVersionSubminor` Integer - The USB protocol subminor version supported by the device
* `deviceClass` Integer - The device class for the communication interface supported by the device
* `deviceSubclass` Integer - The device subclass for the communication interface supported by the device
* `deviceProtocol` Integer - The device protocol for the communication interface supported by the device
* `deviceVersionMajor` Integer - The major version number of the device as defined by the device manufacturer.
* `deviceVersionMinor` Integer - The minor version number of the device as defined by the device manufacturer.
* `deviceVersionSubminor` Integer - The subminor version number of the device as defined by the device manufacturer.
21 changes: 21 additions & 0 deletions docs/fiddles/features/web-usb/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>WebUSB API</title>
</head>
<body>
<h1>WebUSB API</h1>

<button id="clickme">Test WebUSB</button>

<h3>USB devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
<div id="granted-devices"></div>

<h3>USB devices automatically granted access via <i>select-usb-device</i></h3>
<div id="granted-devices2"></div>

<script src="./renderer.js"></script>
</body>
</html>
72 changes: 72 additions & 0 deletions docs/fiddles/features/web-usb/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const {app, BrowserWindow} = require('electron')
const e = require('express')
const path = require('path')

function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})

let grantedDeviceThroughPermHandler

mainWindow.webContents.session.on('select-usb-device', (event, details, callback) => {
//Add events to handle devices being added or removed before the callback on
//`select-usb-device` is called.
mainWindow.webContents.session.on('usb-device-added', (event, device) => {
console.log('usb-device-added FIRED WITH', device)
//Optionally update details.deviceList
})

mainWindow.webContents.session.on('usb-device-removed', (event, device) => {
console.log('usb-device-removed FIRED WITH', device)
//Optionally update details.deviceList
})

event.preventDefault()
if (details.deviceList && details.deviceList.length > 0) {
const deviceToReturn = details.deviceList.find((device) => {
if (!grantedDeviceThroughPermHandler || (device.deviceId != grantedDeviceThroughPermHandler.deviceId)) {
return true
}
})
if (deviceToReturn) {
callback(deviceToReturn.deviceId)
} else {
callback()
}
}
})

mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'usb' && details.securityOrigin === 'file:///') {
return true
}
})


mainWindow.webContents.session.setDevicePermissionHandler((details) => {
if (details.deviceType === 'usb' && details.origin === 'file://') {
if (!grantedDeviceThroughPermHandler) {
grantedDeviceThroughPermHandler = details.device
return true
} else {
return false
}
}
})

mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
33 changes: 33 additions & 0 deletions docs/fiddles/features/web-usb/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function getDeviceDetails(device) {
return grantedDevice.productName || `Unknown device ${grantedDevice.deviceId}`
}

async function testIt() {
const noDevicesFoundMsg = 'No devices found'
const grantedDevices = await navigator.usb.getDevices()
let grantedDeviceList = ''
if (grantedDevices.length > 0) {
grantedDevices.forEach(device => {
grantedDeviceList += `<hr>${getDeviceDetails(device)}</hr>`
})
} else {
grantedDeviceList = noDevicesFoundMsg
}
document.getElementById('granted-devices').innerHTML = grantedDeviceList

grantedDeviceList = ''
try {
const grantedDevice = await navigator.usb.requestDevice({
filters: []
})
grantedDeviceList += `<hr>${getDeviceDetails(device)}</hr>`

} catch (ex) {
if (ex.name === 'NotFoundError') {
grantedDeviceList = noDevicesFoundMsg
}
}
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
}

document.getElementById('clickme').addEventListener('click',testIt)
38 changes: 38 additions & 0 deletions docs/tutorial/devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,41 @@ when the `Test Web Serial` button is clicked.
```javascript fiddle='docs/fiddles/features/web-serial'

```

## WebUSB API

The [WebUSB API](https://web.dev/usb/) can be used to access USB devices.
Electron provides several APIs for working with the WebUSB API:

* The [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device)
can be used to select a USB device when a call to
`navigator.usb.requestDevice` is made. Additionally the [`usb-device-added`](../api/session.md#event-usb-device-added)
and [`usb-device-removed`](../api/session.md#event-usb-device-removed) events
on the Session can be used to handle devices being plugged in or unplugged
when handling the `select-usb-device` event.
**Note:** These two events only fire until the callback from `select-usb-device`
is called. They are not intended to be used as a generic usb device listener.
* The [`usb-device-revoked' event on the Session](../api/session.md#event-usb-device-revoked) can
be used to respond when [device.forget()](https://developer.chrome.com/articles/usb/#revoke-access)
is called on a USB device.
* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
can be used to provide default permissioning to devices without first calling
for permission to devices via `navigator.usb.requestDevice`. Additionally,
the default behavior of Electron is to store granted device permission through
the lifetime of the corresponding WebContents. If longer term storage is
needed, a developer can store granted device permissions (eg when handling
the `select-usb-device` event) and then read from that storage with
`setDevicePermissionHandler`.
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
can be used to disable USB access for specific origins.

### Example

This example demonstrates an Electron application that automatically selects
USB devices (if they are attached) through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
and through [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device)
when the `Test WebUSB` button is clicked.

```javascript fiddle='docs/fiddles/features/web-usb'

```
1 change: 1 addition & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ auto_filenames = {
"docs/api/structures/upload-data.md",
"docs/api/structures/upload-file.md",
"docs/api/structures/upload-raw-data.md",
"docs/api/structures/usb-device.md",
"docs/api/structures/user-default-types.md",
"docs/api/structures/web-request-filter.md",
"docs/api/structures/web-source.md",
Expand Down
9 changes: 9 additions & 0 deletions filenames.gni
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,14 @@ filenames = {
"shell/browser/ui/tray_icon_observer.h",
"shell/browser/ui/webui/accessibility_ui.cc",
"shell/browser/ui/webui/accessibility_ui.h",
"shell/browser/usb/electron_usb_delegate.cc",
"shell/browser/usb/electron_usb_delegate.h",
"shell/browser/usb/usb_chooser_context.cc",
"shell/browser/usb/usb_chooser_context.h",
"shell/browser/usb/usb_chooser_context_factory.cc",
"shell/browser/usb/usb_chooser_context_factory.h",
"shell/browser/usb/usb_chooser_controller.cc",
"shell/browser/usb/usb_chooser_controller.h",
"shell/browser/web_contents_permission_helper.cc",
"shell/browser/web_contents_permission_helper.h",
"shell/browser/web_contents_preferences.cc",
Expand Down Expand Up @@ -586,6 +594,7 @@ filenames = {
"shell/common/gin_converters/std_converter.h",
"shell/common/gin_converters/time_converter.cc",
"shell/common/gin_converters/time_converter.h",
"shell/common/gin_converters/usb_device_info_converter.h",
"shell/common/gin_converters/value_converter.cc",
"shell/common/gin_converters/value_converter.h",
"shell/common/gin_helper/arguments.cc",
Expand Down
6 changes: 6 additions & 0 deletions shell/browser/electron_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,12 @@ content::BluetoothDelegate* ElectronBrowserClient::GetBluetoothDelegate() {
return bluetooth_delegate_.get();
}

content::UsbDelegate* ElectronBrowserClient::GetUsbDelegate() {
if (!usb_delegate_)
usb_delegate_ = std::make_unique<ElectronUsbDelegate>();
return usb_delegate_.get();
}

void BindBadgeServiceForServiceWorker(
const content::ServiceWorkerVersionBaseInfo& info,
mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
Expand Down
Loading

0 comments on commit 53f8915

Please sign in to comment.