@xenon-device-management/xenon 2.16.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/README.md +2 -2
- package/lib/package.json +2 -3
- package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CB8njQvS.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B-804QBS.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
- package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-WzLw1cEV.js} +1 -1
- package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CaQNXgcE.js} +1 -1
- package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-C9gAiypL.js} +1 -1
- package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-CfTti3rw.js} +1 -1
- package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-BcZUFhDm.js} +1 -1
- package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-B_pbl3UE.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-BkVhplE3.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-CDLMLNl5.js} +1 -1
- package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-Lyw5CB1F.js} +1 -1
- package/lib/public/assets/{Table-DwIgERRt.js → Table-CK414bOU.js} +1 -1
- package/lib/public/assets/{activity-DrLbHifn.js → activity-pEyAdE-M.js} +1 -1
- package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-CIhS7MMq.js} +1 -1
- package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-D2J44Ebv.js} +1 -1
- package/lib/public/assets/{apps-BqNVdln7.js → apps-CCV3RZY8.js} +1 -1
- package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DaEUmERk.js} +1 -1
- package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-CnRXHXit.js} +1 -1
- package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-BGCS7zhi.js} +1 -1
- package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-CpW1zsxD.js} +2 -2
- package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-B-xdzjMx.js} +1 -1
- package/lib/public/assets/{button-DDau4Axv.js → button-B5QkOVa4.js} +1 -1
- package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-gX_1EKue.js} +1 -1
- package/lib/public/assets/{check-K1ovTQam.js → check-MIM4THvH.js} +1 -1
- package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-BZIp3-9t.js} +1 -1
- package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Crt-G_uy.js} +1 -1
- package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-C8GvvXVt.js} +1 -1
- package/lib/public/assets/{clock-BPrld-3h.js → clock-MFKV4qGa.js} +1 -1
- package/lib/public/assets/{copy-t0QB14JI.js → copy-BU-Z7mBK.js} +1 -1
- package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
- package/lib/public/assets/{download-BroHxNOh.js → download-DIoLCCYv.js} +1 -1
- package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-vNYlPnia.js} +1 -1
- package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-BLcSplMU.js} +1 -1
- package/lib/public/assets/{index-B0gQkEg0.js → index-BpqyclNY.js} +2 -2
- package/lib/public/assets/{index-B0JeuZbg.js → index-Hl1OL-k_.js} +1 -1
- package/lib/public/assets/{index-72oF89LJ.js → index-QbukVVrl.js} +1 -1
- package/lib/public/assets/{input-DIc4MSC7.js → input-DHH3fHer.js} +1 -1
- package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-PdViEphw.js} +1 -1
- package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-DI_heIg9.js} +1 -1
- package/lib/public/assets/{lock-pVLvnN30.js → lock-DVP_dL6J.js} +1 -1
- package/lib/public/assets/{login-CN77vy--.js → login-B8vtTDcr.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-BMa5EfgI.js} +1 -1
- package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-CtzX_RkD.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BA09sdS9.js} +1 -1
- package/lib/public/assets/{network-BSfA8ZxG.js → network-B_F9Nox-.js} +1 -1
- package/lib/public/assets/{overview-BfTebCK5.js → overview-kWCYMHMV.js} +1 -1
- package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-CAbH1O1a.js} +1 -1
- package/lib/public/assets/{play-Cvc8xzic.js → play-sg95hLqA.js} +1 -1
- package/lib/public/assets/{plus-C9Zj84UF.js → plus-BT9mk2gj.js} +1 -1
- package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-DZOz5Ypc.js} +1 -1
- package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-CqeImlEO.js} +1 -1
- package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-U_8TSysj.js} +1 -1
- package/lib/public/assets/{select-BWpnhWXj.js → select-Cx4JDzsR.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
- package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-CKR5URll.js} +1 -1
- package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-DuIVzTW_.js} +1 -1
- package/lib/public/assets/{settings-V6x-cXwy.js → settings-B5fuvgTP.js} +1 -1
- package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-CtHX1dkh.js} +1 -1
- package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-DCGD0GQt.js} +1 -1
- package/lib/public/assets/{teams-ts8p5rEh.js → teams-MzlELlw9.js} +1 -1
- package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-BIRm2KVs.js} +1 -1
- package/lib/public/assets/{upload-Dv99y_le.js → upload-C-lgyswH.js} +1 -1
- package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-IOydXA8d.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
- package/lib/public/assets/{users-gJ5Y01yo.js → users-hwK5XGIy.js} +1 -1
- package/lib/public/assets/{users-D7tKMESH.js → users-ivNCjeus.js} +1 -1
- package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-DvcIEsdZ.js} +1 -1
- package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-DukgCe1u.js} +1 -1
- package/lib/public/assets/{zap-ClcHFxfo.js → zap-BE9gacFj.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/control.yaml +69 -4
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/routers/control.js +24 -5
- package/lib/src/dashboard/event-manager.js +5 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
- package/lib/src/device-utils.js +61 -24
- package/lib/src/gateway/internalCall.js +33 -5
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- 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/plugin.js +4 -1
- package/lib/src/scripts/initialize-database.js +0 -6
- package/lib/src/scripts/run-migrations.js +123 -880
- package/lib/src/services/ServerManager.js +25 -4
- package/lib/src/services/SessionLifecycleService.js +20 -5
- package/lib/src/services/UserService.js +8 -3
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/sessions/RemoteSession.js +11 -2
- package/lib/src/sessions/SessionManager.js +28 -1
- package/lib/src/sessions/appiumUmbrella.js +11 -0
- package/lib/src/sessions/sessionMemory.js +11 -2
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/unit/UserService.test.js +8 -0
- package/lib/test/unit/boot-device-sync.spec.js +195 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/install-scripts.spec.js +98 -0
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -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/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/run-migrations-database.spec.js +57 -424
- package/lib/test/unit/run-migrations.test.js +74 -674
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +32 -94
- package/lib/test/unit/session-log-indexes.spec.js +15 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/schema.json +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +0 -2
- package/lib/public/assets/device-explorer-BgGZ8AvV.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
|
|
package/lib/schema.json
CHANGED
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"sqlite",
|
|
164
164
|
"postgresql"
|
|
165
165
|
],
|
|
166
|
-
"description": "Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables
|
|
166
|
+
"description": "Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables, otherwise by matching them to this version's schema."
|
|
167
167
|
},
|
|
168
168
|
"databaseUrl": {
|
|
169
169
|
"type": "string",
|
package/lib/src/app/index.js
CHANGED
|
@@ -90,6 +90,7 @@ const PluginContext_1 = require("../PluginContext");
|
|
|
90
90
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
91
91
|
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
92
92
|
const nodeSessionMetrics_1 = require("../gateway/nodeSessionMetrics");
|
|
93
|
+
const nodeSessionDeviceLogs_1 = require("../gateway/nodeSessionDeviceLogs");
|
|
93
94
|
const dashboardPluginLink_1 = require("./dashboardPluginLink");
|
|
94
95
|
const ASYNC_LOCK = new async_lock_1.default();
|
|
95
96
|
// An async handler's error reaches the error handlers, as on Express 5.
|
|
@@ -219,6 +220,7 @@ function createRouter(pluginArgs, ownServer) {
|
|
|
219
220
|
// credentials for the node); it takes the hub's session token instead.
|
|
220
221
|
(0, nodeSessionStatus_1.registerNodeSessionStatus)(apiRouter, pluginArgs);
|
|
221
222
|
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(apiRouter, pluginArgs);
|
|
223
|
+
(0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(apiRouter, pluginArgs);
|
|
222
224
|
// Dashboard login: unauthenticated (rate-limited internally via separate IP logic)
|
|
223
225
|
apiRouter.use('/auth', (0, auth_1.authPublicRouter)()); // login, logout — unauthenticated
|
|
224
226
|
// All remaining /api/* requires authentication (cookie session for
|
|
@@ -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:
|
|
@@ -1736,6 +1736,82 @@ paths:
|
|
|
1736
1736
|
$ref: '#/components/responses/GridNodeUnknownSession'
|
|
1737
1737
|
'503':
|
|
1738
1738
|
$ref: '#/components/responses/GridNodeTokenUnavailable'
|
|
1739
|
+
/api/node/sessions/{sessionId}/device-logs:
|
|
1740
|
+
get:
|
|
1741
|
+
operationId: getGridNodeSessionDeviceLogs
|
|
1742
|
+
summary: Collect a session's device log lines from this node
|
|
1743
|
+
description: |
|
|
1744
|
+
Node only: a hub collects the device log lines a node has recorded
|
|
1745
|
+
for a session on one of its phones, as soon as the session is
|
|
1746
|
+
created, every 10 s after that, and once more when the session
|
|
1747
|
+
ends. The hub writes them as the session's Device logs. Only a
|
|
1748
|
+
server started with `hub` set has this route.
|
|
1749
|
+
|
|
1750
|
+
The node records each session its hub created, with the same limit
|
|
1751
|
+
as a session on the hub's own phone (10,000 lines, then up to 2,000
|
|
1752
|
+
more errors), and keeps the lines in memory until the hub collects
|
|
1753
|
+
them, an ended session's for 10 minutes. If no hub asks within 2
|
|
1754
|
+
minutes of the start, the node stops recording that session.
|
|
1755
|
+
|
|
1756
|
+
Each line has a number, `seq`, counting up from 1. `after` asks for
|
|
1757
|
+
the lines after that number, and the node drops the others, so a
|
|
1758
|
+
later call never returns them again. An answer holds at most 2,000
|
|
1759
|
+
lines; `more` says the node has others, to ask for at once. `state`
|
|
1760
|
+
is `recording`, `ended`, or `off` (the node doesn't record this
|
|
1761
|
+
session). `timestamp` is the line's moment by the node's clock.
|
|
1762
|
+
|
|
1763
|
+
It is answered before the login, with the same rule as
|
|
1764
|
+
`GET /api/node/sessions/{sessionId}`: no credential with
|
|
1765
|
+
per-command auth off or auth disabled, otherwise the hub's session
|
|
1766
|
+
token for that session. Every answer from a node carries
|
|
1767
|
+
`x-xenon-node-device-logs: 1`. It isn't rate-limited.
|
|
1768
|
+
tags:
|
|
1769
|
+
- Hub-Node
|
|
1770
|
+
security:
|
|
1771
|
+
- HubToken: []
|
|
1772
|
+
- {}
|
|
1773
|
+
parameters:
|
|
1774
|
+
- $ref: '#/components/parameters/GridNodeSessionId'
|
|
1775
|
+
- in: query
|
|
1776
|
+
name: after
|
|
1777
|
+
required: false
|
|
1778
|
+
description: A line number; only later lines are returned, and the others are dropped. Anything that isn't a plain number is ignored.
|
|
1779
|
+
schema:
|
|
1780
|
+
type: integer
|
|
1781
|
+
example: 2000
|
|
1782
|
+
responses:
|
|
1783
|
+
'200':
|
|
1784
|
+
description: The session's lines.
|
|
1785
|
+
headers:
|
|
1786
|
+
x-xenon-node-device-logs:
|
|
1787
|
+
description: Always `1` on a node.
|
|
1788
|
+
schema:
|
|
1789
|
+
type: string
|
|
1790
|
+
example: '1'
|
|
1791
|
+
content:
|
|
1792
|
+
application/json:
|
|
1793
|
+
schema:
|
|
1794
|
+
type: object
|
|
1795
|
+
properties:
|
|
1796
|
+
value:
|
|
1797
|
+
$ref: '#/components/schemas/GridNodeDeviceLogs'
|
|
1798
|
+
example:
|
|
1799
|
+
value:
|
|
1800
|
+
state: recording
|
|
1801
|
+
more: false
|
|
1802
|
+
lines:
|
|
1803
|
+
- seq: 2001
|
|
1804
|
+
message: '10-05 14:20:02.114 4127 4127 I ActivityManager: Start proc 4127:com.example.shop/u0a215'
|
|
1805
|
+
timestamp: 1791111602114
|
|
1806
|
+
- seq: 2002
|
|
1807
|
+
message: '10-05 14:20:02.391 4127 4127 E AndroidRuntime: FATAL EXCEPTION: main'
|
|
1808
|
+
timestamp: 1791111602391
|
|
1809
|
+
'401':
|
|
1810
|
+
$ref: '#/components/responses/GridNotANodeUnauthorized'
|
|
1811
|
+
'404':
|
|
1812
|
+
$ref: '#/components/responses/GridNodeUnknownSession'
|
|
1813
|
+
'503':
|
|
1814
|
+
$ref: '#/components/responses/GridNodeTokenUnavailable'
|
|
1739
1815
|
/api/status:
|
|
1740
1816
|
get:
|
|
1741
1817
|
operationId: getGridServerStatus
|
|
@@ -2304,6 +2380,32 @@ components:
|
|
|
2304
2380
|
type: string
|
|
2305
2381
|
nullable: true
|
|
2306
2382
|
description: The app the app figures are for (Android), else null.
|
|
2383
|
+
GridNodeDeviceLogs:
|
|
2384
|
+
type: object
|
|
2385
|
+
properties:
|
|
2386
|
+
state:
|
|
2387
|
+
type: string
|
|
2388
|
+
enum:
|
|
2389
|
+
- recording
|
|
2390
|
+
- ended
|
|
2391
|
+
- 'off'
|
|
2392
|
+
more:
|
|
2393
|
+
type: boolean
|
|
2394
|
+
description: The node holds more lines than this answer; ask again with the last `seq`.
|
|
2395
|
+
lines:
|
|
2396
|
+
type: array
|
|
2397
|
+
items:
|
|
2398
|
+
type: object
|
|
2399
|
+
properties:
|
|
2400
|
+
seq:
|
|
2401
|
+
type: integer
|
|
2402
|
+
description: The line's number in the session, from 1.
|
|
2403
|
+
message:
|
|
2404
|
+
type: string
|
|
2405
|
+
description: The line as the device printed it.
|
|
2406
|
+
timestamp:
|
|
2407
|
+
type: integer
|
|
2408
|
+
description: Epoch milliseconds, by the node's clock.
|
|
2307
2409
|
GridWebDriverError:
|
|
2308
2410
|
type: object
|
|
2309
2411
|
description: A WebDriver-style error, as Appium answers one.
|
|
@@ -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,
|
|
@@ -79,6 +79,7 @@ const NotificationService_1 = require("../services/NotificationService");
|
|
|
79
79
|
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
80
80
|
const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
|
|
81
81
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
82
|
+
const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
|
|
82
83
|
const sessionCommandSummary_1 = require("./sessionCommandSummary");
|
|
83
84
|
const commandLogFields_1 = require("./commandLogFields");
|
|
84
85
|
const typedi_2 = require("typedi");
|
|
@@ -156,19 +157,15 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
156
157
|
// once its row exists (the lines point at it), unless the session turned
|
|
157
158
|
// it off: an Android phone's stream, or the log an iPhone's or
|
|
158
159
|
// simulator's driver captures. Not waited for: an Android phone's clock
|
|
159
|
-
// and log stream are read in the background.
|
|
160
|
+
// and log stream are read in the background. A node's phone's lines are
|
|
161
|
+
// collected from the node, through the session.
|
|
160
162
|
const deviceLogs = typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs);
|
|
161
163
|
if (capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] === false) {
|
|
162
164
|
void deviceLogs.noteOff(session.getId());
|
|
163
165
|
}
|
|
164
166
|
else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
device,
|
|
168
|
-
since: session.allocatedAt,
|
|
169
|
-
driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c),
|
|
170
|
-
appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e),
|
|
171
|
-
});
|
|
167
|
+
const deviceLogSource = (0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(session);
|
|
168
|
+
void deviceLogs.start(Object.assign({ sessionId: session.getId(), device, since: session.allocatedAt, driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c), appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e) }, (deviceLogSource ? { source: deviceLogSource } : {})));
|
|
172
169
|
}
|
|
173
170
|
// Emit session started event
|
|
174
171
|
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
|