@deeeed/metamask-harness 0.34.0 → 0.34.2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/adapters/manifest.json +0 -8
  3. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +236 -3
  4. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
  5. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +22 -15
  6. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
  7. package/adapters/mobile/launch-metro.cjs +6 -4
  8. package/adapters/mobile/open-device.sh +170 -19
  9. package/adapters/mobile/start-metro.sh +165 -93
  10. package/adapters/mobile/stop-metro.sh +1 -0
  11. package/dist/adapters/mobile/prepare.js +1 -4
  12. package/dist/adapters/mobile/video-recorder.js +12 -10
  13. package/dist/adapters.js +134 -6
  14. package/dist/commands/launch/index.js +9 -0
  15. package/dist/commands/launch/mobile.js +17 -3
  16. package/dist/live-adapter-contract.js +10 -3
  17. package/dist/recipe-security.js +2 -0
  18. package/dist/runner.js +53 -8
  19. package/dist/runtime-context.js +1 -0
  20. package/library/actions/mobile/perps/measure_homepage_visible.mjs +4 -0
  21. package/library/actions/mobile/perps/performance-capture.mjs +579 -111
  22. package/library/actions/mobile/perps/perps.mjs +221 -38
  23. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +58 -0
  24. package/library/actions/mobile/platform/bridge.mjs +102 -8
  25. package/library/actions/mobile/wallet/ensure_unlocked.mjs +35 -8
  26. package/library/manifests/mobile.action-manifest.json +243 -511
  27. package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +20 -2
  28. package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +2 -0
  29. package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +120 -0
  30. package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +7 -6
  31. package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +5 -5
  32. package/package.json +1 -1
  33. package/adapters/mobile/metro-config.cjs +0 -93
@@ -90,19 +90,29 @@ ios_app_installed() {
90
90
  }
91
91
 
92
92
  run_ios_native_build() (
93
- local sim_target="$1" reason="$2" build_env=""
94
- printf '%s; installing with yarn start:ios (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
93
+ local sim_target="$1" bundle_id="$2" reason="$3" build_env="" build_script="" build_type=""
94
+ case "$bundle_id" in
95
+ io.metamask.MetaMask|io.metamask) build_script="start:ios"; build_type="main" ;;
96
+ io.metamask.MetaMask-Flask) build_script="start:ios:flask"; build_type="flask" ;;
97
+ *)
98
+ printf 'open-device: no supported local build script for iOS bundle %s\n' "$bundle_id" >&2
99
+ return 1
100
+ ;;
101
+ esac
102
+ printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
95
103
  build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-ios-build-env.XXXXXX")" || return 1
96
104
  chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
97
105
  trap 'rm -f "$build_env"' EXIT HUP INT TERM
98
106
  {
99
107
  printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
100
108
  printf 'export MM_HARNESS_BUILD_IOS_SIMULATOR=%q\n' "$sim_target"
109
+ printf 'export MM_HARNESS_BUILD_TYPE=%q\n' "$build_type"
101
110
  cat <<'BUILD_ENV'
102
111
  mm_harness_restore_build_target() {
103
112
  export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
104
113
  export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
105
114
  export IOS_SIMULATOR="$MM_HARNESS_BUILD_IOS_SIMULATOR"
115
+ export METAMASK_BUILD_TYPE="$MM_HARNESS_BUILD_TYPE"
106
116
  }
107
117
  eval() {
108
118
  builtin eval "$@"
@@ -122,7 +132,7 @@ BUILD_ENV
122
132
  (
123
133
  cd "$TARGET"
124
134
  command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
125
- BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1
135
+ BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" METAMASK_BUILD_TYPE="$build_type" EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
126
136
  )
127
137
  )
128
138
 
@@ -138,10 +148,16 @@ ensure_ios_app_for_mode() {
138
148
  ;;
139
149
  auto|default)
140
150
  if ios_app_installed "$sim_target" "$bundle_id"; then return 0; fi
141
- run_ios_native_build "$sim_target" "iOS dev client ${bundle_id} not installed on ${sim_target}"
151
+ if ! run_ios_native_build "$sim_target" "$bundle_id" "iOS dev client ${bundle_id} not installed on ${sim_target}"; then
152
+ printf 'open-device: iOS native build failed\n' >&2
153
+ return 1
154
+ fi
142
155
  ;;
143
156
  rebuild-native|clean)
144
- run_ios_native_build "$sim_target" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"
157
+ if ! run_ios_native_build "$sim_target" "$bundle_id" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"; then
158
+ printf 'open-device: iOS native build failed\n' >&2
159
+ return 1
160
+ fi
145
161
  ;;
146
162
  *)
147
163
  printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
@@ -264,14 +280,113 @@ android_app_installed() {
264
280
  "$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
265
281
  }
266
282
 
267
- run_android_native_build() {
268
- local reason="$1"
269
- printf '%s; installing with yarn start:android (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
270
- (cd "$TARGET" && WATCHER_PORT="$PORT" yarn start:android 2>&1)
283
+ run_android_native_build() (
284
+ local serial="$1" expo_device="$2" pkg="$3" reason="$4" build_env="" build_script=""
285
+ case "$pkg" in
286
+ io.metamask) build_script="start:android" ;;
287
+ io.metamask.flask) build_script="start:android:flask" ;;
288
+ *)
289
+ printf 'open-device: no supported local build script for Android package %s\n' "$pkg" >&2
290
+ return 1
291
+ ;;
292
+ esac
293
+ printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
294
+ build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-android-build-env.XXXXXX")" || return 1
295
+ chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
296
+ trap 'rm -f "$build_env"' EXIT HUP INT TERM
297
+ {
298
+ printf 'export MM_HARNESS_BUILD_ANDROID_SERIAL=%q\n' "$serial"
299
+ printf 'export MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE=%q\n' "$expo_device"
300
+ printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
301
+ cat <<'BUILD_ENV'
302
+ mm_harness_restore_android_build_target() {
303
+ export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
304
+ export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
305
+ export ADB_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
306
+ export ANDROID_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
307
+ }
308
+ eval() {
309
+ builtin eval "$@"
310
+ local status=$?
311
+ mm_harness_restore_android_build_target
312
+ return "$status"
313
+ }
314
+ source() {
315
+ builtin source "$@"
316
+ local status=$?
317
+ mm_harness_restore_android_build_target
318
+ return "$status"
319
+ }
320
+ yarn() {
321
+ if [ "${1:-}" = expo ] && [ "${2:-}" = run:android ]; then
322
+ command yarn "$@" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE"
323
+ else
324
+ command yarn "$@"
325
+ fi
326
+ }
327
+ mm_harness_restore_android_build_target
328
+ BUILD_ENV
329
+ } > "$build_env"
330
+ (
331
+ cd "$TARGET"
332
+ BASH_ENV="$build_env" \
333
+ WATCHER_PORT="$PORT" METRO_PORT="$PORT" \
334
+ ADB_SERIAL="$serial" ANDROID_SERIAL="$serial" \
335
+ EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
336
+ )
337
+ )
338
+
339
+ resolve_android_expo_device() {
340
+ local serial="$1" rows="" selected_line="" expo_device="" row_serial="" row_state="" row_name="" matches=0
341
+ [ -n "$serial" ] || return 0
342
+ rows="$($ADB_BIN devices -l 2>/dev/null || true)"
343
+ selected_line="$(printf '%s\n' "$rows" | awk -v wanted="$serial" '$1 == wanted { print; exit }')"
344
+ if [ -z "$selected_line" ]; then
345
+ printf 'open-device: configured Android device %s is not attached\n' "$serial" >&2
346
+ return 1
347
+ fi
348
+ row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
349
+ if [ "$row_state" != "device" ]; then
350
+ printf 'open-device: configured Android device %s is not ready (state: %s)\n' "$serial" "$row_state" >&2
351
+ return 1
352
+ fi
353
+ if printf '%s' "$serial" | grep -q '^emulator-'; then
354
+ expo_device="$($ADB_BIN -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
355
+ else
356
+ expo_device="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
357
+ fi
358
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
359
+ expo_device="${expo_device// /_}"
360
+ if [ -z "$expo_device" ]; then
361
+ printf 'open-device: could not resolve Expo device name for Android serial %s\n' "$serial" >&2
362
+ return 1
363
+ fi
364
+
365
+ while IFS= read -r selected_line; do
366
+ row_serial="$(printf '%s\n' "$selected_line" | awk '{print $1}')"
367
+ row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
368
+ [ "$row_state" = "device" ] || continue
369
+ if printf '%s' "$row_serial" | grep -q '^emulator-'; then
370
+ row_name="$($ADB_BIN -s "$row_serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
371
+ else
372
+ row_name="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
373
+ fi
374
+ row_name="${row_name// /_}"
375
+ [ "$row_name" = "$expo_device" ] && matches=$((matches + 1))
376
+ done <<EOF
377
+ $rows
378
+ EOF
379
+ if [ "$matches" -ne 1 ]; then
380
+ printf 'open-device: Expo device name %s is ambiguous across %s attached Android devices; Expo cannot select serial %s safely\n' \
381
+ "$expo_device" "$matches" "$serial" >&2
382
+ return 1
383
+ fi
384
+ printf '%s\n' "$expo_device"
271
385
  }
272
386
 
273
387
  ensure_android_app_for_mode() {
274
388
  local pkg="$1" mode="$2"
389
+ local expo_device=""
275
390
  shift 2
276
391
  case "$mode" in
277
392
  fast)
@@ -281,10 +396,26 @@ ensure_android_app_for_mode() {
281
396
  ;;
282
397
  auto|default)
283
398
  if android_app_installed "$pkg" "$@"; then return 0; fi
284
- run_android_native_build "Android dev client ${pkg} not installed"
399
+ if [ -n "$ADB_SERIAL_ARG" ]; then
400
+ expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
401
+ fi
402
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
403
+ expo_device="${expo_device// /_}"
404
+ if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Android dev client ${pkg} not installed"; then
405
+ printf 'open-device: Android native build failed\n' >&2
406
+ return 1
407
+ fi
285
408
  ;;
286
409
  rebuild-native|clean)
287
- run_android_native_build "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"
410
+ if [ -n "$ADB_SERIAL_ARG" ]; then
411
+ expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
412
+ fi
413
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
414
+ expo_device="${expo_device// /_}"
415
+ if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"; then
416
+ printf 'open-device: Android native build failed\n' >&2
417
+ return 1
418
+ fi
288
419
  ;;
289
420
  *)
290
421
  printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
@@ -301,8 +432,16 @@ PY
301
432
 
302
433
  # --- dispatch -----------------------------------------------------------------
303
434
 
304
- BUNDLE_IDS=("${IOS_BUNDLE_ID:-io.metamask.MetaMask}" "io.metamask" "io.metamask.flask" "io.metamask.qa")
305
- ANDROID_PKGS=("${ANDROID_PACKAGE_ID:-io.metamask}" "io.metamask.flask" "io.metamask.qa")
435
+ if [ -n "${IOS_BUNDLE_ID:-}" ]; then
436
+ BUNDLE_IDS=("$IOS_BUNDLE_ID")
437
+ else
438
+ BUNDLE_IDS=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
439
+ fi
440
+ if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
441
+ ANDROID_PKGS=("$ANDROID_PACKAGE_ID")
442
+ else
443
+ ANDROID_PKGS=("io.metamask" "io.metamask.flask" "io.metamask.qa")
444
+ fi
306
445
 
307
446
  if [ "$PLATFORM" = "ios" ]; then
308
447
  SIM_TARGET="$(sim_udid_for_target "$SIMULATOR")"
@@ -330,7 +469,12 @@ if [ "$PLATFORM" = "ios" ]; then
330
469
 
331
470
  LAUNCHED=false
332
471
  for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
333
- ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE" || continue
472
+ if ! ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE"; then
473
+ case "$PREFLIGHT_MODE" in
474
+ fast) continue ;;
475
+ *) exit 1 ;;
476
+ esac
477
+ fi
334
478
  if $RESTART; then
335
479
  xcrun simctl terminate "$SIM_TARGET" "$BUNDLE_ID" >/dev/null 2>&1 || true
336
480
  printf 'Restarting iOS dev client %s on %s\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
@@ -378,18 +522,25 @@ else
378
522
  ADB_ARGS=()
379
523
  [ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")
380
524
 
381
- # Reverse-tunnel Metro port so the device can reach localhost:PORT.
382
- "$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
383
- printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2
384
-
385
525
  LAUNCHED=false
386
526
  for PKG in "${ANDROID_PKGS[@]}"; do
387
- ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}" || continue
527
+ if ! ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}"; then
528
+ case "$PREFLIGHT_MODE" in
529
+ fast) continue ;;
530
+ *) exit 1 ;;
531
+ esac
532
+ fi
388
533
  if $RESTART; then
389
534
  "$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
390
535
  printf 'Restarting Android dev client %s\n' "$PKG" >&2
391
536
  fi
392
537
 
538
+ # Installing/rebuilding a dev client can invalidate adb reverse mappings.
539
+ # Restore Metro reachability only after the app is ensured, immediately
540
+ # before opening the development-client URL.
541
+ "$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
542
+ printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2
543
+
393
544
  ENCODED_URL="$(urlencode "http://localhost:${PORT}?disableOnboarding=1" 2>/dev/null \
394
545
  || printf 'http%%3A%%2F%%2Flocalhost%%3A%s%%3FdisableOnboarding%%3D1' "$PORT")"
395
546
  DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"
@@ -2,9 +2,10 @@
2
2
  # start-metro — ensure the Metro bundler is running for this MetaMask Mobile checkout.
3
3
  #
4
4
  # Checks if Metro is already running and owned by this checkout with required env vars.
5
- # Clears stale or foreign listeners before starting. Uses yarn expo start --port (not
6
- # yarn start), sources .js.env/.env/.env.local, sets EXPO_NO_TYPESCRIPT_SETUP=1, and
7
- # caps METRO_MAX_WORKERS to 6 (or the physical CPU count).
5
+ # Clears stale or foreign listeners before starting. Uses the checkout's canonical
6
+ # yarn watch script and sets EXPO_NO_TYPESCRIPT_SETUP=1. Cache clearing is
7
+ # injected as Expo --clear without invoking Mobile's global watchman cleanup.
8
+ # Metro owns its default worker count; METRO_MAX_WORKERS is an explicit override.
8
9
  #
9
10
  # Inputs:
10
11
  # --target <metamask-mobile dir> (default $PWD)
@@ -49,7 +50,7 @@ fi
49
50
  # only ever inspect or signal a Metro listening on $PORT.
50
51
  # shellcheck disable=SC1091
51
52
  . "$SCRIPT_DIR/lib/metro-listener.sh"
52
- MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID} MM_HARNESS_METRO_PROJECT_ROOT EXPO_OVERRIDE_METRO_CONFIG"
53
+ MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT}"
53
54
 
54
55
  # RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
55
56
  # sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
@@ -57,39 +58,90 @@ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
57
58
  mkdir -p "$LOG_DIR"
58
59
  LOG_FILE="$LOG_DIR/metro.log"
59
60
  PID_FILE="$LOG_DIR/metro.pid"
60
- INLINE_ENV_FILE="$LOG_DIR/metro-inline-env.sh"
61
+ METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
61
62
 
62
63
  # --- helpers ------------------------------------------------------------------
63
64
 
64
- default_metro_workers() {
65
- local cores=""
66
- if command -v sysctl >/dev/null 2>&1; then
67
- cores="$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null || sysctl -n hw.physicalcpu 2>/dev/null || true)"
68
- fi
69
- if printf '%s' "${cores:-}" | grep -Eq '^[0-9]+$' && [ "${cores:-0}" -gt 0 ]; then
70
- [ "${cores:-4}" -gt 6 ] && cores=6
71
- printf '%s\n' "$cores"
65
+ write_metro_build_env() {
66
+ local temporary name staged_name product_values build_type environment sentry_dsn clear_value=0
67
+ for name in $MOBILE_METRO_REQUIRED_ENV; do
68
+ if ! printf '%s' "$name" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$'; then
69
+ printf 'start-metro: invalid required environment variable name: %s\n' "$name" >&2
70
+ return 1
71
+ fi
72
+ done
73
+ temporary="$(mktemp "$LOG_DIR/.metro-build-env.XXXXXX")" || return 1
74
+ chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
75
+ product_values="$(
76
+ cd "$TARGET" || exit 1
77
+ set -a
78
+ # shellcheck disable=SC1091
79
+ [ ! -f .js.env ] || eval "$(tr -d '\r' < .js.env)"
80
+ set +a
81
+ METAMASK_BUILD_TYPE="${METAMASK_BUILD_TYPE:-main}"
82
+ METAMASK_ENVIRONMENT="${METAMASK_ENVIRONMENT:-dev}"
83
+ printf 'build_type=%q\n' "$METAMASK_BUILD_TYPE"
84
+ printf 'environment=%q\n' "$METAMASK_ENVIRONMENT"
85
+ printf 'sentry_dsn=%q\n' "${MM_SENTRY_DSN:-${MM_SENTRY_DSN_DEV:-}}"
86
+ for name in $MOBILE_METRO_REQUIRED_ENV; do
87
+ printf 'required_%s=%q\n' "$name" "${!name-}"
88
+ done
89
+ )" || { rm -f "$temporary"; return 1; }
90
+ eval "$product_values"
91
+ [ "$CLEAR" = true ] && clear_value=1
92
+ {
93
+ printf 'export MM_HARNESS_METRO_WATCHER_PORT=%q\n' "$PORT"
94
+ printf 'export MM_HARNESS_METRO_BUILD_TYPE=%q\n' "$build_type"
95
+ printf 'export MM_HARNESS_METRO_ENVIRONMENT=%q\n' "$environment"
96
+ printf 'export MM_HARNESS_METRO_SENTRY_DSN=%q\n' "$sentry_dsn"
97
+ printf 'export MM_HARNESS_METRO_CLEAR=%q\n' "$clear_value"
98
+ cat <<'BUILD_ENV'
99
+ mm_harness_restore_metro_env() {
100
+ export WATCHER_PORT="$MM_HARNESS_METRO_WATCHER_PORT"
101
+ export METRO_PORT="$MM_HARNESS_METRO_WATCHER_PORT"
102
+ export METAMASK_BUILD_TYPE="$MM_HARNESS_METRO_BUILD_TYPE"
103
+ export METAMASK_ENVIRONMENT="$MM_HARNESS_METRO_ENVIRONMENT"
104
+ export MM_SENTRY_DSN="$MM_HARNESS_METRO_SENTRY_DSN"
105
+ BUILD_ENV
106
+ for name in $MOBILE_METRO_REQUIRED_ENV; do
107
+ staged_name="required_$name"
108
+ printf ' export %s=%q\n' "$name" "${!staged_name}"
109
+ done
110
+ for name in \
111
+ SEGMENT_PROXY_URL \
112
+ SEGMENT_WRITE_KEY \
113
+ SEGMENT_FLUSH_INTERVAL \
114
+ SEGMENT_FLUSH_EVENT_LIMIT
115
+ do
116
+ if declare -p "$name" >/dev/null 2>&1; then
117
+ printf ' export %s=%q\n' "$name" "${!name}"
118
+ fi
119
+ done
120
+ cat <<'BUILD_ENV'
121
+ }
122
+ eval() {
123
+ builtin eval "$@"
124
+ local status=$?
125
+ mm_harness_restore_metro_env
126
+ return "$status"
127
+ }
128
+ source() {
129
+ builtin source "$@"
130
+ local status=$?
131
+ mm_harness_restore_metro_env
132
+ return "$status"
133
+ }
134
+ yarn() {
135
+ if [ "$MM_HARNESS_METRO_CLEAR" = 1 ] && [ "${1:-}" = expo ] && [ "${2:-}" = start ]; then
136
+ command yarn "$@" --clear
72
137
  else
73
- printf '4\n'
138
+ command yarn "$@"
74
139
  fi
75
140
  }
76
-
77
- write_inline_metro_env() {
78
- local temporary name
79
- temporary="$(mktemp "$LOG_DIR/.metro-inline-env.XXXXXX")" || return 1
80
- chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
81
- for name in \
82
- SEGMENT_PROXY_URL \
83
- SEGMENT_WRITE_KEY \
84
- SEGMENT_FLUSH_INTERVAL \
85
- SEGMENT_FLUSH_EVENT_LIMIT
86
- do
87
- if declare -p "$name" >/dev/null 2>&1; then
88
- printf 'export %s=%q\n' "$name" "${!name}" >> "$temporary" \
89
- || { rm -f "$temporary"; return 1; }
90
- fi
91
- done
92
- mv -f "$temporary" "$INLINE_ENV_FILE"
141
+ mm_harness_restore_metro_env
142
+ BUILD_ENV
143
+ } > "$temporary" || { rm -f "$temporary"; return 1; }
144
+ mv -f "$temporary" "$METRO_BUILD_ENV_FILE"
93
145
  }
94
146
 
95
147
  # Log-tail viewer window (session-safe): sourced so its run-owned-session rule is
@@ -99,37 +151,20 @@ write_inline_metro_env() {
99
151
 
100
152
  write_metro_runner() {
101
153
  local runner="$1"
102
- local clear_flag=""
103
- [ "$CLEAR" = true ] && clear_flag=' --clear'
154
+ local watcher_script="watch"
155
+ local worker_env_line='unset METRO_MAX_WORKERS'
156
+ if [ -n "$METRO_WORKERS" ]; then
157
+ worker_env_line="export METRO_MAX_WORKERS=$(printf '%q' "$METRO_WORKERS")"
158
+ fi
104
159
  cat > "$runner" <<EOF
105
160
  #!/usr/bin/env bash
106
161
  set -uo pipefail
107
162
  cd "$(printf '%q' "$TARGET")"
108
163
  : > "$(printf '%q' "$LOG_FILE")"
109
- set -a
110
- [ ! -f .js.env ] || . ./.js.env
111
- [ ! -f .env ] || . ./.env
112
- [ ! -f .env.local ] || . ./.env.local
113
- set +a
114
- [ -n "\${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
115
- # build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds; quick-launch
116
- # bypasses it, so without this Sentry never initializes in the dev client.
117
- [ -n "\${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="\${MM_SENTRY_DSN_DEV:-}"
118
- [ ! -f $(printf '%q' "$INLINE_ENV_FILE") ] || {
119
- . $(printf '%q' "$INLINE_ENV_FILE")
120
- rm -f $(printf '%q' "$INLINE_ENV_FILE")
121
- }
122
164
  export EXPO_NO_TYPESCRIPT_SETUP=1
123
165
  export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
124
- export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
125
- export MM_HARNESS_METRO_PROJECT_ROOT="$(printf '%q' "$TARGET")"
126
- if [ -n "\${EXPO_OVERRIDE_METRO_CONFIG:-}" ]; then
127
- export MM_HARNESS_METRO_BASE_CONFIG="\$EXPO_OVERRIDE_METRO_CONFIG"
128
- else
129
- export MM_HARNESS_METRO_BASE_CONFIG=$(printf '%q' "$TARGET/metro.config.js")
130
- fi
131
- export EXPO_OVERRIDE_METRO_CONFIG="$(printf '%q' "$SCRIPT_DIR/metro-config.cjs")"
132
- yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
166
+ $worker_env_line
167
+ BASH_ENV=$(printf '%q' "$METRO_BUILD_ENV_FILE") command yarn $(printf '%q' "$watcher_script") 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
133
168
  EOF
134
169
  chmod +x "$runner"
135
170
  }
@@ -180,64 +215,61 @@ fi
180
215
 
181
216
  stop_metro_listener || exit 1
182
217
 
183
- METRO_WORKERS="${METRO_MAX_WORKERS:-$(default_metro_workers)}"
218
+ METRO_WORKERS="${METRO_MAX_WORKERS:-}"
219
+ if [ -n "$METRO_WORKERS" ] && ! printf '%s' "$METRO_WORKERS" | grep -Eq '^[1-9][0-9]*$'; then
220
+ printf 'start-metro: METRO_MAX_WORKERS must be a positive integer, got %s\n' "$METRO_WORKERS" >&2
221
+ exit 2
222
+ fi
223
+ READY_TIMEOUT="${MOBILE_METRO_READY_TIMEOUT_SECONDS:-180}"
224
+ if ! printf '%s' "$READY_TIMEOUT" | grep -Eq '^[1-9][0-9]*$'; then
225
+ printf 'start-metro: MOBILE_METRO_READY_TIMEOUT_SECONDS must be a positive integer, got %s\n' "$READY_TIMEOUT" >&2
226
+ exit 2
227
+ fi
184
228
  CLEAR_LABEL=""
185
229
  [ "$CLEAR" = true ] && CLEAR_LABEL=", clear"
186
230
 
187
- printf 'Starting Metro on port %s (workers=%s%s)\n' \
188
- "$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
231
+ if [ -n "$METRO_WORKERS" ]; then
232
+ printf 'Starting Metro on port %s (workers=%s%s)\n' \
233
+ "$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
234
+ else
235
+ printf 'Starting Metro on port %s (workers=Metro default%s)\n' \
236
+ "$PORT" "$CLEAR_LABEL" >&2
237
+ fi
189
238
  printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
190
239
 
191
- write_inline_metro_env || {
192
- printf 'start-metro: could not stage inline Metro environment\n' >&2
240
+ write_metro_build_env || {
241
+ printf 'start-metro: could not stage the scoped Metro environment\n' >&2
193
242
  exit 1
194
243
  }
195
244
 
196
- if ! start_metro_tmux; then
245
+ STARTED_METRO_PID=""
246
+ STARTED_METRO_TMUX=""
247
+ if start_metro_tmux; then
248
+ STARTED_METRO_TMUX="$(cat "$LOG_DIR/metro.tmux" 2>/dev/null || true)"
249
+ else
197
250
  # Metro runs detached, writing to the log. The tmux window is a read-only tail.
198
251
  (
199
252
  cd "$TARGET"
200
253
  : > "$LOG_FILE"
201
- # Source product env files (provides MM_INFURA_PROJECT_ID and friends).
202
- set -a
203
- # shellcheck disable=SC1091
204
- [ ! -f .js.env ] || . ./.js.env
205
- # shellcheck disable=SC1091
206
- [ ! -f .env ] || . ./.env
207
- # shellcheck disable=SC1091
208
- [ ! -f .env.local ] || . ./.env.local
209
- set +a
210
- # The quick-launch runs `expo start` directly, bypassing scripts/build.sh which
211
- # passes the build type as an argument (start:ios = build.sh ios main dev). The
212
- # fixture .js.env ships METAMASK_BUILD_TYPE="" (empty), which Metro's transform
213
- # rejects. Default to the main dev client (what runway installs / launch targets)
214
- # so already-installed slots work without re-syncing fixtures.
215
- [ -n "${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
216
- # build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds (start:ios
217
- # = build.sh ios main dev); quick-launch bypasses build.sh, so without this the
218
- # dev client's MM_SENTRY_DSN stays empty and Sentry never initializes.
219
- [ -n "${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="${MM_SENTRY_DSN_DEV:-}"
220
- [ ! -f "$INLINE_ENV_FILE" ] || {
221
- # shellcheck disable=SC1090
222
- . "$INLINE_ENV_FILE"
223
- rm -f "$INLINE_ENV_FILE"
224
- }
225
254
  export EXPO_NO_TYPESCRIPT_SETUP=1
226
255
  export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
227
- export METRO_MAX_WORKERS="${METRO_WORKERS}"
228
- export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
229
- export MM_HARNESS_METRO_BASE_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
230
- export EXPO_OVERRIDE_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
256
+ if [ -n "$METRO_WORKERS" ]; then
257
+ export METRO_MAX_WORKERS="${METRO_WORKERS}"
258
+ else
259
+ unset METRO_MAX_WORKERS
260
+ fi
231
261
  launcher_args=(
232
262
  --target "$TARGET"
233
263
  --port "$PORT"
234
264
  --log "$LOG_FILE"
235
265
  --pid-file "$PID_FILE"
236
- --workers "$METRO_WORKERS"
266
+ --build-env "$METRO_BUILD_ENV_FILE"
237
267
  )
268
+ [ -z "$METRO_WORKERS" ] || launcher_args+=(--workers "$METRO_WORKERS")
238
269
  [ "$CLEAR" = true ] && launcher_args+=(--clear)
239
270
  node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
240
271
  ) || exit 1
272
+ STARTED_METRO_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
241
273
 
242
274
  start_viewer_window "$LOG_FILE"
243
275
  fi
@@ -256,9 +288,11 @@ if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1 \
256
288
  TAIL_PID=$!
257
289
  fi
258
290
 
259
- # Ready-poll: 90 × 1s = 90 s timeout.
291
+ # Cold main bundles can spend more than 90 seconds initializing Metro before the
292
+ # status listener opens. Keep the wait bounded, but do not trigger a duplicate
293
+ # recovery process while the checkout-owned launch is still making progress.
260
294
  READY=false
261
- for _i in $(seq 1 90); do
295
+ for _i in $(seq 1 "$READY_TIMEOUT"); do
262
296
  if metro_ready; then
263
297
  READY=true
264
298
  break
@@ -277,12 +311,50 @@ if [ "$READY" = true ]; then
277
311
  exit 0
278
312
  fi
279
313
 
314
+ cleanup_timed_out_metro() {
315
+ local pid pgid i group_alive=false owned_listener_alive=false listener_pid command
316
+ pid="$STARTED_METRO_PID"
317
+ # launch-metro creates a detached group whose PGID equals its initial child
318
+ # PID. The leader can exit before Expo, so probe the group rather than the PID.
319
+ pgid="$pid"
320
+ if [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null; then
321
+ printf 'start-metro: stopping timed-out Metro process group %s\n' "$pgid" >&2
322
+ kill -TERM -- "-$pgid" 2>/dev/null || true
323
+ for i in $(seq 1 20); do
324
+ kill -0 -- "-$pgid" 2>/dev/null || break
325
+ sleep 0.25
326
+ done
327
+ if kill -0 -- "-$pgid" 2>/dev/null; then
328
+ kill -KILL -- "-$pgid" 2>/dev/null || true
329
+ for i in $(seq 1 20); do
330
+ kill -0 -- "-$pgid" 2>/dev/null || break
331
+ sleep 0.25
332
+ done
333
+ fi
334
+ kill -0 -- "-$pgid" 2>/dev/null && group_alive=true
335
+ fi
336
+ if [ -n "$STARTED_METRO_TMUX" ] && command -v tmux >/dev/null 2>&1; then
337
+ tmux kill-window -t "$STARTED_METRO_TMUX" >/dev/null 2>&1 || true
338
+ fi
339
+ for listener_pid in $(metro_listener_pids); do
340
+ command="$(ps eww -p "$listener_pid" -o command= 2>/dev/null || true)"
341
+ case "$command" in *"$TARGET"*) owned_listener_alive=true ;; esac
342
+ done
343
+ if [ "$group_alive" = true ] || [ "$owned_listener_alive" = true ]; then
344
+ printf 'start-metro: timed-out checkout-owned process is still alive; retaining launch metadata for recovery\n' >&2
345
+ return 1
346
+ fi
347
+ rm -f "$PID_FILE" "$LOG_DIR/metro-launch.json" "$LOG_DIR/metro-build-env.sh" "$LOG_DIR/metro.tmux"
348
+ return 0
349
+ }
350
+
280
351
  # Failure: dump log events for diagnosis.
281
352
  if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1; then
282
353
  node "$LOG_TUI" events --log "$LOG_FILE" --events 20 >&2 || true
283
354
  else
284
355
  tail -40 "$LOG_FILE" >&2 || true
285
356
  fi
286
- printf 'start-metro: Metro did not become ready on port %s within 90s\n' "$PORT" >&2
357
+ cleanup_timed_out_metro || true
358
+ printf 'start-metro: Metro did not become ready on port %s within %ss\n' "$PORT" "$READY_TIMEOUT" >&2
287
359
  printf ' Next: check %s for errors, or run mm-harness logs\n' "$LOG_FILE" >&2
288
360
  exit 1
@@ -62,6 +62,7 @@ fi
62
62
  set +e
63
63
  rm -f "$PID_FILE"
64
64
  rm -f "$LOG_DIR/metro-launch.json"
65
+ rm -f "$LOG_DIR/metro-build-env.sh"
65
66
 
66
67
  # start-metro runs a per-checkout console-forwarder holding the device debugger
67
68
  # slot; Metro going down must take it too, or the orphan keeps polling and later
@@ -116,10 +116,7 @@ async function prepareMobile(target, opts = {}) {
116
116
  );
117
117
  }
118
118
  const launch = await dispatchActionSequence(
119
- withAppRestart(
120
- launchActions(path.resolve(target)),
121
- restartApp || iosAccessibilityChanged
122
- ),
119
+ withAppRestart(launchActions(path.resolve(target)), true),
123
120
  target,
124
121
  platform,
125
122
  json,