systemctl reload guard, guard reload or SIGHUP reads the configuration again: a watch keeps its bounties and counts under the same name and takes its new settings at once; its source starts again only if it changed; watches gone stop, new ones start; a file with a mistake is refused. The Housecarl connection, state, socket and log wait for a restart, and guard reload says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
5.2 KiB
Go
186 lines
5.2 KiB
Go
package daemon
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"guard/internal/bounty"
|
|
"guard/internal/config"
|
|
"guard/internal/match"
|
|
)
|
|
|
|
// Reloaded is what a reload changed.
|
|
type Reloaded struct {
|
|
Changes []string `json:"changes"`
|
|
// Restart are the changed settings a reload can't apply: they wait
|
|
// for a restart.
|
|
Restart []string `json:"restart,omitempty"`
|
|
}
|
|
|
|
// ReloadFile reads the configuration again (Load) and reloads it.
|
|
func (d *Daemon) ReloadFile() (*Reloaded, error) {
|
|
if d.Load == nil {
|
|
return nil, errors.New("this Guard can't read its configuration again")
|
|
}
|
|
cfg, err := d.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.Reload(cfg)
|
|
}
|
|
|
|
// Reload puts a new configuration to work without losing what Guard
|
|
// counted: a watch keeps its bounties and counts under the same name,
|
|
// and its source runs on unless it changed. Watches gone stop, new ones
|
|
// start. The housecarl connection, state, socket and log wait for a
|
|
// restart. A configuration with no watch at work is refused, and the
|
|
// old one stays.
|
|
func (d *Daemon) Reload(cfg *config.Config) (*Reloaded, error) {
|
|
if d.ctx == nil {
|
|
return nil, errors.New("Guard isn't running")
|
|
}
|
|
type next struct {
|
|
cfg *config.Watch
|
|
m *match.Matcher
|
|
}
|
|
var want []next
|
|
for _, w := range cfg.Watch {
|
|
if w.Disabled {
|
|
continue
|
|
}
|
|
m, err := match.Compile(w.Match, w.Ignore)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("watch %s: %v", w.Name, err)
|
|
}
|
|
want = append(want, next{w, m})
|
|
}
|
|
if len(want) == 0 {
|
|
return nil, errors.New("every watch is disabled: nothing to do (the configuration stays as it was)")
|
|
}
|
|
|
|
res := &Reloaded{}
|
|
d.mu.Lock()
|
|
old := d.cfg
|
|
oh, nh := old.Housecarl, cfg.Housecarl
|
|
oh.List, nh.List = "", "" // the default list only fills in watches
|
|
if oh != nh {
|
|
res.Restart = append(res.Restart, "housecarl")
|
|
}
|
|
if old.State != cfg.State {
|
|
res.Restart = append(res.Restart, "state")
|
|
}
|
|
if old.Socket != cfg.Socket {
|
|
res.Restart = append(res.Restart, "socket")
|
|
}
|
|
if old.Log != cfg.Log {
|
|
res.Restart = append(res.Restart, "log")
|
|
}
|
|
cfg.Housecarl.URL, cfg.Housecarl.Socket, cfg.Housecarl.TokenFile, cfg.Housecarl.Token = old.Housecarl.URL, old.Housecarl.Socket, old.Housecarl.TokenFile, old.Housecarl.Token
|
|
cfg.Housecarl.CACert, cfg.Housecarl.Insecure = old.Housecarl.CACert, old.Housecarl.Insecure
|
|
cfg.State, cfg.Socket, cfg.Log = old.State, old.Socket, old.Log
|
|
|
|
if !slices.Equal(old.NeverBan, cfg.NeverBan) {
|
|
n := NewNeverBan(cfg.NeverBan, d.log)
|
|
n.Lookup = d.never.Lookup
|
|
d.never.mu.RLock()
|
|
for _, h := range n.names {
|
|
if a, ok := d.never.resolved[h]; ok {
|
|
n.resolved[h] = a
|
|
}
|
|
}
|
|
d.never.mu.RUnlock()
|
|
d.neverStop()
|
|
d.never = n
|
|
d.startNever()
|
|
res.Changes = append(res.Changes, "never_ban")
|
|
}
|
|
if old.Defaults.Repeat != cfg.Defaults.Repeat {
|
|
res.Changes = append(res.Changes, "defaults.repeat: for the next bans")
|
|
}
|
|
|
|
byName := map[string]*Watch{}
|
|
for _, w := range d.watches {
|
|
byName[strings.ToLower(w.Cfg.Name)] = w
|
|
}
|
|
var watches []*Watch
|
|
var stopped []chan struct{}
|
|
for _, n := range want {
|
|
w := byName[strings.ToLower(n.cfg.Name)]
|
|
delete(byName, strings.ToLower(n.cfg.Name))
|
|
if w == nil {
|
|
w = &Watch{Cfg: n.cfg, Matcher: n.m, Tracker: bounty.New(n.cfg.Hits, n.cfg.Within.D())}
|
|
d.start(w)
|
|
watches = append(watches, w)
|
|
res.Changes = append(res.Changes, "watch "+n.cfg.Name+": added")
|
|
continue
|
|
}
|
|
watches = append(watches, w)
|
|
oc, _ := w.setup()
|
|
if reflect.DeepEqual(oc, n.cfg) {
|
|
continue
|
|
}
|
|
var what []string
|
|
if !slices.Equal(oc.Match, n.cfg.Match) || !slices.Equal(oc.Ignore, n.cfg.Ignore) {
|
|
what = append(what, "patterns")
|
|
}
|
|
if oc.Hits != n.cfg.Hits || oc.Within != n.cfg.Within {
|
|
what = append(what, fmt.Sprintf("bounty %d in %s", n.cfg.Hits, n.cfg.Within))
|
|
}
|
|
if !reflect.DeepEqual(oc.Ban, n.cfg.Ban) {
|
|
what = append(what, "ban")
|
|
}
|
|
w.Tracker.Set(n.cfg.Hits, n.cfg.Within.D())
|
|
w.mu.Lock()
|
|
w.Cfg, w.Matcher = n.cfg, n.m
|
|
w.mu.Unlock()
|
|
if sourceKey(oc) != sourceKey(n.cfg) {
|
|
w.stop()
|
|
stopped = append(stopped, w.done)
|
|
w.Source = d.newSource(n.cfg)
|
|
d.start(w)
|
|
what = append(what, "source (started again)")
|
|
}
|
|
if len(what) == 0 {
|
|
what = append(what, "changed")
|
|
}
|
|
res.Changes = append(res.Changes, "watch "+n.cfg.Name+": "+strings.Join(what, ", "))
|
|
}
|
|
for _, w := range d.watches {
|
|
if byName[strings.ToLower(w.Cfg.Name)] == w {
|
|
w.stop()
|
|
stopped = append(stopped, w.done)
|
|
res.Changes = append(res.Changes, "watch "+w.Cfg.Name+": removed")
|
|
}
|
|
}
|
|
d.watches = watches
|
|
d.cfg = cfg
|
|
d.mu.Unlock()
|
|
|
|
// A stopped watch may be handling a line, which needs d.mu.
|
|
for _, done := range stopped {
|
|
<-done
|
|
}
|
|
d.loadGroups(d.ctx)
|
|
if len(res.Changes) == 0 {
|
|
res.Changes = []string{}
|
|
}
|
|
d.log.Info("reloaded", "changes", strings.Join(res.Changes, "; "))
|
|
if len(res.Restart) > 0 {
|
|
d.log.Warn("changed settings that wait for a restart", "settings", strings.Join(res.Restart, ", "))
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// sourceKey is what a watch's source is made from: when it changes, the
|
|
// source starts again.
|
|
func sourceKey(w *config.Watch) string {
|
|
j := ""
|
|
if w.Journal != nil {
|
|
j = fmt.Sprint(w.Journal.Unit, w.Journal.Identifier)
|
|
}
|
|
return fmt.Sprint(w.Kind(), "|", w.File, "|", j, "|", w.Source, "|", w.Protocol, "|", w.Shadow)
|
|
}
|