package updates import ( "context" "os/exec" "runtime" "sync" "time" "github.com/tevino/abool" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" ) const ( // RestartExitCode will instruct portmaster-start to restart the process immediately, potentially with a new version. RestartExitCode = 23 ) var ( // RebootOnRestart defines whether the whole system, not just the service, // should be restarted automatically when triggering a restart internally. RebootOnRestart bool restartTask *modules.Task restartPending = abool.New() restartTriggered = abool.New() restartTime time.Time restartTimeLock sync.Mutex ) // IsRestarting returns whether a restart has been triggered. func IsRestarting() bool { return restartTriggered.IsSet() } // RestartIsPending returns whether a restart is pending. func RestartIsPending() (pending bool, restartAt time.Time) { if restartPending.IsNotSet() { return false, time.Time{} } restartTimeLock.Lock() defer restartTimeLock.Unlock() return true, restartTime } // DelayedRestart triggers a restart of the application by shutting down the // module system gracefully and returning with RestartExitCode. The restart // may be further delayed by up to 10 minutes by the internal task scheduling // system. This only works if the process is managed by portmaster-start. func DelayedRestart(delay time.Duration) { // Check if restart is already pending. if !restartPending.SetToIf(false, true) { return } // Schedule the restart task. log.Warningf("updates: restart triggered, will execute in %s", delay) restartAt := time.Now().Add(delay) restartTask.Schedule(restartAt) // Set restartTime. restartTimeLock.Lock() defer restartTimeLock.Unlock() restartTime = restartAt } // AbortRestart aborts a (delayed) restart. func AbortRestart() { if restartPending.SetToIf(true, false) { log.Warningf("updates: restart aborted") // Cancel schedule. restartTask.Schedule(time.Time{}) } } // TriggerRestartIfPending triggers an automatic restart, if one is pending. // This can be used to prepone a scheduled restart if the conditions are preferable. func TriggerRestartIfPending() { if restartPending.IsSet() { restartTask.StartASAP() } } // RestartNow immediately executes a restart. // This only works if the process is managed by portmaster-start. func RestartNow() { restartPending.Set() restartTask.StartASAP() } func automaticRestart(_ context.Context, _ *modules.Task) error { // Check if the restart is still scheduled. if restartPending.IsNotSet() { return nil } // Trigger restart. if restartTriggered.SetToIf(false, true) { log.Warning("updates: initiating (automatic) restart") // 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") } } // Set restart exit code. if !rebooting { modules.SetExitStatusCode(RestartExitCode) } // Do not use a worker, as this would block itself here. go modules.Shutdown() //nolint:errcheck } return nil } 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 }