diff --git a/internal/http/request/process.go b/internal/http/request/process.go new file mode 100644 index 0000000000..fdffcf6da7 --- /dev/null +++ b/internal/http/request/process.go @@ -0,0 +1,5 @@ +package request + +type ProcessKill struct { + PID int32 `json:"pid" validate:"required"` +} diff --git a/internal/route/http.go b/internal/route/http.go index 8a7194b50a..e1baa884cc 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -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) diff --git a/internal/service/process.go b/internal/service/process.go new file mode 100644 index 0000000000..360b58dd7e --- /dev/null +++ b/internal/service/process.go @@ -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 = "" + } + + 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 +} diff --git a/pkg/types/process.go b/pkg/types/process.go new file mode 100644 index 0000000000..21dae9126d --- /dev/null +++ b/pkg/types/process.go @@ -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"` +} diff --git a/web/src/api/panel/process/index.ts b/web/src/api/panel/process/index.ts new file mode 100644 index 0000000000..9bb2fd21c6 --- /dev/null +++ b/web/src/api/panel/process/index.ts @@ -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 }) +} diff --git a/web/src/views/dashboard/IndexView.vue b/web/src/views/dashboard/IndexView.vue index 82db5bfe45..f3d54e6938 100644 --- a/web/src/views/dashboard/IndexView.vue +++ b/web/src/views/dashboard/IndexView.vue @@ -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) {

负载状态

diff --git a/web/src/views/task/IndexView.vue b/web/src/views/task/IndexView.vue index 12b50f0665..5bd902fff4 100644 --- a/web/src/views/task/IndexView.vue +++ b/web/src/views/task/IndexView.vue @@ -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) + + + diff --git a/web/src/views/task/SystemView.vue b/web/src/views/task/SystemView.vue new file mode 100644 index 0000000000..a568261543 --- /dev/null +++ b/web/src/views/task/SystemView.vue @@ -0,0 +1,164 @@ + + +