Skip to content

Commit 6f7329e

Browse files
committed
OC4.1: read-only fixed-unit systemd timer fallback without guessing monotonic schedules
1 parent 087e828 commit 6f7329e

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

‎src/lib/bob/bob_operations_diagnostics.py‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
from datetime import datetime, timezone, timedelta
10+
import json
1011
import re
1112
import subprocess
1213

@@ -59,11 +60,48 @@ def _entry(key,state,activity,reason,evidence,when="",href="/logs",
5960
code=code,evidence=evidence,updated_at=_time(when),
6061
href=href,next_scheduled=next_run)
6162

63+
def _timer_list_next(unit, *, run):
64+
"""Fixed-unit, read-only fallback: systemctl list-timers JSON if realtime
65+
timer property is unavailable (for example a monotonic timer).
66+
Never derive a wall-clock estimate from monotonic microseconds.
67+
"""
68+
try:
69+
res=run(["systemctl","--user","list-timers","--all","--no-pager",
70+
"--output=json",unit],capture_output=True,text=True,
71+
timeout=1.5,check=False)
72+
if res.returncode!=0 or len(res.stdout)>8192:
73+
return ""
74+
rows=json.loads(res.stdout)
75+
if not isinstance(rows,list) or len(rows)>12:
76+
return ""
77+
for item in rows:
78+
if not isinstance(item,dict) or item.get("unit")!=unit:
79+
continue
80+
value=item.get("next")
81+
if isinstance(value,str):
82+
value=value.strip()
83+
if value not in ("","n/a","-","0") and TIMESTAMP.fullmatch(value):
84+
return value
85+
elif type(value)==int and 10**14<value<10**17:
86+
# systemd JSON MAY use Unix microseconds; these bounds are
87+
# intentionally conservative, otherwise we show unknown.
88+
try:
89+
return datetime.fromtimestamp(
90+
value/1000000,timezone.utc).isoformat(timespec="seconds")
91+
except (OverflowError,ValueError,OSError):
92+
pass
93+
except (OSError,subprocess.TimeoutExpired,TypeError,ValueError,
94+
AttributeError,json.JSONDecodeError):
95+
pass
96+
return ""
97+
98+
6299
def _timer_schedules(*,run=subprocess.run):
63-
"""Read only the two known user timers. Return raw systemd wall-clock text.
100+
"""Observe exactly two allowlisted user timers, never control units.
64101
65-
These are provider-reported scheduled triggers, NOT rate eligibility;
66-
invalid/unavailable response becomes unknown rather than an invented time.
102+
Prefer systemd's realtime next trigger; fall back to systemd's own
103+
list-timers reported next wall time if no realtime property is provided.
104+
Missing schedule is distinct from an inactive timer or publisher fault.
67105
"""
68106
times={}
69107
for key,unit in TIMERS.items():
@@ -74,10 +112,13 @@ def _timer_schedules(*,run=subprocess.run):
74112
value=res.stdout.strip() if res.returncode==0 else ""
75113
times[key]=value if (value not in ("","n/a","0") and
76114
TIMESTAMP.fullmatch(value)) else ""
77-
except (OSError, subprocess.TimeoutExpired, TypeError, AttributeError):
115+
except (OSError,subprocess.TimeoutExpired,TypeError,AttributeError):
78116
times[key]=""
117+
if not times[key]:
118+
times[key]=_timer_list_next(unit,run=run)
79119
return times
80120

121+
81122
def _eligible_time(metrics,key,now):
82123
values=metrics.get("next_possible_utc") if isinstance(metrics,dict) else None
83124
raw=values.get(key) if isinstance(values,dict) else None

0 commit comments

Comments
 (0)