safing-portmaster/cmds/cmdbase/service.go
2024-11-27 16:16:15 +01:00

185 lines
4.2 KiB
Go

package cmdbase
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"runtime"
"runtime/pprof"
"time"
"github.com/spf13/cobra"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service"
"github.com/safing/portmaster/service/mgr"
)
var (
RebootOnRestart bool
PrintStackOnExit bool
)
type SystemService interface {
Run()
IsService() bool
RestartService() error
}
type ServiceInstance interface {
Ready() bool
Start() error
Stop() error
Restart()
Shutdown()
Ctx() context.Context
IsShuttingDown() bool
ShuttingDown() <-chan struct{}
ShutdownCtx() context.Context
IsShutDown() bool
ShutdownComplete() <-chan struct{}
ExitCode() int
ShouldRestartIsSet() bool
CommandLineOperationIsSet() bool
CommandLineOperationExecute() error
}
var (
SvcFactory func(*service.ServiceConfig) (ServiceInstance, error)
SvcConfig *service.ServiceConfig
)
func RunService(cmd *cobra.Command, args []string) {
if SvcFactory == nil || SvcConfig == nil {
fmt.Fprintln(os.Stderr, "internal error: service not set up in cmdbase")
os.Exit(1)
}
// Start logging.
// Note: Must be created before the service instance, so that they use the right logger.
err := log.Start(SvcConfig.LogLevel, SvcConfig.LogToStdout, SvcConfig.LogDir)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(4)
}
// Create instance.
// Instance modules might request a cmdline execution of a function.
var execCmdLine bool
instance, err := SvcFactory(SvcConfig)
switch {
case err == nil:
// Continue
case errors.Is(err, mgr.ErrExecuteCmdLineOp):
execCmdLine = true
default:
fmt.Printf("error creating an instance: %s\n", err)
os.Exit(2)
}
// Execute module command line operation, if requested or available.
switch {
case !execCmdLine:
// Run service.
case !instance.CommandLineOperationIsSet():
fmt.Println("command line operation execution requested, but not set")
os.Exit(3)
default:
// Run the function and exit.
fmt.Println("executing cmdline op")
err = instance.CommandLineOperationExecute()
if err != nil {
fmt.Fprintf(os.Stderr, "command line operation failed: %s\n", err)
os.Exit(3)
}
os.Exit(0)
}
// START
// Create system service.
service := NewSystemService(instance)
// Start instance via system service manager.
go func() {
service.Run()
}()
// SHUTDOWN
// Wait for shutdown to be started.
<-instance.ShuttingDown()
// Wait for shutdown to be finished.
select {
case <-instance.ShutdownComplete():
// Print stack on shutdown, if enabled.
if PrintStackOnExit {
printStackTo(log.GlobalWriter, "PRINTING STACK ON EXIT")
}
case <-time.After(3 * time.Minute):
printStackTo(log.GlobalWriter, "PRINTING STACK - TAKING TOO LONG FOR SHUTDOWN")
}
// Check if restart was triggered and send start service command if true.
if instance.ShouldRestartIsSet() && service.IsService() {
// Check if we should reboot instead.
var rebooting bool
if RebootOnRestart {
// Trigger system reboot and record success.
rebooting = triggerSystemReboot()
if !rebooting {
log.Warningf("updates: rebooting failed, only restarting service instead")
}
}
// Restart service if not rebooting.
if !rebooting {
if err := service.RestartService(); err != nil {
slog.Error("failed to restart service", "err", err)
}
}
}
// Stop logging.
log.Shutdown()
// Give a small amount of time for everything to settle:
// - All logs written.
// - Restart command started, if needed.
// - Windows service manager notified.
time.Sleep(100 * time.Millisecond)
// Exit
os.Exit(instance.ExitCode())
}
func printStackTo(writer io.Writer, msg string) {
_, err := fmt.Fprintf(writer, "===== %s =====\n", msg)
if err == nil {
err = pprof.Lookup("goroutine").WriteTo(writer, 1)
}
if err != nil {
slog.Error("failed to write stack trace", "err", err)
}
}
func triggerSystemReboot() (success bool) {
switch runtime.GOOS {
case "linux":
err := exec.Command("systemctl", "reboot").Run()
if err != nil {
log.Errorf("updates: triggering reboot with systemctl failed: %s", err)
return false
}
default:
log.Warningf("updates: rebooting is not support on %s", runtime.GOOS)
return false
}
return true
}