feat: add support for WebHID (#30213)
* feat: add support for WebHID * Apply suggestions from code review Co-authored-by: Jeremy Rose <jeremya@chromium.org> * Address review feedback * Address review feedback * chore: clear granted_devices on navigation Also added test to verify devices get cleared * fixup testing for device clear * make sure navigator.hid.getDevices is run on correct frame * clear granted devices on RenderFrameHost deletion/change * manage device permissions per RenderFrameHost This change makes sure we don't clear device permission prematurely due to child frame navigation * Update shell/browser/api/electron_api_web_contents.cc Co-authored-by: Jeremy Rose <jeremya@chromium.org> * apply review feedback from @zcbenz * Match upstream ObjectMap This change matches what ObjectPermissionContextBase uses to cache object permissions: https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/object_permission_context_base.h;l=52;drc=8f95b5eab2797a3e26bba299f3b0df85bfc98bf5;bpv=1;bpt=0 The main reason for this was to resolve this crash on Win x64: ok 2 WebContentsView doesn't crash when GCed during allocation Received fatal exception EXCEPTION_ACCESS_VIOLATION Backtrace: gin::WrappableBase::SecondWeakCallback [0x00007FF6F2AFA005+133] (o:\gin\wrappable.cc:53) v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacks [0x00007FF6F028F9AB+171] (o:\v8\src\handles\global-handles.cc:1400) v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacksFromTask [0x00007FF6F028F867+391] (o:\v8\src\handles\global-handles.cc:1387) node::PerIsolatePlatformData::RunForegroundTask [0x00007FF6F3B4D065+317] (o:\third_party\electron_node\src\node_platform.cc:415) node::PerIsolatePlatformData::FlushForegroundTasksInternal [0x00007FF6F3B4C424+776] (o:\third_party\electron_node\src\node_platform.cc:479) uv_run [0x00007FF6F2DDD07C+492] (o:\third_party\electron_node\deps\uv\src\win\core.c:609) electron::NodeBindings::UvRunOnce [0x00007FF6EEE1E036+294] (o:\electron\shell\common\node_bindings.cc:631) base::TaskAnnotator::RunTask [0x00007FF6F2318A19+457] (o:\base\task\common\task_annotator.cc:178) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl [0x00007FF6F2E6F553+963] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:361) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork [0x00007FF6F2E6EC69+137] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:266) base::MessagePumpForUI::DoRunLoop [0x00007FF6F235AA58+216] (o:\base\message_loop\message_pump_win.cc:221) base::MessagePumpWin::Run [0x00007FF6F235A01A+106] (o:\base\message_loop\message_pump_win.cc:79) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run [0x00007FF6F2E702DA+682] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:470) base::RunLoop::Run [0x00007FF6F22F95BA+842] (o:\base\run_loop.cc:136) content::BrowserMainLoop::RunMainMessageLoop [0x00007FF6F14423CC+208] (o:\content\browser\browser_main_loop.cc:990) content::BrowserMainRunnerImpl::Run [0x00007FF6F144402F+143] (o:\content\browser\browser_main_runner_impl.cc:153) content::BrowserMain [0x00007FF6F143F911+257] (o:\content\browser\browser_main.cc:49) content::RunBrowserProcessMain [0x00007FF6EFFA7D18+112] (o:\content\app\content_main_runner_impl.cc:608) content::ContentMainRunnerImpl::RunBrowser [0x00007FF6EFFA8CF4+1220] (o:\content\app\content_main_runner_impl.cc:1104) content::ContentMainRunnerImpl::Run [0x00007FF6EFFA87C9+393] (o:\content\app\content_main_runner_impl.cc:971) content::RunContentProcess [0x00007FF6EFFA73BD+733] (o:\content\app\content_main.cc:394) content::ContentMain [0x00007FF6EFFA79E1+54] (o:\content\app\content_main.cc:422) wWinMain [0x00007FF6EECA1535+889] (o:\electron\shell\app\electron_main.cc:291) __scrt_common_main_seh [0x00007FF6F6F88482+262] (d:\A01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288) BaseThreadInitThunk [0x00007FFEC0087034+20] RtlUserThreadStart [0x00007FFEC1F02651+33] ✗ Electron tests failed with code 0xc0000005. Co-authored-by: Jeremy Rose <jeremya@chromium.org>
This commit is contained in:
parent
77579614e0
commit
6aece4a83d
36 changed files with 2142 additions and 4 deletions
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai';
|
||||
import { BrowserWindow, WebContents, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { emittedOnce } from './events-helpers';
|
||||
import { closeAllWindows } from './window-helpers';
|
||||
import * as https from 'https';
|
||||
|
@ -1878,3 +1878,127 @@ describe('navigator.bluetooth', () => {
|
|||
expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigator.hid', () => {
|
||||
let w: BrowserWindow;
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
before(async () => {
|
||||
w = new BrowserWindow({
|
||||
show: false
|
||||
});
|
||||
await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<body>');
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
serverUrl = `http://localhost:${(server.address() as any).port}`;
|
||||
});
|
||||
|
||||
const getDevices: any = () => {
|
||||
return w.webContents.executeJavaScript(`
|
||||
navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
|
||||
`, true);
|
||||
};
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
closeAllWindows();
|
||||
});
|
||||
afterEach(() => {
|
||||
session.defaultSession.setPermissionCheckHandler(null);
|
||||
session.defaultSession.setDevicePermissionHandler(null);
|
||||
session.defaultSession.removeAllListeners('select-hid-device');
|
||||
});
|
||||
|
||||
it('does not return a device if select-hid-device event is not defined', async () => {
|
||||
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
const device = await getDevices();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('does not return a device when permission denied', async () => {
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setPermissionCheckHandler(() => false);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.false();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('returns a device when select-hid-device event is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
haveDevices = true;
|
||||
callback(details.deviceList[0].deviceId);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
if (process.arch === 'arm64' || process.arch === 'arm') {
|
||||
// arm CI returns HID devices - this block may need to change if CI hardware changes.
|
||||
expect(haveDevices).to.be.true();
|
||||
// Verify that navigation will clear device permissions
|
||||
const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevices).to.not.be.empty();
|
||||
w.loadURL(serverUrl);
|
||||
const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
|
||||
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
|
||||
expect(frame).to.not.be.empty();
|
||||
if (frame) {
|
||||
const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevicesOnNewPage).to.be.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a device when DevicePermissionHandler is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
let gotDevicePerms = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
const foundDevice = details.deviceList.find((device) => {
|
||||
if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
|
||||
haveDevices = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (foundDevice) {
|
||||
callback(foundDevice.deviceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setDevicePermissionHandler(() => {
|
||||
gotDevicePerms = true;
|
||||
return true;
|
||||
});
|
||||
await w.webContents.executeJavaScript('navigator.hid.getDevices();', true);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
expect(gotDevicePerms).to.be.true();
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue