peachsim 0.1.1635 → 0.1.1641
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 +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- 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 +17 -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 +5014 -2268
- 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 +1036 -166
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +4613 -1963
- 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 +58072 -16400
- 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 +2790 -143
- 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 +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- 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 +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- 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 +21 -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 +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
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')
|
|
@@ -526,7 +522,7 @@ export async function runUpload(
|
|
|
526
522
|
bridge,
|
|
527
523
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
528
524
|
)
|
|
529
|
-
if (snapshot?.bundleUrl) {
|
|
525
|
+
if (hasBundle && snapshot?.bundleUrl) {
|
|
530
526
|
const bundleOrigin = new URL(snapshot.bundleUrl).origin
|
|
531
527
|
// manifest stays the body-less re-fetch path for bundle-origin static
|
|
532
528
|
// assets (chunks, fonts, wasm). bundle-origin api endpoints would also
|
|
@@ -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`)
|
|
@@ -788,7 +784,7 @@ export async function runUpload(
|
|
|
788
784
|
.join('\n')
|
|
789
785
|
const timelineGz = gzipSync(traceTimelineJsonl ? `${traceTimelineJsonl}\n` : '')
|
|
790
786
|
const storageGz =
|
|
791
|
-
opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
787
|
+
hasBundle && opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
792
788
|
? gzipSync(JSON.stringify(opts.storageSnapshot))
|
|
793
789
|
: undefined
|
|
794
790
|
console.log(
|
|
@@ -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
|
}
|
|
@@ -975,10 +971,9 @@ export async function runUpload(
|
|
|
975
971
|
// fetcher (same code path as the build's `peach app-fonts stage`) so both
|
|
976
972
|
// pipelines store identical bytes keyed by sha256(url).
|
|
977
973
|
const appFontFiles: PreparedFile[] = []
|
|
978
|
-
const appFontsWire =
|
|
979
|
-
appFontsWireFromPageHref(snapshot.pageHref),
|
|
980
|
-
|
|
981
|
-
)
|
|
974
|
+
const appFontsWire = hasBundle
|
|
975
|
+
? await resolveAppFontsWire(appFontsWireFromPageHref(snapshot.pageHref), fetch)
|
|
976
|
+
: ''
|
|
982
977
|
if (appFontsWire) {
|
|
983
978
|
const fonts = await fetchAppFontFiles(appFontsWire, {
|
|
984
979
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1007,7 +1002,7 @@ export async function runUpload(
|
|
|
1007
1002
|
// still-live metro and persist them keyed by sha256(url). the engine's
|
|
1008
1003
|
// <img src> resolves them through the share fetch proxy in preview mode.
|
|
1009
1004
|
const appSplashFiles: PreparedFile[] = []
|
|
1010
|
-
const appSplashWire = appSplashWireFromPageHref(snapshot.pageHref)
|
|
1005
|
+
const appSplashWire = hasBundle ? appSplashWireFromPageHref(snapshot.pageHref) : ''
|
|
1011
1006
|
if (appSplashWire) {
|
|
1012
1007
|
const images = await fetchAppSplashImages(appSplashWire, {
|
|
1013
1008
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1037,8 +1032,9 @@ export async function runUpload(
|
|
|
1037
1032
|
// explicitly so the installed app keeps its identity in share replay.
|
|
1038
1033
|
const appIdentityFiles: PreparedFile[] = []
|
|
1039
1034
|
let appManifest: string | undefined
|
|
1040
|
-
const metroOrigin =
|
|
1041
|
-
|
|
1035
|
+
const metroOrigin =
|
|
1036
|
+
hasBundle && 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/flow-live-status.ts
CHANGED
|
@@ -13,8 +13,6 @@ export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
|
|
|
13
13
|
|
|
14
14
|
type PlanStep = { index: number; name: string; target?: string }
|
|
15
15
|
|
|
16
|
-
const PAUSE_POLL_MS = 400
|
|
17
|
-
|
|
18
16
|
function replyPaused(reply: unknown) {
|
|
19
17
|
if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
|
|
20
18
|
const control = reply.control
|
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
|
) ||
|
|
@@ -466,7 +466,7 @@ try {
|
|
|
466
466
|
// inner verb's --json payload, so it never needs the shared bridge
|
|
467
467
|
// machinery that lives inside runInspect.
|
|
468
468
|
const { runAssert } = await import('./commands/assert')
|
|
469
|
-
await runAssert(parsed.commandArgs)
|
|
469
|
+
await runAssert(parsed.commandArgs, { port, sim: globalSimTarget })
|
|
470
470
|
break
|
|
471
471
|
}
|
|
472
472
|
|
|
@@ -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
|
|
@@ -372,15 +374,10 @@ export class BridgeSimLockedError extends Error {
|
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
|
|
375
|
-
interface WsResponse {
|
|
376
|
-
id: number
|
|
377
|
-
result?: any
|
|
378
|
-
error?: string
|
|
379
|
-
}
|
|
380
|
-
|
|
381
377
|
interface WsBridgeOptions {
|
|
382
378
|
connectTimeoutMs?: number
|
|
383
379
|
commandTimeoutMs?: number
|
|
380
|
+
explicitTimeout?: boolean
|
|
384
381
|
simId?: string
|
|
385
382
|
cliLabel?: string
|
|
386
383
|
leaseScope?: 'connection'
|
|
@@ -455,12 +452,14 @@ export interface ParsedBridgeCliArgs {
|
|
|
455
452
|
simId?: string
|
|
456
453
|
simIdSource: 'flag' | 'saved' | 'none'
|
|
457
454
|
commandTimeoutMs: number
|
|
455
|
+
explicitTimeout?: boolean
|
|
458
456
|
}
|
|
459
457
|
|
|
460
458
|
interface BridgeFlagState {
|
|
461
459
|
wsPort: number
|
|
462
460
|
explicitPort: boolean
|
|
463
461
|
commandTimeoutMs: number
|
|
462
|
+
explicitTimeout?: boolean
|
|
464
463
|
simId: string | undefined
|
|
465
464
|
simIdSource: ParsedBridgeCliArgs['simIdSource']
|
|
466
465
|
}
|
|
@@ -487,7 +486,10 @@ const BRIDGE_VALUE_FLAGS_TABLE: readonly {
|
|
|
487
486
|
{
|
|
488
487
|
names: ['--timeout'],
|
|
489
488
|
apply: (state, value) => {
|
|
490
|
-
if (value !== undefined)
|
|
489
|
+
if (value !== undefined) {
|
|
490
|
+
state.commandTimeoutMs = Number(value)
|
|
491
|
+
state.explicitTimeout = true
|
|
492
|
+
}
|
|
491
493
|
},
|
|
492
494
|
},
|
|
493
495
|
{
|
|
@@ -573,6 +575,7 @@ export function parseBridgeCliArgs(
|
|
|
573
575
|
simId: state.simId,
|
|
574
576
|
simIdSource: state.simIdSource,
|
|
575
577
|
commandTimeoutMs: state.commandTimeoutMs,
|
|
578
|
+
explicitTimeout: state.explicitTimeout,
|
|
576
579
|
}
|
|
577
580
|
}
|
|
578
581
|
|
|
@@ -749,7 +752,10 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
749
752
|
async send(cmd, callOpts) {
|
|
750
753
|
// NOTE: `opts` (the createBridge closure arg) carries simId; do not
|
|
751
754
|
// shadow it with the per-call arg.
|
|
752
|
-
const effectiveTimeoutMs =
|
|
755
|
+
const effectiveTimeoutMs =
|
|
756
|
+
callOpts?.timeoutMs ??
|
|
757
|
+
(opts.explicitTimeout ? commandTimeoutMs : callOpts?.defaultTimeoutMs) ??
|
|
758
|
+
commandTimeoutMs
|
|
753
759
|
const sendOnce = async (
|
|
754
760
|
defaultSimId?: string,
|
|
755
761
|
cmdOverride?: Record<string, any>,
|
|
@@ -766,11 +772,7 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
766
772
|
const clearTimeoutFn = setPauseAwareTimeout(
|
|
767
773
|
() => {
|
|
768
774
|
pending.delete(id)
|
|
769
|
-
reject(
|
|
770
|
-
new Error(
|
|
771
|
-
`command timed out after ${Math.round(effectiveTimeoutMs / 1000)}s`,
|
|
772
|
-
),
|
|
773
|
-
)
|
|
775
|
+
reject(new Error(`command timed out after ${effectiveTimeoutMs / 1000}s`))
|
|
774
776
|
},
|
|
775
777
|
effectiveTimeoutMs,
|
|
776
778
|
lineageBySimId.get(payload.simId)?.hostPid,
|
|
@@ -955,6 +957,7 @@ export function createBridgeFromParsed(parsed: ParsedBridgeCliArgs): WsBridge {
|
|
|
955
957
|
if (cloudBridge) return cloudBridge
|
|
956
958
|
const options = {
|
|
957
959
|
commandTimeoutMs: parsed.commandTimeoutMs,
|
|
960
|
+
explicitTimeout: parsed.explicitTimeout,
|
|
958
961
|
simId: parsed.simId,
|
|
959
962
|
simIdSource: parsed.simIdSource,
|
|
960
963
|
}
|