peachsim 0.1.1594 → 0.1.1620
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 +34 -11
- package/cli/app-catalog.ts +7 -5
- package/cli/app-config.ts +6 -2
- package/cli/app-fonts.ts +74 -35
- package/cli/app-stubs.ts +173 -42
- package/cli/bin.ts +9 -0
- package/cli/bridge-flow-runner.ts +226 -145
- package/cli/cloud-client.ts +52 -29
- package/cli/commands/agent-indicator-mode.ts +39 -0
- package/cli/commands/box.ts +13 -3
- package/cli/commands/control.ts +132 -108
- package/cli/commands/daemon.ts +99 -24
- package/cli/commands/debug.ts +29 -1
- package/cli/commands/detect.ts +1 -1
- package/cli/commands/device.ts +6 -8
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/doctor.ts +97 -34
- package/cli/commands/flow.ts +22 -12
- package/cli/commands/inspect/actions.ts +2 -0
- package/cli/commands/inspect/core.ts +106 -40
- package/cli/commands/inspect/list.ts +9 -3
- package/cli/commands/inspect/memory.ts +9 -0
- package/cli/commands/inspect/settling.ts +167 -49
- package/cli/commands/inspect/shared.ts +53 -12
- package/cli/commands/inspect/wait-event.ts +0 -1
- package/cli/commands/inspect/wait.ts +2 -2
- package/cli/commands/inspect.ts +19 -4
- package/cli/commands/kotlin.ts +5 -7
- package/cli/commands/maestro-compare.ts +25 -2
- package/cli/commands/mode.ts +13 -1
- package/cli/commands/perf-record.ts +296 -0
- package/cli/commands/perf.ts +6 -1
- package/cli/commands/platform.ts +34 -11
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +48 -20
- package/cli/commands/runtime.ts +9 -11
- package/cli/commands/screenshots.ts +4 -3
- package/cli/commands/setup.ts +17 -5
- package/cli/commands/skills.ts +16 -43
- package/cli/commands/swift.ts +18 -3
- package/cli/commands/upgrade.ts +7 -6
- package/cli/commands/upload.ts +27 -51
- package/cli/commands/window.ts +449 -0
- package/cli/desktop-companion.ts +84 -10
- package/cli/drivers/desktop.ts +2 -0
- package/cli/drivers/playwright-sim-host.ts +12 -1
- package/cli/flow-file.ts +1 -0
- package/cli/maestro-compare.ts +19 -3
- package/cli/main.ts +22 -1
- package/cli/material-symbol-fonts.ts +171 -0
- package/cli/outbound-endpoints.ts +32 -16
- package/cli/preview-capture.ts +2 -2
- package/cli/run-registry.ts +5 -2
- package/cli/telemetry.ts +6 -3
- package/cli/ws-bridge.ts +15 -19
- package/detox/attempt-events.cjs +63 -7
- package/detox/attempt-events.d.cts +7 -0
- package/detox/gestures.ts +115 -2
- package/detox/index.ts +313 -411
- package/detox/jest-environment.cjs +86 -0
- package/detox/jest-preset.cjs +3 -1
- package/detox/jest-setup-after-env.cjs +1 -0
- package/detox/matchers.ts +3 -6
- package/detox/motion-change.cjs +9 -1
- package/detox/proof-frame.cjs +83 -9
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +15 -5
- 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/bundle-channel.cjs +4005 -1362
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +6 -1
- package/dist-lib/cloud-contract.mjs +5 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +26 -7
- package/dist-lib/config.mjs +26 -7
- package/dist-lib/detox/index.cjs +1908 -539
- package/dist-lib/detox/jest-environment.cjs +86 -0
- package/dist-lib/detox/jest-preset.cjs +3 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3617 -1016
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +4760 -1825
- package/dist-lib/host/fetch-proxy-handler.cjs +59 -9
- package/dist-lib/host/fetch-proxy-overrides.cjs +62 -2
- package/dist-lib/host/fetch-proxy-overrides.mjs +56 -2
- package/dist-lib/host/replacement-module-handler.cjs +8 -8
- package/dist-lib/host/websocket-proxy.cjs +22 -6
- package/dist-lib/index.cjs +610 -184
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +33 -4
- package/dist-lib/kotlin-host.cjs +262 -121
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +101 -7
- package/dist-lib/metro-production-bundle.mjs +106 -7
- package/dist-lib/metro.cjs +315 -87
- package/dist-lib/native-app-identity-project.cjs +101 -26
- package/dist-lib/native-app-identity-project.mjs +101 -26
- package/dist-lib/native-app-identity.cjs +33 -7
- package/dist-lib/native-app-identity.mjs +33 -7
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +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/scripts/dev-server-scanner.cjs +30 -9
- package/dist-lib/sdk.cjs +236 -78
- package/dist-lib/sdk.mjs +236 -78
- package/dist-lib/skills.cjs +6788 -2636
- package/dist-lib/swift-compile.cjs +1637 -299
- package/dist-lib/swift-compile.mjs +1558 -219
- package/dist-lib/swift-package.cjs +25 -3
- package/dist-lib/swift-package.mjs +22 -2
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +3675 -1051
- package/dist-lib/vite.cjs +142 -20
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/install.sh +1 -1
- package/kotlin/AppSources/android/view/Window.kt +4 -1
- package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
- package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
- package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
- package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
- package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
- package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +27 -20
- package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
- package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
- package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
- package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
- package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
- package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
- package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
- package/kotlin/README.md +18 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +30 -40
- package/skills/contrast/SKILL.md +1 -1
- package/skills/peach-perf/SKILL.md +25 -0
- package/skills/peach-setup/SKILL.md +25 -1
- package/src/assetCatalogContract.ts +31 -3
- package/src/auth/shared-session.ts +3 -2
- package/src/bridge-contract.ts +5 -0
- package/src/bundle-channel.mjs +1 -1
- package/src/cloud-contract.ts +11 -0
- package/src/cloud.ts +2 -2
- package/src/config.ts +16 -8
- package/src/expo-kotlin-host.ts +7 -2
- package/src/guest-bundle.ts +15 -8
- package/src/host/agent-host.ts +2 -4
- package/src/host/bridge-host.ts +52 -54
- package/src/host/fetch-proxy-handler.ts +94 -31
- package/src/host/fetch-proxy-overrides.ts +91 -0
- package/src/host/replacement-module-handler.ts +11 -10
- package/src/host/server-scan-handler.ts +1 -1
- package/src/host/staged-host-files.ts +92 -0
- package/src/host/websocket-proxy.ts +27 -6
- package/src/installed-package-versions.ts +1 -0
- package/src/jump-to-source-native.ts +3 -3
- package/src/kotlin-host.ts +143 -26
- package/src/kotlin-module.ts +52 -0
- package/src/metro-layout-parser.ts +78 -0
- package/src/metro-production-bundle.ts +59 -10
- package/src/native-app-identity-project.ts +67 -3
- package/src/native-app-identity.ts +48 -0
- package/src/native-firebase-config.ts +3 -2
- package/src/native-seam-manifest.ts +0 -44
- package/src/nativeAppIconContract.ts +11 -6
- package/src/nativeViewSourceContract.ts +49 -0
- package/src/nativeViewWebContract.ts +41 -0
- package/src/nitro-source.ts +3 -7
- package/src/one-widget-metadata.ts +23 -3
- package/src/oneWidgetContract.ts +22 -0
- package/src/process-group.ts +306 -0
- package/src/process-liveness.ts +24 -0
- package/src/process-output.ts +21 -2
- package/src/serverDiscoveryContract.ts +41 -0
- package/src/swift-compile.ts +1063 -265
- package/src/swift-design-time.ts +552 -0
- package/src/swift-distribution.lock.json +12 -11
- package/src/swift-host.ts +133 -25
- package/src/swift-objc-sdk.ts +726 -0
- package/src/swift-objc-standin.ts +441 -0
- package/src/swift-package.ts +25 -0
- package/src/swift-packages.ts +25 -10
- package/src/swift-standin-config.ts +94 -0
- package/src/swift-standin-emit.ts +5 -3
- package/src/swift-standins.ts +225 -81
- package/src/swiftContract.ts +4 -0
- package/src/target-matcher.ts +67 -0
- package/src/visit-parsed-program.ts +21 -0
- package/src/vite-plugin-one.ts +58 -16
- package/src/vite-plugin-swift.ts +1 -0
- package/src/vite-plugin.ts +105 -41
- package/src/worker-system-chunks.ts +86 -9
- package/src/worklets-babel.ts +85 -13
- package/src/xcode-project.ts +40 -15
package/cli/commands/control.ts
CHANGED
|
@@ -45,7 +45,9 @@ import {
|
|
|
45
45
|
runtimesDir,
|
|
46
46
|
} from '../../src/home-paths'
|
|
47
47
|
import { projectLoopbackUrl } from '../../src/host/loopback-ports'
|
|
48
|
+
import { stageHostFile } from '../../src/host/staged-host-files'
|
|
48
49
|
import { readInstalledNativePackageVersions } from '../../src/installed-package-versions'
|
|
50
|
+
import { detectBundleModeFromText } from '../../src/metro-production-bundle'
|
|
49
51
|
import {
|
|
50
52
|
canExtractNativeAppIdentity,
|
|
51
53
|
createNativeAppIdentityHeader,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
readExpoConfigAsync,
|
|
60
62
|
readNativeFirebaseConfig,
|
|
61
63
|
} from '../../src/native-firebase-config'
|
|
64
|
+
import { isProcessRunning } from '../../src/process-liveness'
|
|
62
65
|
import { ensureProfile } from '../../src/profiles'
|
|
63
66
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
64
67
|
import {
|
|
@@ -115,6 +118,7 @@ import {
|
|
|
115
118
|
import { readNativeProjectRenderer } from './detect'
|
|
116
119
|
import { inspectDescribe, inspectReadySnapshot, waitReadyReason } from './inspect/core'
|
|
117
120
|
import { isAgentEnv } from './inspect/env'
|
|
121
|
+
import { waitForSimReady } from './inspect/shared'
|
|
118
122
|
import { hasInstalledPeachSkills } from './skills'
|
|
119
123
|
import type { DriverId, DriverLaunchResult } from '../drivers/types'
|
|
120
124
|
|
|
@@ -144,6 +148,11 @@ const DRIVER_CONNECT_INTERVAL_MS = 100
|
|
|
144
148
|
const DRIVER_CONNECT_HOST_GRACE_MS = 5_000
|
|
145
149
|
const OPEN_BRIDGE_SPAWN_TIMEOUT_MS = 10_000
|
|
146
150
|
const OPEN_BRIDGE_RUNTIME_TIMEOUT_MS = 120_000
|
|
151
|
+
// a closed port refuses at once, so this window only matters when something
|
|
152
|
+
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
153
|
+
// finish the handshake, and missing it spawned a second bridge or reported a
|
|
154
|
+
// live daemon as unreachable.
|
|
155
|
+
const LIVE_BRIDGE_HANDSHAKE_TIMEOUT_MS = 2_000
|
|
147
156
|
|
|
148
157
|
// built-in shell apps use pathname-based routing, not ?bundle= params
|
|
149
158
|
const SHELL_APPS: Record<string, string> = {
|
|
@@ -235,7 +244,10 @@ export async function ensureBridgeForOpen(
|
|
|
235
244
|
)
|
|
236
245
|
// a bridge accepts connections while its first runtime download is still
|
|
237
246
|
// running, and every shell page answers 503 until that finishes.
|
|
238
|
-
if (runtimeSource === 'bridge')
|
|
247
|
+
if (runtimeSource === 'bridge') {
|
|
248
|
+
await waitForBridgeRuntime(port)
|
|
249
|
+
await ensureRuntimeForOpen()
|
|
250
|
+
}
|
|
239
251
|
return port
|
|
240
252
|
}
|
|
241
253
|
|
|
@@ -262,13 +274,8 @@ async function connectBridgeForOpen(
|
|
|
262
274
|
): Promise<number> {
|
|
263
275
|
const lock = readDaemonLockfile()
|
|
264
276
|
assertExplicitBridgeLock(preferredPort, explicitPort, lock)
|
|
265
|
-
// an explicit shell origin owns its assets; the bridge only routes commands.
|
|
266
|
-
if (runtimeSource === 'bridge') await ensureRuntimeForOpen()
|
|
267
277
|
const preferredWorld = worldForBridgePort(preferredPort)
|
|
268
|
-
|
|
269
|
-
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
270
|
-
// finish the handshake, and missing it spawned a second bridge.
|
|
271
|
-
const handshake = await bridgeHandshake(preferredPort, 2_000)
|
|
278
|
+
const handshake = await bridgeHandshake(preferredPort, LIVE_BRIDGE_HANDSHAKE_TIMEOUT_MS)
|
|
272
279
|
if ((explicitPort || preferredWorld !== null) && handshake === 'open') {
|
|
273
280
|
return preferredPort
|
|
274
281
|
}
|
|
@@ -279,7 +286,9 @@ async function connectBridgeForOpen(
|
|
|
279
286
|
!explicitPort &&
|
|
280
287
|
lock &&
|
|
281
288
|
isDaemonLockfileFresh(lock) &&
|
|
282
|
-
(
|
|
289
|
+
(lock.bridgePort === preferredPort
|
|
290
|
+
? handshake === 'open'
|
|
291
|
+
: await canConnectToBridge(lock.bridgePort, LIVE_BRIDGE_HANDSHAKE_TIMEOUT_MS))
|
|
283
292
|
) {
|
|
284
293
|
return lock.bridgePort
|
|
285
294
|
}
|
|
@@ -289,6 +298,9 @@ async function connectBridgeForOpen(
|
|
|
289
298
|
)
|
|
290
299
|
}
|
|
291
300
|
|
|
301
|
+
// starting a bridge needs ready local assets; an existing bridge gets
|
|
302
|
+
// to finish its own install before the caller validates them.
|
|
303
|
+
if (runtimeSource === 'bridge') await ensureRuntimeForOpen()
|
|
292
304
|
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
293
305
|
// the bridge outlives open, so it must not hold the command's captured stderr.
|
|
294
306
|
const bridgeLogPath = path.join(
|
|
@@ -510,7 +522,12 @@ function buildShellPathOpenUrl(target: string, baseUrl: string): string {
|
|
|
510
522
|
: targetUrl.pathname
|
|
511
523
|
|
|
512
524
|
url.pathname = `${prefix}${routePath}`
|
|
525
|
+
const baseDemo = url.searchParams.getAll('demo')
|
|
513
526
|
url.search = targetUrl.search
|
|
527
|
+
// demo also sets embed chrome around native routes; a target can override it.
|
|
528
|
+
if (!targetUrl.searchParams.has('demo')) {
|
|
529
|
+
for (const demo of baseDemo) url.searchParams.append('demo', demo)
|
|
530
|
+
}
|
|
514
531
|
url.hash = targetUrl.hash
|
|
515
532
|
return url.toString()
|
|
516
533
|
}
|
|
@@ -639,7 +656,6 @@ function buildConnectOpenUrl(target: string, baseUrl: string): string {
|
|
|
639
656
|
|
|
640
657
|
url.pathname = `${prefix}/rn`
|
|
641
658
|
url.searchParams.delete('bundle')
|
|
642
|
-
url.searchParams.delete('demo')
|
|
643
659
|
url.searchParams.delete('app')
|
|
644
660
|
url.searchParams.delete('open')
|
|
645
661
|
url.searchParams.delete('port')
|
|
@@ -666,6 +682,39 @@ function shouldResolveBundleTargetInCli(target: string): boolean {
|
|
|
666
682
|
)
|
|
667
683
|
}
|
|
668
684
|
|
|
685
|
+
// a built bundle on disk (`peach open ./dist/ios.js`). the bridge serves a
|
|
686
|
+
// staged copy, so it loads with no dev server running.
|
|
687
|
+
function prebuiltBundleFile(target: string): string | null {
|
|
688
|
+
if (!/\.(js|bundle|jsbundle)$/i.test(target)) return null
|
|
689
|
+
const file = path.resolve(process.cwd(), target)
|
|
690
|
+
return fs.statSync(file, { throwIfNoEntry: false })?.isFile() ? file : null
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// the nearest app above the bundle supplies its fonts, splash and identity
|
|
694
|
+
function appDirAbove(file: string): string | undefined {
|
|
695
|
+
for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
|
|
696
|
+
if (resolveAppProject(dir)) return dir
|
|
697
|
+
}
|
|
698
|
+
return undefined
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// the url names the bundle's own mode, so the engine boots the matching
|
|
702
|
+
// tenant first instead of restarting into it after the fetch.
|
|
703
|
+
async function stagePrebuiltBundle(
|
|
704
|
+
file: string,
|
|
705
|
+
bridgePort: number,
|
|
706
|
+
): Promise<ResolvedDevBundle> {
|
|
707
|
+
const mode = await detectBundleModeFromText(fs.readFileSync(file, 'utf8'))
|
|
708
|
+
if (!mode) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`${file} carries no __DEV__ prelude or Metro module shape, so its development or production build cannot be told apart`,
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
const url = new URL(stageHostFile(file, `http://localhost:${bridgePort}`))
|
|
714
|
+
url.searchParams.set('dev', String(mode === 'development'))
|
|
715
|
+
return { bundleUrl: url.toString(), port: bridgePort, framework: 'unknown' }
|
|
716
|
+
}
|
|
717
|
+
|
|
669
718
|
async function buildResolvedBundleOpenUrl(
|
|
670
719
|
target: string,
|
|
671
720
|
baseUrl: string,
|
|
@@ -677,7 +726,6 @@ async function buildResolvedBundleOpenUrl(
|
|
|
677
726
|
url.pathname = `${prefix}/rn`
|
|
678
727
|
url.searchParams.delete('open')
|
|
679
728
|
url.searchParams.delete('port')
|
|
680
|
-
url.searchParams.delete('demo')
|
|
681
729
|
url.searchParams.delete('app')
|
|
682
730
|
const bundleUrl =
|
|
683
731
|
opts.hot === undefined
|
|
@@ -809,7 +857,12 @@ export function resolveShellBaseUrlForBridgePort(bridgePort: number): string {
|
|
|
809
857
|
// fetches itself. auto-discovery only — scans the repo for expo-font config
|
|
810
858
|
// plugin declarations, no user-facing flag. discovery keeps only font files
|
|
811
859
|
// that exist on disk, so the stamp never waits on the dev server.
|
|
812
|
-
function stampAppFonts(
|
|
860
|
+
function stampAppFonts(
|
|
861
|
+
urlStr: string,
|
|
862
|
+
platform: 'ios' | 'android',
|
|
863
|
+
nativeAssetOrigin: string,
|
|
864
|
+
project?: ResolvedAppProject | null,
|
|
865
|
+
): string {
|
|
813
866
|
try {
|
|
814
867
|
const url = new URL(urlStr)
|
|
815
868
|
const bundleUrl = url.searchParams.get('bundle') || ''
|
|
@@ -817,6 +870,8 @@ function stampAppFonts(urlStr: string, project?: ResolvedAppProject | null): str
|
|
|
817
870
|
const specs = discoverNativeLinkedAppFonts({
|
|
818
871
|
bundleUrl,
|
|
819
872
|
projectDir: project.appDir,
|
|
873
|
+
platform,
|
|
874
|
+
nativeAssetOrigin,
|
|
820
875
|
})
|
|
821
876
|
if (specs.length === 0) return urlStr
|
|
822
877
|
url.searchParams.set('appFonts', encodeAppFontsWire(specs))
|
|
@@ -1095,21 +1150,32 @@ export async function buildShellUrl(
|
|
|
1095
1150
|
const project =
|
|
1096
1151
|
opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
|
|
1097
1152
|
const expoRoot = project?.appDir ?? null
|
|
1153
|
+
const world = resolveBridgeWorld()
|
|
1154
|
+
const port = resolveBridgePortForPin({
|
|
1155
|
+
baseUrl,
|
|
1156
|
+
wsPort: opts.bridgePort ?? world.bridgePort,
|
|
1157
|
+
explicitPort: opts.bridgePort !== undefined,
|
|
1158
|
+
})
|
|
1159
|
+
if (project && port.error) throw new Error(port.error)
|
|
1160
|
+
const device = opts.device ?? new URL(resolved).searchParams.get('device')
|
|
1161
|
+
const bundle = bundleUrlOf(resolved)
|
|
1162
|
+
const platform =
|
|
1163
|
+
device && isDeviceModel(device)
|
|
1164
|
+
? getRuntimePlatform(device)
|
|
1165
|
+
: bundle && new URL(bundle).searchParams.get('platform') === 'android'
|
|
1166
|
+
? 'android'
|
|
1167
|
+
: 'ios'
|
|
1098
1168
|
const stamped = stampAppCatalog(
|
|
1099
|
-
await stampAppSplash(
|
|
1169
|
+
await stampAppSplash(
|
|
1170
|
+
stampAppFonts(resolved, platform, `http://localhost:${port.port}`, project),
|
|
1171
|
+
project,
|
|
1172
|
+
opts.oneAppSplash,
|
|
1173
|
+
),
|
|
1100
1174
|
project,
|
|
1101
1175
|
)
|
|
1102
1176
|
const expoStamped = new URL(stamped)
|
|
1103
1177
|
expoStamped.searchParams.delete('nativeAppIdentity')
|
|
1104
1178
|
if (project) {
|
|
1105
|
-
const device = opts.device ?? expoStamped.searchParams.get('device')
|
|
1106
|
-
const bundle = bundleUrlOf(resolved)
|
|
1107
|
-
const platform =
|
|
1108
|
-
device && isDeviceModel(device)
|
|
1109
|
-
? getRuntimePlatform(device)
|
|
1110
|
-
: bundle && new URL(bundle).searchParams.get('platform') === 'android'
|
|
1111
|
-
? 'android'
|
|
1112
|
-
: 'ios'
|
|
1113
1179
|
const nativeRenderer = readNativeProjectRenderer(project.appDir, platform)
|
|
1114
1180
|
if (nativeRenderer !== undefined) {
|
|
1115
1181
|
const config = readPeachConfigFromSearchParams(expoStamped.searchParams)
|
|
@@ -1148,13 +1214,6 @@ export async function buildShellUrl(
|
|
|
1148
1214
|
expoStamped.searchParams.set('bundleOriginOverride', opts.bundleOriginOverride)
|
|
1149
1215
|
if (opts.hostDriver) expoStamped.searchParams.set('peachHostDriver', opts.hostDriver)
|
|
1150
1216
|
if (expoRoot) {
|
|
1151
|
-
const world = resolveBridgeWorld()
|
|
1152
|
-
const port = resolveBridgePortForPin({
|
|
1153
|
-
baseUrl,
|
|
1154
|
-
wsPort: opts.bridgePort ?? world.bridgePort,
|
|
1155
|
-
explicitPort: opts.bridgePort !== undefined,
|
|
1156
|
-
})
|
|
1157
|
-
if (port.error) throw new Error(port.error)
|
|
1158
1217
|
expoStamped.searchParams.set(
|
|
1159
1218
|
'expoSwift',
|
|
1160
1219
|
`http://localhost:${port.port}/__peach/expo-swift?root=${encodeURIComponent(expoRoot)}`,
|
|
@@ -1198,7 +1257,8 @@ function deriveCurrentSimBaseUrl(
|
|
|
1198
1257
|
try {
|
|
1199
1258
|
const url = new URL(source)
|
|
1200
1259
|
url.searchParams.delete('bundle')
|
|
1201
|
-
|
|
1260
|
+
// demo is embed chrome around the next target, like device/theme: the
|
|
1261
|
+
// selected-sim rebuild keeps it unless the target overrides it.
|
|
1202
1262
|
url.searchParams.delete('app')
|
|
1203
1263
|
url.searchParams.delete('open')
|
|
1204
1264
|
url.searchParams.delete('port')
|
|
@@ -1232,65 +1292,6 @@ function withOpenToken(shellUrl: string, openToken: string): string {
|
|
|
1232
1292
|
return url.toString()
|
|
1233
1293
|
}
|
|
1234
1294
|
|
|
1235
|
-
async function waitForSimReady(
|
|
1236
|
-
wsPort: number,
|
|
1237
|
-
commandTimeoutMs: number,
|
|
1238
|
-
simId: string,
|
|
1239
|
-
opts: {
|
|
1240
|
-
attempts?: number
|
|
1241
|
-
intervalMs?: number
|
|
1242
|
-
minNodeCount?: number
|
|
1243
|
-
} = {},
|
|
1244
|
-
) {
|
|
1245
|
-
// the ceiling here is attempts * intervalMs. a hosted launchApp re-evals the
|
|
1246
|
-
// whole guest bundle over a tunnel, which routinely outlasts the local
|
|
1247
|
-
// default, so scale the poll ceiling by the same factor the flow runner
|
|
1248
|
-
// scales its own waits. this still returns the moment the sim reports enough
|
|
1249
|
-
// nodes; only the deadline moves, and locally the scale is 1.
|
|
1250
|
-
const intervalMs = opts.intervalMs ?? 500
|
|
1251
|
-
const attempts = opts.attempts ?? Math.round(30 * flowTimeoutScale())
|
|
1252
|
-
const minNodeCount = opts.minNodeCount ?? 10
|
|
1253
|
-
|
|
1254
|
-
// a poll that only ever reports "timed out" cannot say whether the guest was
|
|
1255
|
-
// still booting, never installed the test hook, or was unreachable, so keep
|
|
1256
|
-
// the last observation and hand it to the caller for the failure message.
|
|
1257
|
-
let lastCount: unknown
|
|
1258
|
-
let lastError = ''
|
|
1259
|
-
|
|
1260
|
-
for (let i = 0; i < attempts; i++) {
|
|
1261
|
-
const bridge = createBridge(wsPort, {
|
|
1262
|
-
commandTimeoutMs,
|
|
1263
|
-
simId,
|
|
1264
|
-
simIdSource: 'flag',
|
|
1265
|
-
})
|
|
1266
|
-
try {
|
|
1267
|
-
const count = await bridge.send({
|
|
1268
|
-
type: 'evaluate',
|
|
1269
|
-
code: '(async () => (await window.__sootsimTest?.getNodeCount()) ?? null)()',
|
|
1270
|
-
})
|
|
1271
|
-
lastCount = count
|
|
1272
|
-
lastError = ''
|
|
1273
|
-
if (typeof count === 'number' && count > minNodeCount) {
|
|
1274
|
-
return { bridge, count }
|
|
1275
|
-
}
|
|
1276
|
-
} catch (err) {
|
|
1277
|
-
lastError = err instanceof Error ? err.message : String(err)
|
|
1278
|
-
}
|
|
1279
|
-
bridge.close()
|
|
1280
|
-
await sleep(intervalMs)
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
const observed = lastError
|
|
1284
|
-
? `last bridge error: ${lastError}`
|
|
1285
|
-
: lastCount === null
|
|
1286
|
-
? 'guest never installed window.__sootsimTest'
|
|
1287
|
-
: `last node count: ${String(lastCount)} (needs > ${minNodeCount})`
|
|
1288
|
-
return {
|
|
1289
|
-
bridge: null,
|
|
1290
|
-
failure: `after ${attempts} polls over ${Math.round((attempts * intervalMs) / 1000)}s; ${observed}`,
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
1295
|
export async function waitForSimMatch(
|
|
1295
1296
|
wsPort: number,
|
|
1296
1297
|
commandTimeoutMs: number,
|
|
@@ -1335,7 +1336,8 @@ export async function waitForSimMatchOrHostExit(
|
|
|
1335
1336
|
const attempts = opts.attempts ?? 30
|
|
1336
1337
|
const intervalMs = opts.intervalMs ?? 500
|
|
1337
1338
|
const watchHost = !!hostPid && Number.isInteger(hostPid) && hostPid > 1
|
|
1338
|
-
const hostExited = () =>
|
|
1339
|
+
const hostExited = () =>
|
|
1340
|
+
watchHost && hostPid !== undefined && !isProcessRunning(hostPid)
|
|
1339
1341
|
for (let i = 0; i < attempts; i++) {
|
|
1340
1342
|
opts.signal?.throwIfAborted()
|
|
1341
1343
|
if (hostExited()) {
|
|
@@ -1444,22 +1446,13 @@ export function printDriverDiagnosticLogTail(result: DriverLaunchResult) {
|
|
|
1444
1446
|
}
|
|
1445
1447
|
}
|
|
1446
1448
|
|
|
1447
|
-
function isProcessAlive(pid: number): boolean {
|
|
1448
|
-
try {
|
|
1449
|
-
process.kill(pid, 0)
|
|
1450
|
-
return true
|
|
1451
|
-
} catch (err) {
|
|
1452
|
-
return (err as NodeJS.ErrnoException)?.code === 'EPERM'
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
1449
|
async function waitForProcessExit(pid: number, timeoutMs = 2_500): Promise<boolean> {
|
|
1457
1450
|
const deadline = Date.now() + timeoutMs
|
|
1458
1451
|
while (Date.now() < deadline) {
|
|
1459
|
-
if (!
|
|
1452
|
+
if (!isProcessRunning(pid)) return true
|
|
1460
1453
|
await sleep(100)
|
|
1461
1454
|
}
|
|
1462
|
-
return !
|
|
1455
|
+
return !isProcessRunning(pid)
|
|
1463
1456
|
}
|
|
1464
1457
|
|
|
1465
1458
|
// a sim record's host pid can outlive its process; after pid reuse the number
|
|
@@ -1468,6 +1461,17 @@ async function waitForProcessExit(pid: number, timeoutMs = 2_500): Promise<boole
|
|
|
1468
1461
|
// whole dev stack down). only signal a pid whose argv proves it is still a
|
|
1469
1462
|
// peach playwright host launcher.
|
|
1470
1463
|
function isPlaywrightHostProcess(pid: number): boolean {
|
|
1464
|
+
// linux reads the environment from /proc: container images often ship no ps.
|
|
1465
|
+
if (process.platform === 'linux') {
|
|
1466
|
+
try {
|
|
1467
|
+
return fs
|
|
1468
|
+
.readFileSync(`/proc/${pid}/environ`, 'utf8')
|
|
1469
|
+
.split('\0')
|
|
1470
|
+
.some((entry) => entry.startsWith('SOOTSIM_PW_URL='))
|
|
1471
|
+
} catch {
|
|
1472
|
+
return false
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1471
1475
|
// `ps eww` prints argv then the process ENVIRONMENT for same-user
|
|
1472
1476
|
// processes. the marker must live in the env SUFFIX (everything past the
|
|
1473
1477
|
// plain argv), because argv text can contain the string coincidentally —
|
|
@@ -1637,7 +1641,6 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1637
1641
|
filter: '',
|
|
1638
1642
|
compact: true,
|
|
1639
1643
|
hideXy: false,
|
|
1640
|
-
maxDepth: 12,
|
|
1641
1644
|
})
|
|
1642
1645
|
if (!result?.tree) return
|
|
1643
1646
|
console.log('')
|
|
@@ -1876,9 +1879,12 @@ export async function runOpenCommand(
|
|
|
1876
1879
|
return
|
|
1877
1880
|
}
|
|
1878
1881
|
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
+
const prebuiltBundle = prebuiltBundleFile(target)
|
|
1883
|
+
// a bundle file gets its url once the bridge that serves it is settled
|
|
1884
|
+
let resolvedBundle =
|
|
1885
|
+
!prebuiltBundle && shouldResolveBundleTargetInCli(target)
|
|
1886
|
+
? await resolveConnectionInputForCli(target)
|
|
1887
|
+
: undefined
|
|
1882
1888
|
const projectRootIndex = args.indexOf('--project-root')
|
|
1883
1889
|
const projectRoot = projectRootIndex < 0 ? undefined : args[projectRootIndex + 1]
|
|
1884
1890
|
if (projectRootIndex >= 0 && (!projectRoot || projectRoot.startsWith('--'))) {
|
|
@@ -1887,7 +1893,7 @@ export async function runOpenCommand(
|
|
|
1887
1893
|
}
|
|
1888
1894
|
const project = await resolveAppProjectForBundleUrl(
|
|
1889
1895
|
resolvedBundle?.bundleUrl,
|
|
1890
|
-
projectRoot,
|
|
1896
|
+
projectRoot ?? (prebuiltBundle ? appDirAbove(prebuiltBundle) : undefined),
|
|
1891
1897
|
)
|
|
1892
1898
|
startNativeFirebaseConfigRead(project)
|
|
1893
1899
|
// playwright is the browser-sim launcher. it owns an isolated profile and
|
|
@@ -2000,10 +2006,25 @@ export async function runOpenCommand(
|
|
|
2000
2006
|
parsed.wsPort,
|
|
2001
2007
|
parsed.explicitPort,
|
|
2002
2008
|
opts.resources,
|
|
2003
|
-
|
|
2009
|
+
!configuredRuntimeVersion && (explicitBaseUrl || looksLikeSootsimUrl(target))
|
|
2010
|
+
? 'external'
|
|
2011
|
+
: 'bridge',
|
|
2004
2012
|
)
|
|
2005
2013
|
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
2006
2014
|
opts.signal?.throwIfAborted()
|
|
2015
|
+
// the shell opens a bundle file as the url the bridge serves it at
|
|
2016
|
+
let shellTarget = target
|
|
2017
|
+
if (prebuiltBundle) {
|
|
2018
|
+
try {
|
|
2019
|
+
resolvedBundle = await stagePrebuiltBundle(prebuiltBundle, parsed.wsPort)
|
|
2020
|
+
shellTarget = resolvedBundle.bundleUrl
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
console.error(
|
|
2023
|
+
` peach open: ${error instanceof Error ? error.message : String(error)}`,
|
|
2024
|
+
)
|
|
2025
|
+
peachExit(1)
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2007
2028
|
|
|
2008
2029
|
// --posture <open|closed> seeds the fold posture for the opened sim,
|
|
2009
2030
|
// stamped on the engine URL next to ?device= so it round-trips instead of
|
|
@@ -2179,7 +2200,7 @@ export async function runOpenCommand(
|
|
|
2179
2200
|
}
|
|
2180
2201
|
if (!targetSim && parsed.simIdSource !== 'flag') {
|
|
2181
2202
|
targetShellUrl = newSimShellUrl = await buildShellUrl(
|
|
2182
|
-
|
|
2203
|
+
shellTarget,
|
|
2183
2204
|
applyPeachConfigToUrl(baseUrl, runtimeConfig),
|
|
2184
2205
|
buildShellUrlOpts,
|
|
2185
2206
|
)
|
|
@@ -2218,7 +2239,7 @@ export async function runOpenCommand(
|
|
|
2218
2239
|
navigatedShellUrl =
|
|
2219
2240
|
targetShellUrl ??
|
|
2220
2241
|
(await buildShellUrl(
|
|
2221
|
-
|
|
2242
|
+
shellTarget,
|
|
2222
2243
|
applyPeachConfigToUrl(resolvedBaseUrl, runtimeConfig),
|
|
2223
2244
|
buildShellUrlOpts,
|
|
2224
2245
|
))
|
|
@@ -2278,6 +2299,7 @@ export async function runOpenCommand(
|
|
|
2278
2299
|
parsed.wsPort,
|
|
2279
2300
|
parsed.commandTimeoutMs,
|
|
2280
2301
|
targetSim.id,
|
|
2302
|
+
{ attempts: Math.round(30 * flowTimeoutScale()), simIdSource: 'flag' },
|
|
2281
2303
|
)
|
|
2282
2304
|
if (!ready.bridge) {
|
|
2283
2305
|
console.error(
|
|
@@ -2309,7 +2331,8 @@ export async function runOpenCommand(
|
|
|
2309
2331
|
|
|
2310
2332
|
const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
|
|
2311
2333
|
const openUrl = withOpenToken(
|
|
2312
|
-
newSimShellUrl ??
|
|
2334
|
+
newSimShellUrl ??
|
|
2335
|
+
(await buildShellUrl(shellTarget, configuredBaseUrl, buildShellUrlOpts)),
|
|
2313
2336
|
token,
|
|
2314
2337
|
)
|
|
2315
2338
|
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
@@ -2401,6 +2424,7 @@ export async function runOpenCommand(
|
|
|
2401
2424
|
const ready = await waitForSimReady(parsed.wsPort, parsed.commandTimeoutMs, match.id, {
|
|
2402
2425
|
intervalMs: connectWait.intervalMs,
|
|
2403
2426
|
attempts: connectWait.attempts,
|
|
2427
|
+
simIdSource: 'flag',
|
|
2404
2428
|
})
|
|
2405
2429
|
if (!ready.bridge) {
|
|
2406
2430
|
console.error(
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
readlinkSync,
|
|
25
25
|
realpathSync,
|
|
26
26
|
rmSync,
|
|
27
|
+
statSync,
|
|
27
28
|
writeFileSync,
|
|
28
29
|
} from 'fs'
|
|
29
30
|
import { homedir } from 'os'
|
|
@@ -365,6 +366,91 @@ function macBridgeHasListener(pid: number, port: number): boolean {
|
|
|
365
366
|
return result.stdout.trim() === String(pid)
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
// a process runs this invocation's `serve` when its executable is the same
|
|
370
|
+
// file and its arguments start with the entry script and `serve`. argv[0] is
|
|
371
|
+
// spelled however the spawner named the file (a symlinked /tmp, a bin shim,
|
|
372
|
+
// a relative path) while process.execPath is resolved, so the executable is
|
|
373
|
+
// compared by device and inode, never by path text. inodes on the system
|
|
374
|
+
// volume exceed 2^53, so both compare as bigints.
|
|
375
|
+
function sameFile(path: string, device: bigint, inode: bigint): boolean {
|
|
376
|
+
try {
|
|
377
|
+
const stats = statSync(path, { bigint: true })
|
|
378
|
+
return stats.dev === device && stats.ino === inode
|
|
379
|
+
} catch {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function serveArgs(invocation: RnxSelfInvocation): string[] {
|
|
385
|
+
return [...invocation.prefixArgs, 'serve']
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function macProcessRunsServe(pid: number, invocation: RnxSelfInvocation): boolean {
|
|
389
|
+
// lsof names each mapped text file by device and inode; the executable is one.
|
|
390
|
+
const files = spawnSync('lsof', ['-nP', '-a', '-p', String(pid), '-d', 'txt', '-FDi'], {
|
|
391
|
+
encoding: 'utf8',
|
|
392
|
+
})
|
|
393
|
+
let device = -1n
|
|
394
|
+
let runsExecutable = false
|
|
395
|
+
for (const line of files.stdout.split('\n')) {
|
|
396
|
+
if (line.startsWith('D')) device = BigInt(line.slice(1))
|
|
397
|
+
else if (
|
|
398
|
+
line.startsWith('i') &&
|
|
399
|
+
sameFile(invocation.executable, device, BigInt(line.slice(1)))
|
|
400
|
+
)
|
|
401
|
+
runsExecutable = true
|
|
402
|
+
}
|
|
403
|
+
if (!runsExecutable) return false
|
|
404
|
+
const args = macProcessArgs(pid)
|
|
405
|
+
return (
|
|
406
|
+
args !== null && serveArgs(invocation).every((arg, index) => args[index + 1] === arg)
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// the kernel's exact argv vector (KERN_PROCARGS2: argc, executable path, then
|
|
411
|
+
// argc NUL-terminated arguments), read through the system JXA Foundation
|
|
412
|
+
// bridge. `ps` flattens argv with spaces and cannot recover argument
|
|
413
|
+
// boundaries.
|
|
414
|
+
function macProcessArgs(pid: number): string[] | null {
|
|
415
|
+
const int32 = (value: number) => {
|
|
416
|
+
const bytes = Buffer.alloc(4)
|
|
417
|
+
bytes.writeUInt32LE(value)
|
|
418
|
+
return bytes
|
|
419
|
+
}
|
|
420
|
+
const capacity = 1 << 20 // ARG_MAX
|
|
421
|
+
const mib = Buffer.concat([int32(1), int32(49), int32(pid)]).toString('base64')
|
|
422
|
+
const size = Buffer.alloc(8)
|
|
423
|
+
size.writeBigUInt64LE(BigInt(capacity))
|
|
424
|
+
const script = `
|
|
425
|
+
ObjC.import('Foundation');
|
|
426
|
+
ObjC.bindFunction('sysctl', ['int', ['void *', 'unsigned int', 'void *', 'void *', 'void *', 'unsigned long']]);
|
|
427
|
+
const mib = $.NSData.alloc.initWithBase64EncodedStringOptions(${JSON.stringify(mib)}, 0).mutableCopy;
|
|
428
|
+
const size = $.NSData.alloc.initWithBase64EncodedStringOptions(${JSON.stringify(size.toString('base64'))}, 0).mutableCopy;
|
|
429
|
+
const out = $.NSMutableData.dataWithLength(${capacity});
|
|
430
|
+
if ($.sysctl(mib.mutableBytes, 3, out.mutableBytes, size.mutableBytes, null, 0) !== 0)
|
|
431
|
+
throw new Error('sysctl KERN_PROCARGS2 failed');
|
|
432
|
+
ObjC.unwrap(size.base64EncodedStringWithOptions(0)) + ' ' + ObjC.unwrap(out.base64EncodedStringWithOptions(0));
|
|
433
|
+
`
|
|
434
|
+
// runner-toolchain-platform: darwin
|
|
435
|
+
const result = spawnSync('/usr/bin/osascript', ['-l', 'JavaScript', '-e', script], {
|
|
436
|
+
encoding: 'utf8',
|
|
437
|
+
maxBuffer: 4 * capacity,
|
|
438
|
+
})
|
|
439
|
+
if (result.status !== 0) return null
|
|
440
|
+
const [length, data] = result.stdout.trim().split(' ')
|
|
441
|
+
const buffer = Buffer.from(data, 'base64').subarray(
|
|
442
|
+
0,
|
|
443
|
+
Number(Buffer.from(length, 'base64').readBigUInt64LE()),
|
|
444
|
+
)
|
|
445
|
+
const argc = buffer.readInt32LE(0)
|
|
446
|
+
let at = buffer.indexOf(0, 4) // end of the executable path
|
|
447
|
+
while (buffer[at] === 0) at++ // padding before argv[0]
|
|
448
|
+
const args: string[] = []
|
|
449
|
+
for (; args.length < argc; at = buffer.indexOf(0, at) + 1)
|
|
450
|
+
args.push(buffer.toString('utf8', at, buffer.indexOf(0, at)))
|
|
451
|
+
return args
|
|
452
|
+
}
|
|
453
|
+
|
|
368
454
|
export async function stopMacDaemonBridge(
|
|
369
455
|
invocation: RnxSelfInvocation,
|
|
370
456
|
target: string | null = macServiceTarget(),
|
|
@@ -373,16 +459,9 @@ export async function stopMacDaemonBridge(
|
|
|
373
459
|
const lock = readDaemonLockfile()
|
|
374
460
|
if (isDaemonLockfileFresh(lock) && lock.pid !== service?.pid) {
|
|
375
461
|
// a lockfile alone cannot authorize a signal after pid reuse.
|
|
376
|
-
const ownsListener = macBridgeHasListener(lock.pid, lock.bridgePort)
|
|
377
|
-
const processInfo = spawnSync('ps', ['-o', 'command=', '-p', String(lock.pid)], {
|
|
378
|
-
encoding: 'utf8',
|
|
379
|
-
})
|
|
380
|
-
const command = [invocation.executable, ...invocation.prefixArgs, 'serve'].join(' ')
|
|
381
|
-
const runningCommand = processInfo.stdout.trim()
|
|
382
462
|
if (
|
|
383
|
-
!
|
|
384
|
-
|
|
385
|
-
(runningCommand !== command && !runningCommand.startsWith(`${command} `))
|
|
463
|
+
!macBridgeHasListener(lock.pid, lock.bridgePort) ||
|
|
464
|
+
!macProcessRunsServe(lock.pid, invocation)
|
|
386
465
|
) {
|
|
387
466
|
throw new Error(
|
|
388
467
|
`cannot hand off bridge on port ${lock.bridgePort}: pid ${lock.pid} is not this Peach invocation`,
|
|
@@ -441,7 +520,7 @@ export async function startMacDaemon(
|
|
|
441
520
|
|
|
442
521
|
// --- linux bridge ownership --------------------------------------------
|
|
443
522
|
|
|
444
|
-
// the linux counterparts to the mac listener and
|
|
523
|
+
// the linux counterparts to the mac listener and executable checks above. both
|
|
445
524
|
// read /proc directly instead of shelling out to lsof, ss or ps, so minimal
|
|
446
525
|
// containers and fresh users get the same ownership proof with no extra
|
|
447
526
|
// binaries installed.
|
|
@@ -483,16 +562,16 @@ function linuxBridgeHasListener(pid: number, port: number): boolean {
|
|
|
483
562
|
return false
|
|
484
563
|
}
|
|
485
564
|
|
|
486
|
-
function
|
|
487
|
-
// /proc
|
|
488
|
-
//
|
|
565
|
+
function linuxProcessRunsServe(pid: number, invocation: RnxSelfInvocation): boolean {
|
|
566
|
+
// /proc/<pid>/exe resolves to the running executable and cmdline is the argv
|
|
567
|
+
// vector verbatim, so both compare exactly without procps installed.
|
|
489
568
|
try {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return
|
|
569
|
+
const exe = statSync(`/proc/${pid}/exe`, { bigint: true })
|
|
570
|
+
if (!sameFile(invocation.executable, exe.dev, exe.ino)) return false
|
|
571
|
+
const args = readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').slice(1)
|
|
572
|
+
return serveArgs(invocation).every((arg, index) => args[index] === arg)
|
|
494
573
|
} catch {
|
|
495
|
-
return
|
|
574
|
+
return false
|
|
496
575
|
}
|
|
497
576
|
}
|
|
498
577
|
|
|
@@ -514,13 +593,9 @@ export async function stopLinuxDaemonBridge(
|
|
|
514
593
|
const lock = readDaemonLockfile()
|
|
515
594
|
if (!isDaemonLockfileFresh(lock)) return
|
|
516
595
|
// a lockfile alone cannot authorize a signal after pid reuse.
|
|
517
|
-
const ownsListener = linuxBridgeHasListener(lock.pid, lock.bridgePort)
|
|
518
|
-
const runningCommand = readLinuxProcessCommand(lock.pid)
|
|
519
|
-
const command = [invocation.executable, ...invocation.prefixArgs, 'serve'].join(' ')
|
|
520
596
|
if (
|
|
521
|
-
!
|
|
522
|
-
|
|
523
|
-
(runningCommand !== command && !runningCommand.startsWith(`${command} `))
|
|
597
|
+
!linuxBridgeHasListener(lock.pid, lock.bridgePort) ||
|
|
598
|
+
!linuxProcessRunsServe(lock.pid, invocation)
|
|
524
599
|
) {
|
|
525
600
|
throw new Error(
|
|
526
601
|
`cannot hand off bridge on port ${lock.bridgePort}: pid ${lock.pid} is not this Peach invocation`,
|
package/cli/commands/debug.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
ImageAuditReport,
|
|
41
41
|
ImageAuditRow,
|
|
42
42
|
} from '../../../peach-engine/src/render-worker/compositor-protocol'
|
|
43
|
+
import type { PeachDebugSnapshotOperation } from '@peach/globals'
|
|
43
44
|
|
|
44
45
|
interface DebugOptions {
|
|
45
46
|
port?: number
|
|
@@ -70,6 +71,7 @@ usage:
|
|
|
70
71
|
|
|
71
72
|
record, recent, and clear-events target the tenant; --host selects the page buffer.
|
|
72
73
|
snapshot, snapshots, diff, and clear-snapshots also target the tenant by default.
|
|
74
|
+
--shell selects the mounted shell worker trees for snapshot operations.
|
|
73
75
|
enable/disable/toggle also update the tenant unless --host is passed.
|
|
74
76
|
shell event buffers are separate; shell console logs are forwarded to the host.
|
|
75
77
|
|
|
@@ -376,7 +378,7 @@ function printImageAudit(
|
|
|
376
378
|
export async function runDebug(args: string[], opts: DebugOptions) {
|
|
377
379
|
const parsed = parseBridgeCliArgs(args, {
|
|
378
380
|
port: opts.port,
|
|
379
|
-
stripBooleanFlags: ['--pretty', '--json', '--help', '-h', '--host'],
|
|
381
|
+
stripBooleanFlags: ['--pretty', '--json', '--help', '-h', '--host', '--shell'],
|
|
380
382
|
})
|
|
381
383
|
const positional = parsed.positional
|
|
382
384
|
const subcommand = positional[0]
|
|
@@ -422,6 +424,32 @@ export async function runDebug(args: string[], opts: DebugOptions) {
|
|
|
422
424
|
const bridge = createBridgeFromParsed(parsed)
|
|
423
425
|
|
|
424
426
|
try {
|
|
427
|
+
if (args.includes('--host') && args.includes('--shell')) {
|
|
428
|
+
throw new Error('select either --host or --shell')
|
|
429
|
+
}
|
|
430
|
+
if (
|
|
431
|
+
!args.includes('--host') &&
|
|
432
|
+
(sub === 'snapshot' ||
|
|
433
|
+
sub === 'snapshots' ||
|
|
434
|
+
sub === 'diff' ||
|
|
435
|
+
sub === 'clear-snapshots')
|
|
436
|
+
) {
|
|
437
|
+
const operations = {
|
|
438
|
+
snapshot: 'snapshot',
|
|
439
|
+
snapshots: 'snapshots',
|
|
440
|
+
diff: 'diff',
|
|
441
|
+
'clear-snapshots': 'clearSnapshots',
|
|
442
|
+
} satisfies Record<string, PeachDebugSnapshotOperation>
|
|
443
|
+
const result = await callTestBridge(
|
|
444
|
+
bridge,
|
|
445
|
+
'debugSnapshot',
|
|
446
|
+
operations[sub],
|
|
447
|
+
rest,
|
|
448
|
+
args.includes('--shell') ? 'shell' : 'tenant',
|
|
449
|
+
)
|
|
450
|
+
console.log(fmt(result, pretty))
|
|
451
|
+
return
|
|
452
|
+
}
|
|
425
453
|
switch (sub) {
|
|
426
454
|
case 'enable': {
|
|
427
455
|
const channels = parseChannelList(rest[0])
|