daemon/entry.bend

cswap-bend-cli@0.1.0.0 · 179 lines · 32 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/store.bend as Store
6import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
7import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
8import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
9import cswap-bend-cli@0.1.0.0/swlive.bend as L
10import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
11import cswap-bend-cli@0.1.0.0/fmt/out.bend as O
12import cswap-bend-cli@0.1.0.0/daemon/live.bend as Lv
13import cswap-bend-cli@0.1.0.0/profflag.bend as Profflag
14import cswap-bend-cli@0.1.0.0/daemon/auto.bend as Auto
15import cswap-bend-cli@0.1.0.0/policy/types.bend as PT
16import cswap-bend-cli@0.1.0.0/daemon/pidlock.bend as Pid
17import cswap-bend-cli@0.1.0.0/daemon/shadowcli.bend as ShC
18
19# The entry the CLI dispatches `auto` to: flags over settings.json over
20# defaults, the single-decider lock, then one tick (`--once`) or the loop.
21# Exit codes: 0 switched, 1 error, 2 no action (or a usage error), 3 blocked.
22
23def ActM(-A: Type) -> Type:
24  Act.of(A)
25
26def ActM.pure(-A: Type, x: A) -> Act.of(A):
27  Act.pure(A, x)
28
29def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
30  Act.bind(A, B, m, f)
31
32def env_or(r: Result<&1, &1, U32 & String, String>, +d: String) -> String:
33  match r:
34    case Fail{e}:
35      d
36    case Done{s}:
37      s
38
39def oracle_of(r: Result<&1, &1, U32 & String, String>) -> L.Oracle:
40  match r:
41    case Fail{e}:
42      L.OHttp{}
43    case Done{p}:
44      L.OFile{p}
45
46# How the daemon switches: the delay before the post-switch check
47# ($CSWAP_SWITCH_VERIFY_DELAY_MS, default 2000) and where the ownership lookup
48# goes ($CSWAP_SWITCH_ORACLE_FILE answers from a file; else the profile API).
49def switch_opts(d: Result<&1, &1, U32 & String, String>, o: Result<&1, &1, U32 & String, String>) -> L.Opts:
50  L.Opts{Maybe.default(&2, U32, U32.read(env_or(d, "2000")), 2000), oracle_of(o)}
51
52def settings_text(r: Fs.Res(String)) -> String:
53  match r:
54    case Fail{e}:
55      ""
56    case Done{t}:
57      t
58
59def settings_io(+ctx: SBase.Ctx) -> IO(String):
60  IO.bind(Fs.Res(String), String, Fs.read_file(SBase.root_of(ctx) ++ "/settings.json"), r => IO.pure(String, settings_text(r)))
61
62def tick_body(+r: Auto.Run, +once: Bool) -> Act.of(U32):
63  Act.cond(U32, once, Auto.once(r), Auto.forever(r, PT.fresh_mem(), False{}))
64
65# The stream's first line, when the format has one (CSV: the header), then the ticks.
66def body(+r: Auto.Run, +once: Bool) -> Act.of(U32):
67  do ActM<U32>:
68    Act.lift(Unit, IO.write(O.stream_head(Auto.run_fmt(r))))
69    tick_body(r, once)
70
71def flag_dry(+f: Cfg.Flags) -> Bool:
72  match f:
73    case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}:
74      d
75
76def flag_fmt(+f: Cfg.Flags) -> O.Fmt:
77  match f:
78    case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}:
79      O.of_code(j)
80
81def flag_once(+f: Cfg.Flags) -> Bool:
82  match f:
83    case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}:
84      o
85
86def started(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +cfg: Cfg.Cfg, +pid: U32, +ts: String, +lv: Lv.Live) -> Act.of(U32):
87  Pid.with_pidlock(U32, state_dir, body(Auto.Run{ctx, state_dir, cfg, flag_fmt(f), flag_dry(f), opts, ts, version, pid, lv}, flag_once(f)))
88
89def go3(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +text: String, +over: Maybe<&2, String>) -> Act.of(U32):
90  do ActM<U32>:
91    pid : U32 <- Act.lift(U32, Pid.my_pid())
92    t : U32 <- Act.lift(U32, Clock.now_s())
93    started(ctx, f, state_dir, opts, version, Lv.cfg_for(text, f, over), pid, Clock.iso8601_utc(t), Lv.live_for(text, f, over))
94
95# A `--profile` that names nothing is a usage error.
96def go2(+bad: Maybe<&2, String>, +ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +text: String, +over: Maybe<&2, String>) -> Act.of(U32):
97  match bad:
98    case Some{n}:
99      Act.fail(U32, 2, "cswap auto: error: unknown profile '" ++ n ++ "'; `cswap profile list` shows the profiles")
100    case None{}:
101      go3(ctx, f, state_dir, opts, version, text, over)
102
103def go(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>) -> Act.of(U32):
104  do ActM<U32>:
105    +text : String <- Act.lift(String, settings_io(ctx))
106    go2(Lv.unknown(text, over), ctx, f, state_dir, opts, version, text, over)
107
108def with_flags(+ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>, r: Result<&2, &2, String, Cfg.Flags>) -> Act.of(U32):
109  match r:
110    case Fail{msg}:
111      Act.fail(U32, 2, "cswap auto: error: " ++ msg)
112    case Done{f}:
113      go(ctx, f, state_dir, opts, version, over)
114
115# `cswap auto ARGS` (ARGS after the verb). `state_dir` is where the pid file,
116# status file and log live; pass the backup root when there is no better place.
117def plain3(+ok: Bool, +ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>, +rest: List<&2, String>, +err: String) -> Act.of(U32):
118  match ok:
119    case True{}:
120      with_flags(ctx, state_dir, opts, version, over, Cfg.parse_flags(rest))
121    case False{}:
122      Act.fail(U32, 2, "cswap auto: error: " ++ err)
123
124def plain2(+ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +sp: Profflag.Split) -> Act.of(U32):
125  match sp:
126    case Profflag.Split{rest, over, err, pending}:
127      plain3(String.is_empty(err), ctx, state_dir, opts, version, over, rest, err)
128
129def plain(+ctx: SBase.Ctx, +args: List<&2, String>, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32):
130  plain2(ctx, state_dir, opts, version, Profflag.split(args))
131
132def routed(+ctx: SBase.Ctx, +sp: ShC.Split, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32):
133  Act.cond(U32, ShC.sp_shadow(sp), ShC.run(ctx, opts, sp), plain(ctx, ShC.sp_rest(sp), state_dir, opts, version))
134
135def auto(+ctx: SBase.Ctx, +args: List<&2, String>, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32):
136  routed(ctx, ShC.split(Cfg.normalize(args), False{}), state_dir, opts, version)
137
138# ---- as a program entry ----------------------------------------------------------------------------------------
139
140# 2 for a usage error, 1 for everything else.
141def code_for(+code: U32) -> U32:
142  SSeq.sel.u(U32.is_eq(code, 2), 2, 1)
143
144def finish(r: Fs.Res(U32)) -> IO(Unit):
145  match r:
146    case Done{code}:
147      IO.exit(code)
148    case Fail{(code, msg)}:
149      IO.bind(Unit, Unit, IO.print_err(msg), u => IO.exit(code_for(code)))
150
151# The verb as a command result: events already went to stdout as they happened,
152# so only the exit code (and a failure's message) remain.
153def cmd_of(r: Fs.Res(U32)) -> IO(Cmd.Out):
154  match r:
155    case Done{code}:
156      IO.pure(Cmd.Out, Cmd.Out{code, "", ""})
157    case Fail{(code, msg)}:
158      IO.pure(Cmd.Out, Cmd.Out{code_for(code), "", msg ++ "\n"})
159
160def run2(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>, d: Result<&1, &1, U32 & String, String>, o: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)):
161  auto(ctx, args, env_or(st, SBase.root_of(ctx)), switch_opts(d, o), version)
162
163def run1(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>, d: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)):
164  IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_SWITCH_ORACLE_FILE"), o => run2(ctx, args, version, st, d, o))
165
166def run0(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)):
167  IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_SWITCH_VERIFY_DELAY_MS"), d => run1(ctx, args, version, st, d))
168
169# `cswap auto ARGS` reading $HOME, $CLAUDE_CONFIG_DIR and $CSWAP_STATE_DIR itself.
170def run(+args: List<&2, String>, +version: String) -> IO(Fs.Res(U32)):
171  IO.bind(SBase.Ctx, Fs.Res(U32), Store.ctx_env(), ctx => IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_STATE_DIR"), st => run0(ctx, args, version, st)))
172
173# The whole verb as an IO program that exits with the tick's code.
174def auto_main(+args: List<&2, String>, +version: String) -> IO(Unit):
175  IO.bind(Fs.Res(U32), Unit, run(args, version), finish)
176
177# The verb for the CLI dispatch (`cswap auto ARGS`).
178def auto_cmd(+args: List<&2, String>, +version: String) -> IO(Cmd.Out):
179  IO.bind(Fs.Res(U32), Cmd.Out, run(args, version), cmd_of)