Skip to content

Commit d0d1736

Browse files
committed
fix: use stdio for CDP instead of TCP (#14348)
1 parent de8e6c3 commit d0d1736

File tree

6 files changed

+197
-58
lines changed

6 files changed

+197
-58
lines changed

packages/launcher/lib/browsers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function launch (
107107
browser: FoundBrowser,
108108
url: string,
109109
args: string[] = [],
110+
opts: { pipeStdio?: boolean } = {},
110111
) {
111112
log('launching browser %o', { browser, url })
112113

@@ -120,7 +121,15 @@ export function launch (
120121

121122
log('spawning browser with args %o', { args })
122123

123-
const proc = cp.spawn(browser.path, args, { stdio: ['ignore', 'pipe', 'pipe'] })
124+
const stdio = ['ignore', 'pipe', 'pipe']
125+
126+
if (opts.pipeStdio) {
127+
// also pipe stdio 3 and 4 for access to debugger protocol
128+
stdio.push('pipe', 'pipe')
129+
}
130+
131+
// @ts-ignore
132+
const proc = cp.spawn(browser.path, args, { stdio })
124133

125134
proc.stdout.on('data', (buf) => {
126135
log('%s stdout: %s', browser.name, String(buf).trim())

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

Lines changed: 100 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Bluebird from 'bluebird'
22
import debugModule from 'debug'
33
import _ from 'lodash'
4+
import { ChildProcess } from 'child_process'
45

56
const chromeRemoteInterface = require('chrome-remote-interface')
67
const errors = require('../errors')
@@ -85,41 +86,40 @@ const getMajorMinorVersion = (version: string): Version => {
8586

8687
const maybeDebugCdpMessages = (cri) => {
8788
if (debugVerboseReceive.enabled) {
88-
cri._ws.on('message', (data) => {
89-
data = _
90-
.chain(JSON.parse(data))
91-
.tap((data) => {
92-
([
93-
'params.data', // screencast frame data
94-
'result.data', // screenshot data
95-
]).forEach((truncatablePath) => {
96-
const str = _.get(data, truncatablePath)
97-
98-
if (!_.isString(str)) {
99-
return
100-
}
89+
const handleMessage = cri._handleMessage
10190

102-
_.set(data, truncatablePath, _.truncate(str, {
103-
length: 100,
104-
omission: `... [truncated string of total bytes: ${str.length}]`,
105-
}))
106-
})
91+
cri._handleMessage = (message) => {
92+
const formatted = _.cloneDeep(message)
93+
94+
;([
95+
'params.data', // screencast frame data
96+
'result.data', // screenshot data
97+
]).forEach((truncatablePath) => {
98+
const str = _.get(formatted, truncatablePath)
99+
100+
if (!_.isString(str)) {
101+
return
102+
}
107103

108-
return data
104+
_.set(formatted, truncatablePath, _.truncate(str, {
105+
length: 100,
106+
omission: `... [truncated string of total bytes: ${str.length}]`,
107+
}))
109108
})
110-
.value()
111109

112-
debugVerboseReceive('received CDP message %o', data)
113-
})
110+
debugVerboseReceive('received CDP message %o', formatted)
111+
112+
return handleMessage.call(cri, message)
113+
}
114114
}
115115

116116
if (debugVerboseSend.enabled) {
117-
const send = cri._ws.send
117+
const send = cri._send
118118

119-
cri._ws.send = (data, callback) => {
119+
cri._send = (data, callback) => {
120120
debugVerboseSend('sending CDP command %o', JSON.parse(data))
121121

122-
return send.call(cri._ws, data, callback)
122+
return send.call(cri, data, callback)
123123
}
124124
}
125125
}
@@ -135,17 +135,36 @@ export { chromeRemoteInterface }
135135

136136
type DeferredPromise = { resolve: Function, reject: Function }
137137

138-
export const create = Bluebird.method((target: websocketUrl, onAsynchronousError: Function): Bluebird<CRIWrapper> => {
138+
type CreateOpts = {
139+
target?: websocketUrl
140+
process?: ChildProcess
141+
}
142+
143+
type Message = {
144+
method: CRI.Command
145+
params?: any
146+
sessionId?: string
147+
}
148+
149+
export const create = Bluebird.method((opts: CreateOpts, onAsynchronousError: Function): Bluebird<CRIWrapper> => {
139150
const subscriptions: {eventName: CRI.EventName, cb: Function}[] = []
140-
let enqueuedCommands: {command: CRI.Command, params: any, p: DeferredPromise }[] = []
151+
let enqueuedCommands: {message: Message, params: any, p: DeferredPromise }[] = []
141152

142153
let closed = false // has the user called .close on this?
143154
let connected = false // is this currently connected to CDP?
144155

145156
let cri
146157
let client: CRIWrapper
158+
let sessionId: string | undefined
147159

148160
const reconnect = () => {
161+
if (opts.process) {
162+
// reconnecting doesn't make sense for stdio
163+
onAsynchronousError(errors.get('CDP_STDIO_ERROR'))
164+
165+
return
166+
}
167+
149168
debug('disconnected, attempting to reconnect... %o', { closed })
150169

151170
connected = false
@@ -162,7 +181,7 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
162181
})
163182

164183
enqueuedCommands.forEach((cmd) => {
165-
cri.send(cmd.command, cmd.params)
184+
cri.sendRaw(cmd.message)
166185
.then(cmd.p.resolve, cmd.p.reject)
167186
})
168187

@@ -176,10 +195,10 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
176195
const connect = () => {
177196
cri?.close()
178197

179-
debug('connecting %o', { target })
198+
debug('connecting %o', opts)
180199

181200
return chromeRemoteInterface({
182-
target,
201+
...opts,
183202
local: true,
184203
})
185204
.then((newCri) => {
@@ -193,6 +212,46 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
193212

194213
// @see https://github.com/cyrus-and/chrome-remote-interface/issues/72
195214
cri._notifier.on('disconnect', reconnect)
215+
216+
if (opts.process) {
217+
// if using stdio, we need to find the target before declaring the connection complete
218+
return findTarget()
219+
}
220+
221+
return
222+
})
223+
}
224+
225+
const findTarget = () => {
226+
debug('finding CDP target...')
227+
228+
return new Bluebird<void>((resolve, reject) => {
229+
const isAboutBlank = (target) => target.type === 'page' && target.url === 'about:blank'
230+
231+
const attachToTarget = _.once(({ targetId }) => {
232+
debug('attaching to target %o', { targetId })
233+
cri.send('Target.attachToTarget', {
234+
targetId,
235+
flatten: true, // enable selecting via sessionId
236+
}).then((result) => {
237+
debug('attached to target %o', result)
238+
sessionId = result.sessionId
239+
resolve()
240+
}).catch(reject)
241+
})
242+
243+
cri.send('Target.setDiscoverTargets', { discover: true })
244+
.then(() => {
245+
cri.on('Target.targetCreated', (target) => {
246+
if (isAboutBlank(target)) {
247+
attachToTarget(target)
248+
}
249+
})
250+
251+
return cri.send('Target.getTargets')
252+
.then(({ targetInfos }) => targetInfos.filter(isAboutBlank).map(attachToTarget))
253+
})
254+
.catch(reject)
196255
})
197256
}
198257

@@ -222,14 +281,23 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
222281
ensureMinimumProtocolVersion,
223282
getProtocolVersion,
224283
send: Bluebird.method((command: CRI.Command, params?: object) => {
284+
const message: Message = {
285+
method: command,
286+
params,
287+
}
288+
289+
if (sessionId) {
290+
message.sessionId = sessionId
291+
}
292+
225293
const enqueue = () => {
226294
return new Bluebird((resolve, reject) => {
227-
enqueuedCommands.push({ command, params, p: { resolve, reject } })
295+
enqueuedCommands.push({ message, params, p: { resolve, reject } })
228296
})
229297
}
230298

231299
if (connected) {
232-
return cri.send(command, params)
300+
return cri.sendRaw(message)
233301
.catch((err) => {
234302
if (!WEBSOCKET_NOT_OPEN_RE.test(err.message)) {
235303
throw err

packages/server/lib/errors.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const chalk = require('chalk')
55
const AU = require('ansi_up')
66
const Promise = require('bluebird')
77
const { stripIndent } = require('./util/strip_indent')
8+
const humanTime = require('./util/human_time')
89

910
const ansi_up = new AU.default
1011

@@ -871,6 +872,12 @@ const getMsgByType = function (type, arg1 = {}, arg2, arg3) {
871872
There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser.
872873
873874
${arg1.stack}`
875+
case 'CDP_STDIO_ERROR':
876+
return 'The connection between Cypress and Chrome has unexpectedly ended. Please restart the browser.'
877+
case 'CDP_STDIO_TIMEOUT':
878+
return `Warning: Cypress failed to connect to ${arg1} via stdio after ${humanTime.long(arg2)}. Falling back to TCP...`
879+
case 'CDP_FALLBACK_SUCCEEDED':
880+
return `Connecting to ${arg1} via TCP was successful, continuing with tests.`
874881
case 'CDP_RETRYING_CONNECTION':
875882
return `Failed to connect to Chrome, retrying in 1 second (attempt ${chalk.yellow(arg1)}/62)`
876883
case 'DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS':

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"chalk": "2.4.2",
4040
"check-more-types": "2.24.0",
4141
"chokidar": "3.2.2",
42-
"chrome-remote-interface": "0.28.2",
42+
"chrome-remote-interface": "cypress-io/chrome-remote-interface#147192810f29951cd96c5e406495e9b4d740ba95",
4343
"cli-table3": "0.5.1",
4444
"coffeescript": "1.12.7",
4545
"color-string": "1.5.4",

0 commit comments

Comments
 (0)