dirlock.bend

cswap-bend-cli@0.1.0.0 · 101 lines · 18 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/fs.bend as Fs
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/clock.bend as Clock
5import cswap-bend-cli@0.1.0.0/fs/write.bend as FsW
6
7# Directory locks, the npm proper-lockfile protocol Claude Code uses for its
8# credential and config files (contract 1.8): `mkdir` takes the lock, `rmdir`
9# releases it, and a lock whose mtime is older than `staleness` seconds is
10# taken over. cswap holds these for well under a second (and never across the
11# post-switch verification delay), so the reference's 3 s touch thread has
12# nothing to keep alive and is not run.
13
14def ActM(-A: Type) -> Type:
15  Act.of(A)
16
17def ActM.pure(-A: Type, x: A) -> Act.of(A):
18  Act.pure(A, x)
19
20def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
21  Act.bind(A, B, m, f)
22
23# Modification time of a path in whole seconds.
24def Dirlock.mtime(+path: String) -> IO(Result<&1, &1, U32 & String, U32>):
25  import "./effs/mtime.c"
26  import "./effs/mtime.js"
27
28def poll_ms() -> U32:
29  250
30
31def mkdir_ok(r: Fs.Res(String)) -> Fs.Res(Bool):
32  match r:
33    case Fail{e}:
34      Done{False{}}
35    case Done{out}:
36      Done{True{}}
37
38# One attempt: mkdir without -p, so it fails while the lock is held.
39def take_once(+dir: String) -> Act.of(Bool):
40  IO.bind(Fs.Res(String), Fs.Res(Bool), Fs.tool("/bin/mkdir", [dir]), r => IO.pure(Fs.Res(Bool), mkdir_ok(r)))
41
42def stale_of(m: Result<&1, &1, U32 & String, U32>, +now: U32, +staleness: U32) -> Bool:
43  match m:
44    case Fail{e}:
45      False{}
46    case Done{t}:
47      U32.is_gt(now, (t + staleness : U32))
48
49def is_stale(+dir: String, +staleness: U32) -> Act.of(Bool):
50  do ActM<Bool>:
51    now : U32 <- Act.lift(U32, Clock.now_s())
52    m : Result<&1, &1, U32 & String, U32> <- Act.lift(Result<&1, &1, U32 & String, U32>, Dirlock.mtime(dir))
53    return stale_of(m, now, staleness)
54
55def reclaim(+stale: Bool, +dir: String) -> Act.of(Unit):
56  Act.cond(Unit, stale, Fs.tool_unit("/bin/rmdir", [dir]), Act.pure(Unit, Unit{}))
57
58def basename(+p: String) -> String:
59  Maybe.default(&2, String, List.last(&2, String, String.split(p, '/')), p)
60
61def busy_msg(+dir: String) -> String:
62  "Could not acquire " ++ basename(dir) ++ " — Claude Code appears to be refreshing credentials. Retry in a few seconds."
63
64# Take over a stale lock, wait a beat, try again.
65def retake(+dir: String, +staleness: U32) -> Act.of(Bool):
66  do ActM<Bool>:
67    st : Bool <- is_stale(dir, staleness)
68    reclaim(st, dir)
69    Act.lift(Unit, IO.sleep(poll_ms()))
70    take_once(dir)
71
72# Wait for the lock `fuel` more times, taking over a stale one.
73def wait(+fuel: Nat, +dir: String, +staleness: U32, +got: Bool) -> Act.of(Unit):
74  match fuel got:
75    case _ True{}:
76      Act.pure(Unit, Unit{})
77    case 0n False{}:
78      Act.fail(Unit, 4, busy_msg(dir))
79    case 1n++p False{}:
80      do ActM<Unit>:
81        again : Bool <- retake(dir, staleness)
82        wait(p, dir, staleness, again)
83
84def tries(+timeout_ms: U32) -> Nat:
85  U32.to_nat((timeout_ms / poll_ms() : U32))
86
87# Take the lock at `dir` (its parent is created), waiting up to `timeout_ms`.
88def take(+dir: String, +timeout_ms: U32, +staleness: U32) -> Act.of(Unit):
89  do ActM<Unit>:
90    FsW.mkdir_p(Fs.dirname(dir))
91    first : Bool <- take_once(dir)
92    wait(tries(timeout_ms), dir, staleness, first)
93
94def release(+dir: String) -> Act.of(Unit):
95  IO.bind(Fs.Res(Unit), Fs.Res(Unit), Fs.tool_unit("/bin/rmdir", [dir]), r => IO.pure(Fs.Res(Unit), Done{Unit{}}))
96
97# Hold the directory lock around `act`, released whatever `act` answers.
98def with_dirlock(-A: Type, +dir: String, +timeout_ms: U32, +staleness: U32, act: Act.of(A)) -> Act.of(A):
99  do ActM<A>:
100    take(dir, timeout_ms, staleness)
101    Act.finally(A, act, release(dir))