Skip to content

Commit ffae528

Browse files
committed
fix(android): start a helper instead of writing to a session that already died
1 parent 67238e6 commit ffae528

3 files changed

Lines changed: 52 additions & 5 deletions

File tree

‎packages/platform-android/src/__tests__/snapshot-helper-session-lifecycle.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,33 @@ test('restarts the helper session when capture options change', async () => {
174174
);
175175
});
176176

177+
test('a session whose helper process died is not written to again', async () => {
178+
const calls: string[][] = [];
179+
const spawnArgs: string[][] = [];
180+
const processes: FakeAndroidProcess[] = [];
181+
const provider = createSessionProvider({ calls, spawnArgs, processes });
182+
183+
await captureAndroidSnapshotWithHelperSession({
184+
adb: provider.exec,
185+
adbProvider: provider,
186+
deviceKey: 'android:emulator-5554',
187+
});
188+
processes[0]!.emitExit(137, null);
189+
190+
const restarted = await captureAndroidSnapshotWithHelperSession({
191+
adb: provider.exec,
192+
adbProvider: provider,
193+
deviceKey: 'android:emulator-5554',
194+
});
195+
196+
assert.equal(restarted?.metadata.sessionReused, false);
197+
assert.equal(spawnArgs.length, 2);
198+
assert.equal(
199+
calls.some((args) => args[0] === 'forward' && args[1] === '--remove'),
200+
true,
201+
);
202+
});
203+
177204
test('a quit acknowledged and followed by process exit skips the force-stop round trip', async () => {
178205
const calls: string[][] = [];
179206
const processes: FakeAndroidProcess[] = [];

‎packages/platform-android/src/snapshot-helper-retirement.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ export function observeAndroidSnapshotHelperProcessExit(
238238
};
239239
}
240240

241-
function hasAndroidSnapshotHelperProcessEnded(childProcess: AndroidAdbProcess): boolean {
241+
/** Whether the host side of an instrumentation session is already gone. */
242+
export function hasAndroidSnapshotHelperProcessEnded(childProcess: AndroidAdbProcess): boolean {
242243
return childProcess.exitCode != null || childProcess.signalCode != null;
243244
}
244245

‎packages/platform-android/src/snapshot-helper-session-lifecycle.ts‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
ANDROID_SNAPSHOT_HELPER_DEVICE_RETIREMENT_TIMEOUT_MS,
3636
ANDROID_SNAPSHOT_HELPER_HOST_PROCESS_EXIT_GRACE_MS,
3737
getAndroidSnapshotHelperSessionDeviceKey,
38+
hasAndroidSnapshotHelperProcessEnded,
3839
observeAndroidSnapshotHelperProcessExit,
3940
recoverAndroidSnapshotHelperRetirement,
4041
recordAndroidSnapshotHelperRelease,
@@ -137,11 +138,16 @@ async function resolveAndroidSnapshotHelperSession(params: {
137138
resolved: AndroidSnapshotHelperResolvedCaptureOptions;
138139
}): Promise<AndroidSnapshotHelperSession | undefined> {
139140
const { deviceKey, identity, options, resolved } = params;
140-
let session = sessions.get(deviceKey);
141-
if (session && session.identity !== identity) {
142-
await stopAndroidSnapshotHelperSession(deviceKey);
143-
session = undefined;
141+
const cached = sessions.get(deviceKey);
142+
const reusable = cached !== undefined && isReusableAndroidSnapshotHelperSession(cached, identity);
143+
if (cached && !reusable) {
144+
await stopAndroidSnapshotHelperSession(deviceKey, {
145+
// A process that already exited cannot answer the forwarded port: the forward is all that is
146+
// left of it, so there is nothing to quit gracefully.
147+
force: hasAndroidSnapshotHelperProcessEnded(cached.process),
148+
});
144149
}
150+
let session = reusable ? cached : undefined;
145151
if (!session) {
146152
try {
147153
session = await startAndroidSnapshotHelperSession({
@@ -167,6 +173,19 @@ async function resolveAndroidSnapshotHelperSession(params: {
167173
return session;
168174
}
169175

176+
/**
177+
* A cached session is worth writing to only while it belongs to this helper build and its
178+
* instrumentation process is still running. The helper binds its session socket inside that
179+
* process, so a process that has exited has nobody left to accept on the forwarded port: the
180+
* command would die on a dead socket and fall back, instead of starting a helper that can answer.
181+
*/
182+
function isReusableAndroidSnapshotHelperSession(
183+
session: AndroidSnapshotHelperSession,
184+
identity: string,
185+
): boolean {
186+
return session.identity === identity && !hasAndroidSnapshotHelperProcessEnded(session.process);
187+
}
188+
170189
async function startAndroidSnapshotHelperSession(params: {
171190
deviceKey: string;
172191
identity: string;

0 commit comments

Comments
 (0)