2021-04-21 20:46:45 -07:00
|
|
|
/**
|
|
|
|
* 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';
|
2021-09-17 15:24:15 -07:00
|
|
|
import { commandsWithTracingSnapshots, pausesBeforeInputActions } from '../../protocol/channels';
|
2021-04-21 20:46:45 -07:00
|
|
|
|
2021-04-23 18:34:52 -07:00
|
|
|
const symbol = Symbol('Debugger');
|
2021-04-21 20:46:45 -07:00
|
|
|
|
2021-04-23 18:34:52 -07:00
|
|
|
export class Debugger extends EventEmitter implements InstrumentationListener {
|
2021-04-21 20:46:45 -07:00
|
|
|
private _pauseOnNextStatement = false;
|
|
|
|
private _pausedCallsMetadata = new Map<CallMetadata, { resolve: () => void, sdkObject: SdkObject }>();
|
|
|
|
private _enabled: boolean;
|
2021-04-23 18:34:52 -07:00
|
|
|
private _context: BrowserContext;
|
2021-04-21 20:46:45 -07:00
|
|
|
|
|
|
|
static Events = {
|
|
|
|
PausedStateChanged: 'pausedstatechanged'
|
|
|
|
};
|
2021-05-26 15:49:30 -07:00
|
|
|
private _muted = false;
|
2021-04-21 20:46:45 -07:00
|
|
|
|
2021-04-23 18:34:52 -07:00
|
|
|
constructor(context: BrowserContext) {
|
2021-04-21 20:46:45 -07:00
|
|
|
super();
|
2021-04-23 18:34:52 -07:00
|
|
|
this._context = context;
|
|
|
|
(this._context as any)[symbol] = this;
|
2021-04-21 20:46:45 -07:00
|
|
|
this._enabled = debugMode() === 'inspector';
|
|
|
|
if (this._enabled)
|
|
|
|
this.pauseOnNextStatement();
|
|
|
|
}
|
|
|
|
|
2021-04-23 18:34:52 -07:00
|
|
|
static lookup(context?: BrowserContext): Debugger | undefined {
|
2021-04-21 20:46:45 -07:00
|
|
|
if (!context)
|
|
|
|
return;
|
2021-04-23 18:34:52 -07:00
|
|
|
return (context as any)[symbol] as Debugger | undefined;
|
|
|
|
}
|
|
|
|
|
2021-05-26 15:49:30 -07:00
|
|
|
async setMuted(muted: boolean) {
|
|
|
|
this._muted = muted;
|
|
|
|
}
|
|
|
|
|
2021-04-21 20:46:45 -07:00
|
|
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-05-26 15:49:30 -07:00
|
|
|
if (this._muted)
|
|
|
|
return;
|
2021-09-17 15:24:15 -07:00
|
|
|
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata)))
|
2021-04-21 20:46:45 -07:00
|
|
|
await this.pause(sdkObject, metadata);
|
|
|
|
}
|
|
|
|
|
|
|
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
2021-05-26 15:49:30 -07:00
|
|
|
if (this._muted)
|
|
|
|
return;
|
2021-04-21 20:46:45 -07:00
|
|
|
if (this._enabled && this._pauseOnNextStatement)
|
|
|
|
await this.pause(sdkObject, metadata);
|
|
|
|
}
|
|
|
|
|
|
|
|
async pause(sdkObject: SdkObject, metadata: CallMetadata) {
|
2021-05-26 15:49:30 -07:00
|
|
|
if (this._muted)
|
|
|
|
return;
|
2021-04-21 20:46:45 -07:00
|
|
|
this._enabled = true;
|
|
|
|
metadata.pauseStartTime = monotonicTime();
|
|
|
|
const result = new Promise<void>(resolve => {
|
|
|
|
this._pausedCallsMetadata.set(metadata, { resolve, sdkObject });
|
|
|
|
});
|
2021-04-23 18:34:52 -07:00
|
|
|
this.emit(Debugger.Events.PausedStateChanged);
|
2021-04-21 20:46:45 -07:00
|
|
|
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();
|
2021-04-23 18:34:52 -07:00
|
|
|
this.emit(Debugger.Events.PausedStateChanged);
|
2021-04-21 20:46:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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';
|
|
|
|
}
|
|
|
|
|
2021-09-17 15:24:15 -07:00
|
|
|
function shouldPauseBeforeStep(metadata: CallMetadata): boolean {
|
|
|
|
// Always stop on 'close'
|
|
|
|
if (metadata.method === 'close')
|
|
|
|
return true;
|
|
|
|
if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo')
|
|
|
|
return false; // Never stop on those, primarily for the test harness.
|
|
|
|
const step = metadata.type + '.' + metadata.method;
|
|
|
|
// Stop before everything that generates snapshot. But don't stop before those marked as pausesBeforeInputActions
|
|
|
|
// since we stop in them on a separate instrumentation signal.
|
|
|
|
return commandsWithTracingSnapshots.has(step) && !pausesBeforeInputActions.has(metadata.type + '.' + metadata.method);
|
2021-04-21 20:46:45 -07:00
|
|
|
}
|