Skip to content

Commit

Permalink
feat: implement bluetooth event
Browse files Browse the repository at this point in the history
  • Loading branch information
OrKoN committed Aug 13, 2024
1 parent f827567 commit 72fbfc9
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 2 deletions.
15 changes: 15 additions & 0 deletions src/bidiMapper/CommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {Result} from '../utils/result.js';
import {BidiNoOpParser} from './BidiNoOpParser.js';
import type {BidiCommandParameterParser} from './BidiParser.js';
import type {MapperOptions} from './BidiServer.js';
import {BluetoothProcessor} from './modules/bluetooth/BluetoothProcessor';
import {BrowserProcessor} from './modules/browser/BrowserProcessor.js';
import {CdpProcessor} from './modules/cdp/CdpProcessor.js';
import {BrowsingContextProcessor} from './modules/context/BrowsingContextProcessor.js';
Expand Down Expand Up @@ -60,6 +61,7 @@ type CommandProcessorEventsMap = {

export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
// keep-sorted start
#bluetoothProcessor: BluetoothProcessor;
#browserProcessor: BrowserProcessor;
#browsingContextProcessor: BrowsingContextProcessor;
#cdpProcessor: CdpProcessor;
Expand Down Expand Up @@ -91,10 +93,15 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
this.#logger = logger;

// keep-sorted start block=yes
this.#bluetoothProcessor = new BluetoothProcessor(
eventManager,
browsingContextStorage
);
this.#browserProcessor = new BrowserProcessor(browserCdpClient);
this.#browsingContextProcessor = new BrowsingContextProcessor(
browserCdpClient,
browsingContextStorage,
this.#bluetoothProcessor,
eventManager
);
this.#cdpProcessor = new CdpProcessor(
Expand Down Expand Up @@ -137,6 +144,14 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
// TODO: Implement.
break;

// Bluetooth domain
// keep-sorted start block=yes
case 'bluetooth.handleRequestDevicePrompt':
return await this.#bluetoothProcessor.handleRequestDevicePrompt(
command.params
);
// keep-sorted end

// Browser domain
// keep-sorted start block=yes
case 'browser.close':
Expand Down
81 changes: 81 additions & 0 deletions src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright 2024 Google LLC.
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
NoSuchFrameException,
type Bluetooth,
type EmptyResult,
} from '../../../protocol/protocol.js';
import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js';
import type {CdpTarget} from '../context/CdpTarget.js';
import type {EventManager} from '../session/EventManager.js';

export class BluetoothProcessor {
#eventManager: EventManager;
#browsingContextStorage: BrowsingContextStorage;

constructor(
eventManager: EventManager,
browsingContextStorage: BrowsingContextStorage
) {
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
}

onCdpTargetCreated(cdpTarget: CdpTarget) {
cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => {
this.#eventManager.registerEvent(
{
type: 'event',
method: 'bluetooth.requestDevicePromptOpened',
params: {
context: cdpTarget.id,
prompt: event.id,
devices: event.devices,
},
},
cdpTarget.id
);
});
}

async handleRequestDevicePrompt(
params: Bluetooth.HandleRequestDevicePromptParameters
): Promise<EmptyResult> {
const context = this.#browsingContextStorage.findContext(params.context);
if (!context) {
throw new NoSuchFrameException('context not found');
}
if (params.accept) {
await context.cdpTarget.cdpClient.sendCommand(
'DeviceAccess.selectPrompt',
{
id: params.prompt,
deviceId: params.device,
}
);
} else {
await context.cdpTarget.cdpClient.sendCommand(
'DeviceAccess.cancelPrompt',
{
id: params.prompt,
}
);
}
return {};
}
}
9 changes: 9 additions & 0 deletions src/bidiMapper/modules/cdp/CdpTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export class CdpTarget {
}),
this.#initAndEvaluatePreloadScripts(),
this.#cdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
this.toggleDeviceAccessIfNeeded(),
]);
} catch (error: any) {
this.#logger?.(LogType.debugError, 'Failed to unblock target', error);
Expand Down Expand Up @@ -289,6 +290,14 @@ export class CdpTarget {
}
}

async toggleDeviceAccessIfNeeded(): Promise<void> {
const enabled = this.isSubscribedTo(BiDiModule.Bluetooth);
await this.#cdpClient.sendCommand(
enabled ? 'DeviceAccess.enable' : 'DeviceAccess.disable'
);
return;
}

#setEventListeners() {
this.#cdpClient.on('*', (event, params) => {
// We may encounter uses for EventEmitter other than CDP events,
Expand Down
1 change: 1 addition & 0 deletions src/bidiMapper/modules/context/BrowsingContextImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,7 @@ export class BrowsingContextImpl {

async toggleModulesIfNeeded(): Promise<void> {
await this.#cdpTarget.toggleNetworkIfNeeded();
await this.#cdpTarget.toggleDeviceAccessIfNeeded();
}

async locateNodes(
Expand Down
159 changes: 159 additions & 0 deletions src/bidiMapper/modules/context/BrowsingContextProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,37 @@ import {
NoSuchAlertException,
} from '../../../protocol/protocol.js';
import {CdpErrorConstants} from '../../../utils/CdpErrorConstants.js';
<<<<<<< HEAD
=======
import {LogType, type LoggerFn} from '../../../utils/log.js';
import type {BluetoothProcessor} from '../bluetooth/BluetoothProcessor.js';
import type {NetworkStorage} from '../network/NetworkStorage.js';
import type {PreloadScriptStorage} from '../script/PreloadScriptStorage.js';
import type {Realm} from '../script/Realm.js';
import type {RealmStorage} from '../script/RealmStorage.js';
import {WorkerRealm, type WorkerRealmType} from '../script/WorkerRealm.js';
>>>>>>> 91be1069 (feat: implement bluetooth event)
import type {EventManager} from '../session/EventManager.js';

import type {BrowsingContextImpl} from './BrowsingContextImpl.js';
import type {BrowsingContextStorage} from './BrowsingContextStorage.js';
import { BluetoothProcessor } from '../bluetooth/BluetoothProcessor.js';

export class BrowsingContextProcessor {
readonly #browserCdpClient: CdpClient;
readonly #browsingContextStorage: BrowsingContextStorage;
readonly #eventManager: EventManager;
readonly #bluetoothProcessor: BluetoothProcessor;

constructor(
browserCdpClient: CdpClient,
browsingContextStorage: BrowsingContextStorage,
bluetoothProcessor: BluetoothProcessor,
eventManager: EventManager
) {
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#bluetoothProcessor = bluetoothProcessor;
this.#eventManager = eventManager;
this.#eventManager.addSubscribeHook(
ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
Expand Down Expand Up @@ -313,6 +327,151 @@ export class BrowsingContextProcessor {
},
context.id
);
<<<<<<< HEAD
=======
}
}

#handleFrameDetachedEvent(params: Protocol.Page.FrameDetachedEvent) {
// In case of OOPiF no need in deleting BrowsingContext.
if (params.reason === 'swap') {
return;
}
this.#browsingContextStorage.findContext(params.frameId)?.dispose();
}

#handleAttachedToTargetEvent(
params: Protocol.Target.AttachedToTargetEvent,
parentSessionCdpClient: CdpClient
) {
const {sessionId, targetInfo} = params;
const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);

this.#logger?.(
LogType.debugInfo,
'AttachedToTarget event received:',
params
);

switch (targetInfo.type) {
case 'page':
case 'iframe': {
if (targetInfo.targetId === this.#selfTargetId) {
break;
}

const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
const maybeContext = this.#browsingContextStorage.findContext(
targetInfo.targetId
);
if (maybeContext) {
// OOPiF.
maybeContext.updateCdpTarget(cdpTarget);
} else {
// New context.
BrowsingContextImpl.create(
cdpTarget,
this.#realmStorage,
targetInfo.targetId,
null,
targetInfo.browserContextId &&
targetInfo.browserContextId !== this.#defaultUserContextId
? targetInfo.browserContextId
: 'default',
this.#eventManager,
this.#browsingContextStorage,
this.#logger
);
}
return;
}
case 'service_worker':
case 'worker': {
const realm = this.#realmStorage.findRealm({
cdpSessionId: parentSessionCdpClient.sessionId,
});
// If there is no browsing context, this worker is already terminated.
if (!realm) {
break;
}

const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
this.#handleWorkerTarget(
cdpToBidiTargetTypes[targetInfo.type],
cdpTarget,
realm
);
return;
}
// In CDP, we only emit shared workers on the browser and not the set of
// frames that use the shared worker. If we change this in the future to
// behave like service workers (emits on both browser and frame targets),
// we can remove this block and merge service workers with the above one.
case 'shared_worker': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, targetInfo);
this.#handleWorkerTarget(
cdpToBidiTargetTypes[targetInfo.type],
cdpTarget
);
return;
}
}

// DevTools or some other not supported by BiDi target. Just release
// debugger and ignore them.
targetCdpClient
.sendCommand('Runtime.runIfWaitingForDebugger')
.then(() =>
parentSessionCdpClient.sendCommand('Target.detachFromTarget', params)
)
.catch((error) => this.#logger?.(LogType.debugError, error));
}

#createCdpTarget(
targetCdpClient: CdpClient,
targetInfo: Protocol.Target.TargetInfo
) {
this.#setEventListeners(targetCdpClient);

const target = CdpTarget.create(
targetInfo.targetId,
targetCdpClient,
this.#browserCdpClient,
this.#realmStorage,
this.#eventManager,
this.#preloadScriptStorage,
this.#networkStorage,
this.#acceptInsecureCerts,
this.#logger
);

this.#networkStorage.onCdpTargetCreated(target);
this.#bluetoothProcessor.onCdpTargetCreated(target);

return target;
}

#workers = new Map<string, Realm>();
#handleWorkerTarget(
realmType: WorkerRealmType,
cdpTarget: CdpTarget,
ownerRealm?: Realm
) {
cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
const {uniqueId, id, origin} = params.context;
const workerRealm = new WorkerRealm(
cdpTarget.cdpClient,
this.#eventManager,
id,
this.#logger,
serializeOrigin(origin),
ownerRealm ? [ownerRealm] : [],
uniqueId,
this.#realmStorage,
realmType
);
this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
>>>>>>> 91be1069 (feat: implement bluetooth event)
});
return Promise.resolve();
}
Expand Down
4 changes: 4 additions & 0 deletions src/bidiMapper/modules/session/SubscriptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ export class SubscriptionManager {
this.subscribe(specificEvent, contextId, channel)
)
.flat();
case ChromiumBidi.BiDiModule.Bluetooth:
return Object.values(ChromiumBidi.Bluetooth.EventNames).map((specificEvent) =>
this.subscribe(specificEvent, contextId, channel)
).flat();
default:
// Intentionally left empty.
}
Expand Down
Loading

0 comments on commit 72fbfc9

Please sign in to comment.