@xenon-device-management/xenon 2.17.0 → 2.17.1

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.
Files changed (96) hide show
  1. package/lib/package.json +2 -3
  2. package/lib/public/assets/{AnnotationOverlay-CjmMCS0S.js → AnnotationOverlay-CB8njQvS.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-B4Ob0v3C.js → ApiKeyGate-B-804QBS.js} +1 -1
  4. package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
  5. package/lib/public/assets/{EmptyState-DcEG-oAi.js → EmptyState-WzLw1cEV.js} +1 -1
  6. package/lib/public/assets/{FieldGroup-CtnCDshE.js → FieldGroup-CaQNXgcE.js} +1 -1
  7. package/lib/public/assets/{FilterMenu-BK0dE9cc.js → FilterMenu-C9gAiypL.js} +1 -1
  8. package/lib/public/assets/{Menu-eKsmLpKS.js → Menu-CfTti3rw.js} +1 -1
  9. package/lib/public/assets/{Modal-DPwop3Lz.js → Modal-BcZUFhDm.js} +1 -1
  10. package/lib/public/assets/{RecordingPage-CfgSkZkB.js → RecordingPage-B_pbl3UE.js} +1 -1
  11. package/lib/public/assets/{RecordingsPage-Bzl9pTUY.js → RecordingsPage-BkVhplE3.js} +1 -1
  12. package/lib/public/assets/{SegmentedControl-B_TZTrPm.js → SegmentedControl-CDLMLNl5.js} +1 -1
  13. package/lib/public/assets/{SettingCard-DlmvZJPM.js → SettingCard-Lyw5CB1F.js} +1 -1
  14. package/lib/public/assets/{Table-BigPWKXi.js → Table-CK414bOU.js} +1 -1
  15. package/lib/public/assets/{activity-CvuZdQYu.js → activity-pEyAdE-M.js} +1 -1
  16. package/lib/public/assets/{ai-settings-lI2UJ2n7.js → ai-settings-CIhS7MMq.js} +1 -1
  17. package/lib/public/assets/{api-keys-CPgf9XJM.js → api-keys-D2J44Ebv.js} +1 -1
  18. package/lib/public/assets/{apps-DIzOvtUp.js → apps-CCV3RZY8.js} +1 -1
  19. package/lib/public/assets/{arrow-left-DiP5ywBT.js → arrow-left-DaEUmERk.js} +1 -1
  20. package/lib/public/assets/{arrow-right-DtW_8hm5.js → arrow-right-CnRXHXit.js} +1 -1
  21. package/lib/public/assets/{arrow-up-right-C89FbNU0.js → arrow-up-right-BGCS7zhi.js} +1 -1
  22. package/lib/public/assets/{auth-shell-B7sUDb--.js → auth-shell-CpW1zsxD.js} +2 -2
  23. package/lib/public/assets/{builds-page-BBhvrMfA.js → builds-page-B-xdzjMx.js} +1 -1
  24. package/lib/public/assets/{button-DugsdUw1.js → button-B5QkOVa4.js} +1 -1
  25. package/lib/public/assets/{calendar-grKd08ZH.js → calendar-gX_1EKue.js} +1 -1
  26. package/lib/public/assets/{check-ORjyNfnX.js → check-MIM4THvH.js} +1 -1
  27. package/lib/public/assets/{chevron-right-Ei2FxUXq.js → chevron-right-BZIp3-9t.js} +1 -1
  28. package/lib/public/assets/{circle-check-Day0HjW4.js → circle-check-Crt-G_uy.js} +1 -1
  29. package/lib/public/assets/{circle-x-CHxSRLHp.js → circle-x-C8GvvXVt.js} +1 -1
  30. package/lib/public/assets/{clock-BDHewWTx.js → clock-MFKV4qGa.js} +1 -1
  31. package/lib/public/assets/{copy-CTwT4I3D.js → copy-BU-Z7mBK.js} +1 -1
  32. package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
  33. package/lib/public/assets/{download-D3bjz_vr.js → download-DIoLCCYv.js} +1 -1
  34. package/lib/public/assets/{forgot-password-BNBLi023.js → forgot-password-vNYlPnia.js} +1 -1
  35. package/lib/public/assets/{h264Stream-C51Au2i_.js → h264Stream-BLcSplMU.js} +1 -1
  36. package/lib/public/assets/{index-Dk5J6HB1.js → index-BpqyclNY.js} +2 -2
  37. package/lib/public/assets/{index-DncSz5LG.js → index-Hl1OL-k_.js} +1 -1
  38. package/lib/public/assets/{index-Z7_BY3pJ.js → index-QbukVVrl.js} +1 -1
  39. package/lib/public/assets/{input-bTXM4l09.js → input-DHH3fHer.js} +1 -1
  40. package/lib/public/assets/{line-chart-B3YmVYyr.js → line-chart-PdViEphw.js} +1 -1
  41. package/lib/public/assets/{list-checks-BClKEj1-.js → list-checks-DI_heIg9.js} +1 -1
  42. package/lib/public/assets/{lock-CZsenX3D.js → lock-DVP_dL6J.js} +1 -1
  43. package/lib/public/assets/{login-B6A6tOp9.js → login-B8vtTDcr.js} +1 -1
  44. package/lib/public/assets/{maintenance-settings-CmCJGy7O.js → maintenance-settings-BMa5EfgI.js} +1 -1
  45. package/lib/public/assets/{monitor-BdTNEglU.js → monitor-CtzX_RkD.js} +1 -1
  46. package/lib/public/assets/{mouse-pointer-2-BM8CaLtt.js → mouse-pointer-2-BA09sdS9.js} +1 -1
  47. package/lib/public/assets/{network-BS6vUc1x.js → network-B_F9Nox-.js} +1 -1
  48. package/lib/public/assets/{overview-Yfk_Y7g3.js → overview-kWCYMHMV.js} +1 -1
  49. package/lib/public/assets/{page-header-DhXCwpml.js → page-header-CAbH1O1a.js} +1 -1
  50. package/lib/public/assets/{play-D08gP1td.js → play-sg95hLqA.js} +1 -1
  51. package/lib/public/assets/{plus-BKboorfa.js → plus-BT9mk2gj.js} +1 -1
  52. package/lib/public/assets/{profile-page-Cd9NodrA.js → profile-page-DZOz5Ypc.js} +1 -1
  53. package/lib/public/assets/{reset-password-acLDyyDW.js → reset-password-CqeImlEO.js} +1 -1
  54. package/lib/public/assets/{runbook-page-CWLkC1Fd.js → runbook-page-U_8TSysj.js} +1 -1
  55. package/lib/public/assets/{select-DZoRKNti.js → select-Cx4JDzsR.js} +1 -1
  56. package/lib/public/assets/{selector-detail-redirect-DS4qrt84.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
  57. package/lib/public/assets/{selector-health-page-TpmN61G3.js → selector-health-page-CKR5URll.js} +1 -1
  58. package/lib/public/assets/{session-detail-page-C0wYaXqy.js → session-detail-page-DuIVzTW_.js} +1 -1
  59. package/lib/public/assets/{settings-CxPGEY2j.js → settings-B5fuvgTP.js} +1 -1
  60. package/lib/public/assets/{stat-tile-DUpyYgl-.js → stat-tile-CtHX1dkh.js} +1 -1
  61. package/lib/public/assets/{tablet-SA02BJNQ.js → tablet-DCGD0GQt.js} +1 -1
  62. package/lib/public/assets/{teams-jamD0Kxr.js → teams-MzlELlw9.js} +1 -1
  63. package/lib/public/assets/{trash-2-DO8fv6w4.js → trash-2-BIRm2KVs.js} +1 -1
  64. package/lib/public/assets/{upload-CaiGCStb.js → upload-C-lgyswH.js} +1 -1
  65. package/lib/public/assets/{use-builds-data-jS-fcvVf.js → use-builds-data-IOydXA8d.js} +1 -1
  66. package/lib/public/assets/{use-password-reset-mode-BzoenegM.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
  67. package/lib/public/assets/{users-Bnw7Vs_B.js → users-hwK5XGIy.js} +1 -1
  68. package/lib/public/assets/{users-DHvDrriG.js → users-ivNCjeus.js} +1 -1
  69. package/lib/public/assets/{video-off-FEvy0w5U.js → video-off-DvcIEsdZ.js} +1 -1
  70. package/lib/public/assets/{webhook-settings-CBVB5lxi.js → webhook-settings-DukgCe1u.js} +1 -1
  71. package/lib/public/assets/{zap-BMrTJO6M.js → zap-BE9gacFj.js} +1 -1
  72. package/lib/public/index.html +1 -1
  73. package/lib/src/app/openapi/control.yaml +69 -4
  74. package/lib/src/app/routers/control.js +24 -5
  75. package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
  76. package/lib/src/device-utils.js +50 -22
  77. package/lib/src/gateway/internalCall.js +33 -5
  78. package/lib/src/gateway/sessionGateway.js +9 -4
  79. package/lib/src/helpers/index.js +9 -6
  80. package/lib/src/interceptors/CommandInterceptor.js +10 -0
  81. package/lib/src/services/ServerManager.js +25 -4
  82. package/lib/src/services/UserService.js +8 -3
  83. package/lib/src/sessions/appiumUmbrella.js +11 -0
  84. package/lib/src/sessions/sessionMemory.js +11 -2
  85. package/lib/test/unit/UserService.test.js +8 -0
  86. package/lib/test/unit/boot-device-sync.spec.js +195 -0
  87. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
  88. package/lib/test/unit/install-scripts.spec.js +98 -0
  89. package/lib/test/unit/internal-call.spec.js +99 -0
  90. package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
  91. package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
  92. package/lib/test/unit/is-xenon-running.spec.js +10 -0
  93. package/lib/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +2 -3
  95. package/lib/public/assets/DeviceMosaicView-DTVOB_NO.js +0 -2
  96. package/lib/public/assets/device-explorer-DlhrKJ-g.js +0 -282
@@ -4,8 +4,10 @@ exports.INTERNAL_CALL_MARKER = exports.INTERNAL_CALL_HEADER = void 0;
4
4
  exports.internalCallHeaders = internalCallHeaders;
5
5
  exports.hasInternalCallSecret = hasInternalCallSecret;
6
6
  exports.isInternalCall = isInternalCall;
7
+ exports.isInsideInternalCall = isInsideInternalCall;
7
8
  exports.internalCallBaseUrl = internalCallBaseUrl;
8
9
  exports.createInternalCallLayer = createInternalCallLayer;
10
+ const async_hooks_1 = require("async_hooks");
9
11
  const crypto_1 = require("crypto");
10
12
  const appiumBasePath_1 = require("../app/appiumBasePath");
11
13
  /**
@@ -27,11 +29,20 @@ const appiumBasePath_1 = require("../app/appiumBasePath");
27
29
  * The secret never leaves the process: it is sent only to this server's own
28
30
  * address, with any HTTP proxy bypassed, and the gateway removes the header
29
31
  * before anything after it sees the request.
32
+ *
33
+ * Such a call is never one of the test's commands. The rest of its request
34
+ * runs in an async context the plugin can read (`isInsideInternalCall`), since
35
+ * the plugin's `handle`, deep inside Appium's route, is never given the
36
+ * request. Through 2.16 it couldn't tell, so the dashboard recorded the call
37
+ * as the session's own: a performance recording's stop the driver refused at
38
+ * the end of an iPhone session (through 2.15, of every simulator session)
39
+ * failed a session whose commands had all passed.
30
40
  */
31
41
  exports.INTERNAL_CALL_HEADER = 'x-xenon-internal';
32
42
  exports.INTERNAL_CALL_MARKER = '/wd-internal';
33
43
  const SECRET = Buffer.from((0, crypto_1.randomBytes)(32).toString('base64url'));
34
44
  const INTERNAL = Symbol('xenon.internalCall');
45
+ const internalCalls = new async_hooks_1.AsyncLocalStorage();
35
46
  /** The headers a loopback call to this process's own server carries. */
36
47
  function internalCallHeaders() {
37
48
  return { [exports.INTERNAL_CALL_HEADER]: SECRET.toString() };
@@ -48,6 +59,19 @@ function hasInternalCallSecret(headers) {
48
59
  function isInternalCall(req) {
49
60
  return !!req && req[INTERNAL] === true;
50
61
  }
62
+ /**
63
+ * Whether this code runs for a request the internal-call layer accepted: the
64
+ * plugin's `handle` and the driver's command under it, which never see the
65
+ * request itself.
66
+ *
67
+ * Read it where a command enters the plugin (CommandInterceptor.handle). It
68
+ * also holds in whatever the driver starts during the call and runs later (a
69
+ * timer, a socket's callback): base-driver's new-command timer, restarted at
70
+ * the call's end, fires inside it, and so does an idle session's shutdown.
71
+ */
72
+ function isInsideInternalCall() {
73
+ return internalCalls.getStore() === true;
74
+ }
51
75
  /**
52
76
  * Where a local session's loopback calls go: this server, under its base
53
77
  * path, marked `/wd-internal`. A wildcard bind address is reached on 127.0.0.1.
@@ -80,11 +104,15 @@ function createInternalCallLayer(basePath) {
80
104
  req.originalUrl = req.url;
81
105
  return true;
82
106
  }
83
- // One call to next() for every outcome: Appium's 404 carries a stack trace,
84
- // and a second call site would make a refused /wd-internal request's answer
85
- // differ from any other unknown route's by that one frame.
107
+ // Every request this layer leaves alone goes on through the one call to
108
+ // next() below: Appium's 404 carries a stack trace, and a second call site
109
+ // would make a refused /wd-internal request's answer differ from any other
110
+ // unknown route's by that one frame. An accepted call goes on inside its
111
+ // context, down to the plugin's handle (isInsideInternalCall).
86
112
  return function xenonInternalCalls(req, _res, next) {
87
- accept(req);
88
- next();
113
+ if (accept(req))
114
+ internalCalls.run(true, next);
115
+ else
116
+ next();
89
117
  };
90
118
  }
@@ -168,10 +168,15 @@ function createHubRouting(deps) {
168
168
  yield deleteRemote(req, res, sessionId, location);
169
169
  return;
170
170
  }
171
- yield deps
172
- .touch(sessionId)
173
- .catch((error) => logger.warn(`Could not record activity for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
174
- const hooks = location.session && !location.cloud ? deps.dashboard : undefined;
171
+ // Xenon's own call (internalCall.ts) is never one of the test's
172
+ // commands: it isn't the session's activity, nor in its record.
173
+ const own = (0, internalCall_1.isInternalCall)(req);
174
+ if (!own) {
175
+ yield deps
176
+ .touch(sessionId)
177
+ .catch((error) => logger.warn(`Could not record activity for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
178
+ }
179
+ const hooks = location.session && !location.cloud && !own ? deps.dashboard : undefined;
175
180
  const command = hooks
176
181
  ? deps.commandName(`${deps.basePath}/session/${sessionId}${rest.split('?')[0]}`, req.method)
177
182
  : undefined;
@@ -176,15 +176,18 @@ function stripAppiumPrefixes(caps) {
176
176
  }
177
177
  return strippedCaps;
178
178
  }
179
- function isXenonRunning(host, tlsRejectUnauthorized) {
180
- return __awaiter(this, void 0, void 0, function* () {
179
+ function probeConfig(probe) {
180
+ return Object.assign(Object.assign({}, (probe.timeoutMs !== undefined ? { timeout: probe.timeoutMs } : {})), (probe.retry === false ? { retry: false } : {}));
181
+ }
182
+ function isXenonRunning(host_1, tlsRejectUnauthorized_1) {
183
+ return __awaiter(this, arguments, void 0, function* (host, tlsRejectUnauthorized, probe = {}) {
181
184
  try {
182
185
  const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
183
186
  // /health is the one /xenon/api route with no login. The hub probes its
184
187
  // nodes, and a node its hub, without credentials, so a login-only route
185
188
  // (/status) called a server with auth on dead: the hub pruned its node's
186
189
  // phones, and the node never registered with its hub.
187
- yield client.get(`${host}/xenon/api/health`);
190
+ yield client.get(`${host}/xenon/api/health`, probeConfig(probe));
188
191
  return true;
189
192
  }
190
193
  catch (error) {
@@ -193,11 +196,11 @@ function isXenonRunning(host, tlsRejectUnauthorized) {
193
196
  }
194
197
  });
195
198
  }
196
- function isAppiumRunningAt(url, tlsRejectUnauthorized) {
197
- return __awaiter(this, void 0, void 0, function* () {
199
+ function isAppiumRunningAt(url_1, tlsRejectUnauthorized_1) {
200
+ return __awaiter(this, arguments, void 0, function* (url, tlsRejectUnauthorized, probe = {}) {
198
201
  try {
199
202
  const client = InternalHttpClient_1.InternalHttpClient.getClient(tlsRejectUnauthorized);
200
- yield client.get(`${url}/status`);
203
+ yield client.get(`${url}/status`, probeConfig(probe));
201
204
  return true;
202
205
  }
203
206
  catch (error) {
@@ -70,6 +70,7 @@ const waitFor_1 = require("../services/autowait/waitFor");
70
70
  const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
71
71
  const xenonScripts_1 = require("./xenonScripts");
72
72
  const healReport_1 = require("../gateway/healReport");
73
+ const internalCall_1 = require("../gateway/internalCall");
73
74
  const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
74
75
  /**
75
76
  * The commands the interceptor answers itself for an element Xenon found in a
@@ -116,6 +117,15 @@ let CommandInterceptor = class CommandInterceptor {
116
117
  const IGNORED_COMMANDS = ['getScreenshot', 'stopRecordingScreen', 'startRecordingScreen'];
117
118
  if (IGNORED_COMMANDS.includes(commandName))
118
119
  return yield next();
120
+ // Xenon's own call to the session (a LocalSession's loopback, when its
121
+ // in-process call failed; gateway/internalCall.ts) goes straight to the
122
+ // driver. It isn't one of the test's commands: it isn't recorded with
123
+ // them, so it can't decide the session's status, and it doesn't reset
124
+ // Xenon's idle clock. Through 2.16 a performance recording's stop the
125
+ // driver refused at the end was recorded as a failed `execute`, and the
126
+ // session ended failed.
127
+ if ((0, internalCall_1.isInsideInternalCall)())
128
+ return yield next();
119
129
  const sessionId = driver.sessionId || args[args.length - 1];
120
130
  const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
121
131
  const spanId = `${sessionId}:${commandName}`;
@@ -265,10 +265,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
265
265
  // Cleanup any remaining zombie sessions (cross-node fallback)
266
266
  const { cleanupZombieSessions } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
267
267
  yield cleanupZombieSessions(recoveredSessionIds);
268
- // Initial device discovery poll to start managers and trackers
269
- yield (0, device_utils_1.updateDeviceList)(localHosts, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
270
- // remove stale devices
271
- yield (0, device_utils_1.removeStaleDevices)(localHosts, pluginArgs.tlsRejectUnauthorized);
268
+ yield this.syncDevicesAtBoot(localHosts, pluginArgs);
272
269
  // In the background: put back the network a previous run left changed on
273
270
  // this server's phones (an Offline profile, an interceptor proxy).
274
271
  void this.cleanUpPhoneNetworks(localHosts, nodeId);
@@ -280,6 +277,30 @@ let ServerManager = ServerManager_1 = class ServerManager {
280
277
  }
281
278
  });
282
279
  }
280
+ /**
281
+ * The phones as this server starts, the last step before Appium opens its
282
+ * port, which it does only once every plugin's updateServer has finished.
283
+ * So it never waits long on another server. Through 2.17 a row naming a
284
+ * host that drops packets (a node gone, or this machine's own old DHCP
285
+ * address) kept the port closed for two minutes.
286
+ */
287
+ syncDevicesAtBoot(localHosts, pluginArgs) {
288
+ return __awaiter(this, void 0, void 0, function* () {
289
+ const { hub, tlsRejectUnauthorized } = pluginArgs;
290
+ // Initial device discovery poll to start managers and trackers. A session
291
+ // is allocated from these rows, so they are written before the port opens.
292
+ const { devices, gone } = yield (0, device_utils_1.syncLocalDevices)(localHosts);
293
+ // A node's first report to its hub, in the background: nothing here is
294
+ // allocated from it, and the hub may not answer for minutes.
295
+ if (hub) {
296
+ void (0, device_utils_1.reportDevicesToHub)(hub, devices, gone, tlsRejectUnauthorized).catch((err) => { var _a; return this.logger.warn(`Could not report this node's phones to its hub: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
297
+ }
298
+ // Other servers' phones whose server is gone, asked once each, briefly
299
+ // (BOOT_PROBE). Before the port opens: a session could otherwise be
300
+ // allocated one of them and sent to a server that never answers.
301
+ yield (0, device_utils_1.removeStaleDevices)(localHosts, tlsRejectUnauthorized, device_utils_1.BOOT_PROBE);
302
+ });
303
+ }
283
304
  /**
284
305
  * What a previous run of this server left on its phones' network: the
285
306
  * changes its ledger holds, and on this server's own Android phones a proxy
@@ -20,7 +20,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.UserService = void 0;
22
22
  const typedi_1 = require("typedi");
23
- const bcrypt_1 = __importDefault(require("bcrypt"));
23
+ // Pure JavaScript, so it needs no install script: npm 12 skips a dependency's
24
+ // install scripts unless the installing project allows them, and Appium's home
25
+ // allows none. Through 2.17 this was the native `bcrypt`, whose binary only
26
+ // its install script downloads, and the plugin then failed to load. The hashes
27
+ // are the same `$2b$` format both ways.
28
+ const bcryptjs_1 = __importDefault(require("bcryptjs"));
24
29
  const crypto_1 = __importDefault(require("crypto"));
25
30
  const logger_1 = __importDefault(require("../logger"));
26
31
  const prisma_1 = require("../prisma");
@@ -40,14 +45,14 @@ let UserService = class UserService {
40
45
  if (!password || password.length < PASSWORD_MIN) {
41
46
  throw new Error(`password must be at least ${PASSWORD_MIN} characters`);
42
47
  }
43
- return bcrypt_1.default.hash(password, this.bcryptCost);
48
+ return bcryptjs_1.default.hash(password, this.bcryptCost);
44
49
  });
45
50
  }
46
51
  verifyPassword(password, hash) {
47
52
  return __awaiter(this, void 0, void 0, function* () {
48
53
  if (!password || !hash)
49
54
  return false;
50
- return bcrypt_1.default.compare(password, hash);
55
+ return bcryptjs_1.default.compare(password, hash);
51
56
  });
52
57
  }
53
58
  generateAccessKey() {
@@ -25,6 +25,17 @@ let AppiumUmbrella = class AppiumUmbrella {
25
25
  return false;
26
26
  return !!this.umbrella.sessionExists(sessionId);
27
27
  }
28
+ /**
29
+ * The options this session's driver runs with (its capabilities, without
30
+ * the `appium:` prefix), or undefined for a session Appium doesn't have.
31
+ * Read, never a command: see hasSession.
32
+ */
33
+ driverOptions(sessionId) {
34
+ var _a, _b;
35
+ if (!sessionId || !this.umbrella)
36
+ return undefined;
37
+ return (_b = (_a = this.umbrella.sessions) === null || _a === void 0 ? void 0 : _a[sessionId]) === null || _b === void 0 ? void 0 : _b.opts;
38
+ }
28
39
  };
29
40
  exports.AppiumUmbrella = AppiumUmbrella;
30
41
  exports.AppiumUmbrella = AppiumUmbrella = __decorate([
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.forgetSessionMemory = forgetSessionMemory;
4
7
  const typedi_1 = require("typedi");
5
8
  const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
6
9
  const AutowaitService_1 = require("../services/autowait/AutowaitService");
10
+ const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
11
+ const logger_1 = __importDefault(require("../logger"));
7
12
  /**
8
13
  * Drop what this server keeps in memory for a session's commands: the
9
- * elements its finds and heals found in screenshots (OmniVisionService) and
10
- * its autowait settings (AutowaitService). Called however the session ends
14
+ * elements its finds and heals found in screenshots (OmniVisionService), its
15
+ * autowait settings (AutowaitService), and a simulator's live preview of its
16
+ * picture (IOSStreamService). Called however the session ends
11
17
  * here: the test's delete (SessionLifecycleService.deleteSession), Appium's
12
18
  * new-command timeout or a crash (XenonPlugin.onUnexpectedShutdown), the idle
13
19
  * release (releaseBlockedDevices), a stale heartbeat (OrphanSweeper) and a
@@ -22,4 +28,7 @@ function forgetSessionMemory(sessionId) {
22
28
  return;
23
29
  typedi_1.Container.get(OmniVisionService_1.OmniVisionService).forgetSession(sessionId);
24
30
  typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
31
+ typedi_1.Container.get(IOSStreamService_1.default)
32
+ .endSimulatorPreviews(sessionId)
33
+ .catch((err) => { var _a; return logger_1.default.warn(`Ending the simulator preview of ${sessionId} failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
25
34
  }
@@ -26,6 +26,14 @@ describe('UserService', () => {
26
26
  (0, chai_1.expect)(yield svc.verifyPassword('correct-horse-staple', hash)).to.be.true;
27
27
  (0, chai_1.expect)(yield svc.verifyPassword('wrong', hash)).to.be.false;
28
28
  }));
29
+ // Written by the native `bcrypt` 5 that hashed passwords through 2.17, at
30
+ // its cost of 12: every stored password is one of these.
31
+ it('verifies a password stored before 2.18', () => __awaiter(void 0, void 0, void 0, function* () {
32
+ const stored = '$2b$12$5kTngvxC6mQpOaiVCcexfuHPHKvxQMKz/IFm3Gxzia4Sb3lzK85dC';
33
+ const svc = new UserService_1.UserService();
34
+ (0, chai_1.expect)(yield svc.verifyPassword('signed-in-before-2.18', stored)).to.be.true;
35
+ (0, chai_1.expect)(yield svc.verifyPassword('signed-in-before-2.17', stored)).to.be.false;
36
+ }));
29
37
  it('rejects passwords shorter than 8 characters', () => __awaiter(void 0, void 0, void 0, function* () {
30
38
  const svc = new UserService_1.UserService();
31
39
  let err;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const net_1 = __importDefault(require("net"));
50
+ const chai_1 = require("chai");
51
+ const sinon_1 = __importDefault(require("sinon"));
52
+ const typedi_1 = require("typedi");
53
+ const async_wait_until_1 = __importDefault(require("async-wait-until"));
54
+ const prisma_1 = require("../../src/prisma");
55
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
56
+ const device_store_1 = require("../../src/data-service/device-store");
57
+ const deviceUtils = __importStar(require("../../src/device-utils"));
58
+ const helpers = __importStar(require("../../src/helpers"));
59
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
60
+ const device_managers_1 = require("../../src/device-managers");
61
+ const iOSTracker_1 = require("../../src/device-managers/iOSTracker");
62
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
63
+ const ServerManager_1 = require("../../src/services/ServerManager");
64
+ const SocketServer_1 = require("../../src/services/SocketServer");
65
+ const NotificationService_1 = require("../../src/services/NotificationService");
66
+ const container_registration_1 = require("../helpers/container-registration");
67
+ const fake_device_table_1 = require("../helpers/fake-device-table");
68
+ /**
69
+ * Appium opens its port only once every plugin's updateServer has finished,
70
+ * so a start must not wait long on another server. Seen with 2.17.0: a hub
71
+ * whose database still listed its own phones under its old DHCP address
72
+ * (http://192.168.0.104:4723, now .102) asked that address whether it was a
73
+ * live node, and a host that drops packets takes four 30 s timeouts to give
74
+ * up, so the port opened 2 min 4 s after the plugin loaded. A node whose hub
75
+ * doesn't answer waited the same way before reporting its phones.
76
+ */
77
+ const IP = '192.168.0.102';
78
+ const HUB = `http://${IP}:4723`;
79
+ const NODE = `http://${IP}:4725`;
80
+ const phone = (udid, host, over = {}) => (Object.assign({ udid,
81
+ host, name: udid, platform: 'android', deviceType: 'real', realDevice: true, state: 'device', sdk: '14' }, over));
82
+ /** A promise this test settles when it chooses, as a host that drops packets never does. */
83
+ const deferred = () => {
84
+ let resolve;
85
+ const promise = new Promise((r) => (resolve = r));
86
+ return { promise, resolve };
87
+ };
88
+ /** Whether a promise settles within a time no probe of another server can beat. */
89
+ const settlesSoon = (p) => Promise.race([
90
+ p.then(() => true),
91
+ new Promise((r) => setTimeout(() => r(false), 1000)),
92
+ ]);
93
+ describe('A start never waits on another server', () => {
94
+ let table;
95
+ let discover;
96
+ let restoreContainer;
97
+ let savedStore;
98
+ const seed = (rows) => {
99
+ table = (0, fake_device_table_1.fakeDeviceTable)(rows);
100
+ for (const m of Object.keys(table.delegate)) {
101
+ sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
102
+ }
103
+ const settings = (0, fake_device_table_1.fakeDeviceSettingTable)();
104
+ for (const m of Object.keys(settings.delegate)) {
105
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, m).callsFake(settings.delegate[m]);
106
+ }
107
+ };
108
+ const boot = (port, args) => typedi_1.Container.get(ServerManager_1.ServerManager).syncDevicesAtBoot((0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: IP }, port), args);
109
+ beforeEach(() => {
110
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return []; });
111
+ restoreContainer = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager, SocketServer_1.SocketServer, NotificationService_1.NotificationService, iOSTracker_1.IosTracker);
112
+ typedi_1.Container.set({
113
+ id: device_managers_1.XenonManager,
114
+ value: { getDevices: (known) => discover(known) },
115
+ });
116
+ typedi_1.Container.set({
117
+ id: SocketServer_1.SocketServer,
118
+ value: {
119
+ emitToDashboard: () => undefined,
120
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
121
+ hasScopedDashboard: () => false,
122
+ },
123
+ });
124
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }) } });
125
+ sinon_1.default.stub(prisma_1.prisma.lease, 'findMany').resolves([]);
126
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
127
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
128
+ });
129
+ afterEach(() => {
130
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
131
+ sinon_1.default.restore();
132
+ restoreContainer();
133
+ });
134
+ describe('on a hub, a row whose server never answers', () => {
135
+ // Takes the connection and never answers, as a host past a dropped route
136
+ // looks to the client: nothing but the timeout ends the request.
137
+ let silent;
138
+ let sockets;
139
+ let silentHost;
140
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
141
+ sockets = [];
142
+ silent = net_1.default.createServer((s) => {
143
+ sockets.push(s);
144
+ });
145
+ yield new Promise((r) => silent.listen(0, '127.0.0.1', r));
146
+ silentHost = `http://127.0.0.1:${silent.address().port}`;
147
+ // The real probe, with a limit this test can wait for.
148
+ sinon_1.default.stub(deviceUtils.BOOT_PROBE, 'timeoutMs').value(300);
149
+ }));
150
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
151
+ sockets.forEach((s) => s.destroy());
152
+ yield new Promise((r) => silent.close(() => r()));
153
+ }));
154
+ it('is asked once, briefly, and gone before the port opens', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ seed([phone('old-row', silentHost)]);
156
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return [phone('own-phone', HUB)]; });
157
+ (0, chai_1.expect)(yield settlesSoon(boot(4723, {}))).to.equal(true);
158
+ (0, chai_1.expect)(table.row('own-phone')).to.include({ host: HUB });
159
+ (0, chai_1.expect)(table.row('old-row')).to.equal(undefined);
160
+ (0, chai_1.expect)(sockets).to.have.length(1);
161
+ }));
162
+ });
163
+ it('a node opens while its hub never answers, and reports its phones once it does', () => __awaiter(void 0, void 0, void 0, function* () {
164
+ seed([]);
165
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return [phone('node-phone', NODE)]; });
166
+ const hubAnswer = deferred();
167
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').returns(hubAnswer.promise);
168
+ const post = sinon_1.default.stub(NodeDevices_1.default.prototype, 'postDevicesToHub').resolves();
169
+ (0, chai_1.expect)(yield settlesSoon(boot(4725, { hub: HUB }))).to.equal(true);
170
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
171
+ (0, chai_1.expect)(alive.calledWith(HUB)).to.equal(true);
172
+ (0, chai_1.expect)(post.called).to.equal(false);
173
+ hubAnswer.resolve(true);
174
+ yield (0, async_wait_until_1.default)(() => post.called, { timeout: 2000 });
175
+ (0, chai_1.expect)(post.firstCall.args[1]).to.equal('add');
176
+ (0, chai_1.expect)(post.firstCall.args[0].map((d) => d.udid)).to.deep.equal(['node-phone']);
177
+ }));
178
+ it("a node's report that fails is logged, and never fails the start", () => __awaiter(void 0, void 0, void 0, function* () {
179
+ seed([]);
180
+ sinon_1.default.stub(deviceUtils, 'reportDevicesToHub').rejects(new Error('hub refused'));
181
+ const warn = sinon_1.default.spy(typedi_1.Container.get(ServerManager_1.ServerManager).logger, 'warn');
182
+ const unhandled = sinon_1.default.spy();
183
+ process.on('unhandledRejection', unhandled);
184
+ try {
185
+ yield boot(4725, { hub: HUB });
186
+ yield (0, async_wait_until_1.default)(() => warn.called, { timeout: 2000 });
187
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('hub refused');
188
+ yield new Promise((r) => setImmediate(r));
189
+ (0, chai_1.expect)(unhandled.called).to.equal(false);
190
+ }
191
+ finally {
192
+ process.off('unhandledRejection', unhandled);
193
+ }
194
+ }));
195
+ });
@@ -67,6 +67,7 @@ const prisma_store_1 = require("../../src/data-service/prisma-store");
67
67
  const SessionManager_1 = require("../../src/sessions/SessionManager");
68
68
  const RemoteSession_1 = require("../../src/sessions/RemoteSession");
69
69
  const event_manager_1 = require("../../src/dashboard/event-manager");
70
+ const deviceService = __importStar(require("../../src/data-service/device-service"));
70
71
  const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
71
72
  const internalCall_1 = require("../../src/gateway/internalCall");
72
73
  const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
@@ -657,6 +658,26 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
657
658
  (0, chai_1.expect)(nodeRequests).to.have.length(1);
658
659
  (0, chai_1.expect)(nodeRequests[0].headers[internalCall_1.INTERNAL_CALL_HEADER]).to.equal(undefined);
659
660
  }));
661
+ it('is never logged as one of the session’s commands, though forwarded', () => __awaiter(this, void 0, void 0, function* () {
662
+ yield boot({ dashboard: true });
663
+ const { id } = yield remoteSession();
664
+ const before = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'beforeSessionCommand').resolves(true);
665
+ const after = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
666
+ const touched = sinon_1.default.stub(deviceService, 'updateCmdExecutedTime').resolves();
667
+ yield (0, loopbackRequest_1.default)(hubUrl)
668
+ .get(`/wd/hub/wd-internal/session/${id}/url`)
669
+ .set((0, internalCall_1.internalCallHeaders)())
670
+ .expect(200);
671
+ (0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
672
+ (0, chai_1.expect)(before.called, 'the dashboard’s before hook').to.equal(false);
673
+ (0, chai_1.expect)(after.called, 'the dashboard’s after hook').to.equal(false);
674
+ (0, chai_1.expect)(touched.called, 'counted as the session’s activity').to.equal(false);
675
+ // The test's own command is, as before.
676
+ yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).expect(200);
677
+ (0, chai_1.expect)(before.callCount).to.equal(1);
678
+ (0, chai_1.expect)(after.callCount).to.equal(1);
679
+ (0, chai_1.expect)(touched.calledOnceWith(id)).to.equal(true);
680
+ }));
660
681
  });
661
682
  describe('per-command auth on a remote session (at the hub)', () => {
662
683
  it('lets the owner through to the node', () => __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const root = path_1.default.join(__dirname, '..', '..');
10
+ const readJson = (file) => JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
11
+ /**
12
+ * npm 12 runs a dependency's install scripts only when the project installing
13
+ * it allows them (`allowScripts` in its package.json), and skips the rest with
14
+ * a warning. Appium installs a plugin with npm into its own home, whose
15
+ * package.json allows nothing, so every install script in Xenon's tree is
16
+ * skipped there and `appium plugin install` still reports success.
17
+ *
18
+ * Through 2.17 passwords were hashed with `bcrypt`, whose native binary only
19
+ * its `install` script downloads. Installed with npm 12, the plugin then
20
+ * failed to load: "Cannot find module .../bcrypt/lib/binding/napi-v3/bcrypt_lib.node".
21
+ *
22
+ * Each package here has an install script and works without it, for the
23
+ * reason given, checked with npm 12. A new one needs its own reason, checked
24
+ * the same way, or a replacement that needs no script.
25
+ */
26
+ const WORKS_WITHOUT_ITS_SCRIPT = {
27
+ '@ffmpeg-installer/*': 'chmod u+x ffmpeg: every platform package ships ffmpeg executable already',
28
+ '@prisma/client': 'generates the default client in node_modules/.prisma; Xenon imports its own (src/generated/client)',
29
+ '@prisma/engines': "downloads Prisma's engines: the schema engine is fetched on its first use at startup, " +
30
+ 'and the generated client carries its query engines',
31
+ '@scarf/scarf': "swagger-ui-dist's install analytics",
32
+ 'appium-chromedriver': 'downloads a Chromedriver; Xenon downloads its own when needed (ChromeDriverManager)',
33
+ prisma: 'warns about incompatible older Prisma packages',
34
+ protobufjs: 'warns about a dependent pinned to another version scheme',
35
+ sharp: 'checks for its prebuilt binary, which comes in an @img/sharp-* package with no script',
36
+ 'tesseract.js': 'prints an Open Collective notice',
37
+ };
38
+ const matches = (key, name) => key.endsWith('/*') ? name.startsWith(key.slice(0, -1)) : name === key;
39
+ const nameOf = (id) => id.slice(0, id.lastIndexOf('@'));
40
+ /** Where Node would find `name` from `from`: its node_modules, then each parent's. */
41
+ function packageDir(name, from) {
42
+ for (let dir = from;; dir = path_1.default.dirname(dir)) {
43
+ const candidate = path_1.default.join(dir, 'node_modules', name, 'package.json');
44
+ if (fs_1.default.existsSync(candidate))
45
+ return path_1.default.dirname(candidate);
46
+ if (path_1.default.dirname(dir) === dir)
47
+ return undefined;
48
+ }
49
+ }
50
+ /**
51
+ * Every package Xenon runs with (its dependencies, theirs, and so on, as
52
+ * installed) that has an install script: `preinstall`, `install` or
53
+ * `postinstall`, or a `binding.gyp`, which npm builds with `node-gyp rebuild`
54
+ * when no script says otherwise. An optional dependency for another platform
55
+ * isn't installed, and is left out.
56
+ */
57
+ function packagesWithInstallScripts() {
58
+ const found = new Map();
59
+ const seen = new Set();
60
+ const runtimeDeps = (pkg) => Object.keys(Object.assign(Object.assign({}, pkg.dependencies), pkg.optionalDependencies));
61
+ const queue = runtimeDeps(readJson(path_1.default.join(root, 'package.json'))).map((name) => ({
62
+ name,
63
+ from: root,
64
+ }));
65
+ for (let next = queue.shift(); next; next = queue.shift()) {
66
+ const { name, from } = next;
67
+ const dir = packageDir(name, from);
68
+ if (!dir || seen.has(dir))
69
+ continue;
70
+ seen.add(dir);
71
+ const pkg = readJson(path_1.default.join(dir, 'package.json'));
72
+ const scripts = ['preinstall', 'install', 'postinstall']
73
+ .filter((hook) => { var _a; return (_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[hook]; })
74
+ .map((hook) => `${hook}: ${pkg.scripts[hook]}`);
75
+ if (scripts.length === 0 && fs_1.default.existsSync(path_1.default.join(dir, 'binding.gyp'))) {
76
+ scripts.push('binding.gyp: node-gyp rebuild');
77
+ }
78
+ if (scripts.length > 0)
79
+ found.set(`${pkg.name}@${pkg.version}`, scripts.join(' | '));
80
+ queue.push(...runtimeDeps(pkg).map((dep) => ({ name: dep, from: dir })));
81
+ }
82
+ return found;
83
+ }
84
+ describe('install scripts in the runtime dependency tree', () => {
85
+ const found = packagesWithInstallScripts();
86
+ const keys = Object.keys(WORKS_WITHOUT_ITS_SCRIPT);
87
+ it('belong only to packages that work without them, since npm 12 skips them', () => {
88
+ const unexplained = [...found]
89
+ .filter(([id]) => !keys.some((key) => matches(key, nameOf(id))))
90
+ .map(([id, scripts]) => `${id} (${scripts})`);
91
+ (0, chai_1.expect)(unexplained).to.deep.equal([]);
92
+ });
93
+ it('name no package the tree no longer has', () => {
94
+ const names = [...found.keys()].map(nameOf);
95
+ const stale = keys.filter((key) => !names.some((name) => matches(key, name)));
96
+ (0, chai_1.expect)(stale).to.deep.equal([]);
97
+ });
98
+ });