mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
fix: correct process start time in LXC (#13473)
* fix: correct process start time in LXC * fix: reject unresolved process start times
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
agentPsutil "github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
@@ -128,7 +129,7 @@ func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessDa
|
||||
}
|
||||
}
|
||||
|
||||
if createTime, err := p.CreateTime(); err == nil {
|
||||
if createTime, err := agentPsutil.NewProcessCreateTimeResolver().CreateTime(p); err == nil {
|
||||
data.StartTime = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ require (
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.75
|
||||
github.com/tklauser/go-sysconf v0.3.16
|
||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0
|
||||
@@ -193,7 +194,6 @@ require (
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
|
||||
122
agent/utils/psutil/process.go
Normal file
122
agent/utils/psutil/process.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package psutil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"github.com/tklauser/go-sysconf"
|
||||
)
|
||||
|
||||
const defaultClockTicks = 100
|
||||
const maxProcessCreateTimeSkew = time.Minute
|
||||
|
||||
type ProcessCreateTimeResolver struct {
|
||||
procRoot string
|
||||
bootTime int64
|
||||
clockTicks uint64
|
||||
}
|
||||
|
||||
func NewProcessCreateTimeResolver() *ProcessCreateTimeResolver {
|
||||
procRoot := os.Getenv("HOST_PROC")
|
||||
if procRoot == "" {
|
||||
procRoot = "/proc"
|
||||
}
|
||||
|
||||
resolver := &ProcessCreateTimeResolver{
|
||||
procRoot: procRoot,
|
||||
clockTicks: defaultClockTicks,
|
||||
}
|
||||
if clockTicks, err := sysconf.Sysconf(sysconf.SC_CLK_TCK); err == nil && clockTicks > 0 {
|
||||
resolver.clockTicks = uint64(clockTicks)
|
||||
}
|
||||
if bootTime, err := readBootTime(filepath.Join(procRoot, "stat")); err == nil {
|
||||
resolver.bootTime = bootTime
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
|
||||
// CreateTime returns the process start time in milliseconds since Unix epoch.
|
||||
//
|
||||
// On some LXC systems, gopsutil combines a container-relative /proc/uptime with
|
||||
// the host-relative starttime from /proc/<pid>/stat, which can place the start
|
||||
// time in the future. In that case, reading btime and starttime from /proc keeps
|
||||
// both values on the same clock base, matching the calculation used by ps.
|
||||
func (r *ProcessCreateTimeResolver) CreateTime(proc *process.Process) (int64, error) {
|
||||
now := time.Now()
|
||||
createTime, createTimeErr := proc.CreateTime()
|
||||
if createTimeErr == nil && isValidProcessCreateTime(createTime, now) {
|
||||
return createTime, nil
|
||||
}
|
||||
|
||||
if r.bootTime > 0 && r.clockTicks > 0 {
|
||||
statPath := filepath.Join(r.procRoot, strconv.Itoa(int(proc.Pid)), "stat")
|
||||
if content, err := os.ReadFile(statPath); err == nil {
|
||||
if startTicks, err := parseProcessStartTicks(string(content)); err == nil {
|
||||
startMillis := startTicks * 1000 / r.clockTicks
|
||||
fallbackTime := r.bootTime*1000 + int64(startMillis)
|
||||
if isValidProcessCreateTime(fallbackTime, now) {
|
||||
return fallbackTime, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if createTimeErr != nil {
|
||||
return 0, fmt.Errorf("resolve process create time: %w", createTimeErr)
|
||||
}
|
||||
return 0, fmt.Errorf("invalid process create time: %d", createTime)
|
||||
}
|
||||
|
||||
func isValidProcessCreateTime(createTime int64, now time.Time) bool {
|
||||
return createTime > 0 && createTime <= now.Add(maxProcessCreateTimeSkew).UnixMilli()
|
||||
}
|
||||
|
||||
func readBootTime(path string) (int64, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) != 2 || fields[0] != "btime" {
|
||||
continue
|
||||
}
|
||||
bootTime, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse btime: %w", err)
|
||||
}
|
||||
if bootTime <= 0 {
|
||||
return 0, errors.New("invalid btime")
|
||||
}
|
||||
return bootTime, nil
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, errors.New("btime not found")
|
||||
}
|
||||
|
||||
func parseProcessStartTicks(stat string) (uint64, error) {
|
||||
// The second field (comm) is wrapped in parentheses and may contain spaces
|
||||
// or parentheses, so split only after its closing parenthesis. The remaining
|
||||
// fields begin at field 3 (state), making starttime (field 22) index 19.
|
||||
commEnd := strings.LastIndex(stat, ")")
|
||||
if commEnd == -1 || commEnd+1 >= len(stat) {
|
||||
return 0, errors.New("invalid process stat")
|
||||
}
|
||||
fields := strings.Fields(stat[commEnd+1:])
|
||||
const startTimeIndex = 19
|
||||
if len(fields) <= startTimeIndex {
|
||||
return 0, errors.New("process stat has too few fields")
|
||||
}
|
||||
return strconv.ParseUint(fields[startTimeIndex], 10, 64)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
agentPsutil "github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
@@ -159,7 +160,7 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func handleProcessData(proc *process.Process, processConfig *PsProcessConfig, pidConnections map[int32][]net.ConnectionStat) *PsProcessData {
|
||||
func handleProcessData(proc *process.Process, processConfig *PsProcessConfig, pidConnections map[int32][]net.ConnectionStat, createTimeResolver *agentPsutil.ProcessCreateTimeResolver) *PsProcessData {
|
||||
if processConfig.Pid > 0 && processConfig.Pid != proc.Pid {
|
||||
return nil
|
||||
}
|
||||
@@ -185,10 +186,10 @@ func handleProcessData(proc *process.Process, processConfig *PsProcessConfig, pi
|
||||
if len(statusArray) > 0 {
|
||||
procData.Status = strings.Join(statusArray, ",")
|
||||
}
|
||||
createTime, procErr := proc.CreateTime()
|
||||
createTime, procErr := createTimeResolver.CreateTime(proc)
|
||||
if procErr == nil {
|
||||
t := time.Unix(createTime/1000, 0)
|
||||
procData.StartTime = t.Format("2006-1-2 15:04:05")
|
||||
procData.StartTime = t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
procData.NumThreads, _ = proc.NumThreads()
|
||||
procData.CpuValue, _ = proc.CPUPercent()
|
||||
@@ -231,9 +232,10 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
}
|
||||
|
||||
result := make([]PsProcessData, 0, len(processes))
|
||||
createTimeResolver := agentPsutil.NewProcessCreateTimeResolver()
|
||||
|
||||
for _, proc := range processes {
|
||||
procData := handleProcessData(proc, &processConfig, pidConnections)
|
||||
procData := handleProcessData(proc, &processConfig, pidConnections, createTimeResolver)
|
||||
if procData != nil {
|
||||
result = append(result, *procData)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user