peachsim 0.1.1620 → 0.1.1635
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/LICENSE +1 -1
- package/README.md +45 -6
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +39 -18
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +248 -156
- package/cli/cloud-client.ts +68 -57
- package/cli/command-registry.ts +1 -0
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +391 -92
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +71 -22
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +105 -67
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/platform.ts +71 -36
- package/cli/commands/record.ts +21 -1
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +5 -0
- package/cli/commands/upload.ts +51 -8
- package/cli/desktop-companion.ts +102 -17
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +44 -8
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +108 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +35 -6
- package/cli/parent-pid.ts +0 -9
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +241 -181
- package/detox/motion-change.cjs +73 -0
- package/detox/motion-change.d.cts +21 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- 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 +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +10480 -7962
- 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 +3 -2
- package/dist-lib/cloud.mjs +3 -2
- package/dist-lib/config.cjs +10 -6
- package/dist-lib/config.mjs +10 -6
- package/dist-lib/detox/index.cjs +1854 -966
- package/dist-lib/dev-bundle-resolution.cjs +64 -24
- package/dist-lib/expo-swift-bundle.cjs +8715 -6271
- package/dist-lib/expo-swift-metadata.cjs +7 -4
- package/dist-lib/expo-swift-metadata.mjs +7 -4
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +10135 -7060
- 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 +1843 -408
- 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 +3 -2
- package/dist-lib/menu.mjs +3 -2
- 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 +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +1182 -418
- package/dist-lib/native-app-identity-project.cjs +782 -23
- package/dist-lib/native-app-identity-project.mjs +781 -23
- package/dist-lib/native-app-identity.cjs +276 -9
- package/dist-lib/native-app-identity.mjs +273 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- 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 +3 -2
- package/dist-lib/react-native-host-modules.cjs +8 -1
- package/dist-lib/react-native-host-modules.mjs +8 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
- package/dist-lib/sdk.cjs +115 -34
- package/dist-lib/sdk.mjs +115 -33
- package/dist-lib/skills.cjs +15274 -9904
- package/dist-lib/swift-compile.cjs +1165 -173
- package/dist-lib/swift-compile.mjs +1212 -203
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2648 -862
- package/dist-lib/vite.cjs +70 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +35 -14
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/codex-client.ts +3 -2
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +60 -26
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +640 -192
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +19 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +12 -57
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +142 -5
- package/src/native-app-identity.ts +176 -11
- package/src/native-firebase-config.ts +209 -13
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +114 -61
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +7 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +1007 -108
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +40 -12
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +149 -56
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +38 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +185 -25
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +20 -20
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- package/src/xcode-project.ts +10 -0
package/cli/commands/upload.ts
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
// associated with the user's account.
|
|
10
10
|
|
|
11
11
|
import { createHash, randomUUID } from 'crypto'
|
|
12
|
-
import { readFileSync } from 'fs'
|
|
12
|
+
import { readFileSync, rmSync } from 'fs'
|
|
13
|
+
import { tmpdir } from 'os'
|
|
13
14
|
import path from 'path'
|
|
14
15
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
15
16
|
import { readProfileTrace } from '@peach/globals/profile-trace'
|
|
16
17
|
import { toProfileFile, type ProfileFile } from '@peach/globals/profiler'
|
|
18
|
+
import { resolveAppFontsWire } from 'peach-engine/engine/config-plugin-fonts'
|
|
17
19
|
import { shouldCompress } from 'peach-engine/preview/compress'
|
|
18
20
|
import { brotliBundle } from 'peach-engine/preview/compress-node'
|
|
19
21
|
import {
|
|
@@ -35,11 +37,13 @@ import {
|
|
|
35
37
|
mergeProfileTimeline,
|
|
36
38
|
profileFrameSummary,
|
|
37
39
|
} from 'peach-engine/profiler/profile-trace'
|
|
40
|
+
import { readSceneRecordingUpload } from 'peach-engine/remote-stream/scene-recording'
|
|
38
41
|
import {
|
|
39
42
|
resolvedExpoAppIconUrl,
|
|
40
43
|
expoConfigFromManifest,
|
|
41
44
|
} from '../../../peach-engine/src/engine/expo-manifest'
|
|
42
45
|
import { loopbackOriginOf } from '../../src/backend-origin'
|
|
46
|
+
import { parseExpoManifestBody } from '../../src/expo-manifest-body'
|
|
43
47
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
44
48
|
import { fetchAppFontFiles } from '../app-fonts'
|
|
45
49
|
import { fetchAppSplashImages } from '../app-splash'
|
|
@@ -193,8 +197,8 @@ options:
|
|
|
193
197
|
for prod uploads, upload origin for localhost/custom origins).
|
|
194
198
|
override with PEACH_PREVIEW_ORIGIN env var
|
|
195
199
|
--events <path> path to a gzipped events .jsonl.gz file to attach
|
|
196
|
-
required unless --video is present;
|
|
197
|
-
have replay or recording playback data
|
|
200
|
+
required unless --video or --scene is present;
|
|
201
|
+
previews must have replay or recording playback data
|
|
198
202
|
--screenshot <path>
|
|
199
203
|
path to a PNG thumbnail to attach to the share
|
|
200
204
|
--snapshot-manifest <path>
|
|
@@ -204,6 +208,9 @@ options:
|
|
|
204
208
|
/api/preview/flow-video?id=<share-id>
|
|
205
209
|
--video-duration-ms <ms>
|
|
206
210
|
duration hint for attached flow video
|
|
211
|
+
--scene <path> a scene recording (.psr.gz) of a remote viewer's display
|
|
212
|
+
stream. the preview paints it through the simulator's
|
|
213
|
+
renderer instead of playing a video
|
|
207
214
|
--timeline-events <path>
|
|
208
215
|
extra trace timeline rows, JSONL or JSONL gz
|
|
209
216
|
--trace-profile <path>
|
|
@@ -435,6 +442,7 @@ export async function runUpload(
|
|
|
435
442
|
const screenshotPath = extractStringFlag(mutableArgs, '--screenshot')
|
|
436
443
|
const snapshotManifestPath = extractStringFlag(mutableArgs, '--snapshot-manifest')
|
|
437
444
|
const videoPath = extractStringFlag(mutableArgs, '--video')
|
|
445
|
+
const scenePath = extractStringFlag(mutableArgs, '--scene')
|
|
438
446
|
const timelineEventsPath = extractStringFlag(mutableArgs, '--timeline-events')
|
|
439
447
|
const traceProfilePath = extractStringFlag(mutableArgs, '--trace-profile')
|
|
440
448
|
const failureBundleDir = extractStringFlag(mutableArgs, '--failure-bundle')
|
|
@@ -467,9 +475,9 @@ export async function runUpload(
|
|
|
467
475
|
console.error(' --billing-kind test_run is only for hosted test jobs')
|
|
468
476
|
peachExit(1)
|
|
469
477
|
}
|
|
470
|
-
if (!eventsPath && !videoPath) {
|
|
478
|
+
if (!eventsPath && !videoPath && !scenePath) {
|
|
471
479
|
console.error(
|
|
472
|
-
' preview uploads require playback data: pass --events or --
|
|
480
|
+
' preview uploads require playback data: pass --events, --video or --scene.\n' +
|
|
473
481
|
' for a normal local preview, use `peach record --mode combined --open`\n' +
|
|
474
482
|
' or `peach maestro test --preview` instead of uploading a bundle-only snapshot.',
|
|
475
483
|
)
|
|
@@ -492,6 +500,7 @@ export async function runUpload(
|
|
|
492
500
|
})
|
|
493
501
|
|
|
494
502
|
const bridge = createBridgeFromParsed(parsed)
|
|
503
|
+
let shellUrl: string | null = null
|
|
495
504
|
let snapshot: BundleSnapshot
|
|
496
505
|
// bundle-origin manifest (no bodies) — CLI re-fetches these over http so
|
|
497
506
|
// they can be large without blowing the ws frame cap. sibling bundle
|
|
@@ -512,6 +521,7 @@ export async function runUpload(
|
|
|
512
521
|
const localTraceTimelineCount = traceTimelineEvents.length
|
|
513
522
|
let transformedBundleText: string | null = null
|
|
514
523
|
try {
|
|
524
|
+
if (scenePath) shellUrl = await evalInBridge<string>(bridge, 'location.href')
|
|
515
525
|
snapshot = await evalInBridge<BundleSnapshot>(
|
|
516
526
|
bridge,
|
|
517
527
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
@@ -871,6 +881,17 @@ export async function runUpload(
|
|
|
871
881
|
)
|
|
872
882
|
}
|
|
873
883
|
|
|
884
|
+
let sceneBytes: Uint8Array | undefined
|
|
885
|
+
let sceneStream: InitRequestBody['sceneStream']
|
|
886
|
+
if (scenePath) {
|
|
887
|
+
sceneBytes = readFileSync(scenePath)
|
|
888
|
+
const { header, slot } = await readSceneRecordingUpload(sceneBytes)
|
|
889
|
+
sceneStream = slot
|
|
890
|
+
console.log(
|
|
891
|
+
` attaching scene stream: ${(sceneBytes.byteLength / 1024).toFixed(1)} KiB, ${header.frameCount} frames over ${Math.round(header.durationMs)}ms`,
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
874
895
|
// priority: PEACH_API_KEY env → github runner token → saved credential
|
|
875
896
|
// file → session token. github runner auth is what keeps installed PR
|
|
876
897
|
// previews from requiring a new Contrast secret.
|
|
@@ -882,6 +903,21 @@ export async function runUpload(
|
|
|
882
903
|
console.error(' set PEACH_API_KEY=sk_rnx_... or run `peach login`.')
|
|
883
904
|
peachExit(1)
|
|
884
905
|
}
|
|
906
|
+
if (sceneBytes && shellUrl && cliAuth.kind === 'github') {
|
|
907
|
+
const { renderSceneToVideo } = await import('../scene-video')
|
|
908
|
+
const output = path.join(tmpdir(), `peach-scene-video-${Date.now()}.mp4`)
|
|
909
|
+
try {
|
|
910
|
+
const rendered = await renderSceneToVideo({ scene: sceneBytes, shellUrl, output })
|
|
911
|
+
videoBytes = readFileSync(output)
|
|
912
|
+
videoContentType = 'video/mp4'
|
|
913
|
+
videoDurationMs = rendered.durationMs
|
|
914
|
+
console.log(
|
|
915
|
+
` rendered scene video: ${rendered.frameCount} frames at 60 fps, ${rendered.sizeBytes} bytes in ${(rendered.renderTimeMs / 1000).toFixed(1)}s`,
|
|
916
|
+
)
|
|
917
|
+
} finally {
|
|
918
|
+
rmSync(output, { force: true })
|
|
919
|
+
}
|
|
920
|
+
}
|
|
885
921
|
const resolvedAuthHeader = authHeaderValue(cliAuth)
|
|
886
922
|
|
|
887
923
|
const bundleOrigin = (() => {
|
|
@@ -939,7 +975,10 @@ export async function runUpload(
|
|
|
939
975
|
// fetcher (same code path as the build's `peach app-fonts stage`) so both
|
|
940
976
|
// pipelines store identical bytes keyed by sha256(url).
|
|
941
977
|
const appFontFiles: PreparedFile[] = []
|
|
942
|
-
const appFontsWire =
|
|
978
|
+
const appFontsWire = await resolveAppFontsWire(
|
|
979
|
+
appFontsWireFromPageHref(snapshot.pageHref),
|
|
980
|
+
fetch,
|
|
981
|
+
)
|
|
943
982
|
if (appFontsWire) {
|
|
944
983
|
const fonts = await fetchAppFontFiles(appFontsWire, {
|
|
945
984
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1006,8 +1045,8 @@ export async function runUpload(
|
|
|
1006
1045
|
headers: { 'expo-platform': platform, accept: 'application/json' },
|
|
1007
1046
|
signal: AbortSignal.timeout(30_000),
|
|
1008
1047
|
})
|
|
1009
|
-
if (response.ok && response.headers.get('content-type')?.includes('
|
|
1010
|
-
const manifest: unknown = await response.
|
|
1048
|
+
if (response.ok && !response.headers.get('content-type')?.includes('html')) {
|
|
1049
|
+
const manifest: unknown = parseExpoManifestBody(await response.text())
|
|
1011
1050
|
const config = expoConfigFromManifest(manifest)
|
|
1012
1051
|
if (config) {
|
|
1013
1052
|
appManifest = manifestUrl
|
|
@@ -1175,6 +1214,7 @@ export async function runUpload(
|
|
|
1175
1214
|
durationMs: videoDurationMs,
|
|
1176
1215
|
}
|
|
1177
1216
|
: undefined,
|
|
1217
|
+
sceneStream,
|
|
1178
1218
|
}
|
|
1179
1219
|
|
|
1180
1220
|
const filesByHash = new Map<string, Uint8Array>()
|
|
@@ -1197,6 +1237,7 @@ export async function runUpload(
|
|
|
1197
1237
|
for (const snapshot of interactionSnapshots) putBytes += snapshot.bytes.byteLength
|
|
1198
1238
|
if (failureBundleGz) putBytes += failureBundleGz.byteLength
|
|
1199
1239
|
if (videoBytes) putBytes += videoBytes.byteLength
|
|
1240
|
+
if (sceneBytes) putBytes += sceneBytes.byteLength
|
|
1200
1241
|
|
|
1201
1242
|
const initUrl = `${normalizeOrigin(origin)}/api/preview/upload/init`
|
|
1202
1243
|
console.log(` init: ${initUrl}`)
|
|
@@ -1213,6 +1254,7 @@ export async function runUpload(
|
|
|
1213
1254
|
timelineBytes: timelineGz,
|
|
1214
1255
|
storageBytes: storageGz,
|
|
1215
1256
|
videoBytes,
|
|
1257
|
+
sceneStreamBytes: sceneBytes,
|
|
1216
1258
|
failureBundleBytes: failureBundleGz,
|
|
1217
1259
|
snapshotBytes,
|
|
1218
1260
|
},
|
|
@@ -1228,6 +1270,7 @@ export async function runUpload(
|
|
|
1228
1270
|
(result.init.storage ? 1 : 0) +
|
|
1229
1271
|
result.init.snapshots.length +
|
|
1230
1272
|
(result.init.flowVideo ? 1 : 0) +
|
|
1273
|
+
(result.init.sceneStream ? 1 : 0) +
|
|
1231
1274
|
(result.init.failureBundle ? 1 : 0)
|
|
1232
1275
|
console.log(
|
|
1233
1276
|
` PUT ${totalObjects} objects in ${Date.now() - putT0}ms (${(putBytes / 1024).toFixed(1)} KiB)`,
|
package/cli/desktop-companion.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { execFileSync, spawn } from 'child_process'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'fs'
|
|
11
|
+
import { createHash } from 'node:crypto'
|
|
12
|
+
import { tmpdir } from 'os'
|
|
3
13
|
import { dirname, join, resolve } from 'path'
|
|
4
14
|
import { fileURLToPath } from 'url'
|
|
5
|
-
import {
|
|
6
|
-
|
|
15
|
+
import {
|
|
16
|
+
activeDesktopHostApp,
|
|
17
|
+
desktopHostPlatform,
|
|
18
|
+
isNativeHostBundle,
|
|
19
|
+
} from '../src/desktop-host-delivery'
|
|
20
|
+
import {
|
|
21
|
+
isDaemonLockfileFresh,
|
|
22
|
+
isDevBridgeLockfileFresh,
|
|
23
|
+
readDaemonLockfile,
|
|
24
|
+
readDevBridgeLockfiles,
|
|
25
|
+
rnxHomeDir,
|
|
26
|
+
} from '../src/home-paths'
|
|
7
27
|
|
|
8
28
|
// resolved lazily: peach-engine is a workspace package in dev, but absent
|
|
9
29
|
// from the published npm tarball. don't crash module load just because the
|
|
@@ -95,22 +115,31 @@ export function findInstalledDesktopCompanion(): DesktopCompanionInstall | null
|
|
|
95
115
|
: findNativeDesktopCompanion()
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
// leftover Electron bundles. /Applications/Peach.app is the native host on a
|
|
119
|
+
// case-insensitive volume, so bundle id decides, not the path's letter case.
|
|
120
|
+
function retiredElectronApps(): string[] {
|
|
121
|
+
if (process.platform === 'darwin') {
|
|
122
|
+
return [
|
|
123
|
+
'/Applications/peach.app',
|
|
124
|
+
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
125
|
+
].filter((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
126
|
+
}
|
|
127
|
+
if (process.platform !== 'linux') return []
|
|
128
|
+
return [
|
|
129
|
+
resolve(process.env.HOME || '', 'Applications/peach.AppImage'),
|
|
130
|
+
resolve(process.env.HOME || '', '.local/bin/peach.AppImage'),
|
|
131
|
+
'/opt/peach/peach.AppImage',
|
|
132
|
+
'/opt/peach/peach',
|
|
133
|
+
].filter((candidate) => existsSync(candidate))
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
// existing Electron apps stay on disk; CLI users receive the migration once.
|
|
137
|
+
// the notice names those apps and does not reject a native host beside them.
|
|
99
138
|
export function notifyDesktopMigration(): void {
|
|
100
139
|
if (process.platform !== 'darwin' && process.platform !== 'linux') return
|
|
101
|
-
const legacy =
|
|
102
|
-
process.platform === 'darwin'
|
|
103
|
-
? [
|
|
104
|
-
'/Applications/peach.app',
|
|
105
|
-
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
106
|
-
].some((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
107
|
-
: ['Applications/peach.AppImage', '.local/bin/peach.AppImage'].some((candidate) =>
|
|
108
|
-
existsSync(resolve(process.env.HOME || '', candidate)),
|
|
109
|
-
) ||
|
|
110
|
-
existsSync('/opt/peach/peach.AppImage') ||
|
|
111
|
-
existsSync('/opt/peach/peach')
|
|
140
|
+
const legacy = retiredElectronApps()
|
|
112
141
|
const marker = join(rnxHomeDir(), 'electron-native-migration')
|
|
113
|
-
if (
|
|
142
|
+
if (legacy.length === 0 || existsSync(marker)) return
|
|
114
143
|
mkdirSync(rnxHomeDir(), { recursive: true })
|
|
115
144
|
try {
|
|
116
145
|
writeFileSync(marker, '', { flag: 'wx' })
|
|
@@ -119,7 +148,7 @@ export function notifyDesktopMigration(): void {
|
|
|
119
148
|
throw error
|
|
120
149
|
}
|
|
121
150
|
console.error(
|
|
122
|
-
|
|
151
|
+
` Electron has retired on Mac and Linux (${legacy.join(', ')}). Install the native app: peach desktop install`,
|
|
123
152
|
)
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -154,6 +183,21 @@ export function wantsNativeDesktop(opts: DesktopCompanionOptions = {}): boolean
|
|
|
154
183
|
)
|
|
155
184
|
}
|
|
156
185
|
|
|
186
|
+
// the native app is the simulator window wherever it ships: always on Apple
|
|
187
|
+
// silicon, and on Linux x64 in a desktop session once installed or when a
|
|
188
|
+
// person is present to approve its package install's sudo prompt.
|
|
189
|
+
export function nativeDesktopIsDefault(): boolean {
|
|
190
|
+
const platform = desktopHostPlatform()
|
|
191
|
+
if (platform === 'mac-arm64') return true
|
|
192
|
+
if (platform !== 'linux-x64') return false
|
|
193
|
+
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false
|
|
194
|
+
return (
|
|
195
|
+
findNativeDesktopCompanion() !== null ||
|
|
196
|
+
process.stdin.isTTY === true ||
|
|
197
|
+
process.getuid?.() === 0
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
export function getNativeDesktopAppPath(): string | null {
|
|
158
202
|
const explicitApp = process.env.PEACH_DESKTOP_APP
|
|
159
203
|
if (!explicitApp)
|
|
@@ -284,6 +328,47 @@ async function openInConnectedMacApp(
|
|
|
284
328
|
}
|
|
285
329
|
}
|
|
286
330
|
|
|
331
|
+
// PeachDesktop shipped before PEACH_BRIDGE_PORT seeds the page from
|
|
332
|
+
// PEACH_HOME/daemon.json and ignores the launch URL's wsPort. when this
|
|
333
|
+
// command is waiting on a different bridge, give the child a home whose
|
|
334
|
+
// lock names that bridge. the caller's daemon.json is left untouched.
|
|
335
|
+
// a newer host that prefers the env, the URL pin, or a fresh daemon still
|
|
336
|
+
// sees this same port (the isolated-home rule in desktop host bridge selection).
|
|
337
|
+
function macDesktopLaunchHome(bridgePort: number): string {
|
|
338
|
+
const home = rnxHomeDir()
|
|
339
|
+
const daemon = readDaemonLockfile()
|
|
340
|
+
if (isDaemonLockfileFresh(daemon) && daemon.bridgePort === bridgePort) return home
|
|
341
|
+
const dev = readDevBridgeLockfiles().find(
|
|
342
|
+
(lock) => lock.bridgePort === bridgePort && isDevBridgeLockfileFresh(lock),
|
|
343
|
+
)
|
|
344
|
+
const homeKey = createHash('sha256').update(home).digest('hex')
|
|
345
|
+
const dir = join(tmpdir(), 'peach-desktop-launch', homeKey, String(bridgePort))
|
|
346
|
+
mkdirSync(dir, { recursive: true })
|
|
347
|
+
if (existsSync(home)) {
|
|
348
|
+
for (const name of readdirSync(home)) {
|
|
349
|
+
if (name === 'daemon.json') continue
|
|
350
|
+
const dest = join(dir, name)
|
|
351
|
+
try {
|
|
352
|
+
lstatSync(dest)
|
|
353
|
+
} catch {
|
|
354
|
+
symlinkSync(join(home, name), dest)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const lockPath = join(dir, 'daemon.json')
|
|
359
|
+
try {
|
|
360
|
+
if (lstatSync(lockPath).isSymbolicLink()) unlinkSync(lockPath)
|
|
361
|
+
} catch {
|
|
362
|
+
// no lock yet
|
|
363
|
+
}
|
|
364
|
+
const pid = dev && dev.pid > 1 ? dev.pid : process.pid
|
|
365
|
+
writeFileSync(
|
|
366
|
+
lockPath,
|
|
367
|
+
JSON.stringify({ schema: 1, pid, bridgePort, heartbeatAt: Date.now() }),
|
|
368
|
+
)
|
|
369
|
+
return dir
|
|
370
|
+
}
|
|
371
|
+
|
|
287
372
|
export async function launchDesktopCompanion(
|
|
288
373
|
url?: string,
|
|
289
374
|
install = findDesktopCompanion(),
|
|
@@ -339,7 +424,7 @@ export async function launchDesktopCompanion(
|
|
|
339
424
|
'-a',
|
|
340
425
|
install.path,
|
|
341
426
|
'--env',
|
|
342
|
-
`PEACH_HOME=${rnxHomeDir()}`,
|
|
427
|
+
`PEACH_HOME=${opts.bridgePort && launchUrl ? macDesktopLaunchHome(opts.bridgePort) : rnxHomeDir()}`,
|
|
343
428
|
]
|
|
344
429
|
if (opts.bridgePort) {
|
|
345
430
|
openArgs.push('--env', `PEACH_BRIDGE_PORT=${opts.bridgePort}`)
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// watcher would bundle chokidar and private engine/bundler modules into the cli.
|
|
15
15
|
// deps-web belongs to a different served tree.
|
|
16
16
|
|
|
17
|
-
import { readdirSync, statSync, type Dirent } from 'node:fs'
|
|
18
|
-
import { join, relative, sep } from 'node:path'
|
|
17
|
+
import { readdirSync, statSync, utimesSync, type Dirent } from 'node:fs'
|
|
18
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
19
19
|
|
|
20
20
|
export const DEV_BUILD_REBUILD_COMMAND = 'bun run build:peach'
|
|
21
21
|
|
|
@@ -159,6 +159,41 @@ function newestForEntry(monorepoRoot: string, repoPath: string): NewestFile | nu
|
|
|
159
159
|
return null
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** stamps the sentinels under `installedRoot` as built at `sourceCapturedAt`,
|
|
163
|
+
* when the build captured its source. an install that finds identical bytes
|
|
164
|
+
* (remote cache hit, byte-identical promotion) leaves their old mtimes, which
|
|
165
|
+
* the gate would read as a stale build; stamping the install time instead
|
|
166
|
+
* would hide a source edit made while the build ran. */
|
|
167
|
+
export function markDevBuildCurrent(
|
|
168
|
+
monorepoRoot: string,
|
|
169
|
+
installedRoot: string,
|
|
170
|
+
sourceCapturedAt: Date,
|
|
171
|
+
): void {
|
|
172
|
+
const installed = resolve(installedRoot)
|
|
173
|
+
for (const route of ROUTES) {
|
|
174
|
+
for (const artifact of route.artifacts) {
|
|
175
|
+
const absolute = join(monorepoRoot, artifact)
|
|
176
|
+
if (absolute !== installed && !absolute.startsWith(installed + sep)) continue
|
|
177
|
+
const stack = [absolute]
|
|
178
|
+
while (stack.length > 0) {
|
|
179
|
+
const path = stack.pop()
|
|
180
|
+
if (path === undefined) continue
|
|
181
|
+
let stat: ReturnType<typeof statSync>
|
|
182
|
+
try {
|
|
183
|
+
stat = statSync(path)
|
|
184
|
+
} catch {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
if (stat.isDirectory()) {
|
|
188
|
+
for (const name of readdirSync(path)) stack.push(join(path, name))
|
|
189
|
+
} else {
|
|
190
|
+
utimesSync(path, sourceCapturedAt, sourceCapturedAt)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
162
197
|
function toRepoPath(monorepoRoot: string, absolutePath: string): string {
|
|
163
198
|
return relative(monorepoRoot, absolutePath).split(sep).join('/')
|
|
164
199
|
}
|
package/cli/drivers/desktop.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// desktop driver selection is shared with companion discovery.
|
|
2
2
|
|
|
3
|
+
import { desktopHostPlatform, headlessHostCommand } from '../../src/desktop-host-delivery'
|
|
3
4
|
import {
|
|
4
5
|
findDesktopCompanion,
|
|
5
6
|
launchDesktopCompanion,
|
|
@@ -18,7 +19,19 @@ export function createDesktopDriver(id: 'desktop' | 'electron'): Driver {
|
|
|
18
19
|
const missing = `peach ${id} app not installed (run \`peach desktop install${native ? '' : ' --electron'}\`)`
|
|
19
20
|
function probe(): DriverAvailability {
|
|
20
21
|
const install = findDesktopCompanion({ native })
|
|
21
|
-
if (!install)
|
|
22
|
+
if (!install) {
|
|
23
|
+
const host =
|
|
24
|
+
id === 'desktop' && native && desktopHostPlatform() === 'linux-x64'
|
|
25
|
+
? headlessHostCommand()
|
|
26
|
+
: null
|
|
27
|
+
return host
|
|
28
|
+
? {
|
|
29
|
+
available: true,
|
|
30
|
+
reason: null,
|
|
31
|
+
detail: `headless host @ ${host[0]} (use --driver playwright --headless)`,
|
|
32
|
+
}
|
|
33
|
+
: { available: false, reason: missing }
|
|
34
|
+
}
|
|
22
35
|
return {
|
|
23
36
|
available: true,
|
|
24
37
|
reason: null,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
// require, in a child process the CLI spawns as itself.
|
|
19
19
|
import {
|
|
20
20
|
chromeDisabledFeaturesSwitch,
|
|
21
|
+
LINUX_CHROME_SCOPE_ENV,
|
|
21
22
|
softwareChromeLaunchOptions,
|
|
22
23
|
} from '../../../../scripts/lib/reap-browser'
|
|
23
24
|
import { createBrowserPageTargetReader } from '../../../peach-engine/src/browser-page-target'
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from '../../../peach-engine/src/capture/browser-composite'
|
|
28
29
|
import { WEBVIEW_HOST_BOOTSTRAP_SOURCE } from '../../../peach-engine/src/components/native-ui/webview-frame-bridge'
|
|
29
30
|
import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render-worker/browser-pointer'
|
|
31
|
+
import { automateWebAuthenticationSessions } from '../../../peach-engine/src/web-authentication-automation-host'
|
|
30
32
|
import {
|
|
31
33
|
automateWebViewFrames,
|
|
32
34
|
WEBVIEW_HOST_BINDING,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
webViewAutomationHelpersSource,
|
|
38
40
|
} from '../../../peach-engine/src/webview-automation-host'
|
|
39
41
|
import { DISPOSABLE_BROWSER_CACHE_DIR_NAMES } from '../../src/browser-cache'
|
|
42
|
+
import { listProcessChildren } from '../../src/process-group'
|
|
40
43
|
|
|
41
44
|
// a person's tap on the device canvas is a trusted input, which gives the page
|
|
42
45
|
// the user activation browsers require before media plays with sound, and an
|
|
@@ -68,6 +71,9 @@ const callerUserDataDir = process.env.SOOTSIM_PW_USERDATADIR || '';
|
|
|
68
71
|
// SOOTSIM_PW_OWNER_POLL_MS is a test-only override of the 5s poll interval;
|
|
69
72
|
// production never sets it.
|
|
70
73
|
const ownerPid = Number(process.env.SOOTSIM_PW_OWNER_PID || 0);
|
|
74
|
+
// the open command waiting for this host to connect; it may queue for the
|
|
75
|
+
// browser slot and then be killed by its own caller's budget.
|
|
76
|
+
const launcherPid = Number(process.env.SOOTSIM_PW_LAUNCHER_PID || 0);
|
|
71
77
|
const ownerPollMs = Number(process.env.SOOTSIM_PW_OWNER_POLL_MS || 5000);
|
|
72
78
|
// optional CDP port — exposes Chrome's remote-debugging endpoint so a
|
|
73
79
|
// bridge-driven sim can also be cpu-profiled (peach perf cpu --cdp-port).
|
|
@@ -109,16 +115,19 @@ const viewport = viewportSpec
|
|
|
109
115
|
const deviceScaleFactor = Number(process.env.SOOTSIM_PW_DSF || 2);
|
|
110
116
|
const closeTimeoutMs = Number(process.env.SOOTSIM_PW_CLOSE_TIMEOUT_MS || 2500);
|
|
111
117
|
const connectAckFile = process.env.SOOTSIM_PW_CONNECTED_ACK_FILE || '';
|
|
118
|
+
const admittedFile = process.env.SOOTSIM_PW_ADMITTED_FILE || '';
|
|
112
119
|
const completionFile = process.env.PEACH_PW_COMPLETION_FILE || '';
|
|
113
120
|
const disposableBrowserCacheDirs = new Set(${JSON.stringify(
|
|
114
121
|
DISPOSABLE_BROWSER_CACHE_DIR_NAMES,
|
|
115
122
|
)});
|
|
116
|
-
const { existsSync, mkdtempSync, readdirSync, rmSync, unlinkSync, writeFileSync } = require('fs');
|
|
117
|
-
const { execSync, spawn } = require('child_process');
|
|
123
|
+
const { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } = require('fs');
|
|
124
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
125
|
+
const listProcessChildren = ${listProcessChildren.toString()};
|
|
118
126
|
const { tmpdir } = require('os');
|
|
119
127
|
const { join } = require('path');
|
|
120
128
|
const createBrowserPointerDispatcher = ${createBrowserPointerDispatcher.toString()};
|
|
121
129
|
const automateWebViewFrames = ${automateWebViewFrames.toString()};
|
|
130
|
+
const automateWebAuthenticationSessions = ${automateWebAuthenticationSessions.toString()};
|
|
122
131
|
const webViewAutomationHelpers = ${webViewAutomationHelpersSource()};
|
|
123
132
|
const readBrowserCompositeCaptureRequest = ${readBrowserCompositeCaptureRequest.toString()};
|
|
124
133
|
const resolveBrowserCompositeCdpClip = ${resolveBrowserCompositeCdpClip.toString()};
|
|
@@ -193,10 +202,9 @@ const assertNoUnsafeSwiftShaderFlag = () => {
|
|
|
193
202
|
const killTree = (pid) => {
|
|
194
203
|
let kids = [];
|
|
195
204
|
try {
|
|
196
|
-
kids =
|
|
197
|
-
.toString().split('\\n').filter(Boolean);
|
|
205
|
+
kids = listProcessChildren([pid]);
|
|
198
206
|
} catch {}
|
|
199
|
-
for (const k of kids) killTree(
|
|
207
|
+
for (const k of kids) killTree(k);
|
|
200
208
|
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
201
209
|
};
|
|
202
210
|
const reapChrome = () => { for (const pid of chromePidsByProfile()) killTree(pid); };
|
|
@@ -401,6 +409,9 @@ const cleanupProfile = () => {
|
|
|
401
409
|
if (connectAckFile) {
|
|
402
410
|
try { unlinkSync(connectAckFile); } catch {}
|
|
403
411
|
}
|
|
412
|
+
if (admittedFile) {
|
|
413
|
+
try { unlinkSync(admittedFile); } catch {}
|
|
414
|
+
}
|
|
404
415
|
process.exit(code);
|
|
405
416
|
};
|
|
406
417
|
process.once('SIGTERM', () => { void shutdown(0); });
|
|
@@ -433,13 +444,25 @@ const cleanupProfile = () => {
|
|
|
433
444
|
throw new Error('[playwright-driver] missing valid owning process; refusing unsupervised browser host');
|
|
434
445
|
}
|
|
435
446
|
const intervalMs = Number.isFinite(ownerPollMs) && ownerPollMs > 0 ? ownerPollMs : 5000;
|
|
436
|
-
|
|
447
|
+
const gone = (pid) => {
|
|
437
448
|
try {
|
|
438
|
-
process.kill(
|
|
449
|
+
process.kill(pid, 0);
|
|
450
|
+
return false;
|
|
439
451
|
} catch (err) {
|
|
440
|
-
|
|
452
|
+
return !(err && err.code === 'EPERM');
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
ownerWatch = setInterval(() => {
|
|
456
|
+
if (gone(ownerPid)) {
|
|
441
457
|
process.stderr.write('[owner-gone] owning process ' + ownerPid + ' exited; closing playwright host\\n');
|
|
442
458
|
void shutdown(0);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
// until the sim connects, the open command is the only one waiting for
|
|
462
|
+
// it; once that command is gone nobody receives the sim.
|
|
463
|
+
if (!connected && !ackFileExists() && Number.isInteger(launcherPid) && launcherPid > 1 && gone(launcherPid)) {
|
|
464
|
+
process.stderr.write('[launcher-gone] open command ' + launcherPid + ' exited before the sim connected; closing playwright host\\n');
|
|
465
|
+
void shutdown(2);
|
|
443
466
|
}
|
|
444
467
|
}, intervalMs);
|
|
445
468
|
if (typeof ownerWatch.unref === 'function') ownerWatch.unref();
|
|
@@ -478,6 +501,11 @@ const cleanupProfile = () => {
|
|
|
478
501
|
LD_LIBRARY_PATH: [libraryPath, process.env.LD_LIBRARY_PATH].filter(Boolean).join(':'),
|
|
479
502
|
};
|
|
480
503
|
}
|
|
504
|
+
// keep the browser main process with its children in the caller's cgroup.
|
|
505
|
+
// otherwise chromium moves only main into a systemd app scope over dbus.
|
|
506
|
+
if (process.platform === 'linux') {
|
|
507
|
+
opts.env = { ...process.env, ...opts.env, ...${JSON.stringify(LINUX_CHROME_SCOPE_ENV)} };
|
|
508
|
+
}
|
|
481
509
|
// playwright injects --force-color-profile=srgb by default, which reads
|
|
482
510
|
// visibly washed-out next to a normal chrome window on a wide-gamut mac
|
|
483
511
|
// display. strip it so driver-launched sims color-match the user's own
|
|
@@ -548,6 +576,11 @@ const cleanupProfile = () => {
|
|
|
548
576
|
label: 'peach sim host ' + profileDir,
|
|
549
577
|
readQueue: readChromiumQueue,
|
|
550
578
|
});
|
|
579
|
+
// the launcher's connect wait starts here: time queued for the slot is not
|
|
580
|
+
// time the sim had to connect.
|
|
581
|
+
if (admittedFile) {
|
|
582
|
+
try { writeFileSync(admittedFile, JSON.stringify({ admittedAt: Date.now(), pid: process.pid }) + '\\n'); } catch {}
|
|
583
|
+
}
|
|
551
584
|
clearStaleSingletonLock();
|
|
552
585
|
let launchErr = null;
|
|
553
586
|
let launchedChannel = null;
|
|
@@ -590,6 +623,8 @@ const cleanupProfile = () => {
|
|
|
590
623
|
await context.grantPermissions(['camera']);
|
|
591
624
|
const hostMeta = {
|
|
592
625
|
sootsimHostDriver: 'playwright',
|
|
626
|
+
peachRenderer: hostCommand ? 'native' : 'chromium',
|
|
627
|
+
peachHostExecutable: hostCommand ? hostCommand[0] : null,
|
|
593
628
|
sootsimHostPid: process.pid,
|
|
594
629
|
sootsimHostOwnerPid: ownerPid,
|
|
595
630
|
sootsimHostStartedAt: Date.now(),
|
|
@@ -676,6 +711,7 @@ const cleanupProfile = () => {
|
|
|
676
711
|
}, Number.isFinite(connectTimeoutMs) && connectTimeoutMs > 0 ? connectTimeoutMs : 120000);
|
|
677
712
|
if (typeof connectTimer.unref === 'function') connectTimer.unref();
|
|
678
713
|
const page = await context.newPage();
|
|
714
|
+
await automateWebAuthenticationSessions(context, '__peachWebAuthenticationHost', page, !headless);
|
|
679
715
|
if (hardwareGpuRequired) {
|
|
680
716
|
assertNoUnsafeSwiftShaderFlag();
|
|
681
717
|
const gpu = await page.evaluate(() => {
|
|
@@ -151,18 +151,15 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
151
151
|
// renders natively and lets the page present only on capture; the host
|
|
152
152
|
// reaches the browser over cdp, so it needs a debugging port.
|
|
153
153
|
let hostCommand: string[] | null = null
|
|
154
|
-
if (headless && desktopHostPlatform()) {
|
|
154
|
+
if (headless && !opts.chromium && desktopHostPlatform()) {
|
|
155
155
|
try {
|
|
156
156
|
hostCommand = headlessHostCommand()
|
|
157
157
|
if (!hostCommand && desktopHostPlatform() === 'linux-x64') {
|
|
158
|
-
//
|
|
159
|
-
// before the compatible headless host existed leaves this run on the browser engine alone
|
|
158
|
+
// delivery honors PEACH_DESKTOP_VERSION; a pin never follows channel latest.
|
|
160
159
|
await desktopHostDelivery().install()
|
|
161
160
|
hostCommand = headlessHostCommand()
|
|
162
161
|
if (!hostCommand)
|
|
163
|
-
|
|
164
|
-
' native host release has no compatible headless host yet; rendering in the browser',
|
|
165
|
-
)
|
|
162
|
+
throw new Error('selected native host has no compatible headless entrypoint')
|
|
166
163
|
}
|
|
167
164
|
} catch (error) {
|
|
168
165
|
return {
|
|
@@ -175,6 +172,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
175
172
|
|
|
176
173
|
const errLog = join(tmpdir(), playwrightHostLogName(Date.now(), process.pid))
|
|
177
174
|
const connectAckFile = `${errLog}.connected`
|
|
175
|
+
const admittedFile = `${errLog}.admitted`
|
|
178
176
|
const completionFile = `${errLog}.completed`
|
|
179
177
|
const errFd = openSync(errLog, 'a')
|
|
180
178
|
try {
|
|
@@ -210,9 +208,11 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
210
208
|
? { SOOTSIM_PW_VIEWPORT: `${opts.viewport.width}x${opts.viewport.height}` }
|
|
211
209
|
: {}),
|
|
212
210
|
SOOTSIM_PW_CONNECTED_ACK_FILE: connectAckFile,
|
|
211
|
+
SOOTSIM_PW_ADMITTED_FILE: admittedFile,
|
|
213
212
|
PEACH_PW_COMPLETION_FILE: completionFile,
|
|
214
213
|
SOOTSIM_PW_CONNECT_TIMEOUT_MS: String(opts.connectTimeoutMs ?? 120_000),
|
|
215
214
|
SOOTSIM_PW_OWNER_PID: String(ownerPid),
|
|
215
|
+
SOOTSIM_PW_LAUNCHER_PID: String(process.pid),
|
|
216
216
|
SOOTSIM_PW_LIBRARY_PATH: provisioning.libraryPath ?? '',
|
|
217
217
|
},
|
|
218
218
|
},
|
|
@@ -233,11 +233,12 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
233
233
|
const ownerNote = ` — browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits, or after ${idleMinutes} min with no peach command or input if another browser launch is waiting for its slot`
|
|
234
234
|
return {
|
|
235
235
|
launched: true,
|
|
236
|
-
message: `playwright
|
|
236
|
+
message: `playwright ${hostCommand ? 'native host' : 'Chromium'} launched → ${targetUrl.href}${ownerNote}`,
|
|
237
237
|
pid: child.pid,
|
|
238
238
|
target: resolved.spec,
|
|
239
239
|
attachUrl: targetUrl.href,
|
|
240
240
|
connectAckFile,
|
|
241
|
+
admittedFile,
|
|
241
242
|
diagnosticLogPath: errLog,
|
|
242
243
|
completion,
|
|
243
244
|
completionFile,
|
|
@@ -257,7 +258,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
257
258
|
export const playwrightDriver: Driver = {
|
|
258
259
|
id: 'playwright',
|
|
259
260
|
name: 'playwright',
|
|
260
|
-
description: '
|
|
261
|
+
description: 'isolated Chromium; headless uses the native host when available',
|
|
261
262
|
kind: 'automation',
|
|
262
263
|
availability: probe,
|
|
263
264
|
launch,
|
package/cli/drivers/types.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface DriverLaunchOptions {
|
|
|
30
30
|
url?: string
|
|
31
31
|
// run in headless mode where the driver supports it. ignored by electron.
|
|
32
32
|
headless?: boolean
|
|
33
|
+
// opt this open into browser rendering without acquiring or attaching a native host.
|
|
34
|
+
chromium?: boolean
|
|
33
35
|
// initial device model seed forwarded to the peach shell. the electron
|
|
34
36
|
// driver forwards it via argv (--device=<model>); browser drivers receive
|
|
35
37
|
// it already stamped on the URL (?device=<model>) by runOpenCommand.
|
|
@@ -86,6 +88,9 @@ export interface DriverLaunchResult {
|
|
|
86
88
|
target?: string
|
|
87
89
|
attachUrl?: string
|
|
88
90
|
connectAckFile?: string
|
|
91
|
+
// written once the host holds its machine-wide browser slot; its connect
|
|
92
|
+
// budget starts there, so the launcher's wait starts there too.
|
|
93
|
+
admittedFile?: string
|
|
89
94
|
diagnosticLogPath?: string
|
|
90
95
|
// foreground guest hosts await the detached driver's exit without polling.
|
|
91
96
|
completion?: Promise<DriverCompletion>
|