Skip to content

Commit e04c97a

Browse files
committed
fix(platform-ios): bound simulator log stream shutdown
1 parent 42bf35f commit e04c97a

2 files changed

Lines changed: 109 additions & 18 deletions

File tree

packages/platform-ios/src/__tests__/app-monitor.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ const createStreamingSubprocess = (
3232
},
3333
} as unknown as Subprocess);
3434

35+
const createHangingSubprocess = (kill: () => void): Subprocess =>
36+
({
37+
nodeChildProcess: Promise.resolve({
38+
kill,
39+
}),
40+
[Symbol.asyncIterator]() {
41+
return {
42+
next: () => new Promise(() => undefined),
43+
};
44+
},
45+
} as unknown as Subprocess);
46+
3547
const artifactRoot = fs.mkdtempSync(
3648
join(tmpdir(), 'rn-harness-ios-monitor-artifacts-')
3749
);
@@ -128,6 +140,38 @@ describe('createIosSimulatorAppMonitor', () => {
128140
);
129141
});
130142

143+
it('does not hang when simulator log stream does not finish after kill', async () => {
144+
vi.useFakeTimers();
145+
146+
const kill = vi.fn();
147+
vi.spyOn(simctl, 'streamLogs').mockReturnValue(createHangingSubprocess(kill));
148+
vi.spyOn(simctl, 'getAppInfo').mockResolvedValue({
149+
Bundle: 'com.harnessplayground',
150+
CFBundleIdentifier: 'com.harnessplayground',
151+
CFBundleExecutable: 'HarnessPlayground',
152+
CFBundleName: 'HarnessPlayground',
153+
CFBundleDisplayName: 'Harness Playground',
154+
Path: '/tmp/HarnessPlayground.app',
155+
});
156+
157+
const monitor = createIosSimulatorAppMonitor({
158+
udid: 'sim-udid',
159+
bundleId: 'com.harnessplayground',
160+
isAppRunning: async () => true,
161+
});
162+
163+
try {
164+
await monitor.start();
165+
const stopPromise = monitor.stop();
166+
167+
await vi.advanceTimersByTimeAsync(1000);
168+
await expect(stopPromise).resolves.toBeUndefined();
169+
expect(kill).toHaveBeenCalled();
170+
} finally {
171+
vi.useRealTimers();
172+
}
173+
});
174+
131175
it('reports simulator lifecycle and crash events through the reporter', async () => {
132176
vi.useFakeTimers();
133177

packages/platform-ios/src/app-monitor.ts

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const POST_LAUNCH_CRASH_SWEEP_DELAY_MS = 1000;
3232
const RECENT_LAUNCH_WINDOW_MS = 5000;
3333
const SUSPICION_WINDOW_MS = 3000;
3434
const CRASH_ARTIFACT_MIN_OCCURRED_AT_TOLERANCE_MS = 2000;
35+
const LOG_STREAM_SHUTDOWN_TIMEOUT_MS = 1000;
3536

3637
type TimedLogLine = {
3738
line: string;
@@ -473,6 +474,58 @@ const waitForPollInterval = async (signal: AbortSignal) => {
473474
});
474475
};
475476

477+
const waitForTaskShutdown = async (
478+
task: Promise<void> | null,
479+
timeoutMs: number,
480+
timeoutMessage: string
481+
) => {
482+
if (!task) {
483+
return;
484+
}
485+
486+
let timeout: ReturnType<typeof setTimeout> | null = null;
487+
const timeoutTask = new Promise<'timeout'>((resolve) => {
488+
timeout = setTimeout(() => resolve('timeout'), timeoutMs);
489+
});
490+
491+
try {
492+
const result = await Promise.race([
493+
task.then(
494+
() => 'settled' as const,
495+
(error) => {
496+
iosAppMonitorLogger.debug('iOS monitor task stopped with error', error);
497+
return 'settled' as const;
498+
}
499+
),
500+
timeoutTask,
501+
]);
502+
503+
if (result === 'timeout') {
504+
iosAppMonitorLogger.warn(timeoutMessage);
505+
}
506+
} finally {
507+
if (timeout) {
508+
clearTimeout(timeout);
509+
}
510+
}
511+
};
512+
513+
const stopLogProcess = async (process: Subprocess | null) => {
514+
if (!process) {
515+
return;
516+
}
517+
518+
try {
519+
const childProcess = await process.nodeChildProcess;
520+
521+
if (!childProcess.killed) {
522+
childProcess.kill();
523+
}
524+
} catch {
525+
// Ignore termination failures for background monitors.
526+
}
527+
};
528+
476529
const createProcessExitDetails = ({
477530
platform,
478531
processName,
@@ -1063,15 +1116,12 @@ export const createIosSimulatorAppMonitor = ({
10631116
logTask = null;
10641117
pollTask = null;
10651118

1066-
if (currentLogProcess) {
1067-
try {
1068-
(await currentLogProcess.nodeChildProcess).kill();
1069-
} catch {
1070-
// Ignore termination failures for background monitors.
1071-
}
1072-
}
1073-
1074-
await currentLogTask;
1119+
await stopLogProcess(currentLogProcess);
1120+
await waitForTaskShutdown(
1121+
currentLogTask,
1122+
LOG_STREAM_SHUTDOWN_TIMEOUT_MS,
1123+
'iOS simulator log stream did not stop within shutdown timeout; continuing'
1124+
);
10751125
await currentPollTask;
10761126
},
10771127
dispose: async () => {
@@ -1089,15 +1139,12 @@ export const createIosSimulatorAppMonitor = ({
10891139
logTask = null;
10901140
pollTask = null;
10911141

1092-
if (currentLogProcess) {
1093-
try {
1094-
(await currentLogProcess.nodeChildProcess).kill();
1095-
} catch {
1096-
// Ignore termination failures for background monitors.
1097-
}
1098-
}
1099-
1100-
await currentLogTask;
1142+
await stopLogProcess(currentLogProcess);
1143+
await waitForTaskShutdown(
1144+
currentLogTask,
1145+
LOG_STREAM_SHUTDOWN_TIMEOUT_MS,
1146+
'iOS simulator log stream did not stop within dispose timeout; continuing'
1147+
);
11011148
await currentPollTask;
11021149
},
11031150
launchRequested: (event) => {

0 commit comments

Comments
 (0)