fix(self-update): auto-restart live daemon after upgrade

Old isRunningAsDaemon() only matched "start" in argv — never true for
`unarr self-update`, so the daemon kept running the old binary in memory
and heartbeat reported the stale version (web gated features wrong).

Now: detect live daemon via state file + isDaemonAlive (PID alive +
heartbeat fresh), call runDaemonSvcRestart through the system service
manager. On failure show clear manual recovery command instead of
leaving the daemon dead. No-op when daemon is not running.
This commit is contained in:
Deivid Soto 2026-05-08 12:43:59 +02:00
parent 75df0e4308
commit 6ce743c39d

View file

@ -3,14 +3,11 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/exec"
"runtime"
"strings" "strings"
"syscall"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/upgrade" "github.com/torrentclaw/unarr/internal/upgrade"
) )
@ -23,7 +20,11 @@ func newSelfUpdateCmd() *cobra.Command {
Long: `Download and install the latest version of unarr. Long: `Download and install the latest version of unarr.
Checks GitHub for the latest release, verifies the checksum, and Checks GitHub for the latest release, verifies the checksum, and
replaces the current binary. A backup is kept at <binary>.backup.`, replaces the current binary. A backup is kept at <binary>.backup.
If the daemon is running, it is automatically restarted so the new
version is loaded into memory (otherwise heartbeat would keep
reporting the old version until a manual restart).`,
Example: ` unarr self-update Example: ` unarr self-update
unarr self-update --force`, unarr self-update --force`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
@ -40,12 +41,12 @@ func runSelfUpdate(force bool) error {
bold := color.New(color.Bold) bold := color.New(color.Bold)
green := color.New(color.FgGreen) green := color.New(color.FgGreen)
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
red := color.New(color.FgRed)
fmt.Println() fmt.Println()
bold.Println(" unarr self-update") bold.Println(" unarr self-update")
fmt.Println() fmt.Println()
// Check latest version
fmt.Print(" Checking latest version... ") fmt.Print(" Checking latest version... ")
ctx := context.Background() ctx := context.Background()
latest, err := upgrade.CheckLatest(ctx) latest, err := upgrade.CheckLatest(ctx)
@ -89,37 +90,25 @@ func runSelfUpdate(force bool) error {
if result.BackupPath != "" { if result.BackupPath != "" {
fmt.Printf(" Backup: %s\n", result.BackupPath) fmt.Printf(" Backup: %s\n", result.BackupPath)
} }
// Auto-restart daemon if it is running, otherwise the live process keeps
// serving the old version (heartbeat reports old version → web gates
// features against the wrong version).
if state := agent.ReadState(); state != nil && isDaemonAlive(state) {
fmt.Println()
fmt.Printf(" → Daemon running (PID %d), restarting to load new version...\n", state.PID)
if err := runDaemonSvcRestart(); err != nil {
fmt.Println()
red.Printf(" ✗ Auto-restart failed: %v\n", err)
fmt.Println(" The new binary is on disk but the daemon is still running the old version.")
fmt.Println(" Run manually: unarr daemon restart")
fmt.Println(" (If the daemon runs under a different user/session, restart it there.)")
fmt.Println() fmt.Println()
// If running as daemon, re-exec to restart with new binary
// For interactive use, just suggest restarting
if isRunningAsDaemon() {
fmt.Println(" Restarting daemon with new version...")
binPath, err := os.Executable()
if err != nil {
return fmt.Errorf("could not determine executable path: %w", err)
}
execErr := syscall.Exec(binPath, os.Args, os.Environ())
if execErr != nil && runtime.GOOS == "windows" {
// Windows doesn't support syscall.Exec — start new process
proc := exec.Command(binPath, os.Args[1:]...)
proc.Stdout = os.Stdout
proc.Stderr = os.Stderr
proc.Stdin = os.Stdin
return proc.Start()
}
return execErr
}
return nil return nil
} }
green.Println(" ✓ Daemon restarted")
}
func isRunningAsDaemon() bool { fmt.Println()
// Simple heuristic: check if "start" was in the original args return nil
for _, arg := range os.Args {
if arg == "start" {
return true
}
}
return false
} }