Skip to content

Commit 47fadf6

Browse files
authored
fix: clean daemon-owned ios runner leases (#829)
1 parent cb28f68 commit 47fadf6

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

scripts/clean-daemon.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
isAgentDeviceDaemonProcess,
77
stopProcessForTakeover,
88
} from '../src/utils/process-identity.ts';
9+
import { cleanupRunnerLeasesForOwner } from '../src/platforms/ios/runner-lease.ts';
10+
import { runnerLeaseCleanupAdapter } from '../src/platforms/ios/runner-disposal.ts';
911

1012
const DAEMON_TERM_TIMEOUT_MS = 15_000;
1113
const DAEMON_KILL_TIMEOUT_MS = 2_000;
@@ -27,6 +29,10 @@ if (daemonPid !== null) {
2729
killTimeoutMs: DAEMON_KILL_TIMEOUT_MS,
2830
expectedStartTime: info?.processStartTime,
2931
});
32+
await cleanupRunnerLeasesForOwner(
33+
{ pid: daemonPid, startTime: info?.processStartTime },
34+
runnerLeaseCleanupAdapter,
35+
);
3036
}
3137

3238
removeIfPresent(paths.infoPath);

src/platforms/ios/__tests__/runner-session.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import {
108108
validateRunnerDevice,
109109
} from '../runner-session.ts';
110110
import {
111+
cleanupRunnerLeasesForOwner,
111112
RUNNER_OWNER_START_TIME,
112113
RUNNER_OWNER_TOKEN,
113114
writeRunnerLease,
@@ -710,6 +711,40 @@ test('runner session startup reclaims dead foreign runner lease before launching
710711
);
711712
});
712713

714+
test('runner lease cleanup reclaims only leases owned by the stopped daemon', async () => {
715+
const ownerPid = 999_999_991;
716+
const ownerStartTime = 'Fri Jun 19 12:00:00 2026';
717+
const owned = makeRunnerLease({
718+
deviceId: 'runner-session-clean-owned-lease',
719+
ownerPid,
720+
ownerStartTime,
721+
ownerToken: 'owner-clean-owned',
722+
});
723+
const foreign = makeRunnerLease({
724+
deviceId: 'runner-session-clean-foreign-lease',
725+
ownerPid,
726+
ownerStartTime: 'Fri Jun 19 12:01:00 2026',
727+
ownerToken: 'owner-clean-foreign',
728+
});
729+
writeRunnerLease(owned);
730+
writeRunnerLease(foreign);
731+
732+
await cleanupRunnerLeasesForOwner(
733+
{ pid: ownerPid, startTime: ownerStartTime },
734+
{
735+
cleanupRunnerProcessTree: async () => {},
736+
cleanupRunnerXcodebuildProcesses: async () => {},
737+
cleanupTempFile: mockCleanupTempFile,
738+
},
739+
);
740+
741+
const leaseDir = process.env.AGENT_DEVICE_IOS_RUNNER_LEASE_DIR;
742+
assert.ok(leaseDir);
743+
assert.equal(fs.existsSync(path.join(leaseDir, `${owned.deviceId}.json`)), false);
744+
assert.equal(fs.existsSync(path.join(leaseDir, `${foreign.deviceId}.json`)), true);
745+
assert.deepEqual(mockCleanupTempFile.mock.calls, [[owned.xctestrunPath], [owned.jsonPath]]);
746+
});
747+
713748
test('runner session restarts alive runner when expected xctestrun artifact changes', async () => {
714749
const device = { ...IOS_SIMULATOR, id: 'runner-session-stale-artifact-sim' };
715750

src/platforms/ios/runner-lease.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ export async function cleanupOwnedRunnerLease(
141141
}
142142
}
143143

144+
export async function cleanupRunnerLeasesForOwner(
145+
owner: { pid: number; startTime?: string | null },
146+
cleanup: RunnerLeaseCleanupAdapter,
147+
): Promise<void> {
148+
if (!Number.isInteger(owner.pid) || owner.pid <= 0) return;
149+
const leases = listRunnerLeasesForOwner(owner);
150+
await Promise.all(
151+
leases.map(async (lease) => {
152+
await cleanupLeasedRunnerProcesses(lease, 'owned', cleanup);
153+
}),
154+
);
155+
}
156+
144157
export function releaseRunnerLease(lease: RunnerLease | undefined): void {
145158
if (!lease) return;
146159
removeRunnerLease({
@@ -182,6 +195,39 @@ function resolveRunnerLeaseRoot(): string {
182195
return path.join(os.homedir(), '.agent-device', 'ios-runner', 'leases');
183196
}
184197

198+
function listRunnerLeasesForOwner(owner: {
199+
pid: number;
200+
startTime?: string | null;
201+
}): RunnerLease[] {
202+
let entries: fs.Dirent[];
203+
const root = resolveRunnerLeaseRoot();
204+
try {
205+
entries = fs.readdirSync(root, { withFileTypes: true });
206+
} catch {
207+
return [];
208+
}
209+
const leases: RunnerLease[] = [];
210+
for (const entry of entries) {
211+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
212+
const lease = readRunnerLeaseFile(path.join(root, entry.name));
213+
if (!lease) continue;
214+
if (lease.ownerPid !== owner.pid) continue;
215+
if (owner.startTime !== undefined && lease.ownerStartTime !== owner.startTime) continue;
216+
leases.push(lease);
217+
}
218+
return leases;
219+
}
220+
221+
function readRunnerLeaseFile(filePath: string): RunnerLease | null {
222+
try {
223+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<RunnerLease>;
224+
const deviceId = readNonEmptyString(parsed.deviceId);
225+
return deviceId ? normalizeRunnerLease(parsed, deviceId) : null;
226+
} catch {
227+
return null;
228+
}
229+
}
230+
185231
function normalizeRunnerLease(value: unknown, deviceId: string): RunnerLease | null {
186232
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
187233
const raw = value as Partial<RunnerLease>;

0 commit comments

Comments
 (0)