mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
196 lines
5.6 KiB
Go
196 lines
5.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/control"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/openwrt"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/update"
|
|
)
|
|
|
|
type updateController struct {
|
|
mu sync.Mutex
|
|
wg sync.WaitGroup
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
closing bool
|
|
check update.CheckResult
|
|
channel string
|
|
source update.ReleaseSource
|
|
inventory func(context.Context, string) (update.Inventory, error)
|
|
launch func() error
|
|
}
|
|
|
|
type UpdateCheckParams struct {
|
|
Channel string `json:"channel,omitempty"`
|
|
}
|
|
type UpdateStartParams struct {
|
|
PlanID string `json:"plan_id"`
|
|
}
|
|
type UpdateStatusParams struct {
|
|
JobID string `json:"job_id,omitempty"`
|
|
}
|
|
|
|
func (d *Daemon) initUpdater(ctx context.Context, options Options) {
|
|
source := options.UpdateSource
|
|
if source == nil {
|
|
source = update.NewSource()
|
|
}
|
|
inventory := func(ctx context.Context, version string) (update.Inventory, error) {
|
|
return openwrt.CurrentPackageInventory(ctx, openwrt.Runner{}, version)
|
|
}
|
|
if options.UpdateDevice != nil {
|
|
inventory = options.UpdateDevice.Inventory
|
|
}
|
|
background, cancel := context.WithCancel(ctx)
|
|
d.updater = &updateController{ctx: background, cancel: cancel, source: source, inventory: inventory, launch: options.UpdateLaunch}
|
|
if d.updater.launch == nil {
|
|
d.updater.launch = func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
_, err := (openwrt.Runner{Timeout: 10 * time.Second}).Run(ctx, "/etc/init.d/smart_srun_update", "start")
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (u *updateController) close() {
|
|
u.mu.Lock()
|
|
u.closing = true
|
|
u.cancel()
|
|
u.mu.Unlock()
|
|
u.wg.Wait()
|
|
if source, ok := u.source.(interface{ Close() }); ok {
|
|
source.Close()
|
|
}
|
|
}
|
|
|
|
func (d *Daemon) updateCheck(_ context.Context, raw json.RawMessage) (any, error) {
|
|
var params UpdateCheckParams
|
|
if err := control.DecodeParams(raw, ¶ms); err != nil {
|
|
return nil, err
|
|
}
|
|
if params.Channel != "" && params.Channel != "stable" && params.Channel != "rc" {
|
|
return nil, domain.Errorf(domain.CodeInvalidArgument, "更新通道无效")
|
|
}
|
|
channel := params.Channel
|
|
if channel == "" {
|
|
version, err := update.ParseVersion(d.version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channel = version.Channel()
|
|
}
|
|
u := d.updater
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
if u.closing {
|
|
return nil, domain.Errorf(domain.CodeServiceStopped, "服务正在停止")
|
|
}
|
|
if u.check.Running {
|
|
if u.channel != channel {
|
|
return nil, domain.Errorf(domain.CodeBusy, "正在检查另一个更新通道,请稍后重试")
|
|
}
|
|
return u.check, nil
|
|
}
|
|
var id [16]byte
|
|
_, _ = rand.Read(id[:])
|
|
u.check = update.CheckResult{OK: true, Running: true, JobID: hex.EncodeToString(id[:]), CurrentVersion: d.version, Message: "正在检查兼容更新"}
|
|
u.channel = channel
|
|
u.wg.Add(1)
|
|
go func() {
|
|
defer u.wg.Done()
|
|
ctx, cancel := context.WithTimeout(u.ctx, 3*time.Minute)
|
|
defer cancel()
|
|
inventory, err := u.inventory(ctx, d.version)
|
|
result := update.CheckResult{CurrentVersion: d.version}
|
|
if err == nil {
|
|
var candidate update.Candidate
|
|
candidate, result, err = update.Check(ctx, u.source, inventory, channel)
|
|
if err == nil && result.UpdateAvailable {
|
|
err = update.SaveCandidate(d.paths.Update(), candidate)
|
|
}
|
|
}
|
|
if err != nil {
|
|
result.OK = false
|
|
result.Code, result.Message = update.ErrorStatus(err)
|
|
}
|
|
u.mu.Lock()
|
|
result.JobID = u.check.JobID
|
|
u.check = result
|
|
u.mu.Unlock()
|
|
}()
|
|
return u.check, nil
|
|
}
|
|
|
|
func (d *Daemon) updateStatus(_ context.Context, raw json.RawMessage) (any, error) {
|
|
var params UpdateStatusParams
|
|
if err := control.DecodeParams(raw, ¶ms); err != nil {
|
|
return nil, err
|
|
}
|
|
if params.JobID != "" {
|
|
u := d.updater
|
|
u.mu.Lock()
|
|
result := u.check
|
|
u.mu.Unlock()
|
|
if result.JobID == params.JobID {
|
|
return result, nil
|
|
}
|
|
}
|
|
status, err := update.ReadStatus(d.paths.Update())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if params.JobID != "" && params.JobID != status.JobID {
|
|
return nil, domain.Errorf(domain.CodeNotFound, "没有该更新任务")
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func (d *Daemon) updateStart(ctx context.Context, raw json.RawMessage) (any, error) {
|
|
var params UpdateStartParams
|
|
if err := control.DecodeParams(raw, ¶ms); err != nil {
|
|
return nil, err
|
|
}
|
|
// A lost RPC reply may be retried; it must not start a second installer.
|
|
if task, err := update.ReadTask(d.paths.Update()); err == nil && task.Plan.ID == params.PlanID {
|
|
return update.ReadStatus(d.paths.Update())
|
|
}
|
|
candidate, err := update.ReadCandidate(d.paths.Update(), params.PlanID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inventory, err := d.updater.inventory(ctx, d.version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !reflect.DeepEqual(inventory, candidate.Plan.Inventory) {
|
|
return nil, domain.Errorf(domain.CodeConflict, "本机安装状态已变化,请重新检查更新")
|
|
}
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var task update.Task
|
|
err = d.actions.ChangeConfiguration(ctx, func() error {
|
|
if d.wizard.configBlocked() {
|
|
return domain.Errorf(domain.CodeBusy, "无线向导尚未完成,请先保存或取消")
|
|
}
|
|
var err error
|
|
task, err = update.Begin(d.paths.Update(), candidate, true, executable, d.updater.launch)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update.Status{SchemaVersion: 1, JobID: task.JobID, Running: true, OK: true, Phase: "queued", Message: "更新任务已提交"}, nil
|
|
}
|