Skip to content

Commit 4544110

Browse files
authored
fix(oopif): race between detachedFromTarget and frameAttached (#2419)
During remote -> local transition, these two events come in unpredictable order, so we try to handle both cases. Also, remote frame detach was not handled at all.
1 parent de0bbd3 commit 4544110

File tree

2 files changed

+51
-27
lines changed

2 files changed

+51
-27
lines changed

src/chromium/crPage.ts

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class CRPage implements PageDelegate {
8989
await Promise.all(Array.from(this._sessions.values()).map(frame => cb(frame)));
9090
}
9191

92-
_sessionForFrame(frame: frames.Frame): FrameSession {
92+
private _sessionForFrame(frame: frames.Frame): FrameSession {
9393
// Frame id equals target id.
9494
while (!this._sessions.has(frame._id)) {
9595
const parent = frame.parentFrame();
@@ -105,29 +105,6 @@ export class CRPage implements PageDelegate {
105105
return this._sessionForFrame(frame);
106106
}
107107

108-
addFrameSession(targetId: Protocol.Target.TargetID, session: CRSession) {
109-
// Frame id equals target id.
110-
const frame = this._page._frameManager.frame(targetId);
111-
assert(frame);
112-
const parentSession = this._sessionForFrame(frame);
113-
this._page._frameManager.removeChildFramesRecursively(frame);
114-
const frameSession = new FrameSession(this, session, targetId, parentSession);
115-
this._sessions.set(targetId, frameSession);
116-
frameSession._initialize(false).catch(e => e);
117-
}
118-
119-
removeFrameSession(targetId: Protocol.Target.TargetID) {
120-
const frameSession = this._sessions.get(targetId);
121-
if (!frameSession)
122-
return;
123-
// Frame id equals target id.
124-
const frame = this._page._frameManager.frame(targetId);
125-
if (frame)
126-
this._page._frameManager.removeChildFramesRecursively(frame);
127-
frameSession.dispose();
128-
this._sessions.delete(targetId);
129-
}
130-
131108
async pageOrError(): Promise<Page | Error> {
132109
return this._pagePromise;
133110
}
@@ -340,6 +317,9 @@ class FrameSession {
340317
private _firstNonInitialNavigationCommittedFulfill = () => {};
341318
private _firstNonInitialNavigationCommittedReject = (e: Error) => {};
342319
private _windowId: number | undefined;
320+
// Marks the oopif session that remote -> local transition has happened in the parent.
321+
// See Target.detachedFromTarget handler for details.
322+
private _swappedIn = false;
343323

344324
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
345325
this._client = client;
@@ -471,6 +451,7 @@ class FrameSession {
471451
dispose() {
472452
helper.removeEventListeners(this._eventListeners);
473453
this._networkManager.dispose();
454+
this._crPage._sessions.delete(this._targetId);
474455
}
475456

476457
async _navigate(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
@@ -502,8 +483,10 @@ class FrameSession {
502483
}
503484

504485
_onFrameAttached(frameId: string, parentFrameId: string | null) {
505-
if (this._crPage._sessions.has(frameId) && frameId !== this._targetId) {
486+
const frameSession = this._crPage._sessions.get(frameId);
487+
if (frameSession && frameId !== this._targetId) {
506488
// This is a remote -> local frame transition.
489+
frameSession._swappedIn = true;
507490
const frame = this._page._frameManager.frame(frameId)!;
508491
this._page._frameManager.removeChildFramesRecursively(frame);
509492
return;
@@ -565,7 +548,13 @@ class FrameSession {
565548
const session = CRConnection.fromSession(this._client).session(event.sessionId)!;
566549

567550
if (event.targetInfo.type === 'iframe') {
568-
this._crPage.addFrameSession(event.targetInfo.targetId, session);
551+
// Frame id equals target id.
552+
const targetId = event.targetInfo.targetId;
553+
const frame = this._page._frameManager.frame(targetId)!;
554+
this._page._frameManager.removeChildFramesRecursively(frame);
555+
const frameSession = new FrameSession(this._crPage, session, targetId, this);
556+
this._crPage._sessions.set(targetId, frameSession);
557+
frameSession._initialize(false).catch(e => e);
569558
return;
570559
}
571560

@@ -598,8 +587,31 @@ class FrameSession {
598587
}
599588

600589
_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
601-
this._crPage.removeFrameSession(event.targetId!);
590+
// This might be a worker...
602591
this._page._removeWorker(event.sessionId);
592+
593+
// ... or an oopif.
594+
const childFrameSession = this._crPage._sessions.get(event.targetId!);
595+
if (!childFrameSession)
596+
return;
597+
598+
// Usually, we get frameAttached in this session first and mark child as swappedIn.
599+
if (childFrameSession._swappedIn) {
600+
childFrameSession.dispose();
601+
return;
602+
}
603+
604+
// However, sometimes we get detachedFromTarget before frameAttached.
605+
// In this case we don't know wheter this is a remote frame detach,
606+
// or just a remote -> local transition. In the latter case, frameAttached
607+
// is already inflight, so let's make a safe roundtrip to ensure it arrives.
608+
this._client.send('Page.enable').catch(e => null).then(() => {
609+
// Child was not swapped in - that means frameAttached did not happen and
610+
// this is remote detach rather than remote -> local swap.
611+
if (!childFrameSession._swappedIn)
612+
this._page._frameManager.frameDetached(event.targetId!);
613+
childFrameSession.dispose();
614+
});
603615
}
604616

605617
_onWindowOpen(event: Protocol.Page.windowOpenPayload) {

test/chromium/oopif.spec.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ describe('OOPIF', function() {
4141
expect(page.frames().length).toBe(2);
4242
expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
4343
});
44+
it('should handle oopif detach', async function({browser, page, server, context}) {
45+
await page.goto(server.PREFIX + '/dynamic-oopif.html');
46+
expect(await countOOPIFs(browser)).toBe(1);
47+
expect(page.frames().length).toBe(2);
48+
const frame = page.frames()[1];
49+
expect(await frame.evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
50+
const [detachedFrame] = await Promise.all([
51+
page.waitForEvent('framedetached'),
52+
page.evaluate(() => document.querySelector('iframe').remove()),
53+
]);
54+
expect(detachedFrame).toBe(frame);
55+
});
4456
it('should handle remote -> local -> remote transitions', async function({browser, page, server, context}) {
4557
await page.goto(server.PREFIX + '/dynamic-oopif.html');
4658
expect(page.frames().length).toBe(2);

0 commit comments

Comments
 (0)