@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.
- package/package.json +1 -1
- package/payload/lanes/README.md +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +301 -45
- package/payload/lanes/container/lane +42 -1
- package/payload/lanes/container/lane-mcp +65 -6
- package/payload/lanes/container/lane_core.py +178 -0
- package/payload/lanes/docs/ARCHITECTURE.md +192 -20
- package/payload/lanes/docs/DESIGN.md +6 -3
- package/payload/lanes/docs/INSTALL.md +25 -8
- package/payload/lanes/docs/RUNBOOK.md +369 -8
- package/payload/lanes/docs/USERGUIDE.md +201 -25
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +106 -13
- package/payload/lanes/libexec/statusd.py +97 -5
- package/payload/lanes/libexec/tokens.py +31 -9
- package/payload/lanes/libexec/update.py +94 -22
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- 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
|
|
24
|
-
`partial` the release is installed but a step after the commit failed (also while the post-commit
|
|
25
|
-
before the first step); `refused` a prerequisite, no installed release, or another install holding
|
|
26
|
-
said no and nothing changed; `failed` nothing was installed. A warning (WARN_RC) is a step's own
|
|
27
|
-
the update neither caused nor can mend — `lanes sync` exiting 1 for a board with an error
|
|
28
|
-
|
|
29
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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)
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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}",
|
|
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
|
|
854
|
-
|
|
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
|
|