Files
OpenFlare/Wavelet/internal/apps/openflare/update/logics.go
T
2026-06-18 16:08:48 +08:00

263 lines
7.2 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package update
import (
"context"
"errors"
"fmt"
"runtime"
"strings"
"sync"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/admin/updater"
"github.com/Rain-kl/Wavelet/pkg/logger"
)
const (
channelStable = "stable"
channelPreview = "preview"
)
// UpgradeLogRecord is a single upgrade log entry for the legacy update API.
type UpgradeLogRecord struct {
Level string `json:"level"`
Message string `json:"message"`
CreatedAt time.Time `json:"created_at"`
}
// LatestReleaseView mirrors the legacy OpenFlare latest-release payload.
type LatestReleaseView struct {
TagName string `json:"tag_name"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
PublishedAt string `json:"published_at"`
Channel string `json:"channel"`
Prerelease bool `json:"prerelease"`
CurrentVersion string `json:"current_version"`
HasUpdate bool `json:"has_update"`
UpgradeSupported bool `json:"upgrade_supported"`
InProgress bool `json:"in_progress"`
UpgradeStatus string `json:"upgrade_status"`
UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"`
}
// StreamSnapshot is pushed over the upgrade logs websocket.
type StreamSnapshot struct {
InProgress bool `json:"in_progress"`
UpgradeStatus string `json:"upgrade_status"`
UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"`
}
type upgradeRequest struct {
Channel string `json:"channel"`
}
var upgradeState struct {
sync.Mutex
inProgress bool
status string
logs []UpgradeLogRecord
}
var upgradeSubscribers struct {
sync.Mutex
nextID int
listeners map[int]chan StreamSnapshot
}
func init() {
upgradeSubscribers.listeners = make(map[int]chan StreamSnapshot)
upgradeState.status = "idle"
}
func normalizeChannel(channel string) string {
switch strings.ToLower(strings.TrimSpace(channel)) {
case channelPreview:
return channelPreview
default:
return channelStable
}
}
func isDevBuild(version string) bool {
version = strings.TrimSpace(version)
return version == "" || strings.EqualFold(version, "dev")
}
func mapStatusToLatestRelease(status updater.Status, channel string) *LatestReleaseView {
inProgress, upgradeStatus, logs := snapshotUpgradeState()
view := &LatestReleaseView{
TagName: status.LatestVersion,
Body: status.ReleaseNotes,
HTMLURL: status.ReleaseURL,
PublishedAt: status.PublishedAt,
Channel: channel,
Prerelease: status.Prerelease,
CurrentVersion: status.CurrentVersion,
HasUpdate: status.UpdateAvailable,
UpgradeSupported: !isDevBuild(status.CurrentVersion) && runtime.GOOS != "windows",
InProgress: inProgress || updater.IsUpgrading(),
UpgradeStatus: upgradeStatus,
UpgradeLogs: logs,
}
if channel == channelPreview && status.Prerelease {
view.HasUpdate = !isDevBuild(status.CurrentVersion)
}
return view
}
// GetLatestRelease returns the newest upstream release for the requested channel.
func GetLatestRelease(ctx context.Context, channel string) (*LatestReleaseView, error) {
normalizedChannel := normalizeChannel(channel)
status, err := updater.GetStatus(ctx)
if err != nil {
return nil, err
}
return mapStatusToLatestRelease(status, normalizedChannel), nil
}
// ScheduleUpgrade downloads the latest release and restarts with the staged binary.
func ScheduleUpgrade(ctx context.Context, channel string) (*LatestReleaseView, error) {
normalizedChannel := normalizeChannel(channel)
upgradeState.Lock()
if upgradeState.inProgress || updater.IsUpgrading() {
upgradeState.Unlock()
return nil, errors.New("服务升级正在执行中,请稍后再试")
}
resetUpgradeLogsLocked()
upgradeState.inProgress = true
upgradeState.status = "running"
appendUpgradeLogLocked("info", fmt.Sprintf("Automatic upgrade scheduled for channel: %s.", normalizedChannel))
upgradeState.Unlock()
broadcastUpgradeSnapshot()
executable, stagedBinary, status, err := updater.PrepareUpgrade(ctx)
if err != nil {
recordUpgradeFailure(err)
return nil, err
}
view := mapStatusToLatestRelease(status, normalizedChannel)
view.InProgress = true
view.UpgradeStatus = "running"
view.UpgradeLogs = snapshotUpgradeLogs()
appendUpgradeLogLocked("info", fmt.Sprintf("Upgrade package prepared: %s.", status.LatestVersion))
broadcastUpgradeSnapshot()
go func() {
time.Sleep(time.Second)
if err := updater.ApplyPreparedUpgrade(executable, stagedBinary); err != nil {
updater.FinishUpgrade()
recordUpgradeFailure(err)
logger.ErrorF(context.Background(), "[Update] replace and restart failed: %v", err)
}
}()
return view, nil
}
// UploadManualBinary is disabled in the legacy OpenFlare server.
func UploadManualBinary() error {
return errors.New("手动升级功能已禁用")
}
// ConfirmManualUpgrade is disabled in the legacy OpenFlare server.
func ConfirmManualUpgrade() error {
return errors.New("手动升级功能已禁用")
}
// SubscribeUpgradeStream registers a listener for upgrade websocket snapshots.
func SubscribeUpgradeStream() (<-chan StreamSnapshot, func()) {
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
id := upgradeSubscribers.nextID
upgradeSubscribers.nextID++
ch := make(chan StreamSnapshot, 1)
upgradeSubscribers.listeners[id] = ch
unsubscribe := func() {
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
if listener, ok := upgradeSubscribers.listeners[id]; ok {
delete(upgradeSubscribers.listeners, id)
close(listener)
}
}
select {
case ch <- currentUpgradeSnapshot():
default:
}
return ch, unsubscribe
}
func snapshotUpgradeState() (bool, string, []UpgradeLogRecord) {
upgradeState.Lock()
defer upgradeState.Unlock()
return upgradeState.inProgress || updater.IsUpgrading(), upgradeState.status, cloneUpgradeLogsLocked()
}
func snapshotUpgradeLogs() []UpgradeLogRecord {
upgradeState.Lock()
defer upgradeState.Unlock()
return cloneUpgradeLogsLocked()
}
func currentUpgradeSnapshot() StreamSnapshot {
inProgress, status, logs := snapshotUpgradeState()
return StreamSnapshot{
InProgress: inProgress,
UpgradeStatus: status,
UpgradeLogs: logs,
}
}
func resetUpgradeLogsLocked() {
upgradeState.logs = nil
}
func appendUpgradeLogLocked(level, message string) {
upgradeState.logs = append(upgradeState.logs, UpgradeLogRecord{
Level: level,
Message: message,
CreatedAt: time.Now().UTC(),
})
}
func cloneUpgradeLogsLocked() []UpgradeLogRecord {
if len(upgradeState.logs) == 0 {
return []UpgradeLogRecord{}
}
cloned := make([]UpgradeLogRecord, len(upgradeState.logs))
copy(cloned, upgradeState.logs)
return cloned
}
func recordUpgradeFailure(err error) {
upgradeState.Lock()
upgradeState.inProgress = false
upgradeState.status = "failed"
appendUpgradeLogLocked("error", err.Error())
upgradeState.Unlock()
broadcastUpgradeSnapshot()
}
func broadcastUpgradeSnapshot() {
snapshot := currentUpgradeSnapshot()
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
for _, listener := range upgradeSubscribers.listeners {
select {
case listener <- snapshot:
default:
}
}
}