fix: clean up stranded .gsd.lock/ directory to prevent false lock conflicts (#1251)
* fix: clean up stranded .gsd.lock/ directory to prevent false lock conflicts
Three fixes for stranded proper-lockfile lock directories:
1. releaseSessionLock: explicitly removes .gsd.lock/ after releasing
the OS lock and deleting auto.lock
2. acquireSessionLock: when lock acquisition fails, checks if auto.lock
is missing or the owning PID is dead. If so, removes the stale
.gsd.lock/ dir and retries acquisition instead of failing.
3. process.on('exit') handler: registered at lock acquisition time as
a safety net — cleans up .gsd.lock/ on normal process exit if
releaseSessionLock wasn't called.
Fixes #1245
* fix: move gsdDir declaration before try/catch to fix TS2304 scope error
gsdDir was declared inside the try block but referenced in the catch
block's retry logic, causing 'Cannot find name gsdDir' build failures.
This commit is contained in:
parent
4d9aef5705
commit
de1256d352
1 changed files with 53 additions and 4 deletions
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import { existsSync, readFileSync, mkdirSync, unlinkSync, rmSync, statSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { gsdRoot } from "./paths.js";
|
||||
import { atomicWriteSync } from "./atomic-write.js";
|
||||
|
|
@ -92,11 +92,12 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|||
return acquireFallbackLock(basePath, lp, lockData);
|
||||
}
|
||||
|
||||
const gsdDir = gsdRoot(basePath);
|
||||
|
||||
try {
|
||||
// Try to acquire an exclusive OS-level lock on the lock file.
|
||||
// We lock the directory (gsdRoot) since proper-lockfile works best
|
||||
// on directories, and the lock file itself may not exist yet.
|
||||
const gsdDir = gsdRoot(basePath);
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
const release = lockfile.lockSync(gsdDir, {
|
||||
|
|
@ -109,16 +110,53 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|||
_lockedPath = basePath;
|
||||
_lockPid = process.pid;
|
||||
|
||||
// Safety net: clean up lock dir on process exit if _releaseFunction
|
||||
// wasn't called (e.g., normal exit after clean completion) (#1245).
|
||||
const lockDirForCleanup = join(gsdDir + ".lock");
|
||||
process.once("exit", () => {
|
||||
try {
|
||||
if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
|
||||
} catch { /* best-effort */ }
|
||||
try {
|
||||
if (existsSync(lockDirForCleanup)) rmSync(lockDirForCleanup, { recursive: true, force: true });
|
||||
} catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
// Write the informational lock data
|
||||
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
||||
|
||||
return { acquired: true };
|
||||
} catch (err) {
|
||||
// Lock is held by another process
|
||||
// Lock is held by another process — or the .gsd.lock/ directory is stranded.
|
||||
// Check: if auto.lock is gone and no process is alive, the lock dir is stale.
|
||||
const existingData = readExistingLockData(lp);
|
||||
const existingPid = existingData?.pid;
|
||||
|
||||
// If no lock file or no alive process, try to clean up and re-acquire (#1245)
|
||||
if (!existingData || (existingPid && !isPidAlive(existingPid))) {
|
||||
try {
|
||||
const lockDir = join(gsdDir + ".lock");
|
||||
if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
|
||||
if (existsSync(lp)) unlinkSync(lp);
|
||||
|
||||
// Retry acquisition after cleanup
|
||||
const release = lockfile.lockSync(gsdDir, {
|
||||
realpath: false,
|
||||
stale: 300_000,
|
||||
update: 10_000,
|
||||
});
|
||||
_releaseFunction = release;
|
||||
_lockedPath = basePath;
|
||||
_lockPid = process.pid;
|
||||
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
||||
return { acquired: true };
|
||||
} catch {
|
||||
// Retry also failed — fall through to the error path
|
||||
}
|
||||
}
|
||||
|
||||
const reason = existingPid
|
||||
? `Another auto-mode session (PID ${existingPid}) is already running on this project.`
|
||||
? `Another auto-mode session (PID ${existingPid}) appears to be running.\nStop it with \`kill ${existingPid}\` before starting a new session.`
|
||||
: `Another auto-mode session is already running on this project.`;
|
||||
|
||||
return { acquired: false, reason, existingPid };
|
||||
|
|
@ -233,6 +271,17 @@ export function releaseSessionLock(basePath: string): void {
|
|||
// Non-fatal
|
||||
}
|
||||
|
||||
// Remove the proper-lockfile directory (.gsd.lock/) if it exists.
|
||||
// proper-lockfile creates this directory as the OS-level lock mechanism.
|
||||
// If the process exits without calling _releaseFunction (SIGKILL, crash),
|
||||
// this directory is stranded and blocks the next session (#1245).
|
||||
try {
|
||||
const lockDir = join(gsdRoot(basePath) + ".lock");
|
||||
if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
_lockedPath = null;
|
||||
_lockPid = 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue