chore: expose driver protocol on the pw object (#17363)
This commit is contained in:
parent
b09ea69024
commit
872bcc9734
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 } });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 :
|
||||
|
|
@ -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 :
|
||||
|
|
@ -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 :
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -308,6 +308,49 @@ scheme.PlaywrightNewRequestResult = tObject({
|
|||
});
|
||||
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
||||
scheme.PlaywrightHideHighlightResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerInitializer = tOptional(tObject({}));
|
||||
scheme.ReuseControllerInspectRequestedEvent = tObject({
|
||||
selector: tString,
|
||||
});
|
||||
scheme.ReuseControllerBrowsersChangedEvent = tObject({
|
||||
browsers: tArray(tObject({
|
||||
contexts: tArray(tObject({
|
||||
pages: tArray(tString),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
scheme.ReuseControllerSetTrackHierarchyParams = tObject({
|
||||
enabled: tBoolean,
|
||||
});
|
||||
scheme.ReuseControllerSetTrackHierarchyResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerSetReuseBrowserParams = tObject({
|
||||
enabled: tBoolean,
|
||||
});
|
||||
scheme.ReuseControllerSetReuseBrowserResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerResetForReuseParams = tOptional(tObject({}));
|
||||
scheme.ReuseControllerResetForReuseResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerNavigateAllParams = tObject({
|
||||
url: tString,
|
||||
});
|
||||
scheme.ReuseControllerNavigateAllResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerSetRecorderModeParams = tObject({
|
||||
mode: tEnum(['inspecting', 'recording', 'none']),
|
||||
language: tOptional(tString),
|
||||
file: tOptional(tString),
|
||||
});
|
||||
scheme.ReuseControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerSetAutoCloseParams = tObject({
|
||||
enabled: tBoolean,
|
||||
});
|
||||
scheme.ReuseControllerSetAutoCloseResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerHighlightAllParams = tObject({
|
||||
selector: tString,
|
||||
});
|
||||
scheme.ReuseControllerHighlightAllResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerHideHighlightAllParams = tOptional(tObject({}));
|
||||
scheme.ReuseControllerHideHighlightAllResult = tOptional(tObject({}));
|
||||
scheme.ReuseControllerKillParams = tOptional(tObject({}));
|
||||
scheme.ReuseControllerKillResult = tOptional(tObject({}));
|
||||
scheme.SocksSupportInitializer = tOptional(tObject({}));
|
||||
scheme.SocksSupportSocksRequestedEvent = tObject({
|
||||
uid: tString,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { WebSocket } from '../utilsBundle';
|
||||
import type { Playwright } from '../server';
|
||||
import type { DispatcherScope, Playwright } from '../server';
|
||||
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
|
||||
import { Browser } from '../server/browser';
|
||||
import { serverSideCallMetadata } from '../server/instrumentation';
|
||||
|
|
@ -24,6 +24,7 @@ import { SocksProxy } from '../common/socksProxy';
|
|||
import type { Mode } from './playwrightServer';
|
||||
import { assert } from '../utils';
|
||||
import type { LaunchOptions } from '../server/types';
|
||||
import { ReuseControllerDispatcher } from '../server/dispatchers/reuseControllerDispatcher';
|
||||
|
||||
type Options = {
|
||||
enableSocksProxy: boolean,
|
||||
|
|
@ -45,9 +46,9 @@ export class PlaywrightConnection {
|
|||
private _disconnected = false;
|
||||
private _preLaunched: PreLaunched;
|
||||
private _options: Options;
|
||||
private _root: RootDispatcher;
|
||||
private _root: DispatcherScope;
|
||||
|
||||
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, isReuseControllerClient: boolean, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||
this._ws = ws;
|
||||
this._preLaunched = preLaunched;
|
||||
this._options = options;
|
||||
|
|
@ -72,6 +73,11 @@ export class PlaywrightConnection {
|
|||
ws.on('close', () => this._onDisconnect());
|
||||
ws.on('error', error => this._onDisconnect(error));
|
||||
|
||||
if (isReuseControllerClient) {
|
||||
this._root = this._initReuseControllerMode();
|
||||
return;
|
||||
}
|
||||
|
||||
this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
|
||||
if (mode === 'reuse-browser')
|
||||
return await this._initReuseBrowsersMode(scope);
|
||||
|
|
@ -130,6 +136,14 @@ export class PlaywrightConnection {
|
|||
return playwrightDispatcher;
|
||||
}
|
||||
|
||||
private _initReuseControllerMode(): ReuseControllerDispatcher {
|
||||
this._debugLog(`engaged reuse controller mode`);
|
||||
const playwright = this._preLaunched.playwright!;
|
||||
this._cleanups.push(() => gracefullyCloseAll());
|
||||
// Always create new instance based on the reused Playwright instance.
|
||||
return new ReuseControllerDispatcher(this._dispatcherConnection, playwright.reuseController);
|
||||
}
|
||||
|
||||
private async _initReuseBrowsersMode(scope: RootDispatcher) {
|
||||
this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`);
|
||||
const playwright = this._preLaunched.playwright!;
|
||||
|
|
@ -150,7 +164,7 @@ export class PlaywrightConnection {
|
|||
}
|
||||
|
||||
if (!browser) {
|
||||
browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), {
|
||||
browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
|
||||
...this._options.launchOptions,
|
||||
headless: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ function newLogger() {
|
|||
return (message: string) => debugLog(`[id=${id}] ${message}`);
|
||||
}
|
||||
|
||||
// TODO: replace 'reuse-browser' with 'allow-reuse' in 1.27.
|
||||
export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
||||
|
||||
type ServerOptions = {
|
||||
|
|
@ -58,11 +59,11 @@ export class PlaywrightServer {
|
|||
assert(options.preLaunchedBrowser);
|
||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
||||
}
|
||||
if (mode === 'reuse-browser')
|
||||
this._preLaunchedPlaywright = createPlaywright('javascript');
|
||||
}
|
||||
|
||||
preLaunchedPlaywright(): Playwright | null {
|
||||
preLaunchedPlaywright(): Playwright {
|
||||
if (!this._preLaunchedPlaywright)
|
||||
this._preLaunchedPlaywright = createPlaywright('javascript');
|
||||
return this._preLaunchedPlaywright;
|
||||
}
|
||||
|
||||
|
|
@ -87,9 +88,10 @@ export class PlaywrightServer {
|
|||
debugLog('Listening at ' + wsEndpoint);
|
||||
|
||||
this._wsServer = new wsServer({ server, path: this._options.path });
|
||||
const semaphore = new Semaphore(this._options.maxConcurrentConnections);
|
||||
const browserSemaphore = new Semaphore(this._options.maxConcurrentConnections);
|
||||
const controllerSemaphore = new Semaphore(1);
|
||||
this._wsServer.on('connection', (ws, request) => {
|
||||
if (semaphore.requested() >= this._options.maxIncomingConnections) {
|
||||
if (browserSemaphore.requested() >= this._options.maxIncomingConnections) {
|
||||
ws.close(1013, 'Playwright Server is busy');
|
||||
return;
|
||||
}
|
||||
|
|
@ -109,9 +111,27 @@ export class PlaywrightServer {
|
|||
|
||||
const log = newLogger();
|
||||
log(`serving connection: ${request.url}`);
|
||||
const isReuseControllerClient = !!request.headers['x-playwright-reuse-controller'];
|
||||
const semaphore = isReuseControllerClient ? controllerSemaphore : browserSemaphore;
|
||||
|
||||
// If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright.
|
||||
// If we get a reuse-controller request, create this._preLaunchedPlaywright.
|
||||
if (isReuseControllerClient || (this._mode === 'reuse-browser') && !this._preLaunchedPlaywright)
|
||||
this.preLaunchedPlaywright();
|
||||
|
||||
// If we have a playwright to reuse, consult controller for reuse mode.
|
||||
let mode = this._mode;
|
||||
if (mode === 'auto' && this._preLaunchedPlaywright?.reuseController.reuseBrowser())
|
||||
mode = 'reuse-browser';
|
||||
|
||||
if (mode === 'reuse-browser')
|
||||
semaphore.setMax(1);
|
||||
else
|
||||
semaphore.setMax(this._options.maxConcurrentConnections);
|
||||
|
||||
const connection = new PlaywrightConnection(
|
||||
semaphore.aquire(),
|
||||
this._mode, ws,
|
||||
mode, ws, isReuseControllerClient,
|
||||
{ enableSocksProxy, browserName, launchOptions },
|
||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
||||
log, () => semaphore.release());
|
||||
|
|
@ -149,10 +169,15 @@ export class Semaphore {
|
|||
private _max: number;
|
||||
private _aquired = 0;
|
||||
private _queue: ManualPromise[] = [];
|
||||
|
||||
constructor(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
setMax(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
aquire(): Promise<void> {
|
||||
const lock = new ManualPromise();
|
||||
this._queue.push(lock);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* 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 type * as channels from '../../protocol/channels';
|
||||
import { ReuseController } from '../reuseController';
|
||||
import type { DispatcherConnection, RootDispatcher } from './dispatcher';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
|
||||
export class ReuseControllerDispatcher extends Dispatcher<ReuseController, channels.ReuseControllerChannel, RootDispatcher> implements channels.ReuseControllerChannel {
|
||||
_type_ReuseController;
|
||||
|
||||
constructor(connection: DispatcherConnection, reuseController: ReuseController) {
|
||||
super(connection, reuseController, 'ReuseController', {});
|
||||
this._type_ReuseController = true;
|
||||
this._object.on(ReuseController.Events.BrowsersChanged, browsers => {
|
||||
this._dispatchEvent('browsersChanged', { browsers });
|
||||
});
|
||||
this._object.on(ReuseController.Events.InspectRequested, selector => {
|
||||
this._dispatchEvent('inspectRequested', { selector });
|
||||
});
|
||||
}
|
||||
|
||||
async setTrackHierarchy(params: channels.ReuseControllerSetTrackHierarchyParams) {
|
||||
this._object.setTrackHierarcy(params.enabled);
|
||||
}
|
||||
|
||||
async setReuseBrowser(params: channels.ReuseControllerSetReuseBrowserParams) {
|
||||
this._object.setReuseBrowser(params.enabled);
|
||||
}
|
||||
|
||||
async resetForReuse() {
|
||||
await this._object.resetForReuse();
|
||||
}
|
||||
|
||||
async navigateAll(params: channels.ReuseControllerNavigateAllParams) {
|
||||
await this._object.navigateAll(params.url);
|
||||
}
|
||||
|
||||
async setRecorderMode(params: channels.ReuseControllerSetRecorderModeParams) {
|
||||
await this._object.setRecorderMode(params);
|
||||
}
|
||||
|
||||
async setAutoClose(params: channels.ReuseControllerSetAutoCloseParams) {
|
||||
await this._object.setAutoCloseEnabled(params.enabled);
|
||||
}
|
||||
|
||||
async highlightAll(params: channels.ReuseControllerHighlightAllParams) {
|
||||
await this._object.highlightAll(params.selector);
|
||||
}
|
||||
|
||||
async hideHighlightAll() {
|
||||
await this._object.hideHighlightAll();
|
||||
}
|
||||
|
||||
async kill() {
|
||||
await this._object.kill();
|
||||
}
|
||||
|
||||
override _dispose() {
|
||||
super._dispose();
|
||||
this._object.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import type { CallMetadata } from './instrumentation';
|
|||
import { createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import type { Page } from './page';
|
||||
import { ReuseController } from './reuseController';
|
||||
|
||||
export class Playwright extends SdkObject {
|
||||
readonly selectors: Selectors;
|
||||
|
|
@ -35,6 +36,7 @@ export class Playwright extends SdkObject {
|
|||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
readonly options: PlaywrightOptions;
|
||||
readonly reuseController: ReuseController;
|
||||
private _allPages = new Set<Page>();
|
||||
private _allBrowsers = new Set<Browser>();
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ export class Playwright extends SdkObject {
|
|||
this.electron = new Electron(this.options);
|
||||
this.android = new Android(new AdbBackend(), this.options);
|
||||
this.selectors = this.options.selectors;
|
||||
this.reuseController = new ReuseController(this);
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
|
|
|
|||
215
packages/playwright-core/src/server/reuseController.ts
Normal file
215
packages/playwright-core/src/server/reuseController.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/**
|
||||
* 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 type { Mode } from '../server/recorder/recorderTypes';
|
||||
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||
import type { Browser } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { createInstrumentation, SdkObject, serverSideCallMetadata } from './instrumentation';
|
||||
import type { InstrumentationListener } from './instrumentation';
|
||||
import type { Playwright } from './playwright';
|
||||
import { Recorder } from './recorder';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
||||
export class ReuseController extends SdkObject {
|
||||
static Events = {
|
||||
BrowsersChanged: 'browsersChanged',
|
||||
InspectRequested: 'inspectRequested'
|
||||
};
|
||||
|
||||
private _autoCloseTimer: NodeJS.Timeout | undefined;
|
||||
// TODO: remove in 1.27
|
||||
private _autoCloseAllowed = false;
|
||||
private _trackHierarchyListener: InstrumentationListener | undefined;
|
||||
private _playwright: Playwright;
|
||||
private _reuseBrowser = false;
|
||||
|
||||
constructor(playwright: Playwright) {
|
||||
super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'ReuseController');
|
||||
this._playwright = playwright;
|
||||
}
|
||||
|
||||
setAutoCloseAllowed(allowed: boolean) {
|
||||
this._autoCloseAllowed = allowed;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.setTrackHierarcy(false);
|
||||
this.setAutoCloseAllowed(false);
|
||||
this.setReuseBrowser(false);
|
||||
}
|
||||
|
||||
setTrackHierarcy(enabled: boolean) {
|
||||
if (enabled && !this._trackHierarchyListener) {
|
||||
this._trackHierarchyListener = {
|
||||
onPageOpen: () => this._emitSnapshot(),
|
||||
onPageNavigated: () => this._emitSnapshot(),
|
||||
onPageClose: () => this._emitSnapshot(),
|
||||
};
|
||||
this.instrumentation.addListener(this._trackHierarchyListener, null);
|
||||
} else if (!enabled && this._trackHierarchyListener) {
|
||||
this.instrumentation.removeListener(this._trackHierarchyListener);
|
||||
this._trackHierarchyListener = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
reuseBrowser(): boolean {
|
||||
return this._reuseBrowser;
|
||||
}
|
||||
|
||||
setReuseBrowser(enabled: boolean) {
|
||||
this._reuseBrowser = enabled;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async navigateAll(url: string) {
|
||||
for (const p of this._playwright.allPages())
|
||||
await p.mainFrame().goto(internalMetadata, url);
|
||||
}
|
||||
|
||||
async setRecorderMode(params: { mode: Mode, language?: string, file?: string }) {
|
||||
await this._closeBrowsersWithoutPages();
|
||||
|
||||
if (params.mode === 'none') {
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
recorder.setHighlightedSelector('');
|
||||
recorder.setMode('none');
|
||||
}
|
||||
this.setAutoCloseEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._playwright.allBrowsers().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 this._allRecorders()) {
|
||||
recorder.setHighlightedSelector('');
|
||||
if (params.mode === 'recording')
|
||||
recorder.setOutput(params.language!, params.file);
|
||||
recorder.setMode(params.mode);
|
||||
}
|
||||
this.setAutoCloseEnabled(true);
|
||||
}
|
||||
|
||||
async setAutoCloseEnabled(enabled: boolean) {
|
||||
if (!this._autoCloseAllowed)
|
||||
return;
|
||||
if (this._autoCloseTimer)
|
||||
clearTimeout(this._autoCloseTimer);
|
||||
if (!enabled)
|
||||
return;
|
||||
const heartBeat = () => {
|
||||
if (!this._playwright.allPages().length)
|
||||
selfDestruct();
|
||||
else
|
||||
this._autoCloseTimer = setTimeout(heartBeat, 5000);
|
||||
};
|
||||
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||
}
|
||||
|
||||
async highlightAll(selector: string) {
|
||||
for (const recorder of await this._allRecorders())
|
||||
recorder.setHighlightedSelector(selector);
|
||||
}
|
||||
|
||||
async hideHighlightAll() {
|
||||
await this._playwright.hideHighlight();
|
||||
}
|
||||
|
||||
allBrowsers(): Browser[] {
|
||||
return [...this._playwright.allBrowsers()];
|
||||
}
|
||||
|
||||
async kill() {
|
||||
selfDestruct();
|
||||
}
|
||||
|
||||
private _emitSnapshot() {
|
||||
const browsers = [];
|
||||
for (const browser of this._playwright.allBrowsers()) {
|
||||
const b = {
|
||||
contexts: [] as any[]
|
||||
};
|
||||
browsers.push(b);
|
||||
for (const context of browser.contexts()) {
|
||||
const c = {
|
||||
pages: [] as any[]
|
||||
};
|
||||
b.contexts.push(c);
|
||||
for (const page of context.pages())
|
||||
c.pages.push(page.mainFrame().url());
|
||||
}
|
||||
}
|
||||
this.emit(ReuseController.Events.BrowsersChanged, browsers);
|
||||
}
|
||||
|
||||
private async _allRecorders(): Promise<Recorder[]> {
|
||||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
contexts.add(page.context());
|
||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||
return result.filter(Boolean) as Recorder[];
|
||||
}
|
||||
|
||||
private async _closeBrowsersWithoutPages() {
|
||||
for (const browser of this._playwright.allBrowsers()) {
|
||||
for (const context of browser.contexts()) {
|
||||
if (!context.pages().length)
|
||||
await context.close(serverSideCallMetadata());
|
||||
}
|
||||
if (!browser.contexts())
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selfDestruct() {
|
||||
// Force exit after 30 seconds.
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
// Meanwhile, try to gracefully close all browsers.
|
||||
gracefullyCloseAll().then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
class InspectingRecorderApp extends EmptyRecorderApp {
|
||||
private _reuseController: ReuseController;
|
||||
|
||||
constructor(reuseController: ReuseController) {
|
||||
super();
|
||||
this._reuseController = reuseController;
|
||||
}
|
||||
|
||||
override async setSelector(selector: string): Promise<void> {
|
||||
this._reuseController.emit(ReuseController.Events.InspectRequested, selector);
|
||||
}
|
||||
}
|
||||
|
|
@ -218,6 +218,7 @@ export class Runner {
|
|||
};
|
||||
for (const [project, files] of filesByProject) {
|
||||
report.projects.push({
|
||||
docker: process.env.PLAYWRIGHT_DOCKER,
|
||||
name: project.name,
|
||||
testDir: path.resolve(configFile, project.testDir),
|
||||
files: files
|
||||
|
|
|
|||
Loading…
Reference in a new issue