@xenon-device-management/xenon 2.2.0 → 2.4.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 +1 -1
- package/lib/public/assets/{AnnotationOverlay-XpewX5nn.js → AnnotationOverlay-CpoWcXKP.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BQDv3ZCC.js → ApiKeyGate-DAYuoJDl.js} +1 -1
- package/lib/public/assets/{BugReportButton-C7OmS61B.js → BugReportButton-DEXyENUv.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-C-sJRz63.js +2 -0
- package/lib/public/assets/{EmptyState-fPxujqqE.js → EmptyState-B_JRYedM.js} +1 -1
- package/lib/public/assets/{FieldGroup-5bCutWrs.js → FieldGroup-CorXlSLU.js} +1 -1
- package/lib/public/assets/{FilterMenu-owsBiDT1.js → FilterMenu-BKv95DEc.js} +1 -1
- package/lib/public/assets/{Menu-DFhqzPSt.js → Menu-Cpy5yMwg.js} +1 -1
- package/lib/public/assets/{Modal-Bw-a-E-e.js → Modal-Djp4NvyZ.js} +1 -1
- package/lib/public/assets/{RecordingPage-WtByf5bC.js → RecordingPage-CP57OKw9.js} +1 -1
- package/lib/public/assets/{RecordingsPage-FpWWFNgT.js → RecordingsPage-BjrTit38.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Jzn9c7o8.js → SegmentedControl-MGTzJhBf.js} +1 -1
- package/lib/public/assets/{SettingCard-BR7nOhpT.js → SettingCard-CYBPhKPe.js} +1 -1
- package/lib/public/assets/{Table-B_Va3V05.js → Table-CdKQ9hnU.js} +1 -1
- package/lib/public/assets/{activity-DxRFWvkN.js → activity-QsRK21QF.js} +1 -1
- package/lib/public/assets/{ai-settings-CQf-9_V6.js → ai-settings-Dn6DtRn7.js} +1 -1
- package/lib/public/assets/{api-keys-SoE1Kd7N.js → api-keys-DYyf17eU.js} +1 -1
- package/lib/public/assets/{apps-CgGXTlgn.js → apps-HKFKTe1J.js} +1 -1
- package/lib/public/assets/{arrow-left-uQ6WIwiC.js → arrow-left-BkA7oMRe.js} +1 -1
- package/lib/public/assets/{arrow-right-C9RyfzXS.js → arrow-right-BsSr58EU.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DFHi_8yG.js → arrow-up-right-B7TEV_Gr.js} +1 -1
- package/lib/public/assets/{auth-shell-Bii1t4qc.js → auth-shell-BlmNHGcG.js} +2 -2
- package/lib/public/assets/{builds-page-Bnv8Z9hi.js → builds-page-E4WIKtl8.js} +1 -1
- package/lib/public/assets/{button-_Y78wXiP.js → button-BIwCF2bR.js} +1 -1
- package/lib/public/assets/{calendar-D4MqSOc2.js → calendar-CMLnXC7N.js} +1 -1
- package/lib/public/assets/{check-uGHCXe-L.js → check-zugO3p5U.js} +1 -1
- package/lib/public/assets/{chevron-left-Dy1OqpzB.js → chevron-left-sCqxpTY6.js} +1 -1
- package/lib/public/assets/{chevron-right-BUsldEli.js → chevron-right-Bb59xwZc.js} +1 -1
- package/lib/public/assets/{circle-check-D3Qq0tKp.js → circle-check-DsvxbnTg.js} +1 -1
- package/lib/public/assets/{circle-x-B4IOD1DE.js → circle-x-w69AbmbW.js} +1 -1
- package/lib/public/assets/{clock-BfYfzvSg.js → clock-C6XOs4bc.js} +1 -1
- package/lib/public/assets/{copy-DZwYE5Va.js → copy-pHk-SWBM.js} +1 -1
- package/lib/public/assets/{device-explorer-CRG4Utt5.js → device-explorer-BDq38IcO.js} +52 -52
- package/lib/public/assets/{download-CCDYo_TN.js → download-CAHtPZmY.js} +1 -1
- package/lib/public/assets/{forgot-password-DuLCmVFb.js → forgot-password-cmvhGOUu.js} +1 -1
- package/lib/public/assets/{funnel-CtKNPqzb.js → funnel-BrDuhOtM.js} +1 -1
- package/lib/public/assets/{index-3uxl3o7K.js → index-BQCXPOXt.js} +2 -2
- package/lib/public/assets/{index-Zy1Op5C1.js → index-C-Q4QkXb.js} +1 -1
- package/lib/public/assets/{input-CzTWvUXZ.js → input-DnUVHYax.js} +1 -1
- package/lib/public/assets/{keycodes-CI-nYDFM.js → keycodes-CmOptFVa.js} +4 -4
- package/lib/public/assets/{layers-YZZGFyTX.js → layers-DRtz_bzF.js} +1 -1
- package/lib/public/assets/{lock-L8tzWgDo.js → lock-DF2VnPFm.js} +1 -1
- package/lib/public/assets/{login-DBbX-fu8.js → login-DHYffYa4.js} +1 -1
- package/lib/public/assets/{maintenance-settings-AJwkOIsE.js → maintenance-settings-r0uPsJ6n.js} +1 -1
- package/lib/public/assets/{monitor-Di_66F2m.js → monitor-7nt9oGNx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-Cnab2zdg.js → mouse-pointer-2-ys1MKVr2.js} +1 -1
- package/lib/public/assets/{network-DiSumY-1.js → network-B8m-F79k.js} +1 -1
- package/lib/public/assets/{overview-CwE4gXl5.js → overview-BHEntlXz.js} +1 -1
- package/lib/public/assets/{page-header-CfR4Sm9V.js → page-header-CuFXVUop.js} +1 -1
- package/lib/public/assets/{plus-DOS8P9CQ.js → plus-DUSlMac2.js} +1 -1
- package/lib/public/assets/{profile-page-BYpHwQIH.js → profile-page-CBamPy_h.js} +1 -1
- package/lib/public/assets/{reset-password-BRVJU_qV.js → reset-password-DG0vZl-o.js} +1 -1
- package/lib/public/assets/{runbook-page-Bi3JvAFZ.js → runbook-page-5S3T-I4g.js} +1 -1
- package/lib/public/assets/{select-Cqx7pEou.js → select-f0ci1QC7.js} +1 -1
- package/lib/public/assets/{selector-detail-page-C0DsncKj.js → selector-detail-page-BXA9LUf9.js} +1 -1
- package/lib/public/assets/{selector-health-CAhpdilF.js → selector-health-TyMPgAWo.js} +1 -1
- package/lib/public/assets/{selector-health-page-CrCVeW2m.js → selector-health-page-uBlXy33G.js} +1 -1
- package/lib/public/assets/{session-detail-page-F_hwYqEt.js → session-detail-page-DECim5lh.js} +1 -1
- package/lib/public/assets/{settings-C-s5pypL.js → settings-IfOFbqtv.js} +1 -1
- package/lib/public/assets/{status-pill-outline-DY5uzv6A.js → status-pill-outline-grXLf8I8.js} +1 -1
- package/lib/public/assets/{tablet-Dli7gwyg.js → tablet-lmFk4qxW.js} +1 -1
- package/lib/public/assets/{teams-ui4IhHNC.js → teams-CwMSjVVW.js} +1 -1
- package/lib/public/assets/{trash-2-BV4hSmXJ.js → trash-2-BKsgmBS6.js} +1 -1
- package/lib/public/assets/{upload-DkGy_J-L.js → upload-DXH47sQS.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BFtMqQOh.js → use-password-reset-mode-DU98dxpc.js} +1 -1
- package/lib/public/assets/{users-Nb_rzRmF.js → users-V5O64rVx.js} +1 -1
- package/lib/public/assets/{users-BU0nXIVe.js → users-eXWwQZjX.js} +1 -1
- package/lib/public/assets/{video-off-CUNzD2kH.js → video-off-BuoIHEYy.js} +1 -1
- package/lib/public/assets/{webhook-settings-Ca3UFXZe.js → webhook-settings-ClsxsKnC.js} +1 -1
- package/lib/public/assets/{zap-B2bH2vKX.js → zap-I10vdQXd.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/control.js +114 -61
- package/lib/src/app/routers/nodePhoneControl.js +288 -46
- package/lib/src/data-service/device-store.js +7 -3
- package/lib/src/data-service/deviceClaims.js +14 -0
- package/lib/src/data-service/deviceFieldOwners.js +3 -1
- package/lib/src/data-service/prisma-store.js +3 -3
- package/lib/src/device-managers/AndroidDeviceManager.js +11 -1
- 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/services/recording/RecordingOrchestrator.js +39 -5
- package/lib/src/services/recording/busy-precheck.js +5 -2
- package/lib/src/services/recording/nodeRecordingSource.js +156 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +12 -0
- package/lib/test/unit/busy-precheck.spec.js +32 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +66 -10
- package/lib/test/unit/hub-node-race.spec.js +29 -3
- package/lib/test/unit/mark-node-busy-loki.spec.js +17 -4
- package/lib/test/unit/node-phone-control.spec.js +124 -4
- package/lib/test/unit/node-recording-relay.spec.js +165 -0
- package/lib/test/unit/recording-orchestrator.spec.js +86 -2
- package/lib/test/unit/screenshot-route.spec.js +104 -0
- package/lib/test/unit/upload-install-route.spec.js +205 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
- package/prisma/schema.prisma +4 -0
- package/lib/public/assets/DeviceMosaicView-wl3l2Zlt.js +0 -2
|
@@ -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-BQCXPOXt.js";/* empty css */import{F as S}from"./FieldGroup-CorXlSLU.js";import{P as O}from"./page-header-CuFXVUop.js";import{T as V}from"./trash-2-BKsgmBS6.js";import{P as x}from"./plus-DUSlMac2.js";import{C as B}from"./circle-x-w69AbmbW.js";import{C as H}from"./circle-check-DsvxbnTg.js";import{Z as I}from"./zap-I10vdQXd.js";import{A as J}from"./activity-QsRK21QF.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-BQCXPOXt.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");
|
|
@@ -233,6 +235,7 @@ router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
233
235
|
res.status(400).send('Manager not found or pressKey not supported');
|
|
234
236
|
}));
|
|
235
237
|
router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
var _a, _b;
|
|
236
239
|
const { udid } = req.params;
|
|
237
240
|
const device = yield getDeviceInfo(udid);
|
|
238
241
|
if (!device)
|
|
@@ -248,7 +251,16 @@ router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0,
|
|
|
248
251
|
}
|
|
249
252
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
250
253
|
if (manager && manager.getScreenshot) {
|
|
251
|
-
|
|
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
|
+
}
|
|
252
264
|
// CRITICAL: Validate screenshot is not empty before returning success
|
|
253
265
|
if (base64 && base64.length > 100) {
|
|
254
266
|
return res.status(200).send({ screenshot: base64 });
|
|
@@ -415,11 +427,16 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
|
|
|
415
427
|
if (!(0, appVisibility_1.canSeeApp)(app, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds)) {
|
|
416
428
|
return res.status(404).send('App not found in repository');
|
|
417
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
|
+
}
|
|
418
438
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
419
439
|
if (manager && manager.installApp) {
|
|
420
|
-
// If it's a local file, pass the path.
|
|
421
|
-
// In a distributed setup, the node would ideally download it.
|
|
422
|
-
// For now, we assume hub-node shared storage or hub-local execution.
|
|
423
440
|
yield manager.installApp(udid, app.filepath);
|
|
424
441
|
return res.status(200).send({ success: true, message: `Installed ${app.name}` });
|
|
425
442
|
}
|
|
@@ -430,38 +447,71 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
|
|
|
430
447
|
res.status(500).send({ error: err.message });
|
|
431
448
|
}
|
|
432
449
|
}));
|
|
433
|
-
|
|
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;
|
|
434
481
|
const { udid } = req.params;
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
fs_extra_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
447
|
-
const appPath = path_1.default.join(tmpDir, `${Date.now()}-${appFile.name}`);
|
|
448
|
-
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' };
|
|
449
493
|
yield appFile.mv(appPath);
|
|
450
494
|
const manager = yield getDeviceManagerForPlatform(device.platform);
|
|
451
|
-
if (manager
|
|
452
|
-
|
|
453
|
-
// Clean up after installation
|
|
454
|
-
setTimeout(() => fs_extra_1.default.remove(appPath).catch(() => { }), 10000);
|
|
455
|
-
return res
|
|
456
|
-
.status(200)
|
|
457
|
-
.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' };
|
|
458
497
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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) => {
|
|
462
504
|
logger_1.default.error(`Installation failed for ${udid}: ${err.message}`);
|
|
463
|
-
|
|
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);
|
|
464
513
|
}
|
|
514
|
+
res.status(answer.status).send(answer.body);
|
|
465
515
|
}));
|
|
466
516
|
router.post('/:udid/uninstall', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
467
517
|
const { udid } = req.params;
|
|
@@ -1032,6 +1082,33 @@ router.post('/:udid/shell', (req, res) => __awaiter(void 0, void 0, void 0, func
|
|
|
1032
1082
|
}
|
|
1033
1083
|
res.status(400).send('Manager not found or executeShell not supported');
|
|
1034
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
|
+
}
|
|
1035
1112
|
/**
|
|
1036
1113
|
* Omni-Scan for manual control (No Appium Session)
|
|
1037
1114
|
*/
|
|
@@ -1040,28 +1117,12 @@ router.get('/:udid/omni-scan', (req, res) => __awaiter(void 0, void 0, void 0, f
|
|
|
1040
1117
|
const device = yield getDeviceInfo(udid);
|
|
1041
1118
|
if (!device)
|
|
1042
1119
|
return res.status(404).send('Device not found');
|
|
1043
|
-
const
|
|
1044
|
-
if (!
|
|
1120
|
+
const screen = yield omniScreen(req, udid, device);
|
|
1121
|
+
if (!screen)
|
|
1045
1122
|
return res.status(400).send('Manager not found');
|
|
1046
1123
|
try {
|
|
1047
1124
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
1048
|
-
|
|
1049
|
-
const mockDriver = {
|
|
1050
|
-
sessionId: `manual_${udid}`,
|
|
1051
|
-
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1052
|
-
if (manager.getScreenshot) {
|
|
1053
|
-
return yield manager.getScreenshot(udid);
|
|
1054
|
-
}
|
|
1055
|
-
throw new Error('Screenshot not supported for this device');
|
|
1056
|
-
}),
|
|
1057
|
-
// OmniVision might need page source for some analysis later
|
|
1058
|
-
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1059
|
-
if (manager.getPageSource) {
|
|
1060
|
-
return yield manager.getPageSource(udid);
|
|
1061
|
-
}
|
|
1062
|
-
return '';
|
|
1063
|
-
}),
|
|
1064
|
-
};
|
|
1125
|
+
const mockDriver = Object.assign({ sessionId: `manual_${udid}` }, screen);
|
|
1065
1126
|
const result = yield omniService.analyzeScreen(mockDriver);
|
|
1066
1127
|
return res.status(200).send({ status: 'success', value: result });
|
|
1067
1128
|
}
|
|
@@ -1140,20 +1201,12 @@ router.post('/:udid/test-locator', (req, res) => __awaiter(void 0, void 0, void
|
|
|
1140
1201
|
const device = yield getDeviceInfo(udid);
|
|
1141
1202
|
if (!device)
|
|
1142
1203
|
return res.status(404).send('Device not found');
|
|
1143
|
-
const
|
|
1144
|
-
if (!
|
|
1204
|
+
const screen = yield omniScreen(req, udid, device);
|
|
1205
|
+
if (!screen)
|
|
1145
1206
|
return res.status(400).send('Manager not found');
|
|
1146
1207
|
try {
|
|
1147
1208
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
1148
|
-
const mockDriver = {
|
|
1149
|
-
sessionId: `manual_${udid}`,
|
|
1150
|
-
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1151
|
-
if (manager.getScreenshot) {
|
|
1152
|
-
return yield manager.getScreenshot(udid);
|
|
1153
|
-
}
|
|
1154
|
-
throw new Error('Screenshot not supported for this device');
|
|
1155
|
-
}),
|
|
1156
|
-
};
|
|
1209
|
+
const mockDriver = { sessionId: `manual_${udid}`, getScreenshot: screen.getScreenshot };
|
|
1157
1210
|
let value = [];
|
|
1158
1211
|
if (strategy === '-custom:ai-text') {
|
|
1159
1212
|
value = yield omniService.findByText(mockDriver, selector);
|