mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
Hotfix: Auto update capability.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -61,10 +72,11 @@ const reloadPage = () => {
|
||||
<div v-else-if="updateStatus.state === 'done'" class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/20 border border-green-500/50 text-green-400 hover:bg-green-500/30 transition-colors text-sm"
|
||||
@click="reloadPage"
|
||||
:disabled="isRestarting"
|
||||
@click="restartWorker"
|
||||
>
|
||||
<i class="fas fa-check-circle" />
|
||||
<span>Restart</span>
|
||||
<i :class="isRestarting ? 'fas fa-spinner animate-spin' : 'fas fa-check-circle'" />
|
||||
<span>{{ isRestarting ? 'Restarting...' : 'Restart' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +127,7 @@ const reloadPage = () => {
|
||||
<i class="fas fa-times" />
|
||||
</button>
|
||||
|
||||
<div class="text-center mb-6">
|
||||
<div class="text-center mb-4">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
|
||||
<i class="fas fa-arrow-circle-up text-3xl text-white" />
|
||||
</div>
|
||||
@@ -125,6 +137,14 @@ const reloadPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Release Notes -->
|
||||
<div v-if="updateInfo?.release_notes" class="mb-4 max-h-48 overflow-y-auto">
|
||||
<p class="text-slate-500 text-xs uppercase tracking-wide mb-2">What's new</p>
|
||||
<div class="bg-slate-800/50 rounded-lg p-3 text-sm text-slate-300 whitespace-pre-wrap leading-relaxed">
|
||||
{{ updateInfo.release_notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
class="w-full py-3 rounded-xl bg-amber-500 text-slate-900 font-semibold hover:bg-amber-400 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user