Skip to content

Commit 102994e

Browse files
committed
chore: make launch, newContext and newPage progress "strict"
1 parent 6caf344 commit 102994e

File tree

14 files changed

+136
-113
lines changed

14 files changed

+136
-113
lines changed

packages/playwright-core/src/server/browser.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import { Download } from './download';
2020
import { SdkObject } from './instrumentation';
2121
import { Page } from './page';
2222
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
23+
import { ProgressController } from './progress';
2324

2425
import type { CallMetadata } from './instrumentation';
2526
import type * as types from './types';
2627
import type { ProxySettings } from './types';
2728
import type { RecentLogsCollector } from './utils/debugLogger';
2829
import type * as channels from '@protocol/channels';
2930
import type { ChildProcess } from 'child_process';
31+
import type { Progress } from './progress';
3032

3133

3234
export interface BrowserProcess {
@@ -84,25 +86,26 @@ export abstract class Browser extends SdkObject {
8486
abstract version(): string;
8587
abstract userAgent(): string;
8688

87-
async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
89+
newContextFromMetadata(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
90+
const controller = new ProgressController(metadata, this, 'strict');
91+
return controller.run(progress => this.newContext(progress, options));
92+
}
93+
94+
async newContext(progress: Progress, options: types.BrowserContextOptions): Promise<BrowserContext> {
8895
validateBrowserContextOptions(options, this.options);
8996
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
9097
if (options.clientCertificates?.length) {
91-
clientCertificatesProxy = new ClientCertificatesProxy(options);
98+
clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close());
9299
options = { ...options };
93-
options.proxyOverride = await clientCertificatesProxy.listen();
100+
options.proxyOverride = clientCertificatesProxy.proxySettings();
94101
options.internalIgnoreHTTPSErrors = true;
95102
}
96-
let context;
97-
try {
98-
context = await this.doCreateNewContext(options);
99-
} catch (error) {
100-
await clientCertificatesProxy?.close();
101-
throw error;
102-
}
103+
const context = await progress.raceWithCleanup(this.doCreateNewContext(options), context => context.close({ reason: 'Failed to create context' }));
103104
context._clientCertificatesProxy = clientCertificatesProxy;
105+
if ((options as any).__testHookBeforeSetStorageState)
106+
await progress.race((options as any).__testHookBeforeSetStorageState());
104107
if (options.storageState)
105-
await context.setStorageState(metadata, options.storageState);
108+
await context.setStorageState(progress, options.storageState);
106109
this.emit(Browser.Events.Context, context);
107110
return context;
108111
}
@@ -112,7 +115,7 @@ export abstract class Browser extends SdkObject {
112115
if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) {
113116
if (this._contextForReuse)
114117
await this._contextForReuse.context.close({ reason: 'Context reused' });
115-
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
118+
this._contextForReuse = { context: await this.newContextFromMetadata(metadata, params), hash };
116119
return { context: this._contextForReuse.context, needsReset: false };
117120
}
118121
await this._contextForReuse.context.stopPendingOperations('Context recreated');

packages/playwright-core/src/server/browserContext.ts

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export abstract class BrowserContext extends SdkObject {
218218
// Navigate to about:blank first to ensure no page scripts are running after this point.
219219
await page?.mainFrame().gotoImpl(progress, 'about:blank', {});
220220

221-
await this._resetStorage();
221+
await this._resetStorage(progress);
222222
await this.clock.resetForReuse();
223223
// TODO: following can be optimized to not perform noops.
224224
if (this._options.permissions)
@@ -379,14 +379,13 @@ export abstract class BrowserContext extends SdkObject {
379379
async _loadDefaultContextAsIs(progress: Progress): Promise<Page | undefined> {
380380
if (!this.possiblyUninitializedPages().length) {
381381
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
382-
progress.cleanupWhenAborted(() => waitForEvent.dispose);
383382
// Race against BrowserContext.close
384383
await Promise.race([waitForEvent.promise, this._closePromise]);
385384
}
386385
const page = this.possiblyUninitializedPages()[0];
387386
if (!page)
388387
return;
389-
const pageOrError = await page.waitForInitializedOrError();
388+
const pageOrError = await progress.race(page.waitForInitializedOrError());
390389
if (pageOrError instanceof Error)
391390
throw pageOrError;
392391
await page.mainFrame()._waitForLoadState(progress, 'load');
@@ -402,7 +401,7 @@ export abstract class BrowserContext extends SdkObject {
402401
// Workaround for:
403402
// - chromium fails to change isMobile for existing page;
404403
// - webkit fails to change locale for existing page.
405-
await this.newPage(progress.metadata);
404+
await this.newPage(progress, false);
406405
await defaultPage.close();
407406
}
408407
}
@@ -511,9 +510,14 @@ export abstract class BrowserContext extends SdkObject {
511510
await this._closePromise;
512511
}
513512

514-
async newPage(metadata: CallMetadata): Promise<Page> {
515-
const page = await this.doCreateNewPage(metadata.isServerSide);
516-
const pageOrError = await page.waitForInitializedOrError();
513+
newPageFromMetadata(metadata: CallMetadata): Promise<Page> {
514+
const contoller = new ProgressController(metadata, this, 'strict');
515+
return contoller.run(progress => this.newPage(progress, false));
516+
}
517+
518+
async newPage(progress: Progress, isServerSide: boolean): Promise<Page> {
519+
const page = await progress.raceWithCleanup(this.doCreateNewPage(isServerSide), page => page.close());
520+
const pageOrError = await progress.race(page.waitForInitializedOrError());
517521
if (pageOrError instanceof Page) {
518522
if (pageOrError.isClosed())
519523
throw new Error('Page has been closed.');
@@ -526,7 +530,12 @@ export abstract class BrowserContext extends SdkObject {
526530
this._origins.add(origin);
527531
}
528532

529-
async storageState(indexedDB = false): Promise<channels.BrowserContextStorageStateResult> {
533+
storageState(indexedDB = false): Promise<channels.BrowserContextStorageStateResult> {
534+
const controller = new ProgressController(serverSideCallMetadata(), this, 'strict');
535+
return controller.run(progress => this.storageStateImpl(progress, indexedDB));
536+
}
537+
538+
async storageStateImpl(progress: Progress, indexedDB: boolean): Promise<channels.BrowserContextStorageStateResult> {
530539
const result: channels.BrowserContextStorageStateResult = {
531540
cookies: await this.cookies(),
532541
origins: []
@@ -557,15 +566,14 @@ export abstract class BrowserContext extends SdkObject {
557566

558567
// If there are still origins to save, create a blank page to iterate over origins.
559568
if (originsToSave.size) {
560-
const internalMetadata = serverSideCallMetadata();
561-
const page = await this.newPage(internalMetadata);
562-
page.addRequestInterceptor(route => {
569+
const page = await this.newPage(progress, true);
570+
await progress.race(page.addRequestInterceptor(route => {
563571
route.fulfill({ body: '<html></html>' }).catch(() => {});
564-
}, 'prepend');
572+
}, 'prepend'));
565573
for (const origin of originsToSave) {
566574
const frame = page.mainFrame();
567-
await frame.goto(internalMetadata, origin, { timeout: 0 });
568-
const storage: SerializedStorage = await frame.evaluateExpression(collectScript, { world: 'utility' });
575+
await frame.gotoImpl(progress, origin, {});
576+
const storage: SerializedStorage = await progress.race(frame.evaluateExpression(collectScript, { world: 'utility' }));
569577
if (storage.localStorage.length || storage.indexedDB?.length)
570578
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
571579
}
@@ -574,29 +582,27 @@ export abstract class BrowserContext extends SdkObject {
574582
return result;
575583
}
576584

577-
async _resetStorage() {
585+
async _resetStorage(progress: Progress) {
578586
const oldOrigins = this._origins;
579587
const newOrigins = new Map(this._options.storageState?.origins?.map(p => [p.origin, p]) || []);
580588
if (!oldOrigins.size && !newOrigins.size)
581589
return;
582590
let page = this.pages()[0];
583591

584-
const internalMetadata = serverSideCallMetadata();
585-
page = page || await this.newPage({
586-
...internalMetadata,
587-
// Do not mark this page as internal, because we will leave it for later reuse
588-
// as a user-visible page.
589-
isServerSide: false,
590-
});
592+
// Do not mark this page as internal, because we will leave it for later reuse
593+
// as a user-visible page.
594+
page = page || await this.newPage(progress, false);
591595
const interceptor = (route: network.Route) => {
592596
route.fulfill({ body: '<html></html>' }).catch(() => {});
593597
};
594-
await page.addRequestInterceptor(interceptor, 'prepend');
598+
599+
progress.cleanupWhenAborted(() => page.removeRequestInterceptor(interceptor));
600+
await progress.race(page.addRequestInterceptor(interceptor, 'prepend'));
595601

596602
for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) {
597603
const frame = page.mainFrame();
598-
await frame.goto(internalMetadata, origin, { timeout: 0 });
599-
await frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin));
604+
await frame.gotoImpl(progress, origin, {});
605+
await progress.race(frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin)));
600606
}
601607

602608
await page.removeRequestInterceptor(interceptor);
@@ -615,27 +621,26 @@ export abstract class BrowserContext extends SdkObject {
615621
return this._settingStorageState;
616622
}
617623

618-
async setStorageState(metadata: CallMetadata, state: NonNullable<channels.BrowserNewContextParams['storageState']>) {
624+
async setStorageState(progress: Progress, state: NonNullable<channels.BrowserNewContextParams['storageState']>) {
619625
this._settingStorageState = true;
620626
try {
621627
if (state.cookies)
622-
await this.addCookies(state.cookies);
628+
await progress.race(this.addCookies(state.cookies));
623629
if (state.origins && state.origins.length) {
624-
const internalMetadata = serverSideCallMetadata();
625-
const page = await this.newPage(internalMetadata);
626-
await page.addRequestInterceptor(route => {
630+
const page = await this.newPage(progress, true);
631+
await progress.race(page.addRequestInterceptor(route => {
627632
route.fulfill({ body: '<html></html>' }).catch(() => {});
628-
}, 'prepend');
633+
}, 'prepend'));
629634
for (const originState of state.origins) {
630635
const frame = page.mainFrame();
631-
await frame.goto(metadata, originState.origin, { timeout: 0 });
636+
await frame.gotoImpl(progress, originState.origin, {});
632637
const restoreScript = `(() => {
633638
const module = {};
634639
${rawStorageSource.source}
635640
const script = new (module.exports.StorageScript())(${this._browser.options.name === 'firefox'});
636641
return script.restore(${JSON.stringify(originState)});
637642
})()`;
638-
await frame.evaluateExpression(restoreScript, { world: 'utility' });
643+
await progress.race(frame.evaluateExpression(restoreScript, { world: 'utility' }));
639644
}
640645
await page.close();
641646
}

packages/playwright-core/src/server/browserType.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { debugMode } from './utils/debug';
2323
import { assert } from '../utils/isomorphic/assert';
2424
import { ManualPromise } from '../utils/isomorphic/manualPromise';
2525
import { DEFAULT_PLAYWRIGHT_TIMEOUT } from '../utils/isomorphic/time';
26-
import { existsAsync } from './utils/fileUtils';
26+
import { existsAsync, removeFolders } from './utils/fileUtils';
2727
import { helper } from './helper';
2828
import { SdkObject } from './instrumentation';
2929
import { PipeTransport } from './pipeTransport';
@@ -69,7 +69,7 @@ export abstract class BrowserType extends SdkObject {
6969

7070
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
7171
options = this._validateLaunchOptions(options);
72-
const controller = new ProgressController(metadata, this);
72+
const controller = new ProgressController(metadata, this, 'strict');
7373
const browser = await controller.run(progress => {
7474
const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL;
7575
if (seleniumHubUrl)
@@ -81,17 +81,16 @@ export abstract class BrowserType extends SdkObject {
8181

8282
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean }): Promise<BrowserContext> {
8383
const launchOptions = this._validateLaunchOptions(options);
84-
const controller = new ProgressController(metadata, this);
84+
const controller = new ProgressController(metadata, this, 'strict');
8585
const browser = await controller.run(async progress => {
8686
// Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors.
8787
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
8888
if (options.clientCertificates?.length) {
89-
clientCertificatesProxy = new ClientCertificatesProxy(options);
90-
launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
89+
clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close());
90+
launchOptions.proxyOverride = clientCertificatesProxy.proxySettings();
9191
options = { ...options };
9292
options.internalIgnoreHTTPSErrors = true;
9393
}
94-
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
9594
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
9695
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
9796
return browser;
@@ -118,7 +117,7 @@ export abstract class BrowserType extends SdkObject {
118117
const browserLogsCollector = new RecentLogsCollector();
119118
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
120119
if ((options as any).__testHookBeforeCreateBrowser)
121-
await (options as any).__testHookBeforeCreateBrowser();
120+
await progress.race((options as any).__testHookBeforeCreateBrowser());
122121
const browserOptions: BrowserOptions = {
123122
name: this._name,
124123
isChromium: this._name === 'chromium',
@@ -140,30 +139,24 @@ export abstract class BrowserType extends SdkObject {
140139
if (persistent)
141140
validateBrowserContextOptions(persistent, browserOptions);
142141
copyTestHooks(options, browserOptions);
143-
const browser = await this.connectToTransport(transport, browserOptions, browserLogsCollector);
142+
const browser = await progress.race(this.connectToTransport(transport, browserOptions, browserLogsCollector));
144143
(browser as any)._userDataDirForTest = userDataDir;
145144
// We assume no control when using custom arguments, and do not prepare the default context in that case.
146145
if (persistent && !options.ignoreAllDefaultArgs)
147146
await browser._defaultContext!._loadDefaultContext(progress);
148147
return browser;
149148
}
150149

151-
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, artifactsDir: string, userDataDir: string, transport: ConnectionTransport }> {
150+
private async _prepareToLaunch(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string | undefined) {
152151
const {
153152
ignoreDefaultArgs,
154153
ignoreAllDefaultArgs,
155154
args = [],
156155
executablePath = null,
157-
handleSIGINT = true,
158-
handleSIGTERM = true,
159-
handleSIGHUP = true,
160156
} = options;
161-
162-
const env = options.env ? envArrayToObject(options.env) : process.env;
163-
164157
await this._createArtifactDirs(options);
165158

166-
const tempDirectories = [];
159+
const tempDirectories: string[] = [];
167160
const artifactsDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-artifacts-'));
168161
tempDirectories.push(artifactsDir);
169162

@@ -178,7 +171,7 @@ export abstract class BrowserType extends SdkObject {
178171
}
179172
await this.prepareUserDataDir(options, userDataDir);
180173

181-
const browserArguments = [];
174+
const browserArguments: string[] = [];
182175
if (ignoreAllDefaultArgs)
183176
browserArguments.push(...args);
184177
else if (ignoreDefaultArgs)
@@ -199,15 +192,29 @@ export abstract class BrowserType extends SdkObject {
199192
await registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage);
200193
}
201194

195+
return { executable, browserArguments, userDataDir, artifactsDir, tempDirectories };
196+
}
197+
198+
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, artifactsDir: string, userDataDir: string, transport: ConnectionTransport }> {
199+
const {
200+
handleSIGINT = true,
201+
handleSIGTERM = true,
202+
handleSIGHUP = true,
203+
} = options;
204+
205+
const env = options.env ? envArrayToObject(options.env) : process.env;
206+
const prepared = await progress.race(this._prepareToLaunch(options, isPersistent, userDataDir));
207+
progress.cleanupWhenAborted(() => removeFolders(prepared.tempDirectories));
208+
202209
// Note: it is important to define these variables before launchProcess, so that we don't get
203210
// "Cannot access 'browserServer' before initialization" if something went wrong.
204211
let transport: ConnectionTransport | undefined = undefined;
205212
let browserProcess: BrowserProcess | undefined = undefined;
206213
const exitPromise = new ManualPromise();
207214
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
208-
command: executable,
209-
args: browserArguments,
210-
env: this.amendEnvironment(env, userDataDir, executable, browserArguments),
215+
command: prepared.executable,
216+
args: prepared.browserArguments,
217+
env: this.amendEnvironment(env, prepared.userDataDir, prepared.executable, prepared.browserArguments),
211218
handleSIGINT,
212219
handleSIGTERM,
213220
handleSIGHUP,
@@ -216,7 +223,7 @@ export abstract class BrowserType extends SdkObject {
216223
browserLogsCollector.log(message);
217224
},
218225
stdio: 'pipe',
219-
tempDirectories,
226+
tempDirectories: prepared.tempDirectories,
220227
attemptToGracefullyClose: async () => {
221228
if ((options as any).__testHookGracefullyClose)
222229
await (options as any).__testHookGracefullyClose();
@@ -253,7 +260,7 @@ export abstract class BrowserType extends SdkObject {
253260
kill
254261
};
255262
progress.cleanupWhenAborted(() => closeOrKill(progress.timeUntilDeadline()));
256-
const { wsEndpoint } = await Promise.race([
263+
const { wsEndpoint } = await progress.race([
257264
this.waitForReadyState(options, browserLogsCollector),
258265
exitPromise.then(() => ({ wsEndpoint: undefined })),
259266
]);
@@ -263,7 +270,8 @@ export abstract class BrowserType extends SdkObject {
263270
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
264271
transport = new PipeTransport(stdio[3], stdio[4]);
265272
}
266-
return { browserProcess, artifactsDir, userDataDir, transport };
273+
progress.cleanupWhenAborted(() => transport.close());
274+
return { browserProcess, artifactsDir: prepared.artifactsDir, userDataDir: prepared.userDataDir, transport };
267275
}
268276

269277
async _createArtifactDirs(options: types.LaunchOptions): Promise<void> {

0 commit comments

Comments
 (0)