@deeeed/metamask-harness 0.34.0 → 0.34.1
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/CHANGELOG.md +16 -0
- package/adapters/manifest.json +0 -8
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +14 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +22 -15
- package/adapters/mobile/launch-metro.cjs +6 -4
- package/adapters/mobile/open-device.sh +164 -15
- package/adapters/mobile/start-metro.sh +165 -93
- package/adapters/mobile/stop-metro.sh +1 -0
- package/dist/adapters/mobile/video-recorder.js +12 -10
- package/dist/commands/launch/index.js +9 -0
- package/dist/commands/launch/mobile.js +1 -1
- package/dist/live-adapter-contract.js +7 -0
- package/library/actions/mobile/perps/performance-capture.mjs +83 -1
- package/library/actions/mobile/platform/bridge.mjs +64 -3
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +30 -7
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +20 -2
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +2 -0
- package/package.json +1 -1
- package/adapters/mobile/metro-config.cjs +0 -93
|
@@ -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
|
|
6
|
-
# yarn
|
|
7
|
-
#
|
|
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
|
|
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
|
-
|
|
61
|
+
METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
|
|
61
62
|
|
|
62
63
|
# --- helpers ------------------------------------------------------------------
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
local
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
138
|
+
command yarn "$@"
|
|
74
139
|
fi
|
|
75
140
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
temporary
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
printf 'start-metro: could not stage
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
--
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
} from "../../recording-target.js";
|
|
8
8
|
const RECORDING_START_TIMEOUT_MS = 5e3;
|
|
9
9
|
const RECORDING_STOP_TIMEOUT_MS = 1e4;
|
|
10
|
-
const RECORDING_TIME_LIMIT_SECONDS =
|
|
10
|
+
const RECORDING_TIME_LIMIT_SECONDS = 0;
|
|
11
|
+
function androidScreenrecordArgs(remotePath) {
|
|
12
|
+
return [
|
|
13
|
+
"shell",
|
|
14
|
+
"screenrecord",
|
|
15
|
+
"--time-limit",
|
|
16
|
+
String(RECORDING_TIME_LIMIT_SECONDS),
|
|
17
|
+
remotePath
|
|
18
|
+
];
|
|
19
|
+
}
|
|
11
20
|
function createAndroidVideoRecorder(serial) {
|
|
12
21
|
return {
|
|
13
22
|
name: "adb-screenrecord",
|
|
@@ -139,15 +148,7 @@ async function startAndroidRecording(serial, request) {
|
|
|
139
148
|
const remotePath = `/sdcard/Download/mm-harness-recipe-${Date.now()}-${process.pid}.mp4`;
|
|
140
149
|
const child = spawn(
|
|
141
150
|
"adb",
|
|
142
|
-
[
|
|
143
|
-
"-s",
|
|
144
|
-
serial,
|
|
145
|
-
"shell",
|
|
146
|
-
"screenrecord",
|
|
147
|
-
"--time-limit",
|
|
148
|
-
String(RECORDING_TIME_LIMIT_SECONDS),
|
|
149
|
-
remotePath
|
|
150
|
-
],
|
|
151
|
+
["-s", serial, ...androidScreenrecordArgs(remotePath)],
|
|
151
152
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
152
153
|
);
|
|
153
154
|
const stderr = [];
|
|
@@ -277,6 +278,7 @@ function sleep(durationMs) {
|
|
|
277
278
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
278
279
|
}
|
|
279
280
|
export {
|
|
281
|
+
androidScreenrecordArgs,
|
|
280
282
|
createAndroidVideoRecorder,
|
|
281
283
|
createIosSimulatorVideoRecorder
|
|
282
284
|
};
|
|
@@ -316,6 +316,15 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
316
316
|
originalError: attempt.output.trim() || void 0
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
+
if (adapter === "mobile" && tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(attempt.output)) {
|
|
320
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
321
|
+
code: "MOBILE_NATIVE_BUILD_FAILED",
|
|
322
|
+
message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
|
|
323
|
+
recoverable: false,
|
|
324
|
+
exitCode: EXIT.runtime,
|
|
325
|
+
originalError: attempt.output.trim() || void 0
|
|
326
|
+
});
|
|
327
|
+
}
|
|
319
328
|
const bound = checkHealBounds(target, attempt.output, state);
|
|
320
329
|
if (bound !== null) {
|
|
321
330
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
@@ -4,7 +4,7 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
|
|
|
4
4
|
await ensureHarnessFresh(target, "mobile");
|
|
5
5
|
const platform = mobileTarget ?? "ios";
|
|
6
6
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
7
|
-
const preflightMode = tier === "build" ? "
|
|
7
|
+
const preflightMode = tier === "build" ? "rebuild-native" : "fast";
|
|
8
8
|
return prepareMobile(target, {
|
|
9
9
|
platform,
|
|
10
10
|
json,
|
|
@@ -356,6 +356,13 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
356
356
|
const timeoutMs = Number(node.timeout_ms);
|
|
357
357
|
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs + settleAllowance + 5e3 : 6e4;
|
|
358
358
|
}
|
|
359
|
+
const actionTimeouts = [node.target_timeout_ms, node.unlock_timeout_ms].map(Number).filter((value) => Number.isFinite(value) && value > 0);
|
|
360
|
+
if (actionTimeouts.length > 0) {
|
|
361
|
+
return Math.max(
|
|
362
|
+
6e4,
|
|
363
|
+
actionTimeouts.reduce((total, value) => total + value, 0) + 5e3
|
|
364
|
+
);
|
|
365
|
+
}
|
|
359
366
|
return 6e4 + settleAllowance;
|
|
360
367
|
}
|
|
361
368
|
function commandForPrepared(projectRoot, platform) {
|
|
@@ -205,12 +205,65 @@ function socketPipelineMeasurements(records) {
|
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
function firstFreshDeliveryChecks(records, requirements) {
|
|
209
|
+
return requirements.map((requirement) => {
|
|
210
|
+
const stream = String(requirement.stream);
|
|
211
|
+
const requiredLifecycle =
|
|
212
|
+
requirement.lifecycle === undefined
|
|
213
|
+
? null
|
|
214
|
+
: String(requirement.lifecycle);
|
|
215
|
+
const subscriberThrottleMs = Number(requirement.subscriber_throttle_ms);
|
|
216
|
+
const maxSocketToSubscriberMs = Number(
|
|
217
|
+
requirement.max_socket_to_subscriber_ms,
|
|
218
|
+
);
|
|
219
|
+
const socket = records.find(
|
|
220
|
+
(record) =>
|
|
221
|
+
record.stage === 'socket_received' &&
|
|
222
|
+
record.source === 'fresh_socket' &&
|
|
223
|
+
record.stream === stream &&
|
|
224
|
+
(requiredLifecycle === null ||
|
|
225
|
+
record.lifecycle === requiredLifecycle) &&
|
|
226
|
+
record.delivery_id,
|
|
227
|
+
);
|
|
228
|
+
const delivery = socket
|
|
229
|
+
? records.find(
|
|
230
|
+
(record) =>
|
|
231
|
+
record.stage === 'subscriber_delivery' &&
|
|
232
|
+
record.delivery_id === socket.delivery_id &&
|
|
233
|
+
record.stream === stream &&
|
|
234
|
+
record.lifecycle === socket.lifecycle &&
|
|
235
|
+
Number(record.throttle_ms ?? 0) === subscriberThrottleMs,
|
|
236
|
+
)
|
|
237
|
+
: null;
|
|
238
|
+
const socketAt = Number(socket?.monotonic_ms);
|
|
239
|
+
const subscriberAt = Number(delivery?.monotonic_ms);
|
|
240
|
+
const socketToSubscriberMs =
|
|
241
|
+
Number.isFinite(socketAt) && Number.isFinite(subscriberAt)
|
|
242
|
+
? roundMs(subscriberAt - socketAt)
|
|
243
|
+
: null;
|
|
244
|
+
return {
|
|
245
|
+
stream,
|
|
246
|
+
lifecycle: socket?.lifecycle ?? requiredLifecycle,
|
|
247
|
+
subscriberThrottleMs,
|
|
248
|
+
maxSocketToSubscriberMs,
|
|
249
|
+
deliveryId: socket?.delivery_id ?? null,
|
|
250
|
+
socketToSubscriberMs,
|
|
251
|
+
status:
|
|
252
|
+
socketToSubscriberMs !== null &&
|
|
253
|
+
socketToSubscriberMs <= maxSocketToSubscriberMs
|
|
254
|
+
? 'pass'
|
|
255
|
+
: 'fail',
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
208
260
|
function summarize(
|
|
209
261
|
records,
|
|
210
262
|
sourcePath,
|
|
211
263
|
offset,
|
|
212
264
|
bytesScanned,
|
|
213
265
|
requiredFreshContentVariants = [],
|
|
266
|
+
firstFreshDeliveryRequirements = [],
|
|
214
267
|
) {
|
|
215
268
|
const stages = {};
|
|
216
269
|
const lifecycles = {};
|
|
@@ -240,6 +293,10 @@ function summarize(
|
|
|
240
293
|
lastMonotonicMs: monotonicValues.length > 0 ? Math.max(...monotonicValues) : null,
|
|
241
294
|
visibleMeasurements: visibleMeasurements(records, requiredFreshContentVariants),
|
|
242
295
|
socketPipelineMeasurements: socketPipelineMeasurements(records),
|
|
296
|
+
firstFreshDeliveryChecks: firstFreshDeliveryChecks(
|
|
297
|
+
records,
|
|
298
|
+
firstFreshDeliveryRequirements,
|
|
299
|
+
),
|
|
243
300
|
};
|
|
244
301
|
}
|
|
245
302
|
|
|
@@ -317,6 +374,18 @@ export async function captureHomepagePerformance(input) {
|
|
|
317
374
|
contentVariantSatisfies(actual, variant),
|
|
318
375
|
),
|
|
319
376
|
);
|
|
377
|
+
const firstFreshDeliveryRequirements = Array.isArray(
|
|
378
|
+
node.first_fresh_delivery_requirements,
|
|
379
|
+
)
|
|
380
|
+
? node.first_fresh_delivery_requirements
|
|
381
|
+
: [];
|
|
382
|
+
const deliveryChecks = firstFreshDeliveryChecks(
|
|
383
|
+
records,
|
|
384
|
+
firstFreshDeliveryRequirements,
|
|
385
|
+
);
|
|
386
|
+
const failedFirstFreshDeliveries = deliveryChecks.filter(
|
|
387
|
+
(check) => check.status === 'fail',
|
|
388
|
+
);
|
|
320
389
|
|
|
321
390
|
const prefix = String(node.artifact_prefix ?? 'logs/homepage-performance');
|
|
322
391
|
const logPath = `${prefix}.log`;
|
|
@@ -329,7 +398,8 @@ export async function captureHomepagePerformance(input) {
|
|
|
329
398
|
missingStages.length === 0 &&
|
|
330
399
|
missingLifecycles.length === 0 &&
|
|
331
400
|
missingSources.length === 0 &&
|
|
332
|
-
missingFreshContentVariants.length === 0
|
|
401
|
+
missingFreshContentVariants.length === 0 &&
|
|
402
|
+
failedFirstFreshDeliveries.length === 0
|
|
333
403
|
? 'pass'
|
|
334
404
|
: 'fail',
|
|
335
405
|
requireRecords,
|
|
@@ -337,6 +407,7 @@ export async function captureHomepagePerformance(input) {
|
|
|
337
407
|
missingLifecycles,
|
|
338
408
|
missingSources,
|
|
339
409
|
missingFreshContentVariants,
|
|
410
|
+
failedFirstFreshDeliveries,
|
|
340
411
|
};
|
|
341
412
|
const summary = {
|
|
342
413
|
...summarize(
|
|
@@ -345,6 +416,7 @@ export async function captureHomepagePerformance(input) {
|
|
|
345
416
|
offset,
|
|
346
417
|
content.length - offset,
|
|
347
418
|
requiredFreshContentVariants,
|
|
419
|
+
firstFreshDeliveryRequirements,
|
|
348
420
|
),
|
|
349
421
|
validation,
|
|
350
422
|
};
|
|
@@ -371,6 +443,16 @@ export async function captureHomepagePerformance(input) {
|
|
|
371
443
|
`Missing required fresh ${MARKER} content variants: ${missingFreshContentVariants.join(', ')}.`,
|
|
372
444
|
);
|
|
373
445
|
}
|
|
446
|
+
if (failedFirstFreshDeliveries.length > 0) {
|
|
447
|
+
const failures = failedFirstFreshDeliveries.map(
|
|
448
|
+
(check) =>
|
|
449
|
+
`${check.stream}/${check.lifecycle ?? 'any'} throttle=${check.subscriberThrottleMs}ms ` +
|
|
450
|
+
`measured=${check.socketToSubscriberMs ?? 'missing'}ms max=${check.maxSocketToSubscriberMs}ms`,
|
|
451
|
+
);
|
|
452
|
+
throw new Error(
|
|
453
|
+
`First fresh ${MARKER} delivery requirements failed: ${failures.join('; ')}.`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
374
456
|
|
|
375
457
|
return {
|
|
376
458
|
phase,
|
|
@@ -23,6 +23,41 @@ export const MOBILE_BRIDGE_ERROR_CODES = BRIDGE_ERROR_CODES;
|
|
|
23
23
|
|
|
24
24
|
const execFileAsync = promisify(execFile);
|
|
25
25
|
|
|
26
|
+
let adapterActionActive = false;
|
|
27
|
+
let actionBridgeLockPath = null;
|
|
28
|
+
|
|
29
|
+
function resolveBridgeLockPath(input, env) {
|
|
30
|
+
const configuredRuntime = env.RECIPE_RUNTIME_DIR;
|
|
31
|
+
const runtime = configuredRuntime
|
|
32
|
+
? (path.isAbsolute(configuredRuntime)
|
|
33
|
+
? configuredRuntime
|
|
34
|
+
: path.resolve(input.context.projectRoot, configuredRuntime))
|
|
35
|
+
: path.join(input.context.projectRoot, 'temp', 'recipe', 'runtime');
|
|
36
|
+
return path.join(runtime, 'cdp-bridge.lock');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function acquireActionBridgeLock(input, env) {
|
|
40
|
+
if (actionBridgeLockPath) return String(process.pid);
|
|
41
|
+
const lockPath = resolveBridgeLockPath(input, env);
|
|
42
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
43
|
+
await writeFile(lockPath, String(process.pid));
|
|
44
|
+
actionBridgeLockPath = lockPath;
|
|
45
|
+
return String(process.pid);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function releaseActionBridgeLock() {
|
|
49
|
+
const lockPath = actionBridgeLockPath;
|
|
50
|
+
actionBridgeLockPath = null;
|
|
51
|
+
if (!lockPath) return;
|
|
52
|
+
try {
|
|
53
|
+
if ((await readFile(lockPath, 'utf8')).trim() === String(process.pid)) {
|
|
54
|
+
await rm(lockPath, { force: true });
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// The bridge child or a newer owner may already have replaced the lock.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
// Recover the typed code a cdp-bridge child reported: the stderr marker is
|
|
27
62
|
// authoritative (it carries the class the bridge classified at the source), the
|
|
28
63
|
// coded exit status is the fallback for output-less failures, and a raw message
|
|
@@ -52,8 +87,14 @@ export async function writeOutput(input, output) {
|
|
|
52
87
|
|
|
53
88
|
export async function runAdapter(callback) {
|
|
54
89
|
const input = await loadInput();
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
adapterActionActive = true;
|
|
91
|
+
try {
|
|
92
|
+
const output = await callback(input);
|
|
93
|
+
await writeOutput(input, output);
|
|
94
|
+
} finally {
|
|
95
|
+
adapterActionActive = false;
|
|
96
|
+
await releaseActionBridgeLock();
|
|
97
|
+
}
|
|
57
98
|
}
|
|
58
99
|
|
|
59
100
|
function bridgeScript(input) {
|
|
@@ -127,7 +168,13 @@ export async function bridgeEnv(input) {
|
|
|
127
168
|
// (e.g. "Pixel 6" matches Metro deviceName "Pixel 6 - 16 - API 36").
|
|
128
169
|
const serialStr = adbSerial != null ? String(adbSerial) : '';
|
|
129
170
|
const deviceStr = androidDevice != null ? String(androidDevice) : '';
|
|
130
|
-
if (
|
|
171
|
+
if (
|
|
172
|
+
shouldResolveAndroidTargetModel(
|
|
173
|
+
serialStr,
|
|
174
|
+
deviceStr,
|
|
175
|
+
androidTargetDeviceName,
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
131
178
|
const model = await resolveAndroidModel(serialStr);
|
|
132
179
|
if (model) {
|
|
133
180
|
env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
@@ -150,6 +197,17 @@ export async function bridgeEnv(input) {
|
|
|
150
197
|
return env;
|
|
151
198
|
}
|
|
152
199
|
|
|
200
|
+
export function shouldResolveAndroidTargetModel(
|
|
201
|
+
adbSerial,
|
|
202
|
+
androidDevice,
|
|
203
|
+
androidTargetDeviceName,
|
|
204
|
+
) {
|
|
205
|
+
const serial = String(adbSerial ?? '').trim();
|
|
206
|
+
const device = String(androidDevice ?? '').trim();
|
|
207
|
+
const targetName = String(androidTargetDeviceName ?? '').trim();
|
|
208
|
+
return Boolean(serial && !targetName && (!device || device === serial));
|
|
209
|
+
}
|
|
210
|
+
|
|
153
211
|
function resolveMobileTarget(input) {
|
|
154
212
|
const contextEnv = input.context?.env || {};
|
|
155
213
|
const watcherPort = input.node?.watcher_port ?? input.node?.metro_port ?? input.node?.cdp_port ?? contextEnv.WATCHER_PORT ?? contextEnv.CDP_PORT ?? contextEnv.RECIPE_CDP_PORT ?? process.env.WATCHER_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
@@ -314,6 +372,9 @@ export async function bridgeCommand(input, args) {
|
|
|
314
372
|
const script = bridgeScript(input);
|
|
315
373
|
// bridgeEnv is async: it may call `adb getprop` to resolve the Metro device name.
|
|
316
374
|
const env = await bridgeEnv(input);
|
|
375
|
+
if (adapterActionActive) {
|
|
376
|
+
env.CDP_BRIDGE_LOCK_OWNER_PID = await acquireActionBridgeLock(input, env);
|
|
377
|
+
}
|
|
317
378
|
const result = await new Promise((resolve, reject) => {
|
|
318
379
|
const timeoutMs = Number(input.node?.bridge_timeout_ms ?? input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? 30000);
|
|
319
380
|
const child = spawn(process.execPath, [script, ...args], {
|