@buoy-gg/mcp 7.0.71 → 7.0.73
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/dist/{chunk-K4R23SLC.mjs → chunk-AHIKIVTB.mjs} +1 -1
- package/dist/chunk-DZRRLTOD.mjs +4 -0
- package/dist/chunk-MJDNVDJI.mjs +474 -0
- package/dist/{chunk-ISZRKNVH.mjs → chunk-OADIHE3T.mjs} +1 -1
- package/dist/{chunk-PIMFQHV4.mjs → chunk-OUCQTYIW.mjs} +1 -1
- package/dist/{chunk-AN6T7AGX.mjs → chunk-S4JHE46Q.mjs} +92 -92
- package/dist/cli.js +305 -253
- package/dist/cli.mjs +6 -6
- package/dist/{flowRun-M652USJQ.mjs → flowRun-GTYQQXHF.mjs} +1 -1
- package/dist/index.js +30 -28
- package/dist/index.mjs +1 -1
- package/dist/{init-Z4SKOMSV.mjs → init-FDDGVIXA.mjs} +1 -1
- package/dist/{main-MGASA6BJ.mjs → main-DJT43WTJ.mjs} +2 -2
- package/dist/{simFarm-QD6SXEZE.mjs → simFarm-5MWIXNER.mjs} +1 -1
- package/dist/src-ZFIIN4L2.mjs +1 -0
- package/package.json +4 -4
- package/skill/buoy-optimize/MEASURING.md +11 -2
- package/skill/buoy-optimize/SKILL.md +1 -1
- package/dist/chunk-EVLLWLFB.mjs +0 -4
- package/dist/chunk-N573FXZ7.mjs +0 -422
- package/dist/src-TOKDOFTX.mjs +0 -1
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import{createRequire as li}from"module";const Io=li(import.meta.url);var K=1;var Ne=1;var Xe={parkAfterMinutes:10,maxConcurrentBoots:3,reserveRamGB:24,reserveDiskGB:40,slimSimGB:2,stockSimGB:6,cloneDiskGB:2,freezePilot:false,personLeaseHours:8,runCapMinutes:60,keepRunsHours:24,fairMinFreeShare:.25,fairMaxSwingShare:.12,fairMaxThermalLevel:0,fairMaxSwapGrowthMB:256,phoneProfile:"iphone-se-2nd-gen",phoneOnLimit:"warn",phoneCapMB:0};var p=class extends Error{constructor(e,t,s,i=false){super(t);this.code=e;this.next=s;this.retryable=i;this.name="FarmError"}toJSON(){return{code:this.code,message:this.message,next:this.next,retryable:this.retryable}}};import{createHash as ci}from"crypto";var xt=["com.apple.sharingd"];var ve=[{"id":"widgets","name":"Widgets & Wallpaper","downside":"Home and Lock Screen widgets, wallpaper posters, and Live Activities stop updating.","approxMemoryMB":675,"labels":["com.apple.PosterBoard","com.apple.chronod","com.apple.liveactivitiesd"]},{"id":"siri","name":"Siri & Intelligence","downside":"Siri, speech features, and Apple Intelligence services are unavailable.","approxMemoryMB":265,"labels":["com.apple.assistantd","com.apple.assistant_cdmd","com.apple.assistant_service","com.apple.siriactionsd","com.apple.siriinferenced","com.apple.siriknowledged","com.apple.sirittsd","com.apple.siri.context.service","com.apple.siri.acousticsignature","com.apple.corespeechd","com.apple.voiced","com.apple.voicebankingd","com.apple.speechmodeltrainingd","com.apple.intelligenceplatformd","com.apple.intelligencecontextd","com.apple.intelligenceflowd","com.apple.intelligencetasksd","com.apple.generativeexperiencesd","com.apple.knowledgeconstructiond","com.apple.naturallanguaged","com.apple.textunderstandingd","com.apple.modelcatalogd","com.apple.modelmanagerd","com.apple.mlhostd","com.apple.mlruntimed","com.apple.suggestd","com.apple.parsecd","com.apple.parsec-fbf","com.apple.proactiveeventtrackerd"]},{"id":"search","name":"Spotlight & Search","downside":"Spotlight and Settings search return no results.","approxMemoryMB":50,"labels":["com.apple.searchd","com.apple.searchtoold","com.apple.spotlightknowledged","com.apple.spotlightknowledged.updater","com.apple.corespotlightservice"]},{"id":"icloud","name":"iCloud & Apple Account","downside":"iCloud sync, Apple Account, Keychain, and backup workflows will not work.","approxMemoryMB":100,"labels":["com.apple.appleaccountd","com.apple.appleaccounttransparencyd","com.apple.appleidsetupd","com.apple.akd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.cloudd","com.apple.cloudphotod","com.apple.ckdiscretionaryd","com.apple.cloudsettingssyncagent","com.apple.bird","com.apple.syncdefaultsd","com.apple.cdpd","com.apple.sosd","com.apple.SecureBackupDaemon","com.apple.TrustedPeersHelper","com.apple.protectedcloudstorage.protectedcloudkeysyncing","com.apple.icloudmailagent","com.apple.icloudsubscriptionoptimizerd","com.apple.communicationtrustd"]},{"id":"store","name":"App Store, Push & Media","downside":"Remote push notifications and StoreKit or App Store testing will not work.","approxMemoryMB":80,"labels":["com.apple.appstored","com.apple.appstorecomponentsd","com.apple.apsd","com.apple.itunescloudd","com.apple.itunesstored","com.apple.storekitd","com.apple.amsaccountsd","com.apple.amsengagementd","com.apple.amsondevicestoraged","com.apple.passd","com.apple.financed","com.apple.videosubscriptionsd","com.apple.assetsubscriptiond","com.apple.musicd"]},{"id":"pim","name":"Mail, Calendar & Contacts","downside":"Contacts, Calendar, Reminders, and Mail-backed pickers or sync may fail.","approxMemoryMB":80,"labels":["com.apple.email.maild","com.apple.exchangesyncd","com.apple.dataaccess.dataaccessd","com.apple.calaccessd","com.apple.remindd","com.apple.contactsd","com.apple.contacts.postersyncd","com.apple.peopled"]},{"id":"web","name":"Safari Sync & Web Services","downside":"Universal links and Safari sync or background web services will not work.","approxMemoryMB":50,"labels":["com.apple.SafariBookmarksSyncAgent","com.apple.Safari.History","com.apple.Safari.passwordbreachd","com.apple.Safari.SafeBrowsing.Service","com.apple.safarifetcherd","com.apple.WebBookmarks.webbookmarksd","com.apple.webkit.adattributiond","com.apple.webkit.webpushd","com.apple.webprivacyd","com.apple.swcd"]},{"id":"family","name":"Family & Screen Time","downside":"Family Sharing, Screen Time, and usage tracking stop working.","approxMemoryMB":65,"labels":["com.apple.familycircled","com.apple.FamilyControlsAgent","com.apple.familynotification","com.apple.askpermissiond","com.apple.asktod","com.apple.ScreenTimeAgent","com.apple.ScreenTimeSettingsAgent","com.apple.UsageTrackingAgent"]},{"id":"health","name":"Health, Home & Fitness","downside":"HealthKit, HomeKit, and Fitness integrations will not work.","approxMemoryMB":135,"labels":["com.apple.healthd","com.apple.healthappd","com.apple.healthcontentd","com.apple.healtheventsd","com.apple.healthrecordsd","com.apple.finhealthd","com.apple.homed","com.apple.homeeventsd","com.apple.fitcore","com.apple.fitcore.session","com.apple.fitnesscoachingd","com.apple.fitnessintelligenced","com.apple.activityawardsd","com.apple.activitysharingd"]},{"id":"photos","name":"Photos & Media Analysis","downside":"Photo picker, Photos-library workflows, and media analysis may fail.","approxMemoryMB":60,"labels":["com.apple.photoanalysisd","com.apple.photosface","com.apple.mediaanalysisd","com.apple.mediaanalysisd.service","com.apple.mediastream.mstreamd","com.apple.medialibraryd","com.apple.assetsd","com.apple.assetsd.nebulad"]},{"id":"apps","name":"News, Weather, Maps & Games","downside":"News, Weather, Maps background data, and game-controller services are unavailable.","approxMemoryMB":90,"labels":["com.apple.newsd","com.apple.weatherd","com.apple.Maps.mapssyncd","com.apple.Maps.mapspushd","com.apple.Maps.geocorrectiond","com.apple.maps.destinationd","com.apple.MapKit.SnapshotService","com.apple.jetpackassetd","com.apple.tipsd","com.apple.gamed","com.apple.gamesaved","com.apple.GameController.gamecontrollerd"]},{"id":"messaging","name":"Messaging & FaceTime","downside":"iMessage, FaceTime, and related identity services will not work.","approxMemoryMB":60,"labels":["com.apple.identityservicesd","com.apple.ids_simd","com.apple.imautomatichistorydeletionagent","com.apple.imcore.imtransferagent","com.apple.imdpersistence.IMDPersistenceAgent","com.apple.facetimemessagestored","com.apple.telephonyutilities.callservicesd"]},{"id":"connectivity","name":"Sharing & Device Connectivity","downside":"AirDrop, Continuity, CarPlay, Watch, and Find My connectivity will not work.","approxMemoryMB":65,"labels":["com.apple.rapportd","com.apple.companiond","com.apple.carkitd","com.apple.wcd","com.apple.tvremoted","com.apple.avatarsd","com.apple.stickersd","com.apple.sociallayerd","com.apple.announced","com.apple.navd","com.apple.findmy.findmylocated"]},{"id":"telemetry","name":"Ads, Diagnostics & Telemetry","downside":"DeviceCheck plus analytics, diagnostics, and feedback services are unavailable.","approxMemoryMB":105,"labels":["com.apple.ap.adprivacyd","com.apple.ap.promotedcontentd","com.apple.diagnosticextensionsd","com.apple.feedbackd","com.apple.rtcreportingd","com.apple.securityuploadd","com.apple.geoanalyticsd","com.apple.triald","com.apple.followupd","com.apple.purplebuddy.budd","com.apple.devicecheckd"]},{"id":"other","name":"Other Background Services","downside":"Wallet, merchant, business, asset, and miscellaneous background services are unavailable.","approxMemoryMB":195,"labels":["com.apple.financed","com.apple.passd","com.apple.merchantd","com.apple.coreidvd","com.apple.businessservicesd","com.apple.deviceaccessd","com.apple.replicatord","com.apple.linkd","com.apple.ind","com.apple.storagedatad","com.apple.StatusKitAgent","com.apple.countryd","com.apple.mobileassetd","com.apple.managedconfiguration.passcodenagd"]}];function Ut(){const e=new Set(xt);const t=new Set;for(const s of ve)for(const i of s.labels)if(!e.has(i))t.add(i);return[...t].sort()}function ce(e){return e==="slim"?Ut():[]}function jt(e){return ci("sha256").update(e+"\n"+ce(e).join("\n")).digest("hex").slice(0,16)}import{promises as R,existsSync as Ze,readFileSync as di}from"fs";import O from"path";import{AsyncLocalStorage as pi}from"async_hooks";import{createHash as ui,randomBytes as se}from"crypto";import{promises as de}from"fs";import et from"path";import{randomBytes as hi}from"crypto";import{spawn as mi}from"child_process";var Se=new Set;function tt(){return Se.size}function fi(){for(const e of Se){try{e.kill("SIGKILL")}catch{}}}function M(e,t,s={}){const{timeoutMs:i=3e4,maxBytes:a=4*1024*1024}=s;return new Promise(o=>{const n=mi(e,t,{env:s.env??process.env,stdio:[s.input==null?"ignore":"pipe","pipe","pipe"]});Se.add(n);let r="";let l="";let c=false;n.stdout.on("data",u=>{if(r.length<a)r+=u.toString("utf8")});n.stderr.on("data",u=>{if(l.length<a)l+=u.toString("utf8")});if(s.input!=null)n.stdin.end(s.input);const d=setTimeout(()=>{c=true;n.kill("SIGTERM");setTimeout(()=>n.kill("SIGKILL"),2e3).unref?.()},i);n.on("error",u=>{Se.delete(n);clearTimeout(d);o({code:null,stdout:r,stderr:l+String(u),timedOut:c})});n.on("close",u=>{Se.delete(n);clearTimeout(d);o({code:u,stdout:r,stderr:l,timedOut:c})})})}async function N(e,t,s={}){const i=await M(e,t,s);if(i.timedOut)throw new Error(`${e} ${t.slice(0,3).join(" ")} timed out after ${s.timeoutMs??3e4} ms`);if(i.code!==0)throw new Error(`${e} ${t.slice(0,3).join(" ")} failed (${i.code}): ${(i.stderr||i.stdout).trim().slice(0,500)}`);return i.stdout}async function Pe(){const e=await N("ps",["-axww","-o","pid=,ppid=,lstart=,stat=,comm="],{timeoutMs:15e3});const t=await N("ps",["-axww","-o","pid=,command="],{timeoutMs:15e3});const s=new Map;for(const a of t.split("\n")){const o=/^\s*(\d+)\s+(.*)$/.exec(a);if(o)s.set(Number(o[1]),o[2])}const i=[];for(const a of e.split("\n")){const o=/^\s*(\d+)\s+(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(\S+)\s+(.*)$/.exec(a);if(!o)continue;const n=Number(o[1]);i.push({pid:n,ppid:Number(o[2]),start:o[3].replace(/\s+/g," "),stat:o[4],comm:o[5].trim(),command:s.get(n)??o[5].trim()})}return i}function st(e,t){const s=new Map;const i=new Map;for(const l of e){i.set(l.pid,l);const c=s.get(l.ppid)??[];c.push(l);s.set(l.ppid,c)}const a=[];const o=i.get(t);if(!o)return a;const n=[o];const r=new Set;while(n.length){const l=n.pop();if(r.has(l.pid))continue;r.add(l.pid);a.push(l);for(const c of s.get(l.pid)??[])n.push(c)}return a}async function pe(e){const t=await M("ps",["-o","lstart=","-p",String(e)],{timeoutMs:5e3});const s=t.stdout.trim().replace(/\s+/g," ");return t.code===0&&s?s:null}async function j(e,t,s=false){if(!Number.isInteger(e)||e<=0)return false;try{process.kill(e,0)}catch(a){if(a.code!=="EPERM")return false}if(!t)return true;const i=await pe(e);if(i===null)return s;return i===t}function it(e=process.env){return Object.fromEntries(Object.entries(e).filter(([t])=>!t.startsWith("SIMCTL_CHILD_")))}var I=e=>new Promise(t=>setTimeout(t,e));var ue=process.env.BUOY_FARM_DISABLED_ROOT||"/private/var/tmp";function Ft(e,t=ue){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return et.join(t,`com.apple.CoreSimulator.SimDevice.${e}`)}function Wt(e,t=ue){return et.join(Ft(e,t),"disabled.plist")}async function Gt(e,t=ue){const s=Wt(e,t);let i;try{i=await de.lstat(s)}catch{return{}}if(!i.isFile())throw new Error(`refusing ${s}: not a regular file`);const a=await N("plutil",["-convert","json","-o","-",s],{timeoutMs:1e4});const o=JSON.parse(a);const n={};for(const[r,l]of Object.entries(o))if(typeof l==="boolean")n[r]=l;return n}function wi(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function gi(e){const t=Object.keys(e).sort();let s='<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n';for(const i of t)s+=` <key>${wi(i)}</key>
|
|
2
|
+
<${e[i]?"true":"false"}/>
|
|
3
|
+
`;return s+"</dict>\n</plist>\n"}async function Ht(e,t,s=ue){const i=Ft(e,s);await de.mkdir(i,{recursive:true});const a=await de.lstat(i);if(!a.isDirectory())throw new Error(`refusing ${i}: not a directory`);const o=await Gt(e,s);const n=et.join(i,"buoy-farm-prior.json");let r={};try{r=JSON.parse(await de.readFile(n,"utf8"))}catch{}const l={...o};for(const c of t){if(!(c in r))r[c]=c in o?o[c]:null;l[c]=true}await qt(n,JSON.stringify(r,null,2)+"\n");await qt(Wt(e,s),gi(l))}async function yi(e,t,s=ue){const i=await Gt(e,s);return t.every(a=>i[a]===true)}async function qt(e,t){const s=`${e}.${hi(4).toString("hex")}.tmp`;const i=await de.open(s,"w",420);try{await i.writeFile(t);await i.sync()}finally{await i.close()}await de.rename(s,e)}import{promises as L,existsSync as ie,readFileSync as xo}from"fs";import B from"path";import{createHash as zt}from"crypto";var Yt=["@react_buoy_sync_device_id.json","@react_buoy_install.json"];var vi="@buoy~1floating-tool~1*";function ke(e){if(!ie(B.join(e,"seed.json")))return`No seed at ${e}. Run: pnpm e2e:pool seed`;if(ie(B.join(e,"Documents/buoy-license"))){for(const t of["buoy-gg-license.json","buoy-gg-device-fingerprint.json"]){if(!ie(B.join(e,"Documents/buoy-license",t)))return`The seed lacks ${t}.`}}return null}async function Si(e){const t=zt("sha256");async function s(i){let a;try{a=await L.readdir(i,{withFileTypes:true})}catch{return}a.sort((o,n)=>o.name.localeCompare(n.name));for(const o of a){const n=B.join(i,o.name);if(o.isDirectory())await s(n);else{const r=await L.stat(n);t.update(`${B.relative(e,n)}:${r.size}:${Math.floor(r.mtimeMs)}
|
|
4
|
+
`)}}}await s(e);return t.digest("hex").slice(0,16)}async function ki(e){const t=zt("sha256");const s=B.join(e,"Info.plist");t.update(await L.readFile(s));const i=JSON.parse(await N("plutil",["-convert","json","-o","-",s],{timeoutMs:1e4}));const a=B.join(e,String(i.CFBundleExecutable??""));const o=await L.stat(a);t.update(`${o.size}:${Math.floor(o.mtimeMs)}`);return t.digest("hex").slice(0,16)}async function bi(e){const t=JSON.parse(await N("plutil",["-convert","json","-o","-",B.join(e,"Info.plist")],{timeoutMs:1e4}));if(!t.CFBundleIdentifier)throw new Error(`${e} has no CFBundleIdentifier`);return String(t.CFBundleIdentifier)}async function Ce(e,t){const s=B.join(e,"Containers/Data/Application");let i;try{i=await L.readdir(s)}catch{return null}for(const a of i){const o=B.join(s,a,".com.apple.mobile_container_manager.metadata.plist");if(!ie(o))continue;const n=await M("plutil",["-extract","MCMMetadataIdentifier","raw","-o","-",o],{timeoutMs:5e3});if(n.code===0&&n.stdout.trim()===t)return B.join(s,a)}return null}function Mi(e,t){return B.join(e,"Library/Application Support",t,"RCTAsyncLocalStorage_V1")}async function Kt(e,t,s,i){i();await L.mkdir(t,{recursive:true});const a=["-a","--delete"];for(const o of s)a.push("--exclude",o);a.push(e.replace(/\/?$/,"/"),t.replace(/\/?$/,"/"));i();await N("rsync",a,{timeoutMs:6e4})}async function Qt(e,t,s,i=()=>{}){i();const a=ke(s);if(a)throw new Error(a);const o=[...Yt.map(l=>`react-buoy/${l}`),`react-buoy/${vi}`];if(ie(B.join(s,"Documents")))await Kt(B.join(s,"Documents"),B.join(e,"Documents"),o,i);if(ie(B.join(s,"AsyncStorage/manifest.json"))){await Kt(B.join(s,"AsyncStorage"),Mi(e,t),[],i)}const n=B.join(s,"Preferences",`${t}.plist`);if(ie(n)){i();await L.mkdir(B.join(e,"Library/Preferences"),{recursive:true});await L.copyFile(n,B.join(e,"Library/Preferences",`${t}.plist`))}const r=B.join(e,"Documents/react-buoy");try{for(const l of await L.readdir(r)){i();if(l.startsWith("@buoy~1floating-tool~1"))await L.rm(B.join(r,l),{force:true,recursive:true})}}catch{}}async function Le(e,t=()=>{}){t();const s=[];const i=B.join(e,"Documents/react-buoy");let a=[];try{a=await L.readdir(i)}catch{return s}for(const o of a){if(Yt.includes(o)||o.startsWith("@buoy~1floating-tool~1")){t();await L.rm(B.join(i,o),{force:true,recursive:true});s.push(o)}}return s}async function Vt(e){let t;try{t=await L.readFile(B.join(e,"Documents/react-buoy/@react_buoy_sync_device_id.json"),"utf8")}catch{return null}try{const s=JSON.parse(t);if(typeof s==="string")return s||null;if(typeof s?.value==="string")return s.value||null}catch{}return t.trim()||null}var Jt=2*60*6e4;var _i=10;var Ei=5;var Bi=.8;var ae=e=>Math.round(e*10)/10;var Ii=e=>{if(!e.length)return 0;const t=[...e].sort((i,a)=>i-a);const s=t.length>>1;return t.length%2?t[s]:(t[s-1]+t[s])/2};var Xt=class{constructor(e,t,s=1e3,i=Date.now){this.host=e;this.target=t;this.periodMs=s;this.now=i;this.samples=[];this.prev=null;this.pids=[];this.treeAge=0;this.treeKey="";this.sampling=false;this.ticks=0;this.lastProcs=null;this.rates={ours:0,graphics:0,gpuPct:null};this.cpuCount=0}async tick(){if(this.sampling)return;this.sampling=true;try{const e=this.target();if(!e){this.prev=null;this.treeKey="";return}const t=`${e.leaseId}:${e.udids.join(",")}`;if(t!==this.treeKey||++this.treeAge>=_i){const h=await Promise.all(e.udids.map(m=>this.host.simTree(m)));this.pids=h.flat().map(m=>m.pid);this.treeAge=0;if(t!==this.treeKey){this.prev=null;this.lastProcs=null;this.ticks=0;this.rates={ours:0,graphics:0,gpuPct:null}}this.treeKey=t}const s=this.ticks++;const i=await this.host.loadCounters(this.pids,s<2||s%Ei===0);this.cpuCount=i.cpuCount;const a=this.now();const o=this.prev;this.prev={...i,at:a,leaseId:e.leaseId};const n=h=>(i.hostTotalMs-h)/Math.max(1,i.cpuCount);if(i.pidsCpuMs!==null&&i.graphicsCpuMs!==null){const h=this.lastProcs;const m=h?n(h.hostTotalMs):0;if(h&&m>0){this.rates.ours=Math.max(0,(i.pidsCpuMs-h.pidsCpuMs)/m);this.rates.graphics=Math.max(0,(i.graphicsCpuMs-h.graphicsCpuMs)/m)}this.rates.gpuPct=i.gpuPct;this.lastProcs={hostTotalMs:i.hostTotalMs,pidsCpuMs:i.pidsCpuMs,graphicsCpuMs:i.graphicsCpuMs}}if(!o||o.leaseId!==e.leaseId)return;const r=n(o.hostTotalMs);if(r<=0)return;const l=Math.max(0,(i.hostBusyMs-o.hostBusyMs)/r);const c=this.rates.ours;const d=this.rates.graphics;this.samples.push({at:Math.round((a+o.at)/2),leaseId:e.leaseId,freeCores:ae(Math.max(0,i.cpuCount-l)),ourCores:ae(c),graphicsCores:ae(d),outsideCores:ae(Math.max(0,l-c-d)),gpuPct:this.rates.gpuPct,memLevel:i.memPressureLevel,swapMB:i.swapUsedMB,thermal:i.thermalLevel});const u=a-Jt;while(this.samples.length&&this.samples[0].at<u)this.samples.shift()}finally{this.sampling=false}}before(e,t){for(let s=this.samples.length-1;s>=0;s--){const i=this.samples[s];if(i.at<t)return i.leaseId===e?i:void 0}return void 0}latest(){return this.samples[this.samples.length-1]}verdicts(e,t,s){const i=this.periodMs/2;const a=t.map(n=>{const r=this.samples.filter(c=>c.at>=n.from-i&&c.at<=n.to+i);const l=r.filter(c=>c.leaseId===e);return{w:n,all:r,mine:l}});const o=Ii(a.filter(n=>n.mine.length).map(n=>at(n.mine.map(r=>r.outsideCores))));return a.map(({w:n,all:r,mine:l})=>{const c=Math.max(1,Math.round((n.to-n.from)/this.periodMs));const d={id:n.id,fair:false,seconds:l.length};if(!r.length&&(n.from>this.now()+6e4||n.to<this.now()-Jt)){return{...d,reason:"This run's time doesn't match the Mac's clock. Was the app's clock moved? Put it back and run again."}}if(!l.length||l.length<r.length||l.length<Math.floor(c*Bi)){return{...d,reason:"No load record for all of this run. Hold perf-slot for the whole test."}}const u=l.map(w=>w.outsideCores);const h=l.map(w=>w.gpuPct).filter(w=>w!==null);const m=l.map(w=>w.thermal).filter(w=>w!==null);const g={...d,freeCoresMin:Math.min(...l.map(w=>w.freeCores)),outsideCoresAvg:ae(at(u)),outsideCoresMax:Math.max(...u),ourCoresAvg:ae(at(l.map(w=>w.ourCores))),graphicsCoresMax:Math.max(...l.map(w=>w.graphicsCores)),gpuMaxPct:h.length?Math.max(...h):null,swapGrowthMB:Math.round(Math.max(0,l[l.length-1].swapMB-(this.before(e,n.from)?.swapMB??l[0].swapMB))),thermalMax:m.length?Math.max(...m):null};if(g.thermalMax!=null&&g.thermalMax>s.maxThermalLevel){return{...g,reason:"The Mac got hot and slowed itself down. Let it cool, then run again."}}if(l.some(w=>w.memLevel>=4)){return{...g,reason:"The Mac ran low on memory. Close apps or stop sims you don't need."}}if(g.swapGrowthMB>s.maxSwapGrowthMB){return{...g,reason:`The Mac swapped ${g.swapGrowthMB} MB of memory to disk.`}}if(g.freeCoresMin<s.minFreeCores){return{...g,reason:`The Mac ran out of spare CPU (${g.freeCoresMin} cores free, needs ${s.minFreeCores}).`}}const S=ae(g.outsideCoresMax-o);if(S>s.maxSwingCores){return{...g,reason:`Other work on the Mac jumped by ${S} cores during this run.`}}return{...g,fair:true}})}};function at(e){return e.length?e.reduce((t,s)=>t+s,0)/e.length:0}function Zt(e){const t=e.trim().split(":").map(Number);if(!t.length||t.some(s=>!Number.isFinite(s)))return 0;return Math.round(t.reduce((s,i)=>s*60+i,0)*1e3)}import{spawn as es}from"child_process";import{randomBytes as $i}from"crypto";import{existsSync as ot,mkdirSync as Oi,openSync as Ai,closeSync as Di,promises as oe,writeFileSync as ts}from"fs";import Ti from"net";import P from"path";import{createHash as Ri}from"crypto";import{createReadStream as Ni,createWriteStream as Pi,existsSync as be,promises as Q,readFileSync as Ci}from"fs";import nt from"os";import D from"path";import{Readable as Li}from"stream";import{pipeline as ss}from"stream/promises";var Me="https://dl.google.com/android/repository/";var xi="channel-0";var J={api:36,tag:"aosp_atd",abi:Ui()};function Ui(){return nt.arch()==="arm64"?"arm64-v8a":"x86_64"}function xe(e){return`system-images/android-${e.api}/${e.tag}/${e.abi}/`}function is(e){return D.join(e,"sdk")}function rt(e=process.env){const t=nt.homedir();const s=[e.ANDROID_HOME,e.ANDROID_SDK_ROOT,D.join(t,"Library/Android/sdk"),D.join(t,"Android/Sdk")];return[...new Set(s.filter(i=>!!i))]}function as(e){try{const t=/Pkg\.Revision=([\d.]+)/.exec(Ci(D.join(e,"emulator/source.properties"),"utf8"));return t?.[1]??null}catch{return null}}function lt(e,t){const s=e.split(".").map(Number);const i=t.split(".").map(Number);for(let a=0;a<Math.max(s.length,i.length);a++){const o=(s[a]??0)-(i[a]??0);if(o)return o}return 0}var os="35.4.9";function _e(e,t,s=J){const i=D.join(e,"emulator",process.platform==="win32"?"emulator.exe":"emulator");const a=D.join(e,"platform-tools",process.platform==="win32"?"adb.exe":"adb");const o=as(e);if(!be(i)||!be(a)||!o)return null;return{root:e,source:t,emulator:i,emulatorVersion:o,adb:a,hasImage:be(D.join(e,xe(s),"system.img"))}}function Ee(e,t=J,s="auto"){const i=_e(is(e),"buoy",t);if(s!=="system"&&i?.hasImage&<(i.emulatorVersion,os)>=0)return i;if(s==="buoy")return i;for(const a of rt()){const o=_e(a,"system",t);if(o?.hasImage&<(o.emulatorVersion,os)>=0)return o}return s==="system"?null:i}function ji(e){return[...e.matchAll(/<remotePackage path="([^"]+)">([\s\S]*?)<\/remotePackage>/g)].map(t=>({path:t[1],body:t[2]}))}function Fi(e){const t=/<revision>([\s\S]*?)<\/revision>/.exec(e)?.[1]??"";return["major","minor","micro"].map(s=>new RegExp(`<${s}>(\\d+)</${s}>`).exec(t)?.[1]).filter(Boolean).join(".")}function Wi(){return process.platform==="darwin"?"macosx":process.platform==="win32"?"windows":"linux"}function Gi(){return nt.arch()==="arm64"?"aarch64":"x64"}function ct(e,t,s,i,a={}){const o=a.os??Wi();const n=a.arch??Gi();let r=null;for(const l of ji(e)){if(l.path!==t)continue;if(!l.body.includes(`<channelRef ref="${xi}"/>`))continue;const c=Fi(l.body);const d=/<uses-license ref="([^"]+)"/.exec(l.body)?.[1]??"";for(const u of l.body.matchAll(/<archive>([\s\S]*?)<\/archive>/g)){const h=u[1];const m=/<host-os>([^<]+)<\/host-os>/.exec(h)?.[1];const g=/<host-arch>([^<]+)<\/host-arch>/.exec(h)?.[1];if(m&&m!==o)continue;if(g&&g!==n)continue;const S=/<url>([^<]+)<\/url>/.exec(h)?.[1];const w=/<checksum type="sha1">([0-9a-f]+)<\/checksum>/.exec(h)?.[1];const v=Number(/<size>(\d+)<\/size>/.exec(h)?.[1]??0);if(!S||!w)continue;const _=/<display-name>([^<]+)<\/display-name>/.exec(l.body)?.[1]??t;if(!r||lt(c,r.rev)>0){r={rev:c,a:{name:`${_} ${c}`,url:s+S,sha1:w,size:v,license:d,dest:i}}}}}return r?.a??null}function ns(e,t){const s=new RegExp(`<license id="${t}" type="text">([\\s\\S]*?)</license>`).exec(e);return s?s[1].replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&").trim():null}async function rs(e){const t=await fetch(e);if(!t.ok)throw new Error(`GET ${e} failed: ${t.status}`);return t.text()}async function Hi(e=J){const t=await rs(`${Me}repository2-3.xml`);const s=`${Me}sys-img/${e.tag}/`;const i=await rs(`${s}sys-img2-3.xml`);const a=ct(t,"emulator",Me,"emulator");const o=ct(t,"platform-tools",Me,"platform-tools");const n=ct(i,`system-images;android-${e.api};${e.tag};${e.abi}`,s,xe(e));const r=[!a&&"emulator",!o&&"platform-tools",!n&&`${e.tag} image for Android ${e.api}`].filter(Boolean);if(r.length)throw new Error(`Google's list has no stable ${r.join(", ")} for this computer.`);const l=[a,o,n];const c={};for(const d of l){if(c[d.license])continue;c[d.license]=ns(t,d.license)??ns(i,d.license)??`(See ${Me} for license ${d.license}.)`}return{archives:l,licenses:c}}async function ls(e){const t=Ri("sha1");await ss(Ni(e),t);return t.digest("hex")}async function qi(e,t,s=()=>{}){if(process.platform==="win32")throw new Error("The Android farm can't set up its own SDK on Windows yet.");const i=is(e);const a=D.join(e,"download");await Q.mkdir(a,{recursive:true});await Promise.all(t.archives.map(async n=>{const r=D.join(i,n.dest);if(n.dest==="emulator"?as(i):be(D.join(r,n.dest==="platform-tools"?"adb":"system.img"))){s(`${n.name}: already here.`);return}const l=D.join(a,D.basename(n.url));if(!be(l)||await ls(l)!==n.sha1){s(`Downloading ${n.name} (${Math.round(n.size/1024**2)} MB)\u2026`);const u=await fetch(n.url);if(!u.ok||!u.body)throw new Error(`GET ${n.url} failed: ${u.status}`);await ss(Li.fromWeb(u.body),Pi(l));const h=await ls(l);if(h!==n.sha1)throw new Error(`${n.name} failed its checksum (${h}).`)}const c=D.join(a,`unzip-${D.basename(n.url)}`);await Q.rm(c,{recursive:true,force:true});await Q.mkdir(c,{recursive:true});await N("unzip",["-q",l,"-d",c],{timeoutMs:15*6e4});const[d]=await Q.readdir(c);await Q.mkdir(D.dirname(r),{recursive:true});await Q.rm(r,{recursive:true,force:true});await Q.rename(D.join(c,d),r);await Q.rm(c,{recursive:true,force:true});await Q.rm(l,{force:true});s(`${n.name}: ready.`)}));await Q.rm(a,{recursive:true,force:true});const o=_e(i,"buoy");if(!o)throw new Error(`The SDK at ${i} is not complete after the download.`);return o}import{existsSync as zi,promises as dt,readFileSync as Yi}from"fs";import Ki from"path";var pt="android-3gb";var cs={"android-3gb":{name:"Low-end Android (3 GB)",ramMB:3072,cores:4,width:720,height:1600,density:320,heapGrowthMB:192,heapMB:512,source:"built in"},"pixel-4a":{name:"Pixel 4a",ramMB:6144,cores:8,width:1080,height:2340,density:440,heapGrowthMB:256,heapMB:512,source:"built in"}};function ds(e){return Ki.join(e,"phones.json")}function ut(e){const t={...cs};const s=ds(e);if(zi(s)){try{Object.assign(t,JSON.parse(Yi(s,"utf8")))}catch{}}return t}async function Qi(e,t,s){const i=ds(e);let a={};try{a=JSON.parse(await dt.readFile(i,"utf8"))}catch{}a[t]=s;await dt.mkdir(e,{recursive:true});await dt.writeFile(i,JSON.stringify(a,null,2)+"\n")}var Vi=[1024,1536,2048,3072,4096,6144,8192,12288,16384,24576];function Ji(e,t){const s=u=>new RegExp(`\\[${u.replace(/\./g,"\\.")}\\]: \\[([^\\]]*)\\]`).exec(e.props)?.[1]??"";const i=Number(/MemTotal:\s+(\d+)/.exec(e.meminfo)?.[1]??0);const a=i?Vi.find(u=>u>=i/1024)??Math.ceil(i/1024/1024)*1024:0;const o=/Override size: (\d+)x(\d+)/.exec(e.wmSize)??/Physical size: (\d+)x(\d+)/.exec(e.wmSize);const n=/Override density: (\d+)/.exec(e.wmDensity)??/Physical density: (\d+)/.exec(e.wmDensity);const r=u=>Number(/^(\d+)m$/i.exec(u)?.[1]??0);const l=s("ro.product.model")||"Android phone";const c={name:l,ramMB:a||3072,cores:Number(e.nproc.trim())||4,width:o?Number(o[1]):1080,height:o?Number(o[2]):2400,density:n?Number(n[1]):420,heapGrowthMB:r(s("dalvik.vm.heapgrowthlimit"))||256,heapMB:r(s("dalvik.vm.heapsize"))||512,source:t};const d=l.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"android-phone";return{id:d,phone:c}}import{promises as tn,readFileSync as Xi}from"fs";import ht from"os";import mt from"path";import{promises as Zi,existsSync as ft,mkdirSync as wt,readFileSync as ne,renameSync as gt,writeFileSync as Ue,rmSync as Be,openSync as ea,fsyncSync as ta,closeSync as sa,chmodSync as ia,readdirSync as aa,statSync as ps}from"fs";import oa from"os";import T from"path";import{createHash as na,randomBytes as Ie}from"crypto";function F(){return process.env.BUOY_FARM_HOME||T.join(oa.homedir(),".buoy","farm")}var W=(e=F())=>({home:e,lock:T.join(e,"owner.lock"),lockOwner:T.join(e,"owner.lock","owner.json"),socket:T.join(e,"farmd.sock"),token:T.join(e,"token"),state:T.join(e,"state.json"),journal:T.join(e,"journal.ndjson"),log:T.join(e,"farmd.log"),shots:T.join(e,"shots")});function yt(e=F()){wt(e,{recursive:true,mode:448});try{ia(e,448)}catch{}}function us(e=F()){const t=W(e).token;try{const s=ne(t,"utf8").trim();if(s)return s}catch{}yt(e);try{Ue(t,Ie(24).toString("hex")+"\n",{mode:384,flag:"wx"})}catch(s){if(s.code!=="EEXIST")throw s}for(let s=0;s<20;s++){const i=ne(t,"utf8").trim();if(i)return i;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,25)}throw new Error(`farm token at ${t} is empty`)}async function $e(e=F()){try{return JSON.parse(await Zi.readFile(W(e).lockOwner,"utf8"))}catch{return null}}async function Oe(e){return!!e&&await j(e.pid,e.start,true)}async function ra(e,t){const s=T.join(e,"owner.claim");for(let i=0;i<20;i++){try{wt(s,{mode:448});Ue(T.join(s,"pid"),`${process.pid}
|
|
5
|
+
${await pe(process.pid)??""}`);try{return await t()}finally{Be(s,{recursive:true,force:true})}}catch(a){if(a.code!=="EEXIST")throw a;const o=ps(s,{throwIfNoEntry:false});if(o&&Date.now()-o.mtimeMs>3e4){let n=true;try{const[r,l]=ne(T.join(s,"pid"),"utf8").split("\n");n=!await j(Number(r),l||void 0)}catch{}if(n){try{gt(s,T.join(e,`owner.claim-dead-${Ie(4).toString("hex")}`))}catch{}}}await new Promise(n=>setTimeout(n,100))}}return null}async function la(e,t,s=F()){yt(s);const i=W(s);const a=await pe(process.pid)??"";for(let o=0;o<3;o++){try{wt(i.lock,{mode:448})}catch(r){if(r.code!=="EEXIST")throw r;const l=await $e(s);if(l&&await Oe(l))return null;if(!l){await new Promise(u=>setTimeout(u,300));const d=await $e(s);if(d&&await Oe(d))return null}const c=await ra(s,async()=>{const d=await $e(s);if(d&&await Oe(d))return false;if(!ft(i.lock))return true;gt(i.lock,T.join(s,`owner.stale-${Ie(4).toString("hex")}`));return true});if(c===false)return null;continue}const n={pid:process.pid,start:a,version:e,host:t,since:new Date().toISOString()};Ue(i.lockOwner,JSON.stringify(n),{mode:384});ca(s);return{info:n,release(){try{const r=JSON.parse(ne(i.lockOwner,"utf8"));if(r.pid!==process.pid)return}catch{return}try{Be(i.socket,{force:true})}catch{}Be(i.lock,{recursive:true,force:true})}}}return null}function ca(e){try{for(const t of aa(e)){if(t.startsWith("owner.stale-")||t.startsWith("owner.claim-dead-"))Be(T.join(e,t),{recursive:true,force:true})}}catch{}}function hs(){return{version:Ne,seq:0,farmId:Ie(6).toString("hex"),nextIndex:1,templates:{},sims:{},orphans:{},leases:{},runs:{},locks:{},requests:{},events:[],settings:{...Xe}}}var ms=e=>na("sha256").update(JSON.stringify(e)).digest("hex").slice(0,12);function fs(e,t,s){const i=ea(e,s,384);try{Ue(i,t);ta(i)}finally{sa(i)}}var ws=class{constructor(e=F()){this.fenced=false;this.pending=[];this.open=new Map;this.p=W(e);yt(e);this.state=this.load()}get shotsPath(){return this.p.shots}get homeDir(){return this.p.home}fence(){this.fenced=true}assertActive(){if(this.fenced)throw new Error("This farm has stopped.")}load(){let e=hs();if(ft(this.p.state)){const a=JSON.parse(ne(this.p.state,"utf8"));if(a.version>Ne){throw new Error(`farm state is version ${a.version}; this farmd understands ${Ne}. Update Buoy.`)}e={...hs(),...a,settings:{...Xe,...a.settings}}}delete e.settings.activeSlots;const t=new Map;const s=new Set;let i=e.seq;if(ft(this.p.journal)){for(const a of ne(this.p.journal,"utf8").split("\n")){if(!a.trim())continue;let o;try{o=JSON.parse(a)}catch{continue}const{sum:n,...r}=o;if(ms(r)!==n||o.seq<=e.seq)continue;i=Math.max(i,o.seq);if(o.type==="intent")t.set(o.seq,o);else{t.delete(o.seq);s.add(o.seq)}}}for(const a of e.openIntents??[]){if(!s.has(a.seq)&&!t.has(a.seq))t.set(a.seq,{...a,type:"intent",sum:""})}this.pending=[...t.values()].sort((a,o)=>a.seq-o.seq);for(const a of this.pending)this.open.set(a.seq,a);e.seq=i;e.openIntents=[];return e}intent(e,t){this.assertActive();const s=++this.state.seq;const i={seq:s,at:new Date().toISOString(),type:"intent",op:e,data:t};this.append(i);this.open.set(s,{...i,sum:""});return s}result(e,t,s){this.assertActive();this.open.delete(e);this.pending=this.pending.filter(i=>i.seq!==e);this.save();this.append({seq:e,at:new Date().toISOString(),type:"result",op:t,data:s})}append(e){this.assertActive();fs(this.p.journal,JSON.stringify({...e,sum:ms(e)})+"\n","a")}save(){this.assertActive();const e=`${this.p.state}.${Ie(4).toString("hex")}.tmp`;this.state.openIntents=[...this.open.values()].map(({seq:t,at:s,op:i,data:a})=>({seq:t,at:s,op:i,data:a}));fs(e,JSON.stringify(this.state,null,1),"w");gt(e,this.p.state);try{if(ps(this.p.journal).size>2*1024*1024)Be(this.p.journal,{force:true})}catch{}}event(e){this.assertActive();this.state.events.push({at:new Date().toISOString(),...e});if(this.state.events.length>300)this.state.events.splice(0,this.state.events.length-300)}};function da(e=F()){try{const t=JSON.parse(ne(W(e).state,"utf8"));return new Set([...Object.keys(t.sims??{}),...Object.keys(t.orphans??{}),...Object.values(t.templates??{}).map(s=>s.udid)])}catch{return new Set}}var ee=Number(process.env.BUOY_FARM_ADB_PORT)||5137;var pa=1.3;function Ae(e=F()){return mt.join(e,"android")}function gs(e){const t=s=>{const i=s.exec(e);return i?Number(i[1]):null};return{frames:t(/Total frames rendered: (\d+)/)??0,jankyPct:t(/Janky frames: \d+ \(([\d.]+)%\)/),p50:t(/50th percentile: (\d+)ms/),p90:t(/90th percentile: (\d+)ms/),p99:t(/99th percentile: (\d+)ms/)}}function ua(e){if(!e.length)return null;const t=[...e].sort((i,a)=>i-a);const s=Math.floor(t.length/2);return t.length%2?t[s]:Math.round((t[s-1]+t[s])/2)}function ys(e){if(/Successfully loaded snapshot|Loading snapshot '?default_boot'? .*(done|succeeded)|snapshot.*loaded/i.test(e))return"snapshot";if(/Cold boot|failed to load snapshot|Snapshot load failed|could not load snapshot/i.test(e))return"cold";return"unknown"}async function ha(){const e=ht.totalmem()/1024**3;let t=ht.freemem()/1024**3;if(process.platform==="darwin"){const a=await M("vm_stat",[],{timeoutMs:1e4});const o=Number(/page size of (\d+)/.exec(a.stdout)?.[1]??16384);const n=r=>Number(new RegExp(`${r}:\\s+(\\d+)`).exec(a.stdout)?.[1]??0);t=(n("Pages free")+n("Pages inactive")+n("Pages speculative")+n("Pages purgeable"))*o/1024**3}const s=await M("df",["-k",ht.homedir()],{timeoutMs:1e4});const i=Number((s.stdout.trim().split("\n").pop()??"").split(/\s+/)[3]??0);return{availableGB:t,totalGB:e,freeDiskGB:i/1024**2}}async function ma(e){if(process.platform==="darwin"){const i=await M("footprint",[String(e)],{timeoutMs:3e4});const a=/Footprint: ([\d.]+) (MB|GB|KB)/.exec(i.stdout);if(a)return Math.round(Number(a[1])*(a[2]==="GB"?1024:a[2]==="KB"?1/1024:1))}const t=await M("ps",["-o","rss=","-p",String(e)],{timeoutMs:5e3});const s=Number(t.stdout.trim());return s?Math.round(s/1024):null}async function vs(e){const t=await M("ps",["-o","time=","-p",String(e)],{timeoutMs:5e3});const s=/(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)/.exec(t.stdout.trim());if(!s)return null;const[,i,a,o,n]=s;return((Number(i??0)*24+Number(a??0))*60+Number(o))*6e4+Number(n)*1e3}function vt(e,t){return Ss(t).get(e)??null}function Ss(e){const t=new Map;const s=n=>{try{return JSON.parse(Xi(n,"utf8"))}catch{return null}};const i=(n,r)=>{for(const l of Object.values(r??{})){const c=n?.[l.templateId];if(!c?.sdkRoot||!fa(l.pid)||t.has(l.serial))continue;t.set(l.serial,{adb:mt.join(c.sdkRoot,"platform-tools","adb"),port:ee})}};const a=s(W(e??F()).state);i(a?.androidTemplates,a?.androidCopies);const o=s(mt.join(Ae(e),"state.json"));if(!a?.androidImported)i(o?.templates,o?.copies);return t}function fa(e){try{process.kill(e,0);return true}catch(t){return t.code==="EPERM"}}var ks=5800;var bs=5998;var Ms="BuoyFarm_";var wa="sync_device_id|react_buoy_install";var ga=["files/react-buoy","shared_prefs","app_webview","databases","app_flutter"];var _s=Number(process.env.BUOY_BROKER_PORT)||42831;var Es=class{constructor(e={}){this.dir=Ae(e.home);this.avdHome=P.join(this.dir,"avd");this.adbPort=e.adbPort??ee;this.sdkPref=e.sdk??"auto"}sdkFor(e=J){const t=Ee(this.dir,e,this.sdkPref);return t?.hasImage?t:null}sdk(e=J){const t=Ee(this.dir,e,this.sdkPref);if(!t)throw new Error("No Android SDK with an emulator and the slim image yet. Run: npx @buoy-gg/mcp farm android setup");if(!t.hasImage)throw new Error(`The SDK at ${t.root} has no ${xe(e)} image. Run: npx @buoy-gg/mcp farm android setup`);return t}sdkOf(e){const t=this.sdk(e.image);if(t.root!==e.sdkRoot){const s=Ee(this.dir,e.image,t.source==="buoy"?"system":"buoy");if(s?.root===e.sdkRoot)return s}return t}env(e){return{...process.env,ANDROID_HOME:e.root,ANDROID_SDK_ROOT:e.root,ANDROID_AVD_HOME:this.avdHome,ANDROID_ADB_SERVER_PORT:String(this.adbPort)}}run(e,t,s=3e4){return M(e.adb,t,{timeoutMs:s,env:this.env(e)})}adb(e,t,s=3e4){return this.run(this.sdkOf(e),t,s)}async shell(e,t,s,i=3e4){const a=await this.run(e,["-s",t,"shell",s],i);if(a.timedOut)throw new Error(`adb shell on ${t} timed out: ${s}`);if(a.code!==0)throw new Error(`adb shell on ${t} failed: ${s}: ${(a.stderr||a.stdout).trim().slice(0,300)}`);return a.stdout}stale(e){if(!e.usable)return e.note??"not ready";let t;try{t=this.sdkOf(e)}catch(s){return s.message}if(t.root!==e.sdkRoot||t.emulatorVersion!==e.emulatorVersion){return`It was saved with emulator ${e.emulatorVersion}; this SDK has ${t.emulatorVersion}. Snapshots don't load across versions.`}if(!ot(P.join(this.avdHome,`${e.avd}.avd`,"snapshots","default_boot","ram.bin")))return"Its snapshot is gone.";return null}async freePort(e){const t=s=>new Promise(i=>{const a=Ti.createServer();a.once("error",()=>i(false));a.listen(s,"127.0.0.1",()=>a.close(()=>i(true)))});for(let s=ks;s<=bs;s+=2){if(e.has(s))continue;if(await t(s)&&await t(s+1))return s}throw new Error(`No free emulator port from ${ks} to ${bs}.`)}async launch(e,t,s,i,a){const o=P.join(this.dir,"logs");await oe.mkdir(o,{recursive:true});const n=P.join(o,a);const r=Ai(n,"w");const l=["-avd",t,"-port",String(s),"-gpu","host","-no-audio","-no-boot-anim","-camera-back","none","-camera-front","none",...i];const c=es(e.emulator,l,{detached:true,stdio:["ignore",r,r],env:this.env(e)});Di(r);c.unref();if(!c.pid)throw new Error("The emulator did not start.");const d=await pe(c.pid)??"";return{pid:c.pid,pidStart:d,log:n}}async waitBooted(e,t,s,i){const a=Date.now()+i;while(Date.now()<a){if(!await j(s))throw new Error(`The emulator for ${t} stopped while booting.`);const o=await this.run(e,["-s",t,"shell","getprop","sys.boot_completed"],5e3);if(o.code===0&&o.stdout.trim()==="1")return;await I(250)}throw new Error(`${t} did not finish booting in ${Math.round(i/1e3)} s.`)}async stopEmulator(e,t,s,i){if(e)await this.run(e,["-s",t,"emu","kill"],1e4);for(let a=0;a<300;a++){if(!await j(s,i))return;await I(100)}if(await j(s,i)){try{process.kill(s,"SIGKILL")}catch{}}}async startCopy(e,t,s={}){const i=this.sdkOf(e);await this.run(i,["start-server"],2e4);const a=`emulator-${t}`;const o=["-read-only","-no-snapshot-save",...s.window?[]:["-no-window"]];const n=await this.launch(i,e.avd,t,o,`${a}.log`);return{serial:a,port:t,...n}}async readyCopy(e,t,s={}){const i=this.sdkOf(e);await this.waitBooted(i,t.serial,t.pid,s.timeoutMs??5*6e4);let a="unknown";try{a=ys(await oe.readFile(t.log,"utf8"))??"unknown"}catch{}await this.shell(i,t.serial,"setprop debug.hwui.drawing_enabled 1; logcat -c");for(const o of[`${_s}`,...e.reverse??[],...s.reverse??[]]){const[n,r]=o.split(":");const l=await this.run(i,["-s",t.serial,"reverse",`tcp:${n}`,`tcp:${r??n}`],1e4);if(l.code!==0)throw new Error(`Could not link port ${n} on ${t.serial}: ${(l.stderr||l.stdout).trim().slice(0,200)}`)}return{bootedFrom:a}}async stopCopy(e,t){let s=null;try{s=e?this.sdkOf(e):this.sdkFor(J)}catch{}if(await j(t.pid,t.pidStart))await this.stopEmulator(s,t.serial,t.pid,t.pidStart)}alive(e,t){return j(e,t)}async treePids(e){return st(await Pe(),e).map(t=>t.pid)}async findEmulators(){const e=[];for(const t of await Pe()){const s=/-avd (\S+)/.exec(t.command)?.[1];const i=Number(/-port (\d+)/.exec(t.command)?.[1]);if(!s?.startsWith(Ms)||!i)continue;if(e.some(a=>a.port===i))continue;e.push({pid:t.pid,pidStart:t.start,port:i,avd:s})}return e}async screenshot(e,t,s,i=2e4){const a=this.sdkOf(e);const o=await new Promise(n=>{const r=es(a.adb,["-s",t,"exec-out","screencap","-p"],{env:this.env(a)});const l=[];r.stdout.on("data",d=>l.push(d));r.on("error",()=>n({code:-1,buf:Buffer.alloc(0)}));const c=setTimeout(()=>r.kill("SIGKILL"),i);r.on("close",d=>{clearTimeout(c);n({code:d,buf:Buffer.concat(l)})})});if(o.code!==0||o.buf.length<100)return false;await oe.mkdir(P.dirname(P.resolve(s)),{recursive:true});await oe.writeFile(s,o.buf);return true}async launchApp(e,t,s){const i=this.sdkOf(e);await this.shell(i,t,`am force-stop ${e.packageName}`);const a=s??e.launchUrl;const o=a?`am start -W -a android.intent.action.VIEW -d ${Bs(a)} ${e.packageName}`:`am start -W -n ${e.activity}`;const n=await this.shell(i,t,o,12e4);const r=/TotalTime: (\d+)/.exec(n);return{totalMs:r?Number(r[1]):null,output:n.trim()}}async openUrl(e,t,s){await this.shell(this.sdkOf(e),t,`am start -a android.intent.action.VIEW -d ${Bs(s)} ${e.packageName}`,6e4)}async install(e,t,s){const i=this.sdkOf(e);const a=await this.run(i,["-s",t,"install","-r","-g",P.resolve(s)],10*6e4);if(a.code!==0||!/Success/.test(a.stdout))throw new Error(`${t}: install failed: ${(a.stderr||a.stdout).trim().slice(0,300)}`);await this.shell(i,t,`cmd package compile -m speed -f ${e.packageName}`,10*6e4)}async installId(e,t){const s=await this.run(this.sdkOf(e),["-s",t,"shell",`run-as ${e.packageName} cat files/react-buoy/@react_buoy_sync_device_id.json`],5e3);const i=s.code===0?s.stdout.trim().replace(/^"|"$/g,""):"";return/^[\w-]{4,64}$/.test(i)?i:null}async appMemory(e,t){const s=this.sdkOf(e);const i=Number((await this.shell(s,t,`pidof ${e.packageName}; true`)).trim().split(/\s+/)[0])||null;if(!i)return{pid:null,pssMB:null};const a=await this.shell(s,t,`dumpsys meminfo ${i}`,2e4).catch(()=>"");const o=Number(/TOTAL PSS:\s+(\d+)/.exec(a)?.[1]??/TOTAL\s+(\d+)/.exec(a)?.[1]??0);return{pid:i,pssMB:o?Math.round(o/1024):null}}async memoryKill(e,t,s){const i=await this.shell(this.sdkOf(e),t,"logcat -d -b events -s am_kill:I lowmemorykiller:I; true",15e3).catch(()=>"");for(const a of i.split("\n")){const o=/am_kill\s*:\s*\[\d+,(\d+),([^,]+),-?\d+,([^\]]*)\]/.exec(a);if(o&&Number(o[1])===s&&o[2].trim()===e.packageName&&/low ?mem|lmk|oom|memory/i.test(o[3]))return o[3].trim()}return null}writeAvd(e,t,s){const i=P.join(this.avdHome,`${e}.avd`);Oi(i,{recursive:true});const a={AvdId:e,"avd.ini.displayname":e.replace(/_/g," "),"avd.ini.encoding":"UTF-8","abi.type":s.abi,"hw.cpu.arch":s.abi==="arm64-v8a"?"arm64":"x86_64","image.sysdir.1":xe(s),"tag.id":s.tag,"tag.display":s.tag==="aosp_atd"?"AOSP ATD":"Google ATD",target:`android-${s.api}`,"hw.ramSize":t.ramMB,"hw.cpu.ncore":t.cores,"hw.lcd.width":t.width,"hw.lcd.height":t.height,"hw.lcd.density":t.density,"disk.dataPartition.size":"6G","hw.gpu.enabled":"yes","hw.gpu.mode":"host","hw.keyboard":"yes","hw.audioInput":"no","hw.audioOutput":"no","hw.camera.back":"none","hw.camera.front":"none","fastboot.forceColdBoot":"no","fastboot.forceFastBoot":"yes",showDeviceFrame:"no"};ts(P.join(i,"config.ini"),Object.entries(a).map(([o,n])=>`${o}=${n}`).join("\n")+"\n");ts(P.join(this.avdHome,`${e}.ini`),`avd.ini.encoding=UTF-8
|
|
6
|
+
path=${i}
|
|
7
|
+
path.rel=avd/${e}.avd
|
|
8
|
+
target=android-${s.api}
|
|
9
|
+
`);return i}async buildTemplate(e,t){const s=P.resolve(e.apk);if(!ot(s)||!s.endsWith(".apk"))throw new Error(`No APK at ${s}.`);const i=e.image??J;const a=this.sdk(i);const o=e.phoneId??pt;const n=ut(this.dir)[o];if(!n)throw new Error(`No phone "${o}". Try: farm android phone list`);const r=$i(3).toString("hex");const l=`${Ms}${r}`;const c=await oe.stat(s);const d={id:r,avd:l,apk:s,apkSize:c.size,apkMtimeMs:c.mtimeMs,packageName:"",activity:"",phoneId:o,phone:n,image:i,sdkRoot:a.root,emulatorVersion:a.emulatorVersion,createdAt:new Date().toISOString(),usable:false,note:"Being built.",...e.reverse?.length?{reverse:e.reverse}:{},...e.launchUrl?{launchUrl:e.launchUrl}:{}};const u=this.writeAvd(l,n,i);await this.run(a,["start-server"],2e4);const h=await this.freePort(new Set((await this.findEmulators()).map(w=>w.port)));const m=`emulator-${h}`;const g=Date.now();t(`Template ${r}: first boot of ${n.name} (${i.tag}, Android ${i.api})\u2026`);const S=await this.launch(a,l,h,["-no-window","-no-snapshot-load","-no-snapshot-save"],`template-${r}.log`);try{await this.waitBooted(a,m,S.pid,e.bootTimeoutMs??10*6e4);t(`Booted in ${((Date.now()-g)/1e3).toFixed(1)} s. Setting up\u2026`);const w=(y,b)=>this.shell(a,m,y,b);await w("setprop debug.hwui.drawing_enabled 1");await this.setHeap(a,m,n,t);await w("settings put global package_verifier_enable 0; settings put system screen_off_timeout 2147483647; svc power stayon true; input keyevent 82");const v=new Set((await w("pm list packages -3")).split("\n").map(y=>y.trim()).filter(Boolean));t("Installing the app\u2026");const _=await this.run(a,["-s",m,"install","-r","-g",s],10*6e4);if(_.code!==0||!/Success/.test(_.stdout))throw new Error(`Install failed: ${(_.stderr||_.stdout).trim().slice(0,400)}`);const z=(await w("pm list packages -3")).split("\n").map(y=>y.trim()).filter(Boolean);const te=z.filter(y=>!v.has(y)).map(y=>y.replace(/^package:/,""));if(te.length!==1)throw new Error(`Could not tell which package the APK installed (${te.join(", ")||"none"}).`);d.packageName=te[0];d.activity=(await w(`cmd package resolve-activity --brief -c android.intent.category.LAUNCHER ${d.packageName}`)).trim().split("\n").pop().trim();if(!d.activity.includes("/"))throw new Error(`${d.packageName} has no launcher screen.`);t("Compiling the app ahead of time, so every copy times the same code\u2026");await w(`cmd package compile -m speed -f ${d.packageName}`,10*6e4);for(const y of[`${_s}`,...e.reverse??[]]){const[b,E]=y.split(":");await this.run(a,["-s",m,"reverse",`tcp:${b}`,`tcp:${E??b}`],1e4)}await w(`am start -W -n ${d.activity}`,12e4);await I(8e3);await w(`am force-stop ${d.packageName}`);const k=await this.wipeIdentity(a,m,d.packageName);if(k.length)t(`Removed the app's Buoy id so each copy makes its own (${k.length} file${k.length===1?"":"s"}).`);t("Saving the snapshot\u2026");const f=await this.run(a,["-s",m,"emu","avd","snapshot","save","default_boot"],5*6e4);if(f.code!==0||/KO/.test(f.stdout))throw new Error(`Snapshot save failed: ${(f.stdout+f.stderr).trim().slice(0,300)}`)}catch(w){await this.stopEmulator(a,m,S.pid,S.pidStart);await this.removeTemplateFiles(d).catch(()=>{});throw w}await this.stopEmulator(a,m,S.pid,S.pidStart);if(!ot(P.join(u,"snapshots","default_boot","ram.bin"))){await this.removeTemplateFiles(d).catch(()=>{});throw new Error("No snapshot was saved.")}d.usable=true;d.note=void 0;return d}async removeTemplateFiles(e){await oe.rm(P.join(this.avdHome,`${e.avd}.avd`),{recursive:true,force:true});await oe.rm(P.join(this.avdHome,`${e.avd}.ini`),{force:true})}async setHeap(e,t,s,i){const a=(l,c)=>this.shell(e,t,l,c);const o=(await a("getprop dalvik.vm.heapgrowthlimit; getprop dalvik.vm.heapsize")).split("\n").map(l=>l.trim());if(o[0]===`${s.heapGrowthMB}m`&&o[1]===`${s.heapMB}m`)return;const n=await this.run(e,["-s",t,"root"],3e4);if(n.code!==0||/cannot run as root/i.test(n.stdout+n.stderr)){i(`Could not set the heap limit on this image (${(n.stdout+n.stderr).trim()}). Apps keep ${o[0]}.`);return}await I(1500);await this.run(e,["-s",t,"wait-for-device"],6e4);await a(`setprop dalvik.vm.heapgrowthlimit ${s.heapGrowthMB}m && setprop dalvik.vm.heapsize ${s.heapMB}m && stop && start`);const r=Date.now()+3*6e4;for(;;){const l=await this.run(e,["-s",t,"shell","service check package; pidof system_server"],5e3);if(/Service package: found/.test(l.stdout)&&/\d/.test(l.stdout.split("\n")[1]??""))break;if(Date.now()>r)throw new Error("Android's framework did not come back after setting the heap limit.");await I(500)}await I(5e3);await this.run(e,["-s",t,"unroot"],3e4);await I(1500);await this.run(e,["-s",t,"wait-for-device"],6e4)}async wipeIdentity(e,t,s){const i=await this.run(e,["-s",t,"root"],3e4);if(i.code!==0||/cannot run as root/i.test(i.stdout+i.stderr)){throw new Error(`Can't clear the app's Buoy id on this image (${(i.stdout+i.stderr).trim()}). Copies would share one id. Use the aosp_atd image.`)}await I(1500);await this.run(e,["-s",t,"wait-for-device"],6e4);const a=`/data/data/${s}`;const o=ga.map(d=>`${a}/${d}`).join(" ");const n="-name '*sync_device_id*' -o -name '*react_buoy_install*' -o -name '@buoy~1floating-tool~1*'";const r=async()=>(await this.shell(e,t,`find ${o} \\( ${n} \\) 2>/dev/null; grep -rlE '${wa}' ${o} 2>/dev/null; true`)).split("\n").map(d=>d.trim()).filter((d,u,h)=>d.startsWith(`${a}/`)&&h.indexOf(d)===u);const l=await r();for(const d of l)await this.shell(e,t,`rm -rf '${d.replace(/'/g,"")}'`);const c=await r();await this.run(e,["-s",t,"unroot"],3e4);await I(1500);await this.run(e,["-s",t,"wait-for-device"],6e4);if(c.length)throw new Error(`Could not clear the app's Buoy id (${c.join(", ")}).`);return l}async bench(e,t,s={}){const i=this.sdkOf(e);const a=[];for(let u=0;u<(s.launches??3);u++){const h=await this.launchApp(e,t.serial);if(h.totalMs!=null)a.push(h.totalMs)}await I(2e3);await this.shell(i,t.serial,`dumpsys gfxinfo ${e.packageName} reset`);const o=await vs(t.pid);const n=Date.now();await I((s.seconds??15)*1e3);const r=await vs(t.pid);const l=gs(await this.shell(i,t.serial,`dumpsys gfxinfo ${e.packageName}`));const c=await this.shell(i,t.serial,`dumpsys meminfo ${e.packageName}`);const d=Number(/TOTAL PSS:\s+(\d+)/.exec(c)?.[1]??/TOTAL\s+(\d+)/.exec(c)?.[1]??0);return{serial:t.serial,appStartMs:ua(a),appStarts:a,frames:l,appMemoryMB:Math.round(d/1024),macMemoryMB:await ma(t.pid),macCpuPct:o!=null&&r!=null?Math.round((r-o)/(Date.now()-n)*100):null}}};function Bs(e){return`'${e.replace(/'/g,"'\\''")}'`}import{execFile as ya}from"child_process";import{promises as A,readFileSync as va}from"fs";import St from"os";import x from"path";import{promisify as Sa}from"util";var ka=`// BuoyPhoneProfile: makes an app on the iOS Simulator act like a real iPhone.
|
|
10
|
+
//
|
|
11
|
+
// A simulator app is a plain Mac process. It sees the Mac's RAM and cores and
|
|
12
|
+
// nothing ever stops it from growing. The farm loads this library into the app
|
|
13
|
+
// (simctl launch passes SIMCTL_CHILD_DYLD_INSERT_LIBRARIES) so that:
|
|
14
|
+
// - the app is told the phone's RAM and CPU count,
|
|
15
|
+
// - it gets low-memory warnings where the phone would send them,
|
|
16
|
+
// - going past the phone's per-app limit is reported (or ends the app), and
|
|
17
|
+
// - a safety cap always ends a runaway app before it eats the Mac's RAM.
|
|
18
|
+
//
|
|
19
|
+
// Settings (env vars, all optional; bad values fall back to the default and are
|
|
20
|
+
// listed in the "profile_loaded" event as "ignored"):
|
|
21
|
+
// BUOY_PROFILE_NAME label in events (default "iPhone SE (2nd generation)")
|
|
22
|
+
// BUOY_PROFILE_RAM_MB RAM the app is told about (default 2957, a real SE 2)
|
|
23
|
+
// BUOY_PROFILE_CPUS CPU count the app is told (default 6)
|
|
24
|
+
// BUOY_PROFILE_LIMIT_MB the phone's per-app limit (default 2098, a real SE 2)
|
|
25
|
+
// BUOY_PROFILE_WARN_MB first low-memory warning (default 1707, a real SE 2)
|
|
26
|
+
// BUOY_PROFILE_ON_LIMIT "warn" (default): report it, keep running; "kill": end the app like the phone
|
|
27
|
+
// BUOY_PROFILE_CAP_MB safety cap, always ends the app (default limit + 1024; "0" = off)
|
|
28
|
+
// BUOY_PROFILE_EVENTS file to append one JSON event per line to
|
|
29
|
+
// BUOY_PROFILE_OBSERVE "1": change nothing, only report (for measuring a real phone)
|
|
30
|
+
// BUOY_PROFILE_SAMPLE_MS also report the footprint this often (default 0 = off, min 100)
|
|
31
|
+
//
|
|
32
|
+
// Footprint is phys_footprint, the number iOS jetsam uses on a real phone.
|
|
33
|
+
// The library never links UIKit, so it is safe in any process.
|
|
34
|
+
|
|
35
|
+
#import <Foundation/Foundation.h>
|
|
36
|
+
#import <objc/runtime.h>
|
|
37
|
+
#import <objc/message.h>
|
|
38
|
+
#import <mach/mach.h>
|
|
39
|
+
#import <sys/sysctl.h>
|
|
40
|
+
#import <os/proc.h>
|
|
41
|
+
#import <os/log.h>
|
|
42
|
+
#import <signal.h>
|
|
43
|
+
#import <string.h>
|
|
44
|
+
#import <stdlib.h>
|
|
45
|
+
#import <errno.h>
|
|
46
|
+
#import <fcntl.h>
|
|
47
|
+
#import <unistd.h>
|
|
48
|
+
|
|
49
|
+
#define MB (1024ull * 1024ull)
|
|
50
|
+
|
|
51
|
+
static uint64_t gRamBytes, gLimitBytes, gWarnBytes, gCapBytes;
|
|
52
|
+
static int gCpus;
|
|
53
|
+
static bool gKillAtLimit, gObserve;
|
|
54
|
+
static long gSampleMs;
|
|
55
|
+
static NSString *gName;
|
|
56
|
+
static int gEventsFd = -1;
|
|
57
|
+
static os_log_t gLog;
|
|
58
|
+
static dispatch_queue_t gEmitQueue;
|
|
59
|
+
static NSMutableArray<NSString *> *gIgnored;
|
|
60
|
+
|
|
61
|
+
// ---- Settings ---------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
// A whole number in [lo, hi], or \`fallback\` (and the key is noted) when the
|
|
64
|
+
// value is missing, not a number, or out of range. Never returns garbage.
|
|
65
|
+
static long envNumber(const char *key, long fallback, long lo, long hi) {
|
|
66
|
+
const char *v = getenv(key);
|
|
67
|
+
if (!v || !*v) return fallback;
|
|
68
|
+
errno = 0;
|
|
69
|
+
char *end = NULL;
|
|
70
|
+
long n = strtol(v, &end, 10);
|
|
71
|
+
if (errno || end == v || *end != '\\0' || n < lo || n > hi) {
|
|
72
|
+
[gIgnored addObject:[NSString stringWithFormat:@"%s=%s", key, v]];
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static uint64_t footprintBytes(void) {
|
|
79
|
+
task_vm_info_data_t info;
|
|
80
|
+
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
|
|
81
|
+
if (task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&info, &count) != KERN_SUCCESS) return 0;
|
|
82
|
+
return info.phys_footprint;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// One JSON object per line. Writes are serialized and use O_APPEND with a
|
|
86
|
+
// single write(), so lines from several threads (or several apps sharing a
|
|
87
|
+
// file) never interleave.
|
|
88
|
+
static void emit(NSString *event, NSDictionary *extra) {
|
|
89
|
+
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
90
|
+
@"event": event,
|
|
91
|
+
@"profile": gName ?: @"",
|
|
92
|
+
@"pid": @(getpid()),
|
|
93
|
+
@"footprintMB": @(footprintBytes() / MB),
|
|
94
|
+
@"limitMB": @(gLimitBytes / MB),
|
|
95
|
+
@"t": @([[NSDate date] timeIntervalSince1970]),
|
|
96
|
+
}];
|
|
97
|
+
if (extra) [d addEntriesFromDictionary:extra];
|
|
98
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:d options:0 error:nil];
|
|
99
|
+
if (!json) return;
|
|
100
|
+
NSMutableData *line = [json mutableCopy];
|
|
101
|
+
[line appendBytes:"\\n" length:1];
|
|
102
|
+
dispatch_sync(gEmitQueue, ^{
|
|
103
|
+
os_log(gLog, "%{public}.*s", (int)json.length, (const char *)json.bytes);
|
|
104
|
+
if (gEventsFd >= 0) (void)write(gEventsFd, line.bytes, line.length);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- What the app is told about the hardware -------------------------------
|
|
109
|
+
|
|
110
|
+
// libSystem calls sysctl before our constructor runs. Until gReady is set,
|
|
111
|
+
// every hook passes straight through. Calls made from this file are not
|
|
112
|
+
// interposed, so they reach the real functions.
|
|
113
|
+
static volatile bool gReady;
|
|
114
|
+
|
|
115
|
+
static unsigned long long fakePhysicalMemory(id self, SEL _cmd) { return gRamBytes; }
|
|
116
|
+
static NSUInteger fakeProcessorCount(id self, SEL _cmd) { return (NSUInteger)gCpus; }
|
|
117
|
+
|
|
118
|
+
// Answer like the real sysctl: same value size, same NULL-buffer size query,
|
|
119
|
+
// same ENOMEM when the caller's buffer is too small.
|
|
120
|
+
static int fakeValue(void *oldp, size_t *oldlenp, size_t realSize, uint64_t value) {
|
|
121
|
+
if (realSize != 4 && realSize != 8) return -2; // unknown shape: let the real call answer
|
|
122
|
+
if (!oldlenp) { errno = EINVAL; return -1; }
|
|
123
|
+
if (!oldp) { *oldlenp = realSize; return 0; }
|
|
124
|
+
if (*oldlenp < realSize) { errno = ENOMEM; return -1; }
|
|
125
|
+
if (realSize == 8) { uint64_t v = value; memcpy(oldp, &v, 8); }
|
|
126
|
+
else { uint32_t v = value > UINT32_MAX ? UINT32_MAX : (uint32_t)value; memcpy(oldp, &v, 4); }
|
|
127
|
+
*oldlenp = realSize;
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
typedef enum { FAKE_NONE, FAKE_RAM, FAKE_CPUS, FAKE_ONE } FakeKind;
|
|
132
|
+
|
|
133
|
+
static FakeKind kindForName(const char *name) {
|
|
134
|
+
if (!strcmp(name, "hw.memsize") || !strcmp(name, "hw.physmem") || !strcmp(name, "hw.memsize_usable")) return FAKE_RAM;
|
|
135
|
+
if (!strcmp(name, "hw.ncpu") || !strcmp(name, "hw.activecpu") || !strcmp(name, "hw.physicalcpu") ||
|
|
136
|
+
!strcmp(name, "hw.logicalcpu") || !strcmp(name, "hw.physicalcpu_max") || !strcmp(name, "hw.logicalcpu_max"))
|
|
137
|
+
return FAKE_CPUS;
|
|
138
|
+
// Core types: report one kind with all the phone's cores, so code that sizes
|
|
139
|
+
// thread pools per core type (perflevel0 = fastest) sees the phone's count.
|
|
140
|
+
if (!strcmp(name, "hw.nperflevels")) return FAKE_ONE;
|
|
141
|
+
if (!strncmp(name, "hw.perflevel0.", 14)) {
|
|
142
|
+
const char *k = name + 14;
|
|
143
|
+
if (!strcmp(k, "physicalcpu") || !strcmp(k, "logicalcpu") || !strcmp(k, "physicalcpu_max") || !strcmp(k, "logicalcpu_max"))
|
|
144
|
+
return FAKE_CPUS;
|
|
145
|
+
}
|
|
146
|
+
return FAKE_NONE;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static int hookSysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen) {
|
|
150
|
+
if (gReady && name && !newp) {
|
|
151
|
+
FakeKind k = kindForName(name);
|
|
152
|
+
if (k != FAKE_NONE) {
|
|
153
|
+
size_t realSize = 0;
|
|
154
|
+
if (sysctlbyname(name, NULL, &realSize, NULL, 0) == 0) {
|
|
155
|
+
uint64_t v = k == FAKE_RAM ? gRamBytes : k == FAKE_ONE ? 1 : (uint64_t)gCpus;
|
|
156
|
+
int r = fakeValue(oldp, oldlenp, realSize, v);
|
|
157
|
+
if (r != -2) return r;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return sysctlbyname(name, oldp, oldlenp, newp, newlen);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static int hookSysctl(int *mib, u_int len, void *oldp, size_t *oldlenp, void *newp, size_t newlen) {
|
|
165
|
+
if (gReady && mib && len == 2 && mib[0] == CTL_HW && !newp) {
|
|
166
|
+
FakeKind k = (mib[1] == HW_MEMSIZE || mib[1] == HW_PHYSMEM) ? FAKE_RAM
|
|
167
|
+
: (mib[1] == HW_NCPU || mib[1] == HW_AVAILCPU) ? FAKE_CPUS : FAKE_NONE;
|
|
168
|
+
if (k != FAKE_NONE) {
|
|
169
|
+
size_t realSize = 0;
|
|
170
|
+
if (sysctl(mib, len, NULL, &realSize, NULL, 0) == 0) {
|
|
171
|
+
int r = fakeValue(oldp, oldlenp, realSize, k == FAKE_RAM ? gRamBytes : (uint64_t)gCpus);
|
|
172
|
+
if (r != -2) return r;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return sysctl(mib, len, oldp, oldlenp, newp, newlen);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Dart (Flutter), .NET and plain C ask through sysconf instead of sysctl.
|
|
180
|
+
static long hookSysconf(int name) {
|
|
181
|
+
if (gReady) {
|
|
182
|
+
if (name == _SC_NPROCESSORS_CONF || name == _SC_NPROCESSORS_ONLN) return gCpus;
|
|
183
|
+
if (name == _SC_PHYS_PAGES) {
|
|
184
|
+
long page = sysconf(_SC_PAGESIZE);
|
|
185
|
+
if (page > 0) return (long)(gRamBytes / (uint64_t)page);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return sysconf(name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// On a real phone this is "how much more can I use before jetsam". In the
|
|
192
|
+
// simulator it is always 0, so apps that read it behave very differently.
|
|
193
|
+
static size_t hookAvailableMemory(void) {
|
|
194
|
+
if (!gReady) return os_proc_available_memory();
|
|
195
|
+
uint64_t used = footprintBytes();
|
|
196
|
+
return used >= gLimitBytes ? 0 : (size_t)(gLimitBytes - used);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// DYLD interposing works for libraries loaded with DYLD_INSERT_LIBRARIES.
|
|
200
|
+
typedef struct { const void *replacement; const void *original; } Interpose;
|
|
201
|
+
__attribute__((used, section("__DATA,__interpose")))
|
|
202
|
+
static const Interpose kInterposes[] = {
|
|
203
|
+
{ (const void *)hookSysctlbyname, (const void *)sysctlbyname },
|
|
204
|
+
{ (const void *)hookSysctl, (const void *)sysctl },
|
|
205
|
+
{ (const void *)hookSysconf, (const void *)sysconf },
|
|
206
|
+
{ (const void *)hookAvailableMemory, (const void *)os_proc_available_memory },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// ---- Memory watchdog --------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
static void sendMemoryWarning(uint64_t atBytes) {
|
|
212
|
+
NSNumber *atMB = @(atBytes / MB);
|
|
213
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
214
|
+
// Looked up at run time: this library does not link UIKit.
|
|
215
|
+
Class appClass = NSClassFromString(@"UIApplication");
|
|
216
|
+
id app = nil;
|
|
217
|
+
if (appClass && [appClass respondsToSelector:@selector(sharedApplication)]) {
|
|
218
|
+
app = ((id (*)(id, SEL))objc_msgSend)(appClass, @selector(sharedApplication));
|
|
219
|
+
}
|
|
220
|
+
// The same private call the Simulator's "Simulate Memory Warning" uses. It
|
|
221
|
+
// runs the app delegate, view controllers and caches, like a real warning.
|
|
222
|
+
SEL perform = NSSelectorFromString(@"_performMemoryWarning");
|
|
223
|
+
if (app && [app respondsToSelector:perform]) {
|
|
224
|
+
((void (*)(id, SEL))objc_msgSend)(app, perform);
|
|
225
|
+
emit(@"memory_warning", @{@"via": @"_performMemoryWarning", @"triggeredAtMB": atMB});
|
|
226
|
+
} else {
|
|
227
|
+
[NSNotificationCenter.defaultCenter postNotificationName:@"UIApplicationDidReceiveMemoryWarningNotification" object:app];
|
|
228
|
+
emit(@"memory_warning", @{@"via": @"notification", @"triggeredAtMB": atMB});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static void endApp(NSString *event, NSDictionary *info) {
|
|
234
|
+
emit(event, info);
|
|
235
|
+
kill(getpid(), SIGKILL);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static void startWatchdog(void) {
|
|
239
|
+
dispatch_queue_t q = dispatch_queue_create("gg.buoy.phone-profile.watch",
|
|
240
|
+
// User-initiated so a busy Mac can't delay the check much; it is a few
|
|
241
|
+
// microseconds every 50 ms (about 0.1% of a core).
|
|
242
|
+
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0));
|
|
243
|
+
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
|
244
|
+
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 50 * NSEC_PER_MSEC, 10 * NSEC_PER_MSEC);
|
|
245
|
+
__block BOOL warned = NO, over = NO;
|
|
246
|
+
__block uint64_t lastSample = 0, peak = 0, lastWarn = 0;
|
|
247
|
+
dispatch_source_set_event_handler(timer, ^{
|
|
248
|
+
uint64_t now = footprintBytes();
|
|
249
|
+
if (now > peak) peak = now;
|
|
250
|
+
if (gSampleMs > 0) {
|
|
251
|
+
uint64_t t = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / NSEC_PER_MSEC;
|
|
252
|
+
if (t - lastSample >= (uint64_t)gSampleMs) {
|
|
253
|
+
lastSample = t;
|
|
254
|
+
emit(@"sample", @{@"peakMB": @(peak / MB), @"availableMB": @(os_proc_available_memory() / MB)});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (gObserve) return;
|
|
258
|
+
if (gCapBytes && now >= gCapBytes) {
|
|
259
|
+
endApp(@"safety_kill", @{@"capMB": @(gCapBytes / MB), @"peakMB": @(peak / MB), @"triggeredAtMB": @(now / MB)});
|
|
260
|
+
}
|
|
261
|
+
if (now >= gLimitBytes && !over) {
|
|
262
|
+
over = YES;
|
|
263
|
+
if (gKillAtLimit) endApp(@"jetsam_kill", @{@"peakMB": @(peak / MB), @"triggeredAtMB": @(now / MB)});
|
|
264
|
+
// Warn mode: a real phone would have ended the app here. Keep going.
|
|
265
|
+
emit(@"over_limit", @{@"peakMB": @(peak / MB), @"triggeredAtMB": @(now / MB)});
|
|
266
|
+
}
|
|
267
|
+
if (over && now < gLimitBytes * 9 / 10) over = NO;
|
|
268
|
+
uint64_t ms = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / NSEC_PER_MSEC;
|
|
269
|
+
// Like a phone under pressure: warn when the line is crossed, and again
|
|
270
|
+
// every 10 s while the app stays above it.
|
|
271
|
+
if (now >= gWarnBytes && (!warned || ms - lastWarn >= 10000)) { warned = YES; lastWarn = ms; sendMemoryWarning(now); }
|
|
272
|
+
// Re-arm once the app has freed a good share of its memory.
|
|
273
|
+
if (warned && now < gWarnBytes * 9 / 10) warned = NO;
|
|
274
|
+
});
|
|
275
|
+
dispatch_resume(timer);
|
|
276
|
+
// Keep the timer alive for the life of the process.
|
|
277
|
+
objc_setAssociatedObject(NSProcessInfo.processInfo, "buoyPhoneProfileTimer", (id)timer, OBJC_ASSOCIATION_RETAIN);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Take our own path out of DYLD_INSERT_LIBRARIES (keeping anything else, such
|
|
281
|
+
// as Detox's library) so a process this app starts does not get the profile.
|
|
282
|
+
static void dropSelfFromInsertList(void) {
|
|
283
|
+
const char *list = getenv("DYLD_INSERT_LIBRARIES");
|
|
284
|
+
if (!list) return;
|
|
285
|
+
NSString *all = [NSString stringWithUTF8String:list];
|
|
286
|
+
if (!all) return; // not UTF-8: leave it as it is rather than lose someone's library
|
|
287
|
+
NSMutableArray *keep = [NSMutableArray array];
|
|
288
|
+
for (NSString *p in [all componentsSeparatedByString:@":"]) {
|
|
289
|
+
if (p.length && ![p.lastPathComponent hasPrefix:@"BuoyPhoneProfile"]) [keep addObject:p];
|
|
290
|
+
}
|
|
291
|
+
if (keep.count) setenv("DYLD_INSERT_LIBRARIES", [keep componentsJoinedByString:@":"].UTF8String, 1);
|
|
292
|
+
else unsetenv("DYLD_INSERT_LIBRARIES");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
__attribute__((constructor))
|
|
296
|
+
static void buoyPhoneProfileInit(void) {
|
|
297
|
+
@autoreleasepool {
|
|
298
|
+
gLog = os_log_create("gg.buoy.phone-profile", "profile");
|
|
299
|
+
gEmitQueue = dispatch_queue_create("gg.buoy.phone-profile.emit", DISPATCH_QUEUE_SERIAL);
|
|
300
|
+
gIgnored = [NSMutableArray array];
|
|
301
|
+
|
|
302
|
+
const char *name = getenv("BUOY_PROFILE_NAME");
|
|
303
|
+
gName = (name && *name) ? @(name) : @"iPhone SE (2nd generation)";
|
|
304
|
+
long ramMB = envNumber("BUOY_PROFILE_RAM_MB", 2957, 256, 1024 * 1024);
|
|
305
|
+
long cpus = envNumber("BUOY_PROFILE_CPUS", 6, 1, 256);
|
|
306
|
+
long limitMB = envNumber("BUOY_PROFILE_LIMIT_MB", 2098, 64, 1024 * 1024);
|
|
307
|
+
if (limitMB > ramMB) {
|
|
308
|
+
[gIgnored addObject:[NSString stringWithFormat:@"BUOY_PROFILE_LIMIT_MB=%ld (above RAM)", limitMB]];
|
|
309
|
+
limitMB = ramMB * 7 / 10;
|
|
310
|
+
}
|
|
311
|
+
// The default warning is the SE's measured one when it fits under the
|
|
312
|
+
// limit, else 80% of the limit.
|
|
313
|
+
long defaultWarnMB = 1707 < limitMB ? 1707 : limitMB * 8 / 10;
|
|
314
|
+
long warnMB = envNumber("BUOY_PROFILE_WARN_MB", defaultWarnMB, 32, 1024 * 1024);
|
|
315
|
+
if (warnMB >= limitMB) {
|
|
316
|
+
[gIgnored addObject:[NSString stringWithFormat:@"BUOY_PROFILE_WARN_MB=%ld (not below the limit)", warnMB]];
|
|
317
|
+
warnMB = defaultWarnMB;
|
|
318
|
+
}
|
|
319
|
+
long capMB = envNumber("BUOY_PROFILE_CAP_MB", limitMB + 1024, 0, 1024 * 1024);
|
|
320
|
+
if (capMB != 0 && capMB <= limitMB) {
|
|
321
|
+
// A cap under the phone's limit would end apps a real phone keeps. Never.
|
|
322
|
+
[gIgnored addObject:[NSString stringWithFormat:@"BUOY_PROFILE_CAP_MB=%ld (not above the limit)", capMB]];
|
|
323
|
+
capMB = limitMB + 1024;
|
|
324
|
+
}
|
|
325
|
+
gRamBytes = (uint64_t)ramMB * MB;
|
|
326
|
+
gCpus = (int)cpus;
|
|
327
|
+
gLimitBytes = (uint64_t)limitMB * MB;
|
|
328
|
+
gWarnBytes = (uint64_t)warnMB * MB;
|
|
329
|
+
gCapBytes = (uint64_t)capMB * MB;
|
|
330
|
+
|
|
331
|
+
const char *onLimit = getenv("BUOY_PROFILE_ON_LIMIT");
|
|
332
|
+
gKillAtLimit = onLimit && !strcmp(onLimit, "kill");
|
|
333
|
+
if (onLimit && *onLimit && strcmp(onLimit, "kill") && strcmp(onLimit, "warn"))
|
|
334
|
+
[gIgnored addObject:[NSString stringWithFormat:@"BUOY_PROFILE_ON_LIMIT=%s", onLimit]];
|
|
335
|
+
const char *observe = getenv("BUOY_PROFILE_OBSERVE");
|
|
336
|
+
gObserve = observe && !strcmp(observe, "1");
|
|
337
|
+
gSampleMs = envNumber("BUOY_PROFILE_SAMPLE_MS", 0, 0, 3600 * 1000);
|
|
338
|
+
if (gSampleMs > 0 && gSampleMs < 100) gSampleMs = 100;
|
|
339
|
+
|
|
340
|
+
const char *events = getenv("BUOY_PROFILE_EVENTS");
|
|
341
|
+
if (events && *events) {
|
|
342
|
+
gEventsFd = open(events, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
|
|
343
|
+
if (gEventsFd < 0) os_log(gLog, "BuoyPhoneProfile: cannot open events file %{public}s (errno %d)", events, errno);
|
|
344
|
+
}
|
|
345
|
+
dropSelfFromInsertList();
|
|
346
|
+
|
|
347
|
+
if (gObserve) {
|
|
348
|
+
emit(@"observe_loaded", @{@"sampleMs": @(gSampleMs), @"ignored": gIgnored});
|
|
349
|
+
startWatchdog();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
Class pi = NSProcessInfo.class;
|
|
354
|
+
SEL sels[] = { @selector(physicalMemory), @selector(processorCount), @selector(activeProcessorCount) };
|
|
355
|
+
IMP imps[] = { (IMP)fakePhysicalMemory, (IMP)fakeProcessorCount, (IMP)fakeProcessorCount };
|
|
356
|
+
for (int i = 0; i < 3; i++) {
|
|
357
|
+
Method m = class_getInstanceMethod(pi, sels[i]);
|
|
358
|
+
if (m) method_setImplementation(m, imps[i]);
|
|
359
|
+
}
|
|
360
|
+
gReady = true;
|
|
361
|
+
emit(@"profile_loaded", @{
|
|
362
|
+
@"ramMB": @(ramMB), @"cpus": @(gCpus), @"warnMB": @(warnMB), @"capMB": @(capMB),
|
|
363
|
+
@"onLimit": gKillAtLimit ? @"kill" : @"warn", @"ignored": gIgnored,
|
|
364
|
+
});
|
|
365
|
+
startWatchdog();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
`;var ba="499c4e3de0f8";var Ma='// PhoneProbe: a tiny app `farm phone measure` puts on a real iPhone for a few\n// seconds. It prints what the phone reports, then quits:\n// physicalMemory the RAM iOS reports\n// cpus the CPU count\n// footprint this app\'s own footprint right now\n// available os_proc_available_memory(): how much more this app may use\n// footprint + available is the per-app limit (jetsam) on this phone.\n\n#import <UIKit/UIKit.h>\n#import <mach/mach.h>\n#import <os/proc.h>\n\nstatic uint64_t footprintBytes(void) {\n task_vm_info_data_t info;\n mach_msg_type_number_t count = TASK_VM_INFO_COUNT;\n if (task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&info, &count) != KERN_SUCCESS) return 0;\n return info.phys_footprint;\n}\n\n// iOS 27 ends apps that don\'t use scenes, so the window comes from a scene delegate.\n@interface ProbeSceneDelegate : UIResponder <UIWindowSceneDelegate>\n@property (nonatomic, strong) UIWindow *window;\n@end\n\n@implementation ProbeSceneDelegate\n- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options {\n self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];\n self.window.rootViewController = [UIViewController new];\n self.window.rootViewController.view.backgroundColor = UIColor.systemTealColor;\n [self.window makeKeyAndVisible];\n}\n@end\n\n@interface ProbeDelegate : UIResponder <UIApplicationDelegate>\n@end\n\n@implementation ProbeDelegate\n- (UISceneConfiguration *)application:(UIApplication *)app configurationForConnectingSceneSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options {\n UISceneConfiguration *c = [[UISceneConfiguration alloc] initWithName:@"Default" sessionRole:session.role];\n c.delegateClass = ProbeSceneDelegate.class;\n return c;\n}\n- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts {\n NSProcessInfo *pi = NSProcessInfo.processInfo;\n fprintf(stdout, "BUOY_PROBE physicalMemory=%llu cpus=%lu footprint=%llu available=%zu\\n",\n pi.physicalMemory, (unsigned long)pi.processorCount, footprintBytes(), os_proc_available_memory());\n fflush(stdout);\n dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ exit(0); });\n return YES;\n}\n@end\n\nint main(int argc, char *argv[]) {\n @autoreleasepool {\n return UIApplicationMain(argc, argv, nil, NSStringFromClass(ProbeDelegate.class));\n }\n}\n';var je=Sa(ya);var Is={"iphone-se-2nd-gen":{name:"iPhone SE (2nd generation)",productType:"iPhone12,8",ramMB:2957,cpus:6,limitMB:2098,warnMB:1707,warnSource:"measured (plain memory test); the phone warned at 1,134 MB in a photos test",ios:"26.5",measuredAt:"2026-10-14"}};var $s="iphone-se-2nd-gen";var kt="off";function H(){return process.env.BUOY_FARM_HOME||x.join(St.homedir(),".buoy","farm")}function Fe(e=H()){return x.join(e,"phone")}function Os(e=H()){return x.join(Fe(e),"phones.json")}function As(e){const t=e;const s=(i,a,o)=>typeof i==="number"&&Number.isInteger(i)&&i>=a&&i<=o;return!!t&&typeof t==="object"&&typeof t.name==="string"&&t.name.length>0&&t.name.length<=100&&typeof t.productType==="string"&&s(t.ramMB,256,1<<20)&&s(t.cpus,1,256)&&s(t.limitMB,64,t.ramMB)&&s(t.warnMB,32,t.limitMB-1)}function he(e=H()){const t={...Is};try{const s=JSON.parse(va(Os(e),"utf8"));for(const[i,a]of Object.entries(s?.phones??{})){if(/^[a-z0-9][a-z0-9-]{0,63}$/.test(i)&&As(a))t[i]=a}}catch{}return t}async function Ds(e,t,s=H()){if(!/^[a-z0-9][a-z0-9-]{0,63}$/.test(e))throw new Error(`Bad phone id ${e}.`);if(!As(t))throw new Error(`Bad phone numbers for ${e}.`);const i=Os(s);let a={phones:{}};try{a=JSON.parse(await A.readFile(i,"utf8"));if(!a||typeof a.phones!=="object")a={phones:{}}}catch{}a.phones[e]=t;await A.mkdir(x.dirname(i),{recursive:true});const o=`${i}.${process.pid}.tmp`;await A.writeFile(o,JSON.stringify(a,null,2)+"\n");await A.rename(o,i)}function Ts(e){return Math.max(3072,e.limitMB+1024)}function Rs(e,t,s){const i=t.capMB>e.limitMB?t.capMB:Ts(e);return{BUOY_PROFILE_NAME:e.name,BUOY_PROFILE_RAM_MB:String(e.ramMB),BUOY_PROFILE_CPUS:String(e.cpus),BUOY_PROFILE_LIMIT_MB:String(e.limitMB),BUOY_PROFILE_WARN_MB:String(e.warnMB),BUOY_PROFILE_ON_LIMIT:t.onLimit,BUOY_PROFILE_CAP_MB:String(i),BUOY_PROFILE_EVENTS:s}}function bt(...e){const t=new Set;const s=[];for(const i of e){for(const a of(i??"").split(":")){const o=a.trim();if(o&&!t.has(o)){t.add(o);s.push(o)}}}return s.join(":")}function De(e,t=H()){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return x.join(Fe(t),`${e}.launch.json`)}function We(e,t=H()){if(!/^[0-9A-F-]{36}$/i.test(e))throw new Error(`not a UDID: ${e}`);return x.join(Fe(t),`${e}.events.jsonl`)}async function Ns(e,t=H()){const s=De(e.udid,t);await A.mkdir(x.dirname(s),{recursive:true});const i=`${s}.${process.pid}.tmp`;await A.writeFile(i,JSON.stringify(e,null,2)+"\n");await A.rename(i,s)}async function Ge(e,t=H()){await A.rm(De(e,t),{force:true})}async function Ps(e,t=H()){try{const s=JSON.parse(await A.readFile(De(e,t),"utf8"));if(s?.udid!==e||typeof s.dylib!=="string"||!s.vars||typeof s.vars!=="object")return null;await A.access(s.dylib);return s}catch{return null}}async function _a(e){try{const{stdout:t}=await je("xcrun",["simctl","spawn",e,"launchctl","getenv","DYLD_INSERT_LIBRARIES"],{timeout:1e4});return t.trim()}catch{return""}}async function Mt(e,t,s,i=H(),a=_a){const o=await Ps(e,i);if(!o||o.bundleId!==t){return s?{SIMCTL_CHILD_DYLD_INSERT_LIBRARIES:bt(process.env.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES,await a(e),s)}:{}}const n={};for(const[r,l]of Object.entries(o.vars))n[`SIMCTL_CHILD_${r}`]=l;n.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES=bt(process.env.SIMCTL_CHILD_DYLD_INSERT_LIBRARIES,await a(e),s,o.dylib);return n}function Cs(e){const t=[];for(const s of e.split("\n")){if(!s.trim())continue;try{const i=JSON.parse(s);if(i&&typeof i.event==="string"&&typeof i.pid==="number")t.push(i)}catch{}}return t}function Ls(e){let t=-1;for(let o=e.length-1;o>=0;o--)if(e[o].event==="profile_loaded"){t=o;break}if(t<0)return{loaded:false,ignored:[],memoryWarnings:0,overLimit:false,peakMB:0};const s=e[t];const i=e.slice(t).filter(o=>o.pid===s.pid);const a={pid:s.pid,loaded:true,ignored:Array.isArray(s.ignored)?s.ignored:[],memoryWarnings:0,overLimit:false,peakMB:0};for(const o of i){a.peakMB=Math.max(a.peakMB,o.footprintMB??0,typeof o.peakMB==="number"?o.peakMB:0);a.lastFootprintMB=o.footprintMB;if(o.event==="memory_warning")a.memoryWarnings++;if(o.event==="over_limit")a.overLimit=true;if(o.event==="jetsam_kill"){a.overLimit=true;a.ended="limit"}if(o.event==="safety_kill"){a.overLimit=true;a.ended="safety"}}return a}async function He(e,t=H()){const s=We(e,t);try{const i=await A.open(s,"r");try{const{size:a}=await i.stat();const o=Math.min(a,256*1024);const n=Buffer.alloc(o);await i.read(n,0,o,a-o);let r=n.toString("utf8");if(o<a)r=r.slice(r.indexOf("\n")+1);return Ls(Cs(r))}finally{await i.close()}}catch{return null}}async function Ea(){try{const{stdout:e}=await je("xcodebuild",["-version"],{timeout:3e4});return e.trim().split("\n").pop().replace(/^Build version\s*/,"").replace(/[^\w.-]/g,"")||"unknown"}catch{return"unknown"}}async function xs(e){const t=x.join(e,"BuoyPhoneProfile.dylib");try{await A.access(t);return t}catch{}const s=await A.mkdtemp(x.join(St.tmpdir(),"buoy-phone-profile-"));try{const i=x.join(s,"BuoyPhoneProfile.m");await A.writeFile(i,ka);const a=x.join(s,"BuoyPhoneProfile.dylib");await je("xcrun",["--sdk","iphonesimulator","clang","-arch","arm64","-arch","x86_64","-mios-simulator-version-min=13.0","-fobjc-arc","-O2","-Wall","-Werror","-dynamiclib","-framework","Foundation","-install_name","@rpath/BuoyPhoneProfile.dylib",i,"-o",a],{timeout:18e4,maxBuffer:8*1024*1024,env:{...process.env,TMPDIR:s}});await je("codesign",["-f","-s","-",a],{timeout:6e4});await A.mkdir(e,{recursive:true});const o=`${t}.${process.pid}`;await A.copyFile(a,o);await A.rename(o,t);return t}finally{await A.rm(s,{recursive:true,force:true}).catch(()=>void 0)}}var Us=class{constructor(e=x.join(St.homedir(),".buoy","phone-profile")){this.root=e;this.built=null;this.key="";this.error="";this.failedAt=0}dylib(){if(process.platform!=="darwin"){this.error="Phone profiles need a Mac.";return Promise.resolve(null)}const e=process.env.DEVELOPER_DIR??"";if(this.built&&this.key===e)return this.built;if(this.key===e&&this.failedAt&&Date.now()-this.failedAt<5*6e4)return Promise.resolve(null);if(this.key!==e)this.failedAt=0;this.key=e;this.error="";this.built=Ea().then(t=>xs(x.join(this.root,`${t}-${ba}`))).catch(t=>{const s=t;this.error=(s.stderr||s.message||"build failed").trim().split("\n").slice(-3).join(" ").slice(0,300);this.built=null;this.failedAt=Date.now();return null});return this.built}lastError(){return this.error}};var me=1024**3;var _t="Buoy Farm ";var Et=".buoy-farm.json";var Ba=120;var Ia=180;var Bt=9e4;var js=500;var Fs=200;var $a=3;var X=["done","failed","stopped","interrupted"];var Ws=["setting-up","working","waiting","passed","failed","needs-help"];var Oa={now:()=>performance.now()};var Aa=10*6e4;function Z(e){return Math.round((e.phone.ramMB/1024+pa)*10)/10}var Gs=class{constructor(){this.tails=new Map}run(e,t){const s=this.tails.get(e)??Promise.resolve();const i=s.catch(()=>{}).then(t);this.tails.set(e,i);i.finally(()=>{if(this.tails.get(e)===i)this.tails.delete(e)}).catch(()=>{});return i}};var Da=class{constructor(){this.counts=new Map}get size(){return this.counts.size}has(e){return this.counts.has(e)}add(e){this.counts.set(e,(this.counts.get(e)??0)+1)}delete(e){const t=(this.counts.get(e)??0)-1;if(t>0)this.counts.set(e,t);else this.counts.delete(e)}};var Ta=class{constructor(e){this.free=e;this.waiting=[]}async use(e){if(this.free>0)this.free--;else await new Promise(t=>this.waiting.push(t));try{return await e()}finally{const t=this.waiting.shift();if(t)t();else this.free++}}};var Hs=class{constructor(e){this.phoneFailed=new Map;this.simLock=new Gs;this.deadlines=new Map;this.reservations=new Map;this.busy=new Da;this.ticking=false;this.hardDeadlines=new Map;this.acquiring=new Set;this.budgetLock=new Gs;this.labels=new Map;this.copyMemAt=new Map;this.inflight=new Map;this.workContext=new pi;this.activeWork=0;this.lockQueues=new Map;this.fileTasks=new Set;this.checkActive=()=>this.store.assertActive();this.shotsInFlight=0;this.pendingBg=0;this.host=new Proxy(e.host,{get:(s,i)=>{const a=Reflect.get(s,i);if(typeof a!=="function")return a;return async(...o)=>{this.store.assertActive();const n=await a.apply(s,o);this.store.assertActive();return n}}});this.store=e.store;this.clock=e.clock??Oa;this.log=e.log??(()=>{});this.pollMs=e.pollMs??1e3;this.readyTimeoutMs=e.readyTimeoutMs??12e4;this.phoneBuilder=e.phoneBuilder??new Us;this.phoneLoadTimeoutMs=e.phoneLoadTimeoutMs??1e4;this.bootGate=new Ta(this.state.settings.maxConcurrentBoots);this.android=e.android??new Es({home:this.store.homeDir});const t={loadCounters:(s,i)=>this.host.loadCounters(s,i),simTree:async s=>{const i=this.copies[s];if(!i)return this.host.simTree(s);return(await this.android.treePids(i.pid)).map(a=>({pid:a,ppid:0,start:"",stat:"",comm:"",command:""}))}};this.meter=new Xt(t,()=>{const s=this.state.locks["perf-slot"];const i=s&&this.state.leases[s.leaseId];return i?{leaseId:i.id,udids:i.sims}:null})}async createTemplate(e){return this.work(void 0,()=>this.createTemplateWork(e))}async cloneSim(e,t){return this.work(void 0,()=>this.cloneSimWork(e,t))}async bootSim(e,t){return this.work(t,()=>this.bootSimWork(e,t))}async parkSim(e,t="idle"){return this.work(void 0,()=>this.parkSimWork(e,t))}async deleteSim(e,t){return this.work(t,()=>this.deleteSimWork(e,t))}async rebuildSim(e,t){return this.work(void 0,()=>this.simLock.run(`rebuild:${e}`,()=>this.rebuildSimWork(e,t)))}async fastReset(e,t){return this.work(void 0,()=>this.fastResetWork(e,t))}async claimOne(e,t){return this.work(e.id,()=>this.claimOneWork(e,t))}async action(e,t,s,i,a,o){this.touch(this.liveLease(e,t,o));const n=this.state.locks["perf-slot"];if(i==="screenshot"&&n&&n.leaseId!==e)throw new p("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);return this.work(i==="reset"||i==="rebuild"?void 0:e,()=>this.actionWork(e,t,s,i,a,o))}async work(e,t){this.store.assertActive();if(this.workContext.getStore()?.active)return t();while(this.state.locks["perf-slot"]&&this.state.locks["perf-slot"].leaseId!==e){await I(this.pollMs);this.store.assertActive()}const s={active:true,leaseId:e};this.activeWork++;try{return await this.workContext.run(s,t)}finally{s.active=false;this.activeWork--}}async files(e){this.checkActive();const t=e();this.fileTasks.add(t);try{const s=await t;this.checkActive();return s}finally{this.fileTasks.delete(t)}}fence(){this.store.fence()}async settleFiles(){await Promise.allSettled([...this.fileTasks])}get state(){return this.store.state}async recover(){for(const[a,o]of Object.entries(this.state.locks)){if(!this.state.leases[o.leaseId])delete this.state.locks[a]}const e=this.store.pending.filter(a=>a.op==="copy-start"||a.op==="copy-stop").map(({seq:a,op:o,data:n})=>({seq:a,op:o,data:n}));let t;try{t=await this.host.listDevices()}catch(a){if(Object.keys(this.state.sims).length||Object.keys(this.state.templates).length)this.log(`recover: iOS sims not checked: ${a.message}`);for(const o of this.store.pending)this.store.result(o.seq,o.op,{recovered:true});await this.recoverLeases();await this.recoverAndroid(e);this.store.save();return}const s=new Map(t.map(a=>[a.udid,a]));for(const a of Object.values(this.state.sims)){const o=s.get(a.udid);if(!o){this.store.event({kind:"lost",message:`${a.name} was deleted outside the farm.`,udid:a.udid});if(a.leaseId)this.dropFromLease(a.leaseId,a.udid);await this.files(()=>this.forgetPhone(a.udid)).catch(()=>void 0);delete this.state.sims[a.udid];continue}if(o.state==="Booted"){a.power=a.power==="frozen"?"frozen":"awake";if(a.power!=="frozen"){const n=this.state.templates[a.templateId];const r=await this.health(a.udid,n?ce(n.profile):[]);a.profile=r.profile;a.lastCheckAt=new Date().toISOString();if(!r.ok||n?.profile==="slim"&&r.profile==="unknown"){a.health="suspect";a.lastError=r.faults.join("; ")||"Slim state is not known."}}}else{a.power=a.power==="parked"?"parked":"cold"}if(["creating","booting","thawing","deleting"].includes(a.power))a.power=o.state==="Booted"?"awake":"cold"}for(const a of Object.values(this.state.templates)){const o=s.get(a.udid);if(!o){a.usable=false;a.note="Its sim was deleted outside the farm."}else if(!Ze(a.appPath)){a.note="Its app build is gone. Clones still work; rebuild the template for a new build."}}for(const a of Object.keys(this.state.orphans)){if(!s.has(a))delete this.state.orphans[a]}for(const a of this.store.pending){if(a.op==="clone"){const o=a.data?.name;const n=t.find(r=>r.name===o&&!this.state.sims[r.udid]);if(n){this.state.orphans[n.udid]={udid:n.udid,name:n.name,cloneIntentName:o};this.store.event({kind:"orphan",message:`Found ${n.name} with no record after a crash. Held for review. Use farm delete to remove it.`,udid:n.udid})}}if(a.op!=="rebuild")this.store.result(a.seq,a.op,{recovered:true})}const i=new Set;for(const a of[...this.store.pending]){if(a.op!=="rebuild")continue;const{udid:o,name:n,templateId:r}=a.data;if(Object.values(this.state.orphans).some(l=>l.name===n))continue;i.add(o);this.busy.add(o);this.background(`resume rebuild ${n}`,()=>this.work(void 0,async()=>{try{const l=Object.values(this.state.sims).find(c=>c.name===n&&c.udid!==o);if(!l){if(this.state.sims[o]){const c=this.state.sims[o];if(c.leaseId)this.dropFromLease(c.leaseId,o);c.leaseId=void 0;await this.deleteSim(o)}await this.cloneSim(r,n)}this.store.result(a.seq,"rebuild",{recovered:true})}finally{this.busy.delete(o)}}))}await this.recoverLeases();await this.recoverAndroid(e);for(const a of Object.values(this.state.sims)){if(a.health==="dirty"&&!a.leaseId&&!i.has(a.udid)){this.busy.add(a.udid);this.background(`resume reset ${a.name}`,async()=>{try{await this.rebuildSim(a.udid)}finally{this.busy.delete(a.udid)}})}}this.store.save()}async recoverLeases(){for(const e of Object.values(this.state.leases)){this.deadlines.set(e.id,this.clock.now()+Bt);const t=Date.parse(e.createdAt)+e.ttlMs-Date.now();this.hardDeadlines.set(e.id,this.clock.now()+Math.max(t,0));if(t<=0)this.background(`expire ${e.id}`,()=>this.endLease(e.id,"ran out of time while the farm was down","full","deadline"))}}async dataPath(e){const t=await this.host.dataPathOf?.(e);if(t)return t;const s=(await this.host.listDevices()).find(i=>i.udid===e);if(!s)throw new p("NOT_FOUND",`No sim ${e} on this Mac.`);return s.dataPath}async isOwned(e){const t=this.state.sims[e]??Object.values(this.state.templates).find(i=>i.udid===e);if(!t)return false;let s;try{s=await this.dataPath(e)}catch(i){if(i instanceof p&&i.code==="NOT_FOUND")return false;throw i}try{const i=JSON.parse(await R.readFile(O.join(s,Et),"utf8"));return i.farmId===this.state.farmId}catch{return false}}async assertOwned(e){if(!await this.isOwned(e)){throw new p("NOT_OWNED",`Sim ${e} is not a farm sim. The farm never changes sims it did not make.`)}}async writeMarker(e,t){const s=await this.dataPath(e);await this.files(async()=>{await R.mkdir(s,{recursive:true});this.checkActive();await R.writeFile(O.join(s,Et),JSON.stringify({farmId:this.state.farmId,...t})+"\n")})}farmAwakeGB(){const e=this.state.settings;let t=0;for(const s of Object.values(this.state.sims)){if(["awake","booting","frozen","thawing"].includes(s.power))t+=s.profile==="slim"?e.slimSimGB:e.stockSimGB}for(const s of Object.values(this.copies)){const i=this.androidTemplates[s.templateId];t+=i?Z(i):4.3}return t}async budget(e={ramGB:0,diskGB:0}){const t=this.state.settings;const s=await this.host.hostStats();let i=0;let a=0;for(const m of this.reservations.values()){i+=m.ramGB;a+=m.diskGB}for(const m of Object.values(this.copies)){const g=this.androidTemplates[m.templateId];if(g&&(m.state==="starting"||Date.now()-Date.parse(m.startedAt)<6e4))i+=Z(g)}const o=s.totalMemBytes/me;const n=Na(o)*(s.memPressure==="warn"?2:1);const r=s.availableMemBytes/me-i;const l=o-Ra(o,t.reserveRamGB)-this.farmAwakeGB()-i;const c=s.freeDiskBytes/me-a-t.reserveDiskGB;const d=Math.max(0,Math.floor(Math.min(r-n,l,c/t.cloneDiskGB*t.slimSimGB)/t.slimSimGB));const u=(m,g)=>({ok:false,code:m,reason:g,stats:s,roomFor:0});if(s.memPressure==="critical")return u("RAM_LOW","This Mac is low on memory. New sims will wait.");if(e.diskGB>0&&c-e.diskGB<0){return u("DISK_LOW",`Need ${t.reserveDiskGB} GB free after this. This Mac has ${Math.floor(s.freeDiskBytes/me)} GB.`)}if(e.ramGB>0){if(r-e.ramGB<n||l-e.ramGB<0){return u("RAM_LOW",`Not enough free memory for another sim (${Math.max(0,Math.floor(r))} GB free).`)}}const h=this.state.locks["perf-slot"];if(h&&(!this.workContext.getStore()?.active||this.workContext.getStore()?.leaseId!==h.leaseId))return u("HOST_BUSY","A speed test is running. New sims will wait.");return{ok:true,stats:s,roomFor:d,ramRoomGB:Math.max(0,Math.min(r-n,l))}}async withReservation(e,t){const s=se(4).toString("hex");await this.budgetLock.run("budget",async()=>{const i=await this.budget(e);if(!i.ok)throw new p(i.code,i.reason,void 0,true);this.reservations.set(s,e)});try{return await t()}finally{this.reservations.delete(s)}}async health(e,t=[]){const s=[];let i;try{i=await this.host.simTree(e)}catch(r){this.checkActive();return{ok:false,profile:"unknown",faults:[`Could not read sim processes: ${r.message}`],procs:0}}if(i.length===0)return{ok:false,profile:"unknown",faults:["launchd_sim is not running"],procs:0};const a=new Set(i.map(r=>r.comm.split("/").pop()));for(const r of["SpringBoard","backboardd"])if(!a.has(r))s.push(`${r} is not running`);const o=qs(await this.host.launchdLogTail(e));if(o)s.push(`${o} keeps crashing`);if(s.length===0){const r=await this.host.spawn(e,["launchctl","getenv","OS_ACTIVITY_MODE"],1e4);if(r.code===0&&r.stdout.trim())s.push(`OS_ACTIVITY_MODE=${r.stdout.trim()} is set for the whole sim (it was booted with SIMCTL_CHILD_OS_ACTIVITY_MODE)`)}if(s.length===0){const r=O.join(this.store.shotsPath,`${e}.png`);await this.files(()=>R.mkdir(O.dirname(r),{recursive:true}));if(!await this.host.screenshot(e,r,1e4))s.push("screenshot did not come back")}let n=i.length<Ba?"slim":i.length>Ia?"stock":"unknown";if(s.length===0&&t.length){const r=await this.host.spawn(e,["launchctl","print-disabled","system"],2e4);if(r.code===0&&!r.timedOut){const l=new Set([...r.stdout.matchAll(/"([^"]+)" => (?:disabled|true)/g)].map(d=>d[1]));const c=t.filter(d=>!l.has(d)).length;n=c===0?"slim":c===t.length?"stock":"unknown"}else{n="unknown"}}return{ok:s.length===0,profile:n,faults:s,procs:i.length}}async waitHealthy(e,t=[]){const s=performance.now()+this.readyTimeoutMs;let i=await this.health(e,t);while(!i.ok&&performance.now()<s){await I(this.pollMs);i=await this.health(e,t)}return i}async createTemplateWork(e){if(!["slim","stock"].includes(e.profile)||!Number.isInteger(e.metroPort)||e.metroPort<1||e.metroPort>65535){throw new p("BAD_REQUEST","Use slim or stock and a port from 1 to 65535.")}const t=e.seedDir==null?null:ke(e.seedDir);if(t)throw new p("NO_SEED",t,"Run: pnpm e2e:pool seed");if(!Ze(O.join(e.appPath,"Info.plist"))){throw new p("BAD_REQUEST",`No app build at ${e.appPath}.`,"Build the app first.")}const s=await bi(e.appPath);const i=`t-${se(3).toString("hex")}`;const a=e.name??`${_t}Template ${i.slice(2)}`;return this.withReservation({ramGB:this.state.settings.stockSimGB,diskGB:4},async()=>{const o=this.store.intent("create-template",{id:i,name:a});const n=await this.host.create(a,e.deviceType,e.runtime);const r={id:i,udid:n,name:a,bundleId:s,appPath:e.appPath,appHash:await ki(e.appPath),seedDir:e.seedDir??null,seedHash:e.seedDir?await Si(e.seedDir):null,launchUrl:e.launchUrl,deviceType:e.deviceType,runtime:e.runtime,xcodeBuild:await this.host.xcodeBuild(),profile:e.profile,profileHash:jt(e.profile),metroPort:e.metroPort,createdAt:new Date().toISOString(),usable:false,note:"Being built."};this.state.templates[i]=r;await this.writeMarker(n,{template:i});this.store.result(o,"create-template",{udid:n});try{await this.buildTemplate(r,e.warmLaunch!==false);r.usable=true;r.note=void 0;this.store.event({kind:"template",message:`Template ${a} is ready.`,udid:n})}catch(l){r.note=`Build failed: ${l.message}`;this.store.event({kind:"template-failed",message:r.note,udid:n});await this.host.shutdown(n).catch(()=>{});throw l}finally{this.store.save()}return r})}async buildTemplate(e,t){const s=ce(e.profile);if(s.length)await this.files(()=>Ht(e.udid,s,this.host.disabledRoot));await this.host.boot(e.udid);const i=await this.waitHealthy(e.udid,s);if(!i.ok)throw new Error(`template did not come up: ${i.faults.join("; ")}`);if(e.profile==="slim"&&i.profile!=="slim")throw new Error(`slim did not apply (${i.procs} processes)`);await this.host.install(e.udid,e.appPath);const a=await this.host.appContainer(e.udid,e.bundleId);if(!a)throw new Error("app installed but has no data container");if(e.seedDir)await this.files(()=>Qt(a,e.bundleId,e.seedDir,this.checkActive));else await this.files(()=>Le(a,this.checkActive));await this.setPrefs(e.udid,e);if(t){await this.launchApp(e.udid,e,{phone:false});await I(Math.min(15e3,this.pollMs*15));await this.host.terminate(e.udid,e.bundleId)}await this.host.shutdown(e.udid);await this.files(()=>Le(a,this.checkActive))}async launchApp(e,t,s={}){if(s.phone===false){await this.host.launch(e,t.bundleId)}else{await this.launchAsPhone(e,t.bundleId)}if(t.launchUrl)await this.host.openUrl(e,t.launchUrl)}async launchAsPhone(e,t){const s=this.store.homeDir;const i=this.state.settings;const a=v=>{const _=this.state.sims[e];if(_)_.phone=v};const o=async v=>{const _=await this.host.spawn(v,["launchctl","getenv","DYLD_INSERT_LIBRARIES"],1e4).catch(()=>null);return _&&_.code===0?_.stdout.trim():""};const n=async v=>{await this.files(()=>Ge(e,s));const _=await Mt(e,t,void 0,s,o);await this.host.launch(e,t,_);a(v)};if(i.phoneProfile===kt)return n({status:"off"});const r=he(s);const l=r[i.phoneProfile]?i.phoneProfile:$s;const c=r[l];const d=l!==i.phoneProfile?`No phone "${i.phoneProfile}" on this Mac, so it uses ${c.name}. `:"";const u=this.phoneFailed.get(t);if(u!==void 0&&performance.now()-u<Aa){return n({status:"fallback",phoneId:l,phoneName:c.name,note:`${d}This app did not start with the phone profile a moment ago, so it runs as a plain simulator app. The farm tries again in a few minutes.`})}const h=await this.phoneBuilder.dylib();if(!h){const v=this.phoneBuilder.lastError()||"unknown error";this.log(`phone profile: could not build the library: ${v}`);return n({status:"fallback",phoneId:l,phoneName:c.name,note:`${d}The phone profile could not be built (${v}), so the app runs as a plain simulator app.`})}const m=We(e,s);await this.files(async()=>{await R.mkdir(O.dirname(m),{recursive:true});await R.writeFile(m,"");await Ns({udid:e,bundleId:t,dylib:h,phoneId:l,vars:Rs(c,{phone:l,onLimit:i.phoneOnLimit,capMB:i.phoneCapMB},m),writtenAt:new Date().toISOString()},s)});const g=await Mt(e,t,void 0,s,o);await this.host.launch(e,t,g);const S=performance.now()+this.phoneLoadTimeoutMs;let w=null;for(;;){w=await He(e,s);if(w?.loaded||performance.now()>=S)break;await I(Math.min(250,Math.max(10,this.pollMs/4)))}if(w?.loaded){this.phoneFailed.delete(t);const v=w.ignored.length?` Ignored bad settings: ${w.ignored.join(", ")}.`:"";a({status:"on",phoneId:l,phoneName:c.name,pid:w.pid,note:(d+v).trim()||void 0});return}this.log(`phone profile: ${t} on ${e} did not report it; launching without it`);this.phoneFailed.set(t,performance.now());await this.host.terminate(e,t);await n({status:"fallback",phoneId:l,phoneName:c.name,note:`${d}The app did not start with the phone profile, so it runs as a plain simulator app.`})}async setPrefs(e,t){const s=["defaults","write","com.apple.keyboard.preferences"];await this.host.spawn(e,[...s,"AutomaticMinimizationEnabled","-bool","true"]);await this.host.spawn(e,[...s,"DidShowContinuousPathIntroduction","-bool","true"]);await this.host.spawn(e,["defaults","write",t.bundleId,"RCT_jsLocation",`localhost:${t.metroPort}`])}async nextName(){const e=await this.host.listDevices();const t=new Set([...e.map(i=>i.name),...Object.values(this.state.sims).map(i=>i.name)]);let s=this.state.nextIndex;while(t.has(`${_t}${String(s).padStart(2,"0")}`))s++;this.state.nextIndex=s+1;return`${_t}${String(s).padStart(2,"0")}`}template(e){const t=this.state.templates[e];if(!t)throw new p("NO_TEMPLATE",`No template ${e}.`,"Run sim_farm_setup to make one.");if(!t.usable)throw new p("NO_TEMPLATE",`Template ${t.name} can't be used: ${t.note??"not ready"}.`);return t}async cloneSimWork(e,t,s){t??=await this.nextName();const i=t;const a=this.template(e);return this.withReservation({ramGB:0,diskGB:this.state.settings.cloneDiskGB},()=>this.simLock.run(`template:${a.id}`,async()=>{await this.assertOwned(a.udid);const o=this.store.intent("clone",{templateId:e,name:i});const n=await this.host.clone(a.udid,i);const r={udid:n,name:i,templateId:e,power:"creating",profile:"unknown",health:"ok",bootEpoch:0,failures:0,createdAt:new Date().toISOString()};this.state.sims[n]=r;this.store.result(o,"clone",{udid:n});await this.writeMarker(n,{template:a.id});const l=await Ce(await this.dataPath(n),a.bundleId);if(l)await this.files(()=>Le(l,this.checkActive));const c=s?this.state.leases[s.leaseId]:void 0;if(c&&!this.leaseOverdue(c)){r.leaseId=c.id;c.sims=c.sims.map(d=>d===s.udid?n:d)}r.power="cold";this.store.event({kind:"clone",message:`Made ${t}.`,udid:n});this.store.save();return r}))}async bootSimWork(e,t){await this.assertOwned(e);const s=this.sim(e);if(s.power==="frozen"||s.power==="thawing"){throw new p("BAD_REQUEST",`${s.name} is frozen. Thaw it first.`)}const i=this.state.templates[s.templateId];const a=this.state.settings;const o={ramGB:i?.profile==="slim"?a.slimSimGB:a.stockSimGB,diskGB:0};return this.withReservation(o,()=>this.bootGate.use(()=>this.simLock.run(e,async()=>{this.assertLeaseState(s,t);if(s.power==="awake")return s;const n=i?ce(i.profile):[];if(n.length&&!await yi(e,n,this.host.disabledRoot)){await this.files(()=>Ht(e,n,this.host.disabledRoot))}s.power="booting";s.bootEpoch++;this.store.save();await this.host.boot(e);const r=await this.waitHealthy(e,i?.profile==="slim"?n:[]);s.lastCheckAt=new Date().toISOString();s.profile=r.profile;s.power="awake";if(!r.ok){s.failures++;s.health=s.failures>=2?"broken":"suspect";s.lastError=r.faults.join("; ");this.store.event({kind:"unhealthy",message:`${s.name}: ${s.lastError}`,udid:e});this.store.save();throw new p("HOST_ERROR",`${s.name} did not come up: ${s.lastError}`,"The farm will rebuild it.",true)}s.failures=0;if(s.health==="suspect")s.health="ok";s.idleSince=new Date().toISOString();if(i?.profile==="slim"&&r.profile==="unknown"){s.health="suspect";s.lastError="can't tell if slim applied";this.store.event({kind:"unknown-profile",message:`${s.name}: can't tell if slim worked. Held for now.`,udid:e});this.store.save();throw new p("HOST_ERROR",`${s.name}: can't tell if slim applied.`,"The farm will rebuild it.",true)}if(i?.profile==="slim"&&r.profile==="stock"){this.store.event({kind:"stock",message:`${s.name}: slim didn't work here (${r.profile}). It uses full RAM.`,udid:e});const l=await this.budget({ramGB:a.stockSimGB-a.slimSimGB,diskGB:0});if(!l.ok){s.health="suspect";s.lastError="slim did not apply and there is no RAM for a full sim";await this.host.shutdown(e).catch(()=>{});s.power="parked";this.store.save();throw new p("RAM_LOW",`${s.name}: slim didn't apply and there's no room for a full sim.`,void 0,true)}}this.store.save();return s})))}assertLeaseState(e,t){if(!this.state.sims[e.udid])throw new p("NOT_FOUND",`${e.name} is gone.`);if(t){const s=this.state.leases[t];if(e.leaseId!==t||!s||this.leaseOverdue(s)){throw new p("LEASE_EXPIRED",`${e.name} is no longer in lease ${t}.`)}}else if(e.leaseId){throw new p("BAD_REQUEST",`${e.name} is in use. End its lease first.`)}}async parkSimWork(e,t="idle"){await this.assertOwned(e);const s=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(s);if(this.busy.has(e))throw new p("BAD_REQUEST",`${s.name} is being reset.`);if(s.power==="frozen"||s.power==="thawing")throw new p("BAD_REQUEST",`${s.name} is frozen. Wake it before parking.`);if(s.power!=="awake")return;this.busy.add(e);try{await this.host.shutdown(e);s.power="parked"}finally{this.busy.delete(e)}this.store.event({kind:"park",message:`Parked ${s.name} (${t}).`,udid:e});this.store.save()})}sim(e){const t=this.state.sims[e];if(!t)throw new p("NOT_OWNED",`Sim ${e} is not a farm sim.`);return t}async deleteSimWork(e,t){const s=this.state.orphans[e];if(s){await this.simLock.run(e,async()=>{const a=(await this.host.listDevices()).find(r=>r.udid===e);if(!a)throw new p("NOT_FOUND","This sim is gone.");let o=false;try{const r=JSON.parse(await R.readFile(O.join(a.dataPath,Et),"utf8"));o=r.farmId===this.state.farmId}catch(r){o=r.code==="ENOENT"&&a.name===s.cloneIntentName}if(!o)throw new p("NOT_OWNED","This sim has no valid farm mark.");const n=this.store.intent("delete",{udid:e});await this.host.shutdown(e);await this.host.delete(e);await this.files(()=>R.rm(O.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));await this.files(()=>this.forgetPhone(e));delete this.state.orphans[e];this.store.result(n,"delete",{})});return}await this.assertOwned(e);const i=this.sim(e);await this.simLock.run(e,async()=>{this.assertLeaseState(i,t);if(i.power==="frozen"||i.power==="thawing")throw new p("BAD_REQUEST",`${i.name} is frozen. Wake it first.`);i.power="deleting";const a=this.store.intent("delete",{udid:e});await this.host.shutdown(e).catch(()=>{});await this.host.delete(e);await this.files(()=>R.rm(O.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`),{recursive:true,force:true}));await this.files(()=>this.forgetPhone(e));delete this.state.sims[e];this.store.result(a,"delete",{});this.store.event({kind:"delete",message:`Deleted ${i.name}.`,udid:e})})}async rebuildSimWork(e,t){const s=this.sim(e);const{name:i,templateId:a}=s;await this.assertOwned(e);await this.simLock.run(e,async()=>{this.assertLeaseState(s,t);this.busy.add(e)});const o=this.store.intent("rebuild",{udid:e,name:i,templateId:a});try{await this.deleteSim(e,t);const n=await this.cloneSimWork(a,i,t?{udid:e,leaseId:t}:void 0);this.store.result(o,"rebuild",{udid:n.udid});return n}finally{this.busy.delete(e)}}async fastResetWork(e,t){await this.assertOwned(e);const s=this.sim(e);const i=this.template(s.templateId);await this.simLock.run(e,async()=>{this.assertLeaseState(s,t);if(s.power==="frozen"||s.power==="thawing")throw new p("BAD_REQUEST",`${s.name} is frozen. Wake it first.`);this.busy.add(e);try{if(s.power==="awake")await this.host.terminate(e,i.bundleId);const a=await Ce(await this.dataPath(e),i.bundleId);if(!a)throw new Error("app container not found");if(i.seedDir)await this.files(()=>Qt(a,i.bundleId,i.seedDir,this.checkActive));else await this.files(()=>Le(a,this.checkActive));s.health="ok";this.store.save()}finally{this.busy.delete(e)}})}async grow(e,t){if(!Number.isInteger(t)||t<1||t>64)throw new p("BAD_REQUEST","count must be 1 to 64.");const s=[];for(let i=0;i<t;i++)s.push(await this.cloneSim(e));return s}async shrink(e,t){if(!Number.isInteger(t)||t<1||t>64)throw new p("BAD_REQUEST","count must be 1 to 64.");const s=["cold","parked","awake"];const i=Object.values(this.state.sims).filter(o=>o.templateId===e&&!o.leaseId&&s.includes(o.power)&&!this.busy.has(o.udid)).sort((o,n)=>s.indexOf(o.power)-s.indexOf(n.power));const a=[];for(const o of i.slice(0,t)){try{await this.deleteSim(o.udid);a.push(o.name)}catch(n){if(!(n instanceof p))throw n}}return a}async removeTemplate(e){if(this.androidTemplates[e])return this.work(void 0,()=>this.removeAndroidTemplate(e));return this.work(void 0,async()=>{const t=this.state.templates[e];if(!t)throw new p("NO_TEMPLATE",`No template ${e}.`);const s=Object.values(this.state.sims).filter(n=>n.templateId===e);const i=s.filter(n=>n.leaseId||this.busy.has(n.udid));if(i.length){throw new p("HOST_BUSY",`${i.map(n=>n.name).join(", ")} ${i.length===1?"is":"are"} in use.`,"End those uses first.",true)}const a=[];for(const n of s){await this.deleteSim(n.udid);a.push(n.name)}const o=(await this.host.listDevices()).some(n=>n.udid===t.udid);if(o){await this.assertOwned(t.udid);await this.simLock.run(t.udid,async()=>{const n=this.store.intent("delete",{udid:t.udid});await this.host.shutdown(t.udid).catch(()=>{});await this.host.delete(t.udid);await this.files(()=>R.rm(O.join(this.host.disabledRoot,`com.apple.CoreSimulator.SimDevice.${t.udid}`),{recursive:true,force:true}));this.store.result(n,"delete",{})})}delete this.state.templates[e];a.push(t.name);this.store.event({kind:"delete",message:`Removed template ${t.name} and its ${s.length} sim(s).`});return{removed:a}})}leaseOverdue(e){const t=this.clock.now();if(t>(this.hardDeadlines.get(e.id)??Infinity))return true;if(e.owner.kind==="person")return false;return!this.acquiring.has(e.id)&&t>(this.deadlines.get(e.id)??0)}leaseDeadline(e){return Math.min(this.deadlines.get(e.id)??0,this.hardDeadlines.get(e.id)??Infinity)}async acquire(e){if(e.ttlMs!==void 0&&(typeof e.ttlMs!=="number"||!Number.isFinite(e.ttlMs)||e.ttlMs<=0)){throw new p("BAD_REQUEST","ttlMs must be a finite number above zero.")}const t=this.androidTemplates[e.templateId];if(t&&this.android.stale(t)){throw new p("NO_TEMPLATE",`Android template ${t.id} can't start copies: ${this.android.stale(t)}`,`Make a new one: farm android template --apk ${t.apk}`)}const s=t?void 0:this.template(e.templateId);const i=t??s;if(!Number.isInteger(e.count)||e.count<1||e.count>64)throw new p("BAD_REQUEST","count must be 1 to 64.");const a=e.owner.kind==="person"?this.state.settings.personLeaseHours*36e5:24*36e5;const o=Math.min(Math.max(e.ttlMs??15*6e4,3e4),a);const n=e.runId?this.ownRun(e.runId,e.owner.clientId):this.newRun(e,i.id,o);const r=Date.parse(n.deadlineAt)-Date.now();if(r<3e4)throw new p("RUN_ENDED","This run is almost out of time.","Ask for more with sim_farm_extend, or finish it and start a new run.");const l={id:`l-${se(4).toString("hex")}`,epoch:1,owner:e.owner,purpose:e.purpose,runId:n.id,templateId:i.id,sims:[],createdAt:new Date().toISOString(),ttlMs:Math.min(o,r)};n.leases.push(l.id);n.lastActivityAt=l.createdAt;if(e.labels?.length)this.labels.set(l.id,e.labels);this.state.leases[l.id]=l;this.hardDeadlines.set(l.id,this.clock.now()+l.ttlMs);this.acquiring.add(l.id);const c=()=>this.deadlines.set(l.id,this.clock.now()+Math.min(l.ttlMs,Bt));c();const d=performance.now()+(e.waitMs??0);const u=()=>{if(!this.state.leases[l.id])throw new p("LEASE_EXPIRED","The lease ended while sims were being prepared.");if(this.leaseOverdue(l))throw new p("LEASE_EXPIRED","The lease ran out of time while sims were being prepared.")};try{while(l.sims.length<e.count){u();c();const h=t?await this.claimCopy(l,t):await this.claimOne(l,s);u();if(h)continue;if(performance.now()>=d)break;if(!l.sims.length&&n.state==="active"&&!Object.keys(n.lanes).length)n.state="waiting";await I(Math.max(this.pollMs,2e3))}if(l.sims.length<e.count&&!e.allowPartial){const h=await this.budget({ramGB:t?Z(t):this.state.settings.slimSimGB,diskGB:0});throw new p(h.code??"HOST_BUSY",`Got ${l.sims.length} of ${e.count} ${t?"copies":"sims"}. ${h.reason??"Others are in use."}`,"Try again later or ask for fewer.",true)}if(l.sims.length===0)throw new p("HOST_BUSY","No sim is free right now.","Try again later.",true);if(e.launch)await this.work(l.id,async()=>{u();if(t)await Promise.all(l.sims.map(h=>this.launchCopy(h)));else await Promise.all(l.sims.map(h=>this.launchApp(h,s)));c();if(t)await this.checkCopyIdentities(l,6e4);else await this.checkIdentities(l,6e4);u()});c();this.store.event({kind:"lease",message:`${e.owner.label} took ${l.sims.length} sim(s) for ${n.goal}.`,leaseId:l.id,runId:n.id});this.note(n,"farm",`Got ${l.sims.length} sim${l.sims.length===1?"":"s"}.`);this.store.save();return{lease:l,run:n,sims:l.sims.map(h=>this.state.sims[h]??this.copyView(this.copies[h]))}}catch(h){if(this.state.leases[l.id])this.note(n,"farm",`Could not get sims: ${h.message}`);await this.endLease(l.id,"acquire failed","fast","acquire-failed").catch(()=>{});throw h}finally{this.acquiring.delete(l.id);this.labels.delete(l.id)}}async claimOneWork(e,t){const s=Object.values(this.state.sims).filter(n=>n.templateId===t.id&&!n.leaseId&&n.health==="ok"&&!this.busy.has(n.udid));const i=s.find(n=>n.power==="awake")??s.find(n=>n.power==="parked"||n.power==="cold");const a=async n=>{const r=await this.isOwned(n.udid);return this.simLock.run(n.udid,async()=>{if(n.leaseId||this.busy.has(n.udid)||n.health!=="ok"||!this.state.sims[n.udid])return null;if(!r){n.health="broken";n.lastError="farm marker missing";this.store.save();return null}if(!this.state.leases[e.id]||this.leaseOverdue(e))return null;n.leaseId=e.id;e.sims.push(n.udid);n.idleSince=void 0;this.addLane(e,n);this.store.save();return n})};if(i?.power==="awake")return a(i);let o=null;try{if(i){o=await a(i)}else{const n=await this.budget({ramGB:t.profile==="slim"?this.state.settings.slimSimGB:this.state.settings.stockSimGB,diskGB:this.state.settings.cloneDiskGB});if(!n.ok)return null;o=await a(await this.cloneSim(t.id))}if(!o)return null;await this.bootSim(o.udid,e.id);return o}catch(n){if(o)this.unclaim(e,o.udid);if(n instanceof p&&(n.code==="RAM_LOW"||n.code==="DISK_LOW"||n.code==="HOST_BUSY"||n.code==="LEASE_EXPIRED")){return null}if(o&&this.state.sims[o.udid]?.health!=="ok"){const r=o.udid;this.background(`rebuild ${r}`,()=>this.rebuildSim(r))}throw n}}unclaim(e,t){e.sims=e.sims.filter(a=>a!==t);const s=e.runId?this.state.runs[e.runId]:void 0;if(s?.lanes[t])delete s.lanes[t];const i=this.state.sims[t];if(i&&i.leaseId===e.id)i.leaseId=void 0;this.store.save()}async checkIdentities(e,t=6e4){const s=this.template(e.templateId);const i=performance.now()+t;const a=new Map;for(;;){for(const n of e.sims){if(a.has(n))continue;const r=await Ce(await this.dataPath(n),s.bundleId);const l=r?await Vt(r):null;if(!l)continue;const c=[...a.entries()].find(([,u])=>u===l);if(c)throw new p("ID_CLASH",`${this.state.sims[n].name} and ${this.state.sims[c[0]].name} share one Buoy ID.`);const d=Object.values(this.state.sims).find(u=>u.udid!==n&&!e.sims.includes(u.udid)&&u.buoyDeviceId===l);if(d)throw new p("ID_CLASH",`${this.state.sims[n].name} and ${d.name} share one Buoy ID.`);a.set(n,l);this.state.sims[n].buoyDeviceId=l}if(a.size===e.sims.length||performance.now()>=i)break;await I(Math.max(this.pollMs,1e3))}const o=e.sims.filter(n=>!a.has(n));if(o.length){this.store.event({kind:"no-id",message:`No Buoy ID yet on ${o.map(n=>this.state.sims[n].name).join(", ")}. Is the app loading? Check Metro.`,leaseId:e.id})}}liveLease(e,t,s){const i=this.state.leases[e];if(!i){const a=this.runOfLease(e);if(a&&(a.endedBy==="user"||a.endedBy==="deadline"))this.assertRunOpen(a);throw new p("LEASE_EXPIRED",`Lease ${e} has ended.`,"Acquire new sims.")}if(i.epoch!==t)throw new p("STALE_EPOCH",`Lease ${e} is at epoch ${i.epoch}, not ${t}.`);if(s&&i.owner.clientId!==s)throw new p("NOT_OWNED",`Lease ${e} belongs to ${i.owner.label}.`);if(this.leaseOverdue(i)){const a=this.overdueEnd(i);this.background(`expire ${i.id}`,()=>this.endLease(i.id,"expired","full",a));if(a==="deadline")throw new p("RUN_ENDED","This run's time expired.","Ask for more time with sim_farm_extend before the end next time. Start a new run to go on.");throw new p("LEASE_EXPIRED",`Lease ${e} has expired.`,"Acquire new sims.")}return i}renew(e,t,s){const i=this.liveLease(e,t,s);const a=Math.min(this.clock.now()+Math.min(i.ttlMs,Bt),this.hardDeadlines.get(i.id)??Infinity);this.deadlines.set(i.id,a);return{expiresInMs:Math.round(a-this.clock.now())}}async release(e,t,s="fast",i,a=false){if(s!=="fast"&&s!=="full")throw new p("BAD_REQUEST","Use fast or full reset.");this.liveLease(e,t,i);return this.endLease(e,"released",s,a?null:"released")}async endLease(e,t,s,i="released"){const a=this.state.leases[e];if(!a)return{};a.epoch++;delete this.state.leases[e];this.deadlines.delete(e);this.hardDeadlines.delete(e);for(const[l,c]of Object.entries(this.state.locks))if(c.leaseId===e)delete this.state.locks[l];const o=new Set(a.sims.filter(l=>{const c=this.state.sims[l];return c?.bootEpoch===0&&c.power==="cold"&&c.health==="ok"}));const n={};const r=a.sims.filter(l=>this.copies[l]?.leaseId===e);for(const l of r){const c=this.copies[l];c.leaseId=void 0;c.state="stopping";n[c.name]="closing"}if(r.length)this.background(`close copies of ${e}`,()=>this.work(void 0,async()=>{for(const l of r)await this.stopCopy(l,`${a.owner.label}'s lease ended`)}));for(const l of a.sims){const c=this.state.sims[l];if(!c||c.leaseId!==e)continue;c.leaseId=void 0;c.health="dirty";c.idleSince=new Date().toISOString();this.busy.add(l);n[c.name]="resetting";void this.files(()=>Ge(l,this.store.homeDir)).catch(()=>void 0)}this.store.event({kind:"lease-end",message:`${a.owner.label}'s ${a.purpose} ended (${t}).`,leaseId:e,runId:a.runId});this.settleRun(a,i);this.store.save();this.background(`reset after ${e}`,()=>this.work(void 0,async()=>{for(const l of a.sims){if(!this.state.sims[l])continue;try{const c=Date.parse(a.createdAt)-2e3;for(const d of await this.host.clientsOf(l)){if(Date.parse(d.start)>=c)await this.host.kill(d.pid,d.start)}if(s==="full"&&!o.has(l)||this.state.sims[l].failures>0)await this.rebuildSim(l);else await this.fastReset(l)}catch(c){this.store.event({kind:"reset-failed",message:`${this.state.sims[l]?.name??l}: ${c.message}. Rebuilding.`,udid:l});await this.rebuildSim(l).catch(()=>{})}finally{this.busy.delete(l)}}}));return n}dropFromLease(e,t){const s=this.state.leases[e];if(s)s.sims=s.sims.filter(i=>i!==t)}async actionWork(e,t,s,i,a,o){const n=this.liveLease(e,t,o);const r={};for(const l of s){if(!n.sims.includes(l))throw new p("NOT_OWNED",`${l} is not in lease ${e}.`);if(this.copies[l]){r[l]=await this.copyAction(n,l,i,a,()=>this.liveLease(e,t,o));continue}if(i==="install")throw new p("BAD_REQUEST","install is for Android copies. Rebuild the iOS template for a new app build.");await this.assertOwned(l);this.liveLease(e,t,o);const c=this.template(n.templateId);switch(i){case"screenshot":{const d=this.state.locks["perf-slot"];if(d&&d.leaseId!==n.id)throw new p("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);const u=O.join(this.store.shotsPath,`${l}.png`);r[l]=await this.host.screenshot(l,u)?u:null;break}case"open-url":if(!a)throw new p("BAD_REQUEST","open-url needs a url.");await this.host.openUrl(l,a);r[l]="opened";break;case"launch":await this.launchApp(l,c);r[l]="launched";break;case"reset":await this.fastReset(l,n.id);r[l]="reset";break;case"rebuild":{const d=await this.rebuildSim(l,n.id);if(!this.state.leases[n.id])throw new p("LEASE_EXPIRED",`Lease ${e} ended during the rebuild.`);d.leaseId=n.id;n.sims=n.sims.map(m=>m===l?d.udid:m);const u=n.runId?this.state.runs[n.runId]:void 0;const h=u?.lanes[l];if(u&&h){delete u.lanes[l];u.lanes[d.udid]={...h,udid:d.udid,name:d.name,state:"setting-up",note:"Rebuilt"}}await this.bootSim(d.udid,n.id);r[l]=d.udid;break}default:throw new p("BAD_REQUEST",`Unknown action ${i}.`)}}this.store.save();return r}async lock(e,t,s,i=0,a){if(typeof e!=="string"||!/^[a-z0-9-]{1,40}$/.test(e))throw new p("BAD_REQUEST","Lock names are short lowercase words.");this.touch(this.liveLease(t,s,a));if(this.state.locks[e]?.leaseId===t)return{held:true};const o=this.lockQueues.get(e)??[];this.lockQueues.set(e,o);const n={};o.push(n);const r=performance.now()+i;try{for(;;){this.store.assertActive();this.liveLease(t,s,a);const l=this.state.locks[e];const c=e==="perf-slot"&&(this.activeWork>0||this.busy.size>0||this.pendingBg>0);if(o[0]===n&&!l&&!c){this.state.locks[e]={name:e,leaseId:t,since:new Date().toISOString()};this.store.save();return{held:true}}if(performance.now()>=r){return{held:false,holder:l?this.state.leases[l.leaseId]?.owner.label??"someone":"the farm (work is still running)",since:l?.since}}await I(Math.max(1,Math.min(this.pollMs,r-performance.now())))}}finally{o.splice(o.indexOf(n),1);if(!o.length)this.lockQueues.delete(e)}}fairness(e,t,s,i){this.liveLease(e,t,i);const a=this.state.settings;const o=Pa(a,this.meter.cpuCount);const n=this.meter.verdicts(e,s,o);return{rules:o,fair:n.filter(r=>r.fair).length,unfair:n.filter(r=>!r.fair).length,runs:n}}unlock(e,t,s,i){this.liveLease(t,s,i);if(this.state.locks[e]?.leaseId===t)delete this.state.locks[e];this.store.save();return{held:false}}async shot(e){if(this.state.locks["perf-slot"])throw new p("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(this.shotsInFlight>=$a)throw new p("HOST_BUSY","Taking other pictures. Try again in a moment.",void 0,true);this.shotsInFlight++;try{return await this.work(void 0,()=>this.shotWork(e))}finally{this.shotsInFlight--}}async shotWork(e){if(this.copies[e])return this.copyShot(e);await this.assertOwned(e);const t=this.sim(e);if(this.state.locks["perf-slot"])throw new p("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(t.power!=="awake")throw new p("BAD_REQUEST",`${t.name} is ${t.power}.`);const s=O.join(this.store.shotsPath,`${e}.png`);await this.files(()=>R.mkdir(O.dirname(s),{recursive:true}));if(!await this.host.screenshot(e,s,8e3))throw new p("HOST_ERROR",`No still from ${t.name} in time.`,void 0,true);return{file:s,at:new Date().toISOString()}}async show(e){if(this.copies[e])throw new p("BAD_REQUEST","Android copies run with no window, and can't get one later. Use its pictures in Details.");return this.work(this.state.sims[e]?.leaseId,()=>this.showWork(e))}async showWork(e){await this.assertOwned(e);const t=this.sim(e);if(t.power==="frozen"||t.power==="thawing")throw new p("BAD_REQUEST",`${t.name} is paused. Wake it first.`);if(t.power!=="awake")await this.bootSim(e,t.leaseId);await this.host.showWindow(e);return"shown"}async tick(){if(this.ticking)return;this.ticking=true;try{for(const s of Object.values(this.state.leases)){const i=s.owner.kind!=="person"&&!await j(s.owner.pid,s.owner.pidStart,true);if(i)await this.endLease(s.id,"its owner exited","full","owner-exited");else if(this.leaseOverdue(s))await this.endLease(s.id,"no renewal","full",this.overdueEnd(s))}await this.tickRuns();await this.tickCopies().catch(s=>this.log(`copies: ${s.message}`));await this.tickPhones().catch(s=>this.log(`phones: ${s.message}`));const e=this.state.settings.parkAfterMinutes;if(e>0){for(const s of Object.values(this.state.sims)){if(s.power!=="awake"||s.leaseId||this.busy.has(s.udid)||!s.idleSince)continue;if(Date.now()-Date.parse(s.idleSince)>e*6e4){await this.parkSim(s.udid).catch(i=>this.log(`park ${s.name}: ${i.message}`))}}}const t=Object.keys(this.state.requests);if(t.length>js)for(const s of t.slice(0,t.length-js))delete this.state.requests[s]}finally{this.ticking=false}}async drain(e=10*6e4){const t=performance.now()+e;while((this.busy.size>0||this.pendingBg>0||this.activeWork>0)&&performance.now()<t)await I(200);return this.busy.size===0&&this.pendingBg===0&&this.activeWork===0}idle(){return Object.keys(this.state.leases).length===0&&Object.keys(this.copies).length===0&&this.busy.size===0&&this.activeWork===0&&this.pendingBg===0&&this.acquiring.size===0}async parkAllIdle(){for(const e of Object.values(this.state.sims)){if(e.power==="awake"&&!e.leaseId&&!this.busy.has(e.udid))await this.parkSim(e.udid,"farm stopping").catch(()=>{})}}background(e,t){this.pendingBg++;this.workContext.exit(t).catch(s=>{this.log(`${e}: ${s.message}`);try{this.store.event({kind:"error",message:`${e}: ${s.message}`})}catch{}}).finally(()=>this.pendingBg--)}async once(e,t,s,i){if(!t)return i();const a=`${e}:${t}`;const o=ui("sha256").update(JSON.stringify(s)).digest("hex").slice(0,16);const n=this.state.requests[a];if(n){if(n.hash!==o)throw new p("REQUEST_REUSED",`Request ${t} was already used for a different call.`);return n.result}const r=this.inflight.get(a);if(r){if(r.hash!==o)throw new p("REQUEST_REUSED",`Request ${t} was already used for a different call.`);return r.promise}const l=i();this.inflight.set(a,{hash:o,promise:l});try{const c=await l;this.state.requests[a]={hash:o,result:c,at:new Date().toISOString()};this.store.save();return c}finally{this.inflight.delete(a)}}get copies(){return this.state.androidCopies??={}}get androidTemplates(){return this.state.androidTemplates??={}}copyView(e){const t=e.leaseId?this.state.leases[e.leaseId]:void 0;const s=t?.runId?this.state.runs[t.runId]:void 0;return{udid:e.id,name:e.name,platform:"android",templateId:e.templateId,serial:e.serial,state:e.state,startedAt:e.startedAt,readyMs:e.readyMs,bootedFrom:e.bootedFrom,leaseId:e.leaseId,owner:t?.owner.label,purpose:s?.goal??t?.purpose,runId:s?.id,lane:s?.lanes[e.id]?.label,buoyDeviceId:e.buoyDeviceId,phone:e.phone,lastError:e.lastError}}copyTemplate(e){const t=this.androidTemplates[e.templateId];if(!t)throw new p("NO_TEMPLATE",`${e.name}'s template is gone.`);return t}async claimCopy(e,t){return this.work(e.id,async()=>{try{return await this.withReservation({ramGB:Z(t),diskGB:1},()=>this.bootGate.use(()=>this.startCopyWork(t,e)))}catch(s){if(s instanceof p&&(s.code==="RAM_LOW"||s.code==="DISK_LOW"||s.code==="HOST_BUSY"||s.code==="LEASE_EXPIRED"))return null;throw s}})}async startCopyWork(e,t,s={}){if(t&&(!this.state.leases[t.id]||this.leaseOverdue(t)))throw new p("LEASE_EXPIRED","The lease ended while copies were being prepared.");const i=await this.android.freePort(new Set(Object.values(this.copies).map(l=>l.port)));const a=`a-${se(3).toString("hex")}`;this.state.nextCopyIndex=(this.state.nextCopyIndex??0)+1;const o={id:a,name:`Buoy Android ${this.state.nextCopyIndex}`,templateId:e.id,serial:`emulator-${i}`,port:i,pid:0,pidStart:"",log:"",state:"starting",startedAt:new Date().toISOString(),leaseId:t?.id,window:s.window||void 0};this.copies[a]=o;const n=this.store.intent("copy-start",{id:a,port:i,avd:e.avd});const r=performance.now();try{Object.assign(o,await this.android.startCopy(e,i,s))}catch(l){delete this.copies[a];this.store.result(n,"copy-start",{failed:true});throw new p("HOST_ERROR",`Could not start a copy: ${l.message}`)}this.store.result(n,"copy-start",{id:a,pid:o.pid});if(t){t.sims.push(a);this.addLane(t,{udid:a,name:o.name})}this.store.save();try{const l=await this.android.readyCopy(e,o);o.bootedFrom=l.bootedFrom;o.readyMs=Math.round(performance.now()-r);o.state="ready";if(l.bootedFrom==="cold"){this.store.event({kind:"copy-cold",message:`${o.name} booted from scratch: the template's snapshot did not load. Make a new template.`})}}catch(l){o.lastError=l.message;if(t)this.unclaim(t,a);await this.stopCopy(a,"it did not start").catch(()=>{});throw new p("HOST_ERROR",`${o.name} did not start: ${l.message}`)}this.store.save();return o}async stopCopy(e,t){const s=this.copies[e];if(!s)return;s.state="stopping";const i=this.store.intent("copy-stop",{id:e,pid:s.pid,pidStart:s.pidStart,serial:s.serial});try{if(s.pid)await this.android.stopCopy(this.androidTemplates[s.templateId],s)}finally{delete this.copies[e];this.copyMemAt.delete(e);this.store.result(i,"copy-stop",{id:e});this.store.event({kind:"copy-closed",message:`${s.name} closed (${t}).`})}}async launchCopy(e,t){const s=this.copies[e];const i=await this.android.launchApp(this.copyTemplate(s),s.serial,t);s.phone=void 0;this.copyMemAt.delete(e);return i.totalMs}async checkCopyIdentities(e,t=6e4){const s=performance.now()+t;const i=new Map;for(;;){for(const o of e.sims){const n=this.copies[o];if(!n||i.has(o))continue;const r=await this.android.installId(this.copyTemplate(n),n.serial).catch(()=>null);if(!r)continue;const l=[...i.entries()].find(([,c])=>c===r)??Object.values(this.copies).filter(c=>c.id!==o&&!e.sims.includes(c.id)&&c.buoyDeviceId===r).map(c=>[c.id,r])[0];if(l)throw new p("ID_CLASH",`${n.name} and ${this.copies[l[0]]?.name??l[0]} share one Buoy ID.`,"Make a new template: its saved id was not cleared.");i.set(o,r);n.buoyDeviceId=r}if(i.size===e.sims.length||performance.now()>=s)break;await I(Math.max(this.pollMs,1e3))}const a=e.sims.filter(o=>!i.has(o));if(a.length){this.store.event({kind:"no-id",message:`No Buoy ID yet on ${a.map(o=>this.copies[o]?.name??o).join(", ")}. Is the app loading? Check Metro. A release build's id can't be read.`,leaseId:e.id})}}async copyAction(e,t,s,i,a){const o=this.copies[t];if(o.state!=="ready"||!await this.android.alive(o.pid,o.pidStart))throw new p("NOT_FOUND",`${o.name} is not running.`,"Release it and take a new copy.");a();const n=this.copyTemplate(o);switch(s){case"screenshot":{const r=this.state.locks["perf-slot"];if(r&&r.leaseId!==e.id)throw new p("HOST_BUSY","A speed test is running. No screenshots right now.",void 0,true);const l=O.join(this.store.shotsPath,`${t}.png`);return await this.android.screenshot(n,o.serial,l)?l:null}case"open-url":if(!i)throw new p("BAD_REQUEST","open-url needs a url.");await this.android.openUrl(n,o.serial,i);return"opened";case"launch":{const r=await this.launchCopy(t,i);return r!=null?{launched:true,startMs:r}:"launched"}case"install":if(!i||!i.endsWith(".apk")||!Ze(i))throw new p("BAD_REQUEST","install needs the path of an .apk on this Mac.");await this.android.install(n,o.serial,i);return"installed";case"reset":case"rebuild":return this.replaceCopy(e,t);default:throw new p("BAD_REQUEST",`Unknown action ${s}.`)}}async replaceCopy(e,t){const s=this.copies[t];const i=this.copyTemplate(s);await this.stopCopy(t,"reset");e.sims=e.sims.filter(r=>r!==t);const a=e.runId?this.state.runs[e.runId]:void 0;const o=a?.lanes[t];if(a&&o)delete a.lanes[t];const n=await this.withReservation({ramGB:Z(i),diskGB:1},()=>this.startCopyWork(i,e));if(a&&o)a.lanes[n.id]={...o,udid:n.id,name:n.name,state:"setting-up",note:"Reset"};this.store.save();return n.id}async copyShot(e){const t=this.copies[e];if(this.state.locks["perf-slot"])throw new p("HOST_BUSY","A speed test is running. No stills right now.",void 0,true);if(t.state!=="ready")throw new p("BAD_REQUEST",`${t.name} is ${t.state}.`);const s=O.join(this.store.shotsPath,`${e}.png`);await this.files(()=>R.mkdir(O.dirname(s),{recursive:true}));if(!await this.android.screenshot(this.copyTemplate(t),t.serial,s,8e3))throw new p("HOST_ERROR",`No still from ${t.name} in time.`,void 0,true);return{file:s,at:new Date().toISOString()}}async copyBench(e,t,s,i,a){const o=this.liveLease(e,t,a);if(!o.sims.includes(s)||!this.copies[s])throw new p("NOT_OWNED",`${s} is not a copy in lease ${e}.`);this.touch(o);const n=this.copies[s];return this.work(o.id,()=>this.android.bench(this.copyTemplate(n),n,i))}async dropCopies(e,t,s,i){const a=this.liveLease(e,t,i);for(const n of s)if(!a.sims.includes(n)||!this.copies[n])throw new p("NOT_OWNED",`${n} is not a copy in lease ${e}.`);const o=[];await this.work(void 0,async()=>{for(const n of s){o.push(this.copies[n]?.name??n);const r=a.runId?this.state.runs[a.runId]:void 0;if(r?.lanes[n])r.lanes[n].gone=true;this.dropFromLease(a.id,n);await this.stopCopy(n,"closed by its owner")}});if(!this.state.leases[a.id]?.sims.length)await this.endLease(a.id,"its copies were closed","fast","released");this.store.save();return o}async createAndroidTemplate(e){return this.work(void 0,async()=>{const t={phone:{ramMB:4096}};return this.withReservation({ramGB:Z(t),diskGB:4},async()=>{this.store.event({kind:"template",message:`Making an Android template from ${O.basename(e.apk)}\u2026`});let s;try{s=await this.android.buildTemplate(e,a=>{this.log(a);this.store.event({kind:"template-progress",message:a})})}catch(a){this.store.event({kind:"template-failed",message:`Android template failed: ${a.message}`});throw new p("HOST_ERROR",`Android template failed: ${a.message}`)}s.usable=false;s.note="Checking that a copy starts from the snapshot.";this.androidTemplates[s.id]=s;this.store.save();let i=null;try{i=await this.startCopyWork(s);s.copyReadyMs=i.readyMs;if(i.bootedFrom==="cold")throw new Error("Copies cold boot instead of loading the snapshot.");s.usable=true;s.note=void 0;this.store.event({kind:"template",message:`Android template ${s.id} is ready: ${s.packageName}, copies start in ${((i.readyMs??0)/1e3).toFixed(1)} s.`})}catch(a){s.note=`Build failed: ${a.message}`;this.store.event({kind:"template-failed",message:s.note});throw new p("HOST_ERROR",s.note)}finally{if(i)await this.stopCopy(i.id,"template check").catch(()=>{});this.store.save()}return s})})}async removeAndroidTemplate(e){const t=this.androidTemplates[e];const s=Object.values(this.copies).filter(i=>i.templateId===e);if(s.length)throw new p("HOST_BUSY",`Template ${e} still has copies running: ${s.map(i=>i.name).join(", ")}.`,"Close them first.");await this.android.removeTemplateFiles(t);delete this.androidTemplates[e];this.store.event({kind:"template-removed",message:`Removed Android template ${e}.`});this.store.save();return{removed:[]}}async recoverAndroid(e){await this.importLegacyAndroid();for(const s of e){if(s.op==="copy-stop"){const i=s.data;await this.android.stopCopy(this.androidTemplates[this.copies[i.id]?.templateId??""],i).catch(()=>{});delete this.copies[i.id]}if(s.op==="copy-start"){const i=s.data;const a=this.copies[i.id];if(a&&!a.pid)delete this.copies[i.id]}}for(const s of Object.values(this.copies)){const i=s.pid&&await this.android.alive(s.pid,s.pidStart);const a=s.leaseId?this.state.leases[s.leaseId]:void 0;if(!i){this.lostCopy(s,"its emulator stopped while the farm was down");continue}if(!a)await this.stopCopy(s.id,"no one held it after a restart").catch(()=>{})}const t=new Set(Object.values(this.copies).map(s=>s.port));for(const s of await this.android.findEmulators().catch(()=>[])){if(t.has(s.port))continue;this.store.event({kind:"orphan-copy",message:`Closed a farm emulator on port ${s.port} that had no record.`});await this.android.stopCopy(Object.values(this.androidTemplates).find(i=>i.avd===s.avd),{serial:`emulator-${s.port}`,pid:s.pid,pidStart:s.pidStart}).catch(()=>{})}}lostCopy(e,t){delete this.copies[e.id];this.copyMemAt.delete(e.id);this.store.event({kind:"lost",message:`${e.name} is gone: ${t}.`,udid:e.id});const s=e.leaseId?this.state.leases[e.leaseId]:void 0;if(!s)return;this.dropFromLease(s.id,e.id);const i=s.runId?this.state.runs[s.runId]:void 0;if(i?.lanes[e.id]){i.lanes[e.id].gone=true;this.note(i,"farm",`${i.lanes[e.id].label??e.name} stopped by itself (${t}).`,e.id)}if(!s.sims.length)this.background(`end ${s.id}`,()=>this.endLease(s.id,"its copies are gone","fast","lost"))}async importLegacyAndroid(){if(this.state.androidImported)return;const e=O.join(Ae(this.store.homeDir),"state.json");let t=null;try{t=JSON.parse(di(e,"utf8"))}catch{}for(const i of Object.values(t?.templates??{}))if(!this.androidTemplates[i.id])this.androidTemplates[i.id]=i;const s=[];for(const i of Object.values(t?.copies??{})){if(!await this.android.alive(i.pid,i.pidStart))continue;this.state.nextCopyIndex=(this.state.nextCopyIndex??0)+1;s.push({id:`a-${se(3).toString("hex")}`,name:`Buoy Android ${this.state.nextCopyIndex}`,templateId:i.templateId,serial:i.serial,port:i.port,pid:i.pid,pidStart:i.pidStart,log:i.log,state:"ready",startedAt:i.startedAt,readyMs:i.readyMs,bootedFrom:i.bootedFrom,window:i.window||void 0})}if(s.length){const i={clientId:"c-moved",kind:"person",pid:0,label:"You (farm android)"};const a=this.state.settings.personLeaseHours*36e5;const o={templateId:s[0].templateId,count:s.length,purpose:"Copies started before this update",goal:"Copies started before this update",owner:i,ttlMs:a};const n=this.newRun(o,s[0].templateId,a);const r={id:`l-${se(4).toString("hex")}`,epoch:1,owner:i,purpose:o.purpose,runId:n.id,templateId:s[0].templateId,sims:[],createdAt:new Date().toISOString(),ttlMs:a};n.leases.push(r.id);this.state.leases[r.id]=r;this.hardDeadlines.set(r.id,this.clock.now()+a);for(const l of s){l.leaseId=r.id;this.copies[l.id]=l;r.sims.push(l.id);this.addLane(r,{udid:l.id,name:l.name})}}this.state.androidImported=true;if(t)this.store.event({kind:"android-moved",message:`Moved ${Object.keys(t.templates??{}).length} Android template(s) and ${s.length} running cop${s.length===1?"y":"ies"} into the farm.`});this.store.save()}async tickCopies(){for(const e of Object.values(this.copies)){if(e.state!=="ready")continue;if(!await this.android.alive(e.pid,e.pidStart)){this.lostCopy(e,"its emulator stopped");this.store.save();continue}const t=e.leaseId?this.state.leases[e.leaseId]:void 0;if(!t||this.state.locks["perf-slot"]||Date.now()-(this.copyMemAt.get(e.id)??0)<5e3)continue;this.copyMemAt.set(e.id,Date.now());const s=this.androidTemplates[e.templateId];if(!s)continue;const i=await this.android.appMemory(s,e.serial).catch(()=>null);if(i?.pid){if(e.phone?.pid!==i.pid)e.phone={...e.phone,pid:i.pid,peakMB:i.pssMB??void 0,overLimit:void 0,killedFor:void 0};else if(i.pssMB&&i.pssMB>(e.phone.peakMB??0))e.phone.peakMB=i.pssMB}else if(e.phone?.pid&&!e.phone.killedFor){const a=await this.android.memoryKill(s,e.serial,e.phone.pid).catch(()=>null);if(a){e.phone.killedFor=a;e.phone.overLimit=true;const o=t.runId?this.state.runs[t.runId]:void 0;if(o&&e.phone.notedPid!==e.phone.pid){e.phone.notedPid=e.phone.pid;this.note(o,"farm",`${o.lanes[e.id]?.label??e.name}: Android closed the app for memory (${a}), like ${s.phone.name} would. Peak ${e.phone.peakMB??"?"} MB.`,e.id)}}}}}newRun(e,t,s){const i=Date.now();const a=e.owner.agent?this.state.settings.runCapMinutes*6e4:24*36e5;const o=(e.goal??"").trim()||e.purpose;const n={id:`r-${se(4).toString("hex")}`,goal:o.slice(0,200),kind:e.kind?.slice(0,40),owner:e.owner,templateId:t,state:"active",createdAt:new Date(i).toISOString(),deadlineAt:new Date(i+Math.min(s,a)).toISOString(),capAt:new Date(i+a).toISOString(),leases:[],lanes:{},notes:[],lastActivityAt:new Date(i).toISOString(),auto:!e.goal};this.state.runs[n.id]=n;return n}touch(e){const t=e.runId?this.state.runs[e.runId]:void 0;if(t)t.lastActivityAt=new Date().toISOString()}runOfLease(e){return Object.values(this.state.runs).find(t=>t.leases.includes(e))}assertRunOpen(e){if(e.state==="stopping"||e.state==="stopped"){if(e.endedBy==="user")throw new p("RUN_STOPPED","The person stopped this run in Buoy Desktop (reason=user).","Tell them: You stopped this run in Sims. Start a new run only if they ask.");if(e.endedBy==="deadline")throw new p("RUN_ENDED","This run ran out of time.","Start a new run to go on. Ask for more time with sim_farm_extend before the end next time.")}if(X.includes(e.state)||e.state==="stopping")throw new p("RUN_ENDED",`This run has ended (${e.state}).`,"Start a new run.")}ownRun(e,t){const s=this.state.runs[e];if(!s)throw new p("NOT_FOUND",`No run ${e}.`,"Use the runId that sim_farm_acquire returned.");if(t&&s.owner.clientId!==t)throw new p("NOT_OWNED",`Run ${e} belongs to ${s.owner.label}.`);this.assertRunOpen(s);return s}async forgetPhone(e){await R.rm(De(e,this.store.homeDir),{force:true});await R.rm(We(e,this.store.homeDir),{force:true})}async tickPhones(){for(const e of Object.values(this.state.sims)){if(e.phone?.status!=="on"||e.power!=="awake")continue;const t=e.leaseId?this.state.leases[e.leaseId]:void 0;const s=t?.runId?this.state.runs[t.runId]:void 0;if(!s||X.includes(s.state))continue;const i=await He(e.udid,this.store.homeDir);if(!i?.overLimit||!i.pid||e.phone.notedPid===i.pid)continue;e.phone.notedPid=i.pid;const a=s.lanes[e.udid]?.label??e.name;const o=e.phone.phoneName??"the phone";this.note(s,"farm",i.ended==="safety"?`${a}: the app used ${i.peakMB} MB and hit the farm's safety cap, so it was closed. ${o} would have closed it sooner.`:i.ended==="limit"?`${a}: the app went past ${o}'s memory limit (${i.peakMB} MB) and was closed, like on a real phone.`:`${a}: the app went past ${o}'s memory limit (${i.peakMB} MB). A real phone would close it.`,e.udid);this.store.save()}}note(e,t,s,i){e.notes.push({at:new Date().toISOString(),by:t,text:s.slice(0,500),udid:i});if(e.notes.length>Fs)e.notes.splice(0,e.notes.length-Fs)}addLane(e,t){const s=e.runId?this.state.runs[e.runId]:void 0;if(!s)return;const i=this.labels.get(e.id)?.[e.sims.length-1];s.lanes[t.udid]={udid:t.udid,name:t.name,label:i?.slice(0,80),state:"setting-up",at:new Date().toISOString()};if(s.state==="waiting")s.state="active"}overdueEnd(e){return this.clock.now()>=(this.hardDeadlines.get(e.id)??Infinity)?"deadline":"lost"}settleRun(e,t){const s=e.runId?this.state.runs[e.runId]:void 0;if(!s)return;for(const a of e.sims)if(s.lanes[a])s.lanes[a].gone=true;if(X.includes(s.state)||s.state==="stopping"||t===null)return;const i=s.leases.some(a=>this.state.leases[a]);switch(t){case"released":if(!i)this.endRun(s,this.outcomeOf(s)==="failed"?"failed":"done","released");return;case"acquire-failed":if(!i&&!Object.keys(s.lanes).length)this.endRun(s,"failed","acquire-failed");return;case"user":return this.endRun(s,"stopped","user","You stopped this run.");case"deadline":return this.endRun(s,"stopped","deadline","Ran out of time. The sims were taken back.");case"owner-exited":return this.endRun(s,"interrupted","owner-exited","The chat closed. Cleaning up.");case"lost":return this.endRun(s,"interrupted","lost","Lost touch with the chat. Cleaning up.");case"finished":return this.endRun(s,"done","finished")}}endRun(e,t,s,i,a="full"){if(X.includes(e.state))return;e.state="stopping";e.endedBy=s;if(i)this.note(e,s==="user"?"person":"farm",i);for(const o of e.leases){if(this.state.leases[o])void this.endLease(o,i??s,s==="finished"||s==="released"?a:"full",null)}for(const o of Object.values(e.lanes))o.gone=true;e.state=t;e.endedAt=new Date().toISOString();e.outcome=e.outcome??this.outcomeOf(e);this.store.event({kind:"run-end",message:`${e.owner.label}: "${e.goal}" ${t==="done"?"finished":t}.`,runId:e.id})}outcomeOf(e){const t=Object.values(e.lanes);const s=t.filter(a=>a.state==="passed").length;const i=t.filter(a=>a.state==="failed").length;if(!s&&!i)return"unknown";if(i&&s)return"mixed";return i?"failed":"passed"}report(e,t){const s=this.ownRun(e.runId,t);const i=new Date().toISOString();if(e.sequence!==void 0){if(!Number.isInteger(e.sequence)||e.sequence<0)throw new p("BAD_REQUEST","sequence must be a whole number.");if(s.lastSeq!==void 0&&e.sequence<=s.lastSeq)return{applied:false,reason:"already have this report",...this.timeLeft(s)};s.lastSeq=e.sequence}if(e.state!==void 0&&!Ws.includes(e.state))throw new p("BAD_REQUEST",`state must be one of ${Ws.join(", ")}.`);if(e.udid!==void 0){const a=s.lanes[e.udid];if(!a)throw new p("NOT_FOUND",`That sim is not in run ${s.id}.`,"Use a UDID from this run's sims.");if(e.state)a.state=e.state;if(e.state==="passed"||e.state==="failed")a.resultSource=e.resultSource==="test"?"test":"agent";if(e.note!==void 0)a.note=String(e.note).slice(0,200);a.at=i}if(e.counts!==void 0){const a=e.counts;const o=n=>n===void 0||Number.isInteger(n)&&n>=0;if(!a||typeof a!=="object"||!o(a.done)||!o(a.total)||!o(a.passed)||!o(a.failed)||a.done>a.total){throw new p("BAD_REQUEST","counts needs whole numbers, with done no more than total.")}s.counts={done:a.done,total:a.total,passed:a.passed,failed:a.failed}}if(e.note)this.note(s,"agent",String(e.note),e.udid);s.lastReportAt=s.lastActivityAt=i;this.store.save();return{applied:true,...this.timeLeft(s)}}extend(e,t,s){const i=this.ownRun(e,s);if(!Number.isFinite(t)||t<=0)throw new p("BAD_REQUEST","minutes must be above zero.");const a=Date.parse(i.deadlineAt);const o=Date.now()+t*6e4;const n=Math.min(o,Date.parse(i.capAt));if(n<=a){return{granted:false,partly:false,reason:"This run is at its time limit.",...this.timeLeft(i)}}i.deadlineAt=new Date(n).toISOString();i.lastActivityAt=new Date().toISOString();for(const r of i.leases){const l=this.state.leases[r];if(!l)continue;l.ttlMs=n-Date.parse(l.createdAt);this.hardDeadlines.set(r,this.clock.now()+(n-Date.now()))}this.note(i,"farm",`More time: until ${new Date(n).toLocaleTimeString()}.`);this.store.save();return{granted:true,partly:n<o,...this.timeLeft(i)}}finish(e,t,s,i){const a=this.state.runs[e];if(a?.endedBy==="finished"&&a.owner.clientId===i)return{state:a.state,outcome:a.outcome};const o=this.ownRun(e,i);if(t&&!["passed","failed","mixed","unknown"].includes(t))throw new p("BAD_REQUEST","outcome must be passed, failed, mixed or unknown.");o.outcome=t??this.outcomeOf(o);if(s)o.summary=s.slice(0,600);this.endRun(o,o.outcome==="failed"?"failed":"done","finished",void 0,"fast");this.store.save();return{state:o.state,outcome:o.outcome}}stopRun(e,t="you"){const s=this.state.runs[e];if(!s)throw new p("NOT_FOUND",`No run ${e}.`);if(X.includes(s.state))return{state:s.state};this.endRun(s,"stopped","user",t==="you"?"You stopped this run.":`Stopped by ${t}.`);this.store.save();return{state:s.state}}stopAll(e="you"){const t=Object.values(this.state.runs).filter(s=>!X.includes(s.state));for(const s of t)this.stopRun(s.id,e);return{stopped:t.length}}timeLeft(e){return{deadlineAt:e.deadlineAt,capAt:e.capAt,secondsLeft:Math.max(0,Math.round((Date.parse(e.deadlineAt)-Date.now())/1e3))}}async tickRuns(){const e=Date.now();for(const a of Object.values(this.state.runs)){if(X.includes(a.state))continue;if(e>=Date.parse(a.deadlineAt)){this.endRun(a,"stopped","deadline","Ran out of time. The sims were taken back.");this.store.save()}else if(!a.leases.some(o=>this.state.leases[o])&&!await j(a.owner.pid,a.owner.pidStart,true)){this.endRun(a,"interrupted","owner-exited","The chat closed.");this.store.save()}}const t=this.state.settings.keepRunsHours*36e5;const s=Object.values(this.state.runs).filter(a=>X.includes(a.state)).sort((a,o)=>Date.parse(o.endedAt??o.createdAt)-Date.parse(a.endedAt??a.createdAt));let i=false;s.forEach((a,o)=>{if(o>=200||e-Date.parse(a.endedAt??a.createdAt)>t){delete this.state.runs[a.id];i=true}});if(i)this.store.save()}async status(e={}){const t=await this.budget();const s=await Promise.all(Object.values(this.state.sims).map(async a=>{const o=a.leaseId?this.state.leases[a.leaseId]:void 0;const n=o?.runId?this.state.runs[o.runId]:void 0;const r=a.phone?.status==="on"?await He(a.udid,this.store.homeDir):null;const l=a.phone&&{...a.phone,notedPid:void 0,...r?.loaded?{pid:r.pid,peakMB:r.peakMB,memoryWarnings:r.memoryWarnings,overLimit:r.overLimit,ended:r.ended}:{}};return{...a,phone:l,owner:o?.owner.label,purpose:n?.goal??o?.purpose,runId:n?.id,lane:n?.lanes[a.udid]?.label,busy:this.busy.has(a.udid)}}));const i={farmId:this.state.farmId,counts:{pool:s.length,awake:s.filter(a=>a.power==="awake").length,leased:s.filter(a=>a.leaseId).length},budget:{roomForMoreSims:t.roomFor,freeDiskGB:Math.floor(t.stats.freeDiskBytes/me),availableMemGB:Math.floor(t.stats.availableMemBytes/me),memPressure:t.stats.memPressure,loadAvg1:Number(t.stats.loadAvg1.toFixed(1)),cpuCount:t.stats.cpuCount},templates:Object.values(this.state.templates),sims:s,android:{templates:Object.values(this.androidTemplates).map(a=>{const o=this.android.stale(a);return{...a,platform:"android",costGB:Z(a),roomFor:Math.floor((t.ramRoomGB??0)/Z(a)),...o&&a.usable?{usable:false,note:o}:{}}}),copies:Object.values(this.copies).map(a=>this.copyView(a))},orphans:Object.values(this.state.orphans),leases:Object.values(this.state.leases).map(a=>({...a,expiresInMs:Math.max(0,Math.round(this.leaseDeadline(a)-this.clock.now()))})),runs:Object.values(this.state.runs).sort((a,o)=>Date.parse(o.createdAt)-Date.parse(a.createdAt)).map(a=>({...a,notes:a.notes.slice(-50),secondsLeft:X.includes(a.state)?0:Math.max(0,Math.round((Date.parse(a.deadlineAt)-Date.now())/1e3))})),locks:Object.values(this.state.locks),recentEvents:this.state.events.slice(-25),settings:this.state.settings};if(e.outside){const a=new Set([...Object.keys(this.state.sims),...Object.keys(this.state.orphans),...Object.values(this.state.templates).map(o=>o.udid)]);i.outsideSims=(await this.host.listDevices()).filter(o=>!a.has(o.udid)&&o.available).map(o=>({udid:o.udid,name:o.name,state:o.state,runtime:o.runtime}))}return i}setSettings(e){const t=this.state.settings;const s={parkAfterMinutes:[0,24*60],maxConcurrentBoots:[1,8],reserveRamGB:[4,512],reserveDiskGB:[5,2048],slimSimGB:[.5,16],stockSimGB:[1,32],cloneDiskGB:[.1,32],runCapMinutes:[5,24*60],keepRunsHours:[1,24*14],fairMinFreeShare:[0,1],fairMaxSwingShare:[.02,1],fairMaxThermalLevel:[0,4],fairMaxSwapGrowthMB:[0,65536],personLeaseHours:[1,24],phoneCapMB:[0,1048576]};if(!e||typeof e!=="object")throw new p("BAD_REQUEST","settings must be an object.");if("phoneProfile"in e){const a=he(this.store.homeDir);if(e.phoneProfile!==kt&&!(typeof e.phoneProfile==="string"&&a[e.phoneProfile])){throw new p("BAD_REQUEST",`phoneProfile must be "off" or one of: ${Object.keys(a).join(", ")}.`)}}if("phoneOnLimit"in e&&e.phoneOnLimit!=="warn"&&e.phoneOnLimit!=="kill"){throw new p("BAD_REQUEST",`phoneOnLimit must be "warn" or "kill".`)}if("phoneCapMB"in e&&(!Number.isInteger(e.phoneCapMB)||e.phoneCapMB!==0&&e.phoneCapMB<3072)){throw new p("BAD_REQUEST","phoneCapMB must be 0 (automatic) or a whole number of MB, at least 3072.")}for(const[a,o]of Object.entries(e)){if(!(a in t)||typeof o!==typeof t[a])throw new p("BAD_REQUEST",`Bad setting ${a}.`);const n=s[a];if(n&&(typeof o!=="number"||!Number.isFinite(o)||o<n[0]||o>n[1])){throw new p("BAD_REQUEST",`${a} must be ${n[0]} to ${n[1]}.`)}}const i=["phoneProfile","phoneOnLimit","phoneCapMB"].some(a=>a in e&&e[a]!==t[a]);for(const[a,o]of Object.entries(e))t[a]=o;this.store.save();if(i){for(const a of Object.keys(this.state.sims))void Ge(a,this.store.homeDir).catch(()=>void 0)}return t}};function Ra(e,t){return Math.min(t,Math.max(4,Math.round(e*.375)))}function Na(e){return Math.min(4,Math.max(1.5,e*.1))}function Pa(e,t){const s=i=>Math.round(i*10)/10;return{minFreeCores:s(Math.max(2,e.fairMinFreeShare*t)),maxSwingCores:s(Math.max(1,e.fairMaxSwingShare*t)),maxThermalLevel:e.fairMaxThermalLevel,maxSwapGrowthMB:e.fairMaxSwapGrowthMB}}function qs(e,t=Date.now()-3*6e4){const s=new Map;for(const i of e.split("\n")){const a=/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ \((?:user|system)\/\d*\/?([\w.-]+) \[\d+\]\) <\w+>: exited due to (SIG\w+)/.exec(i);if(!a||a[3]==="SIGKILL"||a[3]==="SIGTERM")continue;const o=Date.parse(a[1].replace(" ","T"));if(!(o>=t))continue;const n=s.get(a[2])??[];n.push(o);s.set(a[2],n)}for(const[i,a]of s){a.sort((o,n)=>o-n);for(let o=2;o<a.length;o++)if(a[o]-a[o-2]<=6e4)return i.replace(/^com\.apple\./,"")}return null}import{promises as zs}from"fs";import fe from"os";import we from"path";var Ys=6e4;var It=class{constructor(){this.disabledRoot=ue;this.devicesDir=we.join(fe.homedir(),"Library/Developer/CoreSimulator/Devices");this.graphicsPids=[];this.loadCalls=0}simctl(e,t=Ys){return N("xcrun",["simctl",...e],{timeoutMs:t})}async listDevices(){const e=JSON.parse(await this.simctl(["list","devices","--json"],3e4));const t=[];for(const[s,i]of Object.entries(e.devices??{})){for(const a of i){t.push({udid:a.udid,name:a.name,state:a.state,runtime:s,deviceType:a.deviceTypeIdentifier,dataPath:a.dataPath??we.join(this.devicesDir,a.udid,"data"),available:a.isAvailable!==false})}}return t}async dataPathOf(e){if(!/^[0-9A-F-]{36}$/i.test(e))return null;const t=we.join(this.devicesDir,e);try{await zs.access(we.join(t,"device.plist"));return we.join(t,"data")}catch{return null}}async create(e,t,s){return(await this.simctl(["create",e,t,s])).trim()}async clone(e,t){return(await this.simctl(["clone",e,t])).trim()}async boot(e){const t=await M("xcrun",["simctl","boot",e],{timeoutMs:Ys,env:it()});if(t.code!==0&&!/current state: Booted/i.test(t.stderr)){throw new Error(`simctl boot ${e} failed: ${t.stderr.trim().slice(0,300)}`)}}async shutdown(e){const t=await M("xcrun",["simctl","shutdown",e],{timeoutMs:9e4});if(t.code!==0&&!/current state: Shutdown/i.test(t.stderr)){throw new Error(`simctl shutdown ${e} failed: ${t.stderr.trim().slice(0,300)}`)}}async delete(e){await this.simctl(["delete",e],9e4)}async install(e,t){await this.simctl(["install",e,t],18e4)}async launch(e,t,s){await N("xcrun",["simctl","launch","--terminate-running-process",e,t],{timeoutMs:6e4,env:s&&Object.keys(s).length?{...process.env,...s}:void 0})}async terminate(e,t){await M("xcrun",["simctl","terminate",e,t],{timeoutMs:2e4})}async openUrl(e,t){await this.simctl(["openurl",e,t],2e4)}async screenshot(e,t,s=1e4){const i=await M("xcrun",["simctl","io",e,"screenshot",t],{timeoutMs:s});return i.code===0&&!i.timedOut}spawn(e,t,s=2e4){return M("xcrun",["simctl","spawn",e,...t],{timeoutMs:s})}async appContainer(e,t){const s=await M("xcrun",["simctl","get_app_container",e,t,"data"],{timeoutMs:2e4});return s.code===0?s.stdout.trim()||null:null}async simTree(e){const t=await Pe();const s=t.find(i=>/launchd_sim/.test(i.command)&&i.command.includes(e));return s?st(t,s.pid):[]}async signal(e,t){for(const s of e){try{process.kill(s,t)}catch{}}}async launchdLogTail(e,t=256*1024){const s=we.join(this.disabledRoot,`com.apple.CoreSimulator.SimDevice.${e}`,"launchd.log");try{const i=await zs.open(s,"r");try{const{size:a}=await i.stat();const o=Math.max(0,a-t);const n=Buffer.alloc(a-o);await i.read(n,0,n.length,o);return n.toString("utf8")}finally{await i.close()}}catch{return""}}async hostStats(){const[e,t,s,i]=await Promise.all([N("vm_stat",[],{timeoutMs:1e4}),M("sysctl",["-n","kern.memorystatus_vm_pressure_level"],{timeoutMs:5e3}),M("sysctl",["-n","vm.swapusage"],{timeoutMs:5e3}),N("df",["-k",fe.homedir()],{timeoutMs:1e4})]);const a=Number(/page size of (\d+)/.exec(e)?.[1]??16384);const o=u=>Number(new RegExp(`${u}:\\s+(\\d+)`).exec(e)?.[1]??0);const n=o("Pages free")+o("Pages inactive")+o("Pages speculative")+o("Pages purgeable");const r=Number(t.stdout.trim());const l=/used = ([\d.]+)M/.exec(s.stdout)?.[1];const c=i.trim().split("\n").pop()??"";const d=Number(c.split(/\s+/)[3]??0);return{totalMemBytes:fe.totalmem(),availableMemBytes:n*a,memPressure:r>=4?"critical":r>=2?"warn":"normal",swapUsedBytes:l?Number(l)*1024*1024:0,freeDiskBytes:d*1024,cpuCount:fe.cpus().length,loadAvg1:fe.loadavg()[0]}}async showWindow(e){const t=await M("open",["-b","com.apple.iphonesimulator","--args","-CurrentDeviceUDID",e],{timeoutMs:2e4});if(t.code===0)return;await N("open",["-b","com.apple.dt.Devices"],{timeoutMs:2e4})}async loadCounters(e,t=true){if(t&&this.loadCalls++%10===0){const S=await M("pgrep",["-x","SimMetalHost"],{timeoutMs:5e3});this.graphicsPids=S.stdout.split("\n").map(Number).filter(w=>w>0)}const s=fe.cpus();let i=0;let a=0;for(const S of s){const w=S.times;const v=w.user+w.nice+w.sys+w.idle+w.irq;a+=v;i+=v-w.idle}const o=[...e,...this.graphicsPids];const[n,r,l,c]=await Promise.all([t&&o.length?M("ps",["-o","pid=,time=","-p",o.join(",")],{timeoutMs:5e3}):null,t?M("ioreg",["-r","-c","IOAccelerator","-d","1"],{timeoutMs:5e3}):null,M("sysctl",["-n","kern.memorystatus_vm_pressure_level","vm.swapusage"],{timeoutMs:5e3}),M("notifyutil",["-g","com.apple.system.thermalpressurelevel"],{timeoutMs:5e3})]);const d=new Set(this.graphicsPids);let u=0;let h=0;for(const S of n?.stdout.split("\n")??[]){const w=/^\s*(\d+)\s+(\S+)/.exec(S);if(!w)continue;const v=Zt(w[2]);if(d.has(Number(w[1])))h+=v;else u+=v}const m=r&&/"Device Utilization %"=(\d+)/.exec(r.stdout);const g=/thermalpressurelevel\s+(\d+)/.exec(c.stdout);return{hostBusyMs:i,hostTotalMs:a,cpuCount:s.length,pidsCpuMs:t?u:null,graphicsCpuMs:t?h:null,gpuPct:m?Number(m[1]):null,memPressureLevel:Number(l.stdout.trim().split("\n")[0])||1,swapUsedMB:Number(/used = ([\d.]+)M/.exec(l.stdout)?.[1]??0),thermalLevel:g?Number(g[1]):null}}async xcodeBuild(){const e=await M("xcodebuild",["-version"],{timeoutMs:2e4});return e.stdout.trim().replace(/\s+/g," ")||"unknown"}async clientsOf(e){const t=await Pe();const s=new Set((await this.simTree(e)).map(a=>a.pid));const i=new Set(st(t,process.pid).map(a=>a.pid));return t.filter(a=>!i.has(a.pid)&&!s.has(a.pid)&&a.command.includes(e)&&!/launchd_sim|CoreSimulator/.test(a.command))}async kill(e,t){if(!await j(e,t))return;try{process.kill(e,"SIGTERM")}catch{return}for(let s=0;s<20;s++){await new Promise(i=>setTimeout(i,100));if(!await j(e,t))return}if(await j(e,t)){try{process.kill(e,"SIGKILL")}catch{}}}};import Ca from"net";import{chmodSync as La,rmSync as xa,appendFileSync as Ks,mkdirSync as Ua}from"fs";import{timingSafeEqual as ja}from"crypto";var Fa=["android"];var Qs=new Set(["setup","grow","shrink","acquire","release","action","lock","unlock","park","wake","reset","rebuild","delete","endLease","settings","deleteFarm","show","removeTemplate","report","extend","finish","stopRun","stopAll","bench","dropCopies"]);async function Vs(e={}){const t=e.home??F();const s=W(t);if(Buffer.byteLength(s.socket)>103){const o=new p("BAD_REQUEST",`The farm folder's path is too long for macOS (${Buffer.byteLength(s.socket)} of 103 bytes).`,"Set BUOY_FARM_HOME to a shorter folder.");try{Ua(t,{recursive:true});Ks(s.log,`${new Date().toISOString()} ${o.message} ${o.next}
|
|
369
|
+
`)}catch{}throw o}const i=await la(K,e.hostLabel??"node",t);if(!i)return null;const a=o=>{try{Ks(s.log,`${new Date().toISOString()} ${o}
|
|
370
|
+
`)}catch{}e.log?.(o)};try{return await Wa(e,t,i,a)}catch(o){i.release();throw o}}async function Wa(e,t,s,i){const a=W(t);const o=us(t);const n=new ws(t);const r=new Hs({host:e.host??new It,store:n,log:i,pollMs:e.pollMs,readyTimeoutMs:e.readyTimeoutMs,phoneBuilder:e.phoneBuilder});await r.recover();xa(a.socket,{force:true});const l=new Set;let c=Date.now();let d=false;let u=0;const h=Ca.createServer(k=>{l.add(k);k.on("close",()=>l.delete(k));k.on("error",()=>{});let f="";k.on("data",y=>{f+=y.toString("utf8");if(f.length>1024*1024)return k.destroy();let b;while((b=f.indexOf("\n"))>=0){const E=f.slice(0,b);f=f.slice(b+1);if(E.trim())void g(k,E)}})});const m=(k,f,y)=>{if(!k.destroyed)k.write(JSON.stringify({id:f,...y})+"\n")};async function g(k,f){c=Date.now();let y;try{y=JSON.parse(f)}catch{return m(k,-1,{ok:false,error:{code:"BAD_REQUEST",message:"Not JSON."}})}u++;try{if(!y||typeof y!=="object")throw new p("BAD_REQUEST","Use a request object.");if(!Ga(y.token,o))throw new p("UNAUTHORIZED","Bad farm token.");if(!y.client||typeof y.client.clientId!=="string"||!y.client.clientId.trim())throw new p("BAD_REQUEST","Each call needs a clientId.");if(d&&!["hello","status"].includes(y.method)){throw new p("HOST_BUSY","The farm is stopping.","Try again in a few seconds.",true)}if(y.v>K&&Qs.has(y.method)){throw new p("FARMD_VERSION",`This farm runs protocol ${K}; the client speaks ${y.v}.`,"Ask it to stop when idle (method shutdown), then start a newer farm.")}if(y.v<K&&Qs.has(y.method)){throw new p("READ_ONLY",`This client speaks farm protocol ${y.v}; the farm runs ${K}.`,"Update Buoy to change the farm.")}Ha(y);const b=await S(y);m(k,y.id,{ok:true,result:b})}catch(b){const E=b instanceof p?b.toJSON():{code:"HOST_ERROR",message:b.message,retryable:false};m(k,y?.id??-1,{ok:false,error:E})}finally{u--}}async function S(k){const f=k.params??{};const y=k.client;const b=E=>r.once(y.clientId,k.requestId,{m:k.method,q:f},E);switch(k.method){case"hello":return{protocol:K,features:Fa,pid:process.pid,host:s.info.host,farmId:r.state.farmId};case"status":return r.status({outside:f.outside});case"budget":return r.budget({ramGB:f.ramGB??0,diskGB:f.diskGB??0});case"setup":return b(()=>f.platform==="android"?r.createAndroidTemplate(f):r.createTemplate(f));case"grow":return b(async()=>(await r.grow(f.templateId,f.count)).map(E=>E.name));case"shrink":return b(()=>r.shrink(f.templateId,f.count));case"removeTemplate":return b(()=>r.removeTemplate(f.templateId));case"acquire":return b(async()=>{const{lease:E,run:G,sims:Je}=await r.acquire({...f,owner:y});return{runId:G.id,leaseId:E.id,epoch:E.epoch,deadlineAt:G.deadlineAt,capAt:G.capAt,sims:Je.map(Y=>"platform"in Y?{udid:Y.udid,name:Y.name,platform:Y.platform,serial:Y.serial,buoyDeviceId:Y.buoyDeviceId}:{udid:Y.udid,name:Y.name,buoyDeviceId:Y.buoyDeviceId,profile:Y.profile})}});case"renew":return r.renew(f.leaseId,f.epoch,y.clientId);case"release":return b(()=>r.release(f.leaseId,f.epoch,f.reset,y.clientId,f.keepRun===true));case"action":return b(()=>r.action(f.leaseId,f.epoch,f.udids,f.action,f.arg,y.clientId));case"lock":return r.lock(f.name,f.leaseId,f.epoch,f.waitMs??0,y.clientId);case"unlock":return r.unlock(f.name,f.leaseId,f.epoch,y.clientId);case"fairness":return r.fairness(f.leaseId,f.epoch,f.windows,y.clientId);case"dropCopies":return b(()=>r.dropCopies(f.leaseId,f.epoch,f.udids,y.clientId));case"bench":return r.copyBench(f.leaseId,f.epoch,f.udid,{seconds:f.seconds,launches:f.launches},y.clientId);case"park":return b(async()=>(await r.parkSim(f.udid,"asked"),"parked"));case"wake":return b(async()=>(await r.bootSim(f.udid)).power);case"reset":return b(async()=>(await r.fastReset(f.udid),"reset"));case"rebuild":return b(async()=>(await r.rebuildSim(f.udid)).udid);case"delete":return b(async()=>(await r.deleteSim(f.udid),"deleted"));case"endLease":{const E=r.state.leases[f.leaseId];if(!E)return{};if(E.owner.clientId!==y.clientId&&f.force!==true){throw new p("NOT_OWNED",`Lease ${f.leaseId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return b(()=>r.endLease(f.leaseId,`ended by ${y.label}`,"full",E.owner.clientId===y.clientId?"released":"user"))}case"report":return b(async()=>r.report(f,y.clientId));case"extend":return b(async()=>r.extend(f.runId,f.minutes,y.clientId));case"finish":return b(async()=>r.finish(f.runId,f.outcome,f.summary,y.clientId));case"stopRun":{const E=r.state.runs[f.runId];if(E&&E.owner.clientId!==y.clientId&&f.force!==true){throw new p("NOT_OWNED",`Run ${f.runId} belongs to ${E.owner.label}.`,"Pass force: true only after a person confirms.")}return b(async()=>r.stopRun(f.runId))}case"stopAll":if(f.force!==true)throw new p("BAD_REQUEST","stopAll needs force: true, after a person confirms.");return b(async()=>r.stopAll());case"settings":return r.setSettings(f);case"shot":return r.shot(f.udid);case"show":return r.show(f.udid);case"shutdown":if(f.ifIdle!==false&&(!r.idle()||u>1))return{stopping:false,reason:"busy"};void z("asked to stop");return{stopping:true};default:throw new p("BAD_REQUEST",`Unknown method ${k.method}.`)}}await new Promise((k,f)=>{h.once("error",f);h.listen(a.socket,()=>k())});La(a.socket,384);i(`farmd ${K} up, pid ${process.pid}, host ${e.hostLabel??"node"}`);const w=setInterval(()=>{void r.tick().catch(f=>i(`tick: ${f.message}`));const k=Date.now()-c;if(e.idleExitMs&&l.size===0&&k>e.idleExitMs){void(async()=>{await r.parkAllIdle();if(r.idle()&&l.size===0&&u===0&&Date.now()-c>e.idleExitMs)await z("idle")})()}},e.tickMs??5e3);w.unref?.();const v=setInterval(()=>void r.meter.tick().catch(k=>i(`meter: ${k.message}`)),1e3);v.unref?.();let _=null;function z(k,f=10*6e4){if(!_)_=te(k,f);return _}async function te(k,f){if(d)return;d=true;clearInterval(w);clearInterval(v);i(`farmd stopping: ${k}`);const y=Date.now()+f;while(u>0&&Date.now()<y)await new Promise(G=>setTimeout(G,200));const b=await r.drain(Math.max(0,y-Date.now()));let E=false;if(u>0||!b||tt()>0){n.save();r.fence();E=true;i(`farmd stopping with ${u} call(s) and ${tt()} child process(es) still running; killing them`);fi();const G=Date.now()+5e3;while(tt()>0&&Date.now()<G)await new Promise(Je=>setTimeout(Je,100))}await r.settleFiles();if(!E&&u===0){try{await r.parkAllIdle()}catch{}}for(const G of l)G.destroy();await new Promise(G=>h.close(()=>G()));if(!E){n.save();r.fence()}s.release();e.onExit?.(k)}return{engine:r,owner:s,stop:z}}function Ga(e,t){if(typeof e!=="string"||e.length!==t.length)return false;return ja(Buffer.from(e),Buffer.from(t))}function Ha(e){const t=n=>{throw new p("BAD_REQUEST",n)};const s=e.params??{};if(typeof s!=="object"||Array.isArray(s))t("Params must be an object.");const i=(n,r=false)=>{if(r&&s[n]===void 0)return;if(typeof s[n]!=="string"||!s[n].trim())t(`${n} must be text.`)};const a=n=>{if(s[n]!==void 0&&typeof s[n]!=="boolean")t(`${n} must be true or false.`)};const o=(n,r,l,c=false,d=false)=>{const u=s[n];if(d&&u===void 0)return;if(typeof u!=="number"||!Number.isFinite(u)||u<r||u>l||c&&!Number.isInteger(u))t(`${n} must be a number from ${r} to ${l}.`)};switch(e.method){case"status":a("outside");break;case"budget":o("ramGB",0,Number.MAX_VALUE,false,true);o("diskGB",0,Number.MAX_VALUE,false,true);break;case"setup":if(s.platform==="android"){i("apk");i("phoneId",true);i("launchUrl",true);if(s.reverse!==void 0&&(!Array.isArray(s.reverse)||s.reverse.some(n=>typeof n!=="string"||!/^\d{1,5}(:\d{1,5})?$/.test(n))))t('reverse must be a list like ["8081:8802"].');if(s.image!==void 0&&(typeof s.image!=="object"||!["aosp_atd","google_atd"].includes(s.image?.tag)))t("image tag must be aosp_atd or google_atd.");break}for(const n of["appPath","deviceType","runtime"])i(n);if(s.seedDir!=null)i("seedDir");i("name",true);i("launchUrl",true);a("warmLaunch");if(!["slim","stock"].includes(s.profile))t("Use slim or stock.");o("metroPort",1,65535,true);break;case"removeTemplate":i("templateId");break;case"grow":case"shrink":case"acquire":i("templateId");o("count",1,64,true);if(e.method==="acquire"){i("purpose");o("ttlMs",1,Number.MAX_VALUE,false,true);o("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);a("launch");a("allowPartial");i("goal",true);i("runId",true);i("kind",true);if(s.labels!==void 0&&(!Array.isArray(s.labels)||s.labels.some(n=>typeof n!=="string")))t("labels must be a list of text.");if(!Number.isInteger(e.client.pid)||e.client.pid<=0)t("The client needs a valid pid.")}break;case"release":if(s.reset!==void 0&&!["fast","full"].includes(s.reset))t("Use fast or full reset.");break;case"action":if(!Array.isArray(s.udids)||s.udids.some(n=>typeof n!=="string"||!n))t("udids must be a list of sim ids.");if(!["reset","rebuild","open-url","screenshot","launch","install"].includes(s.action))t("Unknown sim action.");i("arg",s.action!=="open-url"&&s.action!=="install");break;case"lock":case"unlock":i("name");if(!/^[a-z0-9-]{1,40}$/.test(s.name))t("Lock names are short lowercase words.");o("waitMs",0,Number.MAX_SAFE_INTEGER,false,true);break;case"fairness":if(!Array.isArray(s.windows)||s.windows.length>500||s.windows.some(n=>!n||typeof n.id!=="string"||!Number.isFinite(n.from)||!Number.isFinite(n.to)||n.to<n.from)){t("windows must be a list of { id, from, to } times in ms.")}break;case"endLease":i("leaseId");break;case"report":i("runId");i("udid",true);i("note",true);o("sequence",0,Number.MAX_SAFE_INTEGER,true,true);if(s.state!==void 0&&typeof s.state!=="string")t("state must be text.");break;case"extend":i("runId");o("minutes",1,24*60);break;case"dropCopies":if(!Array.isArray(s.udids)||!s.udids.length||s.udids.some(n=>typeof n!=="string"||!n))t("udids must be a list of copy ids.");break;case"bench":i("leaseId");i("udid");o("seconds",1,600,false,true);o("launches",0,20,true,true);break;case"finish":i("runId");i("summary",true);i("outcome",true);break;case"stopRun":i("runId");a("force");break;case"stopAll":a("force");break;case"shutdown":a("ifIdle");break}if(["renew","release","action","lock","unlock","fairness","dropCopies"].includes(e.method))i("leaseId")}import qa from"net";import{existsSync as za}from"fs";import{spawn as Ya}from"child_process";import{randomBytes as Js}from"crypto";function $t(e,t=["farmd"]){return()=>{const s=Ya(process.execPath,[...process.execArgv,e,...t],{detached:true,stdio:"ignore",env:it()});s.unref()}}var Xs=class{constructor(e){this.opts=e;this.sock=null;this.nextId=1;this.pending=new Map;this.buf="";this.renewals=new Map;this.reconnecting=null;this.features=[];this.askedOlderToStop=false;this.closed=false;this.home=e.home??F();this.clientIdValue=e.clientId??`c-${Js(5).toString("hex")}`}get clientId(){return this.owner?.clientId}require(e){if(!this.features.includes(e)){throw new p("FARMD_VERSION",`The farm running on this Mac is too old for ${e}.`,"Update Buoy Desktop, or quit it so a newer farm can start, then try again.")}}async connect(){this.token=us(this.home);this.owner={clientId:this.clientIdValue,pid:process.pid,pidStart:await pe(process.pid)??void 0,label:this.opts.label,...this.opts.person?{kind:"person"}:{},...this.opts.agent?{agent:this.opts.agent}:{}};if(await this.tryOpen())return this.checkVersion();const e=await $e(this.home);if(!await Oe(e)){if(!this.opts.launcher)throw new p("HOST_ERROR","The farm is not running.","Start it from Buoy Desktop or `pnpm farm status`.");this.opts.launcher()}const t=Date.now()+(this.opts.startTimeoutMs??2e4);while(Date.now()<t){await I(250);if(await this.tryOpen())return this.checkVersion()}throw new p("HOST_ERROR","The farm did not start in time.",`Check ${W(this.home).log}.`)}async checkVersion(){const e=await this.send("hello",void 0,{});this.features=e.features??[];const t=e.protocol<K||!this.features.includes("android")&&!this.askedOlderToStop;if(t&&this.opts.launcher){this.askedOlderToStop=true;const s=await this.send("shutdown",{ifIdle:true},{});if(s.stopping){this.close();await I(500);return this.connect()}}return this}tryOpen(){const e=W(this.home).socket;if(!za(e))return Promise.resolve(false);return new Promise(t=>{const s=qa.createConnection(e);s.once("connect",()=>{this.sock=s;s.on("data",i=>this.onData(i));s.on("close",()=>this.onClose());s.on("error",()=>{});t(true)});s.once("error",()=>t(false))})}onData(e){this.buf+=e.toString("utf8");let t;while((t=this.buf.indexOf("\n"))>=0){const s=this.buf.slice(0,t);this.buf=this.buf.slice(t+1);let i;try{i=JSON.parse(s)}catch{continue}const a=this.pending.get(i.id);if(!a)continue;this.pending.delete(i.id);clearTimeout(a.timer);if(i.ok)a.resolve(i.result);else{const o=i.error??{};a.reject(new p(o.code??"HOST_ERROR",o.message??"farm error",o.next,o.retryable))}}}onClose(){this.sock=null;for(const[,e]of this.pending){clearTimeout(e.timer);e.reject(new p("HOST_ERROR","Lost the connection to the farm.",void 0,true))}this.pending.clear()}reconnect(){if(!this.reconnecting){this.reconnecting=this.connect().then(()=>void 0).finally(()=>{this.reconnecting=null})}return this.reconnecting}async call(e,t,s={}){if(!this.sock&&!this.closed)await this.reconnect();return this.send(e,t,s)}send(e,t,s){if(!this.sock)return Promise.reject(new p("HOST_ERROR","Not connected to the farm.",void 0,true));const i=this.nextId++;const a={id:i,v:K,token:this.token,client:this.owner,method:e,params:t,requestId:s.requestId};return new Promise((o,n)=>{const r=setTimeout(()=>{this.pending.delete(i);n(new p("HOST_ERROR",`Farm call ${e} timed out.`,void 0,true))},s.timeoutMs??3e4);this.pending.set(i,{resolve:o,reject:n,timer:r});this.sock.write(JSON.stringify(a)+"\n")})}async acquire(e){const{requestId:t,...s}=e;const i=t??`acq-${Js(4).toString("hex")}`;const a=await this.call("acquire",s,{timeoutMs:15*6e4+(e.waitMs??0),requestId:i});if(this.renewals.has(a.leaseId)||this.opts.person)return a;const o=setInterval(()=>{this.call("renew",{leaseId:a.leaseId,epoch:a.epoch}).catch(n=>{if(["LEASE_EXPIRED","STALE_EPOCH","RUN_STOPPED","RUN_ENDED"].includes(n.code))this.stopRenew(a.leaseId)})},15e3);o.unref?.();this.renewals.set(a.leaseId,o);return a}stopRenew(e){const t=this.renewals.get(e);if(t)clearInterval(t);this.renewals.delete(e)}async release(e,t,s="fast",i=false){this.stopRenew(e);return this.call("release",{leaseId:e,epoch:t,reset:s,keepRun:i},{timeoutMs:6e4,requestId:`rel-${e}-${t}`})}close(){this.closed=true;for(const e of[...this.renewals.keys()])this.stopRenew(e);this.sock?.end();this.sock=null}};async function Ot(e){return new Xs(e).connect()}import Te from"path";import Ka from"os";import{existsSync as At,readdirSync as Qa,statSync as Va}from"fs";function Re(e=process.cwd()){let t=e;for(;;){if(At(Te.join(t,"buoy.tools.json")))return t;const s=Te.dirname(t);if(s===t)return null;t=s}}function qe(){const e=Te.join(Ka.homedir(),"Library/Developer/Xcode/DerivedData");let t=null;try{for(const s of Qa(e)){if(!s.startsWith("exampledevbuild-"))continue;const i=Te.join(e,s,"Build/Products/Debug-iphonesimulator/exampledevbuild.app");if(!At(i))continue;const a=Va(i).mtimeMs;if(!t||a>t.t)t={p:i,t:a}}}catch{}return t?.p??null}function Zs(e=Re()){return e?Te.join(e,"example-dev-build/artifacts/pool-seed"):null}async function Ja(e=Re()){const t=Zs(e);let s=[];let i=[];try{const a=JSON.parse(await N("xcrun",["simctl","list","runtimes","--json"],{timeoutMs:3e4}));s=(a.runtimes??[]).filter(n=>n.isAvailable!==false&&/^iOS/.test(n.name??"")).map(n=>({id:n.identifier,name:n.name}));const o=JSON.parse(await N("xcrun",["simctl","list","devicetypes","--json"],{timeoutMs:3e4}));i=(o.devicetypes??[]).filter(n=>/iPhone|iPad/.test(n.name??"")).map(n=>({id:n.identifier,name:n.name}))}catch{}return{appPath:qe(),seedDir:t&&At(t)?t:null,seedProblem:t?ke(t):"Could not find the repo's seed folder.",runtimes:s,deviceTypes:i,slimTurnsOff:ve.map(a=>({name:a.name,downside:a.downside}))}}import Xa from"path";var ei=[{app:"claude",name:"Claude",match:/(^|\/)claude$/i},{app:"codex",name:"Codex",match:/(^|\/)codex$/i},{app:"cursor",name:"Cursor",match:/cursor/i}];async function Za(e){const t=await M("ps",["-o","ppid=,comm=","-p",String(e)],{timeoutMs:3e3});const s=/^\s*(\d+)\s+(.+?)\s*$/.exec(t.stdout);return t.code===0&&s?{ppid:Number(s[1]),comm:s[2]}:null}async function eo(e){const t=await M("lsof",["-a","-p",String(e),"-d","cwd","-Fn"],{timeoutMs:3e3});const s=t.stdout.split("\n").find(i=>i.startsWith("n"));return s?s.slice(1):null}function to(e){if(!e)return void 0;return ei.find(t=>e.toLowerCase().includes(t.app))}async function so(e={}){const t=e.env??process.env;let s=e.pid??process.pid;let i=null;for(let d=0;d<5&&s>1;d++){const u=await Za(s).catch(()=>null);if(!u)break;const h=ei.find(m=>m.match.test(u.comm));if(d>0&&h){i={app:h,pid:s};break}s=u.ppid}const a=i?.app??to(e.clientName);const o=(i?await eo(i.pid).catch(()=>null):null)??process.cwd();const n=(await M("git",["-C",o,"rev-parse","--abbrev-ref","HEAD"],{timeoutMs:3e3}).catch(()=>null))?.stdout.trim();const r=t.CLAUDE_CODE_SESSION_ID?.trim();let l;let c="none";if(r&&(a?.app??"claude")==="claude"){l=`claude:${r}`;c="claude-session"}else if(i){l=`${i.app.app}:${i.pid}:${await pe(i.pid)??""}`;c="parent-process"}return{app:a?.app??"other",appName:a?.name??(e.clientName||"Agent"),chatKey:l,chatKeySource:c,project:Xa.basename(o)||void 0,projectPath:o,branch:n&&n!=="HEAD"?n:void 0,clientName:e.clientName}}function io(e){return e.project?`${e.appName} \xB7 ${e.project}`:e.appName}import ti from"path";import{existsSync as ao}from"fs";import oo from"os";import{execFile as no,spawn as ro}from"child_process";import{createHash as lo}from"crypto";import{promises as q}from"fs";import ze from"os";import U from"path";import{promisify as co}from"util";var re=co(no);var Ye="gg.buoy.phoneprobe";var Ke=1024*1024;function po(e){const t=e.match(/BUOY_PROBE physicalMemory=(\d+) cpus=(\d+) footprint=(\d+) available=(\d+)/);if(!t)return null;const[s,i,a,o]=t.slice(1).map(Number);const n=a+o;if(!s||!i||o<=0||n>s)return null;return{ramMB:Math.floor(s/Ke),cpus:i,limitMB:Math.floor(n/Ke)}}function si(e,t){return e.match(new RegExp(`<key>${t}</key>\\s*<string>([^<]*)</string>`))?.[1]}function Dt(e,t,s){const i=e.match(new RegExp(`<key>${t}</key>\\s*<array>([\\s\\S]*?)</array>`))?.[1]??"";return[...i.matchAll(new RegExp(`<${s}>([\\s\\S]*?)</${s}>`,"g"))].map(a=>a[1].replace(/\s+/g,""))}function uo(e,t){const s=t.match(/<key>Entitlements<\/key>\s*<dict>([\s\S]*?)<\/dict>/)?.[1]??"";const i=si(s,"application-identifier");const a=Dt(t,"TeamIdentifier","string")[0];const o=t.match(/<key>ExpirationDate<\/key>\s*<date>([^<]+)<\/date>/)?.[1];if(!i||!a||!o)return null;return{file:e,name:si(t,"Name")??U.basename(e),team:a,appId:i,expires:new Date(o),devices:Dt(t,"ProvisionedDevices","string"),allDevices:/<key>ProvisionsAllDevices<\/key>\s*<true\/>/.test(t),getTaskAllow:/<key>get-task-allow<\/key>\s*<true\/>/.test(s),certSha1s:Dt(t,"DeveloperCertificates","data").map(n=>lo("sha1").update(Buffer.from(n,"base64")).digest("hex").toUpperCase())}}function ho(e,t,s,i=new Date){const a=e.filter(n=>n.getTaskAllow&&n.expires>i&&(n.allDevices||n.devices.includes(t))&&(n.appId===`${n.team}.*`||n.appId===`${n.team}.${Ye}`)&&n.certSha1s.some(r=>s.has(r)));a.sort((n,r)=>Number(r.appId.endsWith(".*"))-Number(n.appId.endsWith(".*"))||r.expires.getTime()-n.expires.getTime());const o=a[0];return o?{profile:o,identity:o.certSha1s.find(n=>s.has(n))}:null}async function mo(){const e=[U.join(ze.homedir(),"Library/Developer/Xcode/UserData/Provisioning Profiles"),U.join(ze.homedir(),"Library/MobileDevice/Provisioning Profiles")];const t=[];for(const s of e){let i=[];try{i=(await q.readdir(s)).filter(a=>a.endsWith(".mobileprovision"))}catch{continue}for(const a of i){const o=U.join(s,a);try{const{stdout:n}=await re("security",["cms","-D","-i",o],{timeout:15e3,maxBuffer:16*Ke});const r=uo(o,n);if(r)t.push(r)}catch{}}}return t}async function fo(){const{stdout:e}=await re("security",["find-identity","-v","-p","codesigning"],{timeout:15e3});return new Set([...e.matchAll(/^\s*\d+\)\s+([0-9A-F]{40})\s/gm)].map(t=>t[1]))}async function wo(){const e=await q.mkdtemp(U.join(ze.tmpdir(),"buoy-phones-"));try{const t=U.join(e,"devices.json");await re("xcrun",["devicectl","list","devices","--json-output",t],{timeout:3e4});const s=JSON.parse(await q.readFile(t,"utf8"))?.result?.devices??[];return s.filter(i=>i.hardwareProperties?.reality==="physical"&&i.hardwareProperties?.deviceType==="iPhone"&&i.connectionProperties?.pairingState==="paired").map(i=>({udid:i.hardwareProperties.udid,name:i.hardwareProperties.marketingName??"iPhone",productType:i.hardwareProperties.productType,ios:i.deviceProperties?.osVersionNumber??""}))}finally{await q.rm(e,{recursive:true,force:true}).catch(()=>void 0)}}function go(e,t,s){const i=Object.keys(e).find(o=>e[o].productType===t);if(i)return i;const a=o=>o.toLowerCase().replace(/[()]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");return`${a(s)}-${a(t)}`.slice(0,64).replace(/-$/,"")}async function yo(e,t,s,i){const a=U.join(e,"PhoneProbe.app");await q.mkdir(a,{recursive:true});const o=U.join(e,"PhoneProbe.m");await q.writeFile(o,Ma);await re("xcrun",["--sdk","iphoneos","clang","-arch","arm64","-miphoneos-version-min=13.0","-fobjc-arc","-O2","-framework","Foundation","-framework","UIKit",o,"-o",U.join(a,"PhoneProbe")],{timeout:18e4,maxBuffer:8*Ke,env:{...process.env,TMPDIR:e}});const n=Ye;await q.writeFile(U.join(a,"Info.plist"),`<?xml version="1.0" encoding="UTF-8"?>
|
|
371
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
372
|
+
<plist version="1.0"><dict>
|
|
373
|
+
<key>CFBundleIdentifier</key><string>${n}</string>
|
|
374
|
+
<key>CFBundleExecutable</key><string>PhoneProbe</string>
|
|
375
|
+
<key>CFBundleName</key><string>Buoy Probe</string>
|
|
376
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
377
|
+
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
378
|
+
<key>CFBundleVersion</key><string>1</string>
|
|
379
|
+
<key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
|
|
380
|
+
<key>MinimumOSVersion</key><string>13.0</string>
|
|
381
|
+
<key>UIDeviceFamily</key><array><integer>1</integer></array>
|
|
382
|
+
<key>UIApplicationSceneManifest</key><dict><key>UIApplicationSupportsMultipleScenes</key><false/></dict>
|
|
383
|
+
<key>UILaunchScreen</key><dict/>
|
|
384
|
+
<key>UIRequiredDeviceCapabilities</key><array><string>arm64</string></array>
|
|
385
|
+
</dict></plist>
|
|
386
|
+
`);await q.copyFile(s,U.join(a,"embedded.mobileprovision"));const r=U.join(e,"probe.entitlements");await q.writeFile(r,`<?xml version="1.0" encoding="UTF-8"?>
|
|
387
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
388
|
+
<plist version="1.0"><dict>
|
|
389
|
+
<key>application-identifier</key><string>${t}.${n}</string>
|
|
390
|
+
<key>com.apple.developer.team-identifier</key><string>${t}</string>
|
|
391
|
+
<key>get-task-allow</key><true/>
|
|
392
|
+
</dict></plist>
|
|
393
|
+
`);await re("codesign",["-f","-s",i,"--entitlements",r,a],{timeout:6e4});return a}function vo(e,t=45e3){return new Promise(s=>{const i=ro("xcrun",["devicectl","device","process","launch","--device",e,"--terminate-existing","--console",Ye]);let a="";let o=false;const n=()=>{if(o)return;o=true;clearTimeout(r);i.kill("SIGTERM");s(a)};const r=setTimeout(n,t);const l=c=>{a+=c.toString();if(/BUOY_PROBE .*available=\d+/.test(a))setTimeout(n,300)};i.stdout.on("data",l);i.stderr.on("data",l);i.on("close",n);i.on("error",n)})}async function ii(e={}){const t=e.log??(()=>{});const s=(await wo()).filter(n=>!e.device||n.udid===e.device);if(s.length===0)throw new Error(e.device?`No paired iPhone ${e.device}.`:"No paired iPhone. Plug one in, unlock it and trust this Mac.");if(s.length>1)throw new Error(`More than one iPhone. Pick one with --device:
|
|
394
|
+
${s.map(n=>` ${n.udid} ${n.name}`).join("\n")}`);const i=s[0];t(`Measuring ${i.name} (${i.productType}, iOS ${i.ios})...`);const a=ho(await mo(),i.udid,await fo());if(!a){throw new Error(`No provisioning profile on this Mac can install the measuring app on this iPhone. It needs a development profile for any app ("TEAM.*"), which comes with a paid Apple Developer team: in Xcode, sign in with that team and run any app with automatic signing on this phone once. Free Apple accounts only get profiles for one app, and they can't be used here.`)}const o=await q.mkdtemp(U.join(ze.tmpdir(),"buoy-probe-"));try{const n=await yo(o,a.profile.team,a.profile.file,a.identity);await re("xcrun",["devicectl","device","install","app","--device",i.udid,n],{timeout:12e4});let r=null;try{const m=await vo(i.udid);r=po(m);if(!r){const g=/locked/i.test(m)?" The phone looks locked: unlock it and try again.":"";throw new Error(`Could not read the phone.${g} Output: ${m.trim().slice(-300)}`)}}finally{await re("xcrun",["devicectl","device","uninstall","app","--device",i.udid,Ye],{timeout:6e4}).catch(()=>void 0)}const l=he();const c=go(l,i.productType,i.name);const d=l[c];const u=d?.warnSource?.startsWith("measured")&&d.warnMB<r.limitMB;const h={name:d?.name??i.name,productType:i.productType,ramMB:r.ramMB,cpus:r.cpus,limitMB:r.limitMB,warnMB:u?d.warnMB:Math.round(r.limitMB*.8),warnSource:u?d.warnSource:"80% of the limit (not measured)",ios:i.ios,measuredAt:new Date().toISOString().slice(0,10)};await Ds(c,h);return{id:c,phone:h,previous:d}}finally{await q.rm(o,{recursive:true,force:true}).catch(()=>void 0)}}import{copyFile as So}from"fs/promises";import Tt from"os";import ge from"path";var Rt=e=>`Usage: ${e} android <command>
|
|
395
|
+
|
|
396
|
+
Many slim Android phones on one computer. Each copy starts from a saved
|
|
397
|
+
snapshot in seconds and draws with your Mac's GPU.
|
|
398
|
+
|
|
399
|
+
setup [--accept-license] [--sdk auto|buoy|system] [--image aosp_atd|google_atd]
|
|
400
|
+
Get the emulator, adb and the slim image.
|
|
401
|
+
Uses your Android SDK when it already has them.
|
|
402
|
+
--sdk buoy downloads a pinned copy (about 1.2 GB)
|
|
403
|
+
template --apk <app.apk> [--phone <id>] [--image aosp_atd|google_atd] [--reverse 8081[:8810]]
|
|
404
|
+
Make a template: boot once, install and compile
|
|
405
|
+
the app, save the snapshot copies start from.
|
|
406
|
+
--reverse lets a dev build load from Metro first
|
|
407
|
+
up [n] [--template <id>] [--reverse 8081[:8810]] [--launch]
|
|
408
|
+
Start n copies (default 1). They stay up until
|
|
409
|
+
down, Stop in Buoy Desktop, or 8 hours.
|
|
410
|
+
--reverse maps a copy's port to your computer's
|
|
411
|
+
(for Metro). Repeat it for more ports
|
|
412
|
+
list [--json] Templates and running copies
|
|
413
|
+
down <serial ...> | --all Close your copies (agents' copies stop in Desktop)
|
|
414
|
+
launch <serial> [--url <link>]
|
|
415
|
+
Open the app cold and print its start time
|
|
416
|
+
install <apk> <serial ...> | --all
|
|
417
|
+
Put a new build on running copies
|
|
418
|
+
shot <serial> <file.png> Save a screenshot
|
|
419
|
+
bench <serial> [--seconds 15]
|
|
420
|
+
App start time, frame times and memory
|
|
421
|
+
adb <serial> <args...> Run adb on the farm's own adb server
|
|
422
|
+
share <serial> Also show a copy on your normal adb server, so
|
|
423
|
+
Android Studio, Detox or Buoy's MCP see it
|
|
424
|
+
remove-template <id> Delete a template (no copies may run)
|
|
425
|
+
phone list Phones a copy can act like
|
|
426
|
+
phone measure [--serial <usb serial>]
|
|
427
|
+
Read the Android phone plugged in and save it
|
|
428
|
+
doctor Check the SDK, GPU mode, memory and disk
|
|
429
|
+
|
|
430
|
+
Copies run in the Sim Farm service, so Buoy Desktop's Sims panel and the
|
|
431
|
+
Buoy MCP see them too. The farm runs its own adb server on port
|
|
432
|
+
${ee}, so other tools can't drop its copies. To use plain adb with them:
|
|
433
|
+
ANDROID_ADB_SERVER_PORT=${ee} adb devices
|
|
434
|
+
`;function V(e,t){const s=e.indexOf(`--${t}`);if(s<0)return void 0;const i=e[s+1];if(!i||i.startsWith("--"))throw new p("BAD_REQUEST",`--${t} needs a value.`);return i}function ai(e,t){const s=[];e.forEach((i,a)=>{if(i===`--${t}`){const o=e[a+1];if(!o||o.startsWith("--"))throw new p("BAD_REQUEST",`--${t} needs a value.`);s.push(o)}});return s}function ye(e,t){const s=[];for(let i=0;i<e.length;i++){if(e[i].startsWith("--")){if(t.includes(e[i].slice(2)))i++;continue}s.push(e[i])}return s}function Nt(e){const t=V(e,"image")??J.tag;if(t!=="aosp_atd"&&t!=="google_atd")throw new p("BAD_REQUEST","Use --image aosp_atd or google_atd.");return{...J,tag:t}}function oi(e){const t=V(e,"sdk")??"auto";if(t!=="auto"&&t!=="buoy"&&t!=="system")throw new p("BAD_REQUEST","Use --sdk auto, buoy or system.");return t}function Pt(e){const t=e.usable?"ready":e.note??"not ready";const s=e.copyReadyMs?`, copies ready in ${(e.copyReadyMs/1e3).toFixed(1)} s`:"";const i=e.usable&&e.roomFor!=null?`, room for ${e.roomFor} more (${e.costGB} GB each)`:"";return`Template ${e.id}: ${e.packageName||ge.basename(e.apk)} as ${e.phone.name} (${e.image.tag}, Android ${e.image.api}): ${t}${s}${i}`}var Qe=`p-${Tt.userInfo().username}`;async function le(e){const t=await Ot({label:`You (${Tt.userInfo().username})`,clientId:Qe,person:true,launcher:e?$t(e,["farmd"]):void 0});t.require("android");return t}function ko(e,t){const s=e.android?.copies.find(i=>i.serial===t||i.udid===t);if(!s)throw new p("NOT_FOUND",`${t} is not a running farm copy.`,"See them with: farm android list");return{copy:s,lease:e.leases.find(i=>i.id===s.leaseId)}}function Ct(e,t){const{copy:s,lease:i}=ko(e,t);if(!i||i.owner.clientId!==Qe){throw new p("NOT_OWNED",`${s.serial} belongs to ${i?.owner.label??"no one"}.`,"Stop that run in Buoy Desktop's Sims panel.")}return{copy:s,lease:i}}async function ni(e,t,s){const[i,...a]=e;const o=l=>console.log(l);const n=Ae();const r=async l=>l.call("status");switch(i){case void 0:case"help":case"--help":return o(Rt(t));case"setup":{const l=Nt(a);const c=oi(a);const d=Ee(n,l,c);if(d?.hasImage&&c!=="buoy"){o(`Ready. Using the ${d.source==="buoy"?"farm's own":"existing"} Android SDK at ${d.root} (emulator ${d.emulatorVersion}).`);return o(`Next: ${t} android template --apk <your app.apk>`)}if(d?.hasImage){return o(`Ready. The farm's own SDK is at ${d.root} (emulator ${d.emulatorVersion}).`)}const u=await Hi(l);if(!a.includes("--accept-license")){for(const[g,S]of Object.entries(u.licenses))o(`---- ${g} ----
|
|
435
|
+
${S}
|
|
436
|
+
`);o(`The farm will download:
|
|
437
|
+
${u.archives.map(g=>` ${g.name} (${Math.round(g.size/1024**2)} MB)`).join("\n")}`);return o(`
|
|
438
|
+
These come from Google under the licenses above. To accept them and download, run:
|
|
439
|
+
${t} android setup --sdk buoy --accept-license`)}const h=Date.now();const m=await qi(n,u,g=>console.error(g));o(`Ready in ${((Date.now()-h)/1e3).toFixed(1)} s. Emulator ${m.emulatorVersion} at ${m.root}.`);return o(`Next: ${t} android template --apk <your app.apk>`)}case"template":{const l=V(a,"apk");if(!l)throw new p("BAD_REQUEST","template needs --apk <app.apk>.");const c=await le(s);const d=Date.now();const u=new Set((await r(c)).recentEvents.map(m=>m.at+m.message));const h=setInterval(()=>void r(c).then(m=>{for(const g of m.recentEvents){if(u.has(g.at+g.message))continue;u.add(g.at+g.message);if(g.kind==="template-progress")console.error(g.message)}}).catch(()=>{}),2e3);try{const m=await c.call("setup",{platform:"android",apk:ge.resolve(l),phoneId:V(a,"phone"),image:Nt(a),reverse:ai(a,"reverse"),launchUrl:V(a,"launch-url")},{timeoutMs:30*6e4,requestId:`tpl-${Date.now()}`});o(Pt(m));o(`Built in ${((Date.now()-d)/1e3).toFixed(0)} s. Start copies with: ${t} android up 2`)}finally{clearInterval(h);c.close()}return}case"up":{const[l]=ye(a,["template","reverse"]);const c=Number(l??1);if(!Number.isInteger(c)||c<1)throw new p("BAD_REQUEST","up needs a count of 1 or more.");if(ai(a,"reverse").length)throw new p("BAD_REQUEST","Ports are set on the template now.",`Make it with: ${t} android template --apk <app.apk> --reverse 8081:8802`);const d=await le(s);try{const u=await r(d);const h=V(a,"template")?u.android?.templates.find(w=>w.id===V(a,"template")):u.android?.templates.filter(w=>w.usable).sort((w,v)=>w.createdAt.localeCompare(v.createdAt)).pop();if(!h)throw new p("NO_TEMPLATE","No Android template yet.",`Make one with: ${t} android template --apk <app.apk>`);const m=Date.now();const g=await d.acquire({templateId:h.id,count:c,purpose:"farm android up",goal:`${c} Android cop${c===1?"y":"ies"} from the CLI`,ttlMs:24*36e5,launch:a.includes("--launch")});const S=await r(d);for(const w of g.sims){const v=S.android?.copies.find(_=>_.udid===w.udid);o(`${w.serial} ${w.name} ready in ${((v?.readyMs??0)/1e3).toFixed(1)} s (${v?.bootedFrom??"?"})`)}return o(`${g.sims.length} cop${g.sims.length===1?"y":"ies"} up in ${((Date.now()-m)/1e3).toFixed(1)} s. They stay up until: ${t} android down --all`)}finally{d.close()}}case"list":{const l=await le(s);try{const c=await r(l);const d=c.android?.templates??[];const u=c.android?.copies??[];if(a.includes("--json"))return o(JSON.stringify({adbPort:ee,templates:d,copies:u},null,2));if(!d.length)o(`No templates. Make one with: ${t} android template --apk <app.apk>`);for(const h of d)o(Pt(h));for(const h of u)o(` ${h.serial} ${h.name} ${h.state} ${h.owner?`used by ${h.owner}`:"free"} template ${h.templateId}`);return o(`adb server: ANDROID_ADB_SERVER_PORT=${ee}`)}finally{l.close()}}case"down":{const l=await le(s);try{const c=await r(l);const d=a.includes("--all");const u=d?(c.android?.copies??[]).filter(S=>c.leases.find(w=>w.id===S.leaseId)?.owner.clientId===Qe).map(S=>S.serial):ye(a,[]);if(!d&&!u.length)throw new p("BAD_REQUEST","down needs serials or --all.");const h=new Map;for(const S of u){const{copy:w,lease:v}=Ct(c,S);const _=h.get(v.id)??{lease:v,copies:[]};_.copies.push(w);h.set(v.id,_)}const m=[];for(const{lease:S,copies:w}of h.values()){await l.call("dropCopies",{leaseId:S.id,epoch:S.epoch,udids:w.map(v=>v.udid)},{timeoutMs:12e4,requestId:`drop-${S.id}-${w.map(v=>v.udid).join(",")}`});m.push(...w.map(v=>v.serial))}await Promise.all(m.map(S=>M(Lt(),["disconnect",`127.0.0.1:${Number(S.split("-")[1])+1}`],{timeoutMs:1e4})));const g=(c.android?.copies.length??0)-u.length;o(m.length?`Closed ${m.join(", ")}.`:"You had no copies running.");if(d&&g>0)o(`${g} other cop${g===1?"y is":"ies are"} in use by agents. Stop those runs in Buoy Desktop.`);return}finally{l.close()}}case"launch":case"shot":case"install":case"bench":{const l=await le(s);try{const c=await r(l);const d=ye(a,["url","seconds"]);if(i==="install"){const[v,..._]=d;if(!v)throw new p("BAD_REQUEST","install needs an APK.");const z=a.includes("--all")?(c.android?.copies??[]).filter(k=>c.leases.find(f=>f.id===k.leaseId)?.owner.clientId===Qe).map(k=>k.serial):_;if(!z.length)throw new p("BAD_REQUEST","install needs serials or --all.");const te=Date.now();for(const k of z){const{copy:f,lease:y}=Ct(c,k);await l.call("action",{leaseId:y.id,epoch:y.epoch,udids:[f.udid],action:"install",arg:ge.resolve(v)},{timeoutMs:20*6e4})}return o(`Installed on ${z.length} cop${z.length===1?"y":"ies"} in ${((Date.now()-te)/1e3).toFixed(1)} s.`)}const[u,h]=d;if(!u)throw new p("BAD_REQUEST",`${i} needs a serial.`);const{copy:m,lease:g}=Ct(c,u);const S=(v,_)=>l.call("action",{leaseId:g.id,epoch:g.epoch,udids:[m.udid],action:v,arg:_},{timeoutMs:5*6e4});if(i==="launch"){const v=(await S("launch",V(a,"url")))[m.udid];return o(typeof v==="object"&&v.startMs!=null?`Started in ${v.startMs} ms.`:"Launched.")}if(i==="shot"){if(!h)throw new p("BAD_REQUEST","shot needs a serial and a file.");const v=(await S("screenshot"))[m.udid];if(!v)throw new p("HOST_ERROR",`No screenshot from ${m.serial}.`);await So(v,ge.resolve(h));return o(`Saved ${ge.resolve(h)}.`)}const w=await l.call("bench",{leaseId:g.id,epoch:g.epoch,udid:m.udid,seconds:Number(V(a,"seconds")??15)},{timeoutMs:10*6e4});if(a.includes("--json"))return o(JSON.stringify(w,null,2));o(`App start: ${w.appStartMs??"?"} ms (runs: ${w.appStarts.join(", ")})`);o(`Frames: ${w.frames.frames}, median ${w.frames.p50??"?"} ms, 90% ${w.frames.p90??"?"} ms, 99% ${w.frames.p99??"?"} ms, janky ${w.frames.jankyPct??"?"}%`);return o(`App memory: ${w.appMemoryMB} MB. Mac memory for this copy: ${w.macMemoryMB??"?"} MB. Mac CPU: ${w.macCpuPct??"?"}%`)}finally{l.close()}}case"adb":{const[l,...c]=a;if(!l)throw new p("BAD_REQUEST","adb needs a serial.");const d=vt(l);if(!d)throw new p("NOT_FOUND",`${l} is not a running farm copy.`);const u=await M(d.adb,["-P",String(d.port),"-s",l,...c],{timeoutMs:10*6e4});process.stdout.write(u.stdout);process.stderr.write(u.stderr);if(u.code)process.exitCode=u.code;return}case"share":{const[l]=ye(a,[]);if(!l||!vt(l))throw new p("BAD_REQUEST",`${l??"That"} is not a running farm copy.`);const c=Number(l.split("-")[1])+1;const d=await M(Lt(),["connect",`127.0.0.1:${c}`],{timeoutMs:2e4});if(!/connected to/.test(d.stdout))throw new p("HOST_ERROR",`adb connect failed: ${(d.stdout+d.stderr).trim()}`);return o(`${l} is now also 127.0.0.1:${c} on your normal adb server.`)}case"remove-template":{const[l]=ye(a,[]);if(!l)throw new p("BAD_REQUEST","remove-template needs a template id.");const c=await le(s);try{await c.call("removeTemplate",{templateId:l},{timeoutMs:12e4,requestId:`rmt-${l}`})}finally{c.close()}return o(`Removed template ${l}.`)}case"phone":{const[l]=ye(a,["serial"]);if(!l||l==="list"){for(const[c,d]of Object.entries(ut(n))){o(`${c}${c===pt?" (default)":""}: ${d.name}, ${d.ramMB} MB RAM, ${d.cores} cores, ${d.width}x${d.height} at ${d.density} dpi, heap ${d.heapGrowthMB} MB (large ${d.heapMB} MB), ${d.source}`)}return}if(l==="measure"){const c=Lt();const d=V(a,"serial");const u=d?["-s",d]:["-d"];const h=async w=>{const v=await M(c,[...u,"shell",w],{timeoutMs:2e4});if(v.code!==0)throw new p("HOST_ERROR",`Could not read the phone: ${(v.stderr||v.stdout).trim().slice(0,200)}`,"Plug in one Android phone with USB debugging on, or pass --serial.");return v.stdout};const m={props:await h("getprop"),meminfo:await h("cat /proc/meminfo"),nproc:await h("nproc"),wmSize:await h("wm size"),wmDensity:await h("wm density")};const{id:g,phone:S}=Ji(m,`measured ${new Date().toISOString().slice(0,10)}`);await Qi(n,g,S);return o(`Saved "${g}": ${S.ramMB} MB RAM, ${S.cores} cores, ${S.width}x${S.height} at ${S.density} dpi, heap ${S.heapGrowthMB} MB (large ${S.heapMB} MB).
|
|
440
|
+
Use it with: ${t} android template --apk <app.apk> --phone ${g}`)}throw new p("BAD_REQUEST",`Unknown phone command ${l}.`,"Try: phone list, phone measure")}case"doctor":{const l=Nt(a);const c=Ee(n,l,oi(a));const d=await ha();const u=rt().map(m=>_e(m,"system",l)).filter(Boolean);let h;try{const m=await le(s);h=(await r(m)).android;m.close()}catch(m){h=`farm not reachable: ${m.message}`}o(JSON.stringify({sdk:c?{root:c.root,source:c.source,emulator:c.emulatorVersion,slimImage:c.hasImage}:"none yet: run android setup",otherSdks:u.map(m=>({root:m.root,emulator:m.emulatorVersion,slimImage:m.hasImage})),adbServerPort:ee,avdFolder:ge.join(n,"avd"),gpu:"host (Metal on a Mac); copies never fall back to software drawing",freeMemoryGB:Number(d.availableGB.toFixed(1)),freeDiskGB:Math.floor(d.freeDiskGB),cpuCores:Tt.cpus().length,templates:typeof h==="string"?h:h?.templates.map(Pt),copies:typeof h==="string"?[]:h?.copies.map(m=>m.serial)},null,2));return}default:o(Rt(t));process.exitCode=2}}function Lt(){for(const e of rt()){const t=_e(e,"system");if(t)return t.adb}return"adb"}var Ve=e=>`Usage: ${e} <command>
|
|
441
|
+
|
|
442
|
+
status [--outside] Pool, budgets, sims, leases, recent events
|
|
443
|
+
setup [options] Make a template (the sim clones are made from)
|
|
444
|
+
--app <path.app> App build (default: example-dev-build Debug build)
|
|
445
|
+
--no-seed Skip the seed
|
|
446
|
+
--launch-url <url> Open this URL after each app launch. iOS asks
|
|
447
|
+
"Open in \u2026?" for a custom scheme, so a dev-client
|
|
448
|
+
app is better seeded with Preferences/ (it reopens
|
|
449
|
+
its last Metro URL by itself)
|
|
450
|
+
--seed <dir> Seed folder (default: example-dev-build/artifacts/pool-seed)
|
|
451
|
+
--device <type id> Default: iPhone 17 Pro
|
|
452
|
+
--runtime <runtime id> Default: iOS 26.3
|
|
453
|
+
--profile slim|stock Default: slim
|
|
454
|
+
--metro <port> Default: 8802
|
|
455
|
+
--dry-run Show what it would do and what it costs
|
|
456
|
+
grow <n> [--template id] Add n sims
|
|
457
|
+
shrink <n> [--template id] Remove n idle sims
|
|
458
|
+
remove-template <id> Delete a template and all its sims (none may be in use)
|
|
459
|
+
acquire <n> [--template id] [--purpose text] [--launch]
|
|
460
|
+
Take n sims and hold them until Ctrl-C
|
|
461
|
+
park|wake|reset|rebuild|delete <udid>
|
|
462
|
+
end-lease <leaseId> Force-end a lease (its sims are rebuilt)
|
|
463
|
+
settings [key=value ...] Show or change settings
|
|
464
|
+
phone list Phones a sim can act like (RAM, CPUs, memory limit)
|
|
465
|
+
phone use <id|off> Launch apps as this phone (default: iphone-se-2nd-gen)
|
|
466
|
+
phone measure [--device <udid>]
|
|
467
|
+
Read the iPhone plugged into this Mac and add it
|
|
468
|
+
doctor Check Xcode, disk, memory, the farm service
|
|
469
|
+
android <command> Slim Android phones from one snapshot
|
|
470
|
+
(see: android help)
|
|
471
|
+
stop [--now] Stop the farm service and park free sims
|
|
472
|
+
--now Stop even if leases are still held
|
|
473
|
+
farmd Run the farm service in this process
|
|
474
|
+
`;var jn=Ve("pnpm farm");function C(e,t){const s=e.indexOf(`--${t}`);if(s<0)return void 0;const i=e[s+1];if(!i||i.startsWith("--"))throw new p("BAD_REQUEST",`--${t} needs a value.`);return i}async function bo(e){return Ot({label:`CLI (${oo.userInfo().username})`,launcher:$t(e,["farmd"])})}function $(e){console.log(typeof e==="string"?e:JSON.stringify(e,null,2))}async function ri(e,t,s){if(s)return s;const i=await e.call("status");const a=i.templates.filter(o=>o.usable);if(a.length===0)throw new p("NO_TEMPLATE","No template yet.",`Run: ${t} setup`);return a[a.length-1].id}function Mo(e){const t=[`Pool ${e.counts.pool} \xB7 awake ${e.counts.awake} \xB7 in use ${e.counts.leased} \xB7 room for about ${e.budget.roomForMoreSims} more`,`Memory ${e.budget.availableMemGB} GB free (${e.budget.memPressure}) \xB7 disk ${e.budget.freeDiskGB} GB free \xB7 load ${e.budget.loadAvg1}/${e.budget.cpuCount}`];for(const s of e.templates){t.push(`Template ${s.id} ${s.name}: ${s.usable?"ready":s.note??"not ready"} (${s.profile}, ${s.runtime.split(".").pop()})`);t.push(` seed: ${s.seedDir??"none"} \xB7 launch URL: ${s.launchUrl??"none"}`)}for(const s of e.sims){t.push(` ${s.name.padEnd(14)} ${s.power.padEnd(8)} ${s.health.padEnd(7)} ${s.profile.padEnd(7)} ${s.owner?`in use by ${s.owner}`:""} ${s.udid}`);const i=s.phone;if(i?.status==="on"){t.push(` as ${i.phoneName}${i.peakMB?`, peak ${i.peakMB} MB`:""}${i.overLimit?", WENT PAST THE PHONE'S LIMIT":""}${i.ended?` (closed: ${i.ended})`:""}`)}else if(i?.status==="fallback"){t.push(` plain sim app: ${i.note??"the phone profile did not load"}`)}}for(const s of e.orphans??[])t.push(` ${s.name} ${s.udid}: held after crash`);for(const s of e.recentEvents.slice(-8))t.push(` \xB7 ${new Date(s.at).toLocaleTimeString(void 0,{hour12:false})} ${s.message}`);if(e.outsideSims)t.push(`Other sims (never touched): ${e.outsideSims.length}`);return t.join("\n")}async function _o(e,{prog:t,entry:s}){const[i,...a]=e;if(!i||i==="help"||i==="--help")return $(Ve(t));if(i==="android")return ni(a,t,s);Eo(i,a);if(i==="farmd"){const n=await Vs({hostLabel:"node",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!n){console.error("Another farm service is already running.");process.exit(0)}for(const r of["SIGTERM","SIGINT"])process.on(r,()=>void n.stop(r));return}if(i==="phone"&&a[0]!=="use"){const n=a[0];if(n==="list"||!n){const r=he();for(const[l,c]of Object.entries(r)){$(`${l}: ${c.name} (${c.productType}), ${c.ramMB} MB RAM, ${c.cpus} CPUs, app limit ${c.limitMB} MB, first warning ${c.warnMB} MB, iOS ${c.ios}, measured ${c.measuredAt}`)}return}if(n==="measure"){const r=await ii({device:C(a,"device"),log:l=>console.log(l)});$(`Saved "${r.id}": ${r.phone.ramMB} MB RAM, ${r.phone.cpus} CPUs, app limit ${r.phone.limitMB} MB, first warning ${r.phone.warnMB} MB.`);if(r.previous&&r.previous.limitMB!==r.phone.limitMB)$(`(The limit was ${r.previous.limitMB} MB before.)`);$(`Use it with: ${t} phone use ${r.id}`);return}throw new p("BAD_REQUEST",`Unknown phone command ${n}.`,"Try: phone list, phone use <id|off>, phone measure")}if(i==="doctor"){const n=new It;const r=await n.hostStats();const l=await $e();const c=await n.listDevices().catch(()=>[]);const d=Re();const u=d?ti.join(d,"example-dev-build/artifacts/pool-seed"):null;const h=d?{exampleBuild:qe(),seed:u?ke(u)??"ok":"none"}:{};$({xcode:await n.xcodeBuild(),runtimes:[...new Set(c.map(m=>m.runtime.split(".").pop()))],freeDiskGB:Math.floor(r.freeDiskBytes/1024**3),availableMemGB:Math.floor(r.availableMemBytes/1024**3),memPressure:r.memPressure,farmService:l?{...l,alive:await Oe(l)}:"not running",socket:ao(W().socket),...h,slimLabels:ce("slim").length,slimTurnsOff:ve.map(m=>m.name)});return}const o=await bo(s);try{switch(i){case"status":{const n=await o.call("status",{outside:a.includes("--outside")});return $(a.includes("--json")?n:Mo(n))}case"setup":{const n=Re();const r={appPath:C(a,"app")??qe(),seedDir:a.includes("--no-seed")?null:C(a,"seed")??(n?ti.join(n,"example-dev-build/artifacts/pool-seed"):void 0),launchUrl:C(a,"launch-url"),deviceType:C(a,"device")??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:C(a,"runtime")??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:C(a,"profile")??"slim",metroPort:Number(C(a,"metro")??8802)};if(!r.appPath)throw new p("BAD_REQUEST","No app build found.","Pass --app <path.app>.");if(!r.seedDir&&!a.includes("--no-seed"))throw new p("NO_SEED","No seed folder found.","Pass --seed <dir>, or --no-seed for an app that needs no saved data.");if(a.includes("--dry-run")){const l=await o.call("budget",{ramGB:6,diskGB:4});return $({wouldCreateTemplate:r,seed:r.seedDir?ke(r.seedDir)??"ok":"none",slimTurnsOff:r.profile==="slim"?ve.map(c=>`${c.name}: ${c.downside}`):[],budget:l.ok?"ok":l.reason,roomForSims:l.roomFor})}return $(await o.call("setup",r,{timeoutMs:15*6e4,requestId:`setup-${Date.now()}`}))}case"grow":case"shrink":{const n=await ri(o,t,C(a,"template"));return $(await o.call(i,{templateId:n,count:Number(a[0]??1)},{timeoutMs:30*6e4,requestId:`${i}-${Date.now()}`}))}case"acquire":{const n=await ri(o,t,C(a,"template"));const r=await o.acquire({templateId:n,count:Number(a[0]??1),purpose:C(a,"purpose")??"CLI",launch:a.includes("--launch")});$(r);console.error("Holding these sims. Press Ctrl-C to release them.");await new Promise(l=>process.once("SIGINT",()=>l()));await o.release(r.leaseId,r.epoch);return $("Released.")}case"remove-template":if(!a[0])throw new p("BAD_REQUEST",`Usage: ${t} remove-template <id>`);return $(await o.call("removeTemplate",{templateId:a[0]},{timeoutMs:30*6e4,requestId:`rmt-${a[0]}-${Date.now()}`}));case"park":case"wake":case"reset":case"rebuild":case"delete":if(!a[0])throw new p("BAD_REQUEST",`Usage: ${t} ${i} <udid>`);return $(await o.call(i,{udid:a[0]},{timeoutMs:5*6e4,requestId:`${i}-${a[0]}-${Date.now()}`}));case"stop":return $(await o.call("shutdown",{ifIdle:!a.includes("--now")}));case"end-lease":return $(await o.call("endLease",{leaseId:a[0],force:true},{requestId:`end-${a[0]}`}));case"phone":{if(!a[1])throw new p("BAD_REQUEST","phone use needs a phone id or off.",`See: ${t} phone list`);const n=await o.call("settings",{phoneProfile:a[1]});return $(n.phoneProfile==="off"?"Apps now launch as plain simulator apps.":`Apps now launch as ${he()[n.phoneProfile]?.name??n.phoneProfile}. Apps that are already open keep their old setting until the farm launches them again.`)}case"settings":{if(a.length===0)return $((await o.call("status")).settings);const n={};for(const r of a){const[l,c]=r.split("=");n[l]=c==="true"?true:c==="false"?false:Number.isNaN(Number(c))?c:Number(c)}return $(await o.call("settings",n))}default:$(Ve(t));process.exitCode=2}}finally{o.close()}}function Eo(e,t){const s={setup:["app","seed","launch-url","device","runtime","profile","metro"],grow:["template"],shrink:["template"],acquire:["template","purpose"],phone:["device"]};for(const i of s[e]??[])C(t,i);if(e==="setup"){if(t.includes("--no-seed")&&t.includes("--seed"))throw new p("BAD_REQUEST","Use --seed or --no-seed.");const i=C(t,"profile");const a=Number(C(t,"metro")??8802);if(i&&!["slim","stock"].includes(i))throw new p("BAD_REQUEST","Use slim or stock.");if(!Number.isInteger(a)||a<1||a>65535)throw new p("BAD_REQUEST","Use a port from 1 to 65535.")}}export{K as PROTOCOL_VERSION,Ne as STATE_VERSION,Xe as DEFAULT_SETTINGS,p as FarmError,xt as ALWAYS_ENABLED,ve as SLIM_CATEGORIES,Ut as slimLabels,ce as profileLabels,jt as profileHash,it as bootEnv,Ce as findDataContainer,Vt as readBuoyInstallId,Xt as LoadMeter,Zt as parseCpuTime,pt as DEFAULT_ANDROID_PHONE,cs as BUILT_IN_ANDROID_PHONES,ut as loadAndroidPhones,F as farmHome,W as paths,ws as Store,da as farmOwnedUdids,ee as ANDROID_ADB_PORT,Ae as androidHome,gs as parseGfxinfo,ys as classifyBootLog,vt as farmAdbTarget,Ss as farmAdbTargets,Es as EmulatorHost,Is as BUILT_IN_PHONES,$s as DEFAULT_PHONE,kt as PHONE_OFF,Fe as phoneDir,he as loadPhones,Ds as savePhone,Ts as defaultCapMB,Rs as profileVars,bt as joinInsertList,De as launchFilePath,We as eventsFilePath,Ns as writeLaunchFile,Ge as removeLaunchFile,Ps as readLaunchFile,Mt as simLaunchEnv,Cs as parseEvents,Ls as summarize,He as readSummary,xs as buildDylib,Us as LocalDylibBuilder,Z as copyCostGB,Hs as Engine,qs as crashLoop,It as SimctlHost,Vs as startFarmd,$t as nodeLauncher,Xs as FarmClient,Ot as connectFarm,Re as findRepoRoot,qe as findExampleBuild,Zs as defaultSeedDir,Ja as farmDefaults,so as detectAgent,io as agentLabel,ii as measurePhone,Rt as androidHelp,ni as runAndroidCommand,Ve as farmHelp,_o as runFarmCommand};
|