Skip to content

Commit 0586e88

Browse files
fix(macos): drop this daemon's abandoned whole-Mac claim when it takes an app claim
An app acquisition that found the device key available left this daemon's own abandoned whole-Mac record in place, which kept fencing every other daemon's app opens. Remove the device-key record under the device lock once the app claim is taken, and keep the reboot takeover the device-key settlement reported.
1 parent c269880 commit 0586e88

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎src/daemon/device/__tests__/device-claims.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,3 +967,39 @@ test('an app claim of a dead owner is released by release --stale like any claim
967967
);
968968
assert.equal(fs.existsSync(file), false);
969969
});
970+
971+
test("an app claim taken over this daemon's abandoned whole-Mac claim leaves other daemons' apps free", async () => {
972+
const root = useClaimsRoot();
973+
const whole = await acquireDeviceClaim({
974+
device: mac,
975+
session: 'desktop',
976+
workspace: '/desktop',
977+
stateDir: root,
978+
});
979+
assert.equal(whole.status, 'acquired');
980+
if (whole.status !== 'acquired') return;
981+
assert.equal(await abandonDeviceClaim(whole.ownership), 'abandoned');
982+
983+
const app = await acquireDeviceClaim({
984+
device: mac,
985+
session: 'app',
986+
workspace: '/app',
987+
stateDir: root,
988+
app: { bundleId: 'com.example.one' },
989+
});
990+
assert.equal(app.status, 'acquired');
991+
if (app.status !== 'acquired') return;
992+
assert.deepEqual(app.ownership.app, { bundleId: 'com.example.one' });
993+
assert.equal(fs.existsSync(resolveDeviceClaimPath(canonicalLocalDeviceKey(mac))), false);
994+
995+
const foreignStateDir = path.join(root, 'foreign');
996+
fs.mkdirSync(foreignStateDir);
997+
const foreign = await acquireDeviceClaim({
998+
device: mac,
999+
session: 'foreign',
1000+
workspace: '/foreign',
1001+
stateDir: foreignStateDir,
1002+
app: { bundleId: 'com.example.two' },
1003+
});
1004+
assert.equal(foreign.status, 'acquired');
1005+
});

‎src/daemon/device/device-claims.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,31 @@ export async function acquireDeviceClaim(params: {
107107
if (held) return { status: 'conflict', conflict: held };
108108
}
109109
const appKey = appScopedDeviceKey(deviceKey, app.bundleId);
110-
return await withDeviceClaimLock(
110+
const acquired = await withDeviceClaimLock(
111111
appKey,
112112
async () => await claimHeldDevice({ ...params, deviceKey: appKey, identity, app }),
113113
);
114+
if (acquired.status !== 'acquired') return acquired;
115+
removeSupersededDeviceClaim(deviceKey);
116+
const tookOver =
117+
acquired.tookOver ?? (deviceClaim.status === 'available' ? deviceClaim.tookOver : undefined);
118+
return tookOver ? { ...acquired, tookOver } : acquired;
114119
});
115120
}
116121

122+
/**
123+
* An app acquisition that found the device key available leaves no record there: what remains can
124+
* only be this daemon's own abandoned whole-device claim, which would otherwise keep fencing every
125+
* other daemon's apps. The caller holds the device key's lock.
126+
*/
127+
function removeSupersededDeviceClaim(deviceKey: string): void {
128+
try {
129+
fs.unlinkSync(resolveDeviceClaimPath(deviceKey));
130+
} catch (error) {
131+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
132+
}
133+
}
134+
117135
/**
118136
* #1320 `transient-exclusive`: exclusive ownership for the duration of one
119137
* sessionless device mutation. Identical to a session claim except that a claim

0 commit comments

Comments
 (0)