@vibelord/lanes 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/lanes/README.md +4 -3
  3. package/payload/lanes/VERSION +1 -1
  4. package/payload/lanes/bin/lanes +301 -45
  5. package/payload/lanes/container/lane +42 -1
  6. package/payload/lanes/container/lane-mcp +65 -6
  7. package/payload/lanes/container/lane_core.py +178 -0
  8. package/payload/lanes/docs/ARCHITECTURE.md +192 -20
  9. package/payload/lanes/docs/DESIGN.md +6 -3
  10. package/payload/lanes/docs/INSTALL.md +25 -8
  11. package/payload/lanes/docs/RUNBOOK.md +369 -8
  12. package/payload/lanes/docs/USERGUIDE.md +201 -25
  13. package/payload/lanes/holders.lock +5 -0
  14. package/payload/lanes/install/enable-docs.sh +78 -0
  15. package/payload/lanes/install/host-install.sh +39 -5
  16. package/payload/lanes/libexec/docs_store.py +995 -0
  17. package/payload/lanes/libexec/docsd.py +2010 -0
  18. package/payload/lanes/libexec/mdlite.py +575 -0
  19. package/payload/lanes/libexec/members.py +5 -0
  20. package/payload/lanes/libexec/notify.py +65 -5
  21. package/payload/lanes/libexec/sidebar.py +106 -13
  22. package/payload/lanes/libexec/statusd.py +97 -5
  23. package/payload/lanes/libexec/tokens.py +31 -9
  24. package/payload/lanes/libexec/update.py +94 -22
  25. package/payload/lanes/libexec/uploads.py +538 -0
  26. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  27. package/payload/lanes/web/Caddyfile +24 -1
  28. package/payload/lanes/web/README.md +86 -8
  29. package/payload/lanes/web/browser.js +2 -4
  30. package/payload/lanes/web/docs.css +52 -0
  31. package/payload/lanes/web/docs.js +36 -0
  32. package/payload/lanes/web/mobile.js +3 -2
  33. package/payload/lanes/web/upload.css +17 -0
  34. package/payload/lanes/web/upload.js +84 -0
  35. package/payload/manifest.json +4 -3
@@ -14,19 +14,26 @@ catalog — a failure there leaves the installed release running as it was. comm
14
14
  installed. Its steps run in this order, each whatever the ones before it returned, each bounded by STEP_TIMEOUT seconds
15
15
  (its whole process group is stopped when it does not finish):
16
16
  systemctl --user daemon-reload · lanes sync · systemctl --user restart lanes-statusd.service ·
17
- systemctl --user try-restart <unit> for each optional service in TRY_RESTART (restarted only when it runs) ·
17
+ systemctl --user try-restart <unit> for each optional service in TRY_RESTART (restarted only when it runs), and
18
+ right after lanes-docsd's, `sh install/enable-docs.sh` of the tree (the docs page's pinned Mermaid, plan W10 W8:
19
+ DOCS_STEP; skipped for a tree that has no such script) ·
18
20
  lanes install-container <name> for each container `lanes sync` recorded with a home volume (containers.json) ·
19
21
  lanes reload-views (the viewports; sidebars swap by themselves, W9 U5) · lanes ensure --if-layout-changed (W9 U6).
20
- Step output goes to the log (`update/last.log`); stdout gets one line per step and a summary.
22
+ Step output goes to the log (`update/last.log`); stdout gets one line per step and a summary, which names the Mermaid
23
+ outcome (`mermaid ok`, `mermaid skipped: <reason>`, …) whenever its step ran.
21
24
 
22
25
  The record, `<state>/update/last.json` (DM-LANES-UPDATE-RECORD), is one JSON object {checked_at, latest, installed,
23
- previous, result, steps: [{name, rc[, warning]}], log}. result: `ok` every phase and step exited 0 or only warned;
24
- `partial` the release is installed but a step after the commit failed (also while the post-commit runs: it is written
25
- before the first step); `refused` a prerequisite, no installed release, or another install holding the install lock
26
- said no and nothing changed; `failed` nothing was installed. A warning (WARN_RC) is a step's own report of a condition
27
- the update neither caused nor can mend — `lanes sync` exiting 1 for a board with an error — recorded with its rc and
28
- `"warning": true`, never a failure. Keys it does not own (a rollback's mark, plan W9 U3) are kept. host-install.sh
29
- writes `refused`, `failed` and a `partial` commit through `record`; `post-commit` writes its own.
26
+ previous, result, steps: [{name, rc[, warning][, outcome, reason]}], log}. result: `ok` every phase and step exited 0
27
+ or only warned; `partial` the release is installed but a step after the commit failed (also while the post-commit
28
+ runs: it is written before the first step); `refused` a prerequisite, no installed release, or another install holding
29
+ the install lock said no and nothing changed; `failed` nothing was installed. A warning (WARN_RC) is a step's own
30
+ report of a condition the update neither caused nor can mend — `lanes sync` exiting 1 for a board with an error;
31
+ enable-docs exiting 1 (a prerequisite or the download), 3 (refused: no valid pin, a digest mismatch) or not finishing
32
+ in STEP_TIMEOUT (the download) — recorded with its rc and `"warning": true`, never a failure. A step with outcomes
33
+ (STEP_OUTCOMES: enable-docs) that did not exit 0 also records `outcome` (`skipped`, `refused`, or `failed` for any
34
+ other status, which is a failure) and `reason`, the last line it wrote (else what its status says). Keys it does not
35
+ own (a rollback's mark, plan W9 U3) are kept. host-install.sh writes `refused`, `failed` and a `partial` commit through
36
+ `record`; `post-commit` writes its own.
30
37
 
31
38
  The run lock, `<state>/update/.lock` (fix cycle 1 of W9 U2): every server install and update takes it before it writes
32
39
  anything — the log and the record included — and holds it to its end, so two runs never interleave a post-commit or
@@ -77,8 +84,8 @@ STEP_TIMEOUT = 300 # seconds per post-commit step; then its process gro
77
84
  STOP_WAIT = 5 # seconds between SIGTERM and SIGKILL of a step's process group, and after it
78
85
  # The optional services a post-commit restarts only when they run (`systemctl try-restart`), in this order. Each keeps
79
86
  # its enable state: an update never switches on a unit the operator left off (economy, notify, web: operator gates).
80
- TRY_RESTART = ('lanes-economy.service', 'lanes-notify.service', 'lanes-enrold.service', 'lanes-mobile.service',
81
- 'lanes-web.service', 'lanes-studio-host.service')
87
+ TRY_RESTART = ('lanes-economy.service', 'lanes-notify.service', 'lanes-enrold.service', 'lanes-docsd.service',
88
+ 'lanes-mobile.service', 'lanes-web.service', 'lanes-studio-host.service')
82
89
  RESULTS = ('ok', 'partial', 'refused', 'failed')
83
90
  OWNED = ('checked_at', 'latest', 'installed', 'previous', 'result', 'steps', 'log') # the keys every record rewrites
84
91
  PARTIAL = 4 # post-commit's exit status when a step failed (host-install.sh --update-only's as well)
@@ -90,7 +97,19 @@ REFUSED = 3 # `lock`'s exit status when another run holds the ru
90
97
  # cmd_sync: `return 1 if failed`); counted as a failure, it would make every update `partial` for as long as such a
91
98
  # board exists, and `lanes update` re-run the whole post-commit on every tick (fix cycle 1 of W9 U2). Any other status
92
99
  # of the step — 124 no answer, 127 could not start, 128+N a signal — is a failure like any other.
93
- WARN_RC = {'sync': 1}
100
+ # The docs page's pinned Mermaid (plan W10 W8, LNS-109) is the other: install/enable-docs.sh of the tree just installed,
101
+ # run right after lanes-docsd's try-restart (the full install runs it right after lanes-docsd's restart). Never fatal:
102
+ # Mermaid is optional (without it every fence shows its source), so its exit statuses 1 (a prerequisite is missing or
103
+ # the download failed: skipped), 3 (refused: no valid pin in holders.lock, a digest mismatch; nothing installed) and
104
+ # 124 (the download did not finish in STEP_TIMEOUT: skipped) are warnings; any other status (2 usage, 127 could not
105
+ # start, a signal) is a failure like any other step's. An installed file with the pinned digest is kept: no download.
106
+ DOCS_UNIT = 'lanes-docsd.service'
107
+ DOCS_STEP = 'enable-docs'
108
+ DOCS_SCRIPT = 'install/enable-docs.sh'
109
+ WARN_RC = {'sync': (1,), DOCS_STEP: (1, 3, 124)}
110
+ # The steps whose outcome the record and the summary name (any status not listed: `failed`).
111
+ STEP_OUTCOMES = {DOCS_STEP: {0: 'ok', 1: 'skipped', 3: 'refused', 124: 'skipped'}}
112
+ REASON_LIMIT = 300 # characters of a step's last line kept as its `reason` (a refusal names two digests)
94
113
  LOCK = 'update/.lock' # the run lock, under the state directory
95
114
  MODES = ('update', 'install')
96
115
  RELEASE = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.+-]+)?\Z')
@@ -111,11 +130,14 @@ def utc(at=None):
111
130
 
112
131
 
113
132
  def step_entry(step):
114
- """A record's step from (name, rc) or (name, rc, warning): {name, rc}, plus `"warning": true` for a warning."""
133
+ """A record's step from (name, rc), (name, rc, warning) or (name, rc, warning, extra): {name, rc}, plus
134
+ `"warning": true` for a warning, plus `extra` ({outcome, reason} of a step with outcomes that did not exit 0)."""
115
135
  name, rc, *rest = step
116
136
  entry = {'name': name, 'rc': int(rc)}
117
137
  if rest and rest[0]:
118
138
  entry['warning'] = True
139
+ if len(rest) > 1 and rest[1]:
140
+ entry.update(rest[1])
119
141
  return entry
120
142
 
121
143
 
@@ -128,6 +150,44 @@ def warned_steps(steps):
128
150
  return [entry['name'] for entry in map(step_entry, steps) if entry.get('warning')]
129
151
 
130
152
 
153
+ def step_outcome(name, rc, said, timeout):
154
+ """{} for a step without outcomes (STEP_OUTCOMES) or one that exited 0, else {outcome, reason}: the outcome its
155
+ status names (`failed` when it names none) and `reason`, the last line `said` (its own output) holds, else what
156
+ the status says (describe)."""
157
+ outcomes = STEP_OUTCOMES.get(name)
158
+ if outcomes is None or rc == 0:
159
+ return {}
160
+ lines = [line for line in said.splitlines() if line.strip()]
161
+ reason = printable(lines[-1], REASON_LIMIT) if lines and rc not in (124, 127) else describe(rc, timeout)
162
+ return {'outcome': outcomes.get(rc, 'failed'), 'reason': reason}
163
+
164
+
165
+ def step_said(log, start):
166
+ """What a step wrote to the log after byte `start`: run_step's `== argv` line and its `-> …` line left out."""
167
+ try:
168
+ with open(log, 'rb') as handle:
169
+ handle.seek(start)
170
+ text = handle.read().decode('utf-8', 'replace')
171
+ except OSError:
172
+ return ''
173
+ lines = text.splitlines()
174
+ if lines and lines[0].startswith('== '):
175
+ lines = lines[1:]
176
+ if lines and lines[-1].startswith('-> '):
177
+ lines = lines[:-1]
178
+ return '\n'.join(lines)
179
+
180
+
181
+ def outcome_clause(steps):
182
+ """The summary's words for the steps with outcomes that ran: `; mermaid ok`, `; mermaid skipped: <reason>`, …"""
183
+ words = []
184
+ for entry in map(step_entry, steps):
185
+ if entry['name'] == DOCS_STEP:
186
+ outcome = entry.get('outcome') or ('ok' if entry['rc'] == 0 else 'failed')
187
+ words.append(f"; mermaid {outcome}" + (f": {entry['reason']}" if entry.get('reason') else ''))
188
+ return ''.join(words)
189
+
190
+
131
191
  def installed_tree(home=None):
132
192
  """The existing tree the `current` link names now (resolved), else None."""
133
193
  try:
@@ -252,14 +312,18 @@ def post_commit(tree, state, log, steps=(), before=None, started=None, out=None,
252
312
  Path(log).parent.mkdir(parents=True, exist_ok=True)
253
313
  with open(log, 'a') as handle:
254
314
  def step(name, argv):
315
+ handle.flush()
316
+ start = os.fstat(handle.fileno()).st_size # where this step's lines begin (O_APPEND: the end)
255
317
  try:
256
318
  rc = run(argv, handle, timeout)
257
319
  except (KeyboardInterrupt, SystemExit) as error:
258
320
  steps.append((name, interrupted_rc(error)))
259
321
  print(f'FAIL {name} (interrupted)', file=out, flush=True)
260
322
  raise
261
- warning = rc != 0 and WARN_RC.get(name) == rc
262
- steps.append((name, rc, warning))
323
+ warning = rc != 0 and rc in WARN_RC.get(name, ())
324
+ handle.flush()
325
+ extra = step_outcome(name, rc, step_said(log, start) if rc and name in STEP_OUTCOMES else '', timeout)
326
+ steps.append((name, rc, warning, extra))
263
327
  print('ok ' + name if rc == 0 else f"{'WARN' if warning else 'FAIL'} {name} ({describe(rc, timeout)})",
264
328
  file=out, flush=True)
265
329
  try:
@@ -268,6 +332,10 @@ def post_commit(tree, state, log, steps=(), before=None, started=None, out=None,
268
332
  step('restart lanes-statusd', [systemctl, '--user', 'restart', 'lanes-statusd.service'])
269
333
  for unit in TRY_RESTART:
270
334
  step('try-restart ' + unit[:-len('.service')], [systemctl, '--user', 'try-restart', unit])
335
+ if unit == DOCS_UNIT and (Path(tree) / DOCS_SCRIPT).is_file():
336
+ # `sh`, as the RUNBOOK runs it: a tree whose script lost its mode bit still installs Mermaid. A
337
+ # tree from before W10 W6 (a rollback's) has no script: nothing to run, no step.
338
+ step(DOCS_STEP, ['sh', str(Path(tree) / DOCS_SCRIPT)])
271
339
  for container in recorded_containers(state):
272
340
  step('install-container ' + container, [lanes, 'install-container', container])
273
341
  step('reload-views', [lanes, 'reload-views'])
@@ -276,16 +344,20 @@ def post_commit(tree, state, log, steps=(), before=None, started=None, out=None,
276
344
  finally:
277
345
  ok = finished and not failed_steps(steps)
278
346
  document = write_record(state, 'ok' if ok else 'partial', steps, tree, before, log, started, environ, home)
279
- failed, warned = failed_steps(steps), warned_steps(steps)
347
+ # A step with outcomes is named by its own clause (`; mermaid skipped: …`), not again among the warnings.
348
+ failed, warned = failed_steps(steps), [name for name in warned_steps(steps) if name not in STEP_OUTCOMES]
280
349
  was = f" (was {document['previous']})" if document['previous'] else ''
281
350
  warnings = f"; {count(warned, 'warning')}: {', '.join(warned)}" if warned else ''
282
- if ok and not warned:
283
- print(f"ok: lanes {document['installed']} installed{was}; every step ran — {record_path(state)}", file=out)
351
+ outcomes = outcome_clause(steps)
352
+ if ok and not warned_steps(steps):
353
+ print(f"ok: lanes {document['installed']} installed{was}; every step ran{outcomes} — {record_path(state)}",
354
+ file=out)
284
355
  elif ok:
285
- print(f"ok: lanes {document['installed']} installed{was}; every step ran{warnings} — log {log}", file=out)
356
+ print(f"ok: lanes {document['installed']} installed{was}; every step ran{warnings}{outcomes} — log {log}",
357
+ file=out)
286
358
  else:
287
359
  print(f"partial: lanes {document['installed']} installed{was}; {count(failed, 'step')} failed: "
288
- f"{', '.join(failed)}{warnings} — log {log}", file=out)
360
+ f"{', '.join(failed)}{warnings}{outcomes} — log {log}", file=out)
289
361
  return document
290
362
 
291
363
 
@@ -850,8 +922,8 @@ def update_main(args, key, out=None, environ=None, home=None, clock=None, output
850
922
 
851
923
  def timer_enabled(units):
852
924
  """Whether lanes-update.timer is enabled in one of the unit folders `units`: `<folder>/timers.target.wants/
853
- lanes-update.timer`, the link `systemctl --user enable` makes, names an existing unit. A file read, as every other
854
- doctor line is (doctor never asks systemctl)."""
925
+ lanes-update.timer`, the link `systemctl --user enable` makes, names an existing unit. A file read: this line
926
+ never asks systemctl (the web and docs lines do, one bounded `systemctl --user show` each)."""
855
927
  return any((Path(folder) / TIMER_WANTS / TIMER).exists() for folder in units)
856
928
 
857
929