mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 06:36:38 +08:00
263 lines
7.2 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|