@djolex999/vir-cli 0.23.1 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,16 @@
1
- import { existsSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  // Serializes distiller-calling commands (vir run, vir reconcile): two
5
5
  // concurrent pipelines double-spend on the same sessions. A pidfile with
6
6
  // stale detection — a lock whose PID is dead (or unreadable) is reclaimed,
7
7
  // so a crashed run never wedges the daemon.
8
+ //
9
+ // Creation is atomic (O_EXCL via flag "wx"): of two processes starting at the
10
+ // same moment, exactly one creates the file. A check-then-write let both
11
+ // "acquire" it. Removing a STALE lock needs its own guard: without one, a
12
+ // process that judged the lock stale can delete the fresh lock another
13
+ // process just created in its place (8 racing processes produced 3 winners).
8
14
  export const LOCK_PATH = join(homedir(), ".vir", "vir.lock");
9
15
  export class LockHeldError extends Error {
10
16
  pid;
@@ -26,21 +32,89 @@ function pidAlive(pid) {
26
32
  return err.code === "EPERM";
27
33
  }
28
34
  }
29
- export function acquireLock(lockPath = LOCK_PATH) {
30
- if (existsSync(lockPath)) {
31
- let holder = NaN;
35
+ // Lock file content, or null once it is gone. An unreadable file reads as ""
36
+ // (garbage), which the caller treats as stale, as before.
37
+ function readLock(lockPath) {
38
+ try {
39
+ return readFileSync(lockPath, "utf8");
40
+ }
41
+ catch (err) {
42
+ return err.code === "ENOENT" ? null : "";
43
+ }
44
+ }
45
+ // A reclaim guard is held for microseconds; one older than this was left by a
46
+ // process that crashed mid-reclaim.
47
+ const GUARD_STALE_MS = 10_000;
48
+ function sleepSync(ms) {
49
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
50
+ }
51
+ // Removes the stale lock if it still holds `stale`, under an exclusive guard
52
+ // so that only one process at a time may delete it. While the stale file
53
+ // exists nobody can create the lock (wx) or release it (not their PID), so
54
+ // the re-read below cannot be invalidated before the unlink. Returns false
55
+ // when another process holds the guard.
56
+ function reclaimStale(lockPath, stale) {
57
+ const guard = `${lockPath}.reclaim`;
58
+ try {
59
+ writeFileSync(guard, String(process.pid), { flag: "wx" });
60
+ }
61
+ catch (err) {
62
+ if (err.code !== "EEXIST")
63
+ throw err;
64
+ try {
65
+ if (Date.now() - statSync(guard).mtimeMs > GUARD_STALE_MS) {
66
+ unlinkSync(guard);
67
+ }
68
+ }
69
+ catch {
70
+ // released meanwhile
71
+ }
72
+ return false;
73
+ }
74
+ try {
75
+ if (readLock(lockPath) === stale) {
76
+ try {
77
+ unlinkSync(lockPath);
78
+ }
79
+ catch {
80
+ // not removable; the caller gives up after its rounds
81
+ }
82
+ }
83
+ }
84
+ finally {
32
85
  try {
33
- holder = Number.parseInt(readFileSync(lockPath, "utf8").trim(), 10);
86
+ unlinkSync(guard);
34
87
  }
35
88
  catch {
36
- // unreadable → treat as stale
89
+ // already gone
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+ export function acquireLock(lockPath = LOCK_PATH) {
95
+ // Each round ends with the lock released, a stale lock removed, or the
96
+ // reclaim guard busy (wait 10ms); the next create decides the winner.
97
+ for (let round = 0; round < 50; round++) {
98
+ try {
99
+ writeFileSync(lockPath, String(process.pid), { flag: "wx" });
100
+ return;
101
+ }
102
+ catch (err) {
103
+ if (err.code !== "EEXIST")
104
+ throw err;
37
105
  }
106
+ const content = readLock(lockPath);
107
+ if (content === null)
108
+ continue; // released between our create and read
109
+ const holder = Number.parseInt(content.trim(), 10);
38
110
  if (Number.isInteger(holder) && holder > 0 && pidAlive(holder)) {
39
111
  throw new LockHeldError(holder);
40
112
  }
41
- // Stale (dead PID or corrupt content) — reclaim.
113
+ if (!reclaimStale(lockPath, content))
114
+ sleepSync(10);
42
115
  }
43
- writeFileSync(lockPath, String(process.pid));
116
+ throw new Error(`could not take the vir lock at ${lockPath}: a stale lock could not be ` +
117
+ `removed — delete it if no vir process is running`);
44
118
  }
45
119
  export function releaseLock(lockPath = LOCK_PATH) {
46
120
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/pipeline/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,sEAAsE;AACtE,yEAAyE;AACzE,2EAA2E;AAC3E,4CAA4C;AAE5C,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;AAE7D,MAAM,OAAO,aAAc,SAAQ,KAAK;IACV;IAA5B,YAA4B,GAAW;QACrC,KAAK,CACH,4BAA4B,GAAG,sCAAsC;YACnE,oCAAoC,SAAS,6BAA6B,CAC7E,CAAC;QAJwB,QAAG,GAAH,GAAG,CAAQ;QAKrC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,CAAC;QACH,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAC;IACzD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,WAAmB,SAAS;IACtD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,IAAI,MAAM,GAAG,GAAG,CAAC;QACjB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,8BAA8B;QAChC,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QACD,iDAAiD;IACnD,CAAC;IACD,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,WAAmB,SAAS;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC1E,uEAAuE;QACvE,sCAAsC;QACtC,IAAI,MAAM,KAAK,OAAO,CAAC,GAAG;YAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/pipeline/lock.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djolex999/vir-cli",
3
- "version": "0.23.1",
3
+ "version": "0.23.2",
4
4
  "description": "An LLM Wiki for Claude Code, in your Obsidian vault.",
5
5
  "author": "Djordje Marković <djordje@growthq.rs>",
6
6
  "license": "MIT",