Skip to content

Commit dbd2139

Browse files
fix: issue with proxy correlations and web/shared workers (#28105)
1 parent d960686 commit dbd2139

19 files changed

+313
-79
lines changed

cli/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ _Released 10/24/2023 (PENDING)_
55

66
**Bugfixes:**
77

8+
- Fixed a performance regression in `13.3.1` with proxy correlation timeouts and requests issued from web and shared workers. Fixes [#28104](https://github.com/cypress-io/cypress/issues/28104).
89
- Fixed a performance problem with proxy correlation when requests get aborted and then get miscorrelated with follow up requests. Fixed in [#28094](https://github.com/cypress-io/cypress/pull/28094).
910
- Fixed a regression in [10.0.0](#10.0.0), where search would not find a spec if the file name contains "-" or "\_", but search prompt contains " " instead (e.g. search file "spec-file.cy.ts" with prompt "spec file"). Fixes [#25303](https://github.com/cypress-io/cypress/issues/25303).
1011

packages/server/lib/browsers/browser-cri-client.ts

+60-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import CRI from 'chrome-remote-interface'
33
import Debug from 'debug'
44
import { _connectAsync, _getDelayMsForRetry } from './protocol'
55
import * as errors from '../errors'
6-
import { create, CriClient } from './cri-client'
6+
import { create, CriClient, DEFAULT_NETWORK_ENABLE_OPTIONS } from './cri-client'
77
import type { ProtocolManagerShape } from '@packages/types'
88

99
const debug = Debug('cypress:server:browsers:browser-cri-client')
@@ -13,6 +13,27 @@ interface Version {
1313
minor: number
1414
}
1515

16+
type BrowserCriClientOptions = {
17+
browserClient: CriClient
18+
versionInfo: CRI.VersionResult
19+
host: string
20+
port: number
21+
browserName: string
22+
onAsynchronousError: Function
23+
protocolManager?: ProtocolManagerShape
24+
fullyManageTabs?: boolean
25+
}
26+
27+
type BrowserCriClientCreateOptions = {
28+
hosts: string[]
29+
port: number
30+
browserName: string
31+
onAsynchronousError: Function
32+
onReconnect?: (client: CriClient) => void
33+
protocolManager?: ProtocolManagerShape
34+
fullyManageTabs?: boolean
35+
}
36+
1637
const isVersionGte = (a: Version, b: Version) => {
1738
return a.major > b.major || (a.major === b.major && a.minor >= b.minor)
1839
}
@@ -114,6 +135,14 @@ const retryWithIncreasingDelay = async <T>(retryable: () => Promise<T>, browserN
114135
}
115136

116137
export class BrowserCriClient {
138+
private browserClient: CriClient
139+
private versionInfo: CRI.VersionResult
140+
private host: string
141+
private port: number
142+
private browserName: string
143+
private onAsynchronousError: Function
144+
private protocolManager?: ProtocolManagerShape
145+
private fullyManageTabs?: boolean
117146
currentlyAttachedTarget: CriClient | undefined
118147
// whenever we instantiate the instance we're already connected bc
119148
// we receive an underlying CRI connection
@@ -125,7 +154,16 @@ export class BrowserCriClient {
125154
gracefulShutdown?: Boolean
126155
onClose: Function | null = null
127156

128-
private constructor (private browserClient: CriClient, private versionInfo, public host: string, public port: number, private browserName: string, private onAsynchronousError: Function, private protocolManager?: ProtocolManagerShape) { }
157+
private constructor ({ browserClient, versionInfo, host, port, browserName, onAsynchronousError, protocolManager, fullyManageTabs }: BrowserCriClientOptions) {
158+
this.browserClient = browserClient
159+
this.versionInfo = versionInfo
160+
this.host = host
161+
this.port = port
162+
this.browserName = browserName
163+
this.onAsynchronousError = onAsynchronousError
164+
this.protocolManager = protocolManager
165+
this.fullyManageTabs = fullyManageTabs
166+
}
129167

130168
/**
131169
* Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the
@@ -140,7 +178,7 @@ export class BrowserCriClient {
140178
* @param fullyManageTabs whether or not to fully manage tabs. This is useful for firefox where some work is done with marionette and some with CDP. We don't want to handle disconnections in this class in those scenarios
141179
* @returns a wrapper around the chrome remote interface that is connected to the browser target
142180
*/
143-
static async create (hosts: string[], port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void, protocolManager?: ProtocolManagerShape, { fullyManageTabs }: { fullyManageTabs?: boolean } = {}): Promise<BrowserCriClient> {
181+
static async create ({ hosts, port, browserName, onAsynchronousError, onReconnect, protocolManager, fullyManageTabs }: BrowserCriClientCreateOptions): Promise<BrowserCriClient> {
144182
const host = await ensureLiveBrowser(hosts, port, browserName)
145183

146184
return retryWithIncreasingDelay(async () => {
@@ -151,11 +189,26 @@ export class BrowserCriClient {
151189
onAsynchronousError,
152190
onReconnect,
153191
protocolManager,
192+
fullyManageTabs,
154193
})
155194

156-
const browserCriClient = new BrowserCriClient(browserClient, versionInfo, host!, port, browserName, onAsynchronousError, protocolManager)
195+
const browserCriClient = new BrowserCriClient({ browserClient, versionInfo, host, port, browserName, onAsynchronousError, protocolManager, fullyManageTabs })
157196

158197
if (fullyManageTabs) {
198+
// The basic approach here is we attach to targets and enable network traffic
199+
// We must attach in a paused state so that we can enable network traffic before the target starts running.
200+
browserClient.on('Target.attachedToTarget', async (event) => {
201+
if (event.targetInfo.type !== 'page') {
202+
await browserClient.send('Network.enable', protocolManager?.networkEnableOptions ?? DEFAULT_NETWORK_ENABLE_OPTIONS, event.sessionId)
203+
}
204+
205+
if (event.waitingForDebugger) {
206+
await browserClient.send('Runtime.runIfWaitingForDebugger', undefined, event.sessionId)
207+
}
208+
})
209+
210+
// Ideally we could use filter rather than checking the type above, but that was added relatively recently
211+
await browserClient.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true })
159212
await browserClient.send('Target.setDiscoverTargets', { discover: true })
160213
browserClient.on('Target.targetDestroyed', (event) => {
161214
debug('Target.targetDestroyed %o', {
@@ -270,7 +323,8 @@ export class BrowserCriClient {
270323
throw new Error(`Could not find url target in browser ${url}. Targets were ${JSON.stringify(targets)}`)
271324
}
272325

273-
this.currentlyAttachedTarget = await create({ target: target.targetId, onAsynchronousError: this.onAsynchronousError, host: this.host, port: this.port, protocolManager: this.protocolManager })
326+
this.currentlyAttachedTarget = await create({ target: target.targetId, onAsynchronousError: this.onAsynchronousError, host: this.host, port: this.port, protocolManager: this.protocolManager, fullyManageTabs: this.fullyManageTabs, browserClient: this.browserClient })
327+
274328
await this.protocolManager?.connectToBrowser(this.currentlyAttachedTarget)
275329

276330
return this.currentlyAttachedTarget
@@ -323,6 +377,7 @@ export class BrowserCriClient {
323377
host: this.host,
324378
port: this.port,
325379
protocolManager: this.protocolManager,
380+
fullyManageTabs: this.fullyManageTabs,
326381
})
327382
} else {
328383
this.currentlyAttachedTarget = undefined

packages/server/lib/browsers/cdp_automation.ts

+3-13
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { ResourceType, BrowserPreRequest, BrowserResponseReceived } from '@
1313
import type { CDPClient, ProtocolManagerShape, WriteVideoFrame } from '@packages/types'
1414
import type { Automation } from '../automation'
1515
import { cookieMatches, CyCookie, CyCookieFilter } from '../automation/util'
16-
import type { CriClient } from './cri-client'
16+
import { DEFAULT_NETWORK_ENABLE_OPTIONS, CriClient } from './cri-client'
1717

1818
export type CdpCommand = keyof ProtocolMapping.Commands
1919

@@ -140,7 +140,7 @@ export const normalizeResourceType = (resourceType: string | undefined): Resourc
140140
return ffToStandardResourceTypeMap[resourceType] || 'other'
141141
}
142142

143-
export type SendDebuggerCommand = <T extends CdpCommand>(message: T, data?: ProtocolMapping.Commands[T]['paramsType'][0]) => Promise<ProtocolMapping.Commands[T]['returnType']>
143+
export type SendDebuggerCommand = <T extends CdpCommand>(message: T, data?: ProtocolMapping.Commands[T]['paramsType'][0], sessionId?: string) => Promise<ProtocolMapping.Commands[T]['returnType']>
144144

145145
export type OnFn = <T extends CdpEvent>(eventName: T, cb: (data: ProtocolMapping.Events[T][0]) => void) => void
146146

@@ -198,17 +198,7 @@ export class CdpAutomation implements CDPClient {
198198
static async create (sendDebuggerCommandFn: SendDebuggerCommand, onFn: OnFn, offFn: OffFn, sendCloseCommandFn: SendCloseCommand, automation: Automation, protocolManager?: ProtocolManagerShape): Promise<CdpAutomation> {
199199
const cdpAutomation = new CdpAutomation(sendDebuggerCommandFn, onFn, offFn, sendCloseCommandFn, automation)
200200

201-
const networkEnabledOptions = protocolManager?.protocolEnabled ? {
202-
maxTotalBufferSize: 0,
203-
maxResourceBufferSize: 0,
204-
maxPostDataSize: 64 * 1024,
205-
} : {
206-
maxTotalBufferSize: 0,
207-
maxResourceBufferSize: 0,
208-
maxPostDataSize: 0,
209-
}
210-
211-
await sendDebuggerCommandFn('Network.enable', networkEnabledOptions)
201+
await sendDebuggerCommandFn('Network.enable', protocolManager?.networkEnableOptions ?? DEFAULT_NETWORK_ENABLE_OPTIONS)
212202

213203
return cdpAutomation
214204
}

packages/server/lib/browsers/chrome.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ export = {
473473
debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port })
474474
if (!options.onError) throw new Error('Missing onError in connectToExisting')
475475

476-
const browserCriClient = await BrowserCriClient.create(['127.0.0.1'], port, browser.displayName, options.onError, onReconnect, undefined, { fullyManageTabs: false })
476+
const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1'], port, browserName: browser.displayName, onAsynchronousError: options.onError, onReconnect, fullyManageTabs: false })
477477

478478
if (!options.url) throw new Error('Missing url in connectToExisting')
479479

@@ -488,7 +488,11 @@ export = {
488488
const browserCriClient = this._getBrowserCriClient()
489489

490490
// Handle chrome tab crashes.
491-
pageCriClient.on('Inspector.targetCrashed', async () => {
491+
pageCriClient.on('Target.targetCrashed', async (event) => {
492+
if (event.targetId !== browserCriClient?.currentlyAttachedTarget?.targetId) {
493+
return
494+
}
495+
492496
const err = errors.get('RENDERER_CRASHED', browser.displayName)
493497

494498
await memory.endProfiling()
@@ -597,7 +601,7 @@ export = {
597601
// navigate to the actual url
598602
if (!options.onError) throw new Error('Missing onError in chrome#open')
599603

600-
browserCriClient = await BrowserCriClient.create(['127.0.0.1'], port, browser.displayName, options.onError, onReconnect, options.protocolManager, { fullyManageTabs: true })
604+
browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1'], port, browserName: browser.displayName, onAsynchronousError: options.onError, onReconnect, protocolManager: options.protocolManager, fullyManageTabs: true })
601605

602606
la(browserCriClient, 'expected Chrome remote interface reference', browserCriClient)
603607

packages/server/lib/browsers/cri-client.ts

+63-33
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ type EnqueuedCommand = {
2828
command: CdpCommand
2929
params?: object
3030
p: DeferredPromise
31+
sessionId?: string
3132
}
3233

3334
type EnableCommand = {
3435
command: CdpCommand
3536
params?: object
37+
sessionId?: string
3638
}
3739

3840
type Subscription = {
@@ -45,6 +47,12 @@ interface CDPClient extends CDP.Client {
4547
_ws: WebSocket
4648
}
4749

50+
export const DEFAULT_NETWORK_ENABLE_OPTIONS = {
51+
maxTotalBufferSize: 0,
52+
maxResourceBufferSize: 0,
53+
maxPostDataSize: 0,
54+
}
55+
4856
export interface CriClient {
4957
/**
5058
* The target id attached to by this client
@@ -138,6 +146,8 @@ type CreateParams = {
138146
port?: number
139147
onReconnect?: (client: CriClient) => void
140148
protocolManager?: ProtocolManagerShape
149+
fullyManageTabs?: boolean
150+
browserClient?: CriClient
141151
}
142152

143153
export const create = async ({
@@ -147,6 +157,8 @@ export const create = async ({
147157
port,
148158
onReconnect,
149159
protocolManager,
160+
fullyManageTabs,
161+
browserClient,
150162
}: CreateParams): Promise<CriClient> => {
151163
const subscriptions: Subscription[] = []
152164
const enableCommands: EnableCommand[] = []
@@ -183,12 +195,12 @@ export const create = async ({
183195

184196
// '*.enable' commands need to be resent on reconnect or any events in
185197
// that namespace will no longer be received
186-
await Promise.all(enableCommands.map(({ command, params }) => {
187-
return cri.send(command, params)
198+
await Promise.all(enableCommands.map(({ command, params, sessionId }) => {
199+
return cri.send(command, params, sessionId)
188200
}))
189201

190202
enqueuedCommands.forEach((cmd) => {
191-
cri.send(cmd.command, cmd.params).then(cmd.p.resolve as any, cmd.p.reject as any)
203+
cri.send(cmd.command, cmd.params, cmd.sessionId).then(cmd.p.resolve as any, cmd.p.reject as any)
192204
})
193205

194206
enqueuedCommands = []
@@ -258,35 +270,35 @@ export const create = async ({
258270
cri.on('disconnect', retryReconnect)
259271
}
260272

261-
cri.on('Inspector.targetCrashed', async () => {
262-
debug('crash detected')
263-
crashed = true
264-
})
265-
266-
// We only want to try and add service worker traffic if we have a host set. This indicates that this is the child cri client.
273+
// We only want to try and add child target traffic if we have a host set. This indicates that this is the child cri client.
274+
// Browser cri traffic is handled in browser-cri-client.ts. The basic approach here is we attach to targets and enable network traffic
275+
// We must attach in a paused state so that we can enable network traffic before the target starts running.
267276
if (host) {
268-
cri.on('Target.targetCreated', async (event) => {
269-
if (event.targetInfo.type === 'service_worker') {
270-
const networkEnabledOptions = protocolManager?.protocolEnabled ? {
271-
maxTotalBufferSize: 0,
272-
maxResourceBufferSize: 0,
273-
maxPostDataSize: 64 * 1024,
274-
} : {
275-
maxTotalBufferSize: 0,
276-
maxResourceBufferSize: 0,
277-
maxPostDataSize: 0,
278-
}
279-
280-
const { sessionId } = await cri.send('Target.attachToTarget', {
281-
targetId: event.targetInfo.targetId,
282-
flatten: true,
283-
})
284-
285-
await cri.send('Network.enable', networkEnabledOptions, sessionId)
277+
cri.on('Target.targetCrashed', async (event) => {
278+
if (event.targetId !== target) {
279+
return
286280
}
281+
282+
debug('crash detected')
283+
crashed = true
287284
})
288285

289-
await cri.send('Target.setDiscoverTargets', { discover: true })
286+
if (fullyManageTabs) {
287+
cri.on('Target.attachedToTarget', async (event) => {
288+
// Service workers get attached at the page and browser level. We only want to handle them at the browser level
289+
if (event.targetInfo.type !== 'service_worker' && event.targetInfo.type !== 'page') {
290+
await cri.send('Network.enable', protocolManager?.networkEnableOptions ?? DEFAULT_NETWORK_ENABLE_OPTIONS, event.sessionId)
291+
}
292+
293+
if (event.waitingForDebugger) {
294+
await cri.send('Runtime.runIfWaitingForDebugger', undefined, event.sessionId)
295+
}
296+
})
297+
298+
// Ideally we could use filter rather than checking the type above, but that was added relatively recently
299+
await cri.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true })
300+
await cri.send('Target.setDiscoverTargets', { discover: true })
301+
}
290302
}
291303
}
292304

@@ -295,7 +307,7 @@ export const create = async ({
295307
client = {
296308
targetId: target,
297309

298-
async send (command: CdpCommand, params?: object) {
310+
async send (command: CdpCommand, params?: object, sessionId?: string) {
299311
if (crashed) {
300312
return Promise.reject(new Error(`${command} will not run as the target browser or tab CRI connection has crashed`))
301313
}
@@ -313,6 +325,10 @@ export const create = async ({
313325
obj.params = params
314326
}
315327

328+
if (sessionId) {
329+
obj.sessionId = sessionId
330+
}
331+
316332
enqueuedCommands.push(obj)
317333
})
318334
}
@@ -328,12 +344,16 @@ export const create = async ({
328344
obj.params = params
329345
}
330346

347+
if (sessionId) {
348+
obj.sessionId = sessionId
349+
}
350+
331351
enableCommands.push(obj)
332352
}
333353

334354
if (connected) {
335355
try {
336-
return await cri.send(command, params)
356+
return await cri.send(command, params, sessionId)
337357
} catch (err) {
338358
// This error occurs when the browser has been left open for a long
339359
// time and/or the user's computer has been put to sleep. The
@@ -343,7 +363,7 @@ export const create = async ({
343363
throw err
344364
}
345365

346-
debug('encountered closed websocket on send %o', { command, params, err })
366+
debug('encountered closed websocket on send %o', { command, params, sessionId, err })
347367

348368
const p = enqueue() as Promise<any>
349369

@@ -367,15 +387,25 @@ export const create = async ({
367387
subscriptions.push({ eventName, cb })
368388
debug('registering CDP on event %o', { eventName })
369389

370-
return cri.on(eventName, cb)
390+
cri.on(eventName, cb)
391+
// This ensures that we are notified about the browser's network events that have been registered (e.g. service workers)
392+
// Long term we should use flat mode entirely across all of chrome remote interface
393+
if (eventName.startsWith('Network.')) {
394+
browserClient?.on(eventName, cb)
395+
}
371396
},
372397

373398
off (eventName, cb) {
374399
subscriptions.splice(subscriptions.findIndex((sub) => {
375400
return sub.eventName === eventName && sub.cb === cb
376401
}), 1)
377402

378-
return cri.off(eventName, cb)
403+
cri.off(eventName, cb)
404+
// This ensures that we are notified about the browser's network events that have been registered (e.g. service workers)
405+
// Long term we should use flat mode entirely across all of chrome remote interface
406+
if (eventName.startsWith('Network.')) {
407+
browserClient?.off(eventName, cb)
408+
}
379409
},
380410

381411
get ws () {

0 commit comments

Comments
 (0)