Compare commits

...

2 Commits

Author SHA1 Message Date
MengMengCode 49d5a65357 release: v1.0.8 2026-06-06 12:57:46 +08:00
MengMengCode 8f32765ffe 修复了一些问题 2026-06-06 12:57:17 +08:00
3 changed files with 145 additions and 38 deletions
+143 -22
View File
@@ -1,6 +1,7 @@
package lxc package lxc
import ( import (
"bufio"
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
@@ -105,19 +106,23 @@ func (m *Manager) updateAllRates() {
} }
lxcName := c.LxcName() lxcName := c.LxcName()
// Read raw bytes // Cache init PID once per scan so getContainerNetworkBytes / getContainerDiskIOBytes
memUsage := readIntCommand(fmt.Sprintf( // don't each fork lxc-info separately.
"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+ initPID := m.getContainerInitPID(lxcName)
"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
"cat /sys/fs/cgroup/memory/lxc/%[1]s/memory.usage_in_bytes 2>/dev/null || echo 0", shellQuote(lxcName)))
cpuUsec := uint64(readIntCommand(fmt.Sprintf( // Read memory from cgroup directly (no shell fork)
"(cat /sys/fs/cgroup/lxc/%[1]s/cpu.stat 2>/dev/null || "+ memUsage := readCgroupFile(lxcName,
"cat /sys/fs/cgroup/lxc.payload.%[1]s/cpu.stat 2>/dev/null) | "+ "/sys/fs/cgroup/lxc/%s/memory.current",
"awk '/usage_usec/ {print $2; found=1} END {if (!found) print 0}'", shellQuote(lxcName)))) "/sys/fs/cgroup/lxc.payload.%s/memory.current",
"/sys/fs/cgroup/memory/lxc/%s/memory.usage_in_bytes")
rxBytes, txBytes := m.getContainerNetworkBytes(lxcName) // Read cpu usage from cgroup directly (no shell | awk fork)
readBytes, writeBytes := m.getContainerDiskIOBytes(lxcName) cpuUsec := readCgroupCPUUsec(lxcName,
"/sys/fs/cgroup/lxc/%s/cpu.stat",
"/sys/fs/cgroup/lxc.payload.%s/cpu.stat")
rxBytes, txBytes := getNetworkBytesForPID(initPID)
readBytes, writeBytes := getDiskIOBytesForPID(initPID)
now := time.Now() now := time.Now()
sample := containerUsageSample{ sample := containerUsageSample{
@@ -2245,10 +2250,38 @@ func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) {
if pid == "" { if pid == "" {
return 0, 0 return 0, 0
} }
dir := fmt.Sprintf("/proc/%s/net", pid) return readProcNetDev(fmt.Sprintf("/proc/%s/net/dev", pid))
rx := readIntCommand(fmt.Sprintf("cat %s/dev 2>/dev/null | awk '{rx+=$2; tx+=$10} END {print rx}' || echo 0", shellQuote(dir))) }
tx := readIntCommand(fmt.Sprintf("cat %s/dev 2>/dev/null | awk '{rx+=$2; tx+=$10} END {print tx}' || echo 0", shellQuote(dir)))
return uint64(rx), uint64(tx) // readProcNetDev parses /proc/PID/net/dev directly (no shell/awk fork).
func readProcNetDev(path string) (uint64, uint64) {
data, err := os.ReadFile(path)
if err != nil {
return 0, 0
}
var rx, tx uint64
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
// Skip header lines
if strings.Contains(line, "|") || strings.Contains(line, "face") || strings.TrimSpace(line) == "" {
continue
}
fields := strings.Fields(line)
// Fields: face | rx_bytes rx_packets rx_errs rx_drop rx_fifo rx_frame rx_compressed rx_multicast | tx_bytes tx_packets tx_errs tx_drop tx_fifo tx_colls tx_carrier tx_compressed
// Skip loopback (face starts with "lo")
if len(fields) < 10 {
continue
}
if strings.HasPrefix(fields[0], "lo") {
continue
}
r, _ := strconv.ParseUint(fields[1], 10, 64)
t, _ := strconv.ParseUint(fields[9], 10, 64)
rx += r
tx += t
}
return rx, tx
} }
func (m *Manager) getContainerDiskIOBytes(lxcName string) (uint64, uint64) { func (m *Manager) getContainerDiskIOBytes(lxcName string) (uint64, uint64) {
@@ -2256,10 +2289,22 @@ func (m *Manager) getContainerDiskIOBytes(lxcName string) (uint64, uint64) {
if pid == "" { if pid == "" {
return 0, 0 return 0, 0
} }
// /proc/PID/io format: "field_name: value" per line data, err := os.ReadFile(fmt.Sprintf("/proc/%s/io", pid))
// Fields: rchar, wchar, syscr, syscw, read_bytes, write_bytes, cancelled_write_bytes if err != nil {
readBytes := uint64(readIntCommand(fmt.Sprintf("awk '/^read_bytes:/ {print $2}' /proc/%s/io 2>/dev/null || echo 0", pid))) return 0, 0
writeBytes := uint64(readIntCommand(fmt.Sprintf("awk '/^write_bytes:/ {print $2}' /proc/%s/io 2>/dev/null || echo 0", pid))) }
var readBytes, writeBytes uint64
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "read_bytes:") {
val := strings.TrimSpace(strings.TrimPrefix(line, "read_bytes:"))
readBytes, _ = strconv.ParseUint(val, 10, 64)
} else if strings.HasPrefix(line, "write_bytes:") {
val := strings.TrimSpace(strings.TrimPrefix(line, "write_bytes:"))
writeBytes, _ = strconv.ParseUint(val, 10, 64)
}
}
return readBytes, writeBytes return readBytes, writeBytes
} }
@@ -2272,6 +2317,73 @@ func (m *Manager) getContainerInitPID(lxcName string) string {
return strings.TrimSpace(string(out)) return strings.TrimSpace(string(out))
} }
// readCgroupFile tries each path template in order, reads the file directly (no shell),
// and returns the first valid int64 value.
func readCgroupFile(name string, paths ...string) int64 {
for _, tmpl := range paths {
data, err := os.ReadFile(fmt.Sprintf(tmpl, name))
if err != nil {
continue
}
val, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
if err == nil && val > 0 {
return val
}
}
return 0
}
// readCgroupCPUUsec tries each path template, reads cpu.stat, and extracts usage_usec.
func readCgroupCPUUsec(name string, paths ...string) uint64 {
for _, tmpl := range paths {
data, err := os.ReadFile(fmt.Sprintf(tmpl, name))
if err != nil {
continue
}
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "usage_usec ") {
val, err := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "usage_usec")), 10, 64)
if err == nil {
return val
}
}
}
}
return 0
}
// getNetworkBytesForPID reads /proc/PID/net/dev for a given PID (no lxc-info needed).
func getNetworkBytesForPID(pid string) (uint64, uint64) {
if pid == "" {
return 0, 0
}
return readProcNetDev(fmt.Sprintf("/proc/%s/net/dev", pid))
}
// getDiskIOBytesForPID reads /proc/PID/io for a given PID (no lxc-info needed).
func getDiskIOBytesForPID(pid string) (uint64, uint64) {
if pid == "" {
return 0, 0
}
data, err := os.ReadFile(fmt.Sprintf("/proc/%s/io", pid))
if err != nil {
return 0, 0
}
var readBytes, writeBytes uint64
scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "read_bytes:") {
readBytes, _ = strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "read_bytes:")), 10, 64)
} else if strings.HasPrefix(line, "write_bytes:") {
writeBytes, _ = strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "write_bytes:")), 10, 64)
}
}
return readBytes, writeBytes
}
// getContainerUptimeSeconds returns how long the container has been running (in seconds). // getContainerUptimeSeconds returns how long the container has been running (in seconds).
func (m *Manager) getContainerUptimeSeconds(lxcName string) float64 { func (m *Manager) getContainerUptimeSeconds(lxcName string) float64 {
pid := m.getContainerInitPID(lxcName) pid := m.getContainerInitPID(lxcName)
@@ -2399,6 +2511,7 @@ func (m *Manager) AccumulateTraffic() {
lastTrafficSnapshotMu.Lock() lastTrafficSnapshotMu.Lock()
defer lastTrafficSnapshotMu.Unlock() defer lastTrafficSnapshotMu.Unlock()
changed := false
for i := range config.AppConfig.Containers { for i := range config.AppConfig.Containers {
c := &config.AppConfig.Containers[i] c := &config.AppConfig.Containers[i]
if c.Status != "running" { if c.Status != "running" {
@@ -2412,17 +2525,25 @@ func (m *Manager) AccumulateTraffic() {
c.TrafficUsedTX = 0 c.TrafficUsedTX = 0
c.TrafficResetDate = currentMonth c.TrafficResetDate = currentMonth
delete(lastTrafficSnapshot, c.LxcName()) delete(lastTrafficSnapshot, c.LxcName())
changed = true
} }
rx, tx := m.getContainerNetworkBytes(c.LxcName()) rx, tx := m.getContainerNetworkBytes(c.LxcName())
prev, exists := lastTrafficSnapshot[c.LxcName()] prev, exists := lastTrafficSnapshot[c.LxcName()]
// Only add the DELTA (increment since last snapshot) // Only add the DELTA (increment since last snapshot)
if exists && rx >= prev.RXBytes && tx >= prev.TXBytes { if exists && rx >= prev.RXBytes && tx >= prev.TXBytes {
c.TrafficUsedRX += int64(rx - prev.RXBytes) deltaRX := int64(rx - prev.RXBytes)
c.TrafficUsedTX += int64(tx - prev.TXBytes) deltaTX := int64(tx - prev.TXBytes)
if deltaRX > 0 || deltaTX > 0 {
c.TrafficUsedRX += deltaRX
c.TrafficUsedTX += deltaTX
changed = true
}
} }
lastTrafficSnapshot[c.LxcName()] = trafficSample{RXBytes: rx, TXBytes: tx} lastTrafficSnapshot[c.LxcName()] = trafficSample{RXBytes: rx, TXBytes: tx}
} }
config.SaveConfig() if changed {
config.SaveConfig()
}
} }
// GetTrafficInfo returns traffic usage info for a container // GetTrafficInfo returns traffic usage info for a container
-15
View File
@@ -7,11 +7,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"clicd/internal/api" "clicd/internal/api"
"clicd/internal/config" "clicd/internal/config"
"clicd/internal/lxc"
) )
// webFS holds embedded frontend files // webFS holds embedded frontend files
@@ -149,7 +147,6 @@ func setupRoutes(mux *http.ServeMux) {
func Run() error { func Run() error {
// Use embedded frontend files // Use embedded frontend files
webFS = GetEmbeddedFS() webFS = GetEmbeddedFS()
startExpiryMonitor()
mux := http.NewServeMux() mux := http.NewServeMux()
setupRoutes(mux) setupRoutes(mux)
@@ -166,15 +163,3 @@ func Run() error {
return server.ListenAndServe() return server.ListenAndServe()
} }
func startExpiryMonitor() {
manager := lxc.NewManager()
go func() {
manager.StopExpiredContainers(time.Now())
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for now := range ticker.C {
manager.StopExpiredContainers(now)
}
}()
}
+2 -1
View File
@@ -1,7 +1,7 @@
package version package version
var ( var (
Version = "1.0.7" Version = "1.0.8"
Repo = "MengMengCode/CLICD" Repo = "MengMengCode/CLICD"
) )
@@ -13,3 +13,4 @@ func Current() string {
} }