domain/architecture/system/deployment/launchd/effects.bend

near-v-framework@0.7.0.0 · 128 lines · 0 definitions

1import Base
2import bend-net-json@0.3.0.0/json.bend as Json
3import near-v-framework@0.7.0.0/domain/architecture/system/effs/io.bend as X
4import near-v-framework@0.7.0.0/domain/architecture/system/plan/type.bend as P
5import near-v-framework@0.7.0.0/domain/architecture/system/deployment/nix_darwin/type.bend as ND
6import near-v-framework@0.7.0.0/domain/architecture/system/deployment/nix_darwin/effects.bend as NDE
7import near-v-framework@0.7.0.0/domain/architecture/system/deployment/launchd/type.bend as L
8import near-v-framework@0.7.0.0/domain/architecture/system/deployment/launchd/ops.bend as LO
9
10# What a launchd node needs, from the Mac as it is: each program as its build says, each agent's plist as
11# declared and loaded (and relaunched when a program is installed), its port held by nothing else; and each
12# Tailscale serve as declared.
13
14def tailscale() -> String:
15  "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
16
17def last_segment(+p: String) -> String:
18  Maybe.default(&2, String, List.last(&2, String, String.split(p, '/')), p)
19
20# ---- programs ----
21def bend_step(built: Bool, same: Bool, +entry: String, +from: String, +path: String, +what: String) -> P.Step:
22  Bool.pick(P.Step, Bool.not(built), P.Blocked{what, entry ++ " does not build"},
23    Bool.pick(P.Step, same, P.Current{"program " ++ path}, P.Install{from, path}))
24
25# A Next.js or Node build is current while it was built from the folder's committed tree.
26def next_step(+tree: String, +installed: String, +folder: String, +path: String, +what: String, next: Bool) -> P.Step:
27  Bool.pick(P.Step, String.is_empty(tree), P.Blocked{what, folder ++ " is not in the repository's commit"},
28    Bool.pick(P.Step, String.eq(tree, installed), P.Current{"program " ++ path ++ " (" ++ folder ++ " @ " ++ String.take(tree, 7n) ++ ")"},
29      Bool.pick(P.Step, next, P.BuildNext{folder, tree, path}, P.BuildNode{folder, tree, path})))
30
31def program_step(+root: String, +home: String, +what: String, g: L.Program) -> IO(P.Step):
32  match g:
33    case L.Program{+path, b}:
34      match b:
35        case L.Installed{}:
36          do IO<P.Step>:
37            there : Bool <- X.succeeds("/bin/test", ["-e", path])
38            return Bool.pick(P.Step, there, P.Current{"program " ++ path}, P.Blocked{what, path ++ " is not installed"})
39        case L.Bend{+entry}:
40          do IO<P.Step>:
41            +from : String = root ++ "/.git/v-launchd/" ++ last_segment(path)
42            _ : Bool <- X.succeeds("/bin/mkdir", ["-p", root ++ "/.git/v-launchd"])
43            built : Bool <- X.succeeds(home ++ "/.bend/bin/bend", [root ++ "/" ++ entry, "-o", from])
44            same : Bool <- X.succeeds("/usr/bin/cmp", ["-s", from, path])
45            return bend_step(built, same, entry, from, path, what)
46        case L.Next{+folder}:
47          do IO<P.Step>:
48            tree : String <- X.run("git", ["-C", root, "rev-parse", "HEAD:" ++ folder])
49            installed : String <- X.read(path ++ "/.v-commit")
50            return next_step(String.trim(tree), String.trim(installed), folder, path, what, True{})
51        case L.Node{+folder}:
52          do IO<P.Step>:
53            tree : String <- X.run("git", ["-C", root, "rev-parse", "HEAD:" ++ folder])
54            installed : String <- X.read(path ++ "/.v-commit")
55            return next_step(String.trim(tree), String.trim(installed), folder, path, what, False{})
56
57def program_steps(gs: List<&2, L.Program>, +root: String, +home: String, +what: String) -> IO(List<&2, P.Step>):
58  match gs:
59    case []:
60      IO.pure(List<&2, P.Step>, [])
61    case g <> rest:
62      do IO<List<&2, P.Step>>:
63        s : P.Step <- program_step(root, home, what, g)
64        more : List<&2, P.Step> <- program_steps(rest, root, home, what)
65        return s <> more
66
67def installs(s: P.Step) -> Bool:
68  match s:
69    case P.Install{_, _}:
70      True{}
71    case P.BuildNext{_, _, _}:
72      True{}
73    case P.BuildNode{_, _, _}:
74      True{}
75    case _:
76      False{}
77
78def any_installs(ss: List<&2, P.Step>) -> Bool:
79  match ss:
80    case []:
81      False{}
82    case s <> rest:
83      installs(s) || any_installs(rest)
84
85# ---- agents ----
86def agent_step(changed: Bool, +current: String, loaded: Bool, +home: String, +domain: String, +a: L.Agent) -> P.Step:
87  +label = LO.label_of(a)
88  +want = LO.plist(home, a)
89  Bool.pick(P.Step, Bool.not(changed) && String.eq(current, want) && loaded, P.Current{"launch agent " ++ label},
90    P.Launch{label, LO.path(home, label), want, domain})
91
92def agent_steps(xs: List<&2, L.Agent>, +changed: Bool, +home: String, +domain: String, +socks: List<&2, String>, +what: String) -> IO(List<&2, P.Step>):
93  match xs:
94    case []:
95      IO.pure(List<&2, P.Step>, [])
96    case +a <> rest:
97      do IO<List<&2, P.Step>>:
98        +label : String = LO.label_of(a)
99        current : String <- X.read(LO.path(home, label))
100        loaded : Bool <- X.succeeds("/bin/launchctl", ["print", domain ++ "/" ++ label])
101        owned : List<&2, String> <- NDE.pids([domain ++ "/" ++ label])
102        +port : String = U32.show(LO.port_of(a))
103        +held : List<&2, String> = NDE.dedupe(NDE.holders(socks, port, owned, "IPNExtension"))
104        more : List<&2, P.Step> <- agent_steps(rest, changed, home, domain, socks, what)
105        return agent_step(changed, current, loaded, home, domain, a) <>
106          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)
107
108def observe(+root: String, +what: String, +ld: L.Launchd, +uid: String, lsof: String) -> IO(List<&2, P.Step>):
109  do IO<List<&2, P.Step>>:
110    +home : String <- X.home()
111    status : String <- X.run(tailscale(), ["serve", "status", "--json"])
112    +programs : List<&2, P.Step> <- program_steps(LO.programs(ld), root, home, what)
113    agents : List<&2, P.Step> <- agent_steps(LO.agents(ld), any_installs(programs), home, "gui/" ++ uid, NDE.sockets(NDE.lines(lsof), "", ""), what)
114    return List.append(&2, P.Step, programs, List.append(&2, P.Step, agents,
115      NDE.serve_steps(LO.serves(ld), NDE.at(NDE.or_null(Json.parse(status)), "Web"), NDE.ports(LO.ports(LO.agents(ld))), tailscale(), what)))
116
117def read_or(unread: Bool, +root: String, +what: String, ld: L.Launchd, uid: String, lsof: String) -> IO(List<&2, P.Step>):
118  match unread:
119    case True{}:
120      IO.pure(List<&2, P.Step>, [P.Blocked{what, "cannot read the Mac: `id -u` or `lsof` failed"}])
121    case False{}:
122      observe(root, what, ld, uid, lsof)
123
124def checks(+root: String, +node: String, ld: L.Launchd) -> IO(List<&2, P.Step>):
125  do IO<List<&2, P.Step>>:
126    +uid : String <- X.run("/usr/bin/id", ["-u"])
127    +lsof : String <- X.run("/usr/sbin/lsof", ["+c", "0", "-nP", "-iTCP", "-sTCP:LISTEN", "-F", "pcn"])
128    read_or(String.is_empty(String.trim(uid)) || String.is_empty(lsof), root, "launchd " ++ node, ld, String.trim(uid), lsof)