mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-07 23:30:26 +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"
|
||||
"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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
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