@deeeed/metamask-harness 0.47.3 → 0.47.4
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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.47.4 - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Verify Mobile reloads through execution-context replacement when React Native retains its Hermes target ID, and advance broker runtime identity on context reset for lifecycle readiness checks.
|
|
10
|
+
|
|
5
11
|
## 0.47.3 - 2026-09-05
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -862,6 +862,10 @@ function createCdpBroker({
|
|
|
862
862
|
}
|
|
863
863
|
},
|
|
864
864
|
onCdpEvent(deviceId, method, params) {
|
|
865
|
+
if (method === 'Runtime.executionContextsCleared') {
|
|
866
|
+
const target = knownTargets.get(deviceId);
|
|
867
|
+
if (target) knownTargets.set(deviceId, { ...target, generation: target.generation + 1 });
|
|
868
|
+
}
|
|
865
869
|
for (const socket of clients) {
|
|
866
870
|
if (subscriptions.get(socket)?.has(`${deviceId}\u0000${method}`)) {
|
|
867
871
|
writeMessage(socket, { type: 'event', deviceId, method, params });
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
import { createRequire } from 'node:module';
|
|
5
7
|
import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
|
|
6
8
|
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
const WebSocket = require('ws');
|
|
9
|
-
const { discoverTarget } = require('./bridge-runtime/lib/target-discovery.cjs');
|
|
11
|
+
const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
|
|
12
|
+
const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
|
|
13
|
+
const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
|
|
14
|
+
|
|
15
|
+
async function observeRuntimeReset(port, target) {
|
|
16
|
+
const socketPath = brokerSocketPath(
|
|
17
|
+
process.env.RECIPE_RUNTIME_DIR || path.join(process.cwd(), 'temp/recipe/runtime'),
|
|
18
|
+
port,
|
|
19
|
+
);
|
|
20
|
+
const client = fs.existsSync(socketPath)
|
|
21
|
+
? await createBrokerClient(socketPath, deviceIdFromUrl(target.wsUrl), 5000)
|
|
22
|
+
: await createWSClient(target.wsUrl, 5000);
|
|
23
|
+
let armed = false;
|
|
24
|
+
let cleared = false;
|
|
25
|
+
let recreated = false;
|
|
26
|
+
client.on('Runtime.executionContextsCleared', () => {
|
|
27
|
+
if (armed) cleared = true;
|
|
28
|
+
});
|
|
29
|
+
client.on('Runtime.executionContextCreated', () => {
|
|
30
|
+
if (armed && cleared) recreated = true;
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await client.send('Runtime.enable');
|
|
34
|
+
// Finish subscription/bootstrap before the reload request can generate events.
|
|
35
|
+
await client.send('Runtime.evaluate', { expression: 'true', returnByValue: true });
|
|
36
|
+
armed = true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
client.close();
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
get recreated() { return recreated; },
|
|
43
|
+
async responsive(timeoutMs) {
|
|
44
|
+
const result = await client.send('Runtime.evaluate', {
|
|
45
|
+
expression: 'true', returnByValue: true,
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
return result?.result?.value === true;
|
|
48
|
+
},
|
|
49
|
+
close() { client.close(); },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
10
52
|
|
|
11
53
|
function parseArgs(argv) {
|
|
12
54
|
const args = {
|
|
@@ -46,30 +88,38 @@ async function reload(port) {
|
|
|
46
88
|
'The Android Hermes sampling profiler is active, so a runtime reload can abort the app. Use an explicit app.lifecycle restart.',
|
|
47
89
|
);
|
|
48
90
|
}
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
socket.on('open', () => {
|
|
56
|
-
socket.send(JSON.stringify({ method: 'reload', version: 2 }));
|
|
57
|
-
setTimeout(() => {
|
|
58
|
-
clearTimeout(timeout);
|
|
91
|
+
const observer = await observeRuntimeReset(numericPort, targetBefore);
|
|
92
|
+
let verification;
|
|
93
|
+
try {
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
59
97
|
socket.close();
|
|
60
|
-
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
reject(new Error(`Metro reload timed out on port ${numericPort}`));
|
|
99
|
+
}, 5000);
|
|
100
|
+
socket.on('open', () => {
|
|
101
|
+
socket.send(JSON.stringify({ method: 'reload', version: 2 }));
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
socket.close();
|
|
105
|
+
resolve();
|
|
106
|
+
}, 50);
|
|
107
|
+
});
|
|
108
|
+
socket.on('error', () => {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
reject(new Error(`Metro is not reachable on port ${numericPort}`));
|
|
111
|
+
});
|
|
66
112
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
113
|
+
verification = await waitForReload(
|
|
114
|
+
numericPort,
|
|
115
|
+
targetBefore,
|
|
116
|
+
processBefore,
|
|
117
|
+
observer,
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
observer.close();
|
|
121
|
+
}
|
|
122
|
+
const { target: targetAfter, proof } = verification;
|
|
73
123
|
const processAfter = processBefore ? readAndroidPid() : null;
|
|
74
124
|
if (processBefore && processAfter !== processBefore) {
|
|
75
125
|
throw new Error(
|
|
@@ -84,6 +134,7 @@ async function reload(port) {
|
|
|
84
134
|
port: numericPort,
|
|
85
135
|
targetBefore: targetBefore.id,
|
|
86
136
|
targetAfter: targetAfter.id,
|
|
137
|
+
verification: proof,
|
|
87
138
|
...(processBefore
|
|
88
139
|
? {
|
|
89
140
|
processBefore,
|
|
@@ -94,20 +145,33 @@ async function reload(port) {
|
|
|
94
145
|
};
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
async function waitForReload(port, targetBefore, processBefore) {
|
|
148
|
+
async function waitForReload(port, targetBefore, processBefore, observer) {
|
|
98
149
|
const deadline = Date.now() + 30_000;
|
|
150
|
+
let lastError;
|
|
99
151
|
while (Date.now() < deadline) {
|
|
100
152
|
if (processBefore && readAndroidPid() !== processBefore) {
|
|
101
153
|
throw new Error(
|
|
102
154
|
`Android app process ${processBefore} exited during reload. Inspect the native crash before using an explicit app.lifecycle restart.`,
|
|
103
155
|
);
|
|
104
156
|
}
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/list`, {
|
|
159
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(1000, deadline - Date.now()))),
|
|
160
|
+
});
|
|
161
|
+
const targets = rankRuntimeCandidates(await response.json());
|
|
162
|
+
const target = targets.find((item) => item.deviceName === targetBefore.deviceName);
|
|
163
|
+
if (target && target.id !== targetBefore.id) return { target, proof: 'target-replaced' };
|
|
164
|
+
if (target && observer.recreated && await observer.responsive(Math.max(1, Math.min(1000, deadline - Date.now())))) {
|
|
165
|
+
return { target, proof: 'execution-context-recreated' };
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// A reload temporarily removes the runtime; preserve the last failure if it never returns.
|
|
169
|
+
lastError = error;
|
|
170
|
+
}
|
|
107
171
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
108
172
|
}
|
|
109
173
|
throw new Error(
|
|
110
|
-
`React Native
|
|
174
|
+
`React Native reload was not verified on port ${port}: no target replacement or responsive new execution context${lastError ? ` (${lastError.message})` : ''}.\nNext: mm-harness status --json`,
|
|
111
175
|
);
|
|
112
176
|
}
|
|
113
177
|
|
|
@@ -155,7 +219,7 @@ const args = parseArgs(process.argv.slice(2));
|
|
|
155
219
|
reload(args.port)
|
|
156
220
|
.then((result) => {
|
|
157
221
|
if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
158
|
-
else console.log(`Reload
|
|
222
|
+
else console.log(`Reload verified through Metro :${result.port} (${result.verification})`);
|
|
159
223
|
})
|
|
160
224
|
.catch((error) => {
|
|
161
225
|
const result = { ok: false, adapter: 'mobile', error: String(error?.message || error) };
|