mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49d5a65357 | |||
| 8f32765ffe |
+143
-22
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user