增加edge-admin upgrade命令

This commit is contained in:
GoEdgeLab
2022-09-06 21:19:37 +08:00
parent ce880ab8c2
commit 762d93772b
5 changed files with 373 additions and 8 deletions

BIN
cmd/edge-admin/edge-admin Executable file

Binary file not shown.

View File

@@ -7,17 +7,20 @@ import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/gen"
"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/gosock/pkg/gosock"
"log"
"time"
)
func main() {
app := apps.NewAppCmd().
var app = apps.NewAppCmd().
Version(teaconst.Version).
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]").
Option("-h", "show this help").
Option("-v", "show version").
@@ -30,7 +33,8 @@ func main() {
Option("recover", "enter recovery mode").
Option("demo", "switch to demo 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() {
nodes.NewAdminNode().Daemon()
@@ -115,8 +119,42 @@ func main() {
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() {
adminNode := nodes.NewAdminNode()
var adminNode = nodes.NewAdminNode()
adminNode.Run()
})
}

View File

@@ -123,7 +123,7 @@ func (this *AppCmd) On(arg string, callback func()) {
// Run 运行
func (this *AppCmd) Run(main func()) {
// 获取参数
args := os.Args[1:]
var args = os.Args[1:]
if len(args) > 0 {
switch args[0] {
case "-v", "version", "-version", "--version":
@@ -139,7 +139,7 @@ func (this *AppCmd) Run(main func()) {
this.runStop()
return
case "restart":
this.runRestart()
this.RunRestart()
return
case "status":
this.runStatus()
@@ -160,7 +160,7 @@ func (this *AppCmd) Run(main func()) {
}
// 日志
writer := new(LogWriter)
var writer = new(LogWriter)
writer.Init()
logs.SetWriter(writer)
@@ -210,7 +210,7 @@ func (this *AppCmd) runStop() {
}
// 重启
func (this *AppCmd) runRestart() {
func (this *AppCmd) RunRestart() {
this.runStop()
time.Sleep(1 * time.Second)
this.runStart()

View 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
}

View 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)
}
}