rnxsim 0.1.370 → 0.1.372
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/cli/bin.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +65 -14
- package/cli/commands/box/cloud-box.ts +294 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +269 -13
- package/cli/commands/control.ts +6 -1
- package/cli/outbound-endpoints.ts +37 -1
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-3R4424LW.js → agent-RTAKJ5UM.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-4PPWZWSY.js → agent-wrapper-JWRDI26D.js} +2 -2
- package/dist-cli/chunks/{app-fonts-KYXB6CXQ.js → app-fonts-RKO3N4PB.js} +2 -2
- package/dist-cli/chunks/{assert-7EZOWGYQ.js → assert-Y5LXPWC6.js} +2 -2
- package/dist-cli/chunks/{auth-L7M36H6C.js → auth-4SPTRWTM.js} +2 -2
- package/dist-cli/chunks/{beta-FKAF6YWJ.js → beta-KYS22QPY.js} +2 -2
- package/dist-cli/chunks/{box-5Y5VBMHV.js → box-7CK72ZPF.js} +617 -596
- package/dist-cli/chunks/{camera-XHF2RPFG.js → camera-RUL426MQ.js} +2 -2
- package/dist-cli/chunks/{chunk-EQ3OE2AV.js → chunk-2MA3Q44D.js} +1 -1
- package/dist-cli/chunks/{chunk-DHYTRCIP.js → chunk-2OA2WE7G.js} +2 -2
- package/dist-cli/chunks/{chunk-DMHSD6YJ.js → chunk-3C6LTIDV.js} +2 -2
- package/dist-cli/chunks/{chunk-NR2V2ONN.js → chunk-4UMODU6C.js} +2 -2
- package/dist-cli/chunks/{chunk-Z2VBSNFI.js → chunk-5Q6ZEZFF.js} +1 -1
- package/dist-cli/chunks/{chunk-LUOHRBJG.js → chunk-7FYOZQBM.js} +1 -1
- package/dist-cli/chunks/{chunk-Q4MRJ6PO.js → chunk-BRRWRBW7.js} +2 -2
- package/dist-cli/chunks/chunk-CEQXZ3GA.js +11 -0
- package/dist-cli/chunks/{chunk-DUXHYPBP.js → chunk-CZ2FOZVC.js} +2 -2
- package/dist-cli/chunks/{chunk-CMVRTZ5G.js → chunk-D5AT7UT5.js} +2 -2
- package/dist-cli/chunks/{chunk-D2GZASRV.js → chunk-DQOQQFMP.js} +1 -1
- package/dist-cli/chunks/{chunk-3UXXYJVD.js → chunk-EKSSAC4L.js} +1 -1
- package/dist-cli/chunks/{chunk-5BR33NAZ.js → chunk-EY5OHL2M.js} +1 -1
- package/dist-cli/chunks/{chunk-FK4JSTZ3.js → chunk-FOW2LPM7.js} +2 -2
- package/dist-cli/chunks/{chunk-WFQHT4QD.js → chunk-G5G7PQKO.js} +2 -2
- package/dist-cli/chunks/{chunk-WPV4H5S4.js → chunk-GPYG2L7R.js} +2 -2
- package/dist-cli/chunks/{chunk-3SOPYG7S.js → chunk-H2SKPUSI.js} +2 -2
- package/dist-cli/chunks/{chunk-6A6L3VJR.js → chunk-H567W7JT.js} +2 -2
- package/dist-cli/chunks/{chunk-PAUEZO6F.js → chunk-HDGZAVH3.js} +2 -2
- package/dist-cli/chunks/{chunk-AALKIAUZ.js → chunk-I37LRPZ3.js} +1 -1
- package/dist-cli/chunks/{chunk-JZLDHPB7.js → chunk-I7LRVN5M.js} +2 -2
- package/dist-cli/chunks/{chunk-3BG6UCQY.js → chunk-IX25FFF3.js} +11 -11
- package/dist-cli/chunks/{chunk-72S5FCBY.js → chunk-J2TJRPDB.js} +2 -2
- package/dist-cli/chunks/{chunk-RRX52O37.js → chunk-JPRHJMUJ.js} +1 -1
- package/dist-cli/chunks/{chunk-BMIJZNKD.js → chunk-K4TGUDY4.js} +2 -2
- package/dist-cli/chunks/{chunk-7C7TJYZ3.js → chunk-KSE5XGYU.js} +2 -2
- package/dist-cli/chunks/{chunk-OJZHECGH.js → chunk-KVVNCIJA.js} +1 -1
- package/dist-cli/chunks/{chunk-LXPHL6FO.js → chunk-KWLUOLYQ.js} +2 -2
- package/dist-cli/chunks/{chunk-C7BA5CCM.js → chunk-KXAE64HF.js} +2 -2
- package/dist-cli/chunks/{chunk-EWSWOWXK.js → chunk-L5BEWXQZ.js} +2 -2
- package/dist-cli/chunks/{chunk-D24EOJ4C.js → chunk-LAQKTHRM.js} +2 -2
- package/dist-cli/chunks/{chunk-IDLFVRPN.js → chunk-LWLHVF6Y.js} +1 -1
- package/dist-cli/chunks/{chunk-EOIBLKZO.js → chunk-MKHW3JNL.js} +2 -2
- package/dist-cli/chunks/{chunk-64JAEJ6N.js → chunk-MSM2E74F.js} +1 -1
- package/dist-cli/chunks/{chunk-DXV37CJN.js → chunk-NBEOA72C.js} +1 -1
- package/dist-cli/chunks/{chunk-H5X5FJ63.js → chunk-NLMDMNO6.js} +2 -2
- package/dist-cli/chunks/{chunk-6W4HIWC7.js → chunk-NT43X5LI.js} +2 -2
- package/dist-cli/chunks/{chunk-PNOENVMJ.js → chunk-NWPYM54M.js} +1 -1
- package/dist-cli/chunks/{chunk-O6QLKSAM.js → chunk-OLHHB6LD.js} +2 -2
- package/dist-cli/chunks/{chunk-MDCY532E.js → chunk-P6K6XFWE.js} +2 -2
- package/dist-cli/chunks/{chunk-LIMWG54N.js → chunk-PETP6LVI.js} +1 -1
- package/dist-cli/chunks/{chunk-YIZGNT7A.js → chunk-PKY76S57.js} +1 -1
- package/dist-cli/chunks/chunk-POR7R3TP.js +4 -0
- package/dist-cli/chunks/chunk-PUH6RQBJ.js +7 -0
- package/dist-cli/chunks/{chunk-NJMU5BN6.js → chunk-Q5WPPOFV.js} +1 -1
- package/dist-cli/chunks/{chunk-HQQTS2CB.js → chunk-Q7JSU5DI.js} +3 -3
- package/dist-cli/chunks/{chunk-U63T6LIQ.js → chunk-QA2PTSOC.js} +1 -1
- package/dist-cli/chunks/{chunk-2FOGXVOK.js → chunk-R7HMFLXI.js} +2 -2
- package/dist-cli/chunks/{chunk-FA75F34Z.js → chunk-RXKZJAGY.js} +1 -1
- package/dist-cli/chunks/{chunk-5RKRVS5A.js → chunk-SC7PGULZ.js} +2 -2
- package/dist-cli/chunks/{chunk-UFVAAWGZ.js → chunk-SLTEYNAE.js} +1 -1
- package/dist-cli/chunks/{chunk-6GPS7U4X.js → chunk-SSQCKJQZ.js} +2 -2
- package/dist-cli/chunks/{chunk-ARMXC3C3.js → chunk-SZX6ARJX.js} +3 -3
- package/dist-cli/chunks/{chunk-6S5LJV2O.js → chunk-U3XYGLXG.js} +2 -2
- package/dist-cli/chunks/chunk-UA5WQCBU.js +7 -0
- package/dist-cli/chunks/{chunk-VVQCZ42C.js → chunk-UVKX6FQ4.js} +1 -1
- package/dist-cli/chunks/{chunk-Z3DENGXW.js → chunk-VQQXUXXF.js} +2 -2
- package/dist-cli/chunks/{chunk-I52NT6VG.js → chunk-WR7LRCYW.js} +1 -1
- package/dist-cli/chunks/{chunk-HEKHLQ3B.js → chunk-XA4YU3A4.js} +3 -3
- package/dist-cli/chunks/{chunk-GRO74J2X.js → chunk-YEGWC2OY.js} +1 -1
- package/dist-cli/chunks/{chunk-AVJBT5LF.js → chunk-YWTBN7SD.js} +1 -1
- package/dist-cli/chunks/{chunk-PIZXOXBL.js → chunk-ZRQKEKHX.js} +2 -2
- package/dist-cli/chunks/{chunk-X6WQ4YWE.js → chunk-ZTZTRD5R.js} +1 -1
- package/dist-cli/chunks/{cleanup-MXMMJ7JW.js → cleanup-7ATBNZHC.js} +2 -2
- package/dist-cli/chunks/cli-version-D2IDQO7X.js +4 -0
- package/dist-cli/chunks/{compat-E6KE5KDI.js → compat-4ZC6ZMMA.js} +2 -2
- package/dist-cli/chunks/{config-QSLLVZDS.js → config-EOBODQ32.js} +2 -2
- package/dist-cli/chunks/{connect-QE4VU6GB.js → connect-ZQ72CZ7O.js} +2 -2
- package/dist-cli/chunks/control-FKPKLOE2.js +4 -0
- package/dist-cli/chunks/daemon-WFHWS3JU.js +4 -0
- package/dist-cli/chunks/{debug-5FG3MBJS.js → debug-7Z325GNU.js} +3 -3
- package/dist-cli/chunks/{desktop-5EZX7NX5.js → desktop-CR7ALX7H.js} +3 -3
- package/dist-cli/chunks/{detox-WJGQP5TM.js → detox-RZ37736M.js} +2 -2
- package/dist-cli/chunks/{device-D7LKCAMS.js → device-TA3SQP6K.js} +2 -2
- package/dist-cli/chunks/{diagnose-A3W6ZTGA.js → diagnose-JFKFKR3S.js} +2 -2
- package/dist-cli/chunks/disk-cleanup-HZM7VM4S.js +4 -0
- package/dist-cli/chunks/{do-chain-RUJNRPPF.js → do-chain-MTT65YNX.js} +2 -2
- package/dist-cli/chunks/drivers-BXEXWRVJ.js +4 -0
- package/dist-cli/chunks/{env-U67SQVTV.js → env-SFI24PLS.js} +2 -2
- package/dist-cli/chunks/{film-XC6UAQ3V.js → film-GPHSJLIH.js} +3 -3
- package/dist-cli/chunks/flow-FBCU3TZN.js +4 -0
- package/dist-cli/chunks/help-6MLROWYO.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-7S2TNZXG.js → hidden-runtime-alias-7Z6PYH53.js} +2 -2
- package/dist-cli/chunks/{home-paths-LMO5LB4F.js → home-paths-MEGVA6ET.js} +2 -2
- package/dist-cli/chunks/inspect-EWT5SXBD.js +4 -0
- package/dist-cli/chunks/install-desktop-IFBUH7MH.js +4 -0
- package/dist-cli/chunks/{login-JIU42S4C.js → login-EHUZYRAM.js} +2 -2
- package/dist-cli/chunks/{logout-JTAWQXZO.js → logout-WPGT62N6.js} +2 -2
- package/dist-cli/chunks/{maestro-TYBQYOGB.js → maestro-4HVDLCTI.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-YLAGWVKE.js → maestro-generate-VBS2EZMM.js} +3 -3
- package/dist-cli/chunks/{mode-DV24ZRZ6.js → mode-MGTS4QTF.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-JZH66VIM.js → optional-demo-registry-F3FQT5WT.js} +2 -2
- package/dist-cli/chunks/{perf-LXXJ4EAX.js → perf-47R3OEI7.js} +2 -2
- package/dist-cli/chunks/{permissions-KAOVZUPW.js → permissions-ZOLFZWR2.js} +2 -2
- package/dist-cli/chunks/{platform-KW35446W.js → platform-C4E26ZBZ.js} +3 -3
- package/dist-cli/chunks/privacy-WXGGG4ZI.js +4 -0
- package/dist-cli/chunks/{record-EK3ETFHZ.js → record-OUXWCWUF.js} +3 -3
- package/dist-cli/chunks/{report-issue-NW5PCAZF.js → report-issue-LJJXONXQ.js} +2 -2
- package/dist-cli/chunks/reset-ZZYLPETS.js +4 -0
- package/dist-cli/chunks/resolve-target-MVZZR2JW.js +4 -0
- package/dist-cli/chunks/runtime-DEC5AF3M.js +4 -0
- package/dist-cli/chunks/{screenshot-command-TQSPTFT2.js → screenshot-command-CUHDOS5D.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-IZP3L3AQ.js → screenshot-layers-DVWQVKT6.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-K7LJOVAE.js → screenshots-capture-GWIUJ3A6.js} +2 -2
- package/dist-cli/chunks/{serve-MMX4WXXG.js → serve-TX5OKGJM.js} +19 -19
- package/dist-cli/chunks/setup-X7LLGW67.js +4 -0
- package/dist-cli/chunks/{sim-client-KLAYIFOC.js → sim-client-W4WZOZ3P.js} +2 -2
- package/dist-cli/chunks/{skills-N4D3POIS.js → skills-TDFQAZ6A.js} +2 -2
- package/dist-cli/chunks/{state-ES6TFQZF.js → state-P2Z25OLQ.js} +2 -2
- package/dist-cli/chunks/{storage-52DR5V2C.js → storage-DWEOFXEB.js} +2 -2
- package/dist-cli/chunks/store-TKBLMYCV.js +4 -0
- package/dist-cli/chunks/telemetry-E3CBKJMD.js +4 -0
- package/dist-cli/chunks/{timeline-NRVNB5JR.js → timeline-55XNA4GX.js} +2 -2
- package/dist-cli/chunks/{upgrade-LNED5YQ2.js → upgrade-3XRCVDVC.js} +2 -2
- package/dist-cli/chunks/upload-QB77GSMQ.js +4 -0
- package/dist-cli/chunks/{version-VEF2DYGG.js → version-WTYGLXBJ.js} +2 -2
- package/dist-cli/chunks/{web-CGDE6S73.js → web-ZWRY3VHM.js} +2 -2
- package/dist-cli/chunks/welcome-7ATM5EGX.js +5 -0
- package/dist-cli/chunks/{what-happened-FSIDWL32.js → what-happened-Q3IGIH4K.js} +2 -2
- package/dist-cli/chunks/{ws-bridge-QXFQLRE7.js → ws-bridge-KCPZSBBX.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +69 -34
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +430 -106
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +149 -93
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +679 -307
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +275 -92
- package/src/dev-bundle-resolution.ts +123 -40
- package/src/disk-cleanup.ts +44 -0
- package/src/host/bridge-host.ts +11 -0
- package/src/host/plane-host.ts +203 -0
- package/dist-cli/chunks/chunk-GJRLPJQL.js +0 -6
- package/dist-cli/chunks/chunk-NTC46LSA.js +0 -4
- package/dist-cli/chunks/chunk-OTFTMGAL.js +0 -7
- package/dist-cli/chunks/cli-version-MLS55UTY.js +0 -4
- package/dist-cli/chunks/control-XD5F6K5T.js +0 -4
- package/dist-cli/chunks/daemon-UN5SE2JO.js +0 -4
- package/dist-cli/chunks/disk-cleanup-BDRHX5ZG.js +0 -4
- package/dist-cli/chunks/drivers-3NJ5RL6X.js +0 -4
- package/dist-cli/chunks/flow-FHZZD3P6.js +0 -4
- package/dist-cli/chunks/help-BYYDUSXF.js +0 -4
- package/dist-cli/chunks/inspect-RQCTWDCC.js +0 -4
- package/dist-cli/chunks/install-desktop-GOWXCQUE.js +0 -4
- package/dist-cli/chunks/privacy-MIFE72CG.js +0 -4
- package/dist-cli/chunks/reset-RZ4O5AFJ.js +0 -4
- package/dist-cli/chunks/resolve-target-7R5BNYMM.js +0 -4
- package/dist-cli/chunks/runtime-MZHLSRS6.js +0 -4
- package/dist-cli/chunks/setup-YFP3KEQZ.js +0 -4
- package/dist-cli/chunks/store-HOL45WCI.js +0 -4
- package/dist-cli/chunks/telemetry-BV6OTYPF.js +0 -4
- package/dist-cli/chunks/upload-RXKHFIBU.js +0 -4
- package/dist-cli/chunks/welcome-FT2AMUWW.js +0 -5
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.372 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/package.json
CHANGED
|
@@ -18,10 +18,6 @@ import { readlink } from 'fs/promises'
|
|
|
18
18
|
import http from 'http'
|
|
19
19
|
import net from 'net'
|
|
20
20
|
import { resolve } from 'node:path'
|
|
21
|
-
import { promisify } from 'util'
|
|
22
|
-
import type { AppSettingsSpecifier } from '@sootsim/globals'
|
|
23
|
-
|
|
24
|
-
const execP = promisify(exec)
|
|
25
21
|
import {
|
|
26
22
|
buildLoopbackProbeBaseUrl,
|
|
27
23
|
LOOPBACK_PROBE_HOSTNAMES,
|
|
@@ -37,6 +33,50 @@ import {
|
|
|
37
33
|
readAppSettingsBundle,
|
|
38
34
|
} from '../src/settings-bundle.ts'
|
|
39
35
|
import { loadOptionalDemoApps, optionalDemoAppsSync } from './optional-demo-registry.ts'
|
|
36
|
+
import type { AppSettingsSpecifier } from '@sootsim/globals'
|
|
37
|
+
|
|
38
|
+
// how long past a command's own budget we wait before deciding its exit event is
|
|
39
|
+
// never coming. generous, because this only fires when the child is already
|
|
40
|
+
// overdue and we are choosing between a wrong answer and a hang.
|
|
41
|
+
const EXEC_EXIT_GRACE_MS = 2_000
|
|
42
|
+
|
|
43
|
+
/** every subprocess in this file goes through here.
|
|
44
|
+
*
|
|
45
|
+
* `exec`'s own `timeout` option is NOT a backstop. it kills the child and then
|
|
46
|
+
* STILL waits for a 'close' event to settle the promise, so when that event is
|
|
47
|
+
* lost the promise never settles — the timeout is already spent and nothing is
|
|
48
|
+
* left to rescue it. (same failure m10843 pinned in `CheckoutFilePlane.refresh`.)
|
|
49
|
+
* `lsof` walks every open fd on the machine, which makes it a realistic victim.
|
|
50
|
+
*
|
|
51
|
+
* a hung scan is worse than a slow one: `inflightScan` never clears, so
|
|
52
|
+
* `/__server-scan` stops answering at all instead of answering "could not
|
|
53
|
+
* look". so we race the child against a real wall-clock deadline and reject
|
|
54
|
+
* with `killed`, which callers already read as could-not-look. */
|
|
55
|
+
function execGuarded(
|
|
56
|
+
command: string,
|
|
57
|
+
timeout: number,
|
|
58
|
+
extra: { maxBuffer?: number } = {},
|
|
59
|
+
): Promise<{ stdout: string }> {
|
|
60
|
+
let child: ReturnType<typeof exec> | undefined
|
|
61
|
+
const ran = new Promise<{ stdout: string }>((resolve, reject) => {
|
|
62
|
+
child = exec(command, { encoding: 'utf8', timeout, ...extra }, (err, stdout) => {
|
|
63
|
+
if (err) reject(err)
|
|
64
|
+
else resolve({ stdout })
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
child?.kill('SIGKILL')
|
|
70
|
+
const err = new Error(`\`${command}\` did not exit within ${timeout}ms`)
|
|
71
|
+
// matches the shape `wasKilledByTimeout` reads off a real exec timeout, so
|
|
72
|
+
// a lost exit event and a genuine timeout reach callers as the same fact.
|
|
73
|
+
Object.assign(err, { killed: true })
|
|
74
|
+
reject(err)
|
|
75
|
+
}, timeout + EXEC_EXIT_GRACE_MS)
|
|
76
|
+
timer.unref?.()
|
|
77
|
+
})
|
|
78
|
+
return Promise.race([ran, deadline])
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
export interface DiscoveredServer {
|
|
42
82
|
port: number
|
|
@@ -51,6 +91,11 @@ export interface DiscoveredServer {
|
|
|
51
91
|
settingsBundle?: readonly AppSettingsSpecifier[]
|
|
52
92
|
settingsStorageScopeUrl?: string
|
|
53
93
|
patched?: boolean
|
|
94
|
+
/** true when `bundleUrl` was NOT derived from the server's own manifest —
|
|
95
|
+
* the manifest probe got no answer, so this is the generic `/index.bundle`
|
|
96
|
+
* guess. good enough to LIST the server; never good enough to open it. see
|
|
97
|
+
* `manifestState` below for why the two are different answers. */
|
|
98
|
+
bundleUrlProvisional?: boolean
|
|
54
99
|
/** absolute cwd of the owning node/bun process — resolved from `lsof -d cwd`
|
|
55
100
|
* when the scanner can see the pid. used to auto-attach the project for
|
|
56
101
|
* agent sessions without requiring a manual `sootsim agent attach`. */
|
|
@@ -58,6 +103,18 @@ export interface DiscoveredServer {
|
|
|
58
103
|
pid?: number
|
|
59
104
|
}
|
|
60
105
|
|
|
106
|
+
/** how the manifest probe (`GET /` with expo-platform) actually went. the same
|
|
107
|
+
* three answers `dev-bundle-resolution.ts` uses, deliberately spelled the same
|
|
108
|
+
* way: a second vocabulary for one question is how the two resolvers drift.
|
|
109
|
+
*
|
|
110
|
+
* the rule this encodes, which the whole scanner depends on: A PROBE THAT
|
|
111
|
+
* COULD NOT LOOK MUST NEVER ANSWER AS THOUGH IT LOOKED. silence, timeout and
|
|
112
|
+
* empty are three different answers, and only `no-manifest` means "there is
|
|
113
|
+
* nothing there". reading `no-answer` as `no-manifest` is what made
|
|
114
|
+
* `rnx open` hand back `<base>/index.bundle` for a One app whose manifest
|
|
115
|
+
* names a different entry point entirely. */
|
|
116
|
+
type ManifestState = 'resolved' | 'no-manifest' | 'no-answer'
|
|
117
|
+
|
|
61
118
|
// localhost dev servers respond in well under 100ms when alive. drop dead
|
|
62
119
|
// branches fast so a non-bundler process doesn't drag scan latency.
|
|
63
120
|
const TIMEOUT_MS = 250
|
|
@@ -74,6 +131,11 @@ const MANIFEST_TIMEOUT_MS = 1500
|
|
|
74
131
|
// within this budget the port isn't actually reachable, and all 5 HTTP probes
|
|
75
132
|
// would just time out for nothing.
|
|
76
133
|
const TCP_GATE_MS = 120
|
|
134
|
+
// once a server has sent response headers we never abort it (see httpGet). this
|
|
135
|
+
// only bounds a response that never ends, so it is deliberately far above any
|
|
136
|
+
// real manifest: aborting a slow-but-answering server is the defect this whole
|
|
137
|
+
// path exists to avoid.
|
|
138
|
+
const BODY_TIMEOUT_MS = 30_000
|
|
77
139
|
|
|
78
140
|
interface HttpResult {
|
|
79
141
|
statusCode: number
|
|
@@ -122,33 +184,65 @@ function httpGet(
|
|
|
122
184
|
headers: Record<string, string> = {},
|
|
123
185
|
): Promise<HttpResult | null> {
|
|
124
186
|
return new Promise((resolve) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
req.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
// first answer wins: the response, the deadline, and the connection closing
|
|
188
|
+
// all race, and every one of them can arrive after another has decided.
|
|
189
|
+
let settled = false
|
|
190
|
+
const settle = (value: HttpResult | null) => {
|
|
191
|
+
if (settled) return
|
|
192
|
+
settled = true
|
|
193
|
+
resolve(value)
|
|
194
|
+
}
|
|
195
|
+
// NO socket timeout here, deliberately. `options.timeout` / `req.setTimeout`
|
|
196
|
+
// make node DESTROY the connection when the budget expires, and destroying a
|
|
197
|
+
// dev server's connection while it is still assembling its manifest fires
|
|
198
|
+
// "Cannot pipe to a closed or destroyed stream" into One's manifest
|
|
199
|
+
// middleware as an unhandled rejection — the hazard the knownOne comment
|
|
200
|
+
// below already documents, which can exit the user's dev server. measured:
|
|
201
|
+
// six probes with a socket timeout produced six of those errors, six probes
|
|
202
|
+
// that give up in JS produced none, and the request completes later with the
|
|
203
|
+
// server none the wiser.
|
|
204
|
+
//
|
|
205
|
+
// giving up WAITING and KILLING THE REQUEST are different acts, and only the
|
|
206
|
+
// first one is ours to take. the deadline below resolves the scan on time;
|
|
207
|
+
// the request runs itself out in the background and its result is discarded.
|
|
208
|
+
const req = http.request({ hostname, port, path, method, headers }, (res) => {
|
|
209
|
+
let body = ''
|
|
210
|
+
res.on('data', (c: Buffer) => (body += c.toString()))
|
|
211
|
+
const contentType = (() => {
|
|
212
|
+
const raw = res.headers['content-type']
|
|
213
|
+
if (typeof raw === 'string') return raw
|
|
214
|
+
if (Array.isArray(raw)) return raw[0]
|
|
215
|
+
return undefined
|
|
216
|
+
})()
|
|
217
|
+
res.on('end', () =>
|
|
218
|
+
settle({
|
|
219
|
+
statusCode: res.statusCode || 0,
|
|
220
|
+
body,
|
|
221
|
+
contentType,
|
|
222
|
+
hostname,
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
150
225
|
})
|
|
226
|
+
req.on('error', () => settle(null))
|
|
151
227
|
req.end()
|
|
228
|
+
const giveUp = setTimeout(() => {
|
|
229
|
+
settle(null)
|
|
230
|
+
// nothing is waiting on it now, so it must not hold a CLI process open.
|
|
231
|
+
req.socket?.unref()
|
|
232
|
+
}, timeout)
|
|
233
|
+
// the only thing that ever destroys a connection here. by BODY_TIMEOUT_MS a
|
|
234
|
+
// server has long since answered or died, so there is no live write to cut.
|
|
235
|
+
const abandon = setTimeout(() => req.destroy(), BODY_TIMEOUT_MS)
|
|
236
|
+
abandon.unref?.()
|
|
237
|
+
req.on('close', () => {
|
|
238
|
+
clearTimeout(giveUp)
|
|
239
|
+
clearTimeout(abandon)
|
|
240
|
+
// `close` on the request fires BEFORE the response's own `end` handler,
|
|
241
|
+
// so settling synchronously here turns every successful probe into a
|
|
242
|
+
// null. defer one tick and let a real answer win; this only settles the
|
|
243
|
+
// case where the connection closed without one.
|
|
244
|
+
setImmediate(() => settle(null))
|
|
245
|
+
})
|
|
152
246
|
})
|
|
153
247
|
}
|
|
154
248
|
|
|
@@ -165,70 +259,143 @@ const FALLBACK_PORTS: ListeningProcess[] = [
|
|
|
165
259
|
8081, 8082, 8083, 8084, 8085, 8086, 3000, 3001, 19000,
|
|
166
260
|
].map((port) => ({ port, pid: 0 }))
|
|
167
261
|
|
|
262
|
+
// sootsim's own dev server range at PORT_OFFSET=0. an isolated stack shifts its
|
|
263
|
+
// shell by the same offset as every other port it owns, so the guard has to
|
|
264
|
+
// shift with it: at PORT_OFFSET=90 the shell listens on :5263 and a range fixed
|
|
265
|
+
// at 5170-5200 does not cover it, which lets a scan discover ITSELF and offer
|
|
266
|
+
// the shell as though it were an app's dev server. a guard that is only correct
|
|
267
|
+
// at offset 0 is wrong for everyone running an isolated stack, which is exactly
|
|
268
|
+
// who runs concurrent scans.
|
|
269
|
+
const SELF_PORT_RANGE_START = 5170
|
|
270
|
+
const SELF_PORT_RANGE_END = 5200
|
|
271
|
+
|
|
272
|
+
function isOwnShellPort(port: number): boolean {
|
|
273
|
+
const offset = Number(process.env.PORT_OFFSET) || 0
|
|
274
|
+
// the unshifted range still counts: another checkout's default-offset shell is
|
|
275
|
+
// no more an app dev server than our own is.
|
|
276
|
+
if (port >= SELF_PORT_RANGE_START && port <= SELF_PORT_RANGE_END) return true
|
|
277
|
+
return (
|
|
278
|
+
offset > 0 &&
|
|
279
|
+
port >= SELF_PORT_RANGE_START + offset &&
|
|
280
|
+
port <= SELF_PORT_RANGE_END + offset
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
168
284
|
function acceptPort(port: number, excluded: Set<number>): boolean {
|
|
169
285
|
if (port <= 0 || port >= 20000) return false
|
|
170
286
|
if (excluded.has(port)) return false
|
|
171
|
-
|
|
172
|
-
if (port >= 5170 && port <= 5200) return false
|
|
287
|
+
if (isOwnShellPort(port)) return false
|
|
173
288
|
return true
|
|
174
289
|
}
|
|
175
290
|
|
|
291
|
+
/** ran the listing tool and these are the listeners, versus could not run it at
|
|
292
|
+
* all. an empty `processes` here means the tool looked and there was genuinely
|
|
293
|
+
* nothing; `could-not-look` means we have no information. substituting the
|
|
294
|
+
* hardcoded FALLBACK_PORTS for the second case is what made `/__server-scan`
|
|
295
|
+
* answer `[]` — "there are no dev servers" — to a user running three of them. */
|
|
296
|
+
export type ListeningProcessScan =
|
|
297
|
+
| { state: 'looked'; processes: ListeningProcess[] }
|
|
298
|
+
| { state: 'could-not-look' }
|
|
299
|
+
|
|
300
|
+
/** thrown by `scanDevServers` when listener enumeration itself failed. callers
|
|
301
|
+
* must treat it as "no information" — keep whatever list they already had, or
|
|
302
|
+
* answer their own caller with a failure — never as an empty result. */
|
|
303
|
+
export class DevServerScanUnavailableError extends Error {
|
|
304
|
+
constructor() {
|
|
305
|
+
super('could not enumerate listening processes (lsof/ss unavailable or timed out)')
|
|
306
|
+
this.name = 'DevServerScanUnavailableError'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// lsof walks every open fd on the machine, so on a busy host it is genuinely
|
|
311
|
+
// slow — measured between 1.2s and 5.0s here with 58 matching listeners. the
|
|
312
|
+
// budget has to fit the real worst case, because the alternative is not a
|
|
313
|
+
// faster answer, it is a WRONG one.
|
|
314
|
+
const LISTING_TIMEOUT_MS = 10_000
|
|
315
|
+
|
|
316
|
+
/** true when a failed exec was killed by our own timeout rather than by the
|
|
317
|
+
* command being absent or erroring — the difference between "could not look"
|
|
318
|
+
* and "this tool is not available here". */
|
|
319
|
+
function wasKilledByTimeout(err: unknown): boolean {
|
|
320
|
+
return !!(err && typeof err === 'object' && (err as { killed?: boolean }).killed)
|
|
321
|
+
}
|
|
322
|
+
|
|
176
323
|
export async function discoverListeningProcesses(
|
|
177
324
|
excludePorts: number[] = [],
|
|
178
325
|
): Promise<ListeningProcess[]> {
|
|
326
|
+
const scan = await scanListeningProcesses(excludePorts)
|
|
327
|
+
return scan.state === 'looked' ? scan.processes : []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function scanListeningProcesses(
|
|
331
|
+
excludePorts: number[] = [],
|
|
332
|
+
): Promise<ListeningProcessScan> {
|
|
179
333
|
const excluded = new Set(excludePorts)
|
|
334
|
+
let timedOut = false
|
|
180
335
|
|
|
181
336
|
// lsof layout: COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
|
|
182
337
|
// NAME looks like "*:8081" or "127.0.0.1:8081" for a LISTEN line.
|
|
183
338
|
// async exec — sync blocked CrBrowserMain for ~600ms/scan on macOS.
|
|
184
339
|
try {
|
|
185
|
-
const { stdout } = await
|
|
340
|
+
const { stdout } = await execGuarded(
|
|
186
341
|
`lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E '^(node|bun)'`,
|
|
187
|
-
|
|
342
|
+
LISTING_TIMEOUT_MS,
|
|
188
343
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!seen.has(port)) seen.set(port, pid)
|
|
201
|
-
}
|
|
202
|
-
if (seen.size > 0) {
|
|
203
|
-
return [...seen.entries()].map(([port, pid]) => ({ port, pid }))
|
|
204
|
-
}
|
|
344
|
+
const seen = new Map<number, number>()
|
|
345
|
+
for (const line of stdout.trim() ? stdout.trim().split('\n') : []) {
|
|
346
|
+
const parts = line.trim().split(/\s+/)
|
|
347
|
+
if (parts.length < 9) continue
|
|
348
|
+
const pid = Number(parts[1])
|
|
349
|
+
const addr = parts[8]
|
|
350
|
+
const m = addr.match(/:(\d+)$/)
|
|
351
|
+
if (!m) continue
|
|
352
|
+
const port = Number(m[1])
|
|
353
|
+
if (!acceptPort(port, excluded)) continue
|
|
354
|
+
if (!seen.has(port)) seen.set(port, pid)
|
|
205
355
|
}
|
|
206
|
-
|
|
356
|
+
// lsof ran. an empty result is a real answer: nothing is listening.
|
|
357
|
+
return {
|
|
358
|
+
state: 'looked',
|
|
359
|
+
processes: [...seen.entries()].map(([port, pid]) => ({ port, pid })),
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (wasKilledByTimeout(err)) timedOut = true
|
|
363
|
+
}
|
|
207
364
|
|
|
208
365
|
// ss layout: State Recv-Q Send-Q Local-Address:Port Peer-Addr:Port users:(("node",pid=1234,fd=5))
|
|
209
366
|
try {
|
|
210
|
-
const { stdout } = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!seen.has(port)) seen.set(port, pid)
|
|
224
|
-
}
|
|
225
|
-
if (seen.size > 0) {
|
|
226
|
-
return [...seen.entries()].map(([port, pid]) => ({ port, pid }))
|
|
227
|
-
}
|
|
367
|
+
const { stdout } = await execGuarded(
|
|
368
|
+
`ss -tlnp 2>/dev/null | grep -E '"(node|bun)"'`,
|
|
369
|
+
LISTING_TIMEOUT_MS,
|
|
370
|
+
)
|
|
371
|
+
const seen = new Map<number, number>()
|
|
372
|
+
for (const line of stdout.trim() ? stdout.trim().split('\n') : []) {
|
|
373
|
+
const portMatch = line.match(/:(\d+)\s/)
|
|
374
|
+
const pidMatch = line.match(/pid=(\d+)/)
|
|
375
|
+
if (!portMatch) continue
|
|
376
|
+
const port = Number(portMatch[1])
|
|
377
|
+
const pid = pidMatch ? Number(pidMatch[1]) : 0
|
|
378
|
+
if (!acceptPort(port, excluded)) continue
|
|
379
|
+
if (!seen.has(port)) seen.set(port, pid)
|
|
228
380
|
}
|
|
229
|
-
|
|
381
|
+
return {
|
|
382
|
+
state: 'looked',
|
|
383
|
+
processes: [...seen.entries()].map(([port, pid]) => ({ port, pid })),
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (wasKilledByTimeout(err)) timedOut = true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// a tool that timed out told us nothing, so say so rather than substituting
|
|
390
|
+
// the guess list below and reporting its emptiness as fact.
|
|
391
|
+
if (timedOut) return { state: 'could-not-look' }
|
|
230
392
|
|
|
231
|
-
|
|
393
|
+
// neither tool exists here, so probe the conventional bundler ports directly.
|
|
394
|
+
// that is a real (if narrow) look, not a substitute for one.
|
|
395
|
+
return {
|
|
396
|
+
state: 'looked',
|
|
397
|
+
processes: FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded)),
|
|
398
|
+
}
|
|
232
399
|
}
|
|
233
400
|
|
|
234
401
|
// kept for backwards compatibility with any external callers that only need
|
|
@@ -284,9 +451,7 @@ export async function resolveListeningPid(port: number): Promise<number | null>
|
|
|
284
451
|
// the local address uses a dot before the port, and the owner column is the
|
|
285
452
|
// only token shaped "<name>:<pid>".
|
|
286
453
|
try {
|
|
287
|
-
const { stdout } = await
|
|
288
|
-
encoding: 'utf8',
|
|
289
|
-
timeout: 8000,
|
|
454
|
+
const { stdout } = await execGuarded('netstat -anv -p tcp 2>/dev/null', 8000, {
|
|
290
455
|
maxBuffer: 16 * 1024 * 1024,
|
|
291
456
|
})
|
|
292
457
|
const pid = pidFromNetstat(stdout, port)
|
|
@@ -295,9 +460,7 @@ export async function resolveListeningPid(port: number): Promise<number | null>
|
|
|
295
460
|
|
|
296
461
|
// linux ss: 'LISTEN 0 511 0.0.0.0:8081 0.0.0.0:* users:(("node",pid=1234,fd=5))'
|
|
297
462
|
try {
|
|
298
|
-
const { stdout } = await
|
|
299
|
-
encoding: 'utf8',
|
|
300
|
-
timeout: 8000,
|
|
463
|
+
const { stdout } = await execGuarded(`ss -tlnp 2>/dev/null`, 8000, {
|
|
301
464
|
maxBuffer: 16 * 1024 * 1024,
|
|
302
465
|
})
|
|
303
466
|
for (const line of stdout.split('\n')) {
|
|
@@ -344,10 +507,10 @@ export async function resolveProcessCwd(pid: number): Promise<string | null> {
|
|
|
344
507
|
// p<pid>
|
|
345
508
|
// fcwd
|
|
346
509
|
// n<absolute path>
|
|
347
|
-
const { stdout } = await
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
510
|
+
const { stdout } = await execGuarded(
|
|
511
|
+
`lsof -b -w -p ${pid} -a -d cwd -Fn 2>/dev/null`,
|
|
512
|
+
8000,
|
|
513
|
+
)
|
|
351
514
|
for (const line of stdout.split('\n')) {
|
|
352
515
|
if (line.startsWith('n') && line.length > 1) {
|
|
353
516
|
const cwd = line.slice(1).trim()
|
|
@@ -672,7 +835,16 @@ export async function probePort(
|
|
|
672
835
|
(manifestParsed?.extra?.expoClient as { name?: unknown } | undefined) ||
|
|
673
836
|
(manifestParsed?.extra as { name?: unknown } | undefined) ||
|
|
674
837
|
{}
|
|
675
|
-
|
|
838
|
+
// `manifestRes === null` means the probe got NO answer — it timed out or the
|
|
839
|
+
// connection failed. that is not the same fact as a server that answered and
|
|
840
|
+
// turned out to have no manifest, and only the second one licenses the
|
|
841
|
+
// generic `/index.bundle` entry point below.
|
|
842
|
+
const manifestState: ManifestState = !manifestRes
|
|
843
|
+
? 'no-answer'
|
|
844
|
+
: manifestParsed && (manifestLaunchUrl || typeof manifestClient.name === 'string')
|
|
845
|
+
? 'resolved'
|
|
846
|
+
: 'no-manifest'
|
|
847
|
+
if (manifestState === 'resolved') {
|
|
676
848
|
knownNonPatched.add(port)
|
|
677
849
|
const launchUrl =
|
|
678
850
|
manifestLaunchUrl ||
|
|
@@ -708,17 +880,23 @@ export async function probePort(
|
|
|
708
880
|
// never fire the HEAD against them.
|
|
709
881
|
if (statusRes && statusRes.body.includes('packager-status:running')) {
|
|
710
882
|
knownNonPatched.add(port)
|
|
883
|
+
// `makeResult` builds the generic `<base>/index.bundle` entry point. that is
|
|
884
|
+
// the RIGHT answer for a vanilla Metro or plain Expo packager, which is a
|
|
885
|
+
// server that ANSWERED `/` and had no manifest to give. it is a guess when
|
|
886
|
+
// the manifest simply never answered: this server may well be a One app
|
|
887
|
+
// whose manifest names `node_modules/one/metro-entry.bundle`, and opening
|
|
888
|
+
// the generic path instead makes metro build the whole app a second time
|
|
889
|
+
// under a module graph the app never named. flag it so `open` refuses to
|
|
890
|
+
// act on it, while the dock still lists the server it can plainly see.
|
|
891
|
+
const result = makeResult(
|
|
892
|
+
probeHostname,
|
|
893
|
+
port,
|
|
894
|
+
expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro',
|
|
895
|
+
owner,
|
|
896
|
+
)
|
|
897
|
+
if (manifestState === 'no-answer') result.bundleUrlProvisional = true
|
|
711
898
|
return enrichWithManifest(
|
|
712
|
-
applyManifest(
|
|
713
|
-
makeResult(
|
|
714
|
-
probeHostname,
|
|
715
|
-
port,
|
|
716
|
-
expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro',
|
|
717
|
-
owner,
|
|
718
|
-
),
|
|
719
|
-
manifestRes,
|
|
720
|
-
buildIconProxyUrl,
|
|
721
|
-
),
|
|
899
|
+
applyManifest(result, manifestRes, buildIconProxyUrl),
|
|
722
900
|
buildIconProxyUrl,
|
|
723
901
|
)
|
|
724
902
|
}
|
|
@@ -914,7 +1092,12 @@ export async function scanDevServers(
|
|
|
914
1092
|
// disk contention delays the import. memoized, so this is free after the
|
|
915
1093
|
// first call.
|
|
916
1094
|
await loadOptionalDemoApps()
|
|
917
|
-
const
|
|
1095
|
+
const scan = await scanListeningProcesses(opts.excludePorts)
|
|
1096
|
+
// we could not enumerate listeners, so we know nothing about what is running.
|
|
1097
|
+
// returning [] here would evict every cache entry and report "no dev servers"
|
|
1098
|
+
// to a user who has several — the caller must be able to tell the two apart.
|
|
1099
|
+
if (scan.state === 'could-not-look') throw new DevServerScanUnavailableError()
|
|
1100
|
+
const processes = scan.processes
|
|
918
1101
|
const currentPorts = new Set(processes.map((p) => p.port))
|
|
919
1102
|
|
|
920
1103
|
// evict cache entries for ports that disappeared. a restarted process may
|