Browse Source

发现使用frpc代理内网webui暴露到公网时, 由于webui中间连接的是frpc代理客户端, 不再直接是浏览器, 因此包含实时日志流的页面切换或离开时, 连接断开只通知到frpc, 而frpc不通知webui的go后端程序, 导致journalctl的实时日志流进程无法kill(本地局域网访问不会有该问题), 页面切换过多, 就会不断产生和占用rtu的pid资源, 造成浪费, 留下一些隐患,通过重新优化交互通信方案, 新增、修改相关代码, 已解决该问题

niujiuru 1 day ago
parent
commit
f8b3eb572b

+ 1 - 0
web-ui.service/web/static/js/app.js

@@ -24,6 +24,7 @@ document.addEventListener("DOMContentLoaded", () => {
 });
 
 function logout() {
+  disconnectLog() // 断开日志流连接, 避免在注销后, 日志流可能仍然占用PID资源
   window.location.href = "/api/auth/logout";
 }
 

+ 22 - 1
web-ui.service/web/static/js/log-stream.js

@@ -1,5 +1,6 @@
 let logSource = null;
 let currentUnit = null;
+let logPid = 0;
 
 const MAX_LOG_LINES = 800;
 const MIN_LOG_LINES = 500;
@@ -93,6 +94,13 @@ function connectLog(unit, target) {
         }
     };
 
+    source.addEventListener("pid", (e) => {
+        if (source !== logSource) {
+            return;
+        }
+        logPid = Number(e.data);
+    });
+
     source.onerror = () => {
         if (source !== logSource) {
             return;
@@ -110,6 +118,11 @@ function disconnectLog() {
         logSource = null;
     }
 
+    if (logPid > 0) {
+        fetch(`/api/service/log/close?pid=${logPid}`).catch(() => {});
+        logPid = 0;
+    }
+
     currentUnit = null;
     logLines = [];
 }
@@ -132,7 +145,11 @@ function clearLog(target) {
 }
 
 function copyLog(target) {
-    const text = logLines.join("\n");
+    const el = typeof target === "string"
+        ? document.getElementById(target)
+        : target;
+
+    const text = el ? el.textContent : logLines.join("\n");
 
     if (navigator.clipboard) {
         navigator.clipboard.writeText(text)
@@ -148,3 +165,7 @@ function copyLog(target) {
         alert("复制成功");
     }
 }
+
+["beforeunload", "pagehide"].forEach(e =>
+    window.addEventListener(e, disconnectLog)
+);

+ 54 - 1
web-ui.service/web_handler.go

@@ -9,7 +9,9 @@ import (
 	"net/http"
 	"os"
 	"os/exec"
+	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"hnyfkj.com.cn/rtu/linux/baseapp"
@@ -141,6 +143,8 @@ func (ui *WebUI) rootHandler(w http.ResponseWriter, r *http.Request) {
 	http.NotFound(w, r)
 }
 
+var logCmds sync.Map /* pid -> *exec.Cmd, 用于管理日志流进程, 以便在前端关闭日志流时, 后端可以
+   关闭对应的日志流进程, 解决frpc代理后, sse连接断开时, 日志流进程无法关闭的问题, 占用PID资源 */
 var logFiles = map[string]string{
 	"yfkj-camera-capture.service": "/opt/yfkj/camera-capture.service/log/camera-capture.log",
 }
@@ -200,12 +204,19 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	pid := cmd.Process.Pid
+	logCmds.Store(pid, cmd)
+
 	id := time.Now().UnixNano()
 	if true {
 		baseapp.Logger.Tracef("[服务日志流启动] id=%d unit=%s", id, unit)
 	}
 
+	fmt.Fprintf(w, "event: pid\ndata: %d\n\n", pid)
+	flusher.Flush() // 发送PID给前端, 让前端在关闭日志流时, 可以通知后端关闭对应的日志流进程
+
 	defer func() {
+		logCmds.Delete(pid)
 		if cmd.Process != nil {
 			err := cmd.Process.Kill()
 			baseapp.Logger.Tracef("[journalctl kill] pid=%d err=%v", cmd.Process.Pid, err)
@@ -242,7 +253,7 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 			if !ok {
 				return
 			}
-			if _, err := fmt.Fprintf(w, "data: %s\n\n", line); err != nil {
+			if _, err := fmt.Fprintf(w, "data: %s\n\n", line); err != nil { // 实时推送日志
 				return
 			}
 			flusher.Flush()
@@ -250,6 +261,48 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+func serviceLogCloseHandler(w http.ResponseWriter, r *http.Request) {
+	pid, err := strconv.Atoi(r.URL.Query().Get("pid"))
+	if err != nil || pid <= 0 {
+		http.Error(w, "invalid pid", http.StatusBadRequest)
+		return
+	}
+
+	v, ok := logCmds.Load(pid)
+	if !ok {
+		http.Error(w, "pid not found", http.StatusNotFound)
+		return
+	}
+
+	cmd := v.(*exec.Cmd)
+	if cmd.Process == nil {
+		logCmds.Delete(pid)
+		return
+	}
+
+	data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
+	if err != nil {
+		http.Error(w, "read cmdline failed", http.StatusInternalServerError)
+		return
+	}
+
+	cmdline := strings.ReplaceAll(string(data), "\x00", " ")
+	if !strings.Contains(cmdline, "journalctl") && !strings.Contains(cmdline, "tail") {
+		http.Error(w, "not log process", http.StatusBadRequest) // 只允许关闭日志流进程, 防止误杀其它进程
+		return
+	}
+
+	logCmds.Delete(pid)
+	err = cmd.Process.Kill()
+	baseapp.Logger.Tracef("[服务日志流关闭] pid=%d cmd=%s err=%v", pid, cmdline, err)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
 func runShellAndWrite(w http.ResponseWriter, cmd string) {
 	baseapp.Logger.Tracef("[执行命令] %s", cmd)
 

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

@@ -36,6 +36,11 @@ var routes = []Route{
 	},
 
 	{
+		"/api/service/log/close",
+		serviceLogCloseHandler,
+	},
+
+	{
 		"/api/service/control",
 		serviceControlHandler,
 	},