mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
feat: 进程管理
This commit is contained in:
5
internal/http/request/process.go
Normal file
5
internal/http/request/process.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package request
|
||||
|
||||
type ProcessKill struct {
|
||||
PID int32 `json:"pid" validate:"required"`
|
||||
}
|
||||
@@ -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
110
internal/service/process.go
Normal 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
37
pkg/types/process.go
Normal 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"`
|
||||
}
|
||||
8
web/src/api/panel/process/index.ts
Normal file
8
web/src/api/panel/process/index.ts
Normal 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 })
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
164
web/src/views/task/SystemView.vue
Normal file
164
web/src/views/task/SystemView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user