fix: force-clean stale instance lock when Electron lock guarantees exclusivity (#685)
This commit is contained in:
committed by
GitHub
Unverified
parent
aa98e59317
commit
1292e9f120
@@ -87,6 +87,7 @@ if (gotElectronLock) {
|
|||||||
const fileLock = acquireProcessInstanceFileLock({
|
const fileLock = acquireProcessInstanceFileLock({
|
||||||
userDataDir: app.getPath('userData'),
|
userDataDir: app.getPath('userData'),
|
||||||
lockName: 'clawx',
|
lockName: 'clawx',
|
||||||
|
force: true, // Electron lock already guarantees exclusivity; force-clean orphan/recycled-PID locks
|
||||||
});
|
});
|
||||||
gotFileLock = fileLock.acquired;
|
gotFileLock = fileLock.acquired;
|
||||||
releaseProcessInstanceFileLock = fileLock.release;
|
releaseProcessInstanceFileLock = fileLock.release;
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export interface ProcessInstanceFileLockOptions {
|
|||||||
lockName: string;
|
lockName: string;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
isPidAlive?: (pid: number) => boolean;
|
isPidAlive?: (pid: number) => boolean;
|
||||||
|
/**
|
||||||
|
* When true, unconditionally remove any existing lock file before attempting
|
||||||
|
* to acquire. Use this when an external mechanism (e.g. Electron's
|
||||||
|
* `requestSingleInstanceLock`) already guarantees that no other real instance
|
||||||
|
* is running, so a surviving lock file can only be stale (orphan child
|
||||||
|
* process, PID recycling on Windows, etc.).
|
||||||
|
*/
|
||||||
|
force?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultPidAlive(pid: number): boolean {
|
function defaultPidAlive(pid: number): boolean {
|
||||||
@@ -101,6 +109,23 @@ export function acquireProcessInstanceFileLock(
|
|||||||
mkdirSync(options.userDataDir, { recursive: true });
|
mkdirSync(options.userDataDir, { recursive: true });
|
||||||
const lockPath = join(options.userDataDir, `${options.lockName}.instance.lock`);
|
const lockPath = join(options.userDataDir, `${options.lockName}.instance.lock`);
|
||||||
|
|
||||||
|
// When force mode is enabled, unconditionally remove any existing lock file
|
||||||
|
// before attempting acquisition. This is safe because an external mechanism
|
||||||
|
// (Electron's requestSingleInstanceLock) already guarantees exclusivity.
|
||||||
|
if (options.force && existsSync(lockPath)) {
|
||||||
|
const staleOwner = readLockOwner(lockPath);
|
||||||
|
try {
|
||||||
|
rmSync(lockPath, { force: true });
|
||||||
|
} catch {
|
||||||
|
// best-effort; fall through to normal acquisition
|
||||||
|
}
|
||||||
|
if (staleOwner.kind !== 'unknown') {
|
||||||
|
console.info(
|
||||||
|
`[ClawX] Force-cleaned stale instance lock (pid=${staleOwner.pid}, format=${staleOwner.kind})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ownerPid: number | undefined;
|
let ownerPid: number | undefined;
|
||||||
let ownerFormat: ProcessInstanceFileLock['ownerFormat'] = 'unknown';
|
let ownerFormat: ProcessInstanceFileLock['ownerFormat'] = 'unknown';
|
||||||
|
|
||||||
|
|||||||
@@ -154,4 +154,40 @@ describe('process instance file lock', () => {
|
|||||||
expect(lock.ownerFormat).toBe('unknown');
|
expect(lock.ownerFormat).toBe('unknown');
|
||||||
expect(readFileSync(lockPath, 'utf8')).toContain('future-lock-schema');
|
expect(readFileSync(lockPath, 'utf8')).toContain('future-lock-schema');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('force: true acquires lock even when existing owner pid is alive', () => {
|
||||||
|
const userDataDir = createTempDir();
|
||||||
|
const lockPath = join(userDataDir, 'clawx.instance.lock');
|
||||||
|
// Simulate a lock held by a live process (e.g. orphan Python process after update)
|
||||||
|
writeFileSync(lockPath, '14736', 'utf8');
|
||||||
|
|
||||||
|
const lock = acquireProcessInstanceFileLock({
|
||||||
|
userDataDir,
|
||||||
|
lockName: 'clawx',
|
||||||
|
pid: 5555,
|
||||||
|
isPidAlive: () => true, // owner appears alive (PID recycled on Windows)
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lock.acquired).toBe(true);
|
||||||
|
expect(readFileSync(lockPath, 'utf8')).toBe('5555');
|
||||||
|
lock.release();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('force: true acquires lock when lock file has malformed content', () => {
|
||||||
|
const userDataDir = createTempDir();
|
||||||
|
const lockPath = join(userDataDir, 'clawx.instance.lock');
|
||||||
|
writeFileSync(lockPath, 'garbage-content', 'utf8');
|
||||||
|
|
||||||
|
const lock = acquireProcessInstanceFileLock({
|
||||||
|
userDataDir,
|
||||||
|
lockName: 'clawx',
|
||||||
|
pid: 7777,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lock.acquired).toBe(true);
|
||||||
|
expect(readFileSync(lockPath, 'utf8')).toBe('7777');
|
||||||
|
lock.release();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user