appium-remote-debugger 5.3.0 → 5.7.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.
@@ -1,16 +1,18 @@
1
1
  import events from 'events';
2
2
  import log from './logger';
3
- import { errorFromCode } from 'appium-base-driver';
3
+ import { errors } from 'appium-base-driver';
4
4
  import RpcClientSimulator from './rpc-client-simulator';
5
5
  import messageHandlers from './message-handlers';
6
6
  import { appInfoFromDict, pageArrayFromDict, getDebuggerAppKey,
7
7
  getPossibleDebuggerAppKeys, checkParams, getScriptForAtom,
8
- simpleStringify, deferredPromise, getElapsedTime } from './helpers';
8
+ simpleStringify, deferredPromise, getElapsedTime, convertResult,
9
+ RESPONSE_LOG_LENGTH } from './helpers';
9
10
  import { util } from 'appium-support';
10
11
  import { retryInterval } from 'asyncbox';
11
12
  import _ from 'lodash';
12
13
  import B from 'bluebird';
13
14
  import path from 'path';
15
+ import UUID from 'uuid-js';
14
16
 
15
17
 
16
18
  let VERSION;
@@ -29,8 +31,6 @@ const RPC_RESPONSE_TIMEOUT_MS = 5000;
29
31
 
30
32
  const PAGE_READY_TIMEOUT = 5000;
31
33
 
32
- const RESPONSE_LOG_LENGTH = 100;
33
-
34
34
  const GARBAGE_COLLECT_TIMEOUT = 5000;
35
35
 
36
36
  class RemoteDebugger extends events.EventEmitter {
@@ -134,6 +134,7 @@ class RemoteDebugger extends events.EventEmitter {
134
134
 
135
135
  // initialize the rpc client
136
136
  this.rpcClient = new RpcClientSimulator({
137
+ bundleId: this.bundleId,
137
138
  platformVersion: this.platformVersion,
138
139
  isSafari: this.isSafari,
139
140
  host: this.host,
@@ -302,6 +303,7 @@ class RemoteDebugger extends events.EventEmitter {
302
303
  }
303
304
  }
304
305
  }
306
+
305
307
  log.debug(`Selected app after ${getElapsedTime(startTime)}ms`);
306
308
  return fullPageArray;
307
309
  } finally {
@@ -310,7 +312,7 @@ class RemoteDebugger extends events.EventEmitter {
310
312
  }
311
313
 
312
314
  async searchForApp (currentUrl, maxTries, ignoreAboutBlankUrl) {
313
- const bundleIds = this.includeSafari
315
+ const bundleIds = this.includeSafari && !this.isSafari
314
316
  ? [this.bundleId, SAFARI_BUNDLE_ID]
315
317
  : [this.bundleId];
316
318
  try {
@@ -401,13 +403,71 @@ class RemoteDebugger extends events.EventEmitter {
401
403
  return value;
402
404
  }
403
405
 
404
- async executeAtomAsync (atom, args, frames, responseUrl) {
405
- let asyncCallBack = `function (res) { xmlHttp = new XMLHttpRequest(); ` +
406
- `xmlHttp.open('POST', '${responseUrl}', true);` +
407
- `xmlHttp.setRequestHeader('Content-type','application/json'); ` +
408
- `xmlHttp.send(res); }`;
409
- let script = await getScriptForAtom(atom, args, frames, asyncCallBack);
410
- await this.execute(script);
406
+ async executeAtomAsync (atom, args, frames) {
407
+ // first create a Promise on the page, saving the resolve/reject functions
408
+ // as properties
409
+ const promiseName = `appiumAsyncExecutePromise${UUID.create().toString().replace(/-/g, '')}`;
410
+ let script = `var res, rej;` +
411
+ `window.${promiseName} = new Promise(function (resolve, reject) {` +
412
+ ` res = resolve;` +
413
+ ` rej = reject;` +
414
+ `});` +
415
+ `window.${promiseName}.resolve = res;` +
416
+ `window.${promiseName}.reject = rej;` +
417
+ `window.${promiseName};`;
418
+ const obj = await this.rpcClient.send('sendJSCommand', {
419
+ command: script,
420
+ appIdKey: this.appIdKey,
421
+ pageIdKey: this.pageIdKey,
422
+ returnByValue: false,
423
+ });
424
+ const promiseObjectId = obj.result.objectId;
425
+
426
+ // execute the atom, calling back to the resolve function
427
+ const asyncCallBack = `function (res) {` +
428
+ ` window.${promiseName}.resolve(res);` +
429
+ ` window.${promiseName}Value = res;` +
430
+ `}`;
431
+ await this.execute(await getScriptForAtom(atom, args, frames, asyncCallBack));
432
+
433
+ // wait for the promise to be resolved
434
+ let res;
435
+ const subcommandTimeout = 1000; // timeout on individual commands
436
+ try {
437
+ res = await this.rpcClient.send('awaitPromise', {
438
+ promiseObjectId,
439
+ appIdKey: this.appIdKey,
440
+ pageIdKey: this.pageIdKey,
441
+ });
442
+ } catch (err) {
443
+ if (!err.message.includes(`'Runtime.awaitPromise' was not found`)) {
444
+ throw err;
445
+ }
446
+
447
+ // awaitPromise is not always available, so simulate it with poll
448
+ const retryWait = 100;
449
+ const timeout = (args.length >= 3) ? args[2] : RPC_RESPONSE_TIMEOUT_MS;
450
+ const retries = parseInt(timeout / retryWait, 10);
451
+ const startTime = process.hrtime();
452
+ res = await retryInterval(retries, retryWait, async () => {
453
+ // the atom _will_ return, either because it finished or an error
454
+ // including a timeout error
455
+ if (await this.executeAtom('execute_script', [`return window.hasOwnProperty('${promiseName}Value');`, [null, null], subcommandTimeout], frames)) {
456
+ // we only put the property on `window` when the callback is called,
457
+ // so if it is there, everything is done
458
+ return await this.executeAtom('execute_script', [`return window.${promiseName}Value;`, [null, null], subcommandTimeout], frames);
459
+ }
460
+ // throw a TimeoutError, or else it needs to be caught and re-thrown
461
+ throw new errors.TimeoutError(`Timed out waiting for asynchronous script ` +
462
+ `result after ${getElapsedTime(startTime)} ms'));`);
463
+ });
464
+ } finally {
465
+ try {
466
+ // try to get rid of the promise
467
+ await this.executeAtom('execute_script', [`delete window.${promiseName};`, [null, null], subcommandTimeout], frames);
468
+ } catch (ign) {}
469
+ }
470
+ return convertResult(res);
411
471
  }
412
472
 
413
473
  frameDetached () {
@@ -631,14 +691,14 @@ class RemoteDebugger extends events.EventEmitter {
631
691
  await this.garbageCollect();
632
692
  }
633
693
 
634
- log.debug(`Sending javascript command ${_.truncate(command, {length: 50})}`);
694
+ log.debug(`Sending javascript command: '${_.truncate(command, {length: 50})}'`);
635
695
  let res = await this.rpcClient.send('sendJSCommand', {
636
696
  command,
637
697
  appIdKey: this.appIdKey,
638
698
  pageIdKey: this.pageIdKey,
639
699
  });
640
700
 
641
- return this.convertResult(res);
701
+ return convertResult(res);
642
702
  }
643
703
 
644
704
  async callFunction (objId, fn, args) {
@@ -658,31 +718,7 @@ class RemoteDebugger extends events.EventEmitter {
658
718
  pageIdKey: this.pageIdKey,
659
719
  });
660
720
 
661
- return this.convertResult(res);
662
- }
663
-
664
- convertResult (res) {
665
- if (_.isUndefined(res)) {
666
- throw new Error(`Did not get OK result from remote debugger. Result was: ${_.truncate(simpleStringify(res), {length: RESPONSE_LOG_LENGTH})}`);
667
- } else if (_.isString(res)) {
668
- try {
669
- res = JSON.parse(res);
670
- } catch (err) {
671
- // we might get a serialized object, but we might not
672
- // if we get here, it is just a value
673
- }
674
- } else if (!_.isObject(res)) {
675
- throw new Error(`Result has unexpected type: (${typeof res}).`);
676
- }
677
-
678
- if (res.status && res.status !== 0) {
679
- // we got some form of error.
680
- throw errorFromCode(res.status, res.value.message || res.value);
681
- }
682
-
683
- // with either have an object with a `value` property (even if `null`),
684
- // or a plain object
685
- return res.hasOwnProperty('value') ? res.value : res;
721
+ return convertResult(res);
686
722
  }
687
723
 
688
724
  allowNavigationWithoutReload (allow = true) {
@@ -83,7 +83,7 @@ class RemoteMessages {
83
83
  const method = 'Runtime.evaluate';
84
84
  const params = {
85
85
  expression: opts.command,
86
- returnByValue: true,
86
+ returnByValue: _.isBoolean(opts.returnByValue) ? opts.returnByValue : true,
87
87
  };
88
88
  return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, method, params);
89
89
  }
@@ -173,6 +173,17 @@ class RemoteMessages {
173
173
  return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, method, params);
174
174
  }
175
175
 
176
+ awaitPromise (connId, senderId, appIdKey, pageIdKey, opts) {
177
+ const method = 'Runtime.awaitPromise';
178
+ const params = {
179
+ promiseObjectId: opts.promiseObjectId,
180
+ returnByValue: true,
181
+ generatePreview: true,
182
+ saveResult: true,
183
+ };
184
+ return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, method, params);
185
+ }
186
+
176
187
 
177
188
  /*
178
189
  * Internal functions
@@ -249,6 +260,8 @@ class RemoteMessages {
249
260
  return this.deleteCookie(connId, senderId, appIdKey, pageIdKey, opts);
250
261
  case 'garbageCollect':
251
262
  return this.garbageCollect(connId, senderId, appIdKey, pageIdKey);
263
+ case 'awaitPromise':
264
+ return this.awaitPromise(connId, senderId, appIdKey, pageIdKey, opts);
252
265
  default:
253
266
  throw new Error(`Unknown command: ${command}`);
254
267
  }
@@ -37,6 +37,9 @@ export default class RpcClientRealDevice extends RpcClient {
37
37
  }
38
38
 
39
39
  async receive (data) { // eslint-disable-line require-await
40
+ if (!this.isConnected()) {
41
+ return;
42
+ }
40
43
  this.messageHandler.handleMessage(data);
41
44
  }
42
45
  }
@@ -133,6 +133,10 @@ export default class RpcClientSimulator extends RpcClient {
133
133
  }
134
134
 
135
135
  async receive (data) {
136
+ if (!this.isConnected()) {
137
+ return;
138
+ }
139
+
136
140
  if (!data) {
137
141
  return;
138
142
  }
package/lib/rpc-client.js CHANGED
@@ -6,6 +6,7 @@ import B from 'bluebird';
6
6
  import UUID from 'uuid-js';
7
7
  import RpcMessageHandler from './remote-debugger-message-handler';
8
8
  import { isTargetBased, getElapsedTime } from './helpers';
9
+ import { util } from 'appium-support';
9
10
 
10
11
 
11
12
  const DATA_LOG_LENGTH = {length: 200};
@@ -13,24 +14,30 @@ const DATA_LOG_LENGTH = {length: 200};
13
14
  const WAIT_FOR_TARGET_RETRIES = 10;
14
15
  const WAIT_FOR_TARGET_INTERVAL = 1000;
15
16
 
17
+ const GENERIC_TARGET_ID = 'page-6';
18
+
16
19
  export default class RpcClient {
17
20
  constructor (opts = {}) {
18
21
  this._targets = [];
19
22
  this._shouldCheckForTarget = !!opts.shouldCheckForTarget;
20
23
 
21
24
  const {
25
+ bundleId,
22
26
  platformVersion = {},
23
27
  isSafari = true,
24
28
  specialMessageHandlers = {},
25
29
  logFullResponse = false,
26
30
  } = opts;
27
31
 
32
+ this.isSafari = isSafari;
33
+
28
34
  this.connected = false;
29
35
  this.connId = UUID.create().toString();
30
36
  this.senderId = UUID.create().toString();
31
37
  this.msgId = 0;
32
38
  this.logFullResponse = logFullResponse;
33
39
 
40
+ this.bundleId = bundleId;
34
41
  this.platformVersion = platformVersion;
35
42
 
36
43
  // message handlers
@@ -68,17 +75,33 @@ export default class RpcClient {
68
75
 
69
76
  // on iOS less than 13 have targets that can be computed easily
70
77
  // and sometimes is not reported by the Web Inspector
71
- if (parseInt(this.platformVersion, 10) < 13 && _.isEmpty(this.getTarget(appIdKey, pageIdKey))) {
72
- this.addTarget({targetId: `page-${pageIdKey}`});
78
+ if (util.compareVersions(this.platformVersion, '<', '13.0') && _.isEmpty(this.getTarget(appIdKey, pageIdKey))) {
79
+ if (this.isSafari) {
80
+ this.addTarget({targetId: `page-${pageIdKey}`});
81
+ } else {
82
+ const targets = this.targets[appIdKey];
83
+ const targetIds = _.values(targets)
84
+ .map((targetId) => targetId.replace('page-', ''))
85
+ .sort();
86
+ const lastTargetId = _.last(targetIds) || 0;
87
+ this.addTarget({targetId: `page-${lastTargetId + 1}`});
88
+ }
73
89
  return;
74
90
  }
75
91
 
76
92
  // otherwise waiting is necessary to see what the target is
77
- await retryInterval(WAIT_FOR_TARGET_RETRIES, WAIT_FOR_TARGET_INTERVAL, () => {
78
- if (_.isEmpty(this.getTarget(appIdKey, pageIdKey))) {
79
- throw new Error('No targets found, unable to communicate with device');
80
- }
81
- });
93
+ try {
94
+ await retryInterval(WAIT_FOR_TARGET_RETRIES, WAIT_FOR_TARGET_INTERVAL, () => {
95
+ if (_.isEmpty(this.getTarget(appIdKey, pageIdKey))) {
96
+ throw new Error('No targets found, unable to communicate with device');
97
+ }
98
+ });
99
+ } catch (err) {
100
+ // on some systems sometimes the Web Inspector never sends the target event
101
+ // though the target is available
102
+ log.debug(`No target found. Trying '${GENERIC_TARGET_ID}', which seems to work`);
103
+ this.addTarget({targetId: GENERIC_TARGET_ID});
104
+ }
82
105
  }
83
106
 
84
107
  async send (command, opts = {}) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "keywords": [
5
5
  "appium"
6
6
  ],
7
- "version": "5.3.0",
7
+ "version": "5.7.0",
8
8
  "author": "appium",
9
9
  "license": "Apache-2.0",
10
10
  "repository": {