diff --git a/agent/internal/sync/sync.go b/agent/internal/sync/sync.go index 0a7213f..1f6ca3b 100644 --- a/agent/internal/sync/sync.go +++ b/agent/internal/sync/sync.go @@ -412,13 +412,55 @@ func handleUpdateAgentWindows(cmd *pb.ServerCommand) { } logPath := filepath.Join(os.TempDir(), "vantage-agent-msi.log") - log.Printf("launching msiexec for upgrade to v%s (cmd=%s)", u.Version, cmd.CommandId) - up := exec.Command("cmd", "/c", "start", "", "/wait", "msiexec", "/i", msiPath, "/qn", "/norestart", "/l*v", logPath) - if err := up.Start(); err != nil { + // The MSI stops the vantage-agent service as part of the upgrade. Anything + // descended from this process is killed with it, so msiexec must not be a + // child: run it from a scheduled task, which is parented to the Task + // Scheduler service instead. + if err := launchDetachedUpdate(msiPath, logPath, cmd.CommandId); err != nil { log.Printf("failed to launch msiexec (cmd=%s): %v", cmd.CommandId, err) return } + log.Printf("scheduled msiexec for upgrade to v%s (cmd=%s)", u.Version, cmd.CommandId) +} + +const updateTaskName = "VantageAgentUpdate" + +func launchDetachedUpdate(msiPath, logPath, commandID string) error { + scriptPath := filepath.Join(os.TempDir(), "vantage-agent-update.cmd") + script := fmt.Sprintf("@echo off\r\n"+ + "timeout /t 5 /nobreak >nul\r\n"+ + "msiexec /i \"%s\" /qn /norestart /l*v \"%s\"\r\n"+ + "schtasks /delete /tn %s /f >nul 2>&1\r\n"+ + "del /f /q \"%s\" >nul 2>&1\r\n"+ + "(goto) 2>nul & del /f /q \"%%~f0\"\r\n", + msiPath, logPath, updateTaskName, msiPath) + if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil { + return fmt.Errorf("write update script: %w", err) + } + + // Stale task from a previous attempt would make /create fail even with /f + // if it is still running, so tear it down first and ignore the result. + exec.Command("schtasks", "/end", "/tn", updateTaskName).Run() + exec.Command("schtasks", "/delete", "/tn", updateTaskName, "/f").Run() + + create := exec.Command("schtasks", "/create", + "/tn", updateTaskName, + "/tr", `"`+scriptPath+`"`, + "/sc", "once", + // Already in the past: the task never fires on its own, only via /run. + "/st", "00:00", + "/ru", "SYSTEM", + "/rl", "HIGHEST", + "/f") + if out, err := create.CombinedOutput(); err != nil { + return fmt.Errorf("schtasks create: %v: %s", err, strings.TrimSpace(string(out))) + } + + if out, err := exec.Command("schtasks", "/run", "/tn", updateTaskName).CombinedOutput(); err != nil { + return fmt.Errorf("schtasks run: %v: %s", err, strings.TrimSpace(string(out))) + } + return nil } func downloadFile(url, dest string) error {