Skip to content

Commit

Permalink
feat(rpc): plumb CDPSession (#2862)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Jul 8, 2020
1 parent 2a86ead commit 0c80c22
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 16 deletions.
7 changes: 6 additions & 1 deletion src/chromium/crConnection.ts
Expand Up @@ -122,6 +122,7 @@ export const CRSessionEvents = {

export class CRSession extends EventEmitter {
_connection: CRConnection | null;
_eventListener?: (method: string, params?: Object) => void;
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private readonly _targetType: string;
private readonly _sessionId: string;
Expand Down Expand Up @@ -182,7 +183,11 @@ export class CRSession extends EventEmitter {
callback.resolve(object.result);
} else {
assert(!object.id);
Promise.resolve().then(() => this.emit(object.method!, object.params));
Promise.resolve().then(() => {
if (this._eventListener)
this._eventListener(object.method!, object.params);
this.emit(object.method!, object.params);
});
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/rpc/channels.ts
Expand Up @@ -52,6 +52,9 @@ export interface BrowserChannel extends Channel {

close(): Promise<void>;
newContext(params: types.BrowserContextOptions): Promise<BrowserContextChannel>;

// Chromium-specific.
newBrowserCDPSession(): Promise<CDPSessionChannel>;
}
export type BrowserInitializer = {};

Expand Down Expand Up @@ -330,3 +333,14 @@ export type DownloadInitializer = {
url: string,
suggestedFilename: string,
};


// Chromium-specific.
export interface CDPSessionChannel extends Channel {
on(event: 'event', callback: (params: { method: string, params?: Object }) => void): this;
on(event: 'disconnected', callback: () => void): this;

send(params: { method: string, params?: Object }): Promise<Object>;
detach(): Promise<void>;
}
export type CDPSessionInitializer = {};
6 changes: 6 additions & 0 deletions src/rpc/client/browser.ts
Expand Up @@ -21,6 +21,7 @@ import { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { ConnectionScope } from './connection';
import { Events } from '../../events';
import { CDPSession } from './cdpSession';

export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
readonly _contexts = new Set<BrowserContext>();
Expand Down Expand Up @@ -77,4 +78,9 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
this._isClosedOrClosing = true;
await this._channel.close();
}

// Chromium-specific.
async newBrowserCDPSession(): Promise<CDPSession> {
return CDPSession.from(await this._channel.newBrowserCDPSession());
}
}
57 changes: 57 additions & 0 deletions src/rpc/client/cdpSession.ts
@@ -0,0 +1,57 @@
/**
* 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 { CDPSessionChannel, CDPSessionInitializer } from '../channels';
import { ConnectionScope } from './connection';
import { ChannelOwner } from './channelOwner';
import { Protocol } from '../../chromium/protocol';

export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitializer> {
static from(cdpSession: CDPSessionChannel): CDPSession {
return (cdpSession as any)._object;
}

on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;

constructor(scope: ConnectionScope, guid: string, initializer: CDPSessionInitializer) {
super(scope, guid, initializer, true);

this._channel.on('event', ({ method, params }) => this.emit(method, params));
this._channel.on('disconnected', () => this._scope.dispose());

this.on = super.on;
this.addListener = super.addListener;
this.off = super.removeListener;
this.removeListener = super.removeListener;
this.once = super.once;
}

async send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
const result = await this._channel.send({ method, params });
return result as Protocol.CommandReturnValues[T];
}

async detach() {
return this._channel.detach();
}
}
5 changes: 5 additions & 0 deletions src/rpc/client/connection.ts
Expand Up @@ -30,6 +30,7 @@ import { Dialog } from './dialog';
import { Download } from './download';
import { parseError } from '../serializers';
import { BrowserServer } from './browserServer';
import { CDPSession } from './cdpSession';

export class Connection {
readonly _objects = new Map<string, ChannelOwner<any, any>>();
Expand Down Expand Up @@ -192,6 +193,10 @@ export class ConnectionScope {
case 'browserType':
result = new BrowserType(this, guid, initializer);
break;
case 'cdpSession':
// Chromium-specific.
result = new CDPSession(this, guid, initializer);
break;
case 'context':
result = new BrowserContext(this, guid, initializer);
break;
Expand Down
10 changes: 9 additions & 1 deletion src/rpc/server/browserDispatcher.ts
Expand Up @@ -18,9 +18,11 @@ import { Browser, BrowserBase } from '../../browser';
import { BrowserContextBase } from '../../browserContext';
import { Events } from '../../events';
import * as types from '../../types';
import { BrowserChannel, BrowserContextChannel, BrowserInitializer } from '../channels';
import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel } from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { CRBrowser } from '../../chromium/crBrowser';

export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
constructor(scope: DispatcherScope, browser: BrowserBase) {
Expand All @@ -38,4 +40,10 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
async close(): Promise<void> {
await this._object.close();
}

// Chromium-specific.
async newBrowserCDPSession(): Promise<CDPSessionChannel> {
const crBrowser = this._object as CRBrowser;
return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession());
}
}
38 changes: 38 additions & 0 deletions src/rpc/server/cdpSessionDispatcher.ts
@@ -0,0 +1,38 @@
/**
* 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 { CRSession, CRSessionEvents } from '../../chromium/crConnection';
import { CDPSessionChannel, CDPSessionInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from './dispatcher';

export class CDPSessionDispatcher extends Dispatcher<CRSession, CDPSessionInitializer> implements CDPSessionChannel {
constructor(scope: DispatcherScope, crSession: CRSession) {
super(scope, crSession, 'cdpSession', {}, true);
crSession._eventListener = (method, params) => this._dispatchEvent('event', { method, params });
crSession.on(CRSessionEvents.Disconnected, () => {
this._dispatchEvent('disconnected');
this._scope.dispose();
});
}

async send(params: { method: string, params?: Object }): Promise<Object> {
return this._object.send(params.method as any, params.params);
}

async detach(): Promise<void> {
return this._object.detach();
}
}
32 changes: 27 additions & 5 deletions test/channels.spec.js
Expand Up @@ -15,15 +15,13 @@
* limitations under the License.
*/

const path = require('path');
const util = require('util');
const vm = require('vm');
const { FFOX, CHROMIUM, WEBKIT, WIN, CHANNEL } = require('./utils').testOptions(browserType);

describe.skip(!CHANNEL)('Channels', function() {
it('should work', async({browser}) => {
expect(!!browser._channel).toBeTruthy();
});

it('should scope context handles', async({browser, server}) => {
const GOLDEN_PRECONDITION = {
objects: [ 'chromium', 'browser' ],
Expand All @@ -50,7 +48,31 @@ describe.skip(!CHANNEL)('Channels', function() {
await expectScopeState(browser, GOLDEN_PRECONDITION);
});

it('should browser handles', async({browserType, defaultBrowserOptions}) => {
it('should scope CDPSession handles', async({browserType, browser, server}) => {
const GOLDEN_PRECONDITION = {
objects: [ 'chromium', 'browser' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser' ] },
{ _guid: 'browser', objects: [] }
]
};
await expectScopeState(browserType, GOLDEN_PRECONDITION);

const session = await browser.newBrowserCDPSession();
await expectScopeState(browserType, {
objects: [ 'chromium', 'browser', 'cdpSession' ],
scopes: [
{ _guid: '', objects: [ 'chromium', 'browser' ] },
{ _guid: 'browser', objects: ['cdpSession'] },
{ _guid: 'cdpSession', objects: [] },
]
});

await session.detach();
await expectScopeState(browserType, GOLDEN_PRECONDITION);
});

it('should scope browser handles', async({browserType, defaultBrowserOptions}) => {
const GOLDEN_PRECONDITION = {
objects: [ 'chromium', 'browser' ],
scopes: [
Expand Down Expand Up @@ -96,4 +118,4 @@ function trimGuids(object) {
if (typeof object === 'string')
return object ? object.match(/[^@]+/)[0] : '';
return object;
}
}
9 changes: 8 additions & 1 deletion test/chromium/session.spec.js
Expand Up @@ -95,11 +95,18 @@ describe.skip(CHANNEL)('ChromiumBrowserContext.createSession', function() {
await context.close();
});
});
describe.skip(CHANNEL)('ChromiumBrowser.newBrowserCDPSession', function() {
describe('ChromiumBrowser.newBrowserCDPSession', function() {
it('should work', async function({page, browser, server}) {
const session = await browser.newBrowserCDPSession();

const version = await session.send('Browser.getVersion');
expect(version.userAgent).toBeTruthy();

let gotEvent = false;
session.on('Target.targetCreated', () => gotEvent = true);
await session.send('Target.setDiscoverTargets', { discover: true });
expect(gotEvent).toBe(true);

await session.detach();
});
});
13 changes: 8 additions & 5 deletions utils/doclint/Source.js
Expand Up @@ -25,12 +25,14 @@ const writeFileAsync = util.promisify(fs.writeFile);

const PROJECT_DIR = path.join(__dirname, '..', '..');

async function recursiveReadDir(dirPath) {
async function recursiveReadDir(dirPath, exclude) {
const files = [];
if (exclude.includes(dirPath))
return files;
for (const file of await readdirAsync(dirPath)) {
const fullPath = path.join(dirPath, file);
if ((await statAsync(fullPath)).isDirectory())
files.push(...await recursiveReadDir(fullPath))
files.push(...await recursiveReadDir(fullPath, exclude))
else
files.push(fullPath);
}
Expand Down Expand Up @@ -100,7 +102,7 @@ class Source {
async save() {
await writeFileAsync(this.filePath(), this.text());
}

async saveAs(path) {
await writeFileAsync(path, this.text());
}
Expand All @@ -118,11 +120,12 @@ class Source {
/**
* @param {string} dirPath
* @param {string=} extension
* @param {Array<string>=} exclude
* @return {!Promise<!Array<!Source>>}
*/
static async readdir(dirPath, extension = '') {
static async readdir(dirPath, extension = '', exclude = []) {
extension = extension.toLowerCase();
const filePaths = (await recursiveReadDir(dirPath)).filter(fileName => fileName.toLowerCase().endsWith(extension));
const filePaths = (await recursiveReadDir(dirPath, exclude)).filter(fileName => fileName.toLowerCase().endsWith(extension));
return Promise.all(filePaths.map(filePath => Source.readFile(filePath)));
}
}
Expand Down
3 changes: 2 additions & 1 deletion utils/doclint/cli.js
Expand Up @@ -62,7 +62,8 @@ async function run() {
const browser = await playwright.chromium.launch();
const page = await browser.newPage();
const checkPublicAPI = require('./check_public_api');
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
const rpcDir = path.join(PROJECT_DIR, 'src', 'rpc');
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src'), '', [rpcDir]);
messages.push(...await checkPublicAPI(page, [api], jsSources));
await browser.close();

Expand Down
5 changes: 3 additions & 2 deletions utils/generate_types/index.js
Expand Up @@ -37,7 +37,8 @@ let documentation;
const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));
const {documentation: mdDocumentation} = await require('../doclint/check_public_api/MDBuilder')(page, [api]);
await browser.close();
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
const rpcDir = path.join(PROJECT_DIR, 'src', 'rpc');
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src'), '', [rpcDir]);
const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources);
documentation = mergeDocumentation(mdDocumentation, jsDocumentation);
const handledClasses = new Set();
Expand Down Expand Up @@ -408,7 +409,7 @@ function mergeClasses(mdClass, jsClass) {
}

function generateDevicesTypes() {
const namedDevices =
const namedDevices =
Object.keys(devices)
.map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`)
.join('\n');
Expand Down

0 comments on commit 0c80c22

Please sign in to comment.