From 442b939046917e07559576b7698cb58e13dbc504 Mon Sep 17 00:00:00 2001 From: Torben Schmitz Date: Tue, 27 Aug 2024 06:20:55 -0700 Subject: [PATCH] Implement config reloading The approach here is to pass a channel to InnerMain functions that can be watched for incoming `syscall.SIGHUP` signals. To trigger a config reload, run - `service fleetspeak-client reload` on linux - `launchctl kill SIGHUP system/com.google.code.fleetspeak` on mac - `sc.exe control FleetspeakService paramchange` on windows The default implementation provided simply stops the client and creates another one when a signal is received. PiperOrigin-RevId: 667959949 --- cmd/fleetspeak_client/fleetspeak_client.go | 45 ++++++++++++------- .../debian/fleetspeak-client.service | 1 + fleetspeak/src/client/entry/entry.go | 8 +++- fleetspeak/src/client/entry/entry_unix.go | 7 ++- fleetspeak/src/client/entry/entry_windows.go | 14 ++++-- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/cmd/fleetspeak_client/fleetspeak_client.go b/cmd/fleetspeak_client/fleetspeak_client.go index 9ed1988c..7991635b 100644 --- a/cmd/fleetspeak_client/fleetspeak_client.go +++ b/cmd/fleetspeak_client/fleetspeak_client.go @@ -6,6 +6,7 @@ import ( "fmt" "os" + log "github.com/golang/glog" "google.golang.org/protobuf/encoding/prototext" "github.com/google/fleetspeak/fleetspeak/src/client" @@ -24,18 +25,39 @@ import ( var configFile = flag.String("config", "", "Client configuration file, required.") -func innerMain(ctx context.Context) error { +func innerMain(ctx context.Context, cfgReloadSignals <-chan os.Signal) error { + for { + cl, err := createClient() + if err != nil { + return fmt.Errorf("error starting client: %v", err) + } + + select { + case <-cfgReloadSignals: + // We implement config reloading by tearing down the client and creating a + // new one. + log.Info("Config reload requested") + cl.Stop() + continue + case <-ctx.Done(): + cl.Stop() + return nil + } + } +} + +func createClient() (*client.Client, error) { b, err := os.ReadFile(*configFile) if err != nil { - return fmt.Errorf("unable to read configuration file %q: %v", *configFile, err) + return nil, fmt.Errorf("unable to read configuration file %q: %v", *configFile, err) } cfgPB := &gpb.Config{} if err := prototext.Unmarshal(b, cfgPB); err != nil { - return fmt.Errorf("unable to parse configuration file %q: %v", *configFile, err) + return nil, fmt.Errorf("unable to parse configuration file %q: %v", *configFile, err) } cfg, err := generic.MakeConfiguration(cfgPB) if err != nil { - return fmt.Errorf("error in configuration file: %v", err) + return nil, fmt.Errorf("error in configuration file: %v", err) } var com comms.Communicator @@ -45,7 +67,8 @@ func innerMain(ctx context.Context) error { com = &https.Communicator{} } - cl, err := client.New(cfg, + return client.New( + cfg, client.Components{ ServiceFactories: map[string]service.Factory{ "Daemon": daemonservice.Factory, @@ -55,16 +78,8 @@ func innerMain(ctx context.Context) error { }, Communicator: com, Stats: stats.NoopCollector{}, - }) - if err != nil { - return fmt.Errorf("error starting client: %v", err) - } - - select { - case <-ctx.Done(): - cl.Stop() - } - return nil + }, + ) } func main() { diff --git a/fleetspeak/client-pkg-tmpl/debian/fleetspeak-client.service b/fleetspeak/client-pkg-tmpl/debian/fleetspeak-client.service index 4e10f0d8..8746d3ce 100644 --- a/fleetspeak/client-pkg-tmpl/debian/fleetspeak-client.service +++ b/fleetspeak/client-pkg-tmpl/debian/fleetspeak-client.service @@ -6,6 +6,7 @@ Documentation=https://github.com/google/fleetspeak [Service] User=root ExecStart=/usr/bin/fleetspeak-client --config /etc/fleetspeak-client/client.config +ExecReload=kill -s HUP $MAINPID Restart=always KillMode=process diff --git a/fleetspeak/src/client/entry/entry.go b/fleetspeak/src/client/entry/entry.go index f8c44235..19b4d471 100644 --- a/fleetspeak/src/client/entry/entry.go +++ b/fleetspeak/src/client/entry/entry.go @@ -4,6 +4,7 @@ package entry import ( "context" + "os" "time" ) @@ -16,5 +17,8 @@ const shutdownTimeout = 10 * time.Second // InnerMain is an inner entry function responsible for creating a // [client.Client] and managing its configuration and lifecycle. It is called by // [RunMain] which handles platform-specific mechanics to manage the passed -// Context. -type InnerMain func(ctx context.Context) error +// [Context]. +// The [cfgReloadSignals] channel gets a [syscall.SIGHUP] when a config reload +// is requested. We use UNIX conventions here, the Windows layer can send a +// [syscall.SIGHUP] when appropriate. +type InnerMain func(ctx context.Context, cfgReloadSignals <-chan os.Signal) error diff --git a/fleetspeak/src/client/entry/entry_unix.go b/fleetspeak/src/client/entry/entry_unix.go index 55f8a011..7350203d 100644 --- a/fleetspeak/src/client/entry/entry_unix.go +++ b/fleetspeak/src/client/entry/entry_unix.go @@ -46,7 +46,12 @@ func RunMain(innerMain InnerMain, _ /* windowsServiceName */ string) { }, syscall.SIGUSR1) defer cancelSignal() - err := innerMain(ctx) + sighupCh := make(chan os.Signal, 1) + defer close(sighupCh) + signal.Notify(sighupCh, syscall.SIGHUP) + defer signal.Stop(sighupCh) + + err := innerMain(ctx, sighupCh) if err != nil { log.Exitf("Stopped due to unrecoverable error: %v", err) } diff --git a/fleetspeak/src/client/entry/entry_windows.go b/fleetspeak/src/client/entry/entry_windows.go index 13b118a0..97c05567 100644 --- a/fleetspeak/src/client/entry/entry_windows.go +++ b/fleetspeak/src/client/entry/entry_windows.go @@ -21,7 +21,7 @@ type fleetspeakService struct { } func (m *fleetspeakService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, errno uint32) { - const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange changes <- svc.Status{State: svc.StartPending} changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} tryDisableStderr() @@ -32,6 +32,9 @@ func (m *fleetspeakService) Execute(args []string, r <-chan svc.ChangeRequest, c enforceShutdownTimeout(ctx) + sighupCh := make(chan os.Signal, 1) + defer close(sighupCh) + wg := sync.WaitGroup{} wg.Add(1) go func() { @@ -51,6 +54,11 @@ func (m *fleetspeakService) Execute(args []string, r <-chan svc.ChangeRequest, c case svc.Stop, svc.Shutdown: cancel() return + case svc.ParamChange: + select { + case sighupCh <- syscall.SIGHUP: + default: + } default: log.Warningf("Unsupported control request: %v", c.Cmd) } @@ -58,7 +66,7 @@ func (m *fleetspeakService) Execute(args []string, r <-chan svc.ChangeRequest, c } }() - err := m.innerMain(ctx) + err := m.innerMain(ctx, sighupCh) cancel() wg.Wait() // Returning from this function tells Windows we're shutting down. Even if we @@ -78,7 +86,7 @@ func (m *fleetspeakService) ExecuteAsRegularProcess() { enforceShutdownTimeout(ctx) - err := m.innerMain(ctx) + err := m.innerMain(ctx, nil) if err != nil { log.Exitf("Stopped due to unrecoverable error: %v", err) }