@xenon-device-management/xenon 2.1.0 → 2.3.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/lib/package.json +3 -3
- package/lib/public/assets/{AnnotationOverlay-C-Pw_hvw.js → AnnotationOverlay-AxcBYoKp.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DmYQ0oSk.js → ApiKeyGate-CIU02EIx.js} +1 -1
- package/lib/public/assets/{BugReportButton-n8T1dFUp.js → BugReportButton-_akEEjXm.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-Bhk1udmn.js +2 -0
- package/lib/public/assets/{EmptyState-DExETevp.js → EmptyState-DRKRm19_.js} +1 -1
- package/lib/public/assets/{FieldGroup-CCdFlZm6.js → FieldGroup-zQQHPh7x.js} +1 -1
- package/lib/public/assets/{FilterMenu-DiUSOSL-.js → FilterMenu-C9_StnDi.js} +1 -1
- package/lib/public/assets/{Menu-ZOfpH6gh.js → Menu-0J7io6pF.js} +1 -1
- package/lib/public/assets/{Modal-CZNtoB-1.js → Modal-C677ljNX.js} +1 -1
- package/lib/public/assets/{RecordingPage-BKp6h8Jl.js → RecordingPage-BDJFck-L.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bf881FX4.js → RecordingsPage-DQkimbKK.js} +1 -1
- package/lib/public/assets/{SegmentedControl-EUoyizvW.js → SegmentedControl-DvGMZBIE.js} +1 -1
- package/lib/public/assets/{SettingCard-ClT4hf4_.js → SettingCard-ogyouYBq.js} +1 -1
- package/lib/public/assets/{Table-ucR4bnmc.js → Table-BLFCOLlO.js} +1 -1
- package/lib/public/assets/{activity-K-hdylwq.js → activity-BU55TGRG.js} +1 -1
- package/lib/public/assets/{ai-settings-BLhdwI3c.js → ai-settings-GpDAiHav.js} +1 -1
- package/lib/public/assets/{api-keys-BbiN7rBI.js → api-keys-dOzP0k8_.js} +1 -1
- package/lib/public/assets/{apps-Dez1petz.js → apps-mAwAQmkn.js} +1 -1
- package/lib/public/assets/{arrow-left-UjmFHLAJ.js → arrow-left-Dg9mxe9T.js} +1 -1
- package/lib/public/assets/{arrow-right-BVyEigbu.js → arrow-right-BVcmUyq3.js} +1 -1
- package/lib/public/assets/{arrow-up-right-B-ZlBof3.js → arrow-up-right-ByR5pZrH.js} +1 -1
- package/lib/public/assets/{auth-shell-DAQjAgUv.js → auth-shell-Jpgk8CgY.js} +2 -2
- package/lib/public/assets/{builds-page-CmIPa5JK.js → builds-page-VnRTrcdl.js} +1 -1
- package/lib/public/assets/{button-Dd-IhMC1.js → button-D-X4wjO8.js} +1 -1
- package/lib/public/assets/{calendar-CAIeJwa5.js → calendar-ur_p5ETA.js} +1 -1
- package/lib/public/assets/{check-C4Jrjg8f.js → check-K33iMeyq.js} +1 -1
- package/lib/public/assets/{chevron-left-BGzlf-4i.js → chevron-left-DMVWShd-.js} +1 -1
- package/lib/public/assets/{chevron-right-_wYqDjDC.js → chevron-right-DphLJ2_X.js} +1 -1
- package/lib/public/assets/{circle-check-Ckn00yMB.js → circle-check-CNFhu8oE.js} +1 -1
- package/lib/public/assets/{circle-x-pdVkomLa.js → circle-x-BH_cUbqZ.js} +1 -1
- package/lib/public/assets/{clock-B8vmwkHj.js → clock-CVMVdVvp.js} +1 -1
- package/lib/public/assets/{copy-BAqnFI-F.js → copy-CZ2XHafL.js} +1 -1
- package/lib/public/assets/{device-explorer-CYq_4F_N.js → device-explorer-B94BhaqF.js} +52 -52
- package/lib/public/assets/{download-B6UEAp_1.js → download-UUiyw7kO.js} +1 -1
- package/lib/public/assets/{forgot-password-CLqNvmrN.js → forgot-password-CrS14F79.js} +1 -1
- package/lib/public/assets/{funnel-Da90BBbT.js → funnel-BMOE5qJY.js} +1 -1
- package/lib/public/assets/{index-BuIIcmGp.js → index-2XPPbPwy.js} +43 -43
- package/lib/public/assets/{index-MX7IzqyW.js → index-D0R9Yic3.js} +1 -1
- package/lib/public/assets/{input-Bq9aY-We.js → input-C-1b8J7i.js} +1 -1
- package/lib/public/assets/{keycodes-CIhXIJyP.js → keycodes-Cly7dQc9.js} +4 -4
- package/lib/public/assets/{layers-bXjpswQI.js → layers-DFZfqGdX.js} +1 -1
- package/lib/public/assets/{lock-pJ72y5Fy.js → lock-ylHMJpfP.js} +1 -1
- package/lib/public/assets/{login-UOcHALV2.js → login-DWsWKqDx.js} +1 -1
- package/lib/public/assets/{maintenance-settings-XnJgp5yb.js → maintenance-settings-9B6_MTqW.js} +1 -1
- package/lib/public/assets/{monitor-QkAhI1Jx.js → monitor-CzX0hBH1.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-B42Da0m4.js → mouse-pointer-2-CZHD7N3z.js} +1 -1
- package/lib/public/assets/{network-CKVqcyp6.js → network-Bikc4MyT.js} +1 -1
- package/lib/public/assets/{overview-BORGkN3N.js → overview-DzaA0aMe.js} +1 -1
- package/lib/public/assets/{page-header-CKWFOjOI.js → page-header-DqGlDufy.js} +1 -1
- package/lib/public/assets/{plus-BpFKVdC8.js → plus-Ct6aQTGM.js} +1 -1
- package/lib/public/assets/{profile-page-CcbKBicY.js → profile-page-CCnUQ1Qk.js} +1 -1
- package/lib/public/assets/{reset-password-BA0W9bxl.js → reset-password-AmkGGXo5.js} +1 -1
- package/lib/public/assets/{runbook-page-Cp7ZBq3s.js → runbook-page-DQQn9VyE.js} +1 -1
- package/lib/public/assets/{select-DIZWIoVR.js → select-k5ogtEWW.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C_4YuvAp.js → selector-detail-page-BCcwC5ku.js} +1 -1
- package/lib/public/assets/{selector-health-CXjD_Xth.js → selector-health-BVyX6kIh.js} +1 -1
- package/lib/public/assets/{selector-health-page-CTqIHeoG.js → selector-health-page-CElBB6XT.js} +1 -1
- package/lib/public/assets/{session-detail-page-B0KYkk8z.js → session-detail-page-6bqo_-ND.js} +1 -1
- package/lib/public/assets/{settings-BcRBKUXd.js → settings-DKBj9KyN.js} +1 -1
- package/lib/public/assets/{status-pill-outline-Dh2PXSf_.js → status-pill-outline-CZNpvqej.js} +1 -1
- package/lib/public/assets/{tablet-_sXqZztt.js → tablet-B6HI5OxV.js} +1 -1
- package/lib/public/assets/{teams-DGHT8MKm.js → teams-owoDHbOU.js} +1 -1
- package/lib/public/assets/{trash-2-BV88cV3Z.js → trash-2-D61PoUrZ.js} +1 -1
- package/lib/public/assets/{upload-CIDH9wkj.js → upload-1ZYEVvgD.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DYGuSNwl.js → use-password-reset-mode-Dx0ghiiQ.js} +1 -1
- package/lib/public/assets/{users-7yviqflc.js → users-C5CS5Qez.js} +1 -1
- package/lib/public/assets/{users-BUE7MoTq.js → users-PbGMd5sD.js} +1 -1
- package/lib/public/assets/{video-off-BTRxtlGo.js → video-off-_XusI0HK.js} +1 -1
- package/lib/public/assets/{webhook-settings-DMKD9Pgm.js → webhook-settings-Bi_HHQlQ.js} +1 -1
- package/lib/public/assets/{zap-CWE94qcK.js → zap-uzHWFAKI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +119 -203
- package/lib/src/app/routers/nodePhoneControl.js +520 -0
- package/lib/src/app/ws/h264StreamWs.js +30 -4
- package/lib/src/app/ws/logcatWs.js +36 -7
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/data-service/device-store.js +17 -2
- package/lib/src/data-service/deviceClaims.js +32 -0
- package/lib/src/data-service/deviceFieldOwners.js +3 -1
- package/lib/src/data-service/prisma-store.js +9 -1
- package/lib/src/device-managers/AndroidDeviceManager.js +38 -28
- package/lib/src/device-managers/android/AndroidH264StreamService.js +10 -2
- package/lib/src/device-managers/android/AndroidStreamService.js +3 -1
- package/lib/src/device-managers/ios/IOSStreamService.js +4 -2
- package/lib/src/gateway/forwardToNode.js +10 -3
- package/lib/src/generated/client/edge.js +5 -4
- package/lib/src/generated/client/index-browser.js +2 -1
- package/lib/src/generated/client/index.d.ts +36 -1
- package/lib/src/generated/client/index.js +5 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +4 -0
- package/lib/src/generated/client/wasm.js +2 -1
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/services/ServerManager.js +13 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +4 -2
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +60 -0
- package/lib/test/unit/android-h264-service.spec.js +18 -0
- package/lib/test/unit/clipboard-route.spec.js +4 -0
- package/lib/test/unit/control-forward-decision.spec.js +17 -17
- package/lib/test/unit/device-access-guard.spec.js +38 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +374 -1
- package/lib/test/unit/hub-node-race.spec.js +51 -0
- package/lib/test/unit/install-repository-app-team.spec.js +4 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +15 -0
- package/lib/test/unit/logcat-ws.spec.js +8 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +66 -0
- package/lib/test/unit/node-phone-control.spec.js +225 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/screenshot-route.spec.js +104 -0
- package/lib/test/unit/stream-leave-route.spec.js +4 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +4 -0
- package/lib/test/unit/stream-stop-recording.spec.js +4 -0
- package/lib/test/unit/stream-ticket-identity.spec.js +11 -1
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upload-install-route.spec.js +205 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
- package/prisma/schema.prisma +4 -0
- package/lib/public/assets/DeviceMosaicView-D-rGOBbJ.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,h as u}from"./index-
|
|
1
|
+
import{r as t,h as u}from"./index-2XPPbPwy.js";function n(){const[s,r]=t.useState(null);return t.useEffect(()=>{let e=!0;return u().then(o=>e&&r(o.passwordReset)),()=>{e=!1}},[]),s}export{n as u};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as w,b as k,r as a,j as e,$ as C}from"./index-
|
|
1
|
+
import{c as w,b as k,r as a,j as e,$ as C}from"./index-2XPPbPwy.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-PbGMd5sD.js";import{f as U}from"./time-B8FIgwPE.js";import{P}from"./page-header-DqGlDufy.js";import{B as y}from"./button-D-X4wjO8.js";import{I as N}from"./input-C-1b8J7i.js";import{S as g}from"./select-k5ogtEWW.js";import{P as B}from"./plus-Ct6aQTGM.js";import{T as L}from"./trash-2-D61PoUrZ.js";import"./utils-CDN07tui.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
1
|
+
import{o as s}from"./index-2XPPbPwy.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-
|
|
1
|
+
import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-2XPPbPwy.js";/* empty css */import{F as S}from"./FieldGroup-zQQHPh7x.js";import{P as O}from"./page-header-DqGlDufy.js";import{T as V}from"./trash-2-D61PoUrZ.js";import{P as x}from"./plus-Ct6aQTGM.js";import{C as B}from"./circle-x-BH_cUbqZ.js";import{C as H}from"./circle-check-CNFhu8oE.js";import{Z as I}from"./zap-uzHWFAKI.js";import{A as J}from"./activity-BU55TGRG.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),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(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{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:g.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:s.payloadTemplate?"CUSTOM":"SLACK"}),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:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{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 i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{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:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.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:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{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:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(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:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae 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-2XPPbPwy.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-hymrhlEZ.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
@@ -45,7 +45,8 @@ 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.previewLeaves = exports.previewLeaveDeps = void 0;
|
|
48
|
+
exports.previewLeaves = exports.previewLeaveDeps = exports.UPLOAD_INSTALL_MAX_BYTES = void 0;
|
|
49
|
+
exports.uploadName = uploadName;
|
|
49
50
|
const plugin_1 = require("../../plugin");
|
|
50
51
|
const express_1 = require("express");
|
|
51
52
|
const device_store_1 = require("../../data-service/device-store");
|
|
@@ -61,6 +62,7 @@ const IOSStreamService_1 = __importDefault(require("../../device-managers/ios/IO
|
|
|
61
62
|
const AndroidStreamService_1 = __importDefault(require("../../device-managers/android/AndroidStreamService"));
|
|
62
63
|
const AndroidH264StreamService_1 = __importDefault(require("../../device-managers/android/AndroidH264StreamService"));
|
|
63
64
|
const path_1 = __importDefault(require("path"));
|
|
65
|
+
const express_fileupload_1 = __importDefault(require("express-fileupload"));
|
|
64
66
|
const os_1 = __importDefault(require("os"));
|
|
65
67
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
66
68
|
const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
|
|
@@ -68,7 +70,6 @@ const InspectorService_1 = require("../../services/InspectorService");
|
|
|
68
70
|
const StreamTicketService_1 = require("../../services/token/StreamTicketService");
|
|
69
71
|
const appVisibility_1 = require("../../services/device-access/appVisibility");
|
|
70
72
|
const PluginContext_1 = require("../../PluginContext");
|
|
71
|
-
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
72
73
|
const streamType_1 = require("./streamType");
|
|
73
74
|
const iosStreamPort_1 = require("./iosStreamPort");
|
|
74
75
|
const androidH264Config_1 = require("./androidH264Config");
|
|
@@ -81,10 +82,10 @@ const deviceTeamGuard_1 = require("../../middleware/deviceTeamGuard");
|
|
|
81
82
|
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
82
83
|
const manualLock_1 = require("../../services/recording/manualLock");
|
|
83
84
|
const streamStartConflict_1 = require("./streamStartConflict");
|
|
85
|
+
const nodePhoneControl_1 = require("./nodePhoneControl");
|
|
84
86
|
const streamLeave_1 = require("./streamLeave");
|
|
85
87
|
const SessionOwnerResolver_1 = require("../../services/device-access/SessionOwnerResolver");
|
|
86
88
|
const actor_1 = require("../../services/device-access/actor");
|
|
87
|
-
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
88
89
|
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
89
90
|
const router = (0, express_1.Router)();
|
|
90
91
|
/** True when an in-memory iOS/Android (MJPEG or H.264) stream session exists. */
|
|
@@ -112,76 +113,10 @@ router.use((0, deviceTeamGuard_1.deviceTeamGuard)());
|
|
|
112
113
|
// another user's Appium session. Mounted here so every current and future
|
|
113
114
|
// mutation is covered without per-handler opt-in.
|
|
114
115
|
router.use((0, deviceAccessGuard_1.deviceAccessGuard)());
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'metadata.goog',
|
|
120
|
-
'100.100.100.200', // Alibaba ECS metadata
|
|
121
|
-
'fd00:ec2::254', // AWS IMDSv6
|
|
122
|
-
]);
|
|
123
|
-
/**
|
|
124
|
-
* Build a safe proxy URL from a device's reported host. Strips any path,
|
|
125
|
-
* query, or fragment the host string carried, blocks cloud-metadata targets,
|
|
126
|
-
* and refuses non-http(s) schemes. Returns null if the host is unsafe.
|
|
127
|
-
*/
|
|
128
|
-
function buildProxyUrl(deviceHost, req) {
|
|
129
|
-
let parsed;
|
|
130
|
-
try {
|
|
131
|
-
parsed = new URL(deviceHost);
|
|
132
|
-
}
|
|
133
|
-
catch (_a) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
137
|
-
return null;
|
|
138
|
-
if (FORBIDDEN_PROXY_HOSTS.has(parsed.hostname))
|
|
139
|
-
return null;
|
|
140
|
-
// Only keep scheme + host + port; discard any attacker-baked path/query/fragment.
|
|
141
|
-
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
142
|
-
// Treat req.originalUrl as a path — reparse to strip any control characters.
|
|
143
|
-
const forwardPath = req.originalUrl.startsWith('/') ? req.originalUrl : `/${req.originalUrl}`;
|
|
144
|
-
return `${origin}${forwardPath}`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Forward a control action on another server's phone to that server, signed
|
|
148
|
-
* for the caller: the hub's control token (hubSessionToken.ts) names the
|
|
149
|
-
* user, whether they are an admin, the phone and its node. This hub already
|
|
150
|
-
* checked them (the guards above); a node with auth enabled accepts the token
|
|
151
|
-
* for /control on that phone and runs its own ownership guard with that user.
|
|
152
|
-
* Sent with no credential at all, it was refused by every such node. A cloud
|
|
153
|
-
* provider never gets a hub token.
|
|
154
|
-
*/
|
|
155
|
-
function forwardControl(device, req, target) {
|
|
156
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
-
const actor = (0, actor_1.resolveActor)(req);
|
|
158
|
-
const token = !device.cloud && actor.userId
|
|
159
|
-
? yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor({
|
|
160
|
-
userId: actor.userId,
|
|
161
|
-
isAdmin: actor.isAdmin,
|
|
162
|
-
udid: device.udid,
|
|
163
|
-
host: device.host,
|
|
164
|
-
})
|
|
165
|
-
: null;
|
|
166
|
-
// Sent once: a retry after a slow node's timeout or 5xx could land the
|
|
167
|
-
// same tap twice.
|
|
168
|
-
const config = Object.assign(Object.assign({}, (token ? { headers: { [hubSessionToken_1.HUB_TOKEN_HEADER]: token } } : {})), { retry: false });
|
|
169
|
-
yield InternalHttpClient_1.InternalHttpClient.post(target, req.body, config);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* A phone another server drives, whose /control actions are forwarded there.
|
|
174
|
-
* Decided by the phone's own row (its nodeId, else its exact host against this
|
|
175
|
-
* server's own hosts), never by the Host header the caller used: a substring
|
|
176
|
-
* match on that header forwarded a lab's own phone back to itself, with no
|
|
177
|
-
* login, whenever the lab was reached as `localhost`.
|
|
178
|
-
*/
|
|
179
|
-
function isOtherServersPhone(device) {
|
|
180
|
-
if (!device.host)
|
|
181
|
-
return false;
|
|
182
|
-
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
183
|
-
return !(0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port), context.nodeId, device);
|
|
184
|
-
}
|
|
116
|
+
// Another server's phone (a node's, or a cloud provider's): its actions are
|
|
117
|
+
// sent on to its node, answered here, or refused, before any handler below
|
|
118
|
+
// runs it with this server's own tools. See nodePhoneControl.ts.
|
|
119
|
+
router.use((0, nodePhoneControl_1.nodePhoneControl)());
|
|
185
120
|
function getDeviceInfo(udid) {
|
|
186
121
|
return __awaiter(this, void 0, void 0, function* () {
|
|
187
122
|
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
@@ -213,26 +148,12 @@ function getDeviceManagerForPlatform(platform) {
|
|
|
213
148
|
});
|
|
214
149
|
}
|
|
215
150
|
router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
-
var _a, _b;
|
|
217
151
|
const { udid } = req.params;
|
|
218
152
|
const { x, y } = req.body;
|
|
219
153
|
const device = yield getDeviceInfo(udid);
|
|
220
154
|
if (!device)
|
|
221
155
|
return res.status(404).send('Device not found');
|
|
222
156
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
223
|
-
if (isOtherServersPhone(device)) {
|
|
224
|
-
const target = buildProxyUrl(device.host, req);
|
|
225
|
-
if (!target)
|
|
226
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
227
|
-
logger_1.default.info(`Proxying tap for ${udid} to ${target}`);
|
|
228
|
-
try {
|
|
229
|
-
yield forwardControl(device, req, target);
|
|
230
|
-
return res.status(200).send({ success: true });
|
|
231
|
-
}
|
|
232
|
-
catch (err) {
|
|
233
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
157
|
if (manager && manager.tap) {
|
|
237
158
|
try {
|
|
238
159
|
yield manager.tap(udid, x, y);
|
|
@@ -257,26 +178,12 @@ router.post('/:udid/tap', (req, res) => __awaiter(void 0, void 0, void 0, functi
|
|
|
257
178
|
res.status(400).send('Manager not found or tap not supported');
|
|
258
179
|
}));
|
|
259
180
|
router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
|
-
var _a, _b;
|
|
261
181
|
const { udid } = req.params;
|
|
262
182
|
const { x, y, endX, endY, duration = 1000 } = req.body;
|
|
263
183
|
const device = yield getDeviceInfo(udid);
|
|
264
184
|
if (!device)
|
|
265
185
|
return res.status(404).send('Device not found');
|
|
266
186
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
267
|
-
if (isOtherServersPhone(device)) {
|
|
268
|
-
const target = buildProxyUrl(device.host, req);
|
|
269
|
-
if (!target)
|
|
270
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
271
|
-
logger_1.default.info(`Proxying swipe for ${udid} to ${target}`);
|
|
272
|
-
try {
|
|
273
|
-
yield forwardControl(device, req, target);
|
|
274
|
-
return res.status(200).send({ success: true });
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
187
|
if (manager && manager.swipe) {
|
|
281
188
|
try {
|
|
282
189
|
yield manager.swipe(udid, x, y, endX, endY, duration);
|
|
@@ -290,26 +197,12 @@ router.post('/:udid/swipe', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
290
197
|
res.status(400).send('Manager not found or swipe not supported');
|
|
291
198
|
}));
|
|
292
199
|
router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
293
|
-
var _a, _b;
|
|
294
200
|
const { udid } = req.params;
|
|
295
201
|
const { text } = req.body;
|
|
296
202
|
const device = yield getDeviceInfo(udid);
|
|
297
203
|
if (!device)
|
|
298
204
|
return res.status(404).send('Device not found');
|
|
299
205
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
300
|
-
if (isOtherServersPhone(device)) {
|
|
301
|
-
const target = buildProxyUrl(device.host, req);
|
|
302
|
-
if (!target)
|
|
303
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
304
|
-
logger_1.default.info(`Proxying typeText for ${udid} to ${target}`);
|
|
305
|
-
try {
|
|
306
|
-
yield forwardControl(device, req, target);
|
|
307
|
-
return res.status(200).send({ success: true });
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
206
|
if (manager && manager.typeText) {
|
|
314
207
|
try {
|
|
315
208
|
yield manager.typeText(udid, text);
|
|
@@ -323,26 +216,12 @@ router.post('/:udid/text', (req, res) => __awaiter(void 0, void 0, void 0, funct
|
|
|
323
216
|
res.status(400).send('Manager not found or typeText not supported');
|
|
324
217
|
}));
|
|
325
218
|
router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
326
|
-
var _a, _b;
|
|
327
219
|
const { udid } = req.params;
|
|
328
220
|
const { keyCode } = req.body;
|
|
329
221
|
const device = yield getDeviceInfo(udid);
|
|
330
222
|
if (!device)
|
|
331
223
|
return res.status(404).send('Device not found');
|
|
332
224
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
333
|
-
if (isOtherServersPhone(device)) {
|
|
334
|
-
const target = buildProxyUrl(device.host, req);
|
|
335
|
-
if (!target)
|
|
336
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
337
|
-
logger_1.default.info(`Proxying keyevent for ${udid} to ${target}`);
|
|
338
|
-
try {
|
|
339
|
-
yield forwardControl(device, req, target);
|
|
340
|
-
return res.status(200).send({ success: true });
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
225
|
if (manager && manager.pressKey) {
|
|
347
226
|
try {
|
|
348
227
|
yield manager.pressKey(udid, keyCode);
|
|
@@ -356,6 +235,7 @@ router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
356
235
|
res.status(400).send('Manager not found or pressKey not supported');
|
|
357
236
|
}));
|
|
358
237
|
router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
var _a, _b;
|
|
359
239
|
const { udid } = req.params;
|
|
360
240
|
const device = yield getDeviceInfo(udid);
|
|
361
241
|
if (!device)
|
|
@@ -371,7 +251,16 @@ router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0,
|
|
|
371
251
|
}
|
|
372
252
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
373
253
|
if (manager && manager.getScreenshot) {
|
|
374
|
-
|
|
254
|
+
let base64;
|
|
255
|
+
try {
|
|
256
|
+
base64 = yield manager.getScreenshot(udid);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
// Answered: Express 4 doesn't catch a rejected async handler, and an
|
|
260
|
+
// iPhone with no live preview (no WDA, no tunnel) left this unanswered.
|
|
261
|
+
logger_1.default.error(`Screenshot capture failed for ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
262
|
+
return res.status(502).send({ error: `Screenshot capture failed: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}` });
|
|
263
|
+
}
|
|
375
264
|
// CRITICAL: Validate screenshot is not empty before returning success
|
|
376
265
|
if (base64 && base64.length > 100) {
|
|
377
266
|
return res.status(200).send({ screenshot: base64 });
|
|
@@ -426,26 +315,12 @@ router.post('/:udid/clipboard', (req, res) => __awaiter(void 0, void 0, void 0,
|
|
|
426
315
|
res.status(400).send('Manager not found or setClipboard not supported');
|
|
427
316
|
}));
|
|
428
317
|
router.post('/:udid/touchAndHold', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
429
|
-
var _a, _b;
|
|
430
318
|
const { udid } = req.params;
|
|
431
319
|
const { x, y, duration = 1000 } = req.body;
|
|
432
320
|
const device = yield getDeviceInfo(udid);
|
|
433
321
|
if (!device)
|
|
434
322
|
return res.status(404).send('Device not found');
|
|
435
323
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
436
|
-
if (isOtherServersPhone(device)) {
|
|
437
|
-
const target = buildProxyUrl(device.host, req);
|
|
438
|
-
if (!target)
|
|
439
|
-
return res.status(400).send({ error: 'Unsafe device host' });
|
|
440
|
-
logger_1.default.info(`Proxying touchAndHold for ${udid} to ${target}`);
|
|
441
|
-
try {
|
|
442
|
-
yield forwardControl(device, req, target);
|
|
443
|
-
return res.status(200).send({ success: true });
|
|
444
|
-
}
|
|
445
|
-
catch (err) {
|
|
446
|
-
return res.status(((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) || 500).send(((_b = err.response) === null || _b === void 0 ? void 0 : _b.data) || err.message);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
324
|
if (manager && manager.touchAndHold) {
|
|
450
325
|
try {
|
|
451
326
|
yield manager.touchAndHold(udid, x, y, duration);
|
|
@@ -552,11 +427,16 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
|
|
|
552
427
|
if (!(0, appVisibility_1.canSeeApp)(app, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds)) {
|
|
553
428
|
return res.status(404).send('App not found in repository');
|
|
554
429
|
}
|
|
430
|
+
// A node's phone: the app is in this server's library, not the node's,
|
|
431
|
+
// so the file goes to the node's own upload-install.
|
|
432
|
+
if ((0, nodePhoneControl_1.isOtherServersPhone)(device)) {
|
|
433
|
+
return yield (0, nodePhoneControl_1.installFileOnNode)(req, res, udid, device, {
|
|
434
|
+
path: app.filepath,
|
|
435
|
+
name: app.filename || path_1.default.basename(app.filepath),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
555
438
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
556
439
|
if (manager && manager.installApp) {
|
|
557
|
-
// If it's a local file, pass the path.
|
|
558
|
-
// In a distributed setup, the node would ideally download it.
|
|
559
|
-
// For now, we assume hub-node shared storage or hub-local execution.
|
|
560
440
|
yield manager.installApp(udid, app.filepath);
|
|
561
441
|
return res.status(200).send({ success: true, message: `Installed ${app.name}` });
|
|
562
442
|
}
|
|
@@ -567,38 +447,71 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
|
|
|
567
447
|
res.status(500).send({ error: err.message });
|
|
568
448
|
}
|
|
569
449
|
}));
|
|
570
|
-
|
|
450
|
+
/** Where upload-install writes an uploaded app while it is installed. */
|
|
451
|
+
const UPLOAD_DIR = path_1.default.join(os_1.default.tmpdir(), 'xenon-uploads');
|
|
452
|
+
/** The largest app upload-install takes: room for a large iOS app. */
|
|
453
|
+
exports.UPLOAD_INSTALL_MAX_BYTES = 4 * 1024 ** 3;
|
|
454
|
+
/**
|
|
455
|
+
* upload-install's own multipart parser. /control had none (only /apps
|
|
456
|
+
* mounts one), so every upload was answered "No files were uploaded." The
|
|
457
|
+
* file goes to a temporary file rather than memory, and one past the limit
|
|
458
|
+
* is answered 413.
|
|
459
|
+
*/
|
|
460
|
+
const uploadParser = (0, express_fileupload_1.default)({
|
|
461
|
+
useTempFiles: true,
|
|
462
|
+
tempFileDir: UPLOAD_DIR,
|
|
463
|
+
limits: { fileSize: exports.UPLOAD_INSTALL_MAX_BYTES },
|
|
464
|
+
abortOnLimit: true,
|
|
465
|
+
});
|
|
466
|
+
/**
|
|
467
|
+
* The name an upload is saved under: the client's name, cut to its last
|
|
468
|
+
* path segment and to safe characters, keeping its extension, which the
|
|
469
|
+
* installers read. busboy already cuts the name to its last segment (its
|
|
470
|
+
* `preservePath` is off); this holds if a parser ever keeps paths.
|
|
471
|
+
*/
|
|
472
|
+
function uploadName(clientName) {
|
|
473
|
+
const base = path_1.default
|
|
474
|
+
.basename(String(clientName !== null && clientName !== void 0 ? clientName : ''))
|
|
475
|
+
.replace(/[^\w.-]+/g, '_')
|
|
476
|
+
.replace(/^\.+/, '');
|
|
477
|
+
return `${Date.now()}-${base || 'app'}`;
|
|
478
|
+
}
|
|
479
|
+
router.post('/:udid/upload-install', uploadParser, (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
480
|
+
var _a;
|
|
571
481
|
const { udid } = req.params;
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
fs_extra_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
584
|
-
const appPath = path_1.default.join(tmpDir, `${Date.now()}-${appFile.name}`);
|
|
585
|
-
try {
|
|
482
|
+
const appFile = (_a = req.files) === null || _a === void 0 ? void 0 : _a.app;
|
|
483
|
+
const appPath = appFile ? path_1.default.join(UPLOAD_DIR, uploadName(appFile.name)) : undefined;
|
|
484
|
+
const answer = yield (() => __awaiter(void 0, void 0, void 0, function* () {
|
|
485
|
+
const device = yield getDeviceInfo(udid);
|
|
486
|
+
if (!device)
|
|
487
|
+
return { status: 404, body: 'Device not found' };
|
|
488
|
+
if (!req.files || Object.keys(req.files).length === 0) {
|
|
489
|
+
return { status: 400, body: 'No files were uploaded.' };
|
|
490
|
+
}
|
|
491
|
+
if (!appFile || !appPath)
|
|
492
|
+
return { status: 400, body: 'File "app" is required' };
|
|
586
493
|
yield appFile.mv(appPath);
|
|
587
494
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
588
|
-
if (manager
|
|
589
|
-
|
|
590
|
-
// Clean up after installation
|
|
591
|
-
setTimeout(() => fs_extra_1.default.remove(appPath).catch(() => { }), 10000);
|
|
592
|
-
return res
|
|
593
|
-
.status(200)
|
|
594
|
-
.send({ success: true, message: `App ${appFile.name} installed successfully` });
|
|
495
|
+
if (!(manager === null || manager === void 0 ? void 0 : manager.installApp)) {
|
|
496
|
+
return { status: 400, body: 'Manager not found or installApp not supported' };
|
|
595
497
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
498
|
+
yield manager.installApp(udid, appPath);
|
|
499
|
+
return {
|
|
500
|
+
status: 200,
|
|
501
|
+
body: { success: true, message: `App ${appFile.name} installed successfully` },
|
|
502
|
+
};
|
|
503
|
+
}))().catch((err) => {
|
|
599
504
|
logger_1.default.error(`Installation failed for ${udid}: ${err.message}`);
|
|
600
|
-
|
|
505
|
+
return { status: 500, body: { error: err.message } };
|
|
506
|
+
});
|
|
507
|
+
// The install has finished with the file, whether it worked or not (both
|
|
508
|
+
// installers wait for their process). Removed before answering, so nothing
|
|
509
|
+
// is left behind once the caller has the answer.
|
|
510
|
+
for (const file of [appPath, appFile === null || appFile === void 0 ? void 0 : appFile.tempFilePath]) {
|
|
511
|
+
if (file)
|
|
512
|
+
yield fs_extra_1.default.remove(file).catch(() => undefined);
|
|
601
513
|
}
|
|
514
|
+
res.status(answer.status).send(answer.body);
|
|
602
515
|
}));
|
|
603
516
|
router.post('/:udid/uninstall', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
604
517
|
const { udid } = req.params;
|
|
@@ -1169,6 +1082,33 @@ router.post('/:udid/shell', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
1169
1082
|
}
|
|
1170
1083
|
res.status(400).send('Manager not found or executeShell not supported');
|
|
1171
1084
|
}));
|
|
1085
|
+
/**
|
|
1086
|
+
* Where Omni-Vision reads the screen from, for the stand-in driver it is
|
|
1087
|
+
* given: this server's device manager, or, for a node's phone, the node
|
|
1088
|
+
* (screenshotFromNode). Omni itself runs here either way, with this
|
|
1089
|
+
* server's AI settings. Null when this server has no manager for the phone.
|
|
1090
|
+
*/
|
|
1091
|
+
function omniScreen(req, udid, device) {
|
|
1092
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1093
|
+
if ((0, nodePhoneControl_1.isOtherServersPhone)(device)) {
|
|
1094
|
+
return {
|
|
1095
|
+
getScreenshot: () => (0, nodePhoneControl_1.screenshotFromNode)(req, udid, device),
|
|
1096
|
+
getPageSource: () => __awaiter(this, void 0, void 0, function* () { return ''; }),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
1100
|
+
if (!manager)
|
|
1101
|
+
return null;
|
|
1102
|
+
return {
|
|
1103
|
+
getScreenshot: () => __awaiter(this, void 0, void 0, function* () {
|
|
1104
|
+
if (manager.getScreenshot)
|
|
1105
|
+
return yield manager.getScreenshot(udid);
|
|
1106
|
+
throw new Error('Screenshot not supported for this device');
|
|
1107
|
+
}),
|
|
1108
|
+
getPageSource: () => __awaiter(this, void 0, void 0, function* () { return (manager.getPageSource ? yield manager.getPageSource(udid) : ''); }),
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1172
1112
|
/**
|
|
1173
1113
|
* Omni-Scan for manual control (No Appium Session)
|
|
1174
1114
|
*/
|
|
@@ -1177,28 +1117,12 @@ router.get('/:udid/omni-scan', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
1177
1117
|
const device = yield getDeviceInfo(udid);
|
|
1178
1118
|
if (!device)
|
|
1179
1119
|
return res.status(404).send('Device not found');
|
|
1180
|
-
const
|
|
1181
|
-
if (!
|
|
1120
|
+
const screen = yield omniScreen(req, udid, device);
|
|
1121
|
+
if (!screen)
|
|
1182
1122
|
return res.status(400).send('Manager not found');
|
|
1183
1123
|
try {
|
|
1184
1124
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
1185
|
-
|
|
1186
|
-
const mockDriver = {
|
|
1187
|
-
sessionId: `manual_${udid}`,
|
|
1188
|
-
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1189
|
-
if (manager.getScreenshot) {
|
|
1190
|
-
return yield manager.getScreenshot(udid);
|
|
1191
|
-
}
|
|
1192
|
-
throw new Error('Screenshot not supported for this device');
|
|
1193
|
-
}),
|
|
1194
|
-
// OmniVision might need page source for some analysis later
|
|
1195
|
-
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1196
|
-
if (manager.getPageSource) {
|
|
1197
|
-
return yield manager.getPageSource(udid);
|
|
1198
|
-
}
|
|
1199
|
-
return '';
|
|
1200
|
-
}),
|
|
1201
|
-
};
|
|
1125
|
+
const mockDriver = Object.assign({ sessionId: `manual_${udid}` }, screen);
|
|
1202
1126
|
const result = yield omniService.analyzeScreen(mockDriver);
|
|
1203
1127
|
return res.status(200).send({ status: 'success', value: result });
|
|
1204
1128
|
}
|
|
@@ -1277,20 +1201,12 @@ router.post('/:udid/test-locator', (req, res) => __awaiter(void 0, void 0, void
|
|
|
1277
1201
|
const device = yield getDeviceInfo(udid);
|
|
1278
1202
|
if (!device)
|
|
1279
1203
|
return res.status(404).send('Device not found');
|
|
1280
|
-
const
|
|
1281
|
-
if (!
|
|
1204
|
+
const screen = yield omniScreen(req, udid, device);
|
|
1205
|
+
if (!screen)
|
|
1282
1206
|
return res.status(400).send('Manager not found');
|
|
1283
1207
|
try {
|
|
1284
1208
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
1285
|
-
const mockDriver = {
|
|
1286
|
-
sessionId: `manual_${udid}`,
|
|
1287
|
-
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1288
|
-
if (manager.getScreenshot) {
|
|
1289
|
-
return yield manager.getScreenshot(udid);
|
|
1290
|
-
}
|
|
1291
|
-
throw new Error('Screenshot not supported for this device');
|
|
1292
|
-
}),
|
|
1293
|
-
};
|
|
1209
|
+
const mockDriver = { sessionId: `manual_${udid}`, getScreenshot: screen.getScreenshot };
|
|
1294
1210
|
let value = [];
|
|
1295
1211
|
if (strategy === '-custom:ai-text') {
|
|
1296
1212
|
value = yield omniService.findByText(mockDriver, selector);
|