Bläddra i källkod

优化app-install.service日志打印和编写对应的前端page6.js、page6_handler.go代码

niujiuru 4 dagar sedan
förälder
incheckning
de810f80fa

BIN
app-install.service/hello.tar.gz


+ 5 - 5
app-install.service/rpc_handler.go

@@ -136,7 +136,7 @@ func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
 		return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
 	}
 
-	baseapp.Logger.Infof("[PkgImport] 导入用户应用安装包成功: %s, 版本: %s, 描述: %s",
+	baseapp.Logger.Infof("导入用户应用安装包成功: %s, 版本: %s, 描述: %s",
 		appIns.App.Name, appIns.App.Version, appIns.App.Description)
 
 	return jsonrpc2.BuildResponse(req, "success", nil)
@@ -169,7 +169,7 @@ func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
 		return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
 	}
 
-	baseapp.Logger.Infof("[PkgRemove] 删除用户应用安装包成功: %s, 版本: %s, 描述: %s",
+	baseapp.Logger.Infof("删除用户应用安装包成功: %s, 版本: %s, 描述: %s",
 		appIns.App.Name, appIns.App.Version, appIns.App.Description)
 
 	return jsonrpc2.BuildResponse(req, "success", nil)
@@ -200,7 +200,7 @@ func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
 
 	defer func() {
 		if !success {
-			baseapp.Logger.Warnf("[AppInstall] rollback: %s", userAppInsDir)
+			baseapp.Logger.Warnf("安装用户应用失败状态回滚: %s", userAppInsDir)
 
 			systemctlIgnoreError("stop", "yfkj-user-app.service")
 			systemctlIgnoreError("disable", "yfkj-user-app.service")
@@ -269,7 +269,7 @@ func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
 
 	success = true
 
-	baseapp.Logger.Infof("[AppInstall] 安装用户应用成功: %s, 版本: %s, 描述: %s",
+	baseapp.Logger.Infof("安装用户应用成功: %s, 版本: %s, 描述: %s",
 		appIns.App.Name, appIns.App.Version, appIns.App.Description)
 
 	return jsonrpc2.BuildResponse(req, "success", nil)
@@ -324,7 +324,7 @@ func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response
 		return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
 	}
 
-	baseapp.Logger.Infof("[AppUninstall] 卸载用户应用成功: %s, 版本: %s, 描述: %s",
+	baseapp.Logger.Infof("卸载用户应用成功: %s, 版本: %s, 描述: %s",
 		appIns.App.Name, appIns.App.Version, appIns.App.Description)
 
 	return jsonrpc2.BuildResponse(req, "success", nil)

+ 168 - 0
web-ui.service/page6_handler.go

@@ -1,17 +1,185 @@
 package main
 
 import (
+	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"hnyfkj.com.cn/rtu/linux/baseapp"
 )
 
 const maxUploadSize = 1 << 30 // 1GB
 
+func downloadAppInstallLog(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	filename := fmt.Sprintf("app-install.log.%s.tar.gz", time.Now().Format("20060102150405"))
+	tmpFile := fmt.Sprintf("/tmp/%d.tar.gz", time.Now().UnixNano())
+
+	defer os.Remove(tmpFile)
+
+	cmd := exec.Command(
+		"tar",
+		"--warning=no-file-changed",
+		"-czf",
+		tmpFile,
+		"-C",
+		"/opt/yfkj/app-install.service/log",
+		".",
+	)
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 {
+			http.Error(w, string(out), http.StatusInternalServerError)
+			return
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/gzip")
+	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
+
+	http.ServeFile(w, r, tmpFile)
+}
+
+func restartAppInstallService(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	cmd := exec.Command("systemctl", "restart", "yfkj-app-install.service")
+
+	if out, err := cmd.CombinedOutput(); err != nil {
+		http.Error(w, string(out)+err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte(`{"status":"ok"}`))
+}
+
+func getUserAppStatus() (string, string) {
+	status := noValue
+	pid := noValue
+
+	out, err := exec.Command("systemctl", "show", "yfkj-user-app.service",
+		"-p", "LoadState",
+		"-p", "ActiveState",
+		"-p", "SubState",
+		"-p", "MainPID").Output()
+	if err != nil {
+		return status, pid
+	}
+
+	v := string(out)
+	switch {
+	case strings.Contains(v, "ActiveState=active") &&
+		strings.Contains(v, "SubState=running"):
+		status = "🟢运行中"
+
+	case strings.Contains(v, "ActiveState=active") &&
+		strings.Contains(v, "SubState=exited"):
+		status = "🔵已完成"
+
+	case strings.Contains(v, "LoadState=loaded") &&
+		strings.Contains(v, "ActiveState=inactive"):
+		status = "🟡已停止"
+	}
+
+	for _, line := range strings.Split(v, "\n") {
+		if after, ok := strings.CutPrefix(line, "MainPID="); ok {
+			if after != "" && after != "0" {
+				pid = after
+			}
+			break
+		}
+	}
+
+	return status, pid
+}
+
+func appInstallStatusHandler(w http.ResponseWriter, r *http.Request) {
+	var info *struct {
+		Pkg struct {
+			FileName   string `json:"fileName"`
+			FileSize   int64  `json:"fileSize"`
+			FileMD5    string `json:"fileMD5"`
+			UploadTime string `json:"uploadTime"`
+		} `json:"pkg"`
+		App struct {
+			Name        string `json:"name"`
+			Version     string `json:"version"`
+			Executable  string `json:"executable"`
+			Description string `json:"description"`
+			ReadmeFile  string `json:"readmeFile"`
+		} `json:"app"`
+		Ins struct {
+			Installed   bool   `json:"installed"`
+			InstallTime string `json:"installTime"`
+		} `json:"ins"`
+	}
+
+	callErr := callRPCResult(r.Context(), 7004, "core.getInstallInfo", nil, &info)
+
+	data := map[string]string{
+		"state": "0",
+		"avar1": noValue,
+		"avar2": noValue,
+		"avar3": noValue,
+		"avar4": noValue,
+		"bvar1": noValue,
+		"bvar2": noValue,
+		"bvar3": noValue,
+		"bvar4": noValue,
+		"bvar5": noValue,
+		"cvar1": noValue,
+		"cvar2": noValue,
+		"cvar3": noValue,
+		"cvar4": noValue,
+		"cvar5": noValue,
+	}
+
+	if callErr == nil {
+		data["avar1"] = "🟢正常"
+
+		if info != nil {
+			data["bvar1"] = info.Pkg.FileName
+			data["bvar2"] = fmt.Sprintf("%d 字节", info.Pkg.FileSize)
+			data["bvar3"] = info.Pkg.FileMD5
+			data["bvar4"] = info.Pkg.UploadTime
+
+			data["cvar1"] = info.App.Name
+			data["cvar2"] = info.App.Version
+			data["cvar3"] = info.App.Executable
+			data["cvar4"] = info.Ins.InstallTime
+			data["cvar5"] = info.App.ReadmeFile
+
+			if info.Ins.Installed {
+				data["state"] = "2"
+				data["avar2"] = info.App.Name
+				data["avar3"], data["avar4"] = getUserAppStatus()
+				data["bvar5"] = "已安装"
+			} else {
+				data["state"] = "1"
+				data["bvar5"] = "未安装"
+			}
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(data)
+}
+
 func uploadUserAPP(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
 		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)

+ 2 - 2
web-ui.service/web/pages/page6.html

@@ -71,9 +71,9 @@
           <div class="card-header">
             <h2>用户应用</h2>
             <div class="log-actions">
-              <button class="btn" id="btn4" title="使用当前安装包安装用户应用,安装完成后自动启动,并设置为开机自启动" onclick="onBtn3Click()">安装</button>
+              <button class="btn" id="btn3" title="使用当前安装包安装用户应用,安装完成后自动启动,并设置为开机自启动" onclick="onBtn3Click()">安装</button>
               <span style="margin:0 1px;"></span>
-              <button class="btn" id="btn5" title="卸载当前已安装的用户应用程序" onclick="onBtn4Click()">卸载</button>
+              <button class="btn" id="btn4" title="卸载当前已安装的用户应用程序" onclick="onBtn4Click()">卸载</button>
             </div>
           </div>
           <div class="card-body">

+ 205 - 0
web-ui.service/web/static/js/page6.js

@@ -1,3 +1,204 @@
+let appInsLogLevel = "";
+function setAppInstallLogLevel() {
+  const level = logLevel.value;
+
+  fetch("/api/app-install/loglevel", {
+    method: "POST",
+    headers: {"Content-Type": "application/json"},
+    body: JSON.stringify({level})
+  })
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    appInsLogLevel = level;
+  })
+  .catch(e => {
+    logLevel.value = appInsLogLevel;
+  });
+}
+
+function getAppInstallLevel() {
+  fetch("/api/app-install/loglevel")
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    return r.json();
+  })
+  .then(d => {
+    appInsLogLevel = d.log_level;
+    logLevel.value = d.log_level;
+  })
+  .catch(e => {
+    appInsLogLevel = "";
+    logLevel.value = "";
+  });
+}
+
+function downloadAppInstallLogFile(event) {
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/app-install/logfile")
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error("HTTP " + r.status);
+    }
+
+    const disposition = r.headers.get("Content-Disposition");
+    let filename = "app-install.log.tar.gz";
+    if (disposition) {
+      const match = disposition.match(/filename="?([^";]+)"?/);
+      if (match) {
+        filename = match[1];
+      }
+    }
+
+    return {
+      blob: await r.blob(),
+      filename
+    };
+  })
+  .then(({ blob, filename }) => {
+    const url = URL.createObjectURL(blob);
+
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = filename;
+
+    document.body.appendChild(a);
+    a.click();
+    a.remove();
+
+    setTimeout(() => {
+      URL.revokeObjectURL(url);
+    }, 1000);
+
+    alert("下载成功");
+  })
+  .catch(e => {
+    console.error("download failed:", e);
+    alert("下载失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+    btn.textContent = "下载";
+  });
+}
+
+function restartAppInstallService(event) {
+  if (!confirm("确定重启应该管理服务吗?")) {
+    return;
+  }
+
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/app-install/restart", {
+    method: "POST"
+  })
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+    logLevel.value = "info";
+    appInsLogLevel = logLevel.value;
+    disconnectLog();
+    connectLog("yfkj-app-install.service", "text1");
+    alert("重启成功");
+  })
+  .catch(e => {
+    console.error("restart failed:", e);
+    alert("重启失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+  });
+}
+
+let appInsStatusTimer = null;
+function getAppInstallStatus() {
+  fetch("/api/app-install/status")
+    .then(async r => {
+      if (!r.ok) {
+        throw new Error("HTTP " + r.status);
+      }
+
+      return r.json();
+    })
+    .then(data => {
+      [
+        "avar1", "avar2", "avar3", "avar4",
+        "bvar1", "bvar2", "bvar3", "bvar4", "bvar5",
+        "cvar1", "cvar2", "cvar3", "cvar4", "cvar5"
+      ].forEach(id => {
+        setText(id, data[id], "page6");
+      });
+
+      updateAppInstallButtons(Number(data.state));
+    })
+    .catch(e => {
+      console.error("getAppInstallStatus() failed:", e);
+
+      [
+        "avar1", "avar2", "avar3", "avar4",
+        "bvar1", "bvar2", "bvar3", "bvar4", "bvar5",
+        "cvar1", "cvar2", "cvar3", "cvar4", "cvar5"
+      ].forEach(id => {
+        setText(id, null, "page6");
+      });
+
+      updateAppInstallButtons(-1);
+    });
+
+  getAppInstallLevel();
+}
+
+function startAppInstallStatusRefresh() {
+  if (appInsStatusTimer) {
+    return;
+  }
+
+  getAppInstallStatus();
+
+  appInsStatusTimer = setInterval(() => {
+    getAppInstallStatus();
+  }, 5000);
+}
+
+function stopAppInstallStatusRefresh() {
+  if (appInsStatusTimer) {
+    clearInterval(appInsStatusTimer);
+    appInsStatusTimer = null;
+  }
+}
+
+function updateAppInstallButtons(state) {
+  switch (state) {
+   case 0: // 无安装包, 等待上传
+    setBtnStatus("btn1", true,  "page6")
+    setBtnStatus("btn2", false, "page6")
+    setBtnStatus("btn3", false, "page6")
+    setBtnStatus("btn4", false, "page6")
+    break;
+   case 1: // 有安装包, 未安装时
+    setBtnStatus("btn1", false, "page6")
+    setBtnStatus("btn2", true,  "page6")
+    setBtnStatus("btn3", true,  "page6")
+    setBtnStatus("btn4", false, "page6")
+    break;
+   case 2: // 有安装包, 已安装时
+    setBtnStatus("btn1", false, "page6")
+    setBtnStatus("btn2", false, "page6")
+    setBtnStatus("btn3", false, "page6")
+    setBtnStatus("btn4", true,  "page6")
+    break;
+   default:
+    setBtnStatus("btn1", true,  "page6")
+    setBtnStatus("btn2", true,  "page6")
+    setBtnStatus("btn3", true,  "page6")
+    setBtnStatus("btn4", true,  "page6")
+    break;
+  }
+}
+
 let userAppUploading = false;
 
 function onBtn1Click() {
@@ -68,7 +269,11 @@ function uploadUserAppFile(input) {
 }
 
 function initPage6() {
+  startAppInstallStatusRefresh()
+  connectLog("yfkj-app-install.service", "text1");
 }
 
 function exitPage6() {
+  stopAppInstallStatusRefresh()
+  disconnectLog();
 }

+ 20 - 0
web-ui.service/web_route.go

@@ -174,6 +174,26 @@ var routes = []Route{
 	},
 
 	{
+		"/api/app-install/loglevel",
+		serviceLogLevelHandler(7004),
+	},
+
+	{
+		"/api/app-install/logfile",
+		downloadAppInstallLog,
+	},
+
+	{
+		"/api/app-install/restart",
+		restartAppInstallService,
+	},
+
+	{
+		"/api/app-install/status",
+		appInstallStatusHandler,
+	},
+
+	{
 		"/api/app-install/uploadUserAPP",
 		uploadUserAPP,
 	},