Skip to content

Commit

Permalink
chore: gracefully handle CDP address not returning from webdriver cap…
Browse files Browse the repository at this point in the history
…abilities, prompting a browser relaunch (#30393)

* chore: gracefully handle CDP address not returning from webdriver capabilities, prompting a browser relaunch

Update packages/server/lib/browsers/firefox.ts

add inline comment to github comment thread [run ci]

strongly type errors more consistently [run ci]

* add test for firefox cdp error for visual snapshot [run ci]
  • Loading branch information
AtofStryker authored Oct 19, 2024
1 parent c420d9c commit 0533307
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 17 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ export const AllCypressErrors = {
TESTS_DID_NOT_START_RETRYING: (arg1: string) => {
return errTemplate`Timed out waiting for the browser to connect. ${fmt.off(arg1)}`
},
FIREFOX_CDP_FAILED_TO_CONNECT: (arg1: string) => {
return errTemplate`Failed to spawn CDP with Firefox. ${fmt.off(arg1)}`
},
TESTS_DID_NOT_START_FAILED: () => {
return errTemplate`The browser never connected. Something is wrong. The tests cannot run. Aborting...`
},
Expand Down
5 changes: 5 additions & 0 deletions packages/errors/test/unit/visualSnapshotErrors_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,11 @@ describe('visual error templates', () => {
retryingAgain: ['Retrying again...'],
}
},
FIREFOX_CDP_FAILED_TO_CONNECT: () => {
return {
default: ['Retrying...'],
}
},
TESTS_DID_NOT_START_FAILED: () => {
return {
default: [],
Expand Down
35 changes: 30 additions & 5 deletions packages/server/lib/browsers/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,24 @@ import mimeDb from 'mime-db'
import type { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
import { getCtx } from '@packages/data-context'
import { getError } from '@packages/errors'
import { getError, SerializedError } from '@packages/errors'
import type { BrowserLaunchOpts, BrowserNewTabOpts, RunModeVideoApi } from '@packages/types'
import type { RemoteConfig } from 'webdriver'
import type { GeckodriverParameters } from 'geckodriver'
import { WebDriver } from './webdriver'

export class CDPFailedToStartFirefox extends Error {
private static readonly ERROR_NAME = 'CDPFailedToStartFirefox'
constructor (message) {
super(message)
this.name = CDPFailedToStartFirefox.ERROR_NAME
}

public static isCDPFailedToStartFirefoxError (error?: SerializedError): error is CDPFailedToStartFirefox {
return error?.name === CDPFailedToStartFirefox.ERROR_NAME
}
}

const debug = Debug('cypress:server:browsers:firefox')
const debugVerbose = Debug('cypress-verbose:server:browsers:firefox')

Expand Down Expand Up @@ -623,6 +635,8 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
},
// @see https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-debuggeraddress
// we specify the debugger address option for Webdriver, which will return us the CDP address when the capability is returned.
// NOTE: this typing is fixed in @wdio/types 9.1.0 https://github.com/webdriverio/webdriverio/commit/ed14717ac4269536f9e7906e4d1612f74650b09b
// Once we have a node engine that can support the package (i.e., electron 32+ update) we can update the package
// @ts-expect-error
'moz:debuggerAddress': true,
// @see https://webdriver.io/docs/capabilities/#wdiogeckodriveroptions
Expand All @@ -645,10 +659,6 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc

debugVerbose(`received capabilities %o`, webdriverClient.capabilities)

const cdpPort = parseInt(new URL(`ws://${webdriverClient.capabilities['moz:debuggerAddress']}`).port)

debug(`CDP running on port ${cdpPort}`)

const browserPID: number = webdriverClient.capabilities['moz:processID']

debug(`firefox running on pid: ${browserPID}`)
Expand Down Expand Up @@ -676,6 +686,21 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
return browserReturnStatus || driverReturnStatus
}

// In some cases, the webdriver session will NOT return the moz:debuggerAddress capability even though
// we set it to true in the capabilities. This is out of our control, so when this happens, we fail the browser
// and gracefully terminate the related processes and attempt to relaunch the browser in the hopes we get a
// CDP address. @see https://github.com/cypress-io/cypress/issues/30352#issuecomment-2405701867 for more details.
if (!webdriverClient.capabilities['moz:debuggerAddress']) {
debug(`firefox failed to spawn with CDP connection. Failing current instance and retrying`)
// since this fails before the instance is created, we need to kill the processes here or else they will stay open
browserInstanceWrapper.kill()
throw new CDPFailedToStartFirefox(`webdriver session failed to start CDP even though "moz:debuggerAddress" was provided. Please try to relaunch the browser`)
}

const cdpPort = parseInt(new URL(`ws://${webdriverClient.capabilities['moz:debuggerAddress']}`).port)

debug(`CDP running on port ${cdpPort}`)

// makes it so get getRemoteDebuggingPort() is calculated correctly
process.env.CYPRESS_REMOTE_DEBUGGING_PORT = cdpPort.toString()

Expand Down
48 changes: 36 additions & 12 deletions packages/server/lib/modes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type { ProtocolManager } from '../cloud/protocol'
import { telemetry } from '@packages/telemetry'
import { CypressRunResult, createPublicBrowser, createPublicConfig, createPublicRunResults, createPublicSpec, createPublicSpecResults } from './results'
import { EarlyExitTerminator } from '../util/graceful_crash_handling'
import { CDPFailedToStartFirefox } from '../browsers/firefox'
import type { CypressError } from '@packages/errors'

type SetScreenshotMetadata = (data: TakeScreenshotProps) => void
type ScreenshotMetadata = ReturnType<typeof screenshotMetadata>
Expand Down Expand Up @@ -497,17 +499,7 @@ async function waitForBrowserToConnect (options: { project: Project, socketId: s
coreData.didBrowserPreviouslyHaveUnexpectedExit = false
}

return Bluebird.all([
waitForSocketConnection(project, socketId),
// TODO: remove the need to extend options and coerce this type
launchBrowser(options as typeof options & { setScreenshotMetadata: SetScreenshotMetadata }),
])
.timeout(browserTimeout)
.then(() => {
telemetry.getSpan(`waitForBrowserToConnect:attempt:${browserLaunchAttempt}`)?.end()
})
.catch(Bluebird.TimeoutError, async (err) => {
debug('Catch on waitForBrowserToConnect')
async function retryOnError (err: CypressError) {
telemetry.getSpan(`waitForBrowserToConnect:attempt:${browserLaunchAttempt}`)?.end()
console.log('')

Expand All @@ -519,7 +511,12 @@ async function waitForBrowserToConnect (options: { project: Project, socketId: s
// try again up to 3 attempts
const word = browserLaunchAttempt === 1 ? 'Retrying...' : 'Retrying again...'

errors.warning('TESTS_DID_NOT_START_RETRYING', word)
if (CDPFailedToStartFirefox.isCDPFailedToStartFirefoxError(err?.originalError)) {
errors.warning('FIREFOX_CDP_FAILED_TO_CONNECT', word)
} else {
errors.warning('TESTS_DID_NOT_START_RETRYING', word)
}

browserLaunchAttempt += 1

return await wait()
Expand All @@ -529,6 +526,33 @@ async function waitForBrowserToConnect (options: { project: Project, socketId: s
errors.log(err)

onError(err)
}

return Bluebird.all([
waitForSocketConnection(project, socketId),
// TODO: remove the need to extend options and coerce this type
launchBrowser(options as typeof options & { setScreenshotMetadata: SetScreenshotMetadata }),
]).catch((e: CypressError) => {
// if the error wrapped is a CDPFailedToStartFirefox, try to relaunch the browser
if (CDPFailedToStartFirefox.isCDPFailedToStartFirefoxError(e?.originalError)) {
// if CDP fails to connect, which is ultimately out of our control and in the hands of webdriver
// we retry launching the browser in the hopes the session is spawned correctly
debug(`Caught in launchBrowser: ${e.details}`)

return retryOnError(e)
}

// otherwise, fail
throw e
})
.timeout(browserTimeout)
.then(() => {
telemetry.getSpan(`waitForBrowserToConnect:attempt:${browserLaunchAttempt}`)?.end()
})
.catch(Bluebird.TimeoutError, async (err) => {
debug('Catch on waitForBrowserToConnect')

return retryOnError(err as CypressError)
})
}

Expand Down
16 changes: 16 additions & 0 deletions packages/server/test/unit/browsers/firefox_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,22 @@ describe('lib/browsers/firefox', () => {
// makes sure the exit event is called to signal to the rest of cypress server that the processes are killed
expect(instance.emit).to.have.been.calledWith('exit')
})

it('throws CDPFailedToStartFirefox if the mox:debuggerAddress capability is not returned by webdriver', function () {
delete wdInstance.capabilities['moz:debuggerAddress']
sinon.stub(process, 'kill').returns(true)

return firefox.open(this.browser, 'http://', this.options, this.automation).catch((err) => {
// make sure we through the correct error here to prompt @packages/server/lib/modes/run.ts
// to retry the browser connection
expect(err.details).to.include('CDPFailedToStartFirefox: webdriver session failed to start CDP even though "moz:debuggerAddress" was provided. Please try to relaunch the browser')
expect(err.type).to.equal('FIREFOX_COULD_NOT_CONNECT')
// kills the browser
expect(process.kill).to.have.been.calledWith(1234)
// kills the webdriver process / geckodriver process
expect(process.kill).to.have.been.calledWith(5678)
})
})
})
})

Expand Down

5 comments on commit 0533307

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0533307 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.15.1/linux-x64/develop-05333076c66a4f623237202ad6fcc289cbf10c23/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0533307 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.15.1/linux-arm64/develop-05333076c66a4f623237202ad6fcc289cbf10c23/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0533307 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.15.1/darwin-arm64/develop-05333076c66a4f623237202ad6fcc289cbf10c23/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0533307 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.15.1/darwin-x64/develop-05333076c66a4f623237202ad6fcc289cbf10c23/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0533307 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.15.1/win32-x64/develop-05333076c66a4f623237202ad6fcc289cbf10c23/cypress.tgz

Please sign in to comment.