Files
NianToB/electron/main/process-instance-lock.ts
2026-03-19 10:50:49 +08:00

101 lines
2.4 KiB
TypeScript

import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
export interface ProcessInstanceFileLock {
acquired: boolean;
lockPath: string;
ownerPid?: number;
release: () => void;
}
export interface ProcessInstanceFileLockOptions {
userDataDir: string;
lockName: string;
pid?: number;
isPidAlive?: (pid: number) => boolean;
}
function defaultPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
const errno = (error as NodeJS.ErrnoException).code;
return errno !== 'ESRCH';
}
}
function readLockOwnerPid(lockPath: string): number | undefined {
try {
const raw = readFileSync(lockPath, 'utf8').trim();
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
} catch {
return undefined;
}
}
export function acquireProcessInstanceFileLock(
options: ProcessInstanceFileLockOptions,
): ProcessInstanceFileLock {
const pid = options.pid ?? process.pid;
const isPidAlive = options.isPidAlive ?? defaultPidAlive;
mkdirSync(options.userDataDir, { recursive: true });
const lockPath = join(options.userDataDir, `${options.lockName}.instance.lock`);
let ownerPid: number | undefined;
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
const fd = openSync(lockPath, 'wx');
try {
writeFileSync(fd, String(pid), 'utf8');
} finally {
closeSync(fd);
}
let released = false;
return {
acquired: true,
lockPath,
release: () => {
if (released) return;
released = true;
try {
rmSync(lockPath, { force: true });
} catch {
// best-effort
}
},
};
} catch (error) {
const errno = (error as NodeJS.ErrnoException).code;
if (errno !== 'EEXIST') {
break;
}
ownerPid = readLockOwnerPid(lockPath);
if (ownerPid && !isPidAlive(ownerPid) && existsSync(lockPath)) {
try {
rmSync(lockPath, { force: true });
continue;
} catch {
// If deletion fails, treat as held lock.
}
}
break;
}
}
return {
acquired: false,
lockPath,
ownerPid,
release: () => {
// no-op when lock wasn't acquired
},
};
}