package update import ( "fmt" "io" "log" "net/http" "os" "path/filepath" "runtime" "time" ) // Updater handles self-update operations for the companion agent type Updater struct { currentVersion string } // NewUpdater creates a new updater instance func NewUpdater(currentVersion string) *Updater { return &Updater{ currentVersion: currentVersion, } } // PerformUpdate downloads a new version and replaces the current binary func (u *Updater) PerformUpdate(downloadURL, newVersion string) error { log.Printf("Performing self-update from %s to %s", u.currentVersion, newVersion) log.Printf("Download URL: %s", downloadURL) // Get current executable path exePath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) } log.Printf("Current executable: %s", exePath) // Create temporary file for download tmpFile := exePath + ".new" // Download new binary if err := u.downloadBinary(downloadURL, tmpFile); err != nil { return fmt.Errorf("failed to download update: %w", err) } // Make new binary executable (Unix only) if runtime.GOOS != "windows" { if err := os.Chmod(tmpFile, 0755); err != nil { os.Remove(tmpFile) return fmt.Errorf("failed to make binary executable: %w", err) } } // Create backup of current binary backupFile := exePath + ".backup" if err := u.createBackup(exePath, backupFile); err != nil { os.Remove(tmpFile) return fmt.Errorf("failed to create backup: %w", err) } log.Println("Backup created successfully") // Replace current binary with new one if err := u.replaceBinary(tmpFile, exePath); err != nil { // Attempt to restore from backup log.Printf("Update failed, attempting to restore from backup: %v", err) if restoreErr := u.replaceBinary(backupFile, exePath); restoreErr != nil { return fmt.Errorf("update failed and backup restoration failed: %w (original error: %v)", restoreErr, err) } return fmt.Errorf("update failed, restored from backup: %w", err) } log.Printf("Successfully updated to version %s", newVersion) log.Println("NOTE: Restart the agent to use the new version") // Clean up backup file after successful update os.Remove(backupFile) return nil } // downloadBinary downloads a binary from the given URL to the destination path func (u *Updater) downloadBinary(url, destPath string) error { log.Printf("Downloading binary from %s", url) // Create HTTP client with timeout client := &http.Client{ Timeout: 10 * time.Minute, // Binary download may take longer } // Download file resp, err := client.Get(url) if err != nil { return fmt.Errorf("failed to download: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("download failed with status: %d", resp.StatusCode) } // Create destination file file, err := os.Create(destPath) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer file.Close() // Copy content written, err := io.Copy(file, resp.Body) if err != nil { return fmt.Errorf("failed to write file: %w", err) } log.Printf("Downloaded %d bytes", written) return nil } // createBackup creates a backup copy of a file func (u *Updater) createBackup(src, dest string) error { srcFile, err := os.Open(src) if err != nil { return err } defer srcFile.Close() destFile, err := os.Create(dest) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, srcFile) return err } // replaceBinary replaces the destination binary with the source binary func (u *Updater) replaceBinary(src, dest string) error { // On Windows, we can't replace a running executable directly // We need to rename the old one and move the new one in place if runtime.GOOS == "windows" { oldExe := dest + ".old" // Remove any existing .old file os.Remove(oldExe) // Rename current executable if err := os.Rename(dest, oldExe); err != nil { return fmt.Errorf("failed to rename current executable: %w", err) } // Move new executable into place if err := os.Rename(src, dest); err != nil { // Try to restore os.Rename(oldExe, dest) return fmt.Errorf("failed to move new executable: %w", err) } // Schedule old executable for deletion on next boot // (Windows doesn't allow deleting running executables) return nil } // On Unix, we can replace the file directly // The running process will continue using the old inode return os.Rename(src, dest) } // GetCurrentVersion returns the current version func (u *Updater) GetCurrentVersion() string { return u.currentVersion } // VerifyUpdate verifies that an update was successful by checking the version func (u *Updater) VerifyUpdate(expectedVersion string) error { exePath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) } info, err := os.Stat(exePath) if err != nil { return fmt.Errorf("failed to stat executable: %w", err) } log.Printf("Executable info: size=%d, mod_time=%s", info.Size(), info.ModTime()) // In a real implementation, you might embed version info in the binary // or verify a checksum. For now, we just verify the file exists and was modified recently if time.Since(info.ModTime()) > 5*time.Minute { return fmt.Errorf("executable was not recently modified") } return nil } // CleanupOldVersions removes old backup files func (u *Updater) CleanupOldVersions() error { exePath, err := os.Executable() if err != nil { return err } dir := filepath.Dir(exePath) baseName := filepath.Base(exePath) // Remove backup files patterns := []string{ baseName + ".backup", baseName + ".old", baseName + ".new", } for _, pattern := range patterns { path := filepath.Join(dir, pattern) if err := os.Remove(path); err != nil && !os.IsNotExist(err) { log.Printf("Failed to remove old version %s: %v", path, err) } } return nil }