Skip to content

Commit

Permalink
chore: extract debugger model from inspector (#6261)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Apr 22, 2021
1 parent 34e03fc commit fe4fba4
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 93 deletions.
2 changes: 1 addition & 1 deletion src/dispatchers/browserContextDispatcher.ts
Expand Up @@ -144,7 +144,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}

async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
await RecorderSupplement.getOrCreate(this._context, params);
await RecorderSupplement.show(this._context, params);
}

async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
Expand Down
2 changes: 2 additions & 0 deletions src/server/playwright.ts
Expand Up @@ -28,6 +28,7 @@ import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit';
import { Registry } from '../utils/registry';
import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation';
import { Debugger } from './supplements/debugger';

export class Playwright extends SdkObject {
readonly selectors: Selectors;
Expand All @@ -41,6 +42,7 @@ export class Playwright extends SdkObject {
constructor(isInternal: boolean) {
const listeners: InstrumentationListener[] = [];
if (!isInternal) {
listeners.push(new Debugger());
listeners.push(new Tracer());
listeners.push(new HarTracer());
listeners.push(new InspectorController());
Expand Down
129 changes: 129 additions & 0 deletions src/server/supplements/debugger.ts
@@ -0,0 +1,129 @@
/**
* 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 { EventEmitter } from 'events';
import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils';
import { BrowserContext } from '../browserContext';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import * as consoleApiSource from '../../generated/consoleApiSource';

export class Debugger implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
ContextDebugger.getOrCreate(context);
if (debugMode() === 'console')
await context.extendInjectedScript(consoleApiSource.source);
}

async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata);
}

async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata);
}
}

const symbol = Symbol('ContextDebugger');

export class ContextDebugger extends EventEmitter {
private _pauseOnNextStatement = false;
private _pausedCallsMetadata = new Map<CallMetadata, { resolve: () => void, sdkObject: SdkObject }>();
private _enabled: boolean;

static Events = {
PausedStateChanged: 'pausedstatechanged'
};

static getOrCreate(context: BrowserContext): ContextDebugger {
let contextDebugger = (context as any)[symbol] as ContextDebugger;
if (!contextDebugger) {
contextDebugger = new ContextDebugger();
(context as any)[symbol] = contextDebugger;
}
return contextDebugger;
}

constructor() {
super();
this._enabled = debugMode() === 'inspector';
if (this._enabled)
this.pauseOnNextStatement();
}

static lookup(context?: BrowserContext): ContextDebugger | undefined {
if (!context)
return;
return (context as any)[symbol] as ContextDebugger | undefined;
}

async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
await this.pause(sdkObject, metadata);
}

async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (this._enabled && this._pauseOnNextStatement)
await this.pause(sdkObject, metadata);
}

async pause(sdkObject: SdkObject, metadata: CallMetadata) {
this._enabled = true;
metadata.pauseStartTime = monotonicTime();
const result = new Promise<void>(resolve => {
this._pausedCallsMetadata.set(metadata, { resolve, sdkObject });
});
this.emit(ContextDebugger.Events.PausedStateChanged);
return result;
}

resume(step: boolean) {
this._pauseOnNextStatement = step;
const endTime = monotonicTime();
for (const [metadata, { resolve }] of this._pausedCallsMetadata) {
metadata.pauseEndTime = endTime;
resolve();
}
this._pausedCallsMetadata.clear();
this.emit(ContextDebugger.Events.PausedStateChanged);
}

pauseOnNextStatement() {
this._pauseOnNextStatement = true;
}

isPaused(metadata?: CallMetadata): boolean {
if (metadata)
return this._pausedCallsMetadata.has(metadata);
return !!this._pausedCallsMetadata.size;
}

pausedDetails(): { metadata: CallMetadata, sdkObject: SdkObject }[] {
const result: { metadata: CallMetadata, sdkObject: SdkObject }[] = [];
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata)
result.push({ metadata, sdkObject });
return result;
}
}

function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}

function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
return metadata.method === 'goto' || metadata.method === 'close';
}
46 changes: 14 additions & 32 deletions src/server/supplements/inspectorController.ts
Expand Up @@ -18,54 +18,36 @@ import { BrowserContext } from '../browserContext';
import { RecorderSupplement } from './recorderSupplement';
import { debugLogger } from '../../utils/debugLogger';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { debugMode, isUnderTest } from '../../utils/utils';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { ContextDebugger } from './debugger';

export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
if (debugMode() === 'inspector')
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
else if (debugMode() === 'console')
await context.extendInjectedScript(consoleApiSource.source);
const contextDebugger = ContextDebugger.lookup(context)!;
if (contextDebugger.isPaused())
RecorderSupplement.show(context, {}).catch(() => {});
contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => {
RecorderSupplement.show(context, {}).catch(() => {});
});
}

async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const context = sdkObject.attribution.context;
if (!context)
return;

if (shouldOpenInspector(sdkObject, metadata))
await RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });

const recorder = await RecorderSupplement.getNoCreate(context);
await recorder?.onBeforeCall(sdkObject, metadata);
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onBeforeCall(sdkObject, metadata);
}

async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.context)
return;
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onAfterCall(sdkObject, metadata);
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onAfterCall(sdkObject, metadata);
}

async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.context)
return;
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onBeforeInputAction(sdkObject, metadata);
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onBeforeInputAction(sdkObject, metadata);
}

async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
debugLogger.log(logName as any, message);
if (!sdkObject.attribution.context)
return;
const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.updateCallLog([metadata]);
}
}

function shouldOpenInspector(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}

0 comments on commit fe4fba4

Please sign in to comment.