daemon/sess.bend
cswap-bend-cli@0.1.0.0 · 374 lines · 63 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fs.bend as Fs
4import cswap-bend-cli@0.1.0.0/act.bend as Act
5
6
7
8
9
10
11
12
13def ActM(-A: Type) -> Type:
14 Act.of(A)
15
16def ActM.pure(-A: Type, x: A) -> Act.of(A):
17 Act.pure(A, x)
18
19def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
20 Act.bind(A, B, m, f)
21
22
23
24def keep_char(+n: U32) -> Bool:
25 (U32.is_ge(n, 48) && U32.is_le(n, 57)) || (U32.is_ge(n, 65) && U32.is_le(n, 90)) || (U32.is_ge(n, 97) && U32.is_le(n, 122)) || U32.is_eq(n, 46) || U32.is_eq(n, 95) || U32.is_eq(n, 45)
26
27def pick_char(+k: Bool, +c: Char) -> Char:
28 match k:
29 case True{}:
30 c
31 case False{}:
32 '_'
33
34def slug_char(+c: Char) -> Char:
35 pick_char(keep_char(Char.to_u32(c)), c)
36
37def slug_list(+cs: List<&2, Char>) -> List<&2, Char>:
38 match cs:
39 case Nil{}:
40 []
41 case Con{h, t}:
42 slug_char(h) <> slug_list(t)
43
44
45def slugify(+email: String) -> String:
46 String.from_list(slug_list(String.to_list(email)))
47
48def dir_of(+root: String, +slot: String, +email: String) -> String:
49 root ++ "/sessions/" ++ slot ++ "-" ++ slugify(email) ++ "/sessions"
50
51
52
53def ps_some(+empty: Bool, +t: String) -> Maybe<&2, String>:
54 match empty:
55 case True{}:
56 None{}
57 case False{}:
58 Some{t}
59
60def ps_nonempty(+ok: Bool, +t: String) -> Maybe<&2, String>:
61 match ok:
62 case True{}:
63 ps_some(String.is_empty(t), t)
64 case False{}:
65 None{}
66
67def ps_text(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> Maybe<&2, String>:
68 match r:
69 case Fail{e}:
70 None{}
71 case Done{(st, (out, err))}:
72 ps_nonempty(U32.is_eq(st, 0), String.trim(out))
73
74
75def ps(+cols: String, +pid: U32) -> IO(Maybe<&2, String>):
76 IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, Maybe<&2, String>, Process.run("/usr/bin/env", ["LC_ALL=C", "TZ=UTC", "ps", "-o", cols, "-p", U32.show(pid)], "", 4096, 5000), r => IO.pure(Maybe<&2, String>, ps_text(r)))
77
78
79
80def month_hit(+hit: Bool, +i: U32, +rest: Maybe<&2, U32>) -> Maybe<&2, U32>:
81 match hit:
82 case True{}:
83 Some{i}
84 case False{}:
85 rest
86
87def month_from(+ms: List<&2, String>, +m: String, +i: U32) -> Maybe<&2, U32>:
88 match ms:
89 case Nil{}:
90 None{}
91 case Con{h, t}:
92 month_hit(String.eq(h, m), i, month_from(t, m, (i + 1 : U32)))
93
94def month_idx(+m: String) -> Maybe<&2, U32>:
95 month_from(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], m, 1)
96
97def word_keep(+empty: Bool, +h: String, +rest: List<&2, String>) -> List<&2, String>:
98 match empty:
99 case True{}:
100 rest
101 case False{}:
102 h <> rest
103
104def words(+ws: List<&2, String>) -> List<&2, String>:
105 match ws:
106 case Nil{}:
107 []
108 case Con{h, t}:
109 word_keep(String.is_empty(h), h, words(t))
110
111def sel_u(+c: Bool, +x: U32, +y: U32) -> U32:
112 match c:
113 case True{}:
114 x
115 case False{}:
116 y
117
118def days5(+a: U32, +b: U32, +doy: U32) -> U32:
119 ((((a + b : U32) + doy : U32) - 719468) : U32)
120
121def days4(+era: U32, +yoe: U32, +doy: U32) -> U32:
122 days5((era * 146097 : U32), ((((yoe * 365 : U32) + (yoe / 4 : U32) : U32) - (yoe / 100 : U32)) : U32), doy)
123
124def days3(+era: U32, +yoe: U32, +dm1: U32, +mp: U32) -> U32:
125 days4(era, yoe, ((((153 * mp : U32) + 2 : U32) / 5 : U32) + dm1 : U32))
126
127def days2(+yy: U32, +m: U32, +d: U32) -> U32:
128 days3((yy / 400 : U32), (yy % 400 : U32), (d - 1 : U32), sel_u(U32.is_gt(m, 2), (m - 3 : U32), (m + 9 : U32)))
129
130
131def days_civil(+y: U32, +m: U32, +d: U32) -> U32:
132 days2(sel_u(U32.is_le(m, 2), (y - 1 : U32), y), m, d)
133
134def num(+s: String) -> U32:
135 Maybe.default(&2, U32, U32.read(s), 0)
136
137def clock_secs(+ps: List<&2, String>) -> Maybe<&2, U32>:
138 match ps:
139 case Con{h, Con{m, Con{s, Nil{}}}}:
140 Some{(((num(h) * 3600 : U32) + (num(m) * 60 : U32) : U32) + num(s) : U32)}
141 case _:
142 None{}
143
144def lstart_ok(+ok: Bool, +y: U32, +m: U32, +d: U32, +c: U32) -> Maybe<&2, U32>:
145 match ok:
146 case True{}:
147 Some{((days_civil(y, m, d) * 86400 : U32) + c : U32)}
148 case False{}:
149 None{}
150
151
152
153def lstart_fields(+m: Maybe<&2, U32>, +d: Maybe<&2, U32>, +c: Maybe<&2, U32>, +y: Maybe<&2, U32>) -> Maybe<&2, U32>:
154 match m d c y:
155 case Some{mm} Some{dd} Some{cc} Some{yy}:
156 lstart_ok(U32.is_ge(yy, 1970), yy, mm, dd, cc)
157 case _ _ _ _:
158 None{}
159
160def lstart_dy(+m: Maybe<&2, U32>, +ws: List<&2, String>) -> Maybe<&2, U32>:
161 match ws:
162 case Con{day, Con{clk, Con{yr, Nil{}}}}:
163 lstart_fields(m, U32.read(day), clock_secs(String.split(clk, ':')), U32.read(yr))
164 case _:
165 None{}
166
167def lstart_dmy(+ws: List<&2, String>) -> Maybe<&2, U32>:
168 match ws:
169 case Con{mon, t}:
170 lstart_dy(month_idx(mon), t)
171 case Nil{}:
172 None{}
173
174def lstart_of(+ws: List<&2, String>) -> Maybe<&2, U32>:
175 match ws:
176 case Con{wd, t}:
177 lstart_dmy(t)
178 case Nil{}:
179 None{}
180
181def lstart_secs(+text: String) -> Maybe<&2, U32>:
182 lstart_of(words(String.split(text, ' ')))
183
184def all_digits(+cs: List<&2, Char>) -> Bool:
185 match cs:
186 case Nil{}:
187 True{}
188 case Con{c, t}:
189 Char.is_digit(c) && all_digits(t)
190
191def is_ticks(+t: String) -> Bool:
192 Bool.not(String.is_empty(t)) && all_digits(String.to_list(t))
193
194def nth_hit(+hit: Bool, +h: String, +rest: Maybe<&2, String>) -> Maybe<&2, String>:
195 match hit:
196 case True{}:
197 Some{h}
198 case False{}:
199 rest
200
201
202def nth_word(+ws: List<&2, String>, +n: U32) -> Maybe<&2, String>:
203 match ws:
204 case Nil{}:
205 None{}
206 case Con{h, t}:
207 nth_hit(U32.is_eq(n, 0), h, nth_word(t, (n - 1 : U32)))
208
209def last_part(+ps: List<&2, String>, +d: String) -> String:
210 match ps:
211 case Nil{}:
212 d
213 case Con{h, t}:
214 last_part(t, h)
215
216def stat_ticks(+text: String) -> Maybe<&2, String>:
217 nth_word(words(String.split(last_part(String.split(text, ')'), ""), ' ')), 19)
218
219def ticks_match(+want: String, +m: Maybe<&2, String>) -> Bool:
220 match m:
221 case None{}:
222 True{}
223 case Some{t}:
224 String.eq(t, want)
225
226def stat_text(r: Fs.Res(String)) -> Maybe<&2, String>:
227 match r:
228 case Fail{e}:
229 None{}
230 case Done{t}:
231 stat_ticks(t)
232
233def matches_ticks(+pid: U32, +want: String) -> IO(Bool):
234 IO.bind(Fs.Res(String), Bool, Fs.read_file("/proc/" ++ U32.show(pid) ++ "/stat"), r => IO.pure(Bool, ticks_match(want, stat_text(r))))
235
236def looks_claude(+m: Maybe<&2, String>) -> Bool:
237 match m:
238 case None{}:
239 True{}
240 case Some{t}:
241 String.contains(String.to_lower(t), "claude")
242
243def stranger2(+recent: Bool, +pid: U32) -> IO(Bool):
244 match recent:
245 case True{}:
246 IO.pure(Bool, True{})
247 case False{}:
248 IO.bind(Maybe<&2, String>, Bool, ps("comm=,args=", pid), t => IO.pure(Bool, looks_claude(t)))
249
250
251def stranger(+recorded: U32, +started: Maybe<&2, U32>, +pid: U32) -> IO(Bool):
252 match started:
253 case None{}:
254 IO.pure(Bool, True{})
255 case Some{st}:
256 stranger2(U32.is_le(st, (recorded + 120 : U32)), pid)
257
258def started_of(+m: Maybe<&2, String>) -> Maybe<&2, U32>:
259 match m:
260 case None{}:
261 None{}
262 case Some{t}:
263 lstart_secs(t)
264
265def by_lstart(+pid: U32, +rec: Maybe<&2, U32>) -> IO(Bool):
266 match rec:
267 case None{}:
268 IO.pure(Bool, True{})
269 case Some{recorded}:
270 IO.bind(Maybe<&2, String>, Bool, ps("lstart=", pid), t => stranger(recorded, started_of(t), pid))
271
272def stamped2(+ticks: Bool, +pid: U32, +stamp: String) -> IO(Bool):
273 match ticks:
274 case True{}:
275 matches_ticks(pid, stamp)
276 case False{}:
277 by_lstart(pid, lstart_secs(stamp))
278
279def stamped(+pid: U32, +stamp: String) -> IO(Bool):
280 stamped2(is_ticks(stamp), pid, stamp)
281
282def with_stamp(+pid: U32, +m: Maybe<&2, String>) -> IO(Bool):
283 match m:
284 case None{}:
285 IO.pure(Bool, True{})
286 case Some{stamp}:
287 stamped(pid, stamp)
288
289def alive_then(+up: Bool, +pid: U32, +stamp: Maybe<&2, String>) -> IO(Bool):
290 match up:
291 case True{}:
292 with_stamp(pid, stamp)
293 case False{}:
294 IO.pure(Bool, False{})
295
296def pid_live(+pid: U32, +stamp: Maybe<&2, String>) -> IO(Bool):
297 IO.bind(Maybe<&2, String>, Bool, ps("pid=", pid), t => alive_then(Maybe.is_some(&2, String, t), pid, stamp))
298
299
300
301def stamp_of(+j: Json.Json) -> Maybe<&2, String>:
302 Json.as_string(Maybe.default(&2, Json.Json, Json.get("procStart", j), Json.JNull{}))
303
304def pid_of(+j: Json.Json) -> Maybe<&2, U32>:
305 Json.as_u32(Maybe.default(&2, Json.Json, Json.get("pid", j), Json.JNull{}))
306
307def pid_live_gt1(+ok: Bool, +p: U32, +stamp: Maybe<&2, String>) -> IO(Bool):
308 match ok:
309 case True{}:
310 pid_live(p, stamp)
311 case False{}:
312 IO.pure(Bool, False{})
313
314def record_live2(+pid: Maybe<&2, U32>, +stamp: Maybe<&2, String>) -> IO(Bool):
315 match pid:
316 case None{}:
317 IO.pure(Bool, False{})
318 case Some{p}:
319 pid_live_gt1(U32.is_gt(p, 1), p, stamp)
320
321def record_live(r: Result<&2, &2, String, Json.Json>) -> IO(Bool):
322 match r:
323 case Fail{e}:
324 IO.pure(Bool, False{})
325 case Done{+j}:
326 record_live2(pid_of(j), stamp_of(j))
327
328def file_live(r: Fs.Res(String)) -> IO(Bool):
329 match r:
330 case Fail{e}:
331 IO.pure(Bool, False{})
332 case Done{t}:
333 record_live(Json.parse(t))
334
335
336
337def any_live(+dir: String, +names: List<&2, String>) -> IO(Bool):
338 match names:
339 case Nil{}:
340 IO.pure(Bool, False{})
341 case Con{n, t}:
342 IO.bind(Fs.Res(String), Bool, Fs.read_file(dir ++ "/" ++ n), r => IO.bind(Bool, Bool, file_live(r), hit => IO.bind(Bool, Bool, any_live(dir, t), rest => IO.pure(Bool, hit || rest))))
343
344def rest_live(+hit: Bool, +dir: String, +t: List<&2, String>) -> IO(Bool):
345 match hit:
346 case True{}:
347 IO.pure(Bool, True{})
348 case False{}:
349 any_live(dir, t)
350
351def json_keep(+ok: Bool, +h: String, +rest: List<&2, String>) -> List<&2, String>:
352 match ok:
353 case True{}:
354 h <> rest
355 case False{}:
356 rest
357
358def json_names(+ns: List<&2, String>) -> List<&2, String>:
359 match ns:
360 case Nil{}:
361 []
362 case Con{h, t}:
363 json_keep(String.ends_with(h, ".json"), h, json_names(t))
364
365def scan(+dir: String, r: Fs.Res(List<&2, String>)) -> IO(Bool):
366 match r:
367 case Fail{e}:
368 IO.pure(Bool, False{})
369 case Done{ns}:
370 any_live(dir, json_names(ns))
371
372
373def live(+root: String, +slot: String, +email: String) -> IO(Bool):
374 IO.bind(Fs.Res(List<&2, String>), Bool, Fs.list_dir(dir_of(root, slot, email)), r => scan(dir_of(root, slot, email), r))