Forráskód Böngészése

1, 将暴露webui到公网的frpc添加到服务框架; 2, 优化page1页面和对应的后台服务updateServiceStatus()函数, 整体降低systemd占用cpu过高的问题; 3, 进行整体测试, 没发现什么问题

niujiuru 2 napja
szülő
commit
766865e558

+ 10 - 0
config/yfkj-frp-client.service

@@ -0,0 +1,10 @@
+[Unit]
+Description=Yunfei FRP Client(https://github.com/fatedier/frp)
+After=yfkj-networkd.service
+Wants=yfkj-networkd.service
+
+[Service]
+Type=simple
+ExecStart=/opt/yfkj/frp-client.service/frpc -c /opt/yfkj/frp-client.service/frpc.toml
+RuntimeMaxSec=1h
+Restart=no

+ 0 - 0
config/yfkj-upgrade.service


BIN
frpc/frpc-0.71.0.tar.gz


+ 2 - 1
frpc/readme.txt

@@ -1,7 +1,8 @@
 说明:
 frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。
 可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
+设备端运行frpc,目前放置的frpc-0.71.0.tar.gz已与公司在公网部署的frps对接联调过,暴露的设备公网地址如下:
+http://lyq.yfznscd.com:9182/
 
 网址:
 https://github.com/fatedier/frp
-

+ 120 - 47
web-ui.service/page1_handler.go

@@ -14,9 +14,11 @@ import (
 	"time"
 )
 
-var lastCPU struct {
-	idle, total uint64
-	mu          sync.Mutex
+var cpuStat struct {
+	sync.Mutex
+	lastIdle  uint64
+	lastTotal uint64
+	usage     float64
 }
 
 var systemInfoCache struct {
@@ -25,7 +27,7 @@ var systemInfoCache struct {
 }
 
 func initCPUUsage() {
-	getCPUUsage()
+	updateCPUUsage()
 }
 
 func updateSystemInfoCache() {
@@ -33,7 +35,7 @@ func updateSystemInfoCache() {
 	diskPercent, diskUsed, diskTotal := getDiskInfo()
 
 	data := map[string]any{
-		"cpu_usage": getCPUUsage(),
+		"cpu_usage": updateCPUUsage(),
 		"cpu_cores": getCPUCore(),
 		"cpu_freq":  getCPUFreq(),
 		"cpu_temp":  getCPUTemperature(),
@@ -74,53 +76,56 @@ func systemInfoHandler(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(data)
 }
 
-func getCPUUsage() int {
-	lastCPU.mu.Lock()
-	defer lastCPU.mu.Unlock()
+func updateCPUUsage() float64 {
+	cpuStat.Lock()
+	defer cpuStat.Unlock()
 
 	data, err := os.ReadFile("/proc/stat")
 	if err != nil {
-		return 0
+		return cpuStat.usage
 	}
 
-	f := strings.Fields(strings.Split(string(data), "\n")[0])
-	if len(f) < 5 {
-		return 0
+	fields := strings.Fields(strings.SplitN(string(data), "\n", 2)[0])
+
+	if len(fields) < 5 {
+		return cpuStat.usage
 	}
 
-	var idle, total uint64
+	var idle uint64
+	var total uint64
 
-	for i := 1; i < len(f); i++ {
-		v, err := strconv.ParseUint(f[i], 10, 64)
+	for i := 1; i < len(fields); i++ {
+		v, err := strconv.ParseUint(fields[i], 10, 64)
 		if err != nil {
 			continue
 		}
 
 		total += v
 
-		// idle + iowait
 		if i == 4 || i == 5 {
 			idle += v
 		}
 	}
 
-	if lastCPU.total == 0 {
-		lastCPU.idle = idle
-		lastCPU.total = total
+	if cpuStat.lastTotal == 0 {
+		cpuStat.lastTotal = total
+		cpuStat.lastIdle = idle
 		return 0
 	}
 
-	dt := total - lastCPU.total
-	di := idle - lastCPU.idle
+	deltaTotal := total - cpuStat.lastTotal
+	deltaIdle := idle - cpuStat.lastIdle
 
-	lastCPU.idle = idle
-	lastCPU.total = total
+	cpuStat.lastTotal = total
+	cpuStat.lastIdle = idle
 
-	if dt == 0 {
-		return 0
+	if deltaTotal == 0 {
+		return cpuStat.usage
 	}
 
-	return int((dt - di) * 100 / dt)
+	cpuStat.usage = float64(deltaTotal-deltaIdle) * 100 / float64(deltaTotal)
+
+	return cpuStat.usage
 }
 
 func getCPUCore() int {
@@ -219,9 +224,7 @@ func getMemInfo() (int, string, string) {
 
 	used := total - avail
 
-	return int(used * 100 / total),
-		formatSize(used * 1024),
-		formatSize(total * 1024)
+	return int(used * 100 / total), formatSize(used * 1024), formatSize(total * 1024)
 }
 
 func getDiskInfo() (int, string, string) {
@@ -340,10 +343,16 @@ var services = []struct {
 	{"yfkj-camera-capture.service", "/opt/yfkj/camera-capture.service", "camera-capture"},
 	{"yfkj-app-install.service", "/opt/yfkj/app-install.service", "app-install"},
 	{"yfkj-web-ui.service", "/opt/yfkj/web-ui.service", "web-ui"},
+	{"yfkj-frp-client.service", "/opt/yfkj/frp-client.service", "frpc"},
 	{"yfkj-upgrade.service", "/opt/yfkj/upgrade.service", "upgrade"},
 	{"yfkj-local-startup.service", "/opt/yfkj/local-startup.service", "startup.sh"},
 }
 
+var serviceStatusActive struct {
+	sync.Mutex
+	lastAccess time.Time
+}
+
 var serviceStatusCache struct {
 	sync.RWMutex
 	data map[string]string
@@ -352,26 +361,69 @@ var serviceStatusCache struct {
 func updateServiceStatus() {
 	data := make(map[string]string, len(services)*2)
 
-	for i, s := range services {
-		n := i + 1
+	args := []string{"show",
+		"-p", "Id",
+		"-p", "LoadState",
+		"-p", "ActiveState",
+		"-p", "SubState",
+	}
+
+	for _, s := range services {
+		args = append(args, s.name)
+	}
+
+	statusMap := make(map[string]string, len(services))
+
+	out, _ := exec.Command("systemctl", args...).CombinedOutput()
+
+	var name, loadState, activeState, subState string
+
+	save := func() {
+		if name == "" {
+			return
+		}
+
 		status := noValue
 
-		if out, err := exec.Command("systemctl", "show", s.name,
-			"-p", "LoadState",
-			"-p", "ActiveState",
-			"-p", "SubState").Output(); err == nil {
-			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 = "🟡已停止"
-			}
+		switch {
+		case activeState == "active" && subState == "running":
+			status = "🟢运行中"
+		case activeState == "active" && subState == "exited":
+			status = "🔵已完成"
+		case loadState == "loaded" && activeState == "inactive":
+			status = "🟡已停止"
 		}
 
-		data[fmt.Sprintf("var%d", n)] = status
+		statusMap[name] = status
+	}
+
+	for line := range strings.SplitSeq(string(out), "\n") {
+		if strings.HasPrefix(line, "Id=") {
+			save() // Save the previous service status before starting a new one
+
+			name = strings.TrimPrefix(line, "Id=")
+			loadState = ""
+			activeState = ""
+			subState = ""
+		} else if v, ok := strings.CutPrefix(line, "LoadState="); ok {
+			loadState = v
+		} else if v, ok := strings.CutPrefix(line, "ActiveState="); ok {
+			activeState = v
+		} else if v, ok := strings.CutPrefix(line, "SubState="); ok {
+			subState = v
+		}
+	}
+
+	save() // Save the last service status
+
+	for i, s := range services {
+		n := i + 1
+
+		if v, ok := statusMap[s.name]; ok {
+			data[fmt.Sprintf("var%d", n)] = v
+		} else {
+			data[fmt.Sprintf("var%d", n)] = noValue
+		}
 
 		if v, err := os.ReadFile(filepath.Join(s.dir, "version.txt")); err == nil {
 			data[fmt.Sprintf("var%d%d", n, n)] = "v" + strings.TrimSpace(string(v))
@@ -386,12 +438,24 @@ func updateServiceStatus() {
 }
 
 func startServiceCache() {
-	updateServiceStatus()
 	go func() {
 		ticker := time.NewTicker(5 * time.Second)
 		defer ticker.Stop()
+
 		for range ticker.C {
-			updateServiceStatus()
+			serviceStatusActive.Lock()
+
+			if !serviceStatusActive.lastAccess.IsZero() && time.Since(serviceStatusActive.lastAccess) >= 10*time.Second {
+				serviceStatusActive.lastAccess = time.Time{}
+			}
+
+			active := !serviceStatusActive.lastAccess.IsZero()
+
+			serviceStatusActive.Unlock()
+
+			if active {
+				updateServiceStatus()
+			}
 		}
 	}()
 }
@@ -402,6 +466,15 @@ func serviceStatusHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	serviceStatusActive.Lock()
+	first := serviceStatusActive.lastAccess.IsZero() || time.Since(serviceStatusActive.lastAccess) >= 10*time.Second
+	serviceStatusActive.lastAccess = time.Now()
+	serviceStatusActive.Unlock()
+
+	if first {
+		updateServiceStatus()
+	}
+
 	serviceStatusCache.RLock()
 	data := serviceStatusCache.data
 	serviceStatusCache.RUnlock()

+ 1 - 1
web-ui.service/page6_handler.go

@@ -96,7 +96,7 @@ func getUserAppStatus() (string, string) {
 		status = "🟡已停止"
 	}
 
-	for _, line := range strings.Split(v, "\n") {
+	for line := range strings.SplitSeq(v, "\n") {
 		if after, ok := strings.CutPrefix(line, "MainPID="); ok {
 			if after != "" && after != "0" {
 				pid = after

+ 22 - 7
web-ui.service/web/pages/page1.html

@@ -68,7 +68,7 @@
             <div class="info-name">开机任务执行</div>
             <div class="info-desc">local-startup.service</div>
           </div>
-          <span class="info-value" id="var9">--</span>
+          <span class="info-value" id="var10">--</span>
           <span class="info-value"></span>
           <span></span>
           <span></span>
@@ -166,7 +166,7 @@
 
         <div class="info-item">
           <div class="info-info">
-            <div class="info-name">统一配置管理</div>
+            <div class="info-name">本地配置管理</div>
             <div class="info-desc">web-ui.service</div>
           </div>
           <span class="info-value" id="var7">--</span>
@@ -181,15 +181,30 @@
 
         <div class="info-item">
           <div class="info-info">
-            <div class="info-name">开机自动升级</div>
-            <div class="info-desc">upgrade.service</div>
+            <div class="info-name">公网配置管理</div>
+            <div class="info-desc">web-ui.service</div>
           </div>
           <span class="info-value" id="var8">--</span>
           <span class="info-value" id="var88">--</span>
-          <button class="btn" style="margin-right:02px;" onclick="switchAutoUpgrade('enable')">
-            打开
+          <button class="btn" style="margin-right:32px;" onclick="startPublicAccess(event)">
+            开启公网访问
+          </button>
+          <button class="btn" style="margin-right:32px;" onclick="stopPublicAccess(event)">
+            关闭公网访问
+          </button>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">开机自动升级</div>
+            <div class="info-desc">upgrade.service</div>
+          </div>
+          <span class="info-value" id="var9">--</span>
+          <span class="info-value" id="var99">--</span>
+          <button class="btn" style="margin-right:02px;" onclick="switchAutoUpgrade(event, 'enable')">
+            开启
           </button>
-          <button class="btn" style="margin-right:32px;" onclick="switchAutoUpgrade('disable')">
+          <button class="btn" style="margin-right:32px;" onclick="switchAutoUpgrade(event, 'disable')">
             关闭
           </button>
           <button class="btn" onclick="downloadUpgradeLogFile()">

+ 122 - 23
web-ui.service/web/static/js/page1.js

@@ -30,7 +30,7 @@ function getSystemInfo(){
           fontSize:15
         }
       }],
-      series:[{data:[{value:d.cpu_usage ?? 0}]}]
+      series:[{data:[{value:Number((d.cpu_usage ?? 0).toFixed(1))}]}]
     });
 
     memChart.setOption({
@@ -145,8 +145,8 @@ function getServiceInfo(){
       [
         "var1","var2","var3","var4","var5",
         "var6","var7","var8","var9",
-        "var11","var22","var33","var44",
-        "var55","var66","var77","var88"
+        "var10","var11","var22","var33","var44",
+        "var55","var66","var77","var88","var99",
       ].forEach(id => setText(id, data[id], "page1"));
     })
     .catch(e => {
@@ -155,8 +155,8 @@ function getServiceInfo(){
       [
         "var1","var2","var3","var4","var5",
         "var6","var7","var8","var9",
-        "var11","var22","var33","var44",
-        "var55","var66","var77","var88"
+        "var10","var11","var22","var33","var44",
+        "var55","var66","var77","var88","var99"
       ].forEach(id => setText(id, null, "page1"));
     });
 }
@@ -195,7 +195,8 @@ const serviceNames = {
   "yfkj-camera-capture.service":   "相机图像采集",
   "yfkj-sshd-mqtt-bridge.service": "远程运维通道",
   "yfkj-app-install.service":      "应用安装卸载",
-  "yfkj-web-ui.service":           "统一配置管理",
+  "yfkj-web-ui.service":           "本地配置管理",
+  "yfkj-frp-client.service":       "公网配置管理",
   "yfkj-upgrade.service":          "开机自动升级",
 };
 
@@ -237,38 +238,136 @@ function switchServiceVersion(service, action)
   });
 }
 
-function switchAutoUpgrade(action)
-{
+function switchAutoUpgrade(event, action) {
   let msg = action === "enable"
-    ? "确认要打开 【开机自动升级】 吗?"
+    ? "确认要启用 【开机自动升级】 吗?"
     : "确认要关闭 【开机自动升级】 吗?";
 
-  if(!confirm(msg)){
+  if (!confirm(msg)) {
     return;
   }
 
   let password = prompt("请输入操作密码:");
-  if(password === null){
+
+  if (password !== "yfkj123456") {
+    alert("密码错误");
     return;
   }
-  password = password.trim();
 
-  fetch("/api/upgrade/on-off", {
-    method:"POST",
-    headers:{
-      "Content-Type":"application/json"
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/service/control", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
     },
-    body:JSON.stringify({
-      action:action,
-      password:password
+    body: JSON.stringify({
+      name: "yfkj-upgrade.service",
+      action: action,
+      secret: "yfkj!@#$%^"
     })
   })
-  .then(r=>r.json())
-  .then(d=>{
-    alert(`【开机自动升级】\n\n${d.status=="ok" ? (action=="enable" ? "✅ 已打开" : "✅ 已关闭") : "❌ 操作失败: " + d.message}`);
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+
+    return r.json();
   })
-  .catch(e=>{
+  .then(d => {
+    if (d.ok) {
+      alert(`【开机自动升级】\n\n${action === "enable" ? "✅ 已启用" : "✅ 已禁用"}`);
+    } else {
+      alert("❌ 操作失败");
+    }
+  })
+  .catch(e => {
     alert(`【开机自动升级】\n\n❌ 请求失败: ${e.message}`);
+  })
+  .finally(() => {
+    btn.disabled = false;
+  });
+}
+
+function startPublicAccess(event) {
+  if (!confirm("确定开启公网访问吗?")) {
+    return;
+  }
+
+  let password = prompt("请输入操作密码:");
+
+  if (password !== "yfkj123456") {
+    alert("密码错误");
+    return;
+  }
+
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/service/control", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify({
+      name: "yfkj-frp-client.service",
+      action: "start",
+      secret: "yfkj!@#$%^"
+    })
+  })
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+
+    return r.json();
+  })
+  .then(d => {
+    alert(d.ok ? "开启公网访问成功" : "开启公网访问失败");
+  })
+  .catch(e => {
+    alert("开启公网访问失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+  });
+}
+
+function stopPublicAccess(event) {
+  if (!confirm("确定关闭公网访问吗?")) {
+    return;
+  }
+
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/service/control", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify({
+      name: "yfkj-frp-client.service",
+      action: "stop",
+      secret: "yfkj!@#$%^"
+    })
+  })
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+
+    return r.json();
+  })
+  .then(d => {
+    alert(d.ok ? "关闭公网访问成功" : "关闭公网访问失败");
+  })
+  .catch(e => {
+    alert("关闭公网访问失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
   });
 }
 

+ 33 - 0
web-ui.service/web_handler.go

@@ -382,3 +382,36 @@ func serviceStatus(name string) (SystemdServiceStatus, error) {
 
 	return st, nil
 }
+
+func systemctl(args ...string) error {
+	out, err := exec.Command("systemctl", args...).CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("systemctl %v failed: %v %s", args, err, string(out))
+	}
+	return nil
+}
+
+func serviceControlHandler(w http.ResponseWriter, r *http.Request) {
+	var req struct {
+		Name   string `json:"name"`
+		Action string `json:"action"`
+		Secret string `json:"secret"`
+	}
+
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "参数错误", http.StatusBadRequest)
+		return
+	}
+
+	if req.Secret != "yfkj!@#$%^" {
+		http.Error(w, "密码错误", http.StatusForbidden)
+		return
+	}
+
+	if err := systemctl(req.Action, req.Name); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	json.NewEncoder(w).Encode(map[string]bool{"ok": true})
+}

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

@@ -36,6 +36,11 @@ var routes = []Route{
 	},
 
 	{
+		"/api/service/control",
+		serviceControlHandler,
+	},
+
+	{
 		"/api/system/info",
 		systemInfoHandler,
 	},