@xenon-device-management/xenon 2.0.0 → 2.1.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/README.md +2 -1
- package/lib/package.json +1 -2
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
- package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
- package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/InternalHttpClient.js +5 -1
- package/lib/src/XenonCapabilityManager.js +15 -1
- package/lib/src/app/appiumBasePath.js +32 -0
- package/lib/src/app/index.js +12 -0
- package/lib/src/app/insertBeforeRoutes.js +24 -4
- package/lib/src/app/registerCommandAuth.js +152 -33
- package/lib/src/app/routers/control.js +57 -13
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/ws/h264StreamWs.js +13 -8
- package/lib/src/app/ws/logcatWs.js +12 -7
- package/lib/src/app/ws/upgradeRouter.js +215 -0
- package/lib/src/dashboard/event-manager.js +7 -6
- package/lib/src/data-service/device-service.js +152 -49
- package/lib/src/data-service/device-store.js +81 -10
- package/lib/src/data-service/deviceClaims.js +98 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +94 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
- package/lib/src/device-managers/HealthMonitorService.js +17 -2
- package/lib/src/device-managers/IOSDeviceManager.js +5 -1
- package/lib/src/device-managers/NodeDevices.js +7 -2
- package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
- package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
- package/lib/src/device-managers/android/previewHold.js +2 -2
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
- package/lib/src/device-managers/localDeviceHosts.js +86 -0
- package/lib/src/device-managers/ownDeviceRow.js +38 -0
- package/lib/src/device-utils.js +78 -24
- package/lib/src/gateway/createHandoff.js +73 -0
- package/lib/src/gateway/defaultGateway.js +80 -0
- package/lib/src/gateway/forwardToNode.js +147 -0
- package/lib/src/gateway/hubSessionToken.js +356 -0
- package/lib/src/gateway/internalCall.js +90 -0
- package/lib/src/gateway/nodeSessionStatus.js +140 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
- package/lib/src/gateway/sessionCreate.js +136 -0
- package/lib/src/gateway/sessionGateway.js +201 -0
- package/lib/src/gateway/sessionLocator.js +100 -0
- package/lib/src/generated/client/edge.js +7 -4
- package/lib/src/generated/client/index-browser.js +4 -1
- package/lib/src/generated/client/index.d.ts +112 -1
- package/lib/src/generated/client/index.js +7 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +9 -0
- package/lib/src/generated/client/wasm.js +4 -1
- package/lib/src/helpers/index.js +5 -1
- package/lib/src/logger.js +5 -0
- package/lib/src/middleware/authMiddleware.js +80 -0
- package/lib/src/middleware/commandAuth.js +31 -17
- package/lib/src/middleware/csrfMiddleware.js +3 -2
- package/lib/src/middleware/sessionListingFilter.js +153 -0
- package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
- package/lib/src/plugin.js +27 -1
- package/lib/src/prisma.js +2 -1
- package/lib/src/services/DeviceReconciler.js +5 -2
- package/lib/src/services/OrphanSweeper.js +12 -19
- package/lib/src/services/PortAllocator.js +75 -16
- package/lib/src/services/ServerManager.js +50 -30
- package/lib/src/services/SessionLifecycleService.js +335 -76
- package/lib/src/services/ShutdownCoordinator.js +7 -1
- package/lib/src/services/SocketServer.js +11 -2
- package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
- package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
- package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
- package/lib/src/services/lease/LeaseService.js +19 -5
- package/lib/src/services/session/nodeCreateCaps.js +55 -0
- package/lib/src/services/session/sessionCredentials.js +7 -59
- package/lib/src/sessions/CloudSession.js +12 -0
- package/lib/src/sessions/LocalSession.js +52 -26
- package/lib/src/sessions/RemoteSession.js +116 -9
- package/lib/src/sessions/SessionManager.js +49 -6
- package/lib/src/sessions/appiumUmbrella.js +32 -0
- package/lib/test/helpers/appium-umbrella.js +84 -0
- package/lib/test/helpers/fake-device-table.js +97 -0
- package/lib/test/helpers/loki-stores.js +41 -0
- package/lib/test/helpers/loopbackRequest.js +77 -0
- package/lib/test/helpers/scratch-database.js +100 -0
- package/lib/test/helpers/scratch-port-leases.js +119 -0
- package/lib/test/helpers/test-container.js +35 -27
- package/lib/test/integration/androidDevices.spec.js +5 -4
- package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
- package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
- package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
- package/lib/test/unit/DeviceModel.spec.js +9 -9
- package/lib/test/unit/HealingCascade.test.js +5 -1
- package/lib/test/unit/OrphanSweeper.test.js +17 -13
- package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
- package/lib/test/unit/StreamTicketService.spec.js +4 -1
- package/lib/test/unit/TokenEndpoint.spec.js +17 -3
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
- package/lib/test/unit/android-h264-service.spec.js +6 -0
- package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
- package/lib/test/unit/apps-team-visibility.spec.js +16 -16
- package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
- package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
- package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
- package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
- package/lib/test/unit/authMiddleware.test.js +3 -0
- package/lib/test/unit/bug-report/route.spec.js +9 -9
- package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
- package/lib/test/unit/clipboard-route.spec.js +18 -8
- package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
- package/lib/test/unit/command-auth.spec.js +31 -31
- package/lib/test/unit/control-device-lookup.spec.js +9 -9
- package/lib/test/unit/control-forward-decision.spec.js +115 -0
- package/lib/test/unit/control-lookup-failure.spec.js +3 -3
- package/lib/test/unit/create-error-message.spec.js +43 -0
- package/lib/test/unit/create-handoff.spec.js +86 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/device-access-guard.spec.js +28 -28
- package/lib/test/unit/device-service.spec.js +4 -0
- package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
- package/lib/test/unit/device-team-guard.spec.js +20 -20
- package/lib/test/unit/device-utils.spec.js +8 -0
- package/lib/test/unit/extract-team-cap.spec.js +65 -0
- package/lib/test/unit/grid-admin-only.spec.js +3 -3
- package/lib/test/unit/healing-tier-gate.spec.js +5 -1
- package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
- package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
- package/lib/test/unit/helpers.spec.js +27 -0
- package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
- package/lib/test/unit/hub-create-once.spec.js +190 -0
- package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
- package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +401 -0
- package/lib/test/unit/hub-node-same-host.spec.js +356 -0
- package/lib/test/unit/hub-register-removal.spec.js +165 -0
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
- package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
- package/lib/test/unit/hub-session-token.spec.js +265 -0
- package/lib/test/unit/insert-before-routes.spec.js +5 -5
- package/lib/test/unit/install-repository-app-team.spec.js +2 -2
- package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
- package/lib/test/unit/internal-call.spec.js +181 -0
- package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
- package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
- package/lib/test/unit/is-xenon-running.spec.js +69 -0
- package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
- package/lib/test/unit/lease/LeaseService.spec.js +28 -1
- package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
- package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
- package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
- package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
- package/lib/test/unit/logcat-ws.spec.js +4 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/node-create-caps.spec.js +54 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
- package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
- package/lib/test/unit/omni-vision.spec.js +7 -3
- package/lib/test/unit/plugin.spec.js +45 -4
- package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
- package/lib/test/unit/profile-router.test.js +11 -11
- package/lib/test/unit/queue-requester.spec.js +5 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
- package/lib/test/unit/queue-visibility.spec.js +1 -1
- package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
- package/lib/test/unit/recording-instrumentation.spec.js +5 -0
- package/lib/test/unit/recording-source-ended.spec.js +5 -0
- package/lib/test/unit/recording-store.spec.js +9 -3
- package/lib/test/unit/recordings-library-routes.spec.js +51 -51
- package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
- package/lib/test/unit/remote-session-create.spec.js +130 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
- package/lib/test/unit/reset-link.test.js +14 -14
- package/lib/test/unit/session-app-resolution.spec.js +14 -10
- package/lib/test/unit/session-assets.spec.js +9 -9
- package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
- package/lib/test/unit/session-create-layer.spec.js +255 -0
- package/lib/test/unit/session-credential-strip.spec.js +25 -9
- package/lib/test/unit/session-credentials.spec.js +0 -47
- package/lib/test/unit/session-listing-auth.spec.js +345 -0
- package/lib/test/unit/session-locator.spec.js +250 -0
- package/lib/test/unit/session-node-calls.spec.js +174 -0
- package/lib/test/unit/session-owner-resolver.spec.js +107 -0
- package/lib/test/unit/session-recovery-routing.spec.js +118 -0
- package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
- package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
- package/lib/test/unit/socketServer.bearer.spec.js +3 -1
- package/lib/test/unit/stream-leave-route.spec.js +15 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
- package/lib/test/unit/stream-stop-recording.spec.js +13 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
- package/lib/test/unit/upgrade-router.spec.js +446 -0
- package/lib/test/unit/users-router.test.js +11 -8
- package/lib/test/unit/verify-credential.spec.js +4 -1
- package/lib/test/unit/webdriver-info-route.spec.js +64 -0
- package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -2
- package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
- package/prisma/schema.prisma +9 -0
- package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
- package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
- package/lib/src/proxy/wd-command-proxy.js +0 -221
|
@@ -67,13 +67,18 @@ class NodeDevices {
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Ask the hub to forget this node's phones filed under `host`. With the
|
|
72
|
+
* node's id the hub takes every phone this node reported, whatever its
|
|
73
|
+
* host; it never takes another server's (grid.ts, removeNodeDevices).
|
|
74
|
+
*/
|
|
75
|
+
unRegisterNode(host, nodeId) {
|
|
71
76
|
return __awaiter(this, void 0, void 0, function* () {
|
|
72
77
|
logger_1.default.info(`Unregistering node ${this.host}/xenon/api/register`);
|
|
73
78
|
try {
|
|
74
79
|
const client = InternalHttpClient_1.InternalHttpClient.getClient(this.tlsRejectUnauthorized);
|
|
75
80
|
yield client.post(`${this.host}/xenon/api/register`, [], {
|
|
76
|
-
params: { type: 'unregister', host },
|
|
81
|
+
params: Object.assign({ type: 'unregister', host }, (nodeId ? { nodeId } : {})),
|
|
77
82
|
headers: this.nodeHeaders(),
|
|
78
83
|
});
|
|
79
84
|
logger_1.default.info(`Unregistered node ${host} from hub`);
|
|
@@ -57,7 +57,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
57
57
|
const child_process_1 = require("child_process");
|
|
58
58
|
const typedi_1 = require("typedi");
|
|
59
59
|
const logger_1 = __importDefault(require("../../logger"));
|
|
60
|
-
const
|
|
60
|
+
const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
61
61
|
const H264Multiplexer_1 = require("./H264Multiplexer");
|
|
62
62
|
const h264NalParser_1 = require("./h264NalParser");
|
|
63
63
|
const androidH264Config_1 = require("../../app/routers/androidH264Config");
|
|
@@ -266,7 +266,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
266
266
|
startScrcpyOrThrow(udid, onPacket) {
|
|
267
267
|
return __awaiter(this, void 0, void 0, function* () {
|
|
268
268
|
const { ScrcpyServerSession, scrcpyMaxSizeFromDims } = yield Promise.resolve().then(() => __importStar(require('./ScrcpyServerSession')));
|
|
269
|
-
const device = yield
|
|
269
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
270
270
|
const maxSize = scrcpyMaxSizeFromDims(Number(device === null || device === void 0 ? void 0 : device.screenWidth), Number(device === null || device === void 0 ? void 0 : device.screenHeight));
|
|
271
271
|
const parser = new h264NalParser_1.H264NalParser();
|
|
272
272
|
const session = new ScrcpyServerSession(udid);
|
|
@@ -405,7 +405,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
405
405
|
resolveCaptureSize(udid) {
|
|
406
406
|
return __awaiter(this, void 0, void 0, function* () {
|
|
407
407
|
try {
|
|
408
|
-
const device = yield
|
|
408
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
409
409
|
const sw = Number(device === null || device === void 0 ? void 0 : device.screenWidth);
|
|
410
410
|
const sh = Number(device === null || device === void 0 ? void 0 : device.screenHeight);
|
|
411
411
|
if (Number.isFinite(sw) && Number.isFinite(sh) && sw > 0 && sh > 0) {
|
|
@@ -59,6 +59,7 @@ const http_1 = __importDefault(require("http"));
|
|
|
59
59
|
const sharp_1 = __importDefault(require("sharp"));
|
|
60
60
|
const logger_1 = __importDefault(require("../../logger"));
|
|
61
61
|
const device_store_1 = require("../../data-service/device-store");
|
|
62
|
+
const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
62
63
|
const DeviceLockManager_1 = require("./DeviceLockManager");
|
|
63
64
|
const typedi_1 = require("typedi");
|
|
64
65
|
const ResourceIsolationService_1 = require("../../services/ResourceIsolationService");
|
|
@@ -272,7 +273,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
272
273
|
`(${decision.reason}); discarding it and starting fresh.`);
|
|
273
274
|
yield this.disposeSession(udid, existing);
|
|
274
275
|
}
|
|
275
|
-
const device = yield
|
|
276
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
276
277
|
if (!device)
|
|
277
278
|
throw new Error(`Device ${udid} not found in DB`);
|
|
278
279
|
// Resolve the real adb binary once, up front, so the capture loop never
|
|
@@ -315,7 +316,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
315
316
|
catch (_d) {
|
|
316
317
|
/* ignore */
|
|
317
318
|
}
|
|
318
|
-
yield portAllocator.release(mjpegPort).catch(() => undefined);
|
|
319
|
+
yield portAllocator.release(mjpegPort, udid).catch(() => undefined);
|
|
319
320
|
yield portAllocator.blockPort(mjpegPort, 'mjpeg').catch(() => undefined);
|
|
320
321
|
}
|
|
321
322
|
}
|
|
@@ -346,7 +347,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
346
347
|
logger_1.default.info(`[${udid}] MJPEG stream primed with first frame.`);
|
|
347
348
|
}
|
|
348
349
|
// Update device info in store to ensure other services (like VideoPipeline) can find the port
|
|
349
|
-
const updatedDevice = yield
|
|
350
|
+
const updatedDevice = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
350
351
|
if (updatedDevice) {
|
|
351
352
|
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(udid, updatedDevice.host, {
|
|
352
353
|
mjpegServerPort: mjpegPort,
|
|
@@ -393,7 +394,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
393
394
|
if (this.sessions.get(udid) === session)
|
|
394
395
|
this.sessions.delete(udid);
|
|
395
396
|
try {
|
|
396
|
-
yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort);
|
|
397
|
+
yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort, udid);
|
|
397
398
|
}
|
|
398
399
|
catch (e) {
|
|
399
400
|
logger_1.default.warn(`[${udid}] Failed to release mjpeg port lease while restarting stream: ${e}`);
|
|
@@ -420,7 +421,7 @@ let AndroidStreamService = class AndroidStreamService {
|
|
|
420
421
|
// 'mjpeg' lease is released here — the device-level 'system' port (leased
|
|
421
422
|
// by AndroidDeviceManager) belongs to the device lifecycle, not the stream.
|
|
422
423
|
try {
|
|
423
|
-
yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort);
|
|
424
|
+
yield typedi_1.Container.get(PortAllocator_1.PortAllocator).release(session.mjpegPort, udid);
|
|
424
425
|
}
|
|
425
426
|
catch (e) {
|
|
426
427
|
logger_1.default.warn(`[${udid}] Failed to release mjpeg port lease on stop: ${e}`);
|
|
@@ -16,7 +16,7 @@ exports.mayReleasePreviewHold = mayReleasePreviewHold;
|
|
|
16
16
|
exports.releaseIdlePreviewHold = releaseIdlePreviewHold;
|
|
17
17
|
const typedi_1 = require("typedi");
|
|
18
18
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
-
const
|
|
19
|
+
const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
20
20
|
const device_service_1 = require("../../data-service/device-service");
|
|
21
21
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
22
22
|
const recording_store_1 = require("../../services/recording/recording-store");
|
|
@@ -43,7 +43,7 @@ function releaseIdlePreviewHold(udid) {
|
|
|
43
43
|
return __awaiter(this, void 0, void 0, function* () {
|
|
44
44
|
var _a, _b, _c, _d, _e;
|
|
45
45
|
try {
|
|
46
|
-
const device = yield
|
|
46
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
47
47
|
if (!device)
|
|
48
48
|
return false;
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -76,6 +76,7 @@ const config_1 = require("../../config");
|
|
|
76
76
|
const device_service_1 = require("../../data-service/device-service");
|
|
77
77
|
const iOSTracker_1 = require("../iOSTracker");
|
|
78
78
|
const networkAddresses_1 = require("../../helpers/networkAddresses");
|
|
79
|
+
const localDeviceHosts_1 = require("../localDeviceHosts");
|
|
79
80
|
/**
|
|
80
81
|
* Should this simulator be given wda/mjpeg ports the moment we notice it?
|
|
81
82
|
*
|
|
@@ -152,9 +153,13 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
152
153
|
console.log('[POISON PILL] REAL fetchLocalIOSDevices CALLED IN TEST MODE!');
|
|
153
154
|
}
|
|
154
155
|
const devices = yield this.getConnectedDevices();
|
|
156
|
+
// This server's own row for a phone: the exact host getDeviceInfo files
|
|
157
|
+
// it under. A node on this machine sees the same iPhone and shares the
|
|
158
|
+
// IP, so its row for the udid must not be taken for ours.
|
|
159
|
+
const ownHost = (0, localDeviceHosts_1.iosRealDeviceHost)(this.pluginArgs, this.hostPort);
|
|
155
160
|
const deviceProcessingPromises = devices.map((udid) => __awaiter(this, void 0, void 0, function* () {
|
|
156
161
|
try {
|
|
157
|
-
const existingDevice = existingDeviceDetails.find((device) => device.udid === udid);
|
|
162
|
+
const existingDevice = existingDeviceDetails.find((device) => device.udid === udid && device.host === ownHost);
|
|
158
163
|
if (existingDevice) {
|
|
159
164
|
const networkIp = yield this.fetchRealDeviceNetworkIp(udid);
|
|
160
165
|
return Object.assign(Object.assign({}, existingDevice), { ip: networkIp || (0, networkAddresses_1.sanitizeDeviceNetworkIp)(existingDevice.ip) || '' });
|
|
@@ -176,10 +181,10 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
176
181
|
getDeviceInfo(udid) {
|
|
177
182
|
return __awaiter(this, void 0, void 0, function* () {
|
|
178
183
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
const host = (0, localDeviceHosts_1.iosRealDeviceHost)(this.pluginArgs, this.hostPort);
|
|
185
|
+
// Our own row only (udid AND host): it is spread over the result below,
|
|
186
|
+
// and another server's row would hand us its host, ports and busy state.
|
|
187
|
+
const storeDevice = yield store.findDevice({ udid, host });
|
|
183
188
|
// A real device is going to want these, so ask now — but not at the cost
|
|
184
189
|
// of the whole discovery pass. See tryAcquire: an exhausted range used to
|
|
185
190
|
// throw out of here and take the device list with it.
|
|
@@ -257,9 +262,11 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
257
262
|
}
|
|
258
263
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
259
264
|
const nodeLanIp = this.resolveNodeLanIp();
|
|
265
|
+
// A node on this Mac lists the same simulators; only our own row's ports.
|
|
266
|
+
const simHost = (0, localDeviceHosts_1.iosSimulatorHost)(this.pluginArgs, this.hostPort);
|
|
260
267
|
return yield Promise.all(simulators.map((d) => __awaiter(this, void 0, void 0, function* () {
|
|
261
268
|
var _a;
|
|
262
|
-
const storeDevice = yield store.findDevice({ udid: d.udid });
|
|
269
|
+
const storeDevice = yield store.findDevice({ udid: d.udid, host: simHost });
|
|
263
270
|
const willRunWda = simulatorNeedsPortNow(d.state);
|
|
264
271
|
return Object.assign(Object.assign(Object.assign({}, d), (0, appleIdentity_1.simulatorIdentity)(d.name)), { wdaLocalPort: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.wdaLocalPort) ||
|
|
265
272
|
(willRunWda
|
|
@@ -267,7 +274,7 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
267
274
|
: undefined), mjpegServerPort: (storeDevice === null || storeDevice === void 0 ? void 0 : storeDevice.mjpegServerPort) ||
|
|
268
275
|
(willRunWda
|
|
269
276
|
? yield typedi_1.Container.get(PortAllocator_1.PortAllocator).tryAcquire('mjpeg', d.udid)
|
|
270
|
-
: undefined), busy: false, realDevice: false, platform: (((_a = d.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('tv')) ? 'tvos' : 'ios'), deviceType: 'simulator', host:
|
|
277
|
+
: undefined), busy: false, realDevice: false, platform: (((_a = d.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes('tv')) ? 'tvos' : 'ios'), deviceType: 'simulator', host: simHost, ip: nodeLanIp, totalUtilizationTimeMilliSec: yield (0, device_utils_1.getUtilizationTime)(d.udid), sessionStartTime: 0 });
|
|
271
278
|
})));
|
|
272
279
|
});
|
|
273
280
|
}
|
|
@@ -323,7 +330,11 @@ let IOSDiscoveryService = class IOSDiscoveryService {
|
|
|
323
330
|
}
|
|
324
331
|
}));
|
|
325
332
|
tracker.on('detached', (udid) => __awaiter(this, void 0, void 0, function* () {
|
|
326
|
-
|
|
333
|
+
// The host 'attached' filed it under (getDeviceInfo), exact: a bare IP
|
|
334
|
+
// is matched as a substring and takes other servers' rows with it.
|
|
335
|
+
const deviceRemoved = [
|
|
336
|
+
{ udid, host: (0, localDeviceHosts_1.iosRealDeviceHost)(this.pluginArgs, this.hostPort), nodeId: this.nodeId },
|
|
337
|
+
];
|
|
327
338
|
if (this.pluginArgs.hub) {
|
|
328
339
|
yield new NodeDevices_1.default(this.pluginArgs.hub, {
|
|
329
340
|
tlsRejectUnauthorized: this.pluginArgs.tlsRejectUnauthorized,
|
|
@@ -60,8 +60,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
60
60
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
61
61
|
};
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
-
exports.IOS_IDLE_STOP_MS = void 0;
|
|
63
|
+
exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
|
|
64
64
|
exports.shouldStopIdleIosStream = shouldStopIdleIosStream;
|
|
65
|
+
exports.heldByAppiumSession = heldByAppiumSession;
|
|
65
66
|
const typedi_1 = require("typedi");
|
|
66
67
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
67
68
|
const child_process_1 = require("child_process");
|
|
@@ -77,6 +78,7 @@ const helpers_1 = require("../../helpers");
|
|
|
77
78
|
const singleFlight_1 = require("../../helpers/singleFlight");
|
|
78
79
|
const PortAllocator_1 = require("../../services/PortAllocator");
|
|
79
80
|
const device_store_1 = require("../../data-service/device-store");
|
|
81
|
+
const ownDeviceRow_1 = require("../ownDeviceRow");
|
|
80
82
|
const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
81
83
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
82
84
|
const device_service_1 = require("../../data-service/device-service");
|
|
@@ -98,6 +100,25 @@ function shouldStopIdleIosStream(use) {
|
|
|
98
100
|
return false;
|
|
99
101
|
return !use.sessionId || (0, manualLock_1.isManualLock)(use.sessionId);
|
|
100
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a live Appium session holds the phone: busy under a session id that
|
|
105
|
+
* isn't a live-preview hold. A claim still waiting for its session id isn't
|
|
106
|
+
* one yet.
|
|
107
|
+
*/
|
|
108
|
+
function heldByAppiumSession(device) {
|
|
109
|
+
return !!(device === null || device === void 0 ? void 0 : device.busy) && !!device.session_id && !(0, manualLock_1.isManualLock)(device.session_id);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The ports go-ios's tunnel process listens on: its tunnel-info API
|
|
113
|
+
* (`GO_IOS_AGENT_PORT`, default 60105) and the first phone's userspace tunnel
|
|
114
|
+
* (API port + 1). On iOS 17+ every go-ios command that talks to the phone,
|
|
115
|
+
* `runwda` included, goes through that process.
|
|
116
|
+
*/
|
|
117
|
+
const GO_IOS_AGENT_PORTS = [60105, 60106];
|
|
118
|
+
/** A start refused a restart because an Appium session holds the phone. */
|
|
119
|
+
class StreamRestartRefused extends Error {
|
|
120
|
+
}
|
|
121
|
+
exports.StreamRestartRefused = StreamRestartRefused;
|
|
101
122
|
let IOSStreamService = class IOSStreamService {
|
|
102
123
|
constructor() {
|
|
103
124
|
this.sessions = new Map();
|
|
@@ -146,7 +167,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
146
167
|
logger_1.default.warn(`Stream watchdog detected failure for ${udid}. Attempting autonomous healing...`);
|
|
147
168
|
try {
|
|
148
169
|
// Get device state to check for Session Shield
|
|
149
|
-
const device = yield
|
|
170
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
150
171
|
if (device && device.busy) {
|
|
151
172
|
logger_1.default.info(`🛡️ [Watchdog] Skipping heal for ${udid} as it has an active session.`);
|
|
152
173
|
continue;
|
|
@@ -184,7 +205,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
184
205
|
if (session.viewerCount > 0 || idleMs <= exports.IOS_IDLE_STOP_MS)
|
|
185
206
|
continue;
|
|
186
207
|
try {
|
|
187
|
-
const device = yield
|
|
208
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
188
209
|
const recording = yield typedi_2.Container.get(recording_store_1.RecordingStore).isRecording(udid);
|
|
189
210
|
const stop = shouldStopIdleIosStream({
|
|
190
211
|
idleMs,
|
|
@@ -329,20 +350,28 @@ let IOSStreamService = class IOSStreamService {
|
|
|
329
350
|
});
|
|
330
351
|
}
|
|
331
352
|
/**
|
|
332
|
-
* Kill
|
|
333
|
-
*
|
|
353
|
+
* Kill go-ios tunnel processes left over from earlier runs, so a new tunnel
|
|
354
|
+
* can bind the agent's ports ('address already in use' otherwise).
|
|
355
|
+
*
|
|
356
|
+
* Never while an Appium session holds the phone. That session may be
|
|
357
|
+
* driving a WDA go-ios launched (iOSCapabilities points a session at the
|
|
358
|
+
* stream's WDA whenever a stream runs), and on iOS 17+ that WDA reaches the
|
|
359
|
+
* phone through the tunnel listening on these ports. Nothing here can tell
|
|
360
|
+
* that tunnel from an orphan, so the sweep waits for the next stop or start
|
|
361
|
+
* after the session ends; a restart reaps every go-ios process at boot.
|
|
334
362
|
*/
|
|
335
363
|
cleanupOrphanTunnels(udid) {
|
|
336
364
|
return __awaiter(this, void 0, void 0, function* () {
|
|
365
|
+
if (yield this.appiumSessionMayUse(udid, 'go-ios tunnels'))
|
|
366
|
+
return;
|
|
337
367
|
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
338
368
|
// Reap the tunnel process *group* for this udid so the self-forking go-ios
|
|
339
369
|
// agent children (whose argv carries no udid, so a udid-scoped pkill can't
|
|
340
370
|
// see them) die with their parent instead of orphaning. Group-scoped, so a
|
|
341
371
|
// second device's tunnel is left untouched. See ./tunnelProcess.
|
|
342
372
|
yield (0, tunnelProcess_1.reapTunnelsForUdid)(udid, execPromise);
|
|
343
|
-
//
|
|
344
|
-
const
|
|
345
|
-
for (const port of agentPorts) {
|
|
373
|
+
// Whatever still listens on the go-ios agent's ports.
|
|
374
|
+
for (const port of GO_IOS_AGENT_PORTS) {
|
|
346
375
|
try {
|
|
347
376
|
const { stdout } = yield execPromise(`lsof -ti :${port}`);
|
|
348
377
|
const pids = stdout.trim().split('\n');
|
|
@@ -361,14 +390,65 @@ let IOSStreamService = class IOSStreamService {
|
|
|
361
390
|
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
362
391
|
});
|
|
363
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Whether an Appium session may be using what the caller is about to kill:
|
|
395
|
+
* one holds the phone, or the phone's row can't be read, in which case
|
|
396
|
+
* nothing is known and nothing is killed.
|
|
397
|
+
*/
|
|
398
|
+
appiumSessionMayUse(udid, what) {
|
|
399
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
400
|
+
var _a;
|
|
401
|
+
try {
|
|
402
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
403
|
+
if (!heldByAppiumSession(device))
|
|
404
|
+
return false;
|
|
405
|
+
logger_1.default.info(`[${udid}] Leaving ${what} alone: Appium session ${device === null || device === void 0 ? void 0 : device.session_id} holds the device and may be using them`);
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
logger_1.default.warn(`[${udid}] Leaving ${what} alone: could not read the device: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Throw StreamRestartRefused when an Appium session holds the phone, as the
|
|
415
|
+
* watchdog's heal already skips a busy device. A restart would kill the WDA
|
|
416
|
+
* and go-ios tunnel the session may be driving, or, on a stream attached to
|
|
417
|
+
* the session's own WDA, launch a second WDA over it. A WDA that is only
|
|
418
|
+
* slow to answer /status (a long command) would be killed for nothing. The
|
|
419
|
+
* stream is left as it is; it can restart once the session ends. A row that
|
|
420
|
+
* can't be read refuses too.
|
|
421
|
+
*/
|
|
422
|
+
refuseRestartUnderAppiumSession(udid) {
|
|
423
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
424
|
+
var _a;
|
|
425
|
+
let device;
|
|
426
|
+
try {
|
|
427
|
+
device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
throw new StreamRestartRefused(`Stream for ${udid} is not answering, and the device can't be read to check for an Appium session (${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}): not restarting it.`);
|
|
431
|
+
}
|
|
432
|
+
if (!heldByAppiumSession(device))
|
|
433
|
+
return;
|
|
434
|
+
throw new StreamRestartRefused(`Stream for ${udid} is not answering, but Appium session ${device === null || device === void 0 ? void 0 : device.session_id} holds the device: not restarting the WDA and go-ios tunnel it may be driving. The stream can restart once the session ends.`);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
364
437
|
/**
|
|
365
438
|
* Check if WDA is already running and responding
|
|
366
439
|
* Principal Resilience: Retries transient connection errors (ECONNRESET) up to 2 times
|
|
367
440
|
* with exponential backoff, as these often indicate WDA is restarting or tunnel is reconnecting.
|
|
441
|
+
*
|
|
442
|
+
* It says whether *a* WDA answers on the port, never whose. WDA names no
|
|
443
|
+
* phone: /status carries os, ios.ip, build and device (the form factor),
|
|
444
|
+
* and /wda/device/info's `uuid` is identifierForVendor, not the UDID. So it
|
|
445
|
+
* takes no udid. A caller that must know the phone relies on the port
|
|
446
|
+
* being one it already knows belongs to it: its own stream's leased port,
|
|
447
|
+
* or the Device row's port while an Appium session holds that phone.
|
|
368
448
|
*/
|
|
369
|
-
isWDARunning(wdaPort_1
|
|
370
|
-
return __awaiter(this, arguments, void 0, function* (wdaPort,
|
|
371
|
-
var _a, _b, _c, _d
|
|
449
|
+
isWDARunning(wdaPort_1) {
|
|
450
|
+
return __awaiter(this, arguments, void 0, function* (wdaPort, retries = 2) {
|
|
451
|
+
var _a, _b, _c, _d;
|
|
372
452
|
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
373
453
|
const host = '127.0.0.1'; // Force IPv4 for local tunnels
|
|
374
454
|
const maxRetries = retries;
|
|
@@ -380,15 +460,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
380
460
|
httpAgent: new http_1.default.Agent({ keepAlive: false }),
|
|
381
461
|
validateStatus: (status) => status === 200,
|
|
382
462
|
});
|
|
383
|
-
|
|
384
|
-
const remoteUdid = (_c = (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ios) === null || _c === void 0 ? void 0 : _c.udid;
|
|
385
|
-
if (udid && remoteUdid && remoteUdid !== udid) {
|
|
386
|
-
logger_1.default.warn(`[WDA] Port ${wdaPort} is used by a DIFFERENT device: ${remoteUdid} (expected ${udid})`);
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
const isReady = ((_e = (_d = response.data) === null || _d === void 0 ? void 0 : _d.value) === null || _e === void 0 ? void 0 : _e.ready) === true;
|
|
463
|
+
const isReady = ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
390
464
|
if (!isReady) {
|
|
391
|
-
logger_1.default.debug(`[WDA] Port ${wdaPort} active but not ready. Status: ${JSON.stringify((
|
|
465
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} active but not ready. Status: ${JSON.stringify((_c = response.data) === null || _c === void 0 ? void 0 : _c.value)}`);
|
|
392
466
|
}
|
|
393
467
|
return isReady;
|
|
394
468
|
}
|
|
@@ -400,7 +474,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
400
474
|
(error.response && error.response.status >= 500);
|
|
401
475
|
if (isTransientError && attempt < maxRetries) {
|
|
402
476
|
const backoffMs = Math.min(500 * Math.pow(2, attempt), 2000); // 500ms, 1000ms, 2000ms max
|
|
403
|
-
logger_1.default.debug(`[WDA] Port ${wdaPort} transient error (${error.code || ((
|
|
477
|
+
logger_1.default.debug(`[WDA] Port ${wdaPort} transient error (${error.code || ((_d = error.response) === null || _d === void 0 ? void 0 : _d.status)}), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
404
478
|
yield new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
405
479
|
continue;
|
|
406
480
|
}
|
|
@@ -443,7 +517,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
443
517
|
}
|
|
444
518
|
if (!isWdaIproxyAlive || !isMjpegIproxyAlive) {
|
|
445
519
|
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Tunnel processes are dead. Attempting tunnel-only recovery...`);
|
|
446
|
-
const device = yield
|
|
520
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
447
521
|
if (device && device.ip) {
|
|
448
522
|
// Double check if WDA is alive via network IP
|
|
449
523
|
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
@@ -459,7 +533,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
459
533
|
const isRespondingViaTunnel = yield this.isWDARunning(session.wdaPort);
|
|
460
534
|
if (!isRespondingViaTunnel) {
|
|
461
535
|
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA tunnel on port ${session.wdaPort} is unresponsive. checking network...`);
|
|
462
|
-
const device = yield
|
|
536
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
463
537
|
if (device && device.ip) {
|
|
464
538
|
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
465
539
|
if (isWdaAccessibleViaNetwork) {
|
|
@@ -543,6 +617,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
543
617
|
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
544
618
|
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
545
619
|
}
|
|
620
|
+
// Never under a live Appium session (see refuseRestartUnderAppiumSession).
|
|
621
|
+
yield this.refuseRestartUnderAppiumSession(udid);
|
|
546
622
|
// Stream exists but unhealthy - check cooldown before recovery
|
|
547
623
|
if (!this.canAttemptRecovery(udid)) {
|
|
548
624
|
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
@@ -578,6 +654,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
578
654
|
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
579
655
|
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
580
656
|
}
|
|
657
|
+
yield this.refuseRestartUnderAppiumSession(udid);
|
|
581
658
|
// Stream exists but unhealthy - check cooldown before recovery
|
|
582
659
|
if (!this.canAttemptRecovery(udid)) {
|
|
583
660
|
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
@@ -588,10 +665,17 @@ let IOSStreamService = class IOSStreamService {
|
|
|
588
665
|
logger_1.default.info(`Existing session for ${udid} not responding, restarting...`);
|
|
589
666
|
this.markRecoveryAttempt(udid);
|
|
590
667
|
}
|
|
591
|
-
const device = yield
|
|
668
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
592
669
|
if (!device)
|
|
593
670
|
throw new Error(`Device ${udid} not found`);
|
|
594
|
-
//
|
|
671
|
+
// Tear down this udid's previous session BEFORE acquiring ports, never
|
|
672
|
+
// after. acquire() hands a udid back its own existing leases, so a
|
|
673
|
+
// failed start's session names the very port numbers this start is
|
|
674
|
+
// about to get. Stopped after acquiring, its release() deleted this
|
|
675
|
+
// start's leases: the stream ran on unleased ports, and the allocator
|
|
676
|
+
// gave its 9100 to the S9+. A no-op when there is no previous session.
|
|
677
|
+
yield this.stopStream(udid);
|
|
678
|
+
// Resolve stream ports through the PortAllocator — never a value
|
|
595
679
|
// persisted on the Device row. A stale persisted mjpegServerPort (e.g.
|
|
596
680
|
// 9100) bypassed the allocator and could collide with another device's
|
|
597
681
|
// live lease on the same port (the Android stream leases 9100 too),
|
|
@@ -599,13 +683,29 @@ let IOSStreamService = class IOSStreamService {
|
|
|
599
683
|
// guarantee a non-colliding port; the lease is refreshed by the watchdog
|
|
600
684
|
// and released in stopStream().
|
|
601
685
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
686
|
+
const ttl = { ttlMs: this.STREAM_PORT_TTL_MS };
|
|
687
|
+
// Principal Discovery: attach to the WDA an Appium session runs on this
|
|
688
|
+
// device instead of launching a second one over it. The one exception
|
|
689
|
+
// to the rule above: that session's forwarder listens on the Device
|
|
690
|
+
// row's wdaLocalPort (iOSCapabilities hands it to Appium), and acquire()
|
|
691
|
+
// can't find it, because it refuses any port with a live listener, this
|
|
692
|
+
// device's own included. Only while an Appium session holds the device:
|
|
693
|
+
// WDA names no phone (see isWDARunning), so that session is the only
|
|
694
|
+
// evidence the port is this phone's. Outside one, an answer on a stale
|
|
695
|
+
// row's port may be another phone's WDA.
|
|
696
|
+
const appiumWdaPort = heldByAppiumSession(device) ? device.wdaLocalPort : undefined;
|
|
697
|
+
const alreadyUp = !!appiumWdaPort && (yield this.isWDARunning(appiumWdaPort));
|
|
698
|
+
let wdaPort;
|
|
699
|
+
if (appiumWdaPort && alreadyUp) {
|
|
700
|
+
wdaPort = appiumWdaPort;
|
|
701
|
+
if (!(yield portAllocator.claim('wda', udid, wdaPort, ttl))) {
|
|
702
|
+
logger_1.default.warn(`[${udid}] WDA port ${wdaPort} is leased to another device; attaching unleased`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
wdaPort = yield portAllocator.acquire('wda', udid, ttl);
|
|
707
|
+
}
|
|
708
|
+
const mjpegPort = yield portAllocator.acquire('mjpeg', udid, ttl);
|
|
609
709
|
if (alreadyUp) {
|
|
610
710
|
logger_1.default.info(`[${udid}] WDA already responding on port ${wdaPort}. Attaching to existing tunnel...`);
|
|
611
711
|
// WDA reachable on 127.0.0.1:wdaPort means the WDA iproxy is live, but the
|
|
@@ -653,7 +753,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
653
753
|
return { wdaPort, mjpegPort };
|
|
654
754
|
}
|
|
655
755
|
// Perform aggressive cleanup of any existing processes for THIS device/ports
|
|
656
|
-
|
|
756
|
+
// (the previous session was already stopped, before the ports were acquired)
|
|
657
757
|
yield this.killStaleProcesses(udid, wdaPort, mjpegPort);
|
|
658
758
|
const session = {
|
|
659
759
|
udid,
|
|
@@ -788,7 +888,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
788
888
|
logger_1.default.warn(`Failed to get screen size for ${udid}: ${e}`);
|
|
789
889
|
}
|
|
790
890
|
// Update device info in store
|
|
791
|
-
const device = yield
|
|
891
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
792
892
|
if (device) {
|
|
793
893
|
const updateData = {
|
|
794
894
|
wdaLocalPort: wdaPort,
|
|
@@ -836,6 +936,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
836
936
|
throw new Error(`WDA failed to start within ${timeout / 1000}s. Check logs.`);
|
|
837
937
|
}
|
|
838
938
|
catch (error) {
|
|
939
|
+
// Not a failed start: the running stream is left exactly as it was.
|
|
940
|
+
if (error instanceof StreamRestartRefused)
|
|
941
|
+
throw error;
|
|
839
942
|
const session = this.sessions.get(udid);
|
|
840
943
|
if (session) {
|
|
841
944
|
session.status = 'error';
|
|
@@ -908,12 +1011,29 @@ let IOSStreamService = class IOSStreamService {
|
|
|
908
1011
|
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
909
1012
|
});
|
|
910
1013
|
}
|
|
911
|
-
|
|
912
|
-
|
|
1014
|
+
/**
|
|
1015
|
+
* Stop this udid's stream: its processes, a live-preview hold, the go-ios
|
|
1016
|
+
* orphan sweep (see cleanupOrphanTunnels) and its port leases.
|
|
1017
|
+
*
|
|
1018
|
+
* `forViewer` means the caller is only done watching (stream/stop,
|
|
1019
|
+
* stream/leave). While an Appium session holds the phone and this stream
|
|
1020
|
+
* launched its own WDA, nothing is stopped: a session allocated while the
|
|
1021
|
+
* stream ran drives that WDA, through this stream's forwarder and go-ios
|
|
1022
|
+
* tunnel. The session's teardown stops it (EventManager
|
|
1023
|
+
* stopIdleStreamForDevice), or the idle watchdog once the session has
|
|
1024
|
+
* ended. A restart never passes it: the WDA already failed its health check.
|
|
1025
|
+
*/
|
|
1026
|
+
stopStream(udid_1) {
|
|
1027
|
+
return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
|
|
913
1028
|
var _a, _b;
|
|
914
1029
|
const session = this.sessions.get(udid);
|
|
915
1030
|
if (!session)
|
|
916
1031
|
return;
|
|
1032
|
+
if (opts.forViewer &&
|
|
1033
|
+
session.wdaProcess &&
|
|
1034
|
+
(yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
917
1037
|
// Kill sidecar processes. The go-ios tunnel is detached (its own process
|
|
918
1038
|
// group), so reap the whole group — a plain p.kill() would leave the
|
|
919
1039
|
// self-forking agent children behind to respawn. See ./tunnelProcess.
|
|
@@ -931,7 +1051,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
931
1051
|
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
932
1052
|
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
933
1053
|
try {
|
|
934
|
-
const device = yield
|
|
1054
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
935
1055
|
if (device && ((_b = device.session_id) === null || _b === void 0 ? void 0 : _b.startsWith('manual_'))) {
|
|
936
1056
|
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
937
1057
|
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
@@ -949,8 +1069,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
949
1069
|
// devices — they were held for the lifetime of this stream.
|
|
950
1070
|
try {
|
|
951
1071
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
952
|
-
yield portAllocator.release(session.wdaPort);
|
|
953
|
-
yield portAllocator.release(session.mjpegPort);
|
|
1072
|
+
yield portAllocator.release(session.wdaPort, udid);
|
|
1073
|
+
yield portAllocator.release(session.mjpegPort, udid);
|
|
954
1074
|
}
|
|
955
1075
|
catch (e) {
|
|
956
1076
|
logger_1.default.warn(`Failed to release port leases for ${udid}: ${e}`);
|