mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 增加进程管理 (#1476)
This commit is contained in:
parent
c403eb55b1
commit
38c0d290e7
@ -49,4 +49,5 @@ var (
|
||||
upgradeService = service.NewIUpgradeService()
|
||||
|
||||
runtimeService = service.NewRuntimeService()
|
||||
processService = service.NewIProcessService()
|
||||
)
|
||||
|
@ -734,19 +734,12 @@ var wsUpgrade = websocket.Upgrader{
|
||||
},
|
||||
}
|
||||
|
||||
var WsManager = websocket2.Manager{
|
||||
Group: make(map[string]*websocket2.Client),
|
||||
Register: make(chan *websocket2.Client, 128),
|
||||
UnRegister: make(chan *websocket2.Client, 128),
|
||||
ClientCount: 0,
|
||||
}
|
||||
|
||||
func (b *BaseApi) Ws(c *gin.Context) {
|
||||
ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wsClient := websocket2.NewWsClient("wsClient", ws)
|
||||
wsClient := websocket2.NewWsClient("fileClient", ws)
|
||||
go wsClient.Read()
|
||||
go wsClient.Write()
|
||||
}
|
||||
|
40
backend/app/api/v1/process.go
Normal file
40
backend/app/api/v1/process.go
Normal file
@ -0,0 +1,40 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) ProcessWs(c *gin.Context) {
|
||||
ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wsClient := websocket2.NewWsClient("processClient", ws)
|
||||
go wsClient.Read()
|
||||
go wsClient.Write()
|
||||
}
|
||||
|
||||
// @Tags Process
|
||||
// @Summary Stop Process
|
||||
// @Description 停止进程
|
||||
// @Param request body request.ProcessReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /process/stop [post]
|
||||
// @x-panel-log {"bodyKeys":["PID"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"结束进程 [PID]","formatEN":"结束进程 [PID]"}
|
||||
func (b *BaseApi) StopProcess(c *gin.Context) {
|
||||
var req request.ProcessReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
if err := processService.StopProcess(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
5
backend/app/dto/request/process.go
Normal file
5
backend/app/dto/request/process.go
Normal file
@ -0,0 +1,5 @@
|
||||
package request
|
||||
|
||||
type ProcessReq struct {
|
||||
PID int32 `json:"PID" validate:"required"`
|
||||
}
|
27
backend/app/service/process.go
Normal file
27
backend/app/service/process.go
Normal file
@ -0,0 +1,27 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
type ProcessService struct{}
|
||||
|
||||
type IProcessService interface {
|
||||
StopProcess(req request.ProcessReq) error
|
||||
}
|
||||
|
||||
func NewIProcessService() IProcessService {
|
||||
return &ProcessService{}
|
||||
}
|
||||
|
||||
func (p *ProcessService) StopProcess(req request.ProcessReq) error {
|
||||
proc, err := process.NewProcess(req.PID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := proc.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -86,6 +86,7 @@ func Routers() *gin.Engine {
|
||||
systemRouter.InitWebsiteAcmeAccountRouter(PrivateGroup)
|
||||
systemRouter.InitNginxRouter(PrivateGroup)
|
||||
systemRouter.InitRuntimeRouter(PrivateGroup)
|
||||
systemRouter.InitProcessRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
return Router
|
||||
|
@ -20,6 +20,7 @@ type RouterGroup struct {
|
||||
DatabaseRouter
|
||||
NginxRouter
|
||||
RuntimeRouter
|
||||
ProcessRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = new(RouterGroup)
|
||||
|
20
backend/router/ro_process.go
Normal file
20
backend/router/ro_process.go
Normal file
@ -0,0 +1,20 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/backend/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ProcessRouter struct {
|
||||
}
|
||||
|
||||
func (f *ProcessRouter) InitProcessRouter(Router *gin.RouterGroup) {
|
||||
processRouter := Router.Group("process")
|
||||
processRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
processRouter.GET("/ws", baseApi.ProcessWs)
|
||||
processRouter.POST("/stop", baseApi.StopProcess)
|
||||
}
|
||||
}
|
68
backend/utils/ps/ps_test.go
Normal file
68
backend/utils/ps/ps_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPs(t *testing.T) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, pro := range processes {
|
||||
var (
|
||||
name string
|
||||
parentID int32
|
||||
userName string
|
||||
status string
|
||||
startTime string
|
||||
numThreads int32
|
||||
numConnections int
|
||||
cpuPercent float64
|
||||
//mem string
|
||||
rss string
|
||||
ioRead string
|
||||
ioWrite string
|
||||
)
|
||||
name, _ = pro.Name()
|
||||
parentID, _ = pro.Ppid()
|
||||
userName, _ = pro.Username()
|
||||
array, err := pro.Status()
|
||||
if err == nil {
|
||||
status = array[0]
|
||||
}
|
||||
createTime, err := pro.CreateTime()
|
||||
if err == nil {
|
||||
t := time.Unix(createTime/1000, 0)
|
||||
startTime = t.Format("2006-1-2 15:04:05")
|
||||
}
|
||||
numThreads, _ = pro.NumThreads()
|
||||
connections, err := pro.Connections()
|
||||
if err == nil && len(connections) > 0 {
|
||||
numConnections = len(connections)
|
||||
}
|
||||
cpuPercent, _ = pro.CPUPercent()
|
||||
menInfo, err := pro.MemoryInfo()
|
||||
if err == nil {
|
||||
rssF := float64(menInfo.RSS) / 1048576
|
||||
rss = fmt.Sprintf("%.2f", rssF)
|
||||
}
|
||||
ioStat, err := pro.IOCounters()
|
||||
if err == nil {
|
||||
ioWrite = strconv.FormatUint(ioStat.WriteBytes, 10)
|
||||
ioRead = strconv.FormatUint(ioStat.ReadBytes, 10)
|
||||
}
|
||||
|
||||
cmdLine, err := pro.Cmdline()
|
||||
if err == nil {
|
||||
fmt.Println(cmdLine)
|
||||
}
|
||||
|
||||
fmt.Println(fmt.Sprintf("Name: %s PId: %v ParentID: %v Username: %v status:%s startTime: %s numThreads: %v numConnections:%v cpuPercent:%v rss:%s MB IORead: %s IOWrite: %s",
|
||||
name, pro.Pid, parentID, userName, status, startTime, numThreads, numConnections, cpuPercent, rss, ioRead, ioWrite))
|
||||
}
|
||||
}
|
@ -1,17 +1,9 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WsMsg struct {
|
||||
Type string
|
||||
Keys []string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string
|
||||
Socket *websocket.Conn
|
||||
@ -35,9 +27,7 @@ func (c *Client) Read() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg := &WsMsg{}
|
||||
_ = json.Unmarshal(message, msg)
|
||||
ProcessData(c, msg)
|
||||
ProcessData(c, message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,21 +43,3 @@ func (c *Client) Write() {
|
||||
_ = c.Socket.WriteMessage(websocket.TextMessage, message)
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessData(c *Client, msg *WsMsg) {
|
||||
if msg.Type == "wget" {
|
||||
var res []files.Process
|
||||
for _, k := range msg.Keys {
|
||||
value, err := global.CACHE.Get(k)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get cache error,err %s", err.Error())
|
||||
return
|
||||
}
|
||||
process := &files.Process{}
|
||||
_ = json.Unmarshal(value, process)
|
||||
res = append(res, *process)
|
||||
}
|
||||
reByte, _ := json.Marshal(res)
|
||||
c.Msg <- reByte
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
package websocket
|
||||
|
||||
import "sync"
|
||||
|
||||
type Manager struct {
|
||||
Group map[string]*Client
|
||||
Lock sync.Mutex
|
||||
Register, UnRegister chan *Client
|
||||
ClientCount uint
|
||||
}
|
||||
|
||||
func (m *Manager) Start() {
|
||||
for {
|
||||
select {
|
||||
case client := <-m.Register:
|
||||
m.Lock.Lock()
|
||||
m.Group[client.ID] = client
|
||||
m.ClientCount++
|
||||
m.Lock.Unlock()
|
||||
case client := <-m.UnRegister:
|
||||
m.Lock.Lock()
|
||||
if _, ok := m.Group[client.ID]; ok {
|
||||
close(client.Msg)
|
||||
delete(m.Group, client.ID)
|
||||
m.ClientCount--
|
||||
}
|
||||
m.Lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) RegisterClient(client *Client) {
|
||||
m.Register <- client
|
||||
}
|
||||
|
||||
func (m *Manager) UnRegisterClient(client *Client) {
|
||||
m.UnRegister <- client
|
||||
}
|
222
backend/utils/websocket/process_data.go
Normal file
222
backend/utils/websocket/process_data.go
Normal file
@ -0,0 +1,222 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WsInput struct {
|
||||
Type string `json:"type"`
|
||||
DownloadProgress
|
||||
PsProcessConfig
|
||||
}
|
||||
|
||||
type DownloadProgress struct {
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
|
||||
type PsProcessConfig struct {
|
||||
Pid int32 `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type PsProcessData struct {
|
||||
PID int32 `json:"PID"`
|
||||
Name string `json:"name"`
|
||||
PPID int32 `json:"PPID"`
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"`
|
||||
StartTime string `json:"startTime"`
|
||||
NumThreads int32 `json:"numThreads"`
|
||||
NumConnections int `json:"numConnections"`
|
||||
CpuPercent string `json:"cpuPercent"`
|
||||
|
||||
DiskRead string `json:"diskRead"`
|
||||
DiskWrite string `json:"diskWrite"`
|
||||
CmdLine string `json:"cmdLine"`
|
||||
|
||||
Rss string `json:"rss"`
|
||||
VMS string `json:"vms"`
|
||||
HWM string `json:"hwm"`
|
||||
Data string `json:"data"`
|
||||
Stack string `json:"stack"`
|
||||
Locked string `json:"locked"`
|
||||
Swap string `json:"swap"`
|
||||
|
||||
CpuValue float64 `json:"cpuValue"`
|
||||
RssValue uint64 `json:"rssValue"`
|
||||
|
||||
Envs []string `json:"envs"`
|
||||
|
||||
OpenFiles []process.OpenFilesStat `json:"openFiles"`
|
||||
Connects []processConnect `json:"connects"`
|
||||
}
|
||||
|
||||
type processConnect struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Laddr net.Addr `json:"localaddr"`
|
||||
Raddr net.Addr `json:"remoteaddr"`
|
||||
}
|
||||
|
||||
func ProcessData(c *Client, inputMsg []byte) {
|
||||
wsInput := &WsInput{}
|
||||
err := json.Unmarshal(inputMsg, wsInput)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("unmarshal wsInput error,err %s", err.Error())
|
||||
return
|
||||
}
|
||||
switch wsInput.Type {
|
||||
case "wget":
|
||||
res, err := getDownloadProcess(wsInput.DownloadProgress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Msg <- res
|
||||
case "ps":
|
||||
res, err := getProcessData(wsInput.PsProcessConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Msg <- res
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
|
||||
var result []files.Process
|
||||
for _, k := range progress.Keys {
|
||||
value, err := global.CACHE.Get(k)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get cache error,err %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
downloadProcess := &files.Process{}
|
||||
_ = json.Unmarshal(value, downloadProcess)
|
||||
result = append(result, *downloadProcess)
|
||||
}
|
||||
res, err = json.Marshal(result)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
b = uint64(1)
|
||||
kb = 1024 * b
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
|
||||
func formatBytes(bytes uint64) string {
|
||||
switch {
|
||||
case bytes < kb:
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
case bytes < mb:
|
||||
return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kb))
|
||||
case bytes < gb:
|
||||
return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mb))
|
||||
default:
|
||||
return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gb))
|
||||
}
|
||||
}
|
||||
|
||||
func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
var (
|
||||
result []PsProcessData
|
||||
processes []*process.Process
|
||||
)
|
||||
processes, err = process.Processes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, proc := range processes {
|
||||
procData := PsProcessData{
|
||||
PID: proc.Pid,
|
||||
}
|
||||
if processConfig.Pid > 0 && processConfig.Pid != proc.Pid {
|
||||
continue
|
||||
}
|
||||
if procName, err := proc.Name(); err == nil {
|
||||
procData.Name = procName
|
||||
} else {
|
||||
procData.Name = "<UNKNOWN>"
|
||||
}
|
||||
if processConfig.Name != "" && !strings.Contains(procData.Name, processConfig.Name) {
|
||||
continue
|
||||
}
|
||||
if username, err := proc.Username(); err == nil {
|
||||
procData.Username = username
|
||||
}
|
||||
if processConfig.Username != "" && !strings.Contains(procData.Username, processConfig.Username) {
|
||||
continue
|
||||
}
|
||||
procData.PPID, _ = proc.Ppid()
|
||||
statusArray, _ := proc.Status()
|
||||
if len(statusArray) > 0 {
|
||||
procData.Status = strings.Join(statusArray, ",")
|
||||
}
|
||||
createTime, procErr := proc.CreateTime()
|
||||
if procErr == nil {
|
||||
t := time.Unix(createTime/1000, 0)
|
||||
procData.StartTime = t.Format("2006-1-2 15:04:05")
|
||||
}
|
||||
procData.NumThreads, _ = proc.NumThreads()
|
||||
connections, procErr := proc.Connections()
|
||||
if procErr == nil {
|
||||
procData.NumConnections = len(connections)
|
||||
for _, conn := range connections {
|
||||
if conn.Laddr.IP != "" || conn.Raddr.IP != "" {
|
||||
procData.Connects = append(procData.Connects, processConnect{
|
||||
Status: conn.Status,
|
||||
Laddr: conn.Laddr,
|
||||
Raddr: conn.Raddr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
procData.CpuValue, _ = proc.CPUPercent()
|
||||
procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%"
|
||||
menInfo, procErr := proc.MemoryInfo()
|
||||
if procErr == nil {
|
||||
procData.Rss = formatBytes(menInfo.RSS)
|
||||
procData.RssValue = menInfo.RSS
|
||||
procData.Data = formatBytes(menInfo.Data)
|
||||
procData.VMS = formatBytes(menInfo.VMS)
|
||||
procData.HWM = formatBytes(menInfo.HWM)
|
||||
procData.Stack = formatBytes(menInfo.Stack)
|
||||
procData.Locked = formatBytes(menInfo.Locked)
|
||||
procData.Swap = formatBytes(menInfo.Swap)
|
||||
} else {
|
||||
procData.Rss = "--"
|
||||
procData.Data = "--"
|
||||
procData.VMS = "--"
|
||||
procData.HWM = "--"
|
||||
procData.Stack = "--"
|
||||
procData.Locked = "--"
|
||||
procData.Swap = "--"
|
||||
|
||||
procData.RssValue = 0
|
||||
}
|
||||
ioStat, procErr := proc.IOCounters()
|
||||
if procErr == nil {
|
||||
procData.DiskWrite = formatBytes(ioStat.WriteBytes)
|
||||
procData.DiskRead = formatBytes(ioStat.ReadBytes)
|
||||
} else {
|
||||
procData.DiskWrite = "--"
|
||||
procData.DiskRead = "--"
|
||||
}
|
||||
procData.CmdLine, _ = proc.Cmdline()
|
||||
procData.OpenFiles, _ = proc.OpenFiles()
|
||||
procData.Envs, _ = proc.Environ()
|
||||
|
||||
result = append(result, procData)
|
||||
}
|
||||
res, err = json.Marshal(result)
|
||||
return
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
frontend/src/api/interface/process.ts
Normal file
5
frontend/src/api/interface/process.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export namespace Process {
|
||||
export interface StopReq {
|
||||
PID: number;
|
||||
}
|
||||
}
|
6
frontend/src/api/modules/process.ts
Normal file
6
frontend/src/api/modules/process.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import http from '@/api';
|
||||
import { Process } from '../interface/process';
|
||||
|
||||
export const StopProcess = (req: Process.StopReq) => {
|
||||
return http.post<any>(`/process/stop`, req);
|
||||
};
|
@ -75,11 +75,16 @@ function handleSelectionChange(row: any) {
|
||||
emit('update:selects', row);
|
||||
}
|
||||
|
||||
function sort(prop: string, order: string) {
|
||||
tableRef.value.refElTable.sort(prop, order);
|
||||
}
|
||||
|
||||
function clearSelects() {
|
||||
tableRef.value.refElTable.clearSelection();
|
||||
}
|
||||
defineExpose({
|
||||
clearSelects,
|
||||
sort,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -241,6 +241,8 @@ const message = {
|
||||
logs: 'Log',
|
||||
ssl: 'Certificate',
|
||||
runtime: 'Runtime',
|
||||
processManage: 'Process',
|
||||
process: 'Process',
|
||||
},
|
||||
home: {
|
||||
overview: 'Overview',
|
||||
@ -1619,6 +1621,40 @@ const message = {
|
||||
rebuildHelper:
|
||||
'After editing the extension, you need to go to the [App Store-Installed] page to rebuild the PHP application to take effect',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
ppid: 'Parent process ID',
|
||||
numThreads: 'Threads',
|
||||
username: 'User',
|
||||
memory: 'Memory',
|
||||
diskRead: 'Disk read',
|
||||
diskWrite: 'Disk write',
|
||||
netSent: 'uplink',
|
||||
netRecv: 'downstream',
|
||||
numConnections: 'Connections',
|
||||
startTime: 'Start time',
|
||||
status: 'Status',
|
||||
running: 'Running',
|
||||
sleep: 'sleep',
|
||||
stop: 'stop',
|
||||
idle: 'idle',
|
||||
zombie: 'zombie process',
|
||||
wait: 'waiting',
|
||||
lock: 'lock',
|
||||
blocked: 'blocked',
|
||||
cmdLine: 'Start command',
|
||||
basic: 'Basic information',
|
||||
mem: 'Memory information',
|
||||
openFiles: 'File Open',
|
||||
file: 'File',
|
||||
env: 'Environment variable',
|
||||
noenv: 'None',
|
||||
net: 'Network connection',
|
||||
laddr: 'Source address/port',
|
||||
raddr: 'Destination address/port',
|
||||
stopProcess: 'End',
|
||||
stopProcessWarn: 'Are you sure you want to end this process (PID:{0})? This operation cannot be rolled back',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -244,6 +244,8 @@ const message = {
|
||||
toolbox: '工具箱',
|
||||
logs: '日志审计',
|
||||
runtime: '运行环境',
|
||||
processManage: '进程管理',
|
||||
process: '进程',
|
||||
},
|
||||
home: {
|
||||
overview: '概览',
|
||||
@ -1559,6 +1561,40 @@ const message = {
|
||||
extendHelper: '列表中不存在的扩展,可以手动输入之后选择,例:输入 sockets ,然后在下拉列表中选择第一个',
|
||||
rebuildHelper: '编辑扩展后需要去【应用商店-已安装】页面【重建】PHP 应用之后才能生效',
|
||||
},
|
||||
process: {
|
||||
pid: '进程ID',
|
||||
ppid: '父进程ID',
|
||||
numThreads: '线程',
|
||||
username: '用户',
|
||||
memory: '内存',
|
||||
diskRead: '磁盘读',
|
||||
diskWrite: '磁盘写',
|
||||
netSent: '上行',
|
||||
netRecv: '下行',
|
||||
numConnections: '连接',
|
||||
startTime: '启动时间',
|
||||
status: '状态',
|
||||
running: '运行中',
|
||||
sleep: '睡眠',
|
||||
stop: '停止',
|
||||
idle: '空闲',
|
||||
zombie: '僵尸进程',
|
||||
wait: '等待',
|
||||
lock: '锁定',
|
||||
blocked: '阻塞',
|
||||
cmdLine: '启动命令',
|
||||
basic: '基本信息',
|
||||
mem: '内存信息',
|
||||
openFiles: '文件打开',
|
||||
file: '文件',
|
||||
env: '环境变量',
|
||||
noenv: '无',
|
||||
net: '网络连接',
|
||||
laddr: '源地址/端口',
|
||||
raddr: '目标地址/端口',
|
||||
stopProcess: '结束',
|
||||
stopProcessWarn: '是否确定结束此进程 (PID:{0})?此操作不可回滚',
|
||||
},
|
||||
};
|
||||
export default {
|
||||
...fit2cloudZhLocale,
|
||||
|
@ -69,6 +69,17 @@ const hostRouter = {
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosts/process/process',
|
||||
name: 'Process',
|
||||
component: () => import('@/views/host/process/process/index.vue'),
|
||||
meta: {
|
||||
title: 'menu.processManage',
|
||||
activeMenu: '/hosts/process/process',
|
||||
keepAlive: true,
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosts/ssh/ssh',
|
||||
name: 'SSH',
|
||||
|
20
frontend/src/views/host/process/index.vue
Normal file
20
frontend/src/views/host/process/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton :buttons="buttons" />
|
||||
<LayoutContent>
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import i18n from '@/lang';
|
||||
import RouterButton from '@/components/router-button/index.vue';
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('menu.process'),
|
||||
path: '/hosts/process/process',
|
||||
},
|
||||
];
|
||||
</script>
|
129
frontend/src/views/host/process/process/detail/index.vue
Normal file
129
frontend/src/views/host/process/process/detail/index.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<el-drawer v-model="open" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('app.detail')" :back="handleClose" :resource="resourceName" />
|
||||
</template>
|
||||
<el-row>
|
||||
<el-col>
|
||||
<el-tabs v-model="activeName" type="card">
|
||||
<el-tab-pane :label="$t('process.basic')" name="basic">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="$t('commons.table.name')" min-width="100px">
|
||||
{{ data.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.status')">{{ data.status }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.pid')">{{ data.PID }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.ppid')">{{ data.PPID }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.numThreads')">
|
||||
{{ data.numThreads }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.numConnections')">
|
||||
{{ data.numConnections }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.diskRead')">
|
||||
{{ data.diskRead }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.diskWrite')">
|
||||
{{ data.diskWrite }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.username')">
|
||||
{{ data.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.startTime')">
|
||||
{{ data.startTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('process.cmdLine')">
|
||||
{{ data.cmdLine }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('process.mem')" name="mem">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="'rss'">{{ data.rss }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'swap'">{{ data.swap }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'vms'">{{ data.vms }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'hwm'">{{ data.hwm }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'data'">{{ data.data }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'stack'">{{ data.stack }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="'locked'">{{ data.locked }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('process.openFiles')" name="openFiles">
|
||||
<el-table :data="data.openFiles" border style="width: 100%">
|
||||
<el-table-column prop="path" :label="$t('process.file')" />
|
||||
<el-table-column prop="fd" label="fd" width="100px" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('process.env')" name="env">
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="height: calc(100vh - 200px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="envStr"
|
||||
:disabled="true"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('process.net')" name="net">
|
||||
<el-table :data="data.connects" border style="width: 100%">
|
||||
<el-table-column prop="localaddr" :label="$t('process.laddr')">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.localaddr.ip }}</span>
|
||||
<span v-if="row.localaddr.port > 0">:{{ row.localaddr.port }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remoteaddr" :label="$t('process.raddr')">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.remoteaddr.ip }}</span>
|
||||
<span v-if="row.remoteaddr.port > 0">:{{ row.remoteaddr.port }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="$t('app.status')" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
|
||||
interface InfoProps {
|
||||
info: object;
|
||||
}
|
||||
|
||||
let open = ref(false);
|
||||
let data = ref();
|
||||
const resourceName = ref('');
|
||||
const activeName = ref('basic');
|
||||
const envStr = ref('');
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const acceptParams = async (params: InfoProps): Promise<void> => {
|
||||
activeName.value = 'basic';
|
||||
data.value = params.info;
|
||||
resourceName.value = data.value.name;
|
||||
envStr.value = data.value.envs.join('\n');
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
284
frontend/src/views/host/process/process/index.vue
Normal file
284
frontend/src/views/host/process/process/index.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div>
|
||||
<FireRouter />
|
||||
<LayoutContent :title="$t('menu.process')" v-loading="loading">
|
||||
<template #toolbar>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<div style="width: 100%">
|
||||
<el-form-item style="float: right">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="search-button">
|
||||
<el-input
|
||||
typpe="number"
|
||||
v-model.number="processSearch.pid"
|
||||
clearable
|
||||
@clear="search()"
|
||||
suffix-icon="Search"
|
||||
@keyup.enter="search()"
|
||||
@change="search()"
|
||||
:placeholder="$t('process.pid')"
|
||||
></el-input>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="search-button">
|
||||
<el-input
|
||||
v-model.trim="processSearch.name"
|
||||
clearable
|
||||
@clear="search()"
|
||||
suffix-icon="Search"
|
||||
@keyup.enter="search()"
|
||||
@change="search()"
|
||||
:placeholder="$t('commons.table.name')"
|
||||
></el-input>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="search-button">
|
||||
<el-input
|
||||
v-model.trim="processSearch.username"
|
||||
clearable
|
||||
@clear="search()"
|
||||
suffix-icon="Search"
|
||||
@keyup.enter="search()"
|
||||
@change="search()"
|
||||
:placeholder="$t('process.username')"
|
||||
></el-input>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :data="data" @sort-change="changeSort" @filter-change="changeFilter" ref="tableRef">
|
||||
<el-table-column :label="'PID'" fix prop="PID" max-width="60px" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-link @click="openDetail(row)">{{ row.PID }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.name')"
|
||||
fix
|
||||
prop="name"
|
||||
min-width="120px"
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('process.ppid')" fix prop="PPID" sortable></el-table-column>
|
||||
<el-table-column :label="$t('process.numThreads')" fix prop="numThreads"></el-table-column>
|
||||
<el-table-column :label="$t('process.username')" fix prop="username"></el-table-column>
|
||||
<el-table-column
|
||||
:label="'CPU'"
|
||||
fix
|
||||
prop="cpuValue"
|
||||
:formatter="cpuFormatter"
|
||||
sortable
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('process.memory')"
|
||||
fix
|
||||
prop="rssValue"
|
||||
:formatter="memFormatter"
|
||||
sortable
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('process.numConnections')" fix prop="numConnections"></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('process.status')"
|
||||
fix
|
||||
prop="status"
|
||||
column-key="status"
|
||||
:filters="[
|
||||
{ text: $t('process.running'), value: 'running' },
|
||||
{ text: $t('process.sleep'), value: 'sleep' },
|
||||
{ text: $t('process.stop'), value: 'stop' },
|
||||
{ text: $t('process.idle'), value: 'idle' },
|
||||
{ text: $t('process.wait'), value: 'wait' },
|
||||
{ text: $t('process.lock'), value: 'lock' },
|
||||
{ text: $t('process.zombie'), value: 'zombie' },
|
||||
]"
|
||||
:filter-method="filterStatus"
|
||||
:filtered-value="sortConfig.filters"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status">{{ $t('process.' + row.status) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('process.startTime')"
|
||||
fix
|
||||
prop="startTime"
|
||||
min-width="120px"
|
||||
></el-table-column>
|
||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<ProcessDetail ref="detailRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FireRouter from '@/views/host/process/index.vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick, reactive } from 'vue';
|
||||
import ProcessDetail from './detail/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { StopProcess } from '@/api/modules/process';
|
||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||
|
||||
interface SortStatus {
|
||||
prop: '';
|
||||
order: '';
|
||||
filters: [];
|
||||
}
|
||||
const sortConfig: SortStatus = {
|
||||
prop: '',
|
||||
order: '',
|
||||
filters: [],
|
||||
};
|
||||
|
||||
const processSearch = reactive({
|
||||
type: 'ps',
|
||||
pid: undefined,
|
||||
username: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('app.detail'),
|
||||
click: function (row: any) {
|
||||
openDetail(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('process.stopProcess'),
|
||||
click: function (row: any) {
|
||||
stopProcess(row.PID);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let processSocket = ref(null) as unknown as WebSocket;
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const tableRef = ref();
|
||||
const oldData = ref([]);
|
||||
const detailRef = ref();
|
||||
|
||||
const openDetail = (row: any) => {
|
||||
detailRef.value.acceptParams({ info: row });
|
||||
};
|
||||
|
||||
const changeSort = ({ prop, order }) => {
|
||||
sortConfig.prop = prop;
|
||||
sortConfig.order = order;
|
||||
};
|
||||
|
||||
const changeFilter = (filters: any) => {
|
||||
if (filters.status && filters.status.length > 0) {
|
||||
sortConfig.filters = filters.status;
|
||||
data.value = filterByStatus();
|
||||
sortTable();
|
||||
} else {
|
||||
data.value = oldData.value;
|
||||
sortConfig.filters = [];
|
||||
sortTable();
|
||||
}
|
||||
};
|
||||
|
||||
const filterStatus = (value: string, row: any) => {
|
||||
return row.status === value;
|
||||
};
|
||||
|
||||
const cpuFormatter = (row: any) => {
|
||||
return row.cpuPercent;
|
||||
};
|
||||
|
||||
const memFormatter = (row: any) => {
|
||||
return row.rss;
|
||||
};
|
||||
|
||||
const isWsOpen = () => {
|
||||
const readyState = processSocket && processSocket.readyState;
|
||||
return readyState === 1;
|
||||
};
|
||||
const closeSocket = () => {
|
||||
if (isWsOpen()) {
|
||||
processSocket && processSocket.close();
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenProcess = () => {};
|
||||
const onMessage = (message: any) => {
|
||||
let result: any[] = JSON.parse(message.data);
|
||||
oldData.value = result;
|
||||
data.value = filterByStatus();
|
||||
sortTable();
|
||||
};
|
||||
|
||||
const filterByStatus = () => {
|
||||
if (sortConfig.filters.length > 0) {
|
||||
const newData = oldData.value.filter((re: any) => {
|
||||
return (sortConfig.filters as string[]).indexOf(re.status) > -1;
|
||||
});
|
||||
return newData;
|
||||
} else {
|
||||
return oldData.value;
|
||||
}
|
||||
};
|
||||
|
||||
const sortTable = () => {
|
||||
if (sortConfig.prop != '' && sortConfig.order != '') {
|
||||
nextTick(() => {
|
||||
tableRef.value?.sort(sortConfig.prop, sortConfig.order);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onerror = () => {};
|
||||
const onClose = () => {};
|
||||
|
||||
const initProcess = () => {
|
||||
let href = window.location.href;
|
||||
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
||||
let ipLocal = href.split('//')[1].split('/')[0];
|
||||
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v1/process/ws`);
|
||||
processSocket.onopen = onOpenProcess;
|
||||
processSocket.onmessage = onMessage;
|
||||
processSocket.onerror = onerror;
|
||||
processSocket.onclose = onClose;
|
||||
sendMsg();
|
||||
};
|
||||
|
||||
const sendMsg = () => {
|
||||
setInterval(() => {
|
||||
search();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
if (isWsOpen()) {
|
||||
if (typeof processSearch.pid === 'string') {
|
||||
processSearch.pid = undefined;
|
||||
}
|
||||
processSocket.send(JSON.stringify(processSearch));
|
||||
}
|
||||
};
|
||||
|
||||
const stopProcess = async (PID: number) => {
|
||||
try {
|
||||
await useDeleteData(StopProcess, { PID: PID }, i18n.global.t('process.stopProcessWarn', [PID]));
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initProcess();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
closeSocket();
|
||||
});
|
||||
</script>
|
Loading…
x
Reference in New Issue
Block a user