diff --git a/internal/update/update.go b/internal/update/update.go index dc88597..8fcf439 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -28,6 +28,7 @@ const ( ReleasesAPI = "https://api.github.com/repos/" + GitHubRepo + "/releases/latest" CheckInterval = 24 * time.Hour MaxExtractedSize = 100 * 1024 * 1024 // 100MB max per extracted file + RestartDelay = 500 * time.Millisecond ) // Release represents a GitHub release. @@ -281,6 +282,12 @@ func (u *Updater) ApplyUpdate(ctx context.Context, info *UpdateInfo) error { return fmt.Errorf("failed to replace binaries: %w", err) } + // Clear cache so next check shows no update available + u.mu.Lock() + u.cachedUpdate = nil + u.lastCheck = time.Time{} + u.mu.Unlock() + u.setStatus("done", 1.0, fmt.Sprintf("Updated to v%s. Restart required.", info.LatestVersion)) log.Info().Str("version", info.LatestVersion).Msg("Update applied successfully") @@ -553,3 +560,35 @@ func isNewerVersion(latest, current string) bool { return len(latestParts) > len(currentParts) } + +// Restart spawns the new worker binary and exits the current process. +// This should be called after a successful update to apply the new version. +func (u *Updater) Restart() error { + workerPath := filepath.Join(u.installDir, "worker") + + // Verify the new binary exists + if _, err := os.Stat(workerPath); err != nil { + return fmt.Errorf("new worker binary not found: %w", err) + } + + log.Info().Str("path", workerPath).Msg("Restarting worker with new binary") + + // Start the new process + cmd := exec.Command(workerPath) // #nosec G204 -- workerPath is from internal installDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start new worker: %w", err) + } + + // Give the new process time to start + time.Sleep(RestartDelay) + + // Exit current process - the new one is now running + log.Info().Int("new_pid", cmd.Process.Pid).Msg("New worker started, exiting old process") + os.Exit(0) + + return nil // Never reached +} diff --git a/internal/worker/handlers.go b/internal/worker/handlers.go index ad3d35b..6a8f7b6 100644 --- a/internal/worker/handlers.go +++ b/internal/worker/handlers.go @@ -750,3 +750,30 @@ func (s *Service) handleUpdateStatus(w http.ResponseWriter, r *http.Request) { status := s.updater.GetStatus() writeJSON(w, status) } + +// handleUpdateRestart restarts the worker with the new binary. +func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) { + status := s.updater.GetStatus() + if status.State != "done" { + http.Error(w, "no update has been applied", http.StatusBadRequest) + return + } + + // Send response before restarting + writeJSON(w, map[string]interface{}{ + "success": true, + "message": "Restarting worker...", + }) + + // Flush the response + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + // Restart in background after response is sent + go func() { + if err := s.updater.Restart(); err != nil { + log.Error().Err(err).Msg("Failed to restart worker") + } + }() +} diff --git a/internal/worker/service.go b/internal/worker/service.go index 4f14348..14d65d2 100644 --- a/internal/worker/service.go +++ b/internal/worker/service.go @@ -600,6 +600,7 @@ func (s *Service) setupRoutes() { s.router.Get("/api/update/check", s.handleUpdateCheck) s.router.Post("/api/update/apply", s.handleUpdateApply) s.router.Get("/api/update/status", s.handleUpdateStatus) + s.router.Post("/api/update/restart", s.handleUpdateRestart) // SSE endpoint (works before DB is ready) s.router.Get("/api/events", s.sseBroadcaster.HandleSSE) diff --git a/ui/src/components/Header.vue b/ui/src/components/Header.vue index 237fb3c..75aeea7 100644 --- a/ui/src/components/Header.vue +++ b/ui/src/components/Header.vue @@ -16,9 +16,20 @@ const emit = defineEmits<{ }>() const showUpdateModal = ref(false) +const isRestarting = ref(false) -const reloadPage = () => { - globalThis.location.reload() +const restartWorker = async () => { + isRestarting.value = true + try { + await fetch('/api/update/restart', { method: 'POST' }) + // Wait a moment for the new process to start, then reload + setTimeout(() => { + globalThis.location.reload() + }, 2000) + } catch (error) { + console.error('Failed to restart:', error) + isRestarting.value = false + } } @@ -61,10 +72,11 @@ const reloadPage = () => {
What's new
+