Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent/app/service/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}

Expand Down
2 changes: 1 addition & 1 deletion agent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions agent/utils/psutil/process.go
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 6 additions & 4 deletions agent/utils/websocket/process_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down
Loading