cli/internal/bugseti/updater/service.go
Claude 23b82482f2 refactor: rename module from github.com/host-uk/core to forge.lthn.ai/core/cli
Move module identity to our own Forgejo instance. All import paths
updated across 434 Go files, sub-module go.mod files, and go.work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:53:52 +00:00

322 lines
6.9 KiB
Go

// Package updater provides auto-update functionality for BugSETI.
package updater
import (
"context"
"log"
"sync"
"time"
"forge.lthn.ai/core/cli/internal/bugseti"
)
// Service provides update functionality and Wails bindings.
type Service struct {
config *bugseti.ConfigService
checker *Checker
downloader *Downloader
installer *Installer
mu sync.RWMutex
lastResult *UpdateCheckResult
pendingUpdate *DownloadResult
// Background check
stopCh chan struct{}
running bool
}
// NewService creates a new update service.
func NewService(config *bugseti.ConfigService) (*Service, error) {
downloader, err := NewDownloader()
if err != nil {
return nil, err
}
installer, err := NewInstaller()
if err != nil {
return nil, err
}
return &Service{
config: config,
checker: NewChecker(),
downloader: downloader,
installer: installer,
}, nil
}
// ServiceName returns the service name for Wails.
func (s *Service) ServiceName() string {
return "UpdateService"
}
// Start begins the background update checker.
func (s *Service) Start() {
s.mu.Lock()
if s.running {
s.mu.Unlock()
return
}
s.running = true
s.stopCh = make(chan struct{})
s.mu.Unlock()
go s.runBackgroundChecker()
}
// Stop stops the background update checker.
func (s *Service) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.running {
return
}
s.running = false
close(s.stopCh)
}
// runBackgroundChecker runs periodic update checks.
func (s *Service) runBackgroundChecker() {
// Initial check after a short delay
time.Sleep(30 * time.Second)
for {
select {
case <-s.stopCh:
return
default:
}
if s.config.ShouldCheckForUpdates() {
log.Println("Checking for updates...")
_, err := s.CheckForUpdate()
if err != nil {
log.Printf("Update check failed: %v", err)
}
}
// Check interval from config (minimum 1 hour)
interval := time.Duration(s.config.GetUpdateCheckInterval()) * time.Hour
if interval < time.Hour {
interval = time.Hour
}
select {
case <-s.stopCh:
return
case <-time.After(interval):
}
}
}
// GetSettings returns the update settings.
func (s *Service) GetSettings() bugseti.UpdateSettings {
return s.config.GetUpdateSettings()
}
// SetSettings updates the update settings.
func (s *Service) SetSettings(settings bugseti.UpdateSettings) error {
return s.config.SetUpdateSettings(settings)
}
// GetVersionInfo returns the current version information.
func (s *Service) GetVersionInfo() bugseti.VersionInfo {
return bugseti.GetVersionInfo()
}
// GetChannels returns all available update channels.
func (s *Service) GetChannels() []ChannelInfo {
return GetAllChannelInfo()
}
// CheckForUpdate checks if an update is available.
func (s *Service) CheckForUpdate() (*UpdateCheckResult, error) {
currentVersion := bugseti.GetVersion()
channelStr := s.config.GetUpdateChannel()
channel, err := ParseChannel(channelStr)
if err != nil {
channel = ChannelStable
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := s.checker.CheckForUpdate(ctx, currentVersion, channel)
if err != nil {
return result, err
}
// Update last check time
s.config.SetLastUpdateCheck(time.Now())
// Store result
s.mu.Lock()
s.lastResult = result
s.mu.Unlock()
// If auto-update is enabled and an update is available, download it
if result.Available && s.config.IsAutoUpdateEnabled() {
go s.downloadUpdate(result.Release)
}
return result, nil
}
// GetLastCheckResult returns the last update check result.
func (s *Service) GetLastCheckResult() *UpdateCheckResult {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lastResult
}
// downloadUpdate downloads an update in the background.
func (s *Service) downloadUpdate(release *ReleaseInfo) {
if release == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
log.Printf("Downloading update %s...", release.Version)
result, err := s.downloader.Download(ctx, release)
if err != nil {
log.Printf("Failed to download update: %v", err)
return
}
log.Printf("Update %s downloaded and staged at %s", release.Version, result.BinaryPath)
s.mu.Lock()
s.pendingUpdate = result
s.mu.Unlock()
}
// DownloadUpdate downloads the latest available update.
func (s *Service) DownloadUpdate() (*DownloadResult, error) {
s.mu.RLock()
lastResult := s.lastResult
s.mu.RUnlock()
if lastResult == nil || !lastResult.Available || lastResult.Release == nil {
// Need to check first
result, err := s.CheckForUpdate()
if err != nil {
return nil, err
}
if !result.Available {
return nil, nil
}
lastResult = result
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
downloadResult, err := s.downloader.Download(ctx, lastResult.Release)
if err != nil {
return nil, err
}
s.mu.Lock()
s.pendingUpdate = downloadResult
s.mu.Unlock()
return downloadResult, nil
}
// InstallUpdate installs a previously downloaded update.
func (s *Service) InstallUpdate() (*InstallResult, error) {
s.mu.RLock()
pending := s.pendingUpdate
s.mu.RUnlock()
if pending == nil {
// Try to download first
downloadResult, err := s.DownloadUpdate()
if err != nil {
return nil, err
}
if downloadResult == nil {
return &InstallResult{
Success: false,
Error: "No update available",
}, nil
}
pending = downloadResult
}
result, err := s.installer.Install(pending.BinaryPath)
if err != nil {
return result, err
}
// Clear pending update
s.mu.Lock()
s.pendingUpdate = nil
s.mu.Unlock()
return result, nil
}
// InstallAndRestart installs the update and restarts the application.
func (s *Service) InstallAndRestart() error {
result, err := s.InstallUpdate()
if err != nil {
return err
}
if !result.Success {
return nil
}
return s.installer.Restart()
}
// HasPendingUpdate returns true if there's a downloaded update ready to install.
func (s *Service) HasPendingUpdate() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.pendingUpdate != nil
}
// GetPendingUpdate returns information about the pending update.
func (s *Service) GetPendingUpdate() *DownloadResult {
s.mu.RLock()
defer s.mu.RUnlock()
return s.pendingUpdate
}
// CancelPendingUpdate cancels and removes the pending update.
func (s *Service) CancelPendingUpdate() error {
s.mu.Lock()
defer s.mu.Unlock()
s.pendingUpdate = nil
return s.downloader.Cleanup()
}
// CanSelfUpdate returns true if the application can update itself.
func (s *Service) CanSelfUpdate() bool {
return CanSelfUpdate()
}
// NeedsElevation returns true if the update requires elevated privileges.
func (s *Service) NeedsElevation() bool {
return NeedsElevation()
}
// Rollback restores the previous version.
func (s *Service) Rollback() error {
return s.installer.Rollback()
}
// CleanupAfterUpdate cleans up backup files after a successful update.
func (s *Service) CleanupAfterUpdate() error {
return s.installer.CleanupBackup()
}