This commit is contained in:
ryan
2026-06-18 16:08:48 +08:00
parent 3366edb3a1
commit 772962c2e9
156 changed files with 23460 additions and 183 deletions
@@ -0,0 +1,262 @@
// 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:
}
}
}
@@ -0,0 +1,113 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package update
import (
"encoding/json"
"errors"
"io"
"net/http"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/compat"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgradeLogsUpgrader = websocket.Upgrader{
CheckOrigin: func(_ *http.Request) bool { return true },
}
// GetLatestReleaseHandler returns the newest GitHub release for the legacy update UI.
func GetLatestReleaseHandler(c *gin.Context) {
release, err := GetLatestRelease(c.Request.Context(), c.Query("channel"))
if err != nil {
compat.Fail(c, err.Error())
return
}
compat.OK(c, release)
}
// UpgradeServerHandler schedules an automatic upgrade from the latest release.
func UpgradeServerHandler(c *gin.Context) {
var request upgradeRequest
if err := bindOptionalJSON(c.Request.Body, &request); err != nil {
compat.Fail(c, "无效的参数")
return
}
release, err := ScheduleUpgrade(c.Request.Context(), request.Channel)
if err != nil {
compat.Fail(c, err.Error())
return
}
okWithMessage(c, release, "服务升级任务已启动,下载完成后将自动重启。")
}
// UploadManualServerBinaryHandler rejects manual uploads (feature disabled upstream).
func UploadManualServerBinaryHandler(c *gin.Context) {
if err := UploadManualBinary(); err != nil {
compat.Fail(c, err.Error())
return
}
}
// ConfirmManualServerUpgradeHandler rejects manual upgrades (feature disabled upstream).
func ConfirmManualServerUpgradeHandler(c *gin.Context) {
if err := ConfirmManualUpgrade(); err != nil {
compat.Fail(c, err.Error())
return
}
}
// StreamServerUpgradeLogsHandler streams upgrade progress snapshots over WebSocket.
func StreamServerUpgradeLogsHandler(c *gin.Context) {
conn, err := upgradeLogsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer func() {
_ = conn.Close()
}()
updates, unsubscribe := SubscribeUpgradeStream()
defer unsubscribe()
heartbeatTicker := time.NewTicker(15 * time.Second)
defer heartbeatTicker.Stop()
for {
select {
case snapshot, ok := <-updates:
if !ok {
return
}
if err := conn.WriteJSON(snapshot); err != nil {
return
}
case <-heartbeatTicker.C:
if err := conn.WriteJSON(StreamSnapshot{}); err != nil {
return
}
case <-c.Request.Context().Done():
return
}
}
}
func okWithMessage(c *gin.Context, data any, message string) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": message,
"data": data,
})
}
func bindOptionalJSON(body io.Reader, target any) error {
if err := json.NewDecoder(body).Decode(target); err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
}