108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
"github.com/qianfree/team-api/internal/consts"
|
|
)
|
|
|
|
// Rollback rolls back to the previous version
|
|
func Rollback(ctx context.Context) error {
|
|
if IsDocker() {
|
|
return fmt.Errorf("rollback is not supported in Docker mode")
|
|
}
|
|
|
|
// Windows 无 exec 换壳语义,且运行中二进制无法被重命名,显式拒绝
|
|
if runtime.GOOS == "windows" {
|
|
return fmt.Errorf("rollback is not supported on Windows")
|
|
}
|
|
|
|
if manager.IsUpdating() {
|
|
return fmt.Errorf("cannot rollback while update is in progress")
|
|
}
|
|
|
|
// Read rollback info
|
|
info, err := loadRollbackInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("no rollback information available: %w", err)
|
|
}
|
|
|
|
// Verify backup file exists
|
|
if _, err := os.Stat(info.BackupPath); err != nil {
|
|
return fmt.Errorf("backup file not found: %s", info.BackupPath)
|
|
}
|
|
|
|
g.Log().Infof(ctx, "Rolling back from %s to %s using backup %s",
|
|
consts.Version, info.BackupVersion, info.BackupPath)
|
|
|
|
// Current executable path
|
|
currentExe, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get current executable path: %w", err)
|
|
}
|
|
currentExe, err = filepath.EvalSymlinks(currentExe)
|
|
if err != nil {
|
|
g.Log().Warningf(ctx, "Failed to resolve symlink: %v", err)
|
|
}
|
|
|
|
// Replace current binary with backup
|
|
oldPath := currentExe + ".old"
|
|
_ = os.Remove(oldPath)
|
|
|
|
if err := os.Rename(currentExe, oldPath); err != nil {
|
|
return fmt.Errorf("failed to rename current binary: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(info.BackupPath, currentExe); err != nil {
|
|
// Try to restore
|
|
_ = os.Rename(oldPath, currentExe)
|
|
return fmt.Errorf("failed to restore backup: %w", err)
|
|
}
|
|
|
|
_ = os.Chmod(currentExe, 0755)
|
|
|
|
// 记录待重启的旧版本路径:退出链末尾用 syscall.Exec 原地换壳回到旧版本,
|
|
// 同 PID 继续运行,不依赖外部进程管理器拉起(见 gracefulExit 注释)
|
|
manager.SetRestartBinary(currentExe)
|
|
|
|
// Clean up
|
|
_ = os.Remove(filepath.Join(updateDir, rollbackFile))
|
|
_ = os.Remove(filepath.Join(updateDir, pendingVerificationFile))
|
|
_ = os.Remove(oldPath)
|
|
|
|
g.Log().Infof(ctx, "Rollback to %s complete, scheduling restart...", info.BackupVersion)
|
|
|
|
// 走 SIGTERM 优雅退出(见 gracefulExit 注释):先让 handler 把"回滚已启动"
|
|
// 响应写出去,再触发优雅关闭链路排空任务池/异步 Writer 后自然退出,
|
|
// 由进程管理器(systemd/supervisor 等)拉起旧版本
|
|
gracefulExit(ctx, "rollback complete")
|
|
return nil
|
|
}
|
|
|
|
// loadRollbackInfo reads rollback metadata from disk
|
|
func loadRollbackInfo() (*RollbackInfo, error) {
|
|
data, err := os.ReadFile(filepath.Join(updateDir, rollbackFile))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var info RollbackInfo
|
|
if err := json.Unmarshal(data, &info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
// GetRollbackInfo returns the current rollback information (if any)
|
|
func GetRollbackInfo() (*RollbackInfo, error) {
|
|
return loadRollbackInfo()
|
|
}
|