mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-07 07:10:27 +08:00
增加edge-admin upgrade命令
This commit is contained in:
BIN
cmd/edge-admin/edge-admin
Executable file
BIN
cmd/edge-admin/edge-admin
Executable file
Binary file not shown.
@@ -7,17 +7,20 @@ import (
|
|||||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/gen"
|
"github.com/TeaOSLab/EdgeAdmin/internal/gen"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
|
"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
|
||||||
_ "github.com/iwind/TeaGo/bootstrap"
|
_ "github.com/iwind/TeaGo/bootstrap"
|
||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
"github.com/iwind/gosock/pkg/gosock"
|
"github.com/iwind/gosock/pkg/gosock"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := apps.NewAppCmd().
|
var app = apps.NewAppCmd().
|
||||||
Version(teaconst.Version).
|
Version(teaconst.Version).
|
||||||
Product(teaconst.ProductName).
|
Product(teaconst.ProductName).
|
||||||
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover|demo]").
|
Usage(teaconst.ProcessName+" [-v|start|stop|restart|service|daemon|reset|recover|demo|upgrade]").
|
||||||
Usage(teaconst.ProcessName+" [dev|prod]").
|
Usage(teaconst.ProcessName+" [dev|prod]").
|
||||||
Option("-h", "show this help").
|
Option("-h", "show this help").
|
||||||
Option("-v", "show version").
|
Option("-v", "show version").
|
||||||
@@ -30,7 +33,8 @@ func main() {
|
|||||||
Option("recover", "enter recovery mode").
|
Option("recover", "enter recovery mode").
|
||||||
Option("demo", "switch to demo mode").
|
Option("demo", "switch to demo mode").
|
||||||
Option("dev", "switch to 'dev' mode").
|
Option("dev", "switch to 'dev' mode").
|
||||||
Option("prod", "switch to 'prod' mode")
|
Option("prod", "switch to 'prod' mode").
|
||||||
|
Option("upgrade", "upgrade from official site")
|
||||||
|
|
||||||
app.On("daemon", func() {
|
app.On("daemon", func() {
|
||||||
nodes.NewAdminNode().Daemon()
|
nodes.NewAdminNode().Daemon()
|
||||||
@@ -115,8 +119,42 @@ func main() {
|
|||||||
fmt.Println("switch to '" + env + "' ok")
|
fmt.Println("switch to '" + env + "' ok")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
app.On("upgrade", func() {
|
||||||
|
var manager = utils.NewUpgradeManager("admin")
|
||||||
|
log.Println("checking latest version ...")
|
||||||
|
var ticker = time.NewTicker(1 * time.Second)
|
||||||
|
go func() {
|
||||||
|
var lastProgress float32 = 0
|
||||||
|
var isStarted = false
|
||||||
|
for range ticker.C {
|
||||||
|
if manager.IsDownloading() {
|
||||||
|
if !isStarted {
|
||||||
|
log.Println("start downloading v" + manager.NewVersion() + " ...")
|
||||||
|
isStarted = true
|
||||||
|
}
|
||||||
|
var progress = manager.Progress()
|
||||||
|
if progress >= 0 {
|
||||||
|
if progress == 0 || progress == 1 || progress-lastProgress >= 0.1 {
|
||||||
|
lastProgress = progress
|
||||||
|
log.Println(fmt.Sprintf("%.2f%%", manager.Progress()*100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := manager.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("upgrade failed: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("finished!")
|
||||||
|
log.Println("restarting ...")
|
||||||
|
app.RunRestart()
|
||||||
|
})
|
||||||
app.Run(func() {
|
app.Run(func() {
|
||||||
adminNode := nodes.NewAdminNode()
|
var adminNode = nodes.NewAdminNode()
|
||||||
adminNode.Run()
|
adminNode.Run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (this *AppCmd) On(arg string, callback func()) {
|
|||||||
// Run 运行
|
// Run 运行
|
||||||
func (this *AppCmd) Run(main func()) {
|
func (this *AppCmd) Run(main func()) {
|
||||||
// 获取参数
|
// 获取参数
|
||||||
args := os.Args[1:]
|
var args = os.Args[1:]
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "-v", "version", "-version", "--version":
|
case "-v", "version", "-version", "--version":
|
||||||
@@ -139,7 +139,7 @@ func (this *AppCmd) Run(main func()) {
|
|||||||
this.runStop()
|
this.runStop()
|
||||||
return
|
return
|
||||||
case "restart":
|
case "restart":
|
||||||
this.runRestart()
|
this.RunRestart()
|
||||||
return
|
return
|
||||||
case "status":
|
case "status":
|
||||||
this.runStatus()
|
this.runStatus()
|
||||||
@@ -160,7 +160,7 @@ func (this *AppCmd) Run(main func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 日志
|
// 日志
|
||||||
writer := new(LogWriter)
|
var writer = new(LogWriter)
|
||||||
writer.Init()
|
writer.Init()
|
||||||
logs.SetWriter(writer)
|
logs.SetWriter(writer)
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ func (this *AppCmd) runStop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重启
|
// 重启
|
||||||
func (this *AppCmd) runRestart() {
|
func (this *AppCmd) RunRestart() {
|
||||||
this.runStop()
|
this.runStop()
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
this.runStart()
|
this.runStart()
|
||||||
|
|||||||
292
internal/utils/upgrade_manager.go
Normal file
292
internal/utils/upgrade_manager.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||||
|
"github.com/iwind/TeaGo/Tea"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
"github.com/iwind/TeaGo/types"
|
||||||
|
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpgradeFileWriter struct {
|
||||||
|
rawWriter io.Writer
|
||||||
|
written int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpgradeFileWriter(rawWriter io.Writer) *UpgradeFileWriter {
|
||||||
|
return &UpgradeFileWriter{rawWriter: rawWriter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeFileWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = this.rawWriter.Write(p)
|
||||||
|
this.written += int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeFileWriter) TotalWritten() int64 {
|
||||||
|
return this.written
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpgradeManager struct {
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
component string
|
||||||
|
|
||||||
|
newVersion string
|
||||||
|
contentLength int64
|
||||||
|
isDownloading bool
|
||||||
|
writer *UpgradeFileWriter
|
||||||
|
body io.ReadCloser
|
||||||
|
isCancelled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpgradeManager(component string) *UpgradeManager {
|
||||||
|
return &UpgradeManager{
|
||||||
|
component: component,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CheckRedirect: nil,
|
||||||
|
Jar: nil,
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeManager) Start() error {
|
||||||
|
if this.isDownloading {
|
||||||
|
return errors.New("another process is running")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDownloading = true
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
this.client.CloseIdleConnections()
|
||||||
|
this.isDownloading = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 检查unzip
|
||||||
|
unzipExe, _ := exec.LookPath("unzip")
|
||||||
|
if len(unzipExe) == 0 {
|
||||||
|
// TODO install unzip automatically or pack with a static 'unzip' file
|
||||||
|
return errors.New("can not find 'unzip' command")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查cp
|
||||||
|
cpExe, _ := exec.LookPath("cp")
|
||||||
|
if len(cpExe) == 0 {
|
||||||
|
return errors.New("can not find 'cp' command")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查新版本
|
||||||
|
var downloadURL = ""
|
||||||
|
{
|
||||||
|
var url = teaconst.UpdatesURL
|
||||||
|
var osName = runtime.GOOS
|
||||||
|
if Tea.IsTesting() && osName == "darwin" {
|
||||||
|
osName = "linux"
|
||||||
|
}
|
||||||
|
url = strings.ReplaceAll(url, "${os}", osName)
|
||||||
|
url = strings.ReplaceAll(url, "${arch}", runtime.GOARCH)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create url request failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := this.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("read latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("read latest version failed: invalid response code '" + types.String(resp.StatusCode) + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("read latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = maps.Map{}
|
||||||
|
err = json.Unmarshal(data, &m)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid response data: " + err.Error() + ", origin data: " + string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = m.GetInt("code")
|
||||||
|
if code != 200 {
|
||||||
|
return errors.New(m.GetString("message"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataMap = m.GetMap("data")
|
||||||
|
var downloadHost = dataMap.GetString("host")
|
||||||
|
var versions = dataMap.GetSlice("versions")
|
||||||
|
var downloadPath = ""
|
||||||
|
for _, component := range versions {
|
||||||
|
var componentMap = maps.NewMap(component)
|
||||||
|
if componentMap.Has("version") {
|
||||||
|
if componentMap.GetString("code") == this.component {
|
||||||
|
var version = componentMap.GetString("version")
|
||||||
|
if stringutil.VersionCompare(version, teaconst.Version) > 0 {
|
||||||
|
this.newVersion = version
|
||||||
|
downloadPath = componentMap.GetString("url")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(downloadPath) == 0 {
|
||||||
|
return errors.New("no latest version to download")
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL = downloadHost + downloadPath
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create download request failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := this.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("download failed: " + downloadURL + ": " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("download failed: " + downloadURL + ": invalid response code '" + types.String(resp.StatusCode) + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contentLength = resp.ContentLength
|
||||||
|
this.body = resp.Body
|
||||||
|
|
||||||
|
// download to tmp
|
||||||
|
var tmpDir = os.TempDir()
|
||||||
|
var filename = filepath.Base(downloadURL)
|
||||||
|
|
||||||
|
var destFile = tmpDir + "/" + filename
|
||||||
|
_ = os.Remove(destFile)
|
||||||
|
|
||||||
|
fp, err := os.Create(destFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create file failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// 删除安装文件
|
||||||
|
_ = os.Remove(destFile)
|
||||||
|
}()
|
||||||
|
|
||||||
|
this.writer = NewUpgradeFileWriter(fp)
|
||||||
|
|
||||||
|
_, err = io.Copy(this.writer, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
if this.isCancelled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("download failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = fp.Close()
|
||||||
|
|
||||||
|
// unzip
|
||||||
|
var unzipDir = tmpDir + "/edge-" + this.component + "-tmp"
|
||||||
|
stat, err := os.Stat(unzipDir)
|
||||||
|
if err == nil && stat.IsDir() {
|
||||||
|
err = os.RemoveAll(unzipDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("remove old dir '" + unzipDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var unzipCmd = exec.Command(unzipExe, "-q", "-o", destFile, "-d", unzipDir)
|
||||||
|
var unzipStderr = &bytes.Buffer{}
|
||||||
|
unzipCmd.Stderr = unzipStderr
|
||||||
|
err = unzipCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("unzip installation file failed: " + err.Error() + ": " + unzipStderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
installationFiles, err := filepath.Glob(unzipDir + "/edge-" + this.component + "/*")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("lookup installation files failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// cp to target dir
|
||||||
|
currentExe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("reveal current executable file path failed: " + err.Error())
|
||||||
|
}
|
||||||
|
var targetDir = filepath.Dir(filepath.Dir(currentExe))
|
||||||
|
if !Tea.IsTesting() {
|
||||||
|
for _, installationFile := range installationFiles {
|
||||||
|
var cpCmd = exec.Command(cpExe, "-R", "-f", installationFile, targetDir)
|
||||||
|
var cpStderr = &bytes.Buffer{}
|
||||||
|
cpCmd.Stderr = cpStderr
|
||||||
|
err = cpCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("overwrite installation files failed: '" + cpCmd.String() + "': " + cpStderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove tmp
|
||||||
|
_ = os.RemoveAll(unzipDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeManager) IsDownloading() bool {
|
||||||
|
return this.isDownloading
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeManager) Progress() float32 {
|
||||||
|
if this.contentLength <= 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if this.writer == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return float32(this.writer.TotalWritten()) / float32(this.contentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeManager) NewVersion() string {
|
||||||
|
return this.newVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *UpgradeManager) Cancel() error {
|
||||||
|
this.isCancelled = true
|
||||||
|
this.isDownloading = false
|
||||||
|
|
||||||
|
if this.body != nil {
|
||||||
|
_ = this.body.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
35
internal/utils/upgrade_manager_test.go
Normal file
35
internal/utils/upgrade_manager_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUpgradeManager(t *testing.T) {
|
||||||
|
var manager = utils.NewUpgradeManager("admin")
|
||||||
|
|
||||||
|
var ticker = time.NewTicker(2 * time.Second)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
if manager.IsDownloading() {
|
||||||
|
t.Logf("%.2f%%", manager.Progress()*100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
/**go func() {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
if manager.IsDownloading() {
|
||||||
|
t.Log("cancel downloading")
|
||||||
|
_ = manager.Cancel()
|
||||||
|
}
|
||||||
|
}()**/
|
||||||
|
|
||||||
|
err := manager.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user