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) }