Files
1Panel/agent/app/service/process.go
T
卢亦之 074c0abb3b fix: correct listening status display on the firewall page (#12516)
The bug only appeared when the same process had both TCP and UDP sockets:
every listening port on that process was attributed to whichever protocol
was encountered first. For example, if a process listened on TCP 8000 and
UDP 8001, the UI could wrongly show TCP 8001 as in use while UDP 8001
looked unused.

Use (PID, conn.Type) as the cache key so each protocol has its own
ListeningProcess entry.
2026-04-20 11:27:18 +08:00

345 lines
7.4 KiB
Go

package service
import (
"bufio"
"context"
"fmt"
"os"
"strconv"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/process"
)
type ProcessService struct{}
type IProcessService interface {
StopProcess(req request.ProcessReq) error
GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error)
GetListeningProcess(c context.Context) ([]ListeningProcess, error)
}
func NewIProcessService() IProcessService {
return &ProcessService{}
}
func (ps *ProcessService) StopProcess(req request.ProcessReq) error {
proc, err := process.NewProcess(req.PID)
if err != nil {
return err
}
if err := proc.Kill(); err != nil {
return err
}
return nil
}
type ListeningProcess struct {
PID int32
Port map[uint32]struct{}
Protocol uint32
Name string
}
func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningProcess, error) {
conn, err := net.ConnectionsMaxWithContext(c, "inet", 32768)
if err != nil {
return nil, err
}
// One cache entry per (PID, socket type) so TCP and UDP sockets are not merged under one Protocol.
type procKey struct {
pid int32
protocol uint32
}
procCache := make(map[procKey]ListeningProcess, 64)
for _, conn := range conn {
if conn.Pid == 0 {
continue
}
if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
key := procKey{pid: conn.Pid, protocol: conn.Type}
if _, exists := procCache[key]; !exists {
proc, err := process.NewProcess(conn.Pid)
if err != nil {
continue
}
procData := ListeningProcess{
PID: conn.Pid,
}
procData.Name, _ = proc.Name()
procData.Port = make(map[uint32]struct{})
procData.Port[conn.Laddr.Port] = struct{}{}
procData.Protocol = conn.Type
procCache[key] = procData
} else {
p := procCache[key]
p.Port[conn.Laddr.Port] = struct{}{}
procCache[key] = p
}
}
}
procs := make([]ListeningProcess, 0, len(procCache))
for _, proc := range procCache {
procs = append(procs, proc)
}
return procs, nil
}
func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error) {
p, err := process.NewProcess(pid)
if err != nil {
return nil, fmt.Errorf("get process info by pid %v: %v", pid, err)
}
exists, err := p.IsRunning()
if err != nil || !exists {
return nil, fmt.Errorf("process %v is not running", pid)
}
data := &websocket.PsProcessData{
PID: pid,
}
if name, err := p.Name(); err == nil {
data.Name = name
}
if ppid, err := p.Ppid(); err == nil {
data.PPID = ppid
}
if username, err := p.Username(); err == nil {
data.Username = username
}
if status, err := p.Status(); err == nil {
if len(status) > 0 {
data.Status = status[0]
}
}
if createTime, err := p.CreateTime(); err == nil {
data.StartTime = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05")
}
if numThreads, err := p.NumThreads(); err == nil {
data.NumThreads = numThreads
}
if connections, err := p.Connections(); err == nil {
data.NumConnections = len(connections)
var connects []websocket.ProcessConnect
for _, conn := range connections {
pc := websocket.ProcessConnect{
Status: conn.Status,
Laddr: conn.Laddr,
Raddr: conn.Raddr,
PID: pid,
Name: data.Name,
}
connects = append(connects, pc)
}
data.Connects = connects
}
if cpuPercent, err := p.CPUPercent(); err == nil {
data.CpuValue = cpuPercent
data.CpuPercent = fmt.Sprintf("%.2f%%", cpuPercent)
}
if ioCounters, err := p.IOCounters(); err == nil {
data.DiskRead = common.FormatBytes(ioCounters.ReadBytes)
data.DiskWrite = common.FormatBytes(ioCounters.WriteBytes)
}
if cmdline, err := p.Cmdline(); err == nil {
data.CmdLine = cmdline
}
if memDetail, err := getMemoryDetail(p.Pid); err == nil {
data.Rss = common.FormatBytes(memDetail.RSS)
data.VMS = common.FormatBytes(memDetail.VMS)
data.HWM = common.FormatBytes(memDetail.HWM)
data.Data = common.FormatBytes(memDetail.Data)
data.Stack = common.FormatBytes(memDetail.Stack)
data.Locked = common.FormatBytes(memDetail.Locked)
data.Swap = common.FormatBytes(memDetail.Swap)
data.Dirty = common.FormatBytes(memDetail.Dirty)
data.RssValue = memDetail.RSS
data.PSS = common.FormatBytes(memDetail.PSS)
data.USS = common.FormatBytes(memDetail.USS)
data.Shared = common.FormatBytes(memDetail.Shared)
data.Text = common.FormatBytes(memDetail.Text)
}
if envs, err := p.Environ(); err == nil {
data.Envs = envs
}
if openFiles, err := p.OpenFiles(); err == nil {
data.OpenFiles = openFiles
}
return data, nil
}
type MemoryDetail struct {
RSS uint64
VMS uint64
HWM uint64
Data uint64
Stack uint64
Locked uint64
Swap uint64
PSS uint64
USS uint64
Shared uint64
Text uint64
Dirty uint64
}
func getMemoryDetail(pid int32) (*MemoryDetail, error) {
mem := &MemoryDetail{}
if err := readStatus(pid, mem); err != nil {
return nil, err
}
if err := readSmapsRollup(pid, mem); err != nil {
if err := readSmaps(pid, mem); err != nil {
return nil, err
}
}
return mem, nil
}
func readStatus(pid int32, mem *MemoryDetail) error {
filePath := fmt.Sprintf("/proc/%d/status", pid)
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.TrimSuffix(fields[0], ":")
value, _ := strconv.ParseUint(fields[1], 10, 64)
value *= 1024
switch key {
case "VmRSS":
mem.RSS = value
case "VmSize":
mem.VMS = value
case "VmData":
mem.Data = value
case "VmSwap":
mem.Swap = value
case "VmExe":
mem.Text = value
case "RssShmem":
mem.Shared = value
case "VmHWM":
mem.HWM = value
case "VmStk":
mem.Stack = value
case "VmLck":
mem.Locked = value
}
}
return scanner.Err()
}
func readSmapsRollup(pid int32, mem *MemoryDetail) error {
filePath := fmt.Sprintf("/proc/%d/smaps_rollup", pid)
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.TrimSuffix(fields[0], ":")
value, _ := strconv.ParseUint(fields[1], 10, 64)
value *= 1024
switch key {
case "Pss":
mem.PSS = value
case "Private_Clean", "Private_Dirty":
mem.USS += value
case "Shared_Clean", "Shared_Dirty":
if mem.Shared == 0 {
mem.Shared = value
}
}
}
return scanner.Err()
}
func readSmaps(pid int32, mem *MemoryDetail) error {
filePath := fmt.Sprintf("/proc/%d/smaps", pid)
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.TrimSuffix(fields[0], ":")
value, _ := strconv.ParseUint(fields[1], 10, 64)
value *= 1024
switch key {
case "Pss":
mem.PSS += value
case "Private_Clean", "Private_Dirty":
mem.USS += value
case "Shared_Clean", "Shared_Dirty":
if mem.Shared == 0 {
mem.Shared += value
}
}
}
return scanner.Err()
}