fix(management): ensure management.html is available synchronously and improve asset sync handling

This commit is contained in:
hkfires
2026-02-09 08:32:58 +08:00
parent 63643c44a1
commit 3fbee51e9f
3 changed files with 92 additions and 81 deletions

2
go.mod
View File

@@ -22,6 +22,7 @@ require (
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.45.0
golang.org/x/net v0.47.0 golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.18.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -69,7 +70,6 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect

View File

@@ -655,15 +655,18 @@ func (s *Server) serveManagementControlPanel(c *gin.Context) {
if _, err := os.Stat(filePath); err != nil { if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
go managementasset.EnsureLatestManagementHTML(context.Background(), managementasset.StaticDir(s.configFilePath), cfg.ProxyURL, cfg.RemoteManagement.PanelGitHubRepository) // Synchronously ensure management.html is available with a detached context.
// Control panel bootstrap should not be canceled by client disconnects.
if !managementasset.EnsureLatestManagementHTML(context.Background(), managementasset.StaticDir(s.configFilePath), cfg.ProxyURL, cfg.RemoteManagement.PanelGitHubRepository) {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
return return
} }
} else {
log.WithError(err).Error("failed to stat management control panel asset") log.WithError(err).Error("failed to stat management control panel asset")
c.AbortWithStatus(http.StatusInternalServerError) c.AbortWithStatus(http.StatusInternalServerError)
return return
} }
}
c.File(filePath) c.File(filePath)
} }

View File

@@ -21,6 +21,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
) )
const ( const (
@@ -41,6 +42,7 @@ var (
currentConfigPtr atomic.Pointer[config.Config] currentConfigPtr atomic.Pointer[config.Config]
schedulerOnce sync.Once schedulerOnce sync.Once
schedulerConfigPath atomic.Value schedulerConfigPath atomic.Value
sfGroup singleflight.Group
) )
// SetCurrentConfig stores the latest configuration snapshot for management asset decisions. // SetCurrentConfig stores the latest configuration snapshot for management asset decisions.
@@ -171,8 +173,8 @@ func FilePath(configFilePath string) string {
} }
// EnsureLatestManagementHTML checks the latest management.html asset and updates the local copy when needed. // EnsureLatestManagementHTML checks the latest management.html asset and updates the local copy when needed.
// The function is designed to run in a background goroutine and will never panic. // It coalesces concurrent sync attempts and returns whether the asset exists after the sync attempt.
func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL string, panelRepository string) { func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL string, panelRepository string) bool {
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
@@ -180,9 +182,11 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
staticDir = strings.TrimSpace(staticDir) staticDir = strings.TrimSpace(staticDir)
if staticDir == "" { if staticDir == "" {
log.Debug("management asset sync skipped: empty static directory") log.Debug("management asset sync skipped: empty static directory")
return return false
} }
localPath := filepath.Join(staticDir, managementAssetName)
_, _, _ = sfGroup.Do(localPath, func() (interface{}, error) {
lastUpdateCheckMu.Lock() lastUpdateCheckMu.Lock()
now := time.Now() now := time.Now()
timeSinceLastAttempt := now.Sub(lastUpdateCheckTime) timeSinceLastAttempt := now.Sub(lastUpdateCheckTime)
@@ -193,12 +197,11 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
timeSinceLastAttempt.Round(time.Second), timeSinceLastAttempt.Round(time.Second),
managementSyncMinInterval, managementSyncMinInterval,
) )
return return nil, nil
} }
lastUpdateCheckTime = now lastUpdateCheckTime = now
lastUpdateCheckMu.Unlock() lastUpdateCheckMu.Unlock()
localPath := filepath.Join(staticDir, managementAssetName)
localFileMissing := false localFileMissing := false
if _, errStat := os.Stat(localPath); errStat != nil { if _, errStat := os.Stat(localPath); errStat != nil {
if errors.Is(errStat, os.ErrNotExist) { if errors.Is(errStat, os.ErrNotExist) {
@@ -210,7 +213,7 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
if errMkdirAll := os.MkdirAll(staticDir, 0o755); errMkdirAll != nil { if errMkdirAll := os.MkdirAll(staticDir, 0o755); errMkdirAll != nil {
log.WithError(errMkdirAll).Warn("failed to prepare static directory for management asset") log.WithError(errMkdirAll).Warn("failed to prepare static directory for management asset")
return return nil, nil
} }
releaseURL := resolveReleaseURL(panelRepository) releaseURL := resolveReleaseURL(panelRepository)
@@ -229,17 +232,17 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
if localFileMissing { if localFileMissing {
log.WithError(err).Warn("failed to fetch latest management release information, trying fallback page") log.WithError(err).Warn("failed to fetch latest management release information, trying fallback page")
if ensureFallbackManagementHTML(ctx, client, localPath) { if ensureFallbackManagementHTML(ctx, client, localPath) {
return return nil, nil
} }
return return nil, nil
} }
log.WithError(err).Warn("failed to fetch latest management release information") log.WithError(err).Warn("failed to fetch latest management release information")
return return nil, nil
} }
if remoteHash != "" && localHash != "" && strings.EqualFold(remoteHash, localHash) { if remoteHash != "" && localHash != "" && strings.EqualFold(remoteHash, localHash) {
log.Debug("management asset is already up to date") log.Debug("management asset is already up to date")
return return nil, nil
} }
data, downloadedHash, err := downloadAsset(ctx, client, asset.BrowserDownloadURL) data, downloadedHash, err := downloadAsset(ctx, client, asset.BrowserDownloadURL)
@@ -247,12 +250,12 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
if localFileMissing { if localFileMissing {
log.WithError(err).Warn("failed to download management asset, trying fallback page") log.WithError(err).Warn("failed to download management asset, trying fallback page")
if ensureFallbackManagementHTML(ctx, client, localPath) { if ensureFallbackManagementHTML(ctx, client, localPath) {
return return nil, nil
} }
return return nil, nil
} }
log.WithError(err).Warn("failed to download management asset") log.WithError(err).Warn("failed to download management asset")
return return nil, nil
} }
if remoteHash != "" && !strings.EqualFold(remoteHash, downloadedHash) { if remoteHash != "" && !strings.EqualFold(remoteHash, downloadedHash) {
@@ -261,10 +264,15 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
if err = atomicWriteFile(localPath, data); err != nil { if err = atomicWriteFile(localPath, data); err != nil {
log.WithError(err).Warn("failed to update management asset on disk") log.WithError(err).Warn("failed to update management asset on disk")
return return nil, nil
} }
log.Infof("management asset updated successfully (hash=%s)", downloadedHash) log.Infof("management asset updated successfully (hash=%s)", downloadedHash)
return nil, nil
})
_, err := os.Stat(localPath)
return err == nil
} }
func ensureFallbackManagementHTML(ctx context.Context, client *http.Client, localPath string) bool { func ensureFallbackManagementHTML(ctx context.Context, client *http.Client, localPath string) bool {