1 import Base 2 import bend-net-json@0.3.0.0/json.bend as Json 3 import ../../effs/io.bend as X 4 import ../../plan/type.bend as P 5 import ../nix_darwin/type.bend as ND 6 import ../nix_darwin/effects.bend as NDE 7 import ./type.bend as L 8 import ./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 14 def tailscale() -> String: 15 "/Applications/Tailscale.app/Contents/MacOS/Tailscale" 16 17 def last_segment(+p: String) -> String: 18 Maybe.default(&2, String, List.last(&2, String, String.split(p, '/')), p) 19 20 # ---- programs ---- 21 def 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 build is current while it was built from the folder's committed tree. 26 def next_step(+tree: String, +installed: String, +folder: String, +path: String, +what: String) -> 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 P.BuildNext{folder, tree, path})) 30 31 def 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: 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: 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: 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) 51 52 def program_steps(gs: List<&2, L.Program>, +root: String, +home: String, +what: String) -> IO(List<&2, P.Step>): 53 match gs: 54 case []: 55 IO.pure(List<&2, P.Step>, []) 56 case g <> rest: 57 do IO>: 58 s : P.Step <- program_step(root, home, what, g) 59 more : List<&2, P.Step> <- program_steps(rest, root, home, what) 60 return s <> more 61 62 def installs(s: P.Step) -> Bool: 63 match s: 64 case P.Install{_, _}: 65 True{} 66 case P.BuildNext{_, _, _}: 67 True{} 68 case _: 69 False{} 70 71 def any_installs(ss: List<&2, P.Step>) -> Bool: 72 match ss: 73 case []: 74 False{} 75 case s <> rest: 76 installs(s) || any_installs(rest) 77 78 # ---- agents ---- 79 def agent_step(changed: Bool, +current: String, loaded: Bool, +home: String, +domain: String, +a: L.Agent) -> P.Step: 80 +label = LO.label_of(a) 81 +want = LO.plist(home, a) 82 Bool.pick(P.Step, Bool.not(changed) && String.eq(current, want) && loaded, P.Current{"launch agent " ++ label}, 83 P.Launch{label, LO.path(home, label), want, domain}) 84 85 def agent_steps(xs: List<&2, L.Agent>, +changed: Bool, +home: String, +domain: String, +socks: List<&2, String>, +what: String) -> IO(List<&2, P.Step>): 86 match xs: 87 case []: 88 IO.pure(List<&2, P.Step>, []) 89 case +a <> rest: 90 do IO>: 91 +label : String = LO.label_of(a) 92 current : String <- X.read(LO.path(home, label)) 93 loaded : Bool <- X.succeeds("/bin/launchctl", ["print", domain ++ "/" ++ label]) 94 owned : List<&2, String> <- NDE.pids([domain ++ "/" ++ label]) 95 +port : String = U32.show(LO.port_of(a)) 96 +held : List<&2, String> = NDE.dedupe(NDE.holders(socks, port, owned, "IPNExtension")) 97 more : List<&2, P.Step> <- agent_steps(rest, changed, home, domain, socks, what) 98 return agent_step(changed, current, loaded, home, domain, a) <> 99 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) 100 101 def observe(+root: String, +what: String, +ld: L.Launchd, +uid: String, lsof: String) -> IO(List<&2, P.Step>): 102 do IO>: 103 +home : String <- X.home() 104 status : String <- X.run(tailscale(), ["serve", "status", "--json"]) 105 +programs : List<&2, P.Step> <- program_steps(LO.programs(ld), root, home, what) 106 agents : List<&2, P.Step> <- agent_steps(LO.agents(ld), any_installs(programs), home, "gui/" ++ uid, NDE.sockets(NDE.lines(lsof), "", ""), what) 107 return List.append(&2, P.Step, programs, List.append(&2, P.Step, agents, 108 NDE.serve_steps(LO.serves(ld), NDE.at(NDE.or_null(Json.parse(status)), "Web"), NDE.ports(LO.ports(LO.agents(ld))), tailscale(), what))) 109 110 def read_or(unread: Bool, +root: String, +what: String, ld: L.Launchd, uid: String, lsof: String) -> IO(List<&2, P.Step>): 111 match unread: 112 case True{}: 113 IO.pure(List<&2, P.Step>, [P.Blocked{what, "cannot read the Mac: `id -u` or `lsof` failed"}]) 114 case False{}: 115 observe(root, what, ld, uid, lsof) 116 117 def checks(+root: String, +node: String, ld: L.Launchd) -> IO(List<&2, P.Step>): 118 do IO>: 119 +uid : String <- X.run("/usr/bin/id", ["-u"]) 120 +lsof : String <- X.run("/usr/sbin/lsof", ["+c", "0", "-nP", "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]) 121 read_or(String.is_empty(String.trim(uid)) || String.is_empty(lsof), root, "launchd " ++ node, ld, String.trim(uid), lsof)