Skip to content

Commit

Permalink
feat: 进程管理
Browse files Browse the repository at this point in the history
  • Loading branch information
devhaozi committed Dec 3, 2024
1 parent 8c7a65a commit a3bbbfa
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 10 deletions.
5 changes: 5 additions & 0 deletions internal/http/request/process.go
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"`
}
6 changes: 6 additions & 0 deletions internal/route/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
110 changes: 110 additions & 0 deletions internal/service/process.go
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
}
37 changes: 37 additions & 0 deletions pkg/types/process.go
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"`
}
8 changes: 8 additions & 0 deletions web/src/api/panel/process/index.ts
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 })
}
12 changes: 2 additions & 10 deletions web/src/views/dashboard/IndexView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions web/src/views/task/IndexView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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>
Expand Down
164 changes: 164 additions & 0 deletions web/src/views/task/SystemView.vue
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>

0 comments on commit a3bbbfa

Please sign in to comment.