appium 2.14.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/appium.js CHANGED
@@ -8,8 +8,11 @@ import {
8
8
  CREATE_SESSION_COMMAND,
9
9
  DELETE_SESSION_COMMAND,
10
10
  GET_STATUS_COMMAND,
11
+ LIST_DRIVER_COMMANDS_COMMAND,
12
+ LIST_DRIVER_EXTENSIONS_COMMAND,
11
13
  promoteAppiumOptions,
12
14
  promoteAppiumOptionsForObject,
15
+ generateDriverLogPrefix,
13
16
  } from '@appium/base-driver';
14
17
  import AsyncLock from 'async-lock';
15
18
  import {
@@ -18,10 +21,11 @@ import {
18
21
  makeNonW3cCapsError,
19
22
  validateFeatures,
20
23
  } from './utils';
21
- import {util, node, logger} from '@appium/support';
24
+ import {util} from '@appium/support';
22
25
  import {getDefaultsForExtension} from './schema';
23
26
  import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
24
- import * as bidiHelpers from './bidi';
27
+ import * as bidiCommands from './bidi-commands';
28
+ import * as inspectorCommands from './inspector-commands';
25
29
 
26
30
  const desiredCapabilityConstraints = /** @type {const} */ ({
27
31
  automationName: {
@@ -140,17 +144,6 @@ class AppiumDriver extends DriverCore {
140
144
  })();
141
145
  }
142
146
 
143
- /**
144
- * Retrieves logger instance for the current umbrella driver instance
145
- */
146
- get log() {
147
- if (!this._log) {
148
- const instanceName = `${this.constructor.name}@${node.getObjectId(this).substring(0, 4)}`;
149
- this._log = logger.getLogger(instanceName);
150
- }
151
- return this._log;
152
- }
153
-
154
147
  /**
155
148
  * Cancel commands queueing for the umbrella Appium driver
156
149
  */
@@ -202,6 +195,11 @@ class AppiumDriver extends DriverCore {
202
195
  }));
203
196
  }
204
197
 
198
+ async getAppiumSessions () {
199
+ throw new errors.NotImplementedError('Not implemented yet. ' +
200
+ 'Please check https://github.com/appium/appium/issues/20880 for more details.');
201
+ }
202
+
205
203
  printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
206
204
  this.log.info(
207
205
  driverVersion
@@ -343,7 +341,8 @@ class AppiumDriver extends DriverCore {
343
341
  }
344
342
 
345
343
  // We also want to assign any new Bidi Commands that the driver has specified, including all
346
- // the standard bidi commands
344
+ // the standard bidi commands. But add a method existence guard since some old driver class
345
+ // instances might not have this method
347
346
  if (_.isFunction(driverInstance.updateBidiCommands)) {
348
347
  driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
349
348
  }
@@ -412,7 +411,7 @@ class AppiumDriver extends DriverCore {
412
411
  );
413
412
 
414
413
  // set the New Command Timeout for the inner driver
415
- driverInstance.startNewCommandTimeout();
414
+ await driverInstance.startNewCommandTimeout();
416
415
 
417
416
  // apply initial values to Appium settings (if provided)
418
417
  if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
@@ -429,14 +428,13 @@ class AppiumDriver extends DriverCore {
429
428
  await driverInstance.updateSettings(jwpSettings);
430
429
  }
431
430
 
432
- // if the user has asked for bidi support, and the driver says that it supports bidi,
433
- // send our bidi url back to the user. The inner driver will need to have already saved any
434
- // internal bidi urls it might want to proxy to, cause we are going to overwrite that
435
- // information here!
436
- if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
431
+ // if the user has asked for bidi support, send our bidi url back to the user. The inner
432
+ // driver will need to have already saved any internal bidi urls it might want to proxy to,
433
+ // cause we are going to overwrite that information here!
434
+ if (dCaps.webSocketUrl) {
437
435
  const {address, port, basePath} = this.args;
438
436
  const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
439
- const host = bidiHelpers.determineBiDiHost(address);
437
+ const host = bidiCommands.determineBiDiHost(address);
440
438
  const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
441
439
  this.log.info(
442
440
  `Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
@@ -575,32 +573,6 @@ class AppiumDriver extends DriverCore {
575
573
  }
576
574
  }
577
575
 
578
- /**
579
- * @param {string} sessionId
580
- */
581
- cleanupBidiSockets(sessionId) {
582
- // clean up any bidi sockets associated with session
583
- if (this.bidiSockets[sessionId]) {
584
- try {
585
- this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
586
- for (const ws of this.bidiSockets[sessionId]) {
587
- // 1001 means server is going away
588
- ws.close(1001, 'Appium session is closing');
589
- }
590
- } catch {}
591
- delete this.bidiSockets[sessionId];
592
- const proxyClient = this.bidiProxyClients[sessionId];
593
- if (proxyClient) {
594
- this.log.debug(`Also closing proxy connection to upstream bidi server`);
595
- try {
596
- // 1000 means normal closure, which seems correct when Appium is acting as the client
597
- proxyClient.close(1000);
598
- } catch {}
599
- delete this.bidiProxyClients[sessionId];
600
- }
601
- }
602
- }
603
-
604
576
  async deleteAllSessions(opts = {}) {
605
577
  const sessionsCount = _.size(this.sessions);
606
578
  if (0 === sessionsCount) {
@@ -628,12 +600,13 @@ class AppiumDriver extends DriverCore {
628
600
  * Get the appropriate plugins for a session (or sessionless plugins)
629
601
  *
630
602
  * @param {?string} sessionId - the sessionId (or null) to use to find plugins
631
- * @returns {Array} - array of plugin instances
603
+ * @returns {Array<import('@appium/types').Plugin>} - array of plugin instances
632
604
  */
633
605
  pluginsForSession(sessionId = null) {
634
606
  if (sessionId) {
635
607
  if (!this.sessionPlugins[sessionId]) {
636
- this.sessionPlugins[sessionId] = this.createPluginInstances();
608
+ const driverId = generateDriverLogPrefix(this.sessions[sessionId]);
609
+ this.sessionPlugins[sessionId] = this.createPluginInstances(driverId || null);
637
610
  }
638
611
  return this.sessionPlugins[sessionId];
639
612
  }
@@ -664,14 +637,19 @@ class AppiumDriver extends DriverCore {
664
637
 
665
638
  /**
666
639
  * Creates instances of all of the enabled Plugin classes
640
+ * @param {string|null} driverId - ID to use for linking a driver to a plugin in logs
667
641
  * @returns {Plugin[]}
668
642
  */
669
- createPluginInstances() {
643
+ createPluginInstances(driverId = null) {
670
644
  /** @type {Plugin[]} */
671
645
  const pluginInstances = [];
672
646
  for (const [PluginClass, name] of this.pluginClasses.entries()) {
673
647
  const cliArgs = this.getCliArgsForPlugin(name);
674
- const plugin = new PluginClass(name, cliArgs);
648
+ const plugin = new PluginClass(name, cliArgs, driverId);
649
+ if (_.isFunction(/** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands)) {
650
+ // some old plugin classes don't have `updateBidiCommands`
651
+ /** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands(PluginClass.newBidiCommands ?? {});
652
+ }
675
653
  pluginInstances.push(plugin);
676
654
  }
677
655
  return pluginInstances;
@@ -832,6 +810,12 @@ class AppiumDriver extends DriverCore {
832
810
  `to session ID ${sessionId}`,
833
811
  );
834
812
  this.sessionPlugins[sessionId] = this.sessionlessPlugins;
813
+ for (const p of /** @type {(Plugin & ExtensionCore)[]} */(this.sessionPlugins[sessionId])) {
814
+ if (_.isFunction(p.updateLogPrefix)) {
815
+ // some old plugin classes don't have `updateLogPrefix` yet
816
+ p.updateLogPrefix(`${generateDriverLogPrefix(p)} <${generateDriverLogPrefix(this.sessions[sessionId])}>`);
817
+ }
818
+ }
835
819
  this.sessionlessPlugins = [];
836
820
  }
837
821
 
@@ -933,15 +917,28 @@ class AppiumDriver extends DriverCore {
933
917
  return dstSession && dstSession.canProxy(sessionId);
934
918
  }
935
919
 
936
- onBidiConnection = bidiHelpers.onBidiConnection;
937
- onBidiMessage = bidiHelpers.onBidiMessage;
938
- onBidiServerError = bidiHelpers.onBidiServerError;
920
+ onBidiConnection = bidiCommands.onBidiConnection;
921
+ onBidiMessage = bidiCommands.onBidiMessage;
922
+ onBidiServerError = bidiCommands.onBidiServerError;
923
+ cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
924
+
925
+ listCommands = inspectorCommands.listCommands;
926
+ listExtensions = inspectorCommands.listExtensions;
939
927
  }
940
928
 
941
- // help decide which commands should be proxied to sub-drivers and which
942
- // should be handled by this, our umbrella driver
929
+ /**
930
+ * Help decide which commands should be proxied to sub-drivers and which
931
+ * should be handled by this, our umbrella driver
932
+ * @param {string} cmd
933
+ * @returns {boolean}
934
+ */
943
935
  function isAppiumDriverCommand(cmd) {
944
- return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
936
+ return !isSessionCommand(cmd)
937
+ || _.includes([
938
+ DELETE_SESSION_COMMAND,
939
+ LIST_DRIVER_COMMANDS_COMMAND,
940
+ LIST_DRIVER_EXTENSIONS_COMMAND,
941
+ ], cmd);
945
942
  }
946
943
 
947
944
  /**
@@ -979,6 +976,7 @@ export {AppiumDriver};
979
976
  * @typedef {import('@appium/types').ExternalDriver} ExternalDriver
980
977
  * @typedef {import('@appium/types').PluginClass} PluginClass
981
978
  * @typedef {import('@appium/types').Plugin} Plugin
979
+ * @typedef {import('@appium/base-driver').ExtensionCore} ExtensionCore
982
980
  * @typedef {import('@appium/types').DriverClass<import('@appium/types').Driver>} DriverClass
983
981
  */
984
982
 
@@ -2,6 +2,7 @@ import _ from 'lodash';
2
2
  import B from 'bluebird';
3
3
  import {
4
4
  errors,
5
+ ExtensionCore,
5
6
  } from '@appium/base-driver';
6
7
  import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
7
8
  import WebSocket from 'ws';
@@ -17,14 +18,18 @@ import type {
17
18
  ErrorBiDiCommandResponse,
18
19
  SuccessBiDiCommandResponse,
19
20
  ExternalDriver,
20
- StringRecord
21
+ StringRecord,
22
+ Plugin,
23
+ BiDiResultData
21
24
  } from '@appium/types';
22
25
 
26
+ type ExtensionPlugin = Plugin & ExtensionCore
23
27
  type AnyDriver = ExternalDriver | AppiumDriver;
24
28
  type SendData = (data: string | Buffer) => Promise<void>;
25
29
  type LogSocketError = (err: Error) => void;
26
30
  interface InitBiDiSocketResult {
27
31
  bidiHandlerDriver: AnyDriver;
32
+ bidiHandlerPlugins: ExtensionPlugin[];
28
33
  proxyClient: WebSocket | null;
29
34
  send: SendData;
30
35
  sendToProxy: SendData | null;
@@ -64,7 +69,7 @@ export function determineBiDiHost(address: string): string {
64
69
  export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
65
70
  try {
66
71
  const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
67
- const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
72
+ const {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
68
73
  ws,
69
74
  req,
70
75
  );
@@ -77,6 +82,7 @@ export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: Incomin
77
82
  send,
78
83
  sendToProxy,
79
84
  bidiHandlerDriver,
85
+ bidiHandlerPlugins,
80
86
  logSocketErr,
81
87
  );
82
88
  if (proxyClient) {
@@ -86,7 +92,7 @@ export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: Incomin
86
92
  }
87
93
  const initBidiEventListenersFunc: OmitThisParameter<typeof initBidiEventListeners> = initBidiEventListeners
88
94
  .bind(this);
89
- initBidiEventListenersFunc(ws, bidiHandlerDriver, send);
95
+ initBidiEventListenersFunc(ws, bidiHandlerDriver, bidiHandlerPlugins, send);
90
96
  } catch (err) {
91
97
  this.log.error(err);
92
98
  try {
@@ -95,14 +101,25 @@ export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: Incomin
95
101
  }
96
102
  }
97
103
 
104
+ function wrapCommandWithPlugins(driver: ExtensionCore, plugins: ExtensionCore[], method: string, params: StringRecord): () => Promise<BiDiResultData> {
105
+ const [moduleName, methodName] = method.split('.');
106
+ let next = async () => await driver.executeBidiCommand(method, params);
107
+ for (const plugin of plugins.filter((p) => p.doesBidiCommandExist(moduleName, methodName))) {
108
+ next = ((_next) => async () => await plugin.executeBidiCommand(method, params, _next, driver))(next);
109
+ }
110
+ return next;
111
+ }
112
+
98
113
  /**
99
114
  * @param data
100
115
  * @param driver
116
+ * @param plugins
101
117
  */
102
118
  export async function onBidiMessage(
103
119
  this: AppiumDriver,
104
120
  data: Buffer,
105
- driver: AnyDriver
121
+ driver: AnyDriver,
122
+ plugins: ExtensionPlugin[]
106
123
  ): Promise<SuccessBiDiCommandResponse | ErrorBiDiCommandResponse> {
107
124
  let resMessage: SuccessBiDiCommandResponse | ErrorBiDiCommandResponse;
108
125
  let id: number = 0;
@@ -129,7 +146,8 @@ export async function onBidiMessage(
129
146
  `Missing params for BiDi operation in '${dataTruncated}`,
130
147
  );
131
148
  }
132
- const result = await driver.executeBidiCommand(method, params);
149
+ const executeWrappedCommand = wrapCommandWithPlugins(driver as ExtensionCore, plugins, method, params);
150
+ const result = await executeWrappedCommand();
133
151
  resMessage = {
134
152
  id,
135
153
  type: 'success',
@@ -158,6 +176,38 @@ export function onBidiServerError(this: AppiumDriver, err: Error): void {
158
176
  this.log.warn(`Error from bidi websocket server: ${err}`);
159
177
  }
160
178
 
179
+ /**
180
+ * Clean up any bidi sockets associated with session
181
+ *
182
+ * @param sessionId
183
+ */
184
+ export function cleanupBidiSockets(this: AppiumDriver, sessionId: string): void {
185
+ if (!this.bidiSockets[sessionId]) {
186
+ return;
187
+ }
188
+ try {
189
+ this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
190
+ for (const ws of this.bidiSockets[sessionId]) {
191
+ // 1001 means server is going away
192
+ ws.close(1001, 'Appium session is closing');
193
+ }
194
+ } catch {}
195
+ delete this.bidiSockets[sessionId];
196
+
197
+ const proxyClient = this.bidiProxyClients[sessionId];
198
+ if (!proxyClient) {
199
+ return;
200
+ }
201
+ this.log.debug(`Also closing proxy connection to upstream bidi server`);
202
+ try {
203
+ // 1000 means normal closure, which seems correct when Appium is acting as the client
204
+ proxyClient.close(1000);
205
+ } catch {}
206
+ delete this.bidiProxyClients[sessionId];
207
+ }
208
+
209
+ // #region Private functions
210
+
161
211
  /**
162
212
  * Initialize a new bidi connection
163
213
  * @param ws The websocket connection object
@@ -186,6 +236,7 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
186
236
 
187
237
  let bidiHandlerDriver: AnyDriver;
188
238
  let proxyClient: WebSocket | null = null;
239
+ const bidiHandlerPlugins: ExtensionPlugin[] = [];
189
240
  if (sessionMatch) {
190
241
  // If we found a session id, see if it matches an active session
191
242
  const sessionId = sessionMatch[1];
@@ -217,15 +268,19 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
217
268
  `url ${bidiProxyUrl}, but this was not a valid url`,
218
269
  );
219
270
  }
220
- this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
271
+ this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}. ` +
272
+ `Plugins will not handle bidi commands`);
221
273
  proxyClient = new WebSocket(bidiProxyUrl);
222
274
  this.bidiProxyClients[sessionId] = proxyClient;
275
+ } else {
276
+ bidiHandlerPlugins.push(...this.pluginsForSession(sessionId) as ExtensionPlugin[]);
223
277
  }
224
278
  } else {
225
279
  this.log.info('Bidi websocket connection made to main server');
226
280
  // no need to store the socket connection if it's to the main server since it will just
227
281
  // stay open as long as the server itself is and will close when the server closes.
228
282
  bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
283
+ bidiHandlerPlugins.push(...this.pluginsForSession() as ExtensionPlugin[]);
229
284
  }
230
285
 
231
286
  const driverLog = bidiHandlerDriver.log;
@@ -240,6 +295,7 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
240
295
  const socketSend = B.promisify(socket.send, {context: socket});
241
296
  return async (data: string | Buffer) => {
242
297
  try {
298
+ await assertIsOpen(socket);
243
299
  await socketSend(data);
244
300
  } catch (err) {
245
301
  logSocketErr(err);
@@ -254,7 +310,7 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
254
310
  // bidi socket server (e.g. on a browser)
255
311
  const sendToProxy: SendData | null = proxyClient ? sendFactory(proxyClient) : null;
256
312
 
257
- return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
313
+ return {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr};
258
314
  }
259
315
 
260
316
  /**
@@ -315,6 +371,7 @@ function initBidiProxyHandlers(
315
371
  * upstream socket
316
372
  * @param bidiHandlerDriver - the driver
317
373
  * handling the bidi commands
374
+ * @param bidiHandlerPlugins - plugins that might also handle bidi commands
318
375
  * @param logSocketErr - a special prefixed logger
319
376
  */
320
377
  function initBidiSocketHandlers(
@@ -324,6 +381,7 @@ function initBidiSocketHandlers(
324
381
  send: SendData,
325
382
  sendToProxy: SendData | null,
326
383
  bidiHandlerDriver: AnyDriver,
384
+ bidiHandlerPlugins: ExtensionPlugin[],
327
385
  logSocketErr: LogSocketError,
328
386
  ): void {
329
387
  const driverLog = bidiHandlerDriver.log;
@@ -340,9 +398,11 @@ function initBidiSocketHandlers(
340
398
  ws.on('message', async (data: Buffer) => {
341
399
  if (proxyClient && sendToProxy) {
342
400
  // if we're meant to proxy to an upstream bidi socket, just do that
401
+ // TODO trying to determine how this proxying behaviour would interface with plugins is too
402
+ // complex for now, so just ignore plugins in this case
343
403
  await sendToProxy(data.toString('utf8'));
344
404
  } else {
345
- const res = await this.onBidiMessage(data, bidiHandlerDriver);
405
+ const res = await this.onBidiMessage(data, bidiHandlerDriver, bidiHandlerPlugins);
346
406
  await send(JSON.stringify(res));
347
407
  }
348
408
  });
@@ -381,56 +441,103 @@ function initBidiEventListeners(
381
441
  this: AppiumDriver,
382
442
  ws: WebSocket,
383
443
  bidiHandlerDriver: AnyDriver,
444
+ bidiHandlerPlugins: ExtensionPlugin[],
384
445
  send: SendData,
385
446
  ): void {
386
447
  // If the driver emits a bidi event that should maybe get sent to the client, check to make
387
448
  // sure the client is subscribed and then pass it on
388
- const driverLog = bidiHandlerDriver.log;
389
- const driverEe = bidiHandlerDriver.eventEmitter;
390
449
  const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
391
450
  BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
392
- const eventListener = async ({context, method, params = {}}) => {
393
- // if the driver didn't specify a context, use the empty context
394
- if (!context) {
395
- context = '';
396
- }
397
- if (!method || !params) {
398
- driverLog.warn(
399
- `Driver emitted a bidi event that was malformed. Require method and params keys ` +
400
- `(with optional context). But instead received: ${_.truncate(JSON.stringify({
401
- context,
402
- method,
403
- params,
404
- }), {length: MAX_LOGGED_DATA_LENGTH})}`,
405
- );
406
- return;
407
- }
408
- if (ws.readyState !== WebSocket.OPEN) {
409
- // if the websocket is not still 'open', then we can ignore sending these events
410
- if (ws.readyState > WebSocket.OPEN) {
411
- // if the websocket is closed or closing, we can remove this listener as well to avoid
412
- // leaks
413
- driverEe.removeListener(BIDI_EVENT_NAME, eventListener);
451
+ const eventListenerFactory = (extType: 'driver'|'plugin', ext: ExtensionCore) => {
452
+ const eventListener = async ({context, method, params = {}}) => {
453
+ // if the driver didn't specify a context, use the empty context
454
+ if (!context) {
455
+ context = '';
414
456
  }
415
- return;
416
- }
417
-
418
- const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
419
- if (_.isArray(eventSubs) && eventSubs.includes(context)) {
420
- if (method in eventLogCounts) {
421
- ++eventLogCounts[method];
422
- } else {
423
- driverLog.info(
424
- `<-- BIDI EVENT ${method} (context: '${context}', ` +
425
- `params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
426
- `All further similar events won't be logged.`,
457
+ if (!method || !params) {
458
+ ext.log?.warn( // some old plugins might not have the `log` property
459
+ `${_.capitalize(extType)} emitted a bidi event that was malformed. Require method and params keys ` +
460
+ `(with optional context). But instead received: ${_.truncate(JSON.stringify({
461
+ context,
462
+ method,
463
+ params,
464
+ }), {length: MAX_LOGGED_DATA_LENGTH})}`,
427
465
  );
428
- eventLogCounts[method] = 1;
466
+ return;
429
467
  }
430
- // now we can send the event onto the socket
431
- const ev = {type: 'event', context, method, params};
432
- await send(JSON.stringify(ev));
433
- }
468
+ if (ws.readyState !== WebSocket.OPEN) {
469
+ // if the websocket is not still 'open', then we can ignore sending these events
470
+ if (ws.readyState > WebSocket.OPEN) {
471
+ // if the websocket is closed or closing, we can remove this listener as well to avoid
472
+ // leaks. Some old plugin classes might not have the `eventEmitter` property, so use an
473
+ // existence guard for now.
474
+ ext.eventEmitter?.removeListener(BIDI_EVENT_NAME, eventListener);
475
+ }
476
+ return;
477
+ }
478
+
479
+ const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
480
+ if (_.isArray(eventSubs) && eventSubs.includes(context)) {
481
+ if (method in eventLogCounts) {
482
+ ++eventLogCounts[method];
483
+ } else {
484
+ ext.log?.info( // some old plugins might not have the `log` property
485
+ `<-- BIDI EVENT ${method} (context: '${context}', ` +
486
+ `params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
487
+ `All further similar events won't be logged.`,
488
+ );
489
+ eventLogCounts[method] = 1;
490
+ }
491
+ // now we can send the event onto the socket
492
+ const ev = {type: 'event', context, method, params};
493
+ await send(JSON.stringify(ev));
494
+ }
495
+ };
496
+ return eventListener;
434
497
  };
435
- driverEe.on(BIDI_EVENT_NAME, eventListener);
498
+ bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListenerFactory('driver', bidiHandlerDriver as ExtensionCore));
499
+ for (const plugin of bidiHandlerPlugins) {
500
+ // some old plugins might not have the eventEmitter property
501
+ plugin.eventEmitter?.on(BIDI_EVENT_NAME, eventListenerFactory('plugin', plugin));
502
+ }
503
+ }
504
+
505
+ async function assertIsOpen(
506
+ ws: WebSocket,
507
+ timeoutMs: number = 5000,
508
+ ): Promise<WebSocket> {
509
+ if (ws.readyState === ws.OPEN) {
510
+ return ws;
511
+ }
512
+ if (ws.readyState > ws.OPEN) {
513
+ throw new Error(`The BiDi web socket at ${ws.url} is not open`);
514
+ }
515
+
516
+ let errorListener;
517
+ let openListener;
518
+ // The socket is in CONNECTING state. Wait up to `timeoutMs` until it is open
519
+ try {
520
+ await new Promise((resolve, reject) => {
521
+ setTimeout(() => reject(
522
+ new Error(
523
+ `The BiDi web socket at ${ws.url} did not ` +
524
+ `open after ${timeoutMs}ms timeout`
525
+ )
526
+ ), timeoutMs);
527
+ ws.once('error', reject);
528
+ errorListener = reject;
529
+ ws.once('open', resolve);
530
+ openListener = resolve;
531
+ });
532
+ } finally {
533
+ if (errorListener) {
534
+ ws.off('error', errorListener);
535
+ }
536
+ if (openListener) {
537
+ ws.off('open', openListener);
538
+ }
539
+ }
540
+ return ws;
436
541
  }
542
+
543
+ // #endregion