Skip to content

Commit

Permalink
chore: expose driver protocol on the pw object (#17363)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Sep 15, 2022
1 parent b09ea69 commit 872bcc9
Show file tree
Hide file tree
Showing 11 changed files with 563 additions and 131 deletions.
142 changes: 22 additions & 120 deletions packages/playwright-core/src/cli/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ import * as playwright from '../..';
import type { BrowserType } from '../client/browserType';
import type { LaunchServerOptions } from '../client/types';
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
import type { Playwright } from '../server';
import type { Playwright } from '../server/playwright';
import { IpcTransport, PipeTransport } from '../protocol/transport';
import { PlaywrightServer } from '../remote/playwrightServer';
import { gracefullyCloseAll } from '../utils/processLauncher';
import { Recorder } from '../server/recorder';
import { EmptyRecorderApp } from '../server/recorder/recorderApp';
import type { BrowserContext } from '../server/browserContext';
import { serverSideCallMetadata } from '../server/instrumentation';
import type { Mode } from '../server/recorder/recorderTypes';
import { ReuseController } from '../server/reuseController';

export function printApiJson() {
// Note: this file is generated by build-playwright-driver.sh
Expand Down Expand Up @@ -60,8 +57,8 @@ export async function runServer(port: number | undefined, path = '/', maxClients
process.on('exit', () => server.close().catch(console.error));
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
process.stdin.on('close', () => selfDestruct());
if (process.send && server.preLaunchedPlaywright())
wireController(server.preLaunchedPlaywright()!, wsEndpoint);
if (reuseBrowser && process.send)
wireController(server.preLaunchedPlaywright(), wsEndpoint);
}

export async function launchBrowserServer(browserName: string, configFile?: string) {
Expand All @@ -82,118 +79,48 @@ function selfDestruct() {
});
}

const internalMetadata = serverSideCallMetadata();

class ProtocolHandler {
private _playwright: Playwright;
private _autoCloseTimer: NodeJS.Timeout | undefined;
private _controller: ReuseController;

constructor(playwright: Playwright) {
this._playwright = playwright;
playwright.instrumentation.addListener({
onPageOpen: () => this._sendSnapshot(),
onPageNavigated: () => this._sendSnapshot(),
onPageClose: () => this._sendSnapshot(),
}, null);
}

private _sendSnapshot() {
const browsers = [];
for (const browser of this._playwright.allBrowsers()) {
const b = {
name: browser.options.name,
guid: browser.guid,
contexts: [] as any[]
};
browsers.push(b);
for (const context of browser.contexts()) {
const c = {
guid: context.guid,
pages: [] as any[]
};
b.contexts.push(c);
for (const page of context.pages()) {
const p = {
guid: page.guid,
url: page.mainFrame().url()
};
c.pages.push(p);
}
}
}
process.send!({ method: 'browsersChanged', params: { browsers } });
this._controller = playwright.reuseController;
this._controller.setAutoCloseAllowed(true);
this._controller.setTrackHierarcy(true);
this._controller.setReuseBrowser(true);
this._controller.on(ReuseController.Events.BrowsersChanged, browsers => {
process.send!({ method: 'browsersChanged', params: { browsers } });
});
this._controller.on(ReuseController.Events.InspectRequested, selector => {
process.send!({ method: 'inspectRequested', params: { selector } });
});
}

async resetForReuse() {
const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages())
contexts.add(page.context());
for (const context of contexts)
await context.resetForReuse(internalMetadata, null);
await this._controller.resetForReuse();
}

async navigate(params: { url: string }) {
for (const p of this._playwright.allPages())
await p.mainFrame().goto(internalMetadata, params.url);
await this._controller.navigateAll(params.url);
}

async setMode(params: { mode: Mode, language?: string, file?: string }) {
await gc(this._playwright);

if (params.mode === 'none') {
for (const recorder of await allRecorders(this._playwright)) {
recorder.setHighlightedSelector('');
recorder.setMode('none');
}
this.setAutoClose({ enabled: true });
return;
}

const browsers = this._playwright.allBrowsers();
if (!browsers.length)
await this._playwright.chromium.launch(internalMetadata, { headless: false });
// Create page if none.
const pages = this._playwright.allPages();
if (!pages.length) {
const [browser] = this._playwright.allBrowsers();
const { context } = await browser.newContextForReuse({}, internalMetadata);
await context.newPage(internalMetadata);
}
// Toggle the mode.
for (const recorder of await allRecorders(this._playwright)) {
recorder.setHighlightedSelector('');
if (params.mode === 'recording')
recorder.setOutput(params.language!, params.file);
recorder.setMode(params.mode);
}
this.setAutoClose({ enabled: true });
await this._controller.setRecorderMode(params);
}

async setAutoClose(params: { enabled: boolean }) {
if (this._autoCloseTimer)
clearTimeout(this._autoCloseTimer);
if (!params.enabled)
return;
const heartBeat = () => {
if (!this._playwright.allPages().length)
selfDestruct();
else
this._autoCloseTimer = setTimeout(heartBeat, 5000);
};
this._autoCloseTimer = setTimeout(heartBeat, 30000);
await this._controller.setAutoCloseEnabled(params.enabled);
}

async highlight(params: { selector: string }) {
for (const recorder of await allRecorders(this._playwright))
recorder.setHighlightedSelector(params.selector);
await this._controller.highlightAll(params.selector);
}

async hideHighlight() {
await this._playwright.hideHighlight();
await this._controller.hideHighlightAll();
}

async kill() {
selfDestruct();
await this._controller.kill();
}
}

Expand All @@ -209,28 +136,3 @@ function wireController(playwright: Playwright, wsEndpoint: string) {
}
});
}

async function gc(playwright: Playwright) {
for (const browser of playwright.allBrowsers()) {
for (const context of browser.contexts()) {
if (!context.pages().length)
await context.close(serverSideCallMetadata());
}
if (!browser.contexts())
await browser.close();
}
}

async function allRecorders(playwright: Playwright): Promise<Recorder[]> {
const contexts = new Set<BrowserContext>();
for (const page of playwright.allPages())
contexts.add(page.context());
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp()))));
return result.filter(Boolean) as Recorder[];
}

class InspectingRecorderApp extends EmptyRecorderApp {
override async setSelector(selector: string): Promise<void> {
process.send!({ method: 'inspectRequested', params: { selector } });
}
}
2 changes: 1 addition & 1 deletion packages/playwright-core/src/grid/gridBrowserWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
const log = debug(`pw:grid:worker:${workerId}`);
log('created');
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
new PlaywrightConnection(Promise.resolve(), 'auto', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
new PlaywrightConnection(Promise.resolve(), 'auto', ws, false, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
log('exiting process');
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
Expand Down
91 changes: 91 additions & 0 deletions packages/playwright-core/src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type InitializerTraits<T> =
T extends BrowserTypeChannel ? BrowserTypeInitializer :
T extends SelectorsChannel ? SelectorsInitializer :
T extends SocksSupportChannel ? SocksSupportInitializer :
T extends ReuseControllerChannel ? ReuseControllerInitializer :
T extends PlaywrightChannel ? PlaywrightInitializer :
T extends RootChannel ? RootInitializer :
T extends LocalUtilsChannel ? LocalUtilsInitializer :
Expand Down Expand Up @@ -89,6 +90,7 @@ export type EventsTraits<T> =
T extends BrowserTypeChannel ? BrowserTypeEvents :
T extends SelectorsChannel ? SelectorsEvents :
T extends SocksSupportChannel ? SocksSupportEvents :
T extends ReuseControllerChannel ? ReuseControllerEvents :
T extends PlaywrightChannel ? PlaywrightEvents :
T extends RootChannel ? RootEvents :
T extends LocalUtilsChannel ? LocalUtilsEvents :
Expand Down Expand Up @@ -126,6 +128,7 @@ export type EventTargetTraits<T> =
T extends BrowserTypeChannel ? BrowserTypeEventTarget :
T extends SelectorsChannel ? SelectorsEventTarget :
T extends SocksSupportChannel ? SocksSupportEventTarget :
T extends ReuseControllerChannel ? ReuseControllerEventTarget :
T extends PlaywrightChannel ? PlaywrightEventTarget :
T extends RootChannel ? RootEventTarget :
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
Expand Down Expand Up @@ -555,6 +558,94 @@ export type PlaywrightHideHighlightResult = void;
export interface PlaywrightEvents {
}

// ----------- ReuseController -----------
export type ReuseControllerInitializer = {};
export interface ReuseControllerEventTarget {
on(event: 'inspectRequested', callback: (params: ReuseControllerInspectRequestedEvent) => void): this;
on(event: 'browsersChanged', callback: (params: ReuseControllerBrowsersChangedEvent) => void): this;
}
export interface ReuseControllerChannel extends ReuseControllerEventTarget, Channel {
_type_ReuseController: boolean;
setTrackHierarchy(params: ReuseControllerSetTrackHierarchyParams, metadata?: Metadata): Promise<ReuseControllerSetTrackHierarchyResult>;
setReuseBrowser(params: ReuseControllerSetReuseBrowserParams, metadata?: Metadata): Promise<ReuseControllerSetReuseBrowserResult>;
resetForReuse(params?: ReuseControllerResetForReuseParams, metadata?: Metadata): Promise<ReuseControllerResetForReuseResult>;
navigateAll(params: ReuseControllerNavigateAllParams, metadata?: Metadata): Promise<ReuseControllerNavigateAllResult>;
setRecorderMode(params: ReuseControllerSetRecorderModeParams, metadata?: Metadata): Promise<ReuseControllerSetRecorderModeResult>;
setAutoClose(params: ReuseControllerSetAutoCloseParams, metadata?: Metadata): Promise<ReuseControllerSetAutoCloseResult>;
highlightAll(params: ReuseControllerHighlightAllParams, metadata?: Metadata): Promise<ReuseControllerHighlightAllResult>;
hideHighlightAll(params?: ReuseControllerHideHighlightAllParams, metadata?: Metadata): Promise<ReuseControllerHideHighlightAllResult>;
kill(params?: ReuseControllerKillParams, metadata?: Metadata): Promise<ReuseControllerKillResult>;
}
export type ReuseControllerInspectRequestedEvent = {
selector: string,
};
export type ReuseControllerBrowsersChangedEvent = {
browsers: {
contexts: {
pages: string[],
}[],
}[],
};
export type ReuseControllerSetTrackHierarchyParams = {
enabled: boolean,
};
export type ReuseControllerSetTrackHierarchyOptions = {

};
export type ReuseControllerSetTrackHierarchyResult = void;
export type ReuseControllerSetReuseBrowserParams = {
enabled: boolean,
};
export type ReuseControllerSetReuseBrowserOptions = {

};
export type ReuseControllerSetReuseBrowserResult = void;
export type ReuseControllerResetForReuseParams = {};
export type ReuseControllerResetForReuseOptions = {};
export type ReuseControllerResetForReuseResult = void;
export type ReuseControllerNavigateAllParams = {
url: string,
};
export type ReuseControllerNavigateAllOptions = {

};
export type ReuseControllerNavigateAllResult = void;
export type ReuseControllerSetRecorderModeParams = {
mode: 'inspecting' | 'recording' | 'none',
language?: string,
file?: string,
};
export type ReuseControllerSetRecorderModeOptions = {
language?: string,
file?: string,
};
export type ReuseControllerSetRecorderModeResult = void;
export type ReuseControllerSetAutoCloseParams = {
enabled: boolean,
};
export type ReuseControllerSetAutoCloseOptions = {

};
export type ReuseControllerSetAutoCloseResult = void;
export type ReuseControllerHighlightAllParams = {
selector: string,
};
export type ReuseControllerHighlightAllOptions = {

};
export type ReuseControllerHighlightAllResult = void;
export type ReuseControllerHideHighlightAllParams = {};
export type ReuseControllerHideHighlightAllOptions = {};
export type ReuseControllerHideHighlightAllResult = void;
export type ReuseControllerKillParams = {};
export type ReuseControllerKillOptions = {};
export type ReuseControllerKillResult = void;

export interface ReuseControllerEvents {
'inspectRequested': ReuseControllerInspectRequestedEvent;
'browsersChanged': ReuseControllerBrowsersChangedEvent;
}

// ----------- SocksSupport -----------
export type SocksSupportInitializer = {};
export interface SocksSupportEventTarget {
Expand Down
62 changes: 62 additions & 0 deletions packages/playwright-core/src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,68 @@ Playwright:

hideHighlight:

ReuseController:
type: interface

commands:
setTrackHierarchy:
parameters:
enabled: boolean

setReuseBrowser:
parameters:
enabled: boolean

resetForReuse:

navigateAll:
parameters:
url: string

setRecorderMode:
parameters:
mode:
type: enum
literals:
- inspecting
- recording
- none
language: string?
file: string?

setAutoClose:
parameters:
enabled: boolean

highlightAll:
parameters:
selector: string

hideHighlightAll:

kill:

events:
inspectRequested:
parameters:
selector: string

browsersChanged:
parameters:
browsers:
type: array
items:
type: object
properties:
contexts:
type: array
items:
type: object
properties:
pages:
type: array
items: string

SocksSupport:
type: interface

Expand Down
Loading

0 comments on commit 872bcc9

Please sign in to comment.