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:
co-authored by
Claude Fable 5.1
parent
57c58ebffb
commit
27f9665606
+61
-42
@@ -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), <)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user