-
Notifications
You must be signed in to change notification settings - Fork 151
feat: prevent multiple watchers on the same dir #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
|
|
||
| const LOCK_DIR = "/tmp"; | ||
| const LOCK_PREFIX = "mgrep-watch-lock-"; | ||
|
|
||
| /** | ||
| * Generates a lock file path based on the directory being watched. | ||
| * Uses a hash of the directory path to create a unique lock file name. | ||
| */ | ||
| function getLockFilePath(watchDir: string): string { | ||
| const normalizedPath = path.resolve(watchDir); | ||
| const hash = Buffer.from(normalizedPath).toString("base64url"); | ||
| return path.join(LOCK_DIR, `${LOCK_PREFIX}${hash}.lock`); | ||
| } | ||
|
|
||
| /** | ||
| * Attempts to acquire a lock for the given directory. | ||
| * Returns true if the lock was acquired, false if another process holds it. | ||
| */ | ||
| export function acquireLock(watchDir: string): boolean { | ||
| const lockFile = getLockFilePath(watchDir); | ||
|
|
||
| try { | ||
| if (fs.existsSync(lockFile)) { | ||
| const content = fs.readFileSync(lockFile, "utf-8"); | ||
| const pid = Number.parseInt(content.trim(), 10); | ||
|
|
||
| if (!Number.isNaN(pid)) { | ||
| try { | ||
| process.kill(pid, 0); | ||
| return false; | ||
| } catch { | ||
| fs.unlinkSync(lockFile); | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Corrupt lock file permanently blocks lock acquisitionWhen a lock file exists but contains invalid content (e.g., empty or non-numeric), |
||
| } | ||
|
|
||
| fs.writeFileSync(lockFile, process.pid.toString(), { flag: "wx" }); | ||
| return true; | ||
| } catch (err) { | ||
| if (err instanceof Error && "code" in err && err.code === "EEXIST") { | ||
| return false; | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Releases the lock for the given directory. | ||
| * Only removes the lock file if this process owns it. | ||
| */ | ||
| export function releaseLock(watchDir: string): void { | ||
| const lockFile = getLockFilePath(watchDir); | ||
|
|
||
| try { | ||
| if (fs.existsSync(lockFile)) { | ||
| const content = fs.readFileSync(lockFile, "utf-8"); | ||
| const pid = Number.parseInt(content.trim(), 10); | ||
|
|
||
| if (pid === process.pid) { | ||
| fs.unlinkSync(lockFile); | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore errors during cleanup | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a lock exists for the given directory. | ||
| * Returns true if the lock is held by a running process. | ||
| */ | ||
| export function isLocked(watchDir: string): boolean { | ||
| const lockFile = getLockFilePath(watchDir); | ||
|
|
||
| try { | ||
| if (!fs.existsSync(lockFile)) { | ||
| return false; | ||
| } | ||
|
|
||
| const content = fs.readFileSync(lockFile, "utf-8"); | ||
| const pid = Number.parseInt(content.trim(), 10); | ||
|
|
||
| if (Number.isNaN(pid)) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| process.kill(pid, 0); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Stale lock cleanup can throw unhandled ENOENT error
When multiple processes detect a stale lock file simultaneously, they may both attempt to call
fs.unlinkSync(lockFile). The first process succeeds, but the second throws anENOENTerror because the file no longer exists. This error is not caught by the inner try-catch (which only handlesprocess.killerrors) and propagates to the outer catch which only handlesEEXIST. The unhandledENOENTgets re-thrown, causing the process to crash instead of gracefully failing to acquire the lock.