diff --git a/internal/web/actions/default/dashboard/dashboardutils/utils.go b/internal/web/actions/default/dashboard/dashboardutils/utils.go new file mode 100644 index 00000000..47b28cd5 --- /dev/null +++ b/internal/web/actions/default/dashboard/dashboardutils/utils.go @@ -0,0 +1,169 @@ +// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package dashboardutils + +import ( + "bytes" + "context" + "encoding/json" + teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" + "github.com/TeaOSLab/EdgeAdmin/internal/rpc" + "github.com/TeaOSLab/EdgeAdmin/internal/utils/sizes" + "github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" + stringutil "github.com/iwind/TeaGo/utils/string" + "github.com/shirou/gopsutil/v3/disk" + "os" + "os/exec" + "regexp" + "runtime" +) + +// CheckDiskPartitions 检查服务器磁盘空间 +func CheckDiskPartitions(thresholdPercent float64) (path string, usage uint64, usagePercent float64, shouldWarning bool) { + partitions, err := disk.Partitions(false) + if err != nil { + return + } + if !lists.ContainsString([]string{"darwin", "linux", "freebsd"}, runtime.GOOS) { + return + } + + var rootFS = "" + + for _, p := range partitions { + if p.Mountpoint == "/" { + rootFS = p.Fstype + break + } + } + + for _, p := range partitions { + if p.Mountpoint == "/boot" { + continue + } + if p.Fstype != rootFS { + continue + } + stat, _ := disk.Usage(p.Mountpoint) + if stat != nil { + if stat.Used < 2*uint64(sizes.G) { + continue + } + if stat.UsedPercent > thresholdPercent { + path = stat.Path + usage = stat.Used + usagePercent = stat.UsedPercent + shouldWarning = true + break + } + } + } + + return +} + +// CheckLocalAPINode 检查本地的API节点 +func CheckLocalAPINode(rpcClient *rpc.RPCClient, ctx context.Context) (exePath string, runtimeVersion string, fileVersion string, ok bool) { + resp, err := rpcClient.APINodeRPC().FindCurrentAPINode(ctx, &pb.FindCurrentAPINodeRequest{}) + if err != nil { + return + } + if resp.ApiNode == nil { + return + } + var instanceCode = resp.ApiNode.InstanceCode + if len(instanceCode) == 0 { + return + } + var statusJSON = resp.ApiNode.StatusJSON + if len(statusJSON) == 0 { + return + } + + var status = &nodeconfigs.NodeStatus{} + err = json.Unmarshal(statusJSON, status) + if err != nil { + return + } + runtimeVersion = status.BuildVersion + + if len(runtimeVersion) == 0 { + return + } + + if stringutil.VersionCompare(runtimeVersion, teaconst.APINodeVersion) >= 0 { + return + } + + exePath = status.ExePath + if len(exePath) == 0 { + return + } + + stat, err := os.Stat(exePath) + if err != nil { + return + } + if stat.IsDir() { + return + } + + // 实例信息 + { + var outputBuffer = &bytes.Buffer{} + var cmd = exec.Command(exePath, "instance") + cmd.Stdout = outputBuffer + err = cmd.Run() + if err != nil { + return + } + + var outputBytes = outputBuffer.Bytes() + if len(outputBytes) == 0 { + return + } + + var instanceMap = maps.Map{} + err = json.Unmarshal(bytes.TrimSpace(outputBytes), &instanceMap) + if err != nil { + return + } + + if instanceMap.GetString("code") != instanceCode { + return + } + } + + // 文件版本 + { + var outputBuffer = &bytes.Buffer{} + var cmd = exec.Command(exePath, "-v") + cmd.Stdout = outputBuffer + err = cmd.Run() + if err != nil { + return + } + + var outputString = outputBuffer.String() + if len(outputString) == 0 { + return + } + + var subMatch = regexp.MustCompile(`\s+v([\d.]+)\s+`).FindStringSubmatch(outputString) + if len(subMatch) == 0 { + return + } + fileVersion = subMatch[1] + + // 文件版本是否为最新 + if fileVersion != teaconst.APINodeVersion { + fileVersion = runtimeVersion + } + } + + ok = true + return +} diff --git a/internal/web/actions/default/dashboard/index.go b/internal/web/actions/default/dashboard/index.go index 26d8c362..da636d24 100644 --- a/internal/web/actions/default/dashboard/index.go +++ b/internal/web/actions/default/dashboard/index.go @@ -7,16 +7,13 @@ import ( "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const" "github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils" - "github.com/TeaOSLab/EdgeAdmin/internal/utils/sizes" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard/dashboardutils" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" - "github.com/iwind/TeaGo/lists" "github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/types" - "github.com/shirou/gopsutil/v3/disk" "regexp" - "runtime" ) type IndexAction struct { @@ -66,7 +63,7 @@ func (this *IndexAction) RunPost(params struct{}) { // 检查当前服务器空间 var diskUsageWarning = "" - diskPath, diskUsage, diskUsagePercent, shouldWarning := this.checkDiskPartitions(90) + diskPath, diskUsage, diskUsagePercent, shouldWarning := dashboardutils.CheckDiskPartitions(90) if shouldWarning { diskUsageWarning = "当前服务器磁盘空间不足,请立即扩充容量,文件路径:" + diskPath + ",已使用:" + types.String(diskUsage/1024/1024/1024) + "G,已使用比例:" + fmt.Sprintf("%.2f%%", diskUsagePercent) + ",仅剩余空间:" + fmt.Sprintf("%.2f%%", 100-diskUsagePercent) + "。" } @@ -263,49 +260,18 @@ func (this *IndexAction) RunPost(params struct{}) { this.Data["metricCharts"] = chartMaps } + // 当前API节点版本 + { + exePath, runtimeVersion, fileVersion, ok := dashboardutils.CheckLocalAPINode(this.RPC(), this.AdminContext()) + if ok { + this.Data["localLowerVersionAPINode"] = maps.Map{ + "exePath": exePath, + "runtimeVersion": runtimeVersion, + "fileVersion": fileVersion, + "isRestarting": false, + } + } + } + this.Success() } - -// 检查服务器磁盘空间 -func (this *IndexAction) checkDiskPartitions(thresholdPercent float64) (path string, usage uint64, usagePercent float64, shouldWarning bool) { - partitions, err := disk.Partitions(false) - if err != nil { - return - } - if !lists.ContainsString([]string{"darwin", "linux", "freebsd"}, runtime.GOOS) { - return - } - - var rootFS = "" - - for _, p := range partitions { - if p.Mountpoint == "/" { - rootFS = p.Fstype - break - } - } - - for _, p := range partitions { - if p.Mountpoint == "/boot" { - continue - } - if p.Fstype != rootFS { - continue - } - stat, _ := disk.Usage(p.Mountpoint) - if stat != nil { - if stat.Used < 2*uint64(sizes.G) { - continue - } - if stat.UsedPercent > thresholdPercent { - path = stat.Path - usage = stat.Used - usagePercent = stat.UsedPercent - shouldWarning = true - break - } - } - } - - return -} diff --git a/internal/web/actions/default/dashboard/init.go b/internal/web/actions/default/dashboard/init.go index 23b5de30..e9ddd1cf 100644 --- a/internal/web/actions/default/dashboard/init.go +++ b/internal/web/actions/default/dashboard/init.go @@ -12,6 +12,7 @@ func init() { Data("teaMenu", "dashboard"). Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)). GetPost("", new(IndexAction)). + Post("/restartLocalAPINode", new(RestartLocalAPINodeAction)). EndAll() }) } diff --git a/internal/web/actions/default/dashboard/restartLocalAPINode.go b/internal/web/actions/default/dashboard/restartLocalAPINode.go new file mode 100644 index 00000000..dcfdf242 --- /dev/null +++ b/internal/web/actions/default/dashboard/restartLocalAPINode.go @@ -0,0 +1,73 @@ +// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package dashboard + +import ( + "bytes" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "os/exec" + "regexp" + "time" +) + +type RestartLocalAPINodeAction struct { + actionutils.ParentAction +} + +func (this *RestartLocalAPINodeAction) RunPost(params struct { + ExePath string +}) { + // 检查当前用户是超级用户 + adminResp, err := this.RPC().AdminRPC().FindEnabledAdmin(this.AdminContext(), &pb.FindEnabledAdminRequest{AdminId: this.AdminId()}) + if err != nil { + this.ErrorPage(err) + return + } + if adminResp.Admin == nil || !adminResp.Admin.IsSuper { + this.Fail("请切换到超级用户进行此操作") + } + + var exePath = params.ExePath + if len(exePath) == 0 { + this.Fail("找不到要重启的API节点文件") + } + + { + var stdoutBuffer = &bytes.Buffer{} + var cmd = exec.Command(exePath, "restart") + cmd.Stdout = stdoutBuffer + err = cmd.Run() + if err != nil { + this.Fail("运行失败:输出:" + stdoutBuffer.String()) + } + } + + // 检查是否已启动 + var countTries = 120 + for { + countTries-- + if countTries < 0 { + this.Fail("启动超时,请尝试手动启动") + break + } + + var stdoutBuffer = &bytes.Buffer{} + var cmd = exec.Command(exePath, "status") + cmd.Stdout = stdoutBuffer + err = cmd.Run() + if err != nil { + time.Sleep(1 * time.Second) + continue + } + + if regexp.MustCompile(`pid:\s*\d+`). + MatchString(stdoutBuffer.String()) { + break + } + + time.Sleep(1 * time.Second) + } + + this.Success() +} diff --git a/web/views/@default/dashboard/index.html b/web/views/@default/dashboard/index.html index e2e48a57..13a17d5e 100644 --- a/web/views/@default/dashboard/index.html +++ b/web/views/@default/dashboard/index.html @@ -23,16 +23,26 @@ - +
升级提醒:有 {{nodeUpgradeInfo.count}} 个边缘节点需要升级到 v{{nodeUpgradeInfo.version}} 版本,系统正在尝试自动升级...
+ +
升级提醒:有 {{apiNodeUpgradeInfo.count}} 个API节点需要升级到 v{{apiNodeUpgradeInfo.version}} 版本;如果已经升级,请尝试重启API节点进程。
+ +
+ + 升级提醒:发现一个正在使用的本地API节点版本需要升级,文件位置:{{localLowerVersionAPINode.exePath}},当前版本:v{{localLowerVersionAPINode.runtimeVersion}}。 + 升级提醒:发现一个正在使用的本地API节点版本文件已经更新,但需要重启后生效,文件位置:{{localLowerVersionAPINode.exePath}}。 [帮我重启]尝试重启中... + +
+
diff --git a/web/views/@default/dashboard/index.js b/web/views/@default/dashboard/index.js index 57a060bf..0dd04450 100644 --- a/web/views/@default/dashboard/index.js +++ b/web/views/@default/dashboard/index.js @@ -3,6 +3,7 @@ Tea.context(function () { this.trafficTab = "hourly" this.metricCharts = [] this.dashboard = {} + this.localLowerVersionAPINode = null this.$delay(function () { this.$post("$") @@ -193,4 +194,29 @@ Tea.context(function () { } } } + + // 重启本地API节点 + this.isRestartingLocalAPINode = false + this.restartAPINode = function () { + if (this.isRestartingLocalAPINode) { + return + } + if (this.localLowerVersionAPINode == null) { + return + } + this.isRestartingLocalAPINode = true + this.localLowerVersionAPINode.isRestarting = true + this.$post("/dashboard/restartLocalAPINode") + .params({ + "exePath": this.localLowerVersionAPINode.exePath + }) + .timeout(300) + .success(function () { + teaweb.reload() + }) + .done(function () { + this.isRestartingLocalAPINode = false + this.localLowerVersionAPINode.isRestarting = false + }) + } })