-
-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
336 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package request | ||
|
||
type ProcessKill struct { | ||
PID int32 `json:"pid" validate:"required"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package service | ||
|
||
import ( | ||
"net/http" | ||
"slices" | ||
"time" | ||
|
||
"github.com/go-rat/chix" | ||
"github.com/shirou/gopsutil/process" | ||
|
||
"github.com/TheTNB/panel/internal/http/request" | ||
"github.com/TheTNB/panel/pkg/types" | ||
) | ||
|
||
type ProcessService struct { | ||
} | ||
|
||
func NewProcessService() *ProcessService { | ||
return &ProcessService{} | ||
} | ||
|
||
func (s *ProcessService) List(w http.ResponseWriter, r *http.Request) { | ||
processes, err := process.Processes() | ||
if err != nil { | ||
Error(w, http.StatusInternalServerError, "%v", err) | ||
return | ||
} | ||
|
||
data := make([]types.ProcessData, 0) | ||
for proc := range slices.Values(processes) { | ||
data = append(data, s.processProcess(proc)) | ||
} | ||
|
||
paged, total := Paginate(r, data) | ||
|
||
Success(w, chix.M{ | ||
"total": total, | ||
"items": paged, | ||
}) | ||
} | ||
|
||
func (s *ProcessService) Kill(w http.ResponseWriter, r *http.Request) { | ||
req, err := Bind[request.ProcessKill](r) | ||
if err != nil { | ||
Error(w, http.StatusUnprocessableEntity, "%v", err) | ||
return | ||
} | ||
|
||
proc, err := process.NewProcess(req.PID) | ||
if err != nil { | ||
Error(w, http.StatusInternalServerError, "%v", err) | ||
return | ||
} | ||
|
||
if err = proc.Kill(); err != nil { | ||
Error(w, http.StatusInternalServerError, "%v", err) | ||
return | ||
} | ||
|
||
Success(w, nil) | ||
} | ||
|
||
// processProcess 处理进程数据 | ||
func (s *ProcessService) processProcess(proc *process.Process) types.ProcessData { | ||
data := types.ProcessData{ | ||
PID: proc.Pid, | ||
} | ||
|
||
if name, err := proc.Name(); err == nil { | ||
data.Name = name | ||
} else { | ||
data.Name = "<UNKNOWN>" | ||
} | ||
|
||
if username, err := proc.Username(); err == nil { | ||
data.Username = username | ||
} | ||
data.PPID, _ = proc.Ppid() | ||
data.Status, _ = proc.Status() | ||
data.Background, _ = proc.Background() | ||
if ct, err := proc.CreateTime(); err == nil { | ||
data.StartTime = time.Unix(ct/1000, 0).Format(time.DateTime) | ||
} | ||
data.NumThreads, _ = proc.NumThreads() | ||
data.CPU, _ = proc.CPUPercent() | ||
if mem, err := proc.MemoryInfo(); err == nil { | ||
data.RSS = mem.RSS | ||
data.Data = mem.Data | ||
data.VMS = mem.VMS | ||
data.HWM = mem.HWM | ||
data.Stack = mem.Stack | ||
data.Locked = mem.Locked | ||
data.Swap = mem.Swap | ||
} | ||
|
||
if ioStat, err := proc.IOCounters(); err == nil { | ||
data.DiskWrite = ioStat.WriteBytes | ||
data.DiskRead = ioStat.ReadBytes | ||
} | ||
|
||
data.Nets, _ = proc.NetIOCounters(false) | ||
data.Connections, _ = proc.Connections() | ||
data.CmdLine, _ = proc.Cmdline() | ||
data.OpenFiles, _ = proc.OpenFiles() | ||
data.Envs, _ = proc.Environ() | ||
data.OpenFiles = slices.Compact(data.OpenFiles) | ||
data.Envs = slices.Compact(data.Envs) | ||
|
||
return data | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package types | ||
|
||
import ( | ||
"github.com/shirou/gopsutil/net" | ||
"github.com/shirou/gopsutil/process" | ||
) | ||
|
||
type ProcessData struct { | ||
PID int32 `json:"pid"` | ||
Name string `json:"name"` | ||
PPID int32 `json:"ppid"` | ||
Username string `json:"username"` | ||
Status string `json:"status"` | ||
Background bool `json:"background"` | ||
StartTime string `json:"start_time"` | ||
NumThreads int32 `json:"num_threads"` | ||
CPU float64 `json:"cpu"` | ||
|
||
DiskRead uint64 `json:"disk_read"` | ||
DiskWrite uint64 `json:"disk_write"` | ||
|
||
CmdLine string `json:"cmd_line"` | ||
|
||
RSS uint64 `json:"rss"` | ||
VMS uint64 `json:"vms"` | ||
HWM uint64 `json:"hwm"` | ||
Data uint64 `json:"data"` | ||
Stack uint64 `json:"stack"` | ||
Locked uint64 `json:"locked"` | ||
Swap uint64 `json:"swap"` | ||
|
||
Envs []string `json:"envs"` | ||
|
||
OpenFiles []process.OpenFilesStat `json:"open_files"` | ||
Connections []net.ConnectionStat `json:"connections"` | ||
Nets []net.IOCountersStat `json:"nets"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { http } from '@/utils' | ||
|
||
export default { | ||
// 获取进程列表 | ||
list: (page: number, limit: number) => http.Get(`/process`, { params: { page, limit } }), | ||
// 杀死进程 | ||
kill: (pid: number) => http.Post(`/process/kill`, { pid }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<script setup lang="ts"> | ||
import { NButton, NDataTable, NPopconfirm, NTag } from 'naive-ui' | ||
import process from '@/api/panel/process' | ||
import { formatBytes, formatDateTime, formatPercent, renderIcon } from '@/utils' | ||
const columns: any = [ | ||
{ | ||
title: 'PID', | ||
key: 'pid', | ||
width: 120, | ||
ellipsis: { tooltip: true } | ||
}, | ||
{ | ||
title: '名称', | ||
key: 'name', | ||
minWidth: 250, | ||
resizable: true, | ||
ellipsis: { tooltip: true } | ||
}, | ||
{ | ||
title: '父进程 ID', | ||
key: 'ppid', | ||
width: 120, | ||
ellipsis: { tooltip: true } | ||
}, | ||
{ | ||
title: '线程数', | ||
key: 'num_threads', | ||
width: 100, | ||
ellipsis: { tooltip: true } | ||
}, | ||
{ | ||
title: '用户', | ||
key: 'username', | ||
minWidth: 100, | ||
ellipsis: { tooltip: true } | ||
}, | ||
{ | ||
title: '状态', | ||
key: 'status', | ||
minWidth: 150, | ||
ellipsis: { tooltip: true }, | ||
render(row: any) { | ||
switch (row.status) { | ||
case 'R': | ||
return h(NTag, { type: 'success' }, { default: () => '运行' }) | ||
case 'S': | ||
return h(NTag, { type: 'warning' }, { default: () => '睡眠' }) | ||
case 'T': | ||
return h(NTag, { type: 'error' }, { default: () => '停止' }) | ||
case 'I': | ||
return h(NTag, { type: 'primary' }, { default: () => '空闲' }) | ||
case 'Z': | ||
return h(NTag, { type: 'error' }, { default: () => '僵尸' }) | ||
case 'W': | ||
return h(NTag, { type: 'warning' }, { default: () => '等待' }) | ||
case 'L': | ||
return h(NTag, { type: 'info' }, { default: () => '锁定' }) | ||
default: | ||
return h(NTag, { type: 'default' }, { default: () => row.status }) | ||
} | ||
} | ||
}, | ||
{ | ||
title: 'CPU', | ||
key: 'cpu', | ||
minWidth: 100, | ||
ellipsis: { tooltip: true }, | ||
render(row: any): string { | ||
return formatPercent(row.cpu) + '%' | ||
} | ||
}, | ||
{ | ||
title: '内存', | ||
key: 'rss', | ||
minWidth: 100, | ||
ellipsis: { tooltip: true }, | ||
render(row: any): string { | ||
return formatBytes(row.rss) | ||
} | ||
}, | ||
{ | ||
title: '启动时间', | ||
key: 'start_time', | ||
width: 160, | ||
ellipsis: { tooltip: true }, | ||
render(row: any): string { | ||
return formatDateTime(row.start_time) | ||
} | ||
}, | ||
{ | ||
title: '操作', | ||
key: 'actions', | ||
width: 150, | ||
align: 'center', | ||
hideInExcel: true, | ||
render(row: any) { | ||
return h( | ||
NPopconfirm, | ||
{ | ||
onPositiveClick: async () => { | ||
await process.kill(row.pid) | ||
await refresh() | ||
window.$message.success(`进程 ${row.pid} 已终止`) | ||
} | ||
}, | ||
{ | ||
default: () => { | ||
return '确定终止进程 ' + row.pid + ' ?' | ||
}, | ||
trigger: () => { | ||
return h( | ||
NButton, | ||
{ | ||
size: 'small', | ||
type: 'error' | ||
}, | ||
{ | ||
default: () => '终止', | ||
icon: renderIcon('material-symbols:stop-circle-outline-rounded', { size: 14 }) | ||
} | ||
) | ||
} | ||
} | ||
) | ||
} | ||
} | ||
] | ||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination( | ||
(page, pageSize) => process.list(page, pageSize), | ||
{ | ||
initialData: { total: 0, list: [] }, | ||
total: (res: any) => res.total, | ||
data: (res: any) => res.items | ||
} | ||
) | ||
</script> | ||
|
||
<template> | ||
<n-flex vertical> | ||
<n-data-table | ||
striped | ||
remote | ||
:scroll-x="1400" | ||
:loading="loading" | ||
:columns="columns" | ||
:data="data" | ||
:row-key="(row: any) => row.pid" | ||
v-model:page="page" | ||
v-model:pageSize="pageSize" | ||
:pagination="{ | ||
page: page, | ||
pageCount: pageCount, | ||
pageSize: pageSize, | ||
itemCount: total, | ||
showQuickJumper: true, | ||
showSizePicker: true, | ||
pageSizes: [20, 50, 100, 200] | ||
}" | ||
/> | ||
</n-flex> | ||
</template> |