@xenon-device-management/xenon 2.0.0 → 2.2.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 +3 -4
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.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 +11 -104
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +43 -12
- package/lib/src/app/ws/logcatWs.js +48 -14
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- 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 +92 -10
- package/lib/src/data-service/deviceClaims.js +116 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +102 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
- 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 +13 -5
- package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
- 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 +162 -40
- 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/deviceAccessGuard.js +4 -1
- 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 +63 -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/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -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/own-node-id.js +29 -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/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +56 -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 +24 -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 +22 -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 +66 -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 +608 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +426 -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 +6 -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 +107 -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 +12 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -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/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -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 +19 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
- package/lib/test/unit/stream-stop-recording.spec.js +17 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- 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 +3 -4
- 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
|
@@ -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();
|
|
@@ -117,7 +138,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
117
138
|
startWatchdog() {
|
|
118
139
|
// Every minute, and cheap: an unwatched preview's hold was held for good,
|
|
119
140
|
// because the only idle check ran hourly and kept every busy device.
|
|
120
|
-
|
|
141
|
+
// unref()ed: the server's listener keeps the process alive; these only
|
|
142
|
+
// need to fire while it runs.
|
|
143
|
+
setInterval(() => void this.sweepIdle(), 60000).unref();
|
|
121
144
|
// Hourly: port leases and the stream health check, which probes WDA.
|
|
122
145
|
setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
123
146
|
for (const [udid, session] of this.sessions.entries()) {
|
|
@@ -146,7 +169,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
146
169
|
logger_1.default.warn(`Stream watchdog detected failure for ${udid}. Attempting autonomous healing...`);
|
|
147
170
|
try {
|
|
148
171
|
// Get device state to check for Session Shield
|
|
149
|
-
const device = yield
|
|
172
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
150
173
|
if (device && device.busy) {
|
|
151
174
|
logger_1.default.info(`🛡️ [Watchdog] Skipping heal for ${udid} as it has an active session.`);
|
|
152
175
|
continue;
|
|
@@ -166,7 +189,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
191
|
}
|
|
169
|
-
}), 3600000); // 1hr interval for background stability
|
|
192
|
+
}), 3600000).unref(); // 1hr interval for background stability
|
|
170
193
|
}
|
|
171
194
|
/**
|
|
172
195
|
* Stop streams nobody has watched for 10 minutes, which releases an
|
|
@@ -184,7 +207,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
184
207
|
if (session.viewerCount > 0 || idleMs <= exports.IOS_IDLE_STOP_MS)
|
|
185
208
|
continue;
|
|
186
209
|
try {
|
|
187
|
-
const device = yield
|
|
210
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
188
211
|
const recording = yield typedi_2.Container.get(recording_store_1.RecordingStore).isRecording(udid);
|
|
189
212
|
const stop = shouldStopIdleIosStream({
|
|
190
213
|
idleMs,
|
|
@@ -329,20 +352,28 @@ let IOSStreamService = class IOSStreamService {
|
|
|
329
352
|
});
|
|
330
353
|
}
|
|
331
354
|
/**
|
|
332
|
-
* Kill
|
|
333
|
-
*
|
|
355
|
+
* Kill go-ios tunnel processes left over from earlier runs, so a new tunnel
|
|
356
|
+
* can bind the agent's ports ('address already in use' otherwise).
|
|
357
|
+
*
|
|
358
|
+
* Never while an Appium session holds the phone. That session may be
|
|
359
|
+
* driving a WDA go-ios launched (iOSCapabilities points a session at the
|
|
360
|
+
* stream's WDA whenever a stream runs), and on iOS 17+ that WDA reaches the
|
|
361
|
+
* phone through the tunnel listening on these ports. Nothing here can tell
|
|
362
|
+
* that tunnel from an orphan, so the sweep waits for the next stop or start
|
|
363
|
+
* after the session ends; a restart reaps every go-ios process at boot.
|
|
334
364
|
*/
|
|
335
365
|
cleanupOrphanTunnels(udid) {
|
|
336
366
|
return __awaiter(this, void 0, void 0, function* () {
|
|
367
|
+
if (yield this.appiumSessionMayUse(udid, 'go-ios tunnels'))
|
|
368
|
+
return;
|
|
337
369
|
logger_1.default.debug(`Cleaning up orphan tunnels for ${udid}...`);
|
|
338
370
|
// Reap the tunnel process *group* for this udid so the self-forking go-ios
|
|
339
371
|
// agent children (whose argv carries no udid, so a udid-scoped pkill can't
|
|
340
372
|
// see them) die with their parent instead of orphaning. Group-scoped, so a
|
|
341
373
|
// second device's tunnel is left untouched. See ./tunnelProcess.
|
|
342
374
|
yield (0, tunnelProcess_1.reapTunnelsForUdid)(udid, execPromise);
|
|
343
|
-
//
|
|
344
|
-
const
|
|
345
|
-
for (const port of agentPorts) {
|
|
375
|
+
// Whatever still listens on the go-ios agent's ports.
|
|
376
|
+
for (const port of GO_IOS_AGENT_PORTS) {
|
|
346
377
|
try {
|
|
347
378
|
const { stdout } = yield execPromise(`lsof -ti :${port}`);
|
|
348
379
|
const pids = stdout.trim().split('\n');
|
|
@@ -361,14 +392,65 @@ let IOSStreamService = class IOSStreamService {
|
|
|
361
392
|
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
362
393
|
});
|
|
363
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Whether an Appium session may be using what the caller is about to kill:
|
|
397
|
+
* one holds the phone, or the phone's row can't be read, in which case
|
|
398
|
+
* nothing is known and nothing is killed.
|
|
399
|
+
*/
|
|
400
|
+
appiumSessionMayUse(udid, what) {
|
|
401
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
402
|
+
var _a;
|
|
403
|
+
try {
|
|
404
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
405
|
+
if (!heldByAppiumSession(device))
|
|
406
|
+
return false;
|
|
407
|
+
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`);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
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}`);
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Throw StreamRestartRefused when an Appium session holds the phone, as the
|
|
417
|
+
* watchdog's heal already skips a busy device. A restart would kill the WDA
|
|
418
|
+
* and go-ios tunnel the session may be driving, or, on a stream attached to
|
|
419
|
+
* the session's own WDA, launch a second WDA over it. A WDA that is only
|
|
420
|
+
* slow to answer /status (a long command) would be killed for nothing. The
|
|
421
|
+
* stream is left as it is; it can restart once the session ends. A row that
|
|
422
|
+
* can't be read refuses too.
|
|
423
|
+
*/
|
|
424
|
+
refuseRestartUnderAppiumSession(udid) {
|
|
425
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
426
|
+
var _a;
|
|
427
|
+
let device;
|
|
428
|
+
try {
|
|
429
|
+
device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
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.`);
|
|
433
|
+
}
|
|
434
|
+
if (!heldByAppiumSession(device))
|
|
435
|
+
return;
|
|
436
|
+
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.`);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
364
439
|
/**
|
|
365
440
|
* Check if WDA is already running and responding
|
|
366
441
|
* Principal Resilience: Retries transient connection errors (ECONNRESET) up to 2 times
|
|
367
442
|
* with exponential backoff, as these often indicate WDA is restarting or tunnel is reconnecting.
|
|
443
|
+
*
|
|
444
|
+
* It says whether *a* WDA answers on the port, never whose. WDA names no
|
|
445
|
+
* phone: /status carries os, ios.ip, build and device (the form factor),
|
|
446
|
+
* and /wda/device/info's `uuid` is identifierForVendor, not the UDID. So it
|
|
447
|
+
* takes no udid. A caller that must know the phone relies on the port
|
|
448
|
+
* being one it already knows belongs to it: its own stream's leased port,
|
|
449
|
+
* or the Device row's port while an Appium session holds that phone.
|
|
368
450
|
*/
|
|
369
|
-
isWDARunning(wdaPort_1
|
|
370
|
-
return __awaiter(this, arguments, void 0, function* (wdaPort,
|
|
371
|
-
var _a, _b, _c, _d
|
|
451
|
+
isWDARunning(wdaPort_1) {
|
|
452
|
+
return __awaiter(this, arguments, void 0, function* (wdaPort, retries = 2) {
|
|
453
|
+
var _a, _b, _c, _d;
|
|
372
454
|
const axios = (yield Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
373
455
|
const host = '127.0.0.1'; // Force IPv4 for local tunnels
|
|
374
456
|
const maxRetries = retries;
|
|
@@ -380,15 +462,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
380
462
|
httpAgent: new http_1.default.Agent({ keepAlive: false }),
|
|
381
463
|
validateStatus: (status) => status === 200,
|
|
382
464
|
});
|
|
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;
|
|
465
|
+
const isReady = ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.ready) === true;
|
|
390
466
|
if (!isReady) {
|
|
391
|
-
logger_1.default.debug(`[WDA] Port ${wdaPort} active but not ready. Status: ${JSON.stringify((
|
|
467
|
+
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
468
|
}
|
|
393
469
|
return isReady;
|
|
394
470
|
}
|
|
@@ -400,7 +476,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
400
476
|
(error.response && error.response.status >= 500);
|
|
401
477
|
if (isTransientError && attempt < maxRetries) {
|
|
402
478
|
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 || ((
|
|
479
|
+
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
480
|
yield new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
405
481
|
continue;
|
|
406
482
|
}
|
|
@@ -443,7 +519,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
443
519
|
}
|
|
444
520
|
if (!isWdaIproxyAlive || !isMjpegIproxyAlive) {
|
|
445
521
|
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] Tunnel processes are dead. Attempting tunnel-only recovery...`);
|
|
446
|
-
const device = yield
|
|
522
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
447
523
|
if (device && device.ip) {
|
|
448
524
|
// Double check if WDA is alive via network IP
|
|
449
525
|
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
@@ -459,7 +535,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
459
535
|
const isRespondingViaTunnel = yield this.isWDARunning(session.wdaPort);
|
|
460
536
|
if (!isRespondingViaTunnel) {
|
|
461
537
|
logger_1.default.warn(`🛡️ [${udid}] [Watchdog] WDA tunnel on port ${session.wdaPort} is unresponsive. checking network...`);
|
|
462
|
-
const device = yield
|
|
538
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
463
539
|
if (device && device.ip) {
|
|
464
540
|
const isWdaAccessibleViaNetwork = yield this.isWDARunningOnHost(device.ip, 8100);
|
|
465
541
|
if (isWdaAccessibleViaNetwork) {
|
|
@@ -543,6 +619,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
543
619
|
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
544
620
|
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
545
621
|
}
|
|
622
|
+
// Never under a live Appium session (see refuseRestartUnderAppiumSession).
|
|
623
|
+
yield this.refuseRestartUnderAppiumSession(udid);
|
|
546
624
|
// Stream exists but unhealthy - check cooldown before recovery
|
|
547
625
|
if (!this.canAttemptRecovery(udid)) {
|
|
548
626
|
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
@@ -578,6 +656,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
578
656
|
logger_1.default.debug(`[${udid}] Stream already running and healthy, reusing existing session`);
|
|
579
657
|
return { wdaPort: existingSession.wdaPort, mjpegPort: existingSession.mjpegPort };
|
|
580
658
|
}
|
|
659
|
+
yield this.refuseRestartUnderAppiumSession(udid);
|
|
581
660
|
// Stream exists but unhealthy - check cooldown before recovery
|
|
582
661
|
if (!this.canAttemptRecovery(udid)) {
|
|
583
662
|
const lastAttempt = this.recoveryCooldowns.get(udid);
|
|
@@ -588,10 +667,17 @@ let IOSStreamService = class IOSStreamService {
|
|
|
588
667
|
logger_1.default.info(`Existing session for ${udid} not responding, restarting...`);
|
|
589
668
|
this.markRecoveryAttempt(udid);
|
|
590
669
|
}
|
|
591
|
-
const device = yield
|
|
670
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
592
671
|
if (!device)
|
|
593
672
|
throw new Error(`Device ${udid} not found`);
|
|
594
|
-
//
|
|
673
|
+
// Tear down this udid's previous session BEFORE acquiring ports, never
|
|
674
|
+
// after. acquire() hands a udid back its own existing leases, so a
|
|
675
|
+
// failed start's session names the very port numbers this start is
|
|
676
|
+
// about to get. Stopped after acquiring, its release() deleted this
|
|
677
|
+
// start's leases: the stream ran on unleased ports, and the allocator
|
|
678
|
+
// gave its 9100 to the S9+. A no-op when there is no previous session.
|
|
679
|
+
yield this.stopStream(udid);
|
|
680
|
+
// Resolve stream ports through the PortAllocator — never a value
|
|
595
681
|
// persisted on the Device row. A stale persisted mjpegServerPort (e.g.
|
|
596
682
|
// 9100) bypassed the allocator and could collide with another device's
|
|
597
683
|
// live lease on the same port (the Android stream leases 9100 too),
|
|
@@ -599,13 +685,29 @@ let IOSStreamService = class IOSStreamService {
|
|
|
599
685
|
// guarantee a non-colliding port; the lease is refreshed by the watchdog
|
|
600
686
|
// and released in stopStream().
|
|
601
687
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
688
|
+
const ttl = { ttlMs: this.STREAM_PORT_TTL_MS };
|
|
689
|
+
// Principal Discovery: attach to the WDA an Appium session runs on this
|
|
690
|
+
// device instead of launching a second one over it. The one exception
|
|
691
|
+
// to the rule above: that session's forwarder listens on the Device
|
|
692
|
+
// row's wdaLocalPort (iOSCapabilities hands it to Appium), and acquire()
|
|
693
|
+
// can't find it, because it refuses any port with a live listener, this
|
|
694
|
+
// device's own included. Only while an Appium session holds the device:
|
|
695
|
+
// WDA names no phone (see isWDARunning), so that session is the only
|
|
696
|
+
// evidence the port is this phone's. Outside one, an answer on a stale
|
|
697
|
+
// row's port may be another phone's WDA.
|
|
698
|
+
const appiumWdaPort = heldByAppiumSession(device) ? device.wdaLocalPort : undefined;
|
|
699
|
+
const alreadyUp = !!appiumWdaPort && (yield this.isWDARunning(appiumWdaPort));
|
|
700
|
+
let wdaPort;
|
|
701
|
+
if (appiumWdaPort && alreadyUp) {
|
|
702
|
+
wdaPort = appiumWdaPort;
|
|
703
|
+
if (!(yield portAllocator.claim('wda', udid, wdaPort, ttl))) {
|
|
704
|
+
logger_1.default.warn(`[${udid}] WDA port ${wdaPort} is leased to another device; attaching unleased`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
wdaPort = yield portAllocator.acquire('wda', udid, ttl);
|
|
709
|
+
}
|
|
710
|
+
const mjpegPort = yield portAllocator.acquire('mjpeg', udid, ttl);
|
|
609
711
|
if (alreadyUp) {
|
|
610
712
|
logger_1.default.info(`[${udid}] WDA already responding on port ${wdaPort}. Attaching to existing tunnel...`);
|
|
611
713
|
// WDA reachable on 127.0.0.1:wdaPort means the WDA iproxy is live, but the
|
|
@@ -653,7 +755,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
653
755
|
return { wdaPort, mjpegPort };
|
|
654
756
|
}
|
|
655
757
|
// Perform aggressive cleanup of any existing processes for THIS device/ports
|
|
656
|
-
|
|
758
|
+
// (the previous session was already stopped, before the ports were acquired)
|
|
657
759
|
yield this.killStaleProcesses(udid, wdaPort, mjpegPort);
|
|
658
760
|
const session = {
|
|
659
761
|
udid,
|
|
@@ -788,7 +890,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
788
890
|
logger_1.default.warn(`Failed to get screen size for ${udid}: ${e}`);
|
|
789
891
|
}
|
|
790
892
|
// Update device info in store
|
|
791
|
-
const device = yield
|
|
893
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
792
894
|
if (device) {
|
|
793
895
|
const updateData = {
|
|
794
896
|
wdaLocalPort: wdaPort,
|
|
@@ -836,6 +938,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
836
938
|
throw new Error(`WDA failed to start within ${timeout / 1000}s. Check logs.`);
|
|
837
939
|
}
|
|
838
940
|
catch (error) {
|
|
941
|
+
// Not a failed start: the running stream is left exactly as it was.
|
|
942
|
+
if (error instanceof StreamRestartRefused)
|
|
943
|
+
throw error;
|
|
839
944
|
const session = this.sessions.get(udid);
|
|
840
945
|
if (session) {
|
|
841
946
|
session.status = 'error';
|
|
@@ -908,12 +1013,29 @@ let IOSStreamService = class IOSStreamService {
|
|
|
908
1013
|
yield new Promise((resolve) => setTimeout(resolve, 500));
|
|
909
1014
|
});
|
|
910
1015
|
}
|
|
911
|
-
|
|
912
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* Stop this udid's stream: its processes, a live-preview hold, the go-ios
|
|
1018
|
+
* orphan sweep (see cleanupOrphanTunnels) and its port leases.
|
|
1019
|
+
*
|
|
1020
|
+
* `forViewer` means the caller is only done watching (stream/stop,
|
|
1021
|
+
* stream/leave). While an Appium session holds the phone and this stream
|
|
1022
|
+
* launched its own WDA, nothing is stopped: a session allocated while the
|
|
1023
|
+
* stream ran drives that WDA, through this stream's forwarder and go-ios
|
|
1024
|
+
* tunnel. The session's teardown stops it (EventManager
|
|
1025
|
+
* stopIdleStreamForDevice), or the idle watchdog once the session has
|
|
1026
|
+
* ended. A restart never passes it: the WDA already failed its health check.
|
|
1027
|
+
*/
|
|
1028
|
+
stopStream(udid_1) {
|
|
1029
|
+
return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
|
|
913
1030
|
var _a, _b;
|
|
914
1031
|
const session = this.sessions.get(udid);
|
|
915
1032
|
if (!session)
|
|
916
1033
|
return;
|
|
1034
|
+
if (opts.forViewer &&
|
|
1035
|
+
session.wdaProcess &&
|
|
1036
|
+
(yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
917
1039
|
// Kill sidecar processes. The go-ios tunnel is detached (its own process
|
|
918
1040
|
// group), so reap the whole group — a plain p.kill() would leave the
|
|
919
1041
|
// self-forking agent children behind to respawn. See ./tunnelProcess.
|
|
@@ -931,7 +1053,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
931
1053
|
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
932
1054
|
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
933
1055
|
try {
|
|
934
|
-
const device = yield
|
|
1056
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
935
1057
|
if (device && ((_b = device.session_id) === null || _b === void 0 ? void 0 : _b.startsWith('manual_'))) {
|
|
936
1058
|
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
937
1059
|
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
@@ -949,8 +1071,8 @@ let IOSStreamService = class IOSStreamService {
|
|
|
949
1071
|
// devices — they were held for the lifetime of this stream.
|
|
950
1072
|
try {
|
|
951
1073
|
const portAllocator = typedi_2.Container.get(PortAllocator_1.PortAllocator);
|
|
952
|
-
yield portAllocator.release(session.wdaPort);
|
|
953
|
-
yield portAllocator.release(session.mjpegPort);
|
|
1074
|
+
yield portAllocator.release(session.wdaPort, udid);
|
|
1075
|
+
yield portAllocator.release(session.mjpegPort, udid);
|
|
954
1076
|
}
|
|
955
1077
|
catch (e) {
|
|
956
1078
|
logger_1.default.warn(`Failed to release port leases for ${udid}: ${e}`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serverOrigin = serverOrigin;
|
|
4
|
+
exports.parseAdbRemote = parseAdbRemote;
|
|
5
|
+
exports.androidDeviceHost = androidDeviceHost;
|
|
6
|
+
exports.iosRealDeviceHost = iosRealDeviceHost;
|
|
7
|
+
exports.iosSimulatorHost = iosSimulatorHost;
|
|
8
|
+
exports.localDeviceHosts = localDeviceHosts;
|
|
9
|
+
exports.isLocalDeviceHost = isLocalDeviceHost;
|
|
10
|
+
exports.isOwnDevice = isOwnDevice;
|
|
11
|
+
exports.devicesClearedAtBoot = devicesClearedAtBoot;
|
|
12
|
+
/** This server's origin, `http://<bindHostOrIp>:<port>`. */
|
|
13
|
+
function serverOrigin(args, port) {
|
|
14
|
+
return `http://${args.bindHostOrIp}:${port}`;
|
|
15
|
+
}
|
|
16
|
+
/** An `adbRemote` entry, `host[:port]`, with adb's default port 5037. */
|
|
17
|
+
function parseAdbRemote(value) {
|
|
18
|
+
const [adbHost, adbPort] = value.split(':');
|
|
19
|
+
return { adbHost, adbPort: parseInt(adbPort) || 5037 };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An Android phone: a phone on a remote adb server is filed under that
|
|
23
|
+
* server; otherwise under remoteMachineProxyIP with this server's port;
|
|
24
|
+
* otherwise under this server.
|
|
25
|
+
*/
|
|
26
|
+
function androidDeviceHost(args, port, adb) {
|
|
27
|
+
if ((adb === null || adb === void 0 ? void 0 : adb.adbHost) != null)
|
|
28
|
+
return `http://${adb.adbHost}:${adb.adbPort}`;
|
|
29
|
+
if (args.remoteMachineProxyIP !== undefined)
|
|
30
|
+
return `http://${args.remoteMachineProxyIP}:${port}`;
|
|
31
|
+
return serverOrigin(args, port);
|
|
32
|
+
}
|
|
33
|
+
/** A real iOS device: remoteMachineProxyIP as given (no port added), otherwise this server. */
|
|
34
|
+
function iosRealDeviceHost(args, port) {
|
|
35
|
+
return args.remoteMachineProxyIP ? String(args.remoteMachineProxyIP) : serverOrigin(args, port);
|
|
36
|
+
}
|
|
37
|
+
/** An iOS simulator: always this server. */
|
|
38
|
+
function iosSimulatorHost(args, port) {
|
|
39
|
+
return serverOrigin(args, port);
|
|
40
|
+
}
|
|
41
|
+
function localDeviceHosts(args, port) {
|
|
42
|
+
var _a;
|
|
43
|
+
return {
|
|
44
|
+
origin: serverOrigin(args, port),
|
|
45
|
+
hosts: new Set([
|
|
46
|
+
serverOrigin(args, port),
|
|
47
|
+
androidDeviceHost(args, port),
|
|
48
|
+
...((_a = args.adbRemote) !== null && _a !== void 0 ? _a : []).map((value) => androidDeviceHost(args, port, parseAdbRemote(value))),
|
|
49
|
+
iosRealDeviceHost(args, port),
|
|
50
|
+
iosSimulatorHost(args, port),
|
|
51
|
+
]),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Whether a row's host is one this server writes: exact, never by IP or prefix. */
|
|
55
|
+
function isLocalDeviceHost(local, host) {
|
|
56
|
+
return host != null && local.hosts.has(host);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a phone's row is this server's own, for stale cleanup. A row names
|
|
60
|
+
* the server process that found it (`nodeId`, new on every boot; a node's
|
|
61
|
+
* report carries the node's), so that decides: another server's phone is
|
|
62
|
+
* never pruned as this one's, even under a host this server also files
|
|
63
|
+
* phones under (a shared remoteMachineProxyIP or adb server), and is
|
|
64
|
+
* host-checked as the other server's. A row with no nodeId falls back to its
|
|
65
|
+
* host.
|
|
66
|
+
*/
|
|
67
|
+
function isOwnDevice(local, nodeId, device) {
|
|
68
|
+
if (device.nodeId)
|
|
69
|
+
return device.nodeId === nodeId;
|
|
70
|
+
return isLocalDeviceHost(local, device.host);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Which phones a server forgets when it starts: the hosts to clear, or
|
|
74
|
+
* undefined for every phone.
|
|
75
|
+
*
|
|
76
|
+
* A hub keeps its nodes' phones. Their sessions keep running on the nodes
|
|
77
|
+
* while the hub restarts, and the hub finds where to route them from those
|
|
78
|
+
* rows (SessionManager.recoverActiveSessions, gateway/sessionLocator.ts). A
|
|
79
|
+
* node that died meanwhile is pruned by removeStaleDevices, and a live one's
|
|
80
|
+
* next report refreshes its rows. A hub's own phones are cleared as before:
|
|
81
|
+
* discovery adds back the attached ones, and their sessions died with it.
|
|
82
|
+
* A node clears everything, as it always has.
|
|
83
|
+
*/
|
|
84
|
+
function devicesClearedAtBoot(args, local) {
|
|
85
|
+
return args.hub === undefined ? [...local.hosts] : undefined;
|
|
86
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.findOwnDevice = findOwnDevice;
|
|
13
|
+
const typedi_1 = require("typedi");
|
|
14
|
+
const device_store_1 = require("../data-service/device-store");
|
|
15
|
+
const PluginContext_1 = require("../PluginContext");
|
|
16
|
+
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
17
|
+
/**
|
|
18
|
+
* This server's own Device row for a phone it drives: the udid under one of
|
|
19
|
+
* the hosts its discovery files phones under (localDeviceHosts), never
|
|
20
|
+
* another server's row. A hub's table also holds its nodes' rows, and a node
|
|
21
|
+
* on the same machine sees the same phones (an iPhone, emulator-5554), so a
|
|
22
|
+
* udid alone may name the node's row, with the node's holds and sessions.
|
|
23
|
+
*
|
|
24
|
+
* One lookup per own host, usually just one. Null when none of them has the
|
|
25
|
+
* phone.
|
|
26
|
+
*/
|
|
27
|
+
function findOwnDevice(udid) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
const ctx = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
30
|
+
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
31
|
+
for (const host of (0, localDeviceHosts_1.localDeviceHosts)(ctx.pluginArgs, ctx.port).hosts) {
|
|
32
|
+
const device = yield store.findDevice({ udid, host });
|
|
33
|
+
if (device)
|
|
34
|
+
return device;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|