@xenon-device-management/xenon 2.17.0 → 2.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +2 -3
- package/lib/public/assets/{AnnotationOverlay-CjmMCS0S.js → AnnotationOverlay-CB8njQvS.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B4Ob0v3C.js → ApiKeyGate-B-804QBS.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
- package/lib/public/assets/{EmptyState-DcEG-oAi.js → EmptyState-WzLw1cEV.js} +1 -1
- package/lib/public/assets/{FieldGroup-CtnCDshE.js → FieldGroup-CaQNXgcE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BK0dE9cc.js → FilterMenu-C9gAiypL.js} +1 -1
- package/lib/public/assets/{Menu-eKsmLpKS.js → Menu-CfTti3rw.js} +1 -1
- package/lib/public/assets/{Modal-DPwop3Lz.js → Modal-BcZUFhDm.js} +1 -1
- package/lib/public/assets/{RecordingPage-CfgSkZkB.js → RecordingPage-B_pbl3UE.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bzl9pTUY.js → RecordingsPage-BkVhplE3.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B_TZTrPm.js → SegmentedControl-CDLMLNl5.js} +1 -1
- package/lib/public/assets/{SettingCard-DlmvZJPM.js → SettingCard-Lyw5CB1F.js} +1 -1
- package/lib/public/assets/{Table-BigPWKXi.js → Table-CK414bOU.js} +1 -1
- package/lib/public/assets/{activity-CvuZdQYu.js → activity-pEyAdE-M.js} +1 -1
- package/lib/public/assets/{ai-settings-lI2UJ2n7.js → ai-settings-CIhS7MMq.js} +1 -1
- package/lib/public/assets/{api-keys-CPgf9XJM.js → api-keys-D2J44Ebv.js} +1 -1
- package/lib/public/assets/{apps-DIzOvtUp.js → apps-CCV3RZY8.js} +1 -1
- package/lib/public/assets/{arrow-left-DiP5ywBT.js → arrow-left-DaEUmERk.js} +1 -1
- package/lib/public/assets/{arrow-right-DtW_8hm5.js → arrow-right-CnRXHXit.js} +1 -1
- package/lib/public/assets/{arrow-up-right-C89FbNU0.js → arrow-up-right-BGCS7zhi.js} +1 -1
- package/lib/public/assets/{auth-shell-B7sUDb--.js → auth-shell-CpW1zsxD.js} +2 -2
- package/lib/public/assets/{builds-page-BBhvrMfA.js → builds-page-B-xdzjMx.js} +1 -1
- package/lib/public/assets/{button-DugsdUw1.js → button-B5QkOVa4.js} +1 -1
- package/lib/public/assets/{calendar-grKd08ZH.js → calendar-gX_1EKue.js} +1 -1
- package/lib/public/assets/{check-ORjyNfnX.js → check-MIM4THvH.js} +1 -1
- package/lib/public/assets/{chevron-right-Ei2FxUXq.js → chevron-right-BZIp3-9t.js} +1 -1
- package/lib/public/assets/{circle-check-Day0HjW4.js → circle-check-Crt-G_uy.js} +1 -1
- package/lib/public/assets/{circle-x-CHxSRLHp.js → circle-x-C8GvvXVt.js} +1 -1
- package/lib/public/assets/{clock-BDHewWTx.js → clock-MFKV4qGa.js} +1 -1
- package/lib/public/assets/{copy-CTwT4I3D.js → copy-BU-Z7mBK.js} +1 -1
- package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
- package/lib/public/assets/{download-D3bjz_vr.js → download-DIoLCCYv.js} +1 -1
- package/lib/public/assets/{forgot-password-BNBLi023.js → forgot-password-vNYlPnia.js} +1 -1
- package/lib/public/assets/{h264Stream-C51Au2i_.js → h264Stream-BLcSplMU.js} +1 -1
- package/lib/public/assets/{index-Dk5J6HB1.js → index-BpqyclNY.js} +2 -2
- package/lib/public/assets/{index-DncSz5LG.js → index-Hl1OL-k_.js} +1 -1
- package/lib/public/assets/{index-Z7_BY3pJ.js → index-QbukVVrl.js} +1 -1
- package/lib/public/assets/{input-bTXM4l09.js → input-DHH3fHer.js} +1 -1
- package/lib/public/assets/{line-chart-B3YmVYyr.js → line-chart-PdViEphw.js} +1 -1
- package/lib/public/assets/{list-checks-BClKEj1-.js → list-checks-DI_heIg9.js} +1 -1
- package/lib/public/assets/{lock-CZsenX3D.js → lock-DVP_dL6J.js} +1 -1
- package/lib/public/assets/{login-B6A6tOp9.js → login-B8vtTDcr.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CmCJGy7O.js → maintenance-settings-BMa5EfgI.js} +1 -1
- package/lib/public/assets/{monitor-BdTNEglU.js → monitor-CtzX_RkD.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BM8CaLtt.js → mouse-pointer-2-BA09sdS9.js} +1 -1
- package/lib/public/assets/{network-BS6vUc1x.js → network-B_F9Nox-.js} +1 -1
- package/lib/public/assets/{overview-Yfk_Y7g3.js → overview-kWCYMHMV.js} +1 -1
- package/lib/public/assets/{page-header-DhXCwpml.js → page-header-CAbH1O1a.js} +1 -1
- package/lib/public/assets/{play-D08gP1td.js → play-sg95hLqA.js} +1 -1
- package/lib/public/assets/{plus-BKboorfa.js → plus-BT9mk2gj.js} +1 -1
- package/lib/public/assets/{profile-page-Cd9NodrA.js → profile-page-DZOz5Ypc.js} +1 -1
- package/lib/public/assets/{reset-password-acLDyyDW.js → reset-password-CqeImlEO.js} +1 -1
- package/lib/public/assets/{runbook-page-CWLkC1Fd.js → runbook-page-U_8TSysj.js} +1 -1
- package/lib/public/assets/{select-DZoRKNti.js → select-Cx4JDzsR.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-DS4qrt84.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
- package/lib/public/assets/{selector-health-page-TpmN61G3.js → selector-health-page-CKR5URll.js} +1 -1
- package/lib/public/assets/{session-detail-page-C0wYaXqy.js → session-detail-page-DuIVzTW_.js} +1 -1
- package/lib/public/assets/{settings-CxPGEY2j.js → settings-B5fuvgTP.js} +1 -1
- package/lib/public/assets/{stat-tile-DUpyYgl-.js → stat-tile-CtHX1dkh.js} +1 -1
- package/lib/public/assets/{tablet-SA02BJNQ.js → tablet-DCGD0GQt.js} +1 -1
- package/lib/public/assets/{teams-jamD0Kxr.js → teams-MzlELlw9.js} +1 -1
- package/lib/public/assets/{trash-2-DO8fv6w4.js → trash-2-BIRm2KVs.js} +1 -1
- package/lib/public/assets/{upload-CaiGCStb.js → upload-C-lgyswH.js} +1 -1
- package/lib/public/assets/{use-builds-data-jS-fcvVf.js → use-builds-data-IOydXA8d.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BzoenegM.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
- package/lib/public/assets/{users-Bnw7Vs_B.js → users-hwK5XGIy.js} +1 -1
- package/lib/public/assets/{users-DHvDrriG.js → users-ivNCjeus.js} +1 -1
- package/lib/public/assets/{video-off-FEvy0w5U.js → video-off-DvcIEsdZ.js} +1 -1
- package/lib/public/assets/{webhook-settings-CBVB5lxi.js → webhook-settings-DukgCe1u.js} +1 -1
- package/lib/public/assets/{zap-BMrTJO6M.js → zap-BE9gacFj.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/openapi/control.yaml +69 -4
- package/lib/src/app/routers/control.js +24 -5
- package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
- package/lib/src/device-utils.js +50 -22
- package/lib/src/gateway/internalCall.js +33 -5
- package/lib/src/gateway/sessionGateway.js +9 -4
- package/lib/src/helpers/index.js +9 -6
- package/lib/src/interceptors/CommandInterceptor.js +10 -0
- package/lib/src/services/ServerManager.js +25 -4
- package/lib/src/services/UserService.js +8 -3
- package/lib/src/sessions/appiumUmbrella.js +11 -0
- package/lib/src/sessions/sessionMemory.js +11 -2
- package/lib/test/unit/UserService.test.js +8 -0
- package/lib/test/unit/boot-device-sync.spec.js +195 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
- package/lib/test/unit/install-scripts.spec.js +98 -0
- package/lib/test/unit/internal-call.spec.js +99 -0
- package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
- package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
- package/lib/test/unit/is-xenon-running.spec.js +10 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/lib/public/assets/DeviceMosaicView-DTVOB_NO.js +0 -2
- package/lib/public/assets/device-explorer-DlhrKJ-g.js +0 -282
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as P,r as l,X as p,j as e,P as w,A as x,C as V,Q as I}from"./index-
|
|
1
|
+
import{t as P,r as l,X as p,j as e,P as w,A as x,C as V,Q as I}from"./index-BpqyclNY.js";/* empty css */import{F as k}from"./FieldGroup-CaQNXgcE.js";import{S as U}from"./select-Cx4JDzsR.js";import{P as B}from"./page-header-CAbH1O1a.js";import{T as D}from"./trash-2-BIRm2KVs.js";import{P as _}from"./plus-BT9mk2gj.js";import{C as J}from"./circle-x-C8GvvXVt.js";import{C as K}from"./circle-check-Crt-G_uy.js";import{Z as M}from"./zap-BE9gacFj.js";import{A as X}from"./activity-pEyAdE-M.js";import"./utils-CDN07tui.js";const Z=["udid","name","host","platform"],q=["udid","name","host","platform"],G=["sessionId","sessionName","failureReason","udid","deviceName","platform","osVersion","startTime","endTime"],Q=["windowDays","totalHeals","distinctSelectors"],Y={device_offline:Z,device_new:q,session_failed:G,selector_health_digest:Q},ee=Y,se={eventType:"The event: device_offline, session_failed, ...",udid:"The device's id",name:"The device's name",host:"The machine it is plugged into",platform:"android or ios",sessionId:"The session's id",sessionName:"The session's name, if the test gave it one",failureReason:"Why it failed",deviceName:"The device's name",osVersion:"The device's OS version",startTime:"When it started (ISO 8601)",endTime:"When it ended (ISO 8601)",windowDays:"How many days the digest covers",totalHeals:"Heals in that time",distinctSelectors:"Selectors that were healed"};function te(o){const c=["eventType"];for(const u of o)for(const h of ee[u]??[])c.includes(h)||c.push(h);return c}const ae=[{id:"slack",label:"Slack message"},{id:"webhook",label:"JSON (event and payload)"}];function oe(o){return o.payloadTemplate?"CUSTOM":o.type==="slack"?"SLACK":"JSON"}const j=[{id:"device_offline",label:"Device offline",icon:V,tone:"red"},{id:"device_new",label:"New device",icon:_,tone:"green"},{id:"session_failed",label:"Session failed",icon:J,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:I,tone:"green"}],be=()=>{const{toast:o}=P(),[c,u]=l.useState([]),[h,N]=l.useState(!0),[r,y]=l.useState(""),[i,S]=l.useState(["device_offline","session_failed"]),[f,T]=l.useState(!1),[C,A]=l.useState(!1),[d,E]=l.useState(!1),[m,g]=l.useState(""),[b,O]=l.useState("slack");l.useEffect(()=>{v()},[]);const v=async()=>{N(!0);try{const s=await p.getWebhookConfigs();u(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{N(!1)}},W=async()=>{if(!r.trim()){o("Webhook URL is required.","error");return}if(i.length===0){o("Select at least one trigger event.","error");return}T(!0);try{await p.addWebhookConfig(r.trim(),i,b,m||void 0),y(""),g(""),E(!1),O("slack"),S(["device_offline","session_failed"]),await v(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{T(!1)}},$=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await p.deleteWebhookConfig(s),await v(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(!r.trim()||i.length===0)return;A(!0);const s=t=>{var a;return((a=j.find(n=>n.id===t))==null?void 0:a.label)??t};try{for(const t of i)try{await p.testWebhook(r.trim(),b,m||void 0,t)}catch(a){o(`Test failed for ${s(t)}: ${a.message||"check the URL"}`,"error");return}o(i.length===1?`Test message delivered: ${s(i[0])}.`:`Test messages delivered: ${i.map(s).join(", ")}.`,"success")}finally{A(!1)}},R=s=>{S(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},z=s=>{g(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(B,{icon:w,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[h?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(x,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):c.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(w,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:c.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:oe(s)}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>$(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(D,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const n=j.find(H=>H.id===a),F=(n==null?void 0:n.icon)??w;return e.jsxs("span",{className:`event-pill event-pill--${(n==null?void 0:n.tone)||"gray"}`,children:[e.jsx(F,{size:11}),(n==null?void 0:n.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(_,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(k,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:r,onChange:s=>y(s.target.value)})})}),e.jsx(k,{label:"Message format",description:m.trim()?"Your custom payload is sent as written, whatever the format.":"Slack message for a Slack incoming webhook; JSON for anything else.",htmlFor:"webhook-format",children:e.jsx(U,{id:"webhook-format",value:b,onChange:s=>O(s.target.value),children:ae.map(s=>e.jsx("option",{value:s.id,children:s.label},s.id))})}),e.jsx(k,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:j.map(s=>{const t=i.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>R(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(K,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>E(!d),"aria-expanded":d,children:[e.jsx(M,{size:14,className:d?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:d?"−":"+"})]}),d&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a name below to insert it; the names are the ones your selected events carry."}),e.jsx("div",{className:"template-editor__chips",children:te(i).map(s=>e.jsx("button",{type:"button",className:"template-editor__chip","aria-label":`{{${s}}}`,title:se[s],onClick:()=>z(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:m,onChange:s=>g(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!r.trim()||i.length===0||C,"aria-label":"Send test",title:i.length===0?"Select a trigger event to send a sample of":"Sends a sample message for each selected event",children:[C?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(X,{size:14}),e.jsx("span",{children:"Send test"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:W,disabled:f||!r.trim(),children:[f?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(_,{size:14}),e.jsx("span",{children:f?"Saving…":"Save webhook"})]})]})]})]})]})};export{be as WebhookSettings};
|
package/lib/public/index.html
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
37
37
|
-->
|
|
38
38
|
<title>Xenon</title>
|
|
39
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-BpqyclNY.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
@@ -1941,8 +1941,12 @@ paths:
|
|
|
1941
1941
|
summary: Start the live preview and hold the device
|
|
1942
1942
|
description: >-
|
|
1943
1943
|
Starts the device's live preview and holds the device for you (`session_id` becomes
|
|
1944
|
-
`manual_<yourUserId>_<udid>`), so automation is not given it while you watch.
|
|
1945
|
-
WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+).
|
|
1944
|
+
`manual_<yourUserId>_<udid>`), so automation is not given it while you watch. An iPhone
|
|
1945
|
+
starts WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+). An iOS simulator
|
|
1946
|
+
starts nothing and is not held (its test holds it): it shows the picture of the test
|
|
1947
|
+
running on it, from the WebDriverAgent the XCUITest driver runs for that test, and with no
|
|
1948
|
+
test running it answers `409` with `error: simulator_needs_test` and a `message` saying
|
|
1949
|
+
so. Android starts an MJPEG
|
|
1946
1950
|
screen capture, or, when the server's `streaming.androidH264` setting is on and the device
|
|
1947
1951
|
is not being recorded, an H.264 stream for the WebSocket in `h264Path`. Missing screen
|
|
1948
1952
|
dimensions are filled in and saved.
|
|
@@ -2035,7 +2039,34 @@ paths:
|
|
|
2035
2039
|
'404':
|
|
2036
2040
|
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
2037
2041
|
'409':
|
|
2038
|
-
|
|
2042
|
+
description: >-
|
|
2043
|
+
Another user holds the device, or it runs another user's Appium session (admins are
|
|
2044
|
+
never refused), as for every device action. Or the device is an iOS simulator no test
|
|
2045
|
+
runs on (`simulator_needs_test`): its screen shows only while a test runs on it.
|
|
2046
|
+
content:
|
|
2047
|
+
application/json:
|
|
2048
|
+
schema:
|
|
2049
|
+
oneOf:
|
|
2050
|
+
- $ref: '#/components/schemas/ControlDenied'
|
|
2051
|
+
- $ref: '#/components/schemas/ControlSimulatorNeedsTest'
|
|
2052
|
+
examples:
|
|
2053
|
+
held:
|
|
2054
|
+
summary: Another user's preview or recording
|
|
2055
|
+
value:
|
|
2056
|
+
success: false
|
|
2057
|
+
error: device_held_by_another_user
|
|
2058
|
+
message: >-
|
|
2059
|
+
Device is being controlled by Priya Raman. Ask them to release it, or use an
|
|
2060
|
+
admin key to force-release.
|
|
2061
|
+
holder:
|
|
2062
|
+
userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
|
|
2063
|
+
name: Priya Raman
|
|
2064
|
+
simulatorNeedsTest:
|
|
2065
|
+
summary: An iOS simulator no test runs on
|
|
2066
|
+
value:
|
|
2067
|
+
success: false
|
|
2068
|
+
error: simulator_needs_test
|
|
2069
|
+
message: A simulator's screen shows here only while a test runs on it.
|
|
2039
2070
|
'429':
|
|
2040
2071
|
$ref: '#/components/responses/RateLimited'
|
|
2041
2072
|
'500':
|
|
@@ -2286,7 +2317,9 @@ paths:
|
|
|
2286
2317
|
Says whether the device's preview is running and which transport a player should use
|
|
2287
2318
|
(`type`: `h264` on Android when the server's `streaming.androidH264` setting is on and the
|
|
2288
2319
|
device isn't being recorded, else `mjpeg`). A device with no preview answers `200` with
|
|
2289
|
-
`status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error.
|
|
2320
|
+
`status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error. A
|
|
2321
|
+
simulator no test runs on answers `status: stopped` with `lastError` saying why it shows
|
|
2322
|
+
nothing, and `reason: simulator_needs_test`.
|
|
2290
2323
|
|
|
2291
2324
|
|
|
2292
2325
|
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
@@ -2326,6 +2359,15 @@ paths:
|
|
|
2326
2359
|
udid: R5CT32ABCDE
|
|
2327
2360
|
status: stopped
|
|
2328
2361
|
type: mjpeg
|
|
2362
|
+
simulatorWithoutTest:
|
|
2363
|
+
summary: An iOS simulator no test runs on
|
|
2364
|
+
value:
|
|
2365
|
+
udid: 35429994-B029-4AF4-9634-880B89DAC782
|
|
2366
|
+
status: stopped
|
|
2367
|
+
type: mjpeg
|
|
2368
|
+
mjpegPort: 9101
|
|
2369
|
+
lastError: A simulator's screen shows here only while a test runs on it.
|
|
2370
|
+
reason: simulator_needs_test
|
|
2329
2371
|
'400':
|
|
2330
2372
|
description: The udid is malformed.
|
|
2331
2373
|
content:
|
|
@@ -3147,6 +3189,13 @@ components:
|
|
|
3147
3189
|
lastError:
|
|
3148
3190
|
type: string
|
|
3149
3191
|
description: iOS only.
|
|
3192
|
+
reason:
|
|
3193
|
+
type: string
|
|
3194
|
+
enum:
|
|
3195
|
+
- simulator_needs_test
|
|
3196
|
+
description: >-
|
|
3197
|
+
Set when the preview is refused rather than failed: `simulator_needs_test`, an iOS
|
|
3198
|
+
simulator no test runs on.
|
|
3150
3199
|
ControlStreamLeaveResult:
|
|
3151
3200
|
type: object
|
|
3152
3201
|
properties:
|
|
@@ -3156,6 +3205,22 @@ components:
|
|
|
3156
3205
|
pending:
|
|
3157
3206
|
type: boolean
|
|
3158
3207
|
example: true
|
|
3208
|
+
ControlSimulatorNeedsTest:
|
|
3209
|
+
type: object
|
|
3210
|
+
required:
|
|
3211
|
+
- error
|
|
3212
|
+
- message
|
|
3213
|
+
properties:
|
|
3214
|
+
success:
|
|
3215
|
+
type: boolean
|
|
3216
|
+
example: false
|
|
3217
|
+
error:
|
|
3218
|
+
type: string
|
|
3219
|
+
enum:
|
|
3220
|
+
- simulator_needs_test
|
|
3221
|
+
message:
|
|
3222
|
+
type: string
|
|
3223
|
+
description: Why the simulator shows nothing, for testers.
|
|
3159
3224
|
ControlDenied:
|
|
3160
3225
|
type: object
|
|
3161
3226
|
required:
|
|
@@ -58,7 +58,7 @@ const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
|
58
58
|
const device_service_1 = require("../../data-service/device-service");
|
|
59
59
|
const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
|
|
60
60
|
const DisplayStateService_1 = require("../../services/DisplayStateService");
|
|
61
|
-
const IOSStreamService_1 =
|
|
61
|
+
const IOSStreamService_1 = __importStar(require("../../device-managers/ios/IOSStreamService"));
|
|
62
62
|
const AndroidStreamService_1 = __importDefault(require("../../device-managers/android/AndroidStreamService"));
|
|
63
63
|
const AndroidH264StreamService_1 = __importDefault(require("../../device-managers/android/AndroidH264StreamService"));
|
|
64
64
|
const path_1 = __importDefault(require("path"));
|
|
@@ -748,9 +748,14 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
748
748
|
// Mark device as "Busy" so automation sessions don't pick it up.
|
|
749
749
|
// Lock is keyed on the user, not the credential — see
|
|
750
750
|
// src/services/device-access/deviceAccessPolicy.ts.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
751
|
+
// A simulator is previewed only while a test holds it, so a preview hold
|
|
752
|
+
// adds nothing, and written after the test ended it would hold a free
|
|
753
|
+
// simulator for nobody's test.
|
|
754
|
+
if (!(0, IOSStreamService_1.isIosSimulator)(device)) {
|
|
755
|
+
const manualSid = (0, manualLock_1.formatManualLock)(actorUserId, udid);
|
|
756
|
+
yield (0, device_service_1.blockDevice)(udid, device.host, manualSid);
|
|
757
|
+
logger_1.default.info(`Manual Control: Device ${udid} locked for active UI session (${manualSid}).`);
|
|
758
|
+
}
|
|
754
759
|
if (streamType === 'h264') {
|
|
755
760
|
logger_1.default.info(`H.264 stream started for ${udid}`);
|
|
756
761
|
return res.status(200).send({
|
|
@@ -774,6 +779,13 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
774
779
|
});
|
|
775
780
|
}
|
|
776
781
|
catch (err) {
|
|
782
|
+
// Not a failure: a simulator shows its running test's picture, and none
|
|
783
|
+
// runs. Said in plain words; the dashboard toasts it.
|
|
784
|
+
if (err instanceof IOSStreamService_1.SimulatorPreviewNeedsTest) {
|
|
785
|
+
return res
|
|
786
|
+
.status(409)
|
|
787
|
+
.send({ success: false, error: 'simulator_needs_test', message: err.message });
|
|
788
|
+
}
|
|
777
789
|
logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
|
|
778
790
|
return res.status(500).send({
|
|
779
791
|
success: false,
|
|
@@ -1037,6 +1049,9 @@ router.get('/:udid/stream/status', (req, res) => __awaiter(void 0, void 0, void
|
|
|
1037
1049
|
type,
|
|
1038
1050
|
h264Path,
|
|
1039
1051
|
mjpegPort: device.mjpegServerPort,
|
|
1052
|
+
// Why a simulator shows nothing: a Live devices tile says it once it stops retrying.
|
|
1053
|
+
lastError: (0, IOSStreamService_1.simulatorPreviewRefusal)(device),
|
|
1054
|
+
reason: (0, IOSStreamService_1.simulatorPreviewRefusal)(device) ? 'simulator_needs_test' : undefined,
|
|
1040
1055
|
});
|
|
1041
1056
|
}));
|
|
1042
1057
|
/**
|
|
@@ -1083,7 +1098,11 @@ router.get('/:udid/stream', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
1083
1098
|
});
|
|
1084
1099
|
}
|
|
1085
1100
|
catch (err) {
|
|
1086
|
-
|
|
1101
|
+
// A simulator no test runs on: expected, and asked again by each retry.
|
|
1102
|
+
if (err instanceof IOSStreamService_1.SimulatorPreviewNeedsTest)
|
|
1103
|
+
logger_1.default.debug(`${udid}: ${err.message}`);
|
|
1104
|
+
else
|
|
1105
|
+
logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
|
|
1087
1106
|
return res.status(503).send({
|
|
1088
1107
|
error: 'Stream not available',
|
|
1089
1108
|
message: err.message,
|
|
@@ -60,9 +60,11 @@ 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.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
|
|
63
|
+
exports.SimulatorPreviewNeedsTest = exports.SIMULATOR_PREVIEW_NEEDS_TEST = exports.StreamRestartRefused = exports.IOS_IDLE_STOP_MS = void 0;
|
|
64
64
|
exports.shouldStopIdleIosStream = shouldStopIdleIosStream;
|
|
65
65
|
exports.heldByAppiumSession = heldByAppiumSession;
|
|
66
|
+
exports.isIosSimulator = isIosSimulator;
|
|
67
|
+
exports.simulatorPreviewRefusal = simulatorPreviewRefusal;
|
|
66
68
|
const typedi_1 = require("typedi");
|
|
67
69
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
68
70
|
const child_process_1 = require("child_process");
|
|
@@ -82,6 +84,7 @@ const iosStreamDiagnostics_1 = require("./iosStreamDiagnostics");
|
|
|
82
84
|
const tunnelProcess_1 = require("./tunnelProcess");
|
|
83
85
|
const IOSTunnels_1 = require("./IOSTunnels");
|
|
84
86
|
const goIosBinary_1 = require("./goIosBinary");
|
|
87
|
+
const appiumUmbrella_1 = require("../../sessions/appiumUmbrella");
|
|
85
88
|
const device_service_1 = require("../../data-service/device-service");
|
|
86
89
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
87
90
|
const recording_store_1 = require("../../services/recording/recording-store");
|
|
@@ -113,9 +116,39 @@ function heldByAppiumSession(device) {
|
|
|
113
116
|
class StreamRestartRefused extends Error {
|
|
114
117
|
}
|
|
115
118
|
exports.StreamRestartRefused = StreamRestartRefused;
|
|
119
|
+
/** Why a simulator shows no live preview: for testers, on the device page and its tile. */
|
|
120
|
+
exports.SIMULATOR_PREVIEW_NEEDS_TEST = "A simulator's screen shows here only while a test runs on it.";
|
|
121
|
+
/** A simulator's preview was asked for while no test runs on it. */
|
|
122
|
+
class SimulatorPreviewNeedsTest extends Error {
|
|
123
|
+
constructor() {
|
|
124
|
+
super(exports.SIMULATOR_PREVIEW_NEEDS_TEST);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.SimulatorPreviewNeedsTest = SimulatorPreviewNeedsTest;
|
|
128
|
+
/**
|
|
129
|
+
* An iOS or tvOS simulator's row. `realDevice` is false only there among
|
|
130
|
+
* Apple rows (discovery writes it, the store reads a missing one as true); an
|
|
131
|
+
* Android emulator's is false too.
|
|
132
|
+
*/
|
|
133
|
+
function isIosSimulator(device) {
|
|
134
|
+
return (device === null || device === void 0 ? void 0 : device.realDevice) === false && (device.platform === 'ios' || device.platform === 'tvos');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Why this phone can't be previewed now, or undefined when it can: a
|
|
138
|
+
* simulator no Appium session holds.
|
|
139
|
+
*/
|
|
140
|
+
function simulatorPreviewRefusal(device) {
|
|
141
|
+
if (!isIosSimulator(device))
|
|
142
|
+
return undefined;
|
|
143
|
+
return heldByAppiumSession(device) ? undefined : exports.SIMULATOR_PREVIEW_NEEDS_TEST;
|
|
144
|
+
}
|
|
145
|
+
/** How many ended sessions a simulator preview remembers, to refuse a start mid-teardown. */
|
|
146
|
+
const ENDED_SESSIONS_KEPT = 200;
|
|
116
147
|
let IOSStreamService = class IOSStreamService {
|
|
117
148
|
constructor() {
|
|
118
149
|
this.sessions = new Map();
|
|
150
|
+
/** Sessions whose simulator previews have ended (endSimulatorPreviews), newest last. */
|
|
151
|
+
this.endedSimulatorSessions = new Set();
|
|
119
152
|
this.startFlight = new singleFlight_1.SingleFlight();
|
|
120
153
|
this.recoveryCooldowns = new Map(); // Track last recovery attempt time
|
|
121
154
|
this.RECOVERY_COOLDOWN_MS = 30000; // 30s cooldown between recovery attempts
|
|
@@ -411,6 +444,10 @@ let IOSStreamService = class IOSStreamService {
|
|
|
411
444
|
const session = this.sessions.get(udid);
|
|
412
445
|
if (!session || session.status !== 'running')
|
|
413
446
|
return false;
|
|
447
|
+
// A simulator's preview has no process of its own to check or heal: the
|
|
448
|
+
// picture is its session's, and the preview ends with it.
|
|
449
|
+
if (session.simulatorSession)
|
|
450
|
+
return true;
|
|
414
451
|
// 1. Process Check: verify child processes haven't exited
|
|
415
452
|
const isWdaAlive = session.wdaProcess && session.wdaProcess.exitCode === null;
|
|
416
453
|
const isWdaIproxyAlive = session.forwardWDAProcess && session.forwardWDAProcess.exitCode === null;
|
|
@@ -453,6 +490,9 @@ let IOSStreamService = class IOSStreamService {
|
|
|
453
490
|
}
|
|
454
491
|
startStream(udid) {
|
|
455
492
|
return __awaiter(this, void 0, void 0, function* () {
|
|
493
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
494
|
+
if (isIosSimulator(device))
|
|
495
|
+
return this.startSimulatorStream(udid, device);
|
|
456
496
|
// Check if stream is already running - avoid unnecessary restarts
|
|
457
497
|
const existingSession = this.sessions.get(udid);
|
|
458
498
|
if (existingSession && existingSession.status === 'running') {
|
|
@@ -811,6 +851,82 @@ let IOSStreamService = class IOSStreamService {
|
|
|
811
851
|
return this.startFlight.run(udid, performStartup);
|
|
812
852
|
});
|
|
813
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* A simulator's preview is its running test's picture. The XCUITest
|
|
856
|
+
* driver's WDA serves it on this Mac at the session's mjpegServerPort, so
|
|
857
|
+
* this starts nothing (go-ios, iproxy and runwda reach only an iPhone) and
|
|
858
|
+
* leases no port (the session's are its own). With no test running there
|
|
859
|
+
* is nothing to show: refused in plain words, with no cool-down, since a
|
|
860
|
+
* test may start any moment.
|
|
861
|
+
*
|
|
862
|
+
* Through 2.15 a simulator went through the iPhone's start. With no test it
|
|
863
|
+
* failed, and left its iproxy forwards on the ports it had leased to the
|
|
864
|
+
* simulator: a test started meanwhile was given those ports and its WDA
|
|
865
|
+
* couldn't open them. With a test it read the picture through a forward
|
|
866
|
+
* that carries nothing, so a Live devices tile stayed empty.
|
|
867
|
+
*/
|
|
868
|
+
startSimulatorStream(udid, device) {
|
|
869
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
870
|
+
var _a;
|
|
871
|
+
let sessionId = simulatorPreviewRefusal(device) ? undefined : ((_a = device.session_id) !== null && _a !== void 0 ? _a : undefined);
|
|
872
|
+
// An ended test's row can still name it while its phone is being released.
|
|
873
|
+
if (sessionId && this.endedSimulatorSessions.has(sessionId))
|
|
874
|
+
sessionId = undefined;
|
|
875
|
+
// Only the ports of a session Appium has now: the row's would outlive it.
|
|
876
|
+
const driver = sessionId ? typedi_2.Container.get(appiumUmbrella_1.AppiumUmbrella).driverOptions(sessionId) : undefined;
|
|
877
|
+
const wdaPort = Number(driver === null || driver === void 0 ? void 0 : driver.wdaLocalPort);
|
|
878
|
+
const mjpegPort = Number(driver === null || driver === void 0 ? void 0 : driver.mjpegServerPort);
|
|
879
|
+
const existing = this.sessions.get(udid);
|
|
880
|
+
if (!sessionId || !wdaPort || !mjpegPort) {
|
|
881
|
+
if (existing)
|
|
882
|
+
yield this.stopStream(udid);
|
|
883
|
+
throw new SimulatorPreviewNeedsTest();
|
|
884
|
+
}
|
|
885
|
+
if ((existing === null || existing === void 0 ? void 0 : existing.simulatorSession) === sessionId && existing.status === 'running') {
|
|
886
|
+
return { wdaPort: existing.wdaPort, mjpegPort: existing.mjpegPort };
|
|
887
|
+
}
|
|
888
|
+
// An earlier test's preview, or anything else held for this simulator.
|
|
889
|
+
if (existing)
|
|
890
|
+
yield this.stopStream(udid);
|
|
891
|
+
this.sessions.set(udid, {
|
|
892
|
+
udid,
|
|
893
|
+
wdaProcess: null,
|
|
894
|
+
forwardWDAProcess: null,
|
|
895
|
+
forwardMJPEGProcess: null,
|
|
896
|
+
tunnelPort: null,
|
|
897
|
+
wdaPort,
|
|
898
|
+
mjpegPort,
|
|
899
|
+
status: 'running',
|
|
900
|
+
startedAt: new Date(),
|
|
901
|
+
lastViewerAt: Date.now(),
|
|
902
|
+
viewerCount: 0,
|
|
903
|
+
simulatorSession: sessionId,
|
|
904
|
+
});
|
|
905
|
+
logger_1.default.info(`[${udid}] Simulator preview: the picture of session ${sessionId} (port ${mjpegPort})`);
|
|
906
|
+
return { wdaPort, mjpegPort };
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* End the simulator previews that show this session's picture. Called
|
|
911
|
+
* however the session ends (forgetSessionMemory): the driver's WDA may
|
|
912
|
+
* outlive it, but no test runs any more.
|
|
913
|
+
*/
|
|
914
|
+
endSimulatorPreviews(sessionId) {
|
|
915
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
916
|
+
this.endedSimulatorSessions.add(sessionId);
|
|
917
|
+
if (this.endedSimulatorSessions.size > ENDED_SESSIONS_KEPT) {
|
|
918
|
+
const oldest = this.endedSimulatorSessions.values().next().value;
|
|
919
|
+
if (oldest !== undefined)
|
|
920
|
+
this.endedSimulatorSessions.delete(oldest);
|
|
921
|
+
}
|
|
922
|
+
for (const [udid, session] of this.sessions.entries()) {
|
|
923
|
+
if (session.simulatorSession !== sessionId)
|
|
924
|
+
continue;
|
|
925
|
+
logger_1.default.info(`[${udid}] Simulator preview ends with session ${sessionId}`);
|
|
926
|
+
yield this.stopStream(udid);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
814
930
|
killStaleProcesses(udid, wdaPort, mjpegPort) {
|
|
815
931
|
return __awaiter(this, void 0, void 0, function* () {
|
|
816
932
|
logger_1.default.info(`Cleaning up stale processes for ${udid} (Ports: ${wdaPort}, ${mjpegPort})`);
|
|
@@ -879,10 +995,16 @@ let IOSStreamService = class IOSStreamService {
|
|
|
879
995
|
*/
|
|
880
996
|
stopStream(udid_1) {
|
|
881
997
|
return __awaiter(this, arguments, void 0, function* (udid, opts = {}) {
|
|
882
|
-
var _a;
|
|
883
998
|
const session = this.sessions.get(udid);
|
|
884
999
|
if (!session)
|
|
885
1000
|
return;
|
|
1001
|
+
// A simulator's preview started nothing and leased no port: the ports and
|
|
1002
|
+
// WDA are its session's. Only the entry goes (and a manual hold, below).
|
|
1003
|
+
if (session.simulatorSession) {
|
|
1004
|
+
this.sessions.delete(udid);
|
|
1005
|
+
yield this.releaseManualHold(udid);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
886
1008
|
if (opts.forViewer &&
|
|
887
1009
|
session.wdaProcess &&
|
|
888
1010
|
(yield this.appiumSessionMayUse(udid, "the stream's WDA and go-ios tunnel"))) {
|
|
@@ -902,22 +1024,7 @@ let IOSStreamService = class IOSStreamService {
|
|
|
902
1024
|
// A stream attached to an Appium session's WDA started none.
|
|
903
1025
|
if (session.tunnelPort != null)
|
|
904
1026
|
yield this.tunnels().stop(udid);
|
|
905
|
-
|
|
906
|
-
// The lock could belong to an Appium automation session (session_id is a real UUID).
|
|
907
|
-
// We should only unblock if it's a manual control lock (session_id starts with 'manual_').
|
|
908
|
-
try {
|
|
909
|
-
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
910
|
-
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
911
|
-
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
912
|
-
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
913
|
-
}
|
|
914
|
-
else if (device && device.busy) {
|
|
915
|
-
logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
catch (e) {
|
|
919
|
-
logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
|
|
920
|
-
}
|
|
1027
|
+
yield this.releaseManualHold(udid);
|
|
921
1028
|
// Also clean up any orphan processes (belt and suspenders)
|
|
922
1029
|
yield this.cleanupOrphanTunnels(udid);
|
|
923
1030
|
// Release the port leases so the allocator can reuse these ports for other
|
|
@@ -933,6 +1040,28 @@ let IOSStreamService = class IOSStreamService {
|
|
|
933
1040
|
this.sessions.delete(udid);
|
|
934
1041
|
});
|
|
935
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Release the phone's manual hold (a preview's, a recording's), never an
|
|
1045
|
+
* Appium session's claim: that session_id is a real id, not `manual_...`.
|
|
1046
|
+
*/
|
|
1047
|
+
releaseManualHold(udid) {
|
|
1048
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1049
|
+
var _a;
|
|
1050
|
+
try {
|
|
1051
|
+
const device = yield (0, ownDeviceRow_1.findOwnDevice)(udid);
|
|
1052
|
+
if (device && ((_a = device.session_id) === null || _a === void 0 ? void 0 : _a.startsWith('manual_'))) {
|
|
1053
|
+
logger_1.default.info(`Stream Stop: Releasing manual control lock for ${udid}`);
|
|
1054
|
+
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
1055
|
+
}
|
|
1056
|
+
else if (device && device.busy) {
|
|
1057
|
+
logger_1.default.info(`Stream Stop: Device ${udid} is busy with session ${device.session_id}. NOT releasing lock.`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch (e) {
|
|
1061
|
+
logger_1.default.error(`Failed to check/release lock during stream stop for ${udid}: ${e}`);
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
936
1065
|
updateWDASettings(wdaPort) {
|
|
937
1066
|
return __awaiter(this, void 0, void 0, function* () {
|
|
938
1067
|
const urls = [`http://127.0.0.1:${wdaPort}/appium/settings`];
|
package/lib/src/device-utils.js
CHANGED
|
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.getDeviceTypeFromApp = void 0;
|
|
48
|
+
exports.BOOT_PROBE = exports.getDeviceTypeFromApp = void 0;
|
|
49
49
|
exports.isAndroid = isAndroid;
|
|
50
50
|
exports.deviceType = deviceType;
|
|
51
51
|
exports.isIOS = isIOS;
|
|
@@ -59,6 +59,8 @@ exports.setUtilizationTime = setUtilizationTime;
|
|
|
59
59
|
exports.getDeviceFiltersFromCapability = getDeviceFiltersFromCapability;
|
|
60
60
|
exports.getRunningSessionsCount = getRunningSessionsCount;
|
|
61
61
|
exports.updateDeviceList = updateDeviceList;
|
|
62
|
+
exports.syncLocalDevices = syncLocalDevices;
|
|
63
|
+
exports.reportDevicesToHub = reportDevicesToHub;
|
|
62
64
|
exports.refreshSimulatorState = refreshSimulatorState;
|
|
63
65
|
exports.setupCronCheckStaleDevices = setupCronCheckStaleDevices;
|
|
64
66
|
exports.removeStaleDevices = removeStaleDevices;
|
|
@@ -530,6 +532,19 @@ function getRunningSessionsCount() {
|
|
|
530
532
|
});
|
|
531
533
|
}
|
|
532
534
|
function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
535
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
536
|
+
const { devices, gone } = yield syncLocalDevices(local);
|
|
537
|
+
if (hubArgument) {
|
|
538
|
+
yield reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized);
|
|
539
|
+
}
|
|
540
|
+
return devices;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* This server's own phones: what discovery reports now is written to the
|
|
545
|
+
* table, and its own rows discovery no longer reports are removed (`gone`).
|
|
546
|
+
*/
|
|
547
|
+
function syncLocalDevices(local) {
|
|
533
548
|
return __awaiter(this, void 0, void 0, function* () {
|
|
534
549
|
const allExistingDevices = yield (0, device_service_1.getAllDevices)();
|
|
535
550
|
const devices = yield getDeviceManager().getDevices(allExistingDevices);
|
|
@@ -547,28 +562,31 @@ function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
|
|
|
547
562
|
logger_1.default.info(`Removing ${staleLocalDevices.length} stale devices/simulators no longer discovered on this host.`);
|
|
548
563
|
yield (0, device_service_1.removeDevice)(staleLocalDevices.map((d) => ({ udid: d.udid, host: d.host })));
|
|
549
564
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
+
return { devices, gone: staleLocalDevices };
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/** A node tells its hub the phones it has, and the ones it no longer has. */
|
|
569
|
+
function reportDevicesToHub(hubArgument, devices, gone, tlsRejectUnauthorized) {
|
|
570
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
571
|
+
if (yield (0, helpers_1.isXenonRunning)(hubArgument, tlsRejectUnauthorized)) {
|
|
572
|
+
const nodeDevices = new NodeDevices_1.default(hubArgument, {
|
|
573
|
+
tlsRejectUnauthorized,
|
|
574
|
+
hubAccessKey: config_1.config.hubAccessKey,
|
|
575
|
+
hubToken: config_1.config.hubToken,
|
|
576
|
+
});
|
|
577
|
+
try {
|
|
578
|
+
yield nodeDevices.postDevicesToHub(devices, 'add');
|
|
579
|
+
if (gone.length > 0) {
|
|
580
|
+
yield nodeDevices.postDevicesToHub(gone, 'remove');
|
|
565
581
|
}
|
|
566
582
|
}
|
|
567
|
-
|
|
568
|
-
logger_1.default.
|
|
583
|
+
catch (error) {
|
|
584
|
+
logger_1.default.error(`Cannot send device list update. Reason: ${error}`);
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
|
-
|
|
587
|
+
else {
|
|
588
|
+
logger_1.default.warn(`Not sending device update since hub ${hubArgument} is not running`);
|
|
589
|
+
}
|
|
572
590
|
});
|
|
573
591
|
}
|
|
574
592
|
function refreshSimulatorState(pluginArgs, hostPort) {
|
|
@@ -590,11 +608,21 @@ function setupCronCheckStaleDevices(intervalMs, local, tlsRejectUnauthorized) {
|
|
|
590
608
|
}), intervalMs);
|
|
591
609
|
});
|
|
592
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* How a start asks another server whether it is alive: once, for at most 5 s.
|
|
613
|
+
* Appium opens its port only once every plugin's updateServer has finished,
|
|
614
|
+
* and the client's own way (four 30 s tries) held it closed for two minutes
|
|
615
|
+
* when a row named a host that drops packets. A live server answers in
|
|
616
|
+
* milliseconds; one that answers slower has its phones dropped and reports
|
|
617
|
+
* them again at its next report.
|
|
618
|
+
*/
|
|
619
|
+
exports.BOOT_PROBE = { timeoutMs: 5000, retry: false };
|
|
593
620
|
/**
|
|
594
621
|
* Remove devices where the host is not alive nor defined.
|
|
595
622
|
* @param local the hosts this server files its own phones under
|
|
623
|
+
* @param probe how each host is asked (the client's own way when not given)
|
|
596
624
|
*/
|
|
597
|
-
function removeStaleDevices(local, tlsRejectUnauthorized) {
|
|
625
|
+
function removeStaleDevices(local, tlsRejectUnauthorized, probe) {
|
|
598
626
|
return __awaiter(this, void 0, void 0, function* () {
|
|
599
627
|
const allDevices = yield (0, device_service_1.getAllDevices)();
|
|
600
628
|
const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
|
|
@@ -611,13 +639,13 @@ function removeStaleDevices(local, tlsRejectUnauthorized) {
|
|
|
611
639
|
const aliveHosts = (yield Promise.allSettled(nodeHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
|
|
612
640
|
return {
|
|
613
641
|
host: host,
|
|
614
|
-
alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized),
|
|
642
|
+
alive: yield (0, helpers_1.isXenonRunning)(host, tlsRejectUnauthorized, probe),
|
|
615
643
|
};
|
|
616
644
|
}))));
|
|
617
645
|
const aliveCloudHosts = (yield Promise.allSettled(cloudHosts.map((host) => __awaiter(this, void 0, void 0, function* () {
|
|
618
646
|
return {
|
|
619
647
|
host: host,
|
|
620
|
-
alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized),
|
|
648
|
+
alive: yield (0, helpers_1.isAppiumRunningAt)(host, tlsRejectUnauthorized, probe),
|
|
621
649
|
};
|
|
622
650
|
}))));
|
|
623
651
|
// summarize alive hosts
|