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:
ssongliu
2026-08-05 11:22:26 +08:00
committed by GitHub
parent 17285d4397
commit 01aee89f3b
4 changed files with 131 additions and 6 deletions

View File

@@ -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")
}

View File

@@ -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

View 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)
}

View File

@@ -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)
}