Bug bash fixes: special-file reads, per-client lockout, prefs validation, security headers, kernel-thread guard, login feedback, error text; plus journal-quiet sudoers + journal groups, upload overwrite guard, machines pattern, dnf freshness, cron/services speed, clean version

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 17:41:52 -07:00
co-authored by Claude Fable 5.1
parent 57c58ebffb
commit 27f9665606
18 changed files with 306 additions and 85 deletions
+61 -42
View File
@@ -82,6 +82,7 @@ type Manager struct {
mu sync.Mutex
lastRuns map[string]int64 // user\x00command -> unix ms
lastAt time.Time
lastCursor string // journal cursor of the newest entry folded into lastRuns
logging bool
runs map[string]*Run
}
@@ -388,32 +389,48 @@ func (m *Manager) Overview(ctx context.Context) Overview {
var cronLogRe = regexp.MustCompile(`^\((\S+)\) CMD \((.*)\)$`)
// lastRunsFromJournal maps user+command to the most recent "(user) CMD (command)" line.
// The first call scans 30 days; later calls only read entries after the last cursor, so
// the cost per refresh is proportional to new cron activity, not to history.
func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 {
m.mu.Lock()
if time.Since(m.lastAt) < 5*time.Second && m.lastRuns != nil {
if time.Since(m.lastAt) < 2*time.Second && m.lastRuns != nil {
r := m.lastRuns
m.mu.Unlock()
return r
}
cursor := m.lastCursor
res := map[string]int64{}
for k, v := range m.lastRuns {
res[k] = v
}
m.mu.Unlock()
args := []string{"journalctl", "-t", "CRON", "-t", "cron", "-t", "crond", "-o", "json", "--no-pager", "-q", "--since", "-30d", "-n", "20000"}
args := []string{"journalctl", "-t", "CRON", "-t", "cron", "-t", "crond", "-o", "json", "--no-pager", "-q"}
if cursor != "" {
args = append(args, "--after-cursor="+cursor)
} else {
args = append(args, "--since", "-30d", "-n", "20000")
}
if m.journalSudo {
args = append([]string{"sudo", "-n"}, args...)
}
out, _ := run(ctx, "", args...)
res := map[string]int64{}
seen := false
seen := m.logging
sc := bufio.NewScanner(strings.NewReader(out))
sc.Buffer(make([]byte, 0, 64<<10), 4<<20)
for sc.Scan() {
var raw struct {
Msg any `json:"MESSAGE"`
TS string `json:"__REALTIME_TIMESTAMP"`
Msg any `json:"MESSAGE"`
TS string `json:"__REALTIME_TIMESTAMP"`
Cursor string `json:"__CURSOR"`
}
if json.Unmarshal(sc.Bytes(), &raw) != nil {
continue
}
seen = true
if raw.Cursor != "" {
cursor = raw.Cursor
}
msg, _ := raw.Msg.(string)
mm := cronLogRe.FindStringSubmatch(msg)
if mm == nil {
@@ -426,7 +443,7 @@ func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 {
}
}
m.mu.Lock()
m.lastRuns, m.lastAt, m.logging = res, time.Now(), seen
m.lastRuns, m.lastAt, m.lastCursor, m.logging = res, time.Now(), cursor, seen
m.mu.Unlock()
return res
}
@@ -480,48 +497,50 @@ func (m *Manager) OutputTail(ctx context.Context, j Job, n int) (string, error)
// ---- timers ----
// timers: one `systemctl show` over every loaded timer instead of three list commands.
func (m *Manager) timers(ctx context.Context) []Timer {
out, err := run(ctx, "", "systemctl", "list-timers", "--all", "--output=json", "--no-pager")
out, err := run(ctx, "", "systemctl", "show", "*.timer", "--timestamp=unix", "--no-pager",
"-p", "Id,Description,ActiveState,SubState,UnitFileState,NextElapseUSecRealtime,LastTriggerUSec,Triggers")
if err != nil {
return []Timer{}
}
var lt []struct {
Next int64 `json:"next"`
Last int64 `json:"last"`
Unit string `json:"unit"`
Activates string `json:"activates"`
}
_ = json.Unmarshal([]byte(out), &lt)
units, _ := run(ctx, "", "systemctl", "list-units", "--type=timer", "--all", "--output=json", "--no-pager", "--plain")
var lu []struct{ Unit, Active, Sub, Description string }
_ = json.Unmarshal([]byte(units), &lu)
meta := map[string]struct{ Active, Sub, Description string }{}
for _, u := range lu {
meta[u.Unit] = struct{ Active, Sub, Description string }{u.Active, u.Sub, u.Description}
}
files, _ := run(ctx, "", "systemctl", "list-unit-files", "--type=timer", "--output=json", "--no-pager")
var uf []struct {
UnitFile string `json:"unit_file"`
State string `json:"state"`
}
_ = json.Unmarshal([]byte(files), &uf)
enabled := map[string]string{}
for _, f := range uf {
enabled[f.UnitFile] = f.State
}
res := make([]Timer, 0, len(lt))
for _, t := range lt {
x := Timer{Unit: t.Unit, Activates: t.Activates, Enabled: enabled[t.Unit]}
if mt, ok := meta[t.Unit]; ok {
x.Active, x.Sub, x.Description = mt.Active, mt.Sub, mt.Description
unix := func(v string) int64 { // "@1789711405" (seconds) or "n/a"
if strings.HasPrefix(v, "@") {
s, _ := strconv.ParseInt(strings.TrimPrefix(v, "@"), 10, 64)
return s * 1000
}
if t.Next > 0 {
x.Next = t.Next / 1000
return 0
}
res := []Timer{}
for _, block := range strings.Split(out, "\n\n") {
t := Timer{}
for _, line := range strings.Split(block, "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
switch k {
case "Id":
t.Unit = v
case "Description":
t.Description = v
case "ActiveState":
t.Active = v
case "SubState":
t.Sub = v
case "UnitFileState":
t.Enabled = v
case "NextElapseUSecRealtime":
t.Next = unix(v)
case "LastTriggerUSec":
t.Last = unix(v)
case "Triggers":
t.Activates = strings.Fields(v + " ")[0:1][0]
}
}
if t.Last > 0 {
x.Last = t.Last / 1000
if t.Unit != "" {
res = append(res, t)
}
res = append(res, x)
}
sort.Slice(res, func(i, j int) bool { return res[i].Unit < res[j].Unit })
return res