2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00

feat: 进程管理

This commit is contained in:
耗子
2024-12-03 21:14:27 +08:00
parent 8c7a65a8b6
commit a3bbbfa87c
8 changed files with 336 additions and 10 deletions

View File

@@ -0,0 +1,5 @@
package request
type ProcessKill struct {
PID int32 `json:"pid" validate:"required"`
}

View File

@@ -160,6 +160,12 @@ func Http(r chi.Router) {
r.Post("/{id}/status", cron.Status)
})
r.Route("/process", func(r chi.Router) {
process := service.NewProcessService()
r.Get("/", process.List)
r.Post("/kill", process.Kill)
})
r.Route("/safe", func(r chi.Router) {
safe := service.NewSafeService()
r.Get("/ssh", safe.GetSSH)

110
internal/service/process.go Normal file
View File

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

37
pkg/types/process.go Normal file
View File

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

View File

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

View File

@@ -330,11 +330,7 @@ const handleUpdate = () => {
}
const toSponsor = () => {
if (locale.value === 'en') {
window.open('https://opencollective.com/tnb')
} else {
window.open('https://afdian.com/a/TheTNB')
}
window.open('https://afdian.com/a/TheTNB')
}
const handleManageApp = (slug: string) => {
@@ -443,11 +439,7 @@ if (import.meta.hot) {
<p>负载状态</p>
<n-progress
type="dashboard"
:percentage="
Math.round(formatPercent((realtime.load.load1 / cores) * 100)) > 100
? 100
: Math.round(formatPercent((realtime.load.load1 / cores) * 100))
"
:percentage="Math.round(formatPercent((realtime.load.load1 / cores) * 100))"
:color="statusColor((realtime.load.load1 / cores) * 100)"
>
</n-progress>

View File

@@ -8,6 +8,7 @@ defineOptions({
import TheIcon from '@/components/custom/TheIcon.vue'
import CreateModal from '@/views/task/CreateModal.vue'
import CronView from '@/views/task/CronView.vue'
import SystemView from '@/views/task/SystemView.vue'
import TaskView from '@/views/task/TaskView.vue'
const current = ref('cron')
@@ -27,6 +28,9 @@ const create = ref(false)
<n-tab-pane name="cron" tab="计划任务">
<cron-view />
</n-tab-pane>
<n-tab-pane name="system" tab="系统进程">
<system-view />
</n-tab-pane>
<n-tab-pane name="task" tab="面板任务">
<task-view />
</n-tab-pane>

View File

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