695 lines
22 KiB
Go
695 lines
22 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"scout/pkg/client"
|
|
"scout/pkg/scoutpkg"
|
|
)
|
|
|
|
const ExitRestartNow = 42
|
|
|
|
type Config struct {
|
|
Client *client.Client
|
|
PackageName string
|
|
CurrentVersion string
|
|
CurrentPath string
|
|
RootDir string
|
|
Platform string
|
|
Architecture string
|
|
Channel string
|
|
Entrypoint string
|
|
HealthTimeout time.Duration
|
|
KeepReleases int
|
|
}
|
|
|
|
type ReleasePointer struct {
|
|
Version string `json:"version"`
|
|
Path string `json:"path"`
|
|
Entrypoint string `json:"entrypoint"`
|
|
Platform string `json:"platform"`
|
|
Architecture string `json:"architecture"`
|
|
PackageType string `json:"package_type"`
|
|
SHA256 string `json:"sha256"`
|
|
PreparedAt time.Time `json:"prepared_at"`
|
|
ActivatedAt time.Time `json:"activated_at,omitempty"`
|
|
HealthyAt time.Time `json:"healthy_at,omitempty"`
|
|
}
|
|
|
|
type State struct {
|
|
Stage string `json:"stage"`
|
|
CurrentVersion string `json:"current_version,omitempty"`
|
|
PreviousVersion string `json:"previous_version,omitempty"`
|
|
Previous *ReleasePointer `json:"previous,omitempty"`
|
|
PendingVersion string `json:"pending_version,omitempty"`
|
|
ActivatedAt time.Time `json:"activated_at,omitempty"`
|
|
Healthy bool `json:"healthy"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type Status struct {
|
|
Current *ReleasePointer `json:"current,omitempty"`
|
|
Pending *ReleasePointer `json:"pending,omitempty"`
|
|
State State `json:"state"`
|
|
}
|
|
|
|
type Manager struct {
|
|
config Config
|
|
}
|
|
|
|
func New(config Config) (*Manager, error) {
|
|
if config.Client == nil {
|
|
return nil, errors.New("\u5ba2\u6237\u7aef\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
if strings.TrimSpace(config.PackageName) == "" {
|
|
return nil, errors.New("\u5305\u540d\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
if strings.TrimSpace(config.RootDir) == "" {
|
|
return nil, errors.New("\u66f4\u65b0\u6839\u76ee\u5f55\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
platform, architecture := config.Platform, config.Architecture
|
|
if platform == "" && architecture == "" {
|
|
platform, architecture = scoutpkg.CurrentTarget()
|
|
} else {
|
|
var err error
|
|
platform, err = scoutpkg.NormalizePlatform(platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
architecture, err = scoutpkg.NormalizeArchitecture(architecture)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
config.Platform = platform
|
|
config.Architecture = architecture
|
|
if config.Channel == "" {
|
|
config.Channel = "stable"
|
|
}
|
|
if config.HealthTimeout <= 0 {
|
|
config.HealthTimeout = 2 * time.Minute
|
|
}
|
|
if config.KeepReleases <= 0 {
|
|
config.KeepReleases = 2
|
|
}
|
|
root, err := filepath.Abs(config.RootDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("\u89e3\u6790\u66f4\u65b0\u6839\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
config.RootDir = root
|
|
for _, name := range []string{"releases", "downloads"} {
|
|
if err := os.MkdirAll(filepath.Join(root, name), 0o755); err != nil {
|
|
return nil, fmt.Errorf("\u521b\u5efa\u66f4\u65b0\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
}
|
|
manager := &Manager{config: config}
|
|
if _, err := manager.readPointer("current.json"); os.IsNotExist(err) && config.CurrentVersion != "" && config.CurrentPath != "" && config.Entrypoint != "" {
|
|
if _, err := manager.RegisterCurrent(config.CurrentVersion, config.CurrentPath, config.Entrypoint); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return manager, nil
|
|
}
|
|
|
|
func (manager *Manager) RegisterCurrent(version, releasePath, entrypoint string) (ReleasePointer, error) {
|
|
if strings.TrimSpace(version) == "" {
|
|
return ReleasePointer{}, errors.New("\u5f53\u524d\u7248\u672c\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
releasePath, err := filepath.Abs(releasePath)
|
|
if err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u89e3\u6790\u5f53\u524d\u7a0b\u5e8f\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
executable, err := safeEntrypoint(releasePath, entrypoint)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if _, err := os.Stat(executable); err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u5f53\u524d\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
|
}
|
|
now := time.Now().UTC()
|
|
pointer := ReleasePointer{
|
|
Version: version,
|
|
Path: releasePath,
|
|
Entrypoint: entrypoint,
|
|
Platform: manager.config.Platform,
|
|
Architecture: manager.config.Architecture,
|
|
PackageType: "full",
|
|
PreparedAt: now,
|
|
ActivatedAt: now,
|
|
HealthyAt: now,
|
|
}
|
|
if err := manager.writeJSON("current.json", pointer); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
state := State{Stage: "healthy", CurrentVersion: version, Healthy: true, UpdatedAt: now}
|
|
if err := manager.writeJSON("state.json", state); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
return pointer, nil
|
|
}
|
|
|
|
func (manager *Manager) Check(ctx context.Context) (client.Package, bool, error) {
|
|
currentVersion := manager.currentVersion()
|
|
return manager.config.Client.CheckUpdateFor(ctx, manager.config.PackageName, currentVersion, manager.queryOptions(currentVersion))
|
|
}
|
|
|
|
func (manager *Manager) Watch(ctx context.Context) (<-chan client.Update, error) {
|
|
currentVersion := manager.currentVersion()
|
|
return manager.config.Client.WatchUpdatesFor(ctx, client.WatchRequest{
|
|
Packages: []string{manager.config.PackageName},
|
|
Versions: map[string]string{manager.config.PackageName: currentVersion},
|
|
Platform: manager.config.Platform,
|
|
Architecture: manager.config.Architecture,
|
|
Channel: manager.config.Channel,
|
|
})
|
|
}
|
|
|
|
func (manager *Manager) Prepare(ctx context.Context, version string) (ReleasePointer, error) {
|
|
if pending, err := manager.readPointer("pending.json"); err == nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u5df2\u6709\u5f85\u6fc0\u6d3b\u7248\u672c %s", pending.Version)
|
|
}
|
|
currentVersion := manager.currentVersion()
|
|
options := manager.queryOptions(currentVersion)
|
|
item, err := manager.resolvePackage(ctx, version, options)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if currentVersion != "" && client.CompareVersions(item.Version, currentVersion) <= 0 {
|
|
return ReleasePointer{}, errors.New("\u76ee\u6807\u7248\u672c\u5fc5\u987b\u9ad8\u4e8e\u5f53\u524d\u7248\u672c")
|
|
}
|
|
releaseDir, err := manager.safeReleaseDir(item.Version)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if _, err := os.Stat(releaseDir); err == nil {
|
|
return ReleasePointer{}, errors.New("\u76ee\u6807\u7248\u672c\u76ee\u5f55\u5df2\u7ecf\u5b58\u5728")
|
|
} else if !os.IsNotExist(err) {
|
|
return ReleasePointer{}, fmt.Errorf("\u68c0\u67e5\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
stagingDir, err := os.MkdirTemp(filepath.Dir(releaseDir), ".staging-"+item.Version+"-")
|
|
if err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u521b\u5efa\u7248\u672c\u4e34\u65f6\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
stagingReady := false
|
|
defer func() {
|
|
if !stagingReady {
|
|
_ = os.RemoveAll(stagingDir)
|
|
}
|
|
}()
|
|
|
|
fileName := item.FileName
|
|
if fileName == "" && len(item.Targets) > 0 {
|
|
fileName = item.Targets[0].FileName
|
|
}
|
|
if fileName == "" {
|
|
fileName = fmt.Sprintf("%s-%s.pkg", item.Name, item.Version)
|
|
}
|
|
downloadPath := filepath.Join(manager.config.RootDir, "downloads", item.Version+"-"+filepath.Base(fileName))
|
|
downloaded, err := manager.config.Client.DownloadFor(ctx, item.Name, item.Version, downloadPath, options)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
|
|
entrypoint := manager.config.Entrypoint
|
|
if strings.EqualFold(filepath.Ext(fileName), ".scout") {
|
|
entrypoint, err = manager.extractScout(downloadPath, stagingDir, entrypoint)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
} else {
|
|
destination := filepath.Join(stagingDir, filepath.Base(fileName))
|
|
if err := moveDownloadedFile(downloadPath, destination); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if entrypoint == "" {
|
|
entrypoint = filepath.Base(fileName)
|
|
}
|
|
}
|
|
if entrypoint == "" {
|
|
return ReleasePointer{}, errors.New("\u66f4\u65b0\u5305\u672a\u6307\u5b9a\u7a0b\u5e8f\u5165\u53e3")
|
|
}
|
|
executable, err := safeEntrypoint(stagingDir, entrypoint)
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if _, err := os.Stat(executable); err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u65b0\u7248\u672c\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
|
}
|
|
if runtime.GOOS != "windows" && manager.config.Platform != "android" {
|
|
if info, err := os.Stat(executable); err == nil && info.Mode().Perm()&0o111 == 0 {
|
|
_ = os.Chmod(executable, info.Mode().Perm()|0o755)
|
|
}
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
pointer := ReleasePointer{
|
|
Version: item.Version,
|
|
Path: releaseDir,
|
|
Entrypoint: entrypoint,
|
|
Platform: item.Platform,
|
|
Architecture: item.Architecture,
|
|
PackageType: item.PackageType,
|
|
SHA256: downloaded.SHA256,
|
|
PreparedAt: now,
|
|
}
|
|
if err := manager.writeReleaseMetadataAt(stagingDir, pointer); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if err := os.Rename(stagingDir, releaseDir); err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u6fc0\u6d3b\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
stagingReady = true
|
|
if err := manager.writeJSON("pending.json", pointer); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
state := State{
|
|
Stage: "prepared",
|
|
CurrentVersion: currentVersion,
|
|
PendingVersion: pointer.Version,
|
|
Healthy: false,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := manager.writeJSON("state.json", state); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
return pointer, nil
|
|
}
|
|
|
|
func (manager *Manager) Activate() (ReleasePointer, error) {
|
|
pending, err := manager.readPointer("pending.json")
|
|
if err != nil {
|
|
return ReleasePointer{}, errors.New("\u6ca1\u6709\u5f85\u6fc0\u6d3b\u7248\u672c")
|
|
}
|
|
var previous *ReleasePointer
|
|
if current, err := manager.readPointer("current.json"); err == nil {
|
|
previous = ¤t
|
|
}
|
|
now := time.Now().UTC()
|
|
pending.ActivatedAt = now
|
|
pending.HealthyAt = time.Time{}
|
|
if err := manager.writeJSON("current.json", pending); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
state := State{
|
|
Stage: "activated",
|
|
CurrentVersion: pending.Version,
|
|
PendingVersion: pending.Version,
|
|
ActivatedAt: now,
|
|
Healthy: false,
|
|
UpdatedAt: now,
|
|
}
|
|
if previous != nil {
|
|
state.Previous = previous
|
|
state.PreviousVersion = previous.Version
|
|
}
|
|
if err := manager.writeJSON("state.json", state); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
return pending, nil
|
|
}
|
|
|
|
func (manager *Manager) ConfirmHealthy(version string) error {
|
|
current, err := manager.readPointer("current.json")
|
|
if err != nil {
|
|
return errors.New("\u5f53\u524d\u7248\u672c\u6307\u9488\u4e0d\u5b58\u5728")
|
|
}
|
|
if version != "" && current.Version != version {
|
|
return errors.New("\u5065\u5eb7\u786e\u8ba4\u7248\u672c\u4e0e\u5f53\u524d\u7248\u672c\u4e0d\u4e00\u81f4")
|
|
}
|
|
state, err := manager.readState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if state.Stage != "activated" || state.CurrentVersion != current.Version {
|
|
return errors.New("\u5f53\u524d\u72b6\u6001\u4e0d\u5141\u8bb8\u5065\u5eb7\u786e\u8ba4")
|
|
}
|
|
now := time.Now().UTC()
|
|
current.HealthyAt = now
|
|
if err := manager.writeJSON("current.json", current); err != nil {
|
|
return err
|
|
}
|
|
state.Stage = "healthy"
|
|
state.PendingVersion = ""
|
|
state.Healthy = true
|
|
state.UpdatedAt = now
|
|
if err := manager.writeJSON("state.json", state); err != nil {
|
|
return err
|
|
}
|
|
_ = os.Remove(manager.filePath("pending.json"))
|
|
return manager.Cleanup()
|
|
}
|
|
|
|
func (manager *Manager) Rollback(reason error) (ReleasePointer, error) {
|
|
state, err := manager.readState()
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if state.Previous == nil {
|
|
return ReleasePointer{}, errors.New("\u6ca1\u6709\u53ef\u56de\u6eda\u7684\u65e7\u7248\u672c\u6307\u9488")
|
|
}
|
|
previous := *state.Previous
|
|
if _, err := safeEntrypoint(previous.Path, previous.Entrypoint); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
if err := manager.writeJSON("current.json", previous); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
state.Stage = "rolled_back"
|
|
state.CurrentVersion = previous.Version
|
|
state.PendingVersion = ""
|
|
state.Healthy = true
|
|
if reason != nil {
|
|
state.LastError = reason.Error()
|
|
}
|
|
state.UpdatedAt = time.Now().UTC()
|
|
if err := manager.writeJSON("state.json", state); err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
_ = os.Remove(manager.filePath("pending.json"))
|
|
return previous, nil
|
|
}
|
|
|
|
func (manager *Manager) RecoverStaleActivation(now time.Time) (bool, error) {
|
|
state, err := manager.readState()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
if state.Stage != "activated" || state.Healthy {
|
|
return false, nil
|
|
}
|
|
if now.Before(state.ActivatedAt.Add(manager.config.HealthTimeout)) {
|
|
return false, nil
|
|
}
|
|
if state.Previous == nil {
|
|
return false, errors.New("\u5065\u5eb7\u786e\u8ba4\u8d85\u65f6\uff0c\u4f46\u6ca1\u6709\u53ef\u56de\u6eda\u7684\u65e7\u7248\u672c")
|
|
}
|
|
_, rollbackErr := manager.Rollback(errors.New("\u65b0\u7248\u672c\u5065\u5eb7\u786e\u8ba4\u8d85\u65f6\uff0c\u5df2\u81ea\u52a8\u56de\u6eda"))
|
|
return rollbackErr == nil, rollbackErr
|
|
}
|
|
|
|
func (manager *Manager) Status() (Status, error) {
|
|
result := Status{}
|
|
if current, err := manager.readPointer("current.json"); err == nil {
|
|
result.Current = ¤t
|
|
} else if !os.IsNotExist(err) {
|
|
return Status{}, err
|
|
}
|
|
if pending, err := manager.readPointer("pending.json"); err == nil {
|
|
result.Pending = &pending
|
|
} else if !os.IsNotExist(err) {
|
|
return Status{}, err
|
|
}
|
|
state, err := manager.readState()
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return Status{}, err
|
|
}
|
|
result.State = state
|
|
return result, nil
|
|
}
|
|
|
|
func (manager *Manager) ResolveCurrent() (string, ReleasePointer, error) {
|
|
current, err := manager.readPointer("current.json")
|
|
if err != nil {
|
|
return "", ReleasePointer{}, errors.New("\u5f53\u524d\u7248\u672c\u6307\u9488\u4e0d\u5b58\u5728")
|
|
}
|
|
executable, err := safeEntrypoint(current.Path, current.Entrypoint)
|
|
if err != nil {
|
|
return "", ReleasePointer{}, err
|
|
}
|
|
if _, err := os.Stat(executable); err != nil {
|
|
return "", ReleasePointer{}, fmt.Errorf("\u5f53\u524d\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
|
}
|
|
return executable, current, nil
|
|
}
|
|
|
|
func (manager *Manager) Launch(ctx context.Context, args ...string) (*exec.Cmd, error) {
|
|
executable, current, err := manager.ResolveCurrent()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
command := exec.CommandContext(ctx, executable, args...)
|
|
command.Dir = current.Path
|
|
command.Env = append(os.Environ(),
|
|
"SCOUT_PACKAGE="+manager.config.PackageName,
|
|
"SCOUT_VERSION="+current.Version,
|
|
"SCOUT_UPDATE_ROOT="+manager.config.RootDir,
|
|
"SCOUT_PLATFORM="+manager.config.Platform,
|
|
"SCOUT_ARCHITECTURE="+manager.config.Architecture,
|
|
)
|
|
if err := command.Start(); err != nil {
|
|
return nil, fmt.Errorf("\u542f\u52a8\u5f53\u524d\u7248\u672c\u5931\u8d25: %w", err)
|
|
}
|
|
return command, nil
|
|
}
|
|
|
|
func (manager *Manager) Cleanup() error {
|
|
releasesDir := filepath.Join(manager.config.RootDir, "releases")
|
|
entries, err := os.ReadDir(releasesDir)
|
|
if err != nil {
|
|
return fmt.Errorf("\u8bfb\u53d6\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
status, err := manager.Status()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
protected := make(map[string]bool)
|
|
if status.Current != nil && isManagedRelease(releasesDir, status.Current.Path) {
|
|
protected[status.Current.Version] = true
|
|
}
|
|
if status.Pending != nil && isManagedRelease(releasesDir, status.Pending.Path) {
|
|
protected[status.Pending.Version] = true
|
|
}
|
|
candidates := make([]os.DirEntry, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.IsDir() && !protected[entry.Name()] {
|
|
candidates = append(candidates, entry)
|
|
}
|
|
}
|
|
sort.Slice(candidates, func(left, right int) bool {
|
|
leftInfo, _ := candidates[left].Info()
|
|
rightInfo, _ := candidates[right].Info()
|
|
return leftInfo.ModTime().After(rightInfo.ModTime())
|
|
})
|
|
keepCandidates := manager.config.KeepReleases - len(protected)
|
|
if keepCandidates < 0 {
|
|
keepCandidates = 0
|
|
}
|
|
for index, entry := range candidates {
|
|
if index < keepCandidates {
|
|
continue
|
|
}
|
|
target, err := manager.safeReleaseDir(entry.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.RemoveAll(target); err != nil {
|
|
return fmt.Errorf("\u6e05\u7406\u65e7\u7248\u672c\u5931\u8d25: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (manager *Manager) resolvePackage(ctx context.Context, version string, options client.QueryOptions) (client.Package, error) {
|
|
if strings.TrimSpace(version) == "" {
|
|
return manager.config.Client.GetLatestFor(ctx, manager.config.PackageName, options)
|
|
}
|
|
versions, err := manager.config.Client.ListVersionsFor(ctx, manager.config.PackageName, options)
|
|
if err != nil {
|
|
return client.Package{}, err
|
|
}
|
|
for _, item := range versions {
|
|
if item.Version == version {
|
|
return item, nil
|
|
}
|
|
}
|
|
return client.Package{}, errors.New("\u6ca1\u6709\u627e\u5230\u6307\u5b9a\u7684\u517c\u5bb9\u7248\u672c")
|
|
}
|
|
|
|
func (manager *Manager) extractScout(downloadPath, releaseDir, fallbackEntrypoint string) (string, error) {
|
|
archive, err := scoutpkg.Open(downloadPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
target, extractErr := archive.Extract(manager.config.Platform, manager.config.Architecture, releaseDir)
|
|
closeErr := archive.Close()
|
|
if extractErr != nil {
|
|
return "", extractErr
|
|
}
|
|
if closeErr != nil {
|
|
return "", closeErr
|
|
}
|
|
if target.Entrypoint == "" {
|
|
return fallbackEntrypoint, nil
|
|
}
|
|
relative := strings.TrimPrefix(target.Entrypoint, strings.TrimSuffix(target.Path, "/"))
|
|
relative = strings.TrimPrefix(relative, "/")
|
|
if relative == "" {
|
|
relative = filepath.Base(filepath.FromSlash(target.Path))
|
|
}
|
|
return filepath.FromSlash(relative), nil
|
|
}
|
|
|
|
func (manager *Manager) queryOptions(currentVersion string) client.QueryOptions {
|
|
return client.QueryOptions{
|
|
Platform: manager.config.Platform,
|
|
Architecture: manager.config.Architecture,
|
|
Channel: manager.config.Channel,
|
|
CurrentVersion: currentVersion,
|
|
}
|
|
}
|
|
|
|
func (manager *Manager) currentVersion() string {
|
|
if current, err := manager.readPointer("current.json"); err == nil && current.Version != "" {
|
|
return current.Version
|
|
}
|
|
return manager.config.CurrentVersion
|
|
}
|
|
|
|
func (manager *Manager) writeReleaseMetadata(pointer ReleasePointer) error {
|
|
return manager.writeReleaseMetadataAt(pointer.Path, pointer)
|
|
}
|
|
|
|
func (manager *Manager) writeReleaseMetadataAt(directory string, pointer ReleasePointer) error {
|
|
data, err := json.MarshalIndent(pointer, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("\u7f16\u7801\u7248\u672c\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(directory, "scout-release.json"), data, 0o644); err != nil {
|
|
return fmt.Errorf("\u4fdd\u5b58\u7248\u672c\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (manager *Manager) writeJSON(name string, value any) error {
|
|
data, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("\u7f16\u7801\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
|
}
|
|
temp, err := os.CreateTemp(manager.config.RootDir, ".state-*")
|
|
if err != nil {
|
|
return fmt.Errorf("\u521b\u5efa\u72b6\u6001\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
tempPath := temp.Name()
|
|
defer os.Remove(tempPath)
|
|
if _, err := temp.Write(data); err != nil {
|
|
temp.Close()
|
|
return fmt.Errorf("\u5199\u5165\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
|
}
|
|
if err := temp.Close(); err != nil {
|
|
return fmt.Errorf("\u5173\u95ed\u72b6\u6001\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
if err := os.Rename(tempPath, manager.filePath(name)); err != nil {
|
|
return fmt.Errorf("\u4fdd\u5b58\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (manager *Manager) readPointer(name string) (ReleasePointer, error) {
|
|
data, err := os.ReadFile(manager.filePath(name))
|
|
if err != nil {
|
|
return ReleasePointer{}, err
|
|
}
|
|
var pointer ReleasePointer
|
|
if err := json.Unmarshal(data, &pointer); err != nil {
|
|
return ReleasePointer{}, fmt.Errorf("\u8bfb\u53d6\u7248\u672c\u6307\u9488\u5931\u8d25: %w", err)
|
|
}
|
|
return pointer, nil
|
|
}
|
|
|
|
func (manager *Manager) readState() (State, error) {
|
|
data, err := os.ReadFile(manager.filePath("state.json"))
|
|
if err != nil {
|
|
return State{}, err
|
|
}
|
|
var state State
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
return State{}, fmt.Errorf("\u8bfb\u53d6\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func (manager *Manager) safeReleaseDir(version string) (string, error) {
|
|
version = strings.TrimSpace(version)
|
|
if version == "" || version == "." || version == ".." || filepath.Base(version) != version || strings.ContainsAny(version, `/\\`) {
|
|
return "", errors.New("\u7248\u672c\u76ee\u5f55\u540d\u65e0\u6548")
|
|
}
|
|
releasesDir := filepath.Join(manager.config.RootDir, "releases")
|
|
target := filepath.Join(releasesDir, version)
|
|
if !isManagedRelease(releasesDir, target) {
|
|
return "", errors.New("\u7248\u672c\u76ee\u5f55\u8d85\u51fa\u66f4\u65b0\u6839\u76ee\u5f55")
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func (manager *Manager) filePath(name string) string {
|
|
return filepath.Join(manager.config.RootDir, name)
|
|
}
|
|
|
|
func safeEntrypoint(releaseDir, entrypoint string) (string, error) {
|
|
if strings.TrimSpace(entrypoint) == "" {
|
|
return "", errors.New("\u7a0b\u5e8f\u5165\u53e3\u4e0d\u80fd\u4e3a\u7a7a")
|
|
}
|
|
releaseDir, err := filepath.Abs(releaseDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("\u89e3\u6790\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
|
}
|
|
executable := filepath.Join(releaseDir, filepath.FromSlash(entrypoint))
|
|
relation, err := filepath.Rel(releaseDir, executable)
|
|
if err != nil || relation == ".." || strings.HasPrefix(relation, ".."+string(filepath.Separator)) || filepath.IsAbs(entrypoint) {
|
|
return "", errors.New("\u7a0b\u5e8f\u5165\u53e3\u8d85\u51fa\u7248\u672c\u76ee\u5f55")
|
|
}
|
|
return executable, nil
|
|
}
|
|
|
|
func moveDownloadedFile(source, destination string) error {
|
|
if err := os.Rename(source, destination); err == nil {
|
|
return nil
|
|
}
|
|
input, err := os.Open(source)
|
|
if err != nil {
|
|
return fmt.Errorf("\u8bfb\u53d6\u5df2\u4e0b\u8f7d\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
defer input.Close()
|
|
output, err := os.Create(destination)
|
|
if err != nil {
|
|
return fmt.Errorf("\u521b\u5efa\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", err)
|
|
}
|
|
_, copyErr := io.Copy(output, input)
|
|
closeErr := output.Close()
|
|
if copyErr != nil {
|
|
return fmt.Errorf("\u590d\u5236\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", copyErr)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("\u5173\u95ed\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", closeErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isManagedRelease(releasesDir, candidate string) bool {
|
|
releasesDir, err := filepath.Abs(releasesDir)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
candidate, err = filepath.Abs(candidate)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
relation, err := filepath.Rel(releasesDir, candidate)
|
|
return err == nil && relation != "." && relation != ".." && !strings.HasPrefix(relation, ".."+string(filepath.Separator))
|
|
}
|