peachsim 0.1.1635 → 0.1.1640
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 +5 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +62 -13
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/commands/doctor.ts +13 -5
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -47
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +97 -47
- package/cli/commands/swift.ts +9 -3
- package/cli/commands/upload.ts +48 -42
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -10
- package/detox/attempt-events.cjs +62 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +121 -47
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +1 -1
- package/dist-lib/backend-origin.cjs +8 -7
- 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 +4038 -1983
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +509 -86
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3551 -1574
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +43633 -2635
- 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 +2748 -128
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- 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 +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +2547 -83
- package/dist-lib/native-app-identity-project.cjs +237 -50
- package/dist-lib/native-app-identity-project.mjs +237 -50
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- 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 +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +65403 -22739
- package/dist-lib/swift-compile.cjs +93 -32
- package/dist-lib/swift-compile.mjs +93 -32
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +7 -5
- package/dist-lib/swift-submit.mjs +7 -5
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7335 -5432
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +13 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +21 -6
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +91 -33
- package/src/swift-distribution.lock.json +22 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +89 -26
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +689 -18
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +39 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +3 -5
- package/src/swift-toolchain.ts +378 -46
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/cli/commands/upload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// peach record upload —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// peach record upload — publish a recording from a running simulator. scene
|
|
2
|
+
// recordings carry their own content identity; --live-bundle adds the app
|
|
3
|
+
// bundle for interactive playback under the existing bundle identity.
|
|
4
4
|
//
|
|
5
5
|
// uploads with no repository association are private by default: only
|
|
6
6
|
// signed-in members of the uploader's account can open the link until the
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
parseBridgeCliArgs,
|
|
61
61
|
} from '../ws-bridge'
|
|
62
62
|
import type { SootSimTimelineEvent } from '@peach/globals'
|
|
63
|
+
import type { BundleSnapshot } from 'peach-engine/preview/capture-bundle'
|
|
63
64
|
|
|
64
65
|
interface UploadOptions {
|
|
65
66
|
port?: number
|
|
@@ -86,19 +87,6 @@ function parseServerMessage(responseText?: string): string | null {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
type BundleSnapshot = {
|
|
90
|
-
bundleUrl: string | null
|
|
91
|
-
entry: string | null
|
|
92
|
-
deviceSpec: InitRequestBody['deviceSpec']
|
|
93
|
-
runtimeVersion: string | null
|
|
94
|
-
pageHref: string
|
|
95
|
-
transformedBundle: {
|
|
96
|
-
url: string
|
|
97
|
-
byteLength: number
|
|
98
|
-
timestamp: number
|
|
99
|
-
} | null
|
|
100
|
-
}
|
|
101
|
-
|
|
102
90
|
type AttachedInteractionSnapshot = {
|
|
103
91
|
meta: NonNullable<InitRequestBody['snapshots']>[number] & { id: string }
|
|
104
92
|
bytes: Uint8Array
|
|
@@ -184,10 +172,10 @@ export function resolvePublicPreviewOrigin(
|
|
|
184
172
|
|
|
185
173
|
function printHelp() {
|
|
186
174
|
console.log(`
|
|
187
|
-
peach record upload — publish
|
|
175
|
+
peach record upload — publish a recording as a /preview/<id> link
|
|
188
176
|
|
|
189
177
|
usage:
|
|
190
|
-
peach record upload [--origin <url>] [--public-origin <url>] (--events <path> | --video <path>) [--screenshot <path>] [--owner <org> --repo <repo>] [--sim <sim>] [--open] [--assets-only]
|
|
178
|
+
peach record upload [--origin <url>] [--public-origin <url>] (--events <path> | --video <path> | --scene <path>) [--live-bundle] [--screenshot <path>] [--owner <org> --repo <repo>] [--sim <sim>] [--open] [--assets-only]
|
|
191
179
|
|
|
192
180
|
options:
|
|
193
181
|
--origin <url> upload target (default: ${REMOTE_DEFAULT_ORIGIN})
|
|
@@ -210,7 +198,9 @@ options:
|
|
|
210
198
|
duration hint for attached flow video
|
|
211
199
|
--scene <path> a scene recording (.psr.gz) of a remote viewer's display
|
|
212
200
|
stream. the preview paints it through the simulator's
|
|
213
|
-
renderer instead of playing a video
|
|
201
|
+
renderer instead of playing a video; uploads without a
|
|
202
|
+
live bundle by default
|
|
203
|
+
--live-bundle include the running app bundle for Live alongside --scene
|
|
214
204
|
--timeline-events <path>
|
|
215
205
|
extra trace timeline rows, JSONL or JSONL gz
|
|
216
206
|
--trace-profile <path>
|
|
@@ -443,6 +433,12 @@ export async function runUpload(
|
|
|
443
433
|
const snapshotManifestPath = extractStringFlag(mutableArgs, '--snapshot-manifest')
|
|
444
434
|
const videoPath = extractStringFlag(mutableArgs, '--video')
|
|
445
435
|
const scenePath = extractStringFlag(mutableArgs, '--scene')
|
|
436
|
+
const includeLiveBundle = extractBoolFlag(mutableArgs, '--live-bundle')
|
|
437
|
+
if (includeLiveBundle && !scenePath) {
|
|
438
|
+
console.error(' --live-bundle requires --scene')
|
|
439
|
+
peachExit(1)
|
|
440
|
+
}
|
|
441
|
+
const hasBundle = includeLiveBundle || !scenePath
|
|
446
442
|
const timelineEventsPath = extractStringFlag(mutableArgs, '--timeline-events')
|
|
447
443
|
const traceProfilePath = extractStringFlag(mutableArgs, '--trace-profile')
|
|
448
444
|
const failureBundleDir = extractStringFlag(mutableArgs, '--failure-bundle')
|
|
@@ -669,7 +665,7 @@ export async function runUpload(
|
|
|
669
665
|
}
|
|
670
666
|
}
|
|
671
667
|
}
|
|
672
|
-
if (snapshot?.transformedBundle) {
|
|
668
|
+
if (hasBundle && snapshot?.transformedBundle) {
|
|
673
669
|
transformedBundleText = await pullTransformedBundle(
|
|
674
670
|
bridge,
|
|
675
671
|
snapshot.transformedBundle.byteLength,
|
|
@@ -712,7 +708,7 @@ export async function runUpload(
|
|
|
712
708
|
)
|
|
713
709
|
peachExit(2)
|
|
714
710
|
}
|
|
715
|
-
if (!snapshot.bundleUrl) {
|
|
711
|
+
if (hasBundle && !snapshot.bundleUrl) {
|
|
716
712
|
console.error(
|
|
717
713
|
' no ?bundle= URL on the current peach tab.\n' +
|
|
718
714
|
' open the app you want to share first (e.g. peach open 8082), then run upload.',
|
|
@@ -725,15 +721,15 @@ export async function runUpload(
|
|
|
725
721
|
// does not have to re-run sootsim's transforms. fall back to re-fetching
|
|
726
722
|
// the raw metro URL + forcing lazy=false when the transform hasn't
|
|
727
723
|
// published yet (e.g., tab was opened but bundle not finished loading).
|
|
728
|
-
let bundleBytes: Uint8Array
|
|
724
|
+
let bundleBytes: Uint8Array | undefined
|
|
729
725
|
let isTransformed = false
|
|
730
|
-
if (transformedBundleText !== null) {
|
|
726
|
+
if (hasBundle && transformedBundleText !== null) {
|
|
731
727
|
bundleBytes = new TextEncoder().encode(transformedBundleText)
|
|
732
728
|
isTransformed = true
|
|
733
729
|
console.log(
|
|
734
730
|
` using post-transform bundle: ${(bundleBytes.byteLength / 1024).toFixed(1)} KiB`,
|
|
735
731
|
)
|
|
736
|
-
} else {
|
|
732
|
+
} else if (hasBundle && snapshot.bundleUrl) {
|
|
737
733
|
const fetchBundleUrl = forceInlineBundle(snapshot.bundleUrl)
|
|
738
734
|
if (fetchBundleUrl !== snapshot.bundleUrl) {
|
|
739
735
|
console.log(` forcing lazy=false for self-contained bundle`)
|
|
@@ -922,7 +918,7 @@ export async function runUpload(
|
|
|
922
918
|
|
|
923
919
|
const bundleOrigin = (() => {
|
|
924
920
|
try {
|
|
925
|
-
return new URL(snapshot.bundleUrl).origin
|
|
921
|
+
return snapshot.bundleUrl ? new URL(snapshot.bundleUrl).origin : null
|
|
926
922
|
} catch {
|
|
927
923
|
return null
|
|
928
924
|
}
|
|
@@ -1037,8 +1033,8 @@ export async function runUpload(
|
|
|
1037
1033
|
// explicitly so the installed app keeps its identity in share replay.
|
|
1038
1034
|
const appIdentityFiles: PreparedFile[] = []
|
|
1039
1035
|
let appManifest: string | undefined
|
|
1040
|
-
const metroOrigin = loopbackOriginOf(snapshot.bundleUrl)
|
|
1041
|
-
if (metroOrigin) {
|
|
1036
|
+
const metroOrigin = snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
|
|
1037
|
+
if (metroOrigin && snapshot.bundleUrl) {
|
|
1042
1038
|
const platform = new URL(snapshot.bundleUrl).searchParams.get('platform') || 'ios'
|
|
1043
1039
|
const manifestUrl = `${metroOrigin}/`
|
|
1044
1040
|
const response = await fetch(manifestUrl, {
|
|
@@ -1120,19 +1116,31 @@ export async function runUpload(
|
|
|
1120
1116
|
// brotli source bytes for the upload. finalize verifies the source hash,
|
|
1121
1117
|
// minifies in the node build container, and stores one canonical brotli
|
|
1122
1118
|
// bundle. assets stay gzip below.
|
|
1123
|
-
const bundleBr = brotliBundle(bundleBytes)
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
)
|
|
1119
|
+
const bundleBr = bundleBytes ? brotliBundle(bundleBytes) : undefined
|
|
1120
|
+
const identityBytes = bundleBytes ?? sceneBytes
|
|
1121
|
+
if (!identityBytes) throw new Error('upload requires a bundle or scene recording')
|
|
1122
|
+
const contentHash = createHash('sha256').update(identityBytes).digest('hex')
|
|
1123
|
+
if (bundleBytes && bundleBr) {
|
|
1124
|
+
console.log(
|
|
1125
|
+
` bundle raw=${(bundleBytes.byteLength / 1024).toFixed(1)} KiB br=${(bundleBr.byteLength / 1024).toFixed(1)} KiB`,
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1128
1128
|
|
|
1129
1129
|
const initBody: InitRequestBody = {
|
|
1130
|
-
contentHash
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1130
|
+
contentHash,
|
|
1131
|
+
hasBundle,
|
|
1132
|
+
...(bundleBytes && snapshot.bundleUrl
|
|
1133
|
+
? {
|
|
1134
|
+
bundleSizeBytes: bundleBytes.byteLength,
|
|
1135
|
+
bundleNativeAppIdentityHeader: snapshot.bundleNativeAppIdentityHeader,
|
|
1136
|
+
bundleContentType: guessBundleContentType(snapshot.bundleUrl),
|
|
1137
|
+
bundleEncoding: 'br',
|
|
1138
|
+
bundleMinified: false,
|
|
1139
|
+
bundleOrigin,
|
|
1140
|
+
entry: snapshot.entry,
|
|
1141
|
+
isTransformed,
|
|
1142
|
+
}
|
|
1143
|
+
: {}),
|
|
1136
1144
|
runtimeVersion: snapshot.runtimeVersion ?? undefined,
|
|
1137
1145
|
// config-only fonts resolved by `peach open` and carried on the engine
|
|
1138
1146
|
// URL. persist them onto the share meta so /preview/<id> re-applies
|
|
@@ -1143,8 +1151,6 @@ export async function runUpload(
|
|
|
1143
1151
|
// /preview/<id> re-applies `?appSplash=` and paints the same splash; the
|
|
1144
1152
|
// image bytes are staged above so the engine's <img> resolves in preview.
|
|
1145
1153
|
appSplash: appSplashWire || undefined,
|
|
1146
|
-
entry: snapshot.entry,
|
|
1147
|
-
isTransformed,
|
|
1148
1154
|
deviceSpec: snapshot.deviceSpec,
|
|
1149
1155
|
installationId: gitHubIdentity?.installationId ?? undefined,
|
|
1150
1156
|
repoId: gitHubIdentity?.repoId,
|
|
@@ -1227,7 +1233,7 @@ export async function runUpload(
|
|
|
1227
1233
|
}
|
|
1228
1234
|
|
|
1229
1235
|
const putT0 = Date.now()
|
|
1230
|
-
let putBytes = bundleBr
|
|
1236
|
+
let putBytes = bundleBr?.byteLength ?? 0
|
|
1231
1237
|
for (const f of prepared) putBytes += f.bytes.byteLength
|
|
1232
1238
|
if (eventsGz) putBytes += eventsGz.byteLength
|
|
1233
1239
|
putBytes += timelineGz.byteLength
|
|
@@ -1263,7 +1269,7 @@ export async function runUpload(
|
|
|
1263
1269
|
})
|
|
1264
1270
|
finalize = result.finalize
|
|
1265
1271
|
const totalObjects =
|
|
1266
|
-
1 +
|
|
1272
|
+
(result.init.bundle ? 1 : 0) +
|
|
1267
1273
|
result.init.files.length +
|
|
1268
1274
|
(result.init.events ? 1 : 0) +
|
|
1269
1275
|
(result.init.timeline ? 1 : 0) +
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
softwareChromeLaunchOptions,
|
|
23
23
|
} from '../../../../scripts/lib/reap-browser'
|
|
24
24
|
import { createBrowserPageTargetReader } from '../../../peach-engine/src/browser-page-target'
|
|
25
|
+
import { captureBrowserAccessibility } from '../../../peach-engine/src/capture/browser-accessibility-host'
|
|
25
26
|
import {
|
|
26
27
|
readBrowserCompositeCaptureRequest,
|
|
27
28
|
resolveBrowserCompositeCdpClip,
|
|
@@ -108,10 +109,10 @@ const viewportSpec = /^(\\d+)x(\\d+)$/.exec(process.env.SOOTSIM_PW_VIEWPORT || '
|
|
|
108
109
|
const viewport = viewportSpec
|
|
109
110
|
? { width: Number(viewportSpec[1]), height: Number(viewportSpec[2]) }
|
|
110
111
|
: { width: 600, height: 900 };
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
112
|
+
// host display pixels per css pixel; default to a retina Mac browser's 2.
|
|
113
|
+
// medium renders at 2 and high at device native, independently of browser DPR.
|
|
114
|
+
// low follows measured on-screen density, capped at device native.
|
|
115
|
+
// SOOTSIM_PW_DSF changes browser DPR, not the selected render resolution.
|
|
115
116
|
const deviceScaleFactor = Number(process.env.SOOTSIM_PW_DSF || 2);
|
|
116
117
|
const closeTimeoutMs = Number(process.env.SOOTSIM_PW_CLOSE_TIMEOUT_MS || 2500);
|
|
117
118
|
const connectAckFile = process.env.SOOTSIM_PW_CONNECTED_ACK_FILE || '';
|
|
@@ -126,6 +127,7 @@ const listProcessChildren = ${listProcessChildren.toString()};
|
|
|
126
127
|
const { tmpdir } = require('os');
|
|
127
128
|
const { join } = require('path');
|
|
128
129
|
const createBrowserPointerDispatcher = ${createBrowserPointerDispatcher.toString()};
|
|
130
|
+
const captureBrowserAccessibility = ${captureBrowserAccessibility.toString()};
|
|
129
131
|
const automateWebViewFrames = ${automateWebViewFrames.toString()};
|
|
130
132
|
const automateWebAuthenticationSessions = ${automateWebAuthenticationSessions.toString()};
|
|
131
133
|
const webViewAutomationHelpers = ${webViewAutomationHelpersSource()};
|
|
@@ -143,12 +145,9 @@ const readChromiumQueue = ${readChromiumQueue.toString()};
|
|
|
143
145
|
// aborted/closed \`peach open\` leaked a renderer + gpu helper burning 100%+
|
|
144
146
|
// CPU until manually killed (multiple incidents 2026-06).
|
|
145
147
|
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// — Chrome for Testing and bundled playwright chromium alike. as a side benefit we
|
|
150
|
-
// no longer need the launch / launchPersistentContext fork: we ALWAYS use a
|
|
151
|
-
// profile dir (mkdtemp'd if the caller didn't pass one, then removed at exit).
|
|
148
|
+
// a named profile can already belong to another host. its path finds browser
|
|
149
|
+
// candidates; the unique launch environment proves which ones we may reap,
|
|
150
|
+
// including helpers reparented after the browser root exits.
|
|
152
151
|
const reapEnabled = process.platform !== 'win32';
|
|
153
152
|
let profileDir = callerUserDataDir;
|
|
154
153
|
let ephemeralProfile = false;
|
|
@@ -160,33 +159,35 @@ if (!profileDir) {
|
|
|
160
159
|
// host a bounded temporary cache so persistent named profiles retain cookies
|
|
161
160
|
// and storage without multiplying a private cache for every profile.
|
|
162
161
|
const diskCacheDir = mkdtempSync(join(tmpdir(), 'peach-playwright-cache-'));
|
|
163
|
-
|
|
162
|
+
let profileLaunched = false;
|
|
163
|
+
// [pid, command line] for every process on the profile. linux reads /proc,
|
|
164
|
+
// since minimal and rootless images ship no ps.
|
|
165
|
+
const profileProcesses = () => {
|
|
164
166
|
if (!reapEnabled) return [];
|
|
165
167
|
try {
|
|
166
168
|
// match the whole --user-data-dir argument: a pattern search also found
|
|
167
169
|
// browsers whose profile path merely starts with this one.
|
|
168
170
|
const flag = ' --user-data-dir=' + profileDir + ' ';
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
maxBuffer: Infinity,
|
|
187
|
-
}).toString().split('\\n').filter(Boolean);
|
|
188
|
-
} catch { return []; }
|
|
171
|
+
const rows = process.platform === 'linux'
|
|
172
|
+
? readdirSync('/proc').filter((name) => /^\\d+$/.test(name)).map((name) => {
|
|
173
|
+
let command = '';
|
|
174
|
+
// a process can exit between the listing and this read
|
|
175
|
+
try { command = readFileSync('/proc/' + name + '/cmdline', 'utf8').split('\\0').join(' ').trim(); } catch {}
|
|
176
|
+
return [Number(name), command];
|
|
177
|
+
})
|
|
178
|
+
: execSync('ps -axo pid=,command=', {
|
|
179
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
180
|
+
maxBuffer: Infinity,
|
|
181
|
+
})
|
|
182
|
+
.toString().split('\\n').map((line) => line.trim().match(/^(\\d+)\\s+(.*)$/))
|
|
183
|
+
.filter(Boolean).map((row) => [Number(row[1]), row[2]]);
|
|
184
|
+
return rows.filter(([pid, command]) => pid && pid !== process.pid && (' ' + command + ' ').includes(flag));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error('[playwright-driver] cannot establish browser profile ownership: ' + String(error));
|
|
187
|
+
}
|
|
189
188
|
};
|
|
189
|
+
const chromePidsByProfile = () => profileProcesses().map(([pid]) => pid);
|
|
190
|
+
const chromeCommandsByProfile = () => profileProcesses().map(([, command]) => command);
|
|
190
191
|
const assertNoUnsafeSwiftShaderFlag = () => {
|
|
191
192
|
if (!hardwareGpuRequired) return;
|
|
192
193
|
const unsafe = chromeCommandsByProfile().find((command) =>
|
|
@@ -207,7 +208,29 @@ const killTree = (pid) => {
|
|
|
207
208
|
for (const k of kids) killTree(k);
|
|
208
209
|
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
209
210
|
};
|
|
210
|
-
const reapChrome = () => {
|
|
211
|
+
const reapChrome = () => {
|
|
212
|
+
try {
|
|
213
|
+
for (const pid of chromePidsByProfile()) {
|
|
214
|
+
let owned = false;
|
|
215
|
+
if (process.platform === 'linux') {
|
|
216
|
+
try {
|
|
217
|
+
owned = readFileSync('/proc/' + pid + '/environ', 'utf8').split('\\0')
|
|
218
|
+
.includes('PEACH_PW_BROWSER_OWNER=' + diskCacheDir);
|
|
219
|
+
} catch {}
|
|
220
|
+
} else {
|
|
221
|
+
const plain = spawnSync('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
222
|
+
const withEnv = spawnSync('ps', ['eww', '-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
223
|
+
const argv = (plain.stdout || '').trimEnd();
|
|
224
|
+
const full = (withEnv.stdout || '').trimEnd();
|
|
225
|
+
owned = plain.status === 0 && withEnv.status === 0 && full.startsWith(argv) &&
|
|
226
|
+
(full.slice(argv.length) + ' ').includes(' PEACH_PW_BROWSER_OWNER=' + diskCacheDir + ' ');
|
|
227
|
+
}
|
|
228
|
+
if (owned) killTree(pid);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
process.stderr.write('[playwright-driver] browser teardown refused: ' + String(error) + '\\n');
|
|
232
|
+
}
|
|
233
|
+
};
|
|
211
234
|
const removeDisposableCaches = (root) => {
|
|
212
235
|
let children = [];
|
|
213
236
|
try { children = readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
@@ -237,7 +260,7 @@ const cleanupProfile = () => {
|
|
|
237
260
|
try { rmSync(profileDir, { recursive: true, force: true }); } catch {}
|
|
238
261
|
return;
|
|
239
262
|
}
|
|
240
|
-
removeDisposableCaches(profileDir);
|
|
263
|
+
if (profileLaunched) removeDisposableCaches(profileDir);
|
|
241
264
|
};
|
|
242
265
|
(async () => {
|
|
243
266
|
const pw = require(modulePath);
|
|
@@ -401,9 +424,8 @@ const cleanupProfile = () => {
|
|
|
401
424
|
await bounded('desktop host stop', stopDesktopHost, timeoutMs);
|
|
402
425
|
if (context) await bounded('context.close', () => context.close(), timeoutMs);
|
|
403
426
|
await terminateBrowserProcess();
|
|
404
|
-
// close()
|
|
405
|
-
//
|
|
406
|
-
// we always sweep by our unique profile dir as the load-bearing teardown.
|
|
427
|
+
// close() can leave reparented helpers; reap only this launch's browser
|
|
428
|
+
// and helpers, even if another host now uses the same named profile.
|
|
407
429
|
reapChrome();
|
|
408
430
|
cleanupProfile();
|
|
409
431
|
if (connectAckFile) {
|
|
@@ -494,10 +516,11 @@ const cleanupProfile = () => {
|
|
|
494
516
|
const opts = channel
|
|
495
517
|
? { headless, channel, viewport, deviceScaleFactor }
|
|
496
518
|
: { headless, viewport, deviceScaleFactor };
|
|
519
|
+
opts.env = { ...process.env, PEACH_PW_BROWSER_OWNER: diskCacheDir };
|
|
497
520
|
opts.executablePath = executablePath;
|
|
498
521
|
if (libraryPath) {
|
|
499
522
|
opts.env = {
|
|
500
|
-
...
|
|
523
|
+
...opts.env,
|
|
501
524
|
LD_LIBRARY_PATH: [libraryPath, process.env.LD_LIBRARY_PATH].filter(Boolean).join(':'),
|
|
502
525
|
};
|
|
503
526
|
}
|
|
@@ -561,11 +584,9 @@ const cleanupProfile = () => {
|
|
|
561
584
|
// room for chrome's own toolbar, so the window isn't desktop-shaped
|
|
562
585
|
// around a letterboxed page.
|
|
563
586
|
if (!headless) opts.args.push('--window-size=' + viewport.width + ',' + (viewport.height + 88));
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// an orphaned helper back to us, which is the leak we just fixed.
|
|
568
|
-
return await pw.chromium.launchPersistentContext(profileDir, opts);
|
|
587
|
+
const launched = await pw.chromium.launchPersistentContext(profileDir, opts);
|
|
588
|
+
profileLaunched = true;
|
|
589
|
+
return launched;
|
|
569
590
|
};
|
|
570
591
|
// the host-wide chromium slot is held for this host's life; its exit frees it
|
|
571
592
|
await acquireChromiumAdmission({
|
|
@@ -663,6 +684,8 @@ const cleanupProfile = () => {
|
|
|
663
684
|
await shutdown(0);
|
|
664
685
|
});
|
|
665
686
|
const pointerHosts = new WeakMap();
|
|
687
|
+
await context.exposeBinding('__peachHostAccessibility', (source, instanceIds) =>
|
|
688
|
+
captureBrowserAccessibility(source.page, instanceIds));
|
|
666
689
|
await context.exposeBinding('__sootsimHostPointer', async (source, value) => {
|
|
667
690
|
let pending = pointerHosts.get(source.page);
|
|
668
691
|
if (!pending) {
|
package/cli/main.ts
CHANGED
|
@@ -42,7 +42,7 @@ async function handleTopLevelError(err: unknown): Promise<never> {
|
|
|
42
42
|
// peach's known top-level errors are all single-line, narrow strings.
|
|
43
43
|
// everything else gets the full stack so unexpected crashes still surface.
|
|
44
44
|
const isKnownCliFailure =
|
|
45
|
-
/^command timed out after \d+s$/.test(msg) ||
|
|
45
|
+
/^command timed out after \d+(?:\.\d+)?s$/.test(msg) ||
|
|
46
46
|
/^cannot hand off bridge on port \d+: pid \d+ is not this Peach invocation$/.test(
|
|
47
47
|
msg,
|
|
48
48
|
) ||
|
|
@@ -528,6 +528,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
528
528
|
category: 'guest_app_network',
|
|
529
529
|
count: 1,
|
|
530
530
|
},
|
|
531
|
+
"packages/peach-engine/src/mcp/shared-config-transport.ts :: fetch :: '/__sootsim/shared-config'":
|
|
532
|
+
{ category: 'same_origin_runtime', count: 1 },
|
|
531
533
|
'packages/peach-engine/src/mcp/ws-bridge.ts :: fetch :: bridgeHealthUrl': {
|
|
532
534
|
category: 'local_development',
|
|
533
535
|
count: 1,
|
|
@@ -561,8 +563,6 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
561
563
|
category: 'guest_app_network',
|
|
562
564
|
count: 1,
|
|
563
565
|
},
|
|
564
|
-
"packages/peach-engine/src/projects/projects-persistence.ts :: fetch :: '/__sootsim/shared-config'":
|
|
565
|
-
{ category: 'same_origin_runtime', count: 1 },
|
|
566
566
|
'packages/peach-engine/src/react-native/index.ts :: fetch :: resource': {
|
|
567
567
|
category: 'guest_app_network',
|
|
568
568
|
count: 1,
|
|
@@ -655,7 +655,7 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
655
655
|
count: 1,
|
|
656
656
|
},
|
|
657
657
|
"packages/peach-engine/src/settings/persistence.ts :: fetch :: '/__sootsim/shared-config'":
|
|
658
|
-
{ category: 'same_origin_runtime', count:
|
|
658
|
+
{ category: 'same_origin_runtime', count: 1 },
|
|
659
659
|
'packages/peachsim/cli/app-fonts.ts :: fetch :: `${origin}/`': {
|
|
660
660
|
category: 'local_development',
|
|
661
661
|
count: 1,
|
|
@@ -867,8 +867,10 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
867
867
|
category: 'same_origin_runtime',
|
|
868
868
|
count: 1,
|
|
869
869
|
},
|
|
870
|
-
'packages/peach-engine/src/engine/image-loader.ts :: fetch-alias ::
|
|
871
|
-
|
|
870
|
+
'packages/peach-engine/src/engine/image-loader.ts :: fetch-alias :: uri': {
|
|
871
|
+
category: 'guest_app_network',
|
|
872
|
+
count: 1,
|
|
873
|
+
},
|
|
872
874
|
'packages/peach-engine/src/ExternalApp.tsx :: fetch-alias :: requestUrl': {
|
|
873
875
|
category: 'guest_app_network',
|
|
874
876
|
count: 1,
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -333,9 +333,11 @@ export function printBridgeWorldNotice(
|
|
|
333
333
|
export interface WsBridge {
|
|
334
334
|
readonly plane?: 'local' | 'cloud'
|
|
335
335
|
// `opts.timeoutMs` overrides the bridge-wide commandTimeoutMs for a single
|
|
336
|
-
// call
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
// call. defaultTimeoutMs supplies a probe cap unless --timeout was explicit.
|
|
337
|
+
send(
|
|
338
|
+
cmd: WsBridgeCommand,
|
|
339
|
+
opts?: { timeoutMs?: number; defaultTimeoutMs?: number },
|
|
340
|
+
): Promise<any>
|
|
339
341
|
// includeRetained appends the bridge's retained disconnected sims
|
|
340
342
|
// (readyState 'closed' with disconnectCause) after the live ones. only
|
|
341
343
|
// loss-reporting surfaces pass it; every other consumer keeps the
|
|
@@ -381,6 +383,7 @@ interface WsResponse {
|
|
|
381
383
|
interface WsBridgeOptions {
|
|
382
384
|
connectTimeoutMs?: number
|
|
383
385
|
commandTimeoutMs?: number
|
|
386
|
+
explicitTimeout?: boolean
|
|
384
387
|
simId?: string
|
|
385
388
|
cliLabel?: string
|
|
386
389
|
leaseScope?: 'connection'
|
|
@@ -455,12 +458,14 @@ export interface ParsedBridgeCliArgs {
|
|
|
455
458
|
simId?: string
|
|
456
459
|
simIdSource: 'flag' | 'saved' | 'none'
|
|
457
460
|
commandTimeoutMs: number
|
|
461
|
+
explicitTimeout?: boolean
|
|
458
462
|
}
|
|
459
463
|
|
|
460
464
|
interface BridgeFlagState {
|
|
461
465
|
wsPort: number
|
|
462
466
|
explicitPort: boolean
|
|
463
467
|
commandTimeoutMs: number
|
|
468
|
+
explicitTimeout?: boolean
|
|
464
469
|
simId: string | undefined
|
|
465
470
|
simIdSource: ParsedBridgeCliArgs['simIdSource']
|
|
466
471
|
}
|
|
@@ -487,7 +492,10 @@ const BRIDGE_VALUE_FLAGS_TABLE: readonly {
|
|
|
487
492
|
{
|
|
488
493
|
names: ['--timeout'],
|
|
489
494
|
apply: (state, value) => {
|
|
490
|
-
if (value !== undefined)
|
|
495
|
+
if (value !== undefined) {
|
|
496
|
+
state.commandTimeoutMs = Number(value)
|
|
497
|
+
state.explicitTimeout = true
|
|
498
|
+
}
|
|
491
499
|
},
|
|
492
500
|
},
|
|
493
501
|
{
|
|
@@ -573,6 +581,7 @@ export function parseBridgeCliArgs(
|
|
|
573
581
|
simId: state.simId,
|
|
574
582
|
simIdSource: state.simIdSource,
|
|
575
583
|
commandTimeoutMs: state.commandTimeoutMs,
|
|
584
|
+
explicitTimeout: state.explicitTimeout,
|
|
576
585
|
}
|
|
577
586
|
}
|
|
578
587
|
|
|
@@ -749,7 +758,10 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
749
758
|
async send(cmd, callOpts) {
|
|
750
759
|
// NOTE: `opts` (the createBridge closure arg) carries simId; do not
|
|
751
760
|
// shadow it with the per-call arg.
|
|
752
|
-
const effectiveTimeoutMs =
|
|
761
|
+
const effectiveTimeoutMs =
|
|
762
|
+
callOpts?.timeoutMs ??
|
|
763
|
+
(opts.explicitTimeout ? commandTimeoutMs : callOpts?.defaultTimeoutMs) ??
|
|
764
|
+
commandTimeoutMs
|
|
753
765
|
const sendOnce = async (
|
|
754
766
|
defaultSimId?: string,
|
|
755
767
|
cmdOverride?: Record<string, any>,
|
|
@@ -766,11 +778,7 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
766
778
|
const clearTimeoutFn = setPauseAwareTimeout(
|
|
767
779
|
() => {
|
|
768
780
|
pending.delete(id)
|
|
769
|
-
reject(
|
|
770
|
-
new Error(
|
|
771
|
-
`command timed out after ${Math.round(effectiveTimeoutMs / 1000)}s`,
|
|
772
|
-
),
|
|
773
|
-
)
|
|
781
|
+
reject(new Error(`command timed out after ${effectiveTimeoutMs / 1000}s`))
|
|
774
782
|
},
|
|
775
783
|
effectiveTimeoutMs,
|
|
776
784
|
lineageBySimId.get(payload.simId)?.hostPid,
|
|
@@ -955,6 +963,7 @@ export function createBridgeFromParsed(parsed: ParsedBridgeCliArgs): WsBridge {
|
|
|
955
963
|
if (cloudBridge) return cloudBridge
|
|
956
964
|
const options = {
|
|
957
965
|
commandTimeoutMs: parsed.commandTimeoutMs,
|
|
966
|
+
explicitTimeout: parsed.explicitTimeout,
|
|
958
967
|
simId: parsed.simId,
|
|
959
968
|
simIdSource: parsed.simIdSource,
|
|
960
969
|
}
|
package/detox/attempt-events.cjs
CHANGED
|
@@ -43,6 +43,15 @@ function proofBoundPath(root, relative) {
|
|
|
43
43
|
return resolved
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// lifecycle receipts bind every clock and census read to the retained bytes.
|
|
47
|
+
function boundText(root, relative, sha256, label) {
|
|
48
|
+
const bytes = fs.readFileSync(proofBoundPath(root, relative))
|
|
49
|
+
if (proofByteHash(bytes) !== sha256)
|
|
50
|
+
throw new Error(`${label} does not match its recorded hash`)
|
|
51
|
+
return bytes.toString('utf8')
|
|
52
|
+
}
|
|
53
|
+
module.exports.boundText = boundText
|
|
54
|
+
|
|
46
55
|
function authenticateDiscreteFrameAsset(root, declaration, calibrate = false) {
|
|
47
56
|
const cadence = parseDiscreteFrameCadence(declaration)
|
|
48
57
|
const fixture = fs.readFileSync(proofBoundPath(root, cadence.asset.fixturePath))
|
|
@@ -322,13 +331,21 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
322
331
|
)
|
|
323
332
|
)
|
|
324
333
|
throw new Error('native cadence has no source-matched framebuffer consumer')
|
|
325
|
-
const frames =
|
|
326
|
-
|
|
334
|
+
const frames = boundText(
|
|
335
|
+
root,
|
|
336
|
+
native.receipt,
|
|
337
|
+
native.receiptSha256,
|
|
338
|
+
'native cadence source receipt',
|
|
339
|
+
)
|
|
327
340
|
.trim()
|
|
328
341
|
.split('\n')
|
|
329
342
|
.map(JSON.parse)
|
|
330
|
-
const media =
|
|
331
|
-
|
|
343
|
+
const media = boundText(
|
|
344
|
+
root,
|
|
345
|
+
native.mediaFrames,
|
|
346
|
+
native.mediaFramesSha256,
|
|
347
|
+
'native cadence presentation census',
|
|
348
|
+
)
|
|
332
349
|
.trim()
|
|
333
350
|
.split('\n')
|
|
334
351
|
.map(JSON.parse)
|
|
@@ -339,11 +356,34 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
339
356
|
requireNativeRawFrame,
|
|
340
357
|
} = require('../../peach-conformance/e2e/utils/native-frame-receipt.cjs')
|
|
341
358
|
const rawRoot = path.dirname(proofBoundPath(root, native.receipt))
|
|
359
|
+
// a callback whose frame a newer one overwrote mid-read keeps no bytes; its
|
|
360
|
+
// superseded receipt fills exactly the gap it leaves in the callback order
|
|
361
|
+
const superseded = new Set(
|
|
362
|
+
boundText(
|
|
363
|
+
root,
|
|
364
|
+
native.superseded,
|
|
365
|
+
native.supersededSha256,
|
|
366
|
+
'native cadence superseded receipt',
|
|
367
|
+
)
|
|
368
|
+
.split('\n')
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.map((line) => {
|
|
371
|
+
const row = JSON.parse(line)
|
|
372
|
+
if (
|
|
373
|
+
!Number.isSafeInteger(row.callbackIndex) ||
|
|
374
|
+
row.seedBeforeRead === row.seedAfterRead
|
|
375
|
+
)
|
|
376
|
+
throw new Error('native superseded receipt is not an overwritten read')
|
|
377
|
+
return row.callbackIndex
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
342
380
|
const origin = frames[0].consumerArrival
|
|
381
|
+
let expectedCallback = 0
|
|
343
382
|
for (const [index, frame] of frames.entries()) {
|
|
383
|
+
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
344
384
|
const clock = frame.consumerArrival
|
|
345
385
|
if (
|
|
346
|
-
frame.callbackIndex !==
|
|
386
|
+
frame.callbackIndex !== expectedCallback++ ||
|
|
347
387
|
media[index].sourceFrameIndex !== index ||
|
|
348
388
|
!clock ||
|
|
349
389
|
clock.domain !== 'host-mach-absolute-time' ||
|
|
@@ -374,6 +414,9 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
374
414
|
),
|
|
375
415
|
)
|
|
376
416
|
}
|
|
417
|
+
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
418
|
+
if (superseded.size)
|
|
419
|
+
throw new Error('native superseded receipts fall outside the callback order')
|
|
377
420
|
}
|
|
378
421
|
|
|
379
422
|
function readDiscreteFrameReceipt(root, sourceRoot, reference, expected) {
|
|
@@ -710,9 +753,23 @@ function discreteFrameEvidenceFiles(root, evidence) {
|
|
|
710
753
|
const life = JSON.parse(
|
|
711
754
|
fs.readFileSync(proofBoundPath(root, receipt.lifecycle.path), 'utf8'),
|
|
712
755
|
)
|
|
756
|
+
// the app's ui frame clock and delivered input are replayed from their
|
|
757
|
+
// own receipts, so their bytes travel bound to the hashes the lifecycle
|
|
758
|
+
// recorded
|
|
759
|
+
for (const [label, auxiliary] of [
|
|
760
|
+
['ui frame clock', life.uiFrameClock],
|
|
761
|
+
['delivered input', life.deliveredUIKitInput],
|
|
762
|
+
]) {
|
|
763
|
+
if (!auxiliary) continue
|
|
764
|
+
const bytes = fs.readFileSync(proofBoundPath(root, auxiliary.sourceReceipt))
|
|
765
|
+
if (proofByteHash(bytes) !== auxiliary.sourceReceiptSha256)
|
|
766
|
+
throw new Error(`native ${label} receipt does not match its recorded hash`)
|
|
767
|
+
paths.add(auxiliary.sourceReceipt)
|
|
768
|
+
}
|
|
713
769
|
const native = life.nativeFramebuffer
|
|
714
770
|
if (native) {
|
|
715
771
|
paths.add(native.receipt)
|
|
772
|
+
paths.add(native.superseded)
|
|
716
773
|
paths.add(native.mediaFrames)
|
|
717
774
|
const rows = fs
|
|
718
775
|
.readFileSync(proofBoundPath(root, native.receipt), 'utf8')
|