code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Are this host's installed systemd *timers* actually enabled and running?
|
|
2
|
+
(#2082)
|
|
3
|
+
|
|
4
|
+
:mod:`coord.health.checks.unit_drift` (#1831) proves an installed unit's
|
|
5
|
+
**content** matches the release. It says nothing about whether systemd was
|
|
6
|
+
ever told to run it — and `coord-release-propagate.timer` demonstrated that
|
|
7
|
+
gap directly. On 2026-08-10 its content matched `deploy/` byte for byte
|
|
8
|
+
(`unit_drift` green, always) while the fleet sat eleven releases behind:
|
|
9
|
+
|
|
10
|
+
$ systemctl --user list-unit-files 'coord-release-propagate*'
|
|
11
|
+
coord-release-propagate.timer disabled enabled
|
|
12
|
+
^^^^^^^^ ^^^^^^^
|
|
13
|
+
STATE VENDOR PRESET
|
|
14
|
+
|
|
15
|
+
A `.timer` unit's *file* being current says nothing about whether
|
|
16
|
+
``systemctl --user enable --now`` was ever run on it. Writing (or
|
|
17
|
+
refreshing) a unit's content — by hand, or via
|
|
18
|
+
:func:`coord.deploy_units.install_units` — never implies enabling it;
|
|
19
|
+
those are two independent systemd operations, and the second one has no
|
|
20
|
+
visible symptom until someone thinks to ask "when did this last fire?".
|
|
21
|
+
That is precisely how the timer's off state and its working state produced
|
|
22
|
+
identical files on disk and stayed indistinguishable from the outside for a
|
|
23
|
+
day.
|
|
24
|
+
|
|
25
|
+
Deliberately generic over every packaged ``*.timer`` (not hardcoded to
|
|
26
|
+
``coord-release-propagate.timer``): every timer this fleet ships
|
|
27
|
+
(``coord-drive-queue``, ``coord-notify``, ``coord-web-dist-build``, this
|
|
28
|
+
one) declares ``[Install] WantedBy=timers.target`` — see ``deploy/*.timer``
|
|
29
|
+
— so "installed but not enabled" is the same defect on any of them, not a
|
|
30
|
+
property of this one release's timer.
|
|
31
|
+
|
|
32
|
+
Reuses :mod:`coord.health.checks.unit_drift`'s reference resolution
|
|
33
|
+
(:func:`~coord.health.checks.unit_drift.resolve_reference`,
|
|
34
|
+
:func:`~coord.health.checks.unit_drift.resolve_systemd_user_dir`) rather
|
|
35
|
+
than a second implementation of "which ``deploy/`` is authoritative" —
|
|
36
|
+
two answers to that question is exactly the class of defect #2096 names:
|
|
37
|
+
two surfaces that happen to agree today and can silently drift apart.
|
|
38
|
+
|
|
39
|
+
Judgement (:func:`grade_timer_state`) is pure and takes plain
|
|
40
|
+
``systemctl --user show`` fields, so it is unit-testable against a
|
|
41
|
+
hand-built dict with no systemd, no fleet, and no root — same split
|
|
42
|
+
``spawned_coord.py`` documents for "measure locally, judge centrally".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import subprocess
|
|
48
|
+
|
|
49
|
+
from coord.health.checks.unit_drift import (
|
|
50
|
+
_unit_files,
|
|
51
|
+
resolve_reference,
|
|
52
|
+
resolve_systemd_user_dir,
|
|
53
|
+
)
|
|
54
|
+
from coord.health.models import CheckResult, HealthContext, Severity
|
|
55
|
+
from coord.health.registry import check
|
|
56
|
+
|
|
57
|
+
# systemctl is fast, but a wedged call must not eat the whole health-tick
|
|
58
|
+
# budget — same rationale/value as spawned_coord._SYSTEMCTL_TIMEOUT.
|
|
59
|
+
_SYSTEMCTL_TIMEOUT = 5.0
|
|
60
|
+
|
|
61
|
+
#: ``UnitFileState`` values that mean "systemd will not fire this unit on
|
|
62
|
+
#: its own schedule", whatever content is on disk.
|
|
63
|
+
_INACTIVE_STATES = frozenset(
|
|
64
|
+
{"disabled", "linked", "linked-runtime", "masked", "masked-runtime"}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _timer_states(units: tuple[str, ...]) -> dict[str, dict[str, str]]:
|
|
69
|
+
"""One ``systemctl --user show`` call for every timer in *units*.
|
|
70
|
+
|
|
71
|
+
Batched into a single subprocess the same way
|
|
72
|
+
``spawned_coord.running_unit_pids`` batches its own systemctl call: this
|
|
73
|
+
runs on every health tick on every machine, and N timers must cost one
|
|
74
|
+
subprocess, not N. Returns ``{}`` when there is no systemd on this host
|
|
75
|
+
(macOS, a container, a thin client) — that is "no data", handled by the
|
|
76
|
+
caller, not a crash.
|
|
77
|
+
"""
|
|
78
|
+
if not units:
|
|
79
|
+
return {}
|
|
80
|
+
argv = [
|
|
81
|
+
"systemctl",
|
|
82
|
+
"--user",
|
|
83
|
+
"show",
|
|
84
|
+
"--property=Id",
|
|
85
|
+
"--property=UnitFileState",
|
|
86
|
+
"--property=UnitFilePreset",
|
|
87
|
+
"--property=ActiveState",
|
|
88
|
+
"--property=SubState",
|
|
89
|
+
*units,
|
|
90
|
+
]
|
|
91
|
+
try:
|
|
92
|
+
proc = subprocess.run(
|
|
93
|
+
argv,
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=_SYSTEMCTL_TIMEOUT,
|
|
97
|
+
check=False,
|
|
98
|
+
)
|
|
99
|
+
except (OSError, subprocess.SubprocessError):
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
out: dict[str, dict[str, str]] = {}
|
|
103
|
+
# `systemctl show` emits one KEY=VALUE block per unit, blank-line
|
|
104
|
+
# separated, same shape spawned_coord already parses defensively.
|
|
105
|
+
for block in proc.stdout.split("\n\n"):
|
|
106
|
+
fields: dict[str, str] = {}
|
|
107
|
+
for line in block.splitlines():
|
|
108
|
+
key, sep, value = line.partition("=")
|
|
109
|
+
if sep:
|
|
110
|
+
fields[key.strip()] = value.strip()
|
|
111
|
+
unit_id = fields.get("Id", "")
|
|
112
|
+
if unit_id:
|
|
113
|
+
out[unit_id] = fields
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def grade_timer_state(fields: dict[str, str]) -> tuple[Severity, str]:
|
|
118
|
+
"""Pure judgement over one timer's ``systemctl --user show`` fields.
|
|
119
|
+
|
|
120
|
+
The rule is deliberately simple and does not consult
|
|
121
|
+
``UnitFilePreset``: a ``.timer`` unit copied into
|
|
122
|
+
``~/.config/systemd/user/`` is, by construction, meant to run — nobody
|
|
123
|
+
installs a schedule to leave it off, and every packaged timer in this
|
|
124
|
+
fleet declares ``WantedBy=timers.target``. So *installed* implies
|
|
125
|
+
*should be enabled and active*, full stop; the moment that stops being
|
|
126
|
+
true the fleet has exactly the invisible-off-switch problem #2082
|
|
127
|
+
describes. (Contrast ``coord.deploy_units.enable_timers``, the write
|
|
128
|
+
side, which applies the identical rule when it (re)installs a timer's
|
|
129
|
+
content — one invariant, asserted by both the detector and the fixer.)
|
|
130
|
+
"""
|
|
131
|
+
state = fields.get("UnitFileState", "")
|
|
132
|
+
active = fields.get("ActiveState", "")
|
|
133
|
+
sub = fields.get("SubState", "")
|
|
134
|
+
|
|
135
|
+
if not state and not active:
|
|
136
|
+
return Severity.UNKNOWN, "systemd reported no state for this unit"
|
|
137
|
+
|
|
138
|
+
if state in _INACTIVE_STATES:
|
|
139
|
+
return (
|
|
140
|
+
Severity.CRIT,
|
|
141
|
+
f"installed but {state.upper()} — a copied .timer unit that is "
|
|
142
|
+
"never enabled will not fire on its own; run 'systemctl --user "
|
|
143
|
+
f"enable --now <unit>' (ActiveState={active or '?'})",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if active != "active":
|
|
147
|
+
return (
|
|
148
|
+
Severity.CRIT,
|
|
149
|
+
f"enabled but not active (ActiveState={active or '?'}, "
|
|
150
|
+
f"SubState={sub or '?'}) — check 'systemctl --user status <unit>'",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return Severity.OK, f"enabled and active (state={state or '?'})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@check(
|
|
157
|
+
id="timer_active",
|
|
158
|
+
scope="machine",
|
|
159
|
+
title="timer active",
|
|
160
|
+
order=45,
|
|
161
|
+
description=(
|
|
162
|
+
"Installed systemd *user* timers (deploy/*.timer) are enabled and "
|
|
163
|
+
"active, not merely present with matching content (#2082) — a "
|
|
164
|
+
"disabled timer's file is indistinguishable from an active one's "
|
|
165
|
+
"until something asks systemd whether it actually runs."
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
def probe_timer_active(ctx: HealthContext) -> list[CheckResult]:
|
|
169
|
+
reference = resolve_reference(ctx)
|
|
170
|
+
if reference is None:
|
|
171
|
+
return [
|
|
172
|
+
CheckResult(
|
|
173
|
+
check_id="timer_active",
|
|
174
|
+
scope="machine",
|
|
175
|
+
severity=Severity.OK,
|
|
176
|
+
headroom="no deploy/ checkout found on this machine",
|
|
177
|
+
values={"reference_source": None},
|
|
178
|
+
)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
timers = [p.name for p in _unit_files(reference.path) if p.name.endswith(".timer")]
|
|
182
|
+
if not timers:
|
|
183
|
+
return [
|
|
184
|
+
CheckResult(
|
|
185
|
+
check_id="timer_active",
|
|
186
|
+
scope="machine",
|
|
187
|
+
severity=Severity.OK,
|
|
188
|
+
headroom="no timer units in the packaged reference",
|
|
189
|
+
values={"reference_source": reference.source},
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
installed_dir = resolve_systemd_user_dir(ctx)
|
|
194
|
+
present = [name for name in timers if (installed_dir / name).exists()]
|
|
195
|
+
if not present:
|
|
196
|
+
# The common case — most machines run none of the fleet's timers.
|
|
197
|
+
# Same "absence is OK, not a fault" convention as spawned_coord /
|
|
198
|
+
# cli_venv / tui_binary.
|
|
199
|
+
return [
|
|
200
|
+
CheckResult(
|
|
201
|
+
check_id="timer_active",
|
|
202
|
+
scope="machine",
|
|
203
|
+
severity=Severity.OK,
|
|
204
|
+
headroom="no packaged timer is installed on this machine",
|
|
205
|
+
values={"reference_source": reference.source},
|
|
206
|
+
)
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
states = _timer_states(tuple(present))
|
|
210
|
+
results: list[CheckResult] = []
|
|
211
|
+
for name in present:
|
|
212
|
+
fields = states.get(name)
|
|
213
|
+
values = {"unit": name, "reference_source": reference.source}
|
|
214
|
+
if fields is None:
|
|
215
|
+
results.append(
|
|
216
|
+
CheckResult(
|
|
217
|
+
check_id="timer_active",
|
|
218
|
+
scope="machine",
|
|
219
|
+
subject=name,
|
|
220
|
+
severity=Severity.UNKNOWN,
|
|
221
|
+
headroom="could not read systemd state for this unit",
|
|
222
|
+
detail=(
|
|
223
|
+
"no systemd on this host, or 'systemctl --user show' "
|
|
224
|
+
"failed — this lane is unverified, not confirmed active"
|
|
225
|
+
),
|
|
226
|
+
error="systemctl unavailable",
|
|
227
|
+
values=values,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
severity, headroom = grade_timer_state(fields)
|
|
233
|
+
values.update(
|
|
234
|
+
{
|
|
235
|
+
"unit_file_state": fields.get("UnitFileState"),
|
|
236
|
+
"unit_file_preset": fields.get("UnitFilePreset"),
|
|
237
|
+
"active_state": fields.get("ActiveState"),
|
|
238
|
+
"sub_state": fields.get("SubState"),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
results.append(
|
|
242
|
+
CheckResult(
|
|
243
|
+
check_id="timer_active",
|
|
244
|
+
scope="machine",
|
|
245
|
+
subject=name,
|
|
246
|
+
severity=severity,
|
|
247
|
+
headroom=headroom,
|
|
248
|
+
detail=f"systemctl --user enable --now {name}" if severity is Severity.CRIT else "",
|
|
249
|
+
threshold="crit when an installed timer is not enabled and active",
|
|
250
|
+
values=values,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return results
|