domain/architecture/system/deployment/launchd/effects.bend
near-v-framework@0.5.1.0 · 80 lines · 0 definitions
1import Base
2import bend-net-json@0.3.0.0/json.bend as Json
3import near-v-framework@0.5.1.0/domain/architecture/system/effs/io.bend as X
4import near-v-framework@0.5.1.0/domain/architecture/system/plan/type.bend as P
5import near-v-framework@0.5.1.0/domain/architecture/system/deployment/nix_darwin/type.bend as ND
6import near-v-framework@0.5.1.0/domain/architecture/system/deployment/nix_darwin/effects.bend as NDE
7import near-v-framework@0.5.1.0/domain/architecture/system/deployment/launchd/type.bend as L
8import near-v-framework@0.5.1.0/domain/architecture/system/deployment/launchd/ops.bend as LO
9
10# What a launchd node needs, from the Mac as it is: each agent's program installed, its plist as declared and
11# loaded, its port held by nothing else; and each Tailscale serve as declared.
12
13def tailscale() -> String:
14 "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
15
16def program_of(a: L.Agent) -> String:
17 match a:
18 case L.Agent{_, _, program, _, _, _, _, _}:
19 program
20
21# The program: current, to install from its build, or blocked when it neither builds nor is installed.
22def program_step(+entry: String, built: Bool, same: Bool, present: Bool, +from: String, +program: String, +what: String) -> List<&2, P.Step>:
23 Bool.pick(List<&2, P.Step>, String.is_empty(entry),
24 Bool.pick(List<&2, P.Step>, present, [], [P.Blocked{what, program ++ " is not installed"}]),
25 Bool.pick(List<&2, P.Step>, Bool.not(built), [P.Blocked{what, entry ++ " does not build"}],
26 Bool.pick(List<&2, P.Step>, same, [P.Current{"program " ++ program}], [P.Install{from, program}])))
27
28# The agent is current only while its program is as built, its plist as declared, and it is loaded; otherwise it
29# is (re)launched, after its program installs.
30def agent_step(present: Bool, +current: String, loaded: Bool, +home: String, +domain: String, +what: String, a: L.Agent) -> P.Step:
31 match a:
32 case L.Agent{_, +label, +program, _, _, _, _, _}:
33 +want = LO.plist(home, a)
34 Bool.pick(P.Step, present && String.eq(current, want) && loaded, P.Current{"launch agent " ++ label},
35 P.Launch{label, LO.path(home, label), want, domain})
36
37def agent_steps(xs: List<&2, L.Agent>, +root: String, +home: String, +domain: String, +socks: List<&2, String>, +what: String) -> IO(List<&2, P.Step>):
38 match xs:
39 case []:
40 IO.pure(List<&2, P.Step>, [])
41 case +a <> rest:
42 do IO<List<&2, P.Step>>:
43 +label : String = LO.label_of(a)
44 +entry : String = LO.entry_of(a)
45 +from : String = root ++ "/.git/v-launchd/" ++ label
46 _ : Bool <- X.succeeds("/bin/mkdir", ["-p", root ++ "/.git/v-launchd"])
47 built : Bool <- Bool.pick(IO(Bool), String.is_empty(entry), IO.pure(Bool, False{}), X.succeeds(home ++ "/.bend/bin/bend", [root ++ "/" ++ entry, "-o", from]))
48 +same : Bool <- X.succeeds("/usr/bin/cmp", ["-s", from, program_of(a)])
49 +installed : Bool <- X.succeeds("/bin/test", ["-x", program_of(a)])
50 +present : Bool = installed && (String.is_empty(entry) || same)
51 current : String <- X.read(LO.path(home, label))
52 loaded : Bool <- X.succeeds("/bin/launchctl", ["print", domain ++ "/" ++ label])
53 owned : List<&2, String> <- NDE.pids([domain ++ "/" ++ label])
54 +port : String = U32.show(LO.port_of(a))
55 +held : List<&2, String> = NDE.dedupe(NDE.holders(socks, port, owned, "IPNExtension"))
56 more : List<&2, P.Step> <- agent_steps(rest, root, home, domain, socks, what)
57 return List.append(&2, P.Step, program_step(entry, built, same, installed, from, program_of(a), what),
58 agent_step(present, current, loaded, home, domain, what, a) <>
59 List.append(&2, P.Step, Bool.pick(List<&2, P.Step>, U32.is_eq(LO.port_of(a), 0), [], NDE.blocked_by(held, what, "local port " ++ port ++ " is held by ")), more))
60
61def observe(+root: String, +what: String, +ld: L.Launchd, +uid: String, lsof: String) -> IO(List<&2, P.Step>):
62 do IO<List<&2, P.Step>>:
63 +home : String <- X.home()
64 status : String <- X.run(tailscale(), ["serve", "status", "--json"])
65 agents : List<&2, P.Step> <- agent_steps(LO.agents(ld), root, home, "gui/" ++ uid, NDE.sockets(NDE.lines(lsof), "", ""), what)
66 return List.append(&2, P.Step, agents,
67 NDE.serve_steps(LO.serves(ld), NDE.at(NDE.or_null(Json.parse(status)), "Web"), NDE.ports(LO.ports(LO.agents(ld))), tailscale(), what))
68
69def read_or(unread: Bool, +root: String, +what: String, ld: L.Launchd, uid: String, lsof: String) -> IO(List<&2, P.Step>):
70 match unread:
71 case True{}:
72 IO.pure(List<&2, P.Step>, [P.Blocked{what, "cannot read the Mac: `id -u` or `lsof` failed"}])
73 case False{}:
74 observe(root, what, ld, uid, lsof)
75
76def checks(+root: String, +node: String, ld: L.Launchd) -> IO(List<&2, P.Step>):
77 do IO<List<&2, P.Step>>:
78 +uid : String <- X.run("/usr/bin/id", ["-u"])
79 +lsof : String <- X.run("/usr/sbin/lsof", ["+c", "0", "-nP", "-iTCP", "-sTCP:LISTEN", "-F", "pcn"])
80 read_or(String.is_empty(String.trim(uid)) || String.is_empty(lsof), root, "launchd " ++ node, ld, String.trim(uid), lsof)