Skip to content

Commit e3deb89

Browse files
committed
fix: close ios runner host after sessionless commands
1 parent ea323f5 commit e3deb89

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

src/daemon/handlers/__tests__/snapshot-handler.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import os from 'node:os';
44
import path from 'node:path';
55
import { PNG } from '../../../utils/png.ts';
66
import { handleSnapshotCommands } from '../snapshot.ts';
7+
import { withSessionlessRunnerCleanup } from '../snapshot-session.ts';
78
import { captureSnapshot } from '../snapshot-capture.ts';
89
import { SessionStore } from '../../session-store.ts';
910
import type { SessionState } from '../../types.ts';
@@ -29,11 +30,25 @@ vi.mock('../../../platforms/ios/runner-client.ts', async (importOriginal) => {
2930
};
3031
});
3132

33+
vi.mock('../../../platforms/ios/apps.ts', async (importOriginal) => {
34+
const actual = await importOriginal<typeof import('../../../platforms/ios/apps.ts')>();
35+
return {
36+
...actual,
37+
closeIosApp: vi.fn(async () => {}),
38+
};
39+
});
40+
3241
import { dispatchCommand } from '../../../core/dispatch.ts';
33-
import { runIosRunnerCommand } from '../../../platforms/ios/runner-client.ts';
42+
import {
43+
runIosRunnerCommand,
44+
stopIosRunnerSession,
45+
} from '../../../platforms/ios/runner-client.ts';
46+
import { closeIosApp } from '../../../platforms/ios/apps.ts';
3447

3548
const mockDispatch = vi.mocked(dispatchCommand);
3649
const mockRunnerCommand = vi.mocked(runIosRunnerCommand);
50+
const mockStopIosRunnerSession = vi.mocked(stopIosRunnerSession);
51+
const mockCloseIosApp = vi.mocked(closeIosApp);
3752

3853
function makeSessionStore(): SessionStore {
3954
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-snapshot-handler-'));
@@ -80,6 +95,10 @@ beforeEach(() => {
8095
mockDispatch.mockResolvedValue({});
8196
mockRunnerCommand.mockReset();
8297
mockRunnerCommand.mockResolvedValue({});
98+
mockStopIosRunnerSession.mockReset();
99+
mockStopIosRunnerSession.mockResolvedValue();
100+
mockCloseIosApp.mockReset();
101+
mockCloseIosApp.mockResolvedValue();
83102
});
84103

85104
function writeSolidPng(filePath: string, width = 390, height = 844): void {
@@ -1848,3 +1867,31 @@ test('wait sleep bypasses sessionless runner cleanup wrapper', async () => {
18481867
expect(response).toBeTruthy();
18491868
expect(response?.ok).toBe(true);
18501869
});
1870+
1871+
test('sessionless iOS runner cleanup stops the runner host app', async () => {
1872+
const result = await withSessionlessRunnerCleanup(undefined, iosSimulatorDevice, async () => {
1873+
return 'ok';
1874+
});
1875+
1876+
expect(result).toBe('ok');
1877+
expect(mockStopIosRunnerSession).toHaveBeenCalledWith(iosSimulatorDevice.id);
1878+
expect(mockCloseIosApp).toHaveBeenCalledWith(
1879+
iosSimulatorDevice,
1880+
'com.callstack.agentdevice.runner',
1881+
);
1882+
});
1883+
1884+
test('sessionless iOS runner host close is best effort', async () => {
1885+
mockCloseIosApp.mockRejectedValueOnce(new Error('terminate failed'));
1886+
1887+
const result = await withSessionlessRunnerCleanup(undefined, iosSimulatorDevice, async () => {
1888+
return 'ok';
1889+
});
1890+
1891+
expect(result).toBe('ok');
1892+
expect(mockStopIosRunnerSession).toHaveBeenCalledWith(iosSimulatorDevice.id);
1893+
expect(mockCloseIosApp).toHaveBeenCalledWith(
1894+
iosSimulatorDevice,
1895+
'com.callstack.agentdevice.runner',
1896+
);
1897+
});

src/daemon/handlers/snapshot-session.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { resolveTargetDevice } from '../../core/dispatch.ts';
2-
import { stopIosRunnerSession } from '../../platforms/ios/runner-client.ts';
2+
import {
3+
resolveRunnerAppBundleId,
4+
stopIosRunnerSession,
5+
} from '../../platforms/ios/runner-client.ts';
6+
import { closeIosApp } from '../../platforms/ios/apps.ts';
7+
import { emitDiagnostic } from '../../utils/diagnostics.ts';
38
import type { DaemonRequest, SessionState } from '../types.ts';
49
import { ensureDeviceReady } from '../device-ready.ts';
510
import { SessionStore } from '../session-store.ts';
@@ -28,10 +33,26 @@ export async function withSessionlessRunnerCleanup<T>(
2833
// For multi-command flows, keep an active session via `open` so the runner can be reused.
2934
if (shouldCleanupSessionlessIosRunner) {
3035
await stopIosRunnerSession(device.id);
36+
await closeSessionlessIosRunnerHostApp(device);
3137
}
3238
}
3339
}
3440

41+
async function closeSessionlessIosRunnerHostApp(device: SessionState['device']): Promise<void> {
42+
const bundleId = resolveRunnerAppBundleId();
43+
await closeIosApp(device, bundleId).catch((error) => {
44+
emitDiagnostic({
45+
level: 'debug',
46+
phase: 'ios_sessionless_runner_host_close_failed',
47+
data: {
48+
deviceId: device.id,
49+
bundleId,
50+
error: error instanceof Error ? error.message : String(error),
51+
},
52+
});
53+
});
54+
}
55+
3556
export function recordIfSession(
3657
sessionStore: SessionStore,
3758
session: SessionState | undefined,

src/platforms/ios/runner-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ export {
675675
resolveRunnerDestination,
676676
resolveRunnerBuildDestination,
677677
resolveRunnerMaxConcurrentDestinationsFlag,
678+
resolveRunnerAppBundleId,
678679
resolveRunnerSigningBuildSettings,
679680
resolveRunnerBundleBuildSettings,
680681
assertSafeDerivedCleanup,

src/platforms/ios/runner-xctestrun.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ function normalizeBundleId(value: string | undefined): string {
114114
return value?.trim() ?? '';
115115
}
116116

117-
function resolveRunnerAppBundleId(env: NodeJS.ProcessEnv = process.env): string {
117+
export function resolveRunnerAppBundleId(env: NodeJS.ProcessEnv = process.env): string {
118118
const configured =
119119
normalizeBundleId(env.AGENT_DEVICE_IOS_BUNDLE_ID) ||
120120
normalizeBundleId(env.AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID);

0 commit comments

Comments
 (0)