Browse Source

feat: 增加进程管理 (#1476)

pull/1478/head
zhengkunwang223 1 year ago committed by GitHub
parent
commit
38c0d290e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/app/api/v1/entry.go
  2. 9
      backend/app/api/v1/file.go
  3. 40
      backend/app/api/v1/process.go
  4. 5
      backend/app/dto/request/process.go
  5. 27
      backend/app/service/process.go
  6. 1
      backend/init/router/router.go
  7. 1
      backend/router/entry.go
  8. 20
      backend/router/ro_process.go
  9. 68
      backend/utils/ps/ps_test.go
  10. 30
      backend/utils/websocket/client.go
  11. 38
      backend/utils/websocket/client_manager.go
  12. 222
      backend/utils/websocket/process_data.go
  13. 482
      cmd/server/docs/docs.go
  14. 411
      cmd/server/docs/swagger.json
  15. 384
      cmd/server/docs/swagger.yaml
  16. 5
      frontend/src/api/interface/process.ts
  17. 6
      frontend/src/api/modules/process.ts
  18. 5
      frontend/src/components/complex-table/index.vue
  19. 36
      frontend/src/lang/modules/en.ts
  20. 36
      frontend/src/lang/modules/zh.ts
  21. 11
      frontend/src/routers/modules/host.ts
  22. 20
      frontend/src/views/host/process/index.vue
  23. 129
      frontend/src/views/host/process/process/detail/index.vue
  24. 284
      frontend/src/views/host/process/process/index.vue

1
backend/app/api/v1/entry.go

@ -49,4 +49,5 @@ var (
upgradeService = service.NewIUpgradeService()
runtimeService = service.NewRuntimeService()
processService = service.NewIProcessService()
)

9
backend/app/api/v1/file.go

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

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

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

27
backend/app/service/process.go

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

1
backend/init/router/router.go

@ -86,6 +86,7 @@ func Routers() *gin.Engine {
systemRouter.InitWebsiteAcmeAccountRouter(PrivateGroup)
systemRouter.InitNginxRouter(PrivateGroup)
systemRouter.InitRuntimeRouter(PrivateGroup)
systemRouter.InitProcessRouter(PrivateGroup)
}
return Router

1
backend/router/entry.go

@ -20,6 +20,7 @@ type RouterGroup struct {
DatabaseRouter
NginxRouter
RuntimeRouter
ProcessRouter
}
var RouterGroupApp = new(RouterGroup)

20
backend/router/ro_process.go

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

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

30
backend/utils/websocket/client.go

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

38
backend/utils/websocket/client_manager.go

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

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

482
cmd/server/docs/docs.go

File diff suppressed because it is too large Load Diff

411
cmd/server/docs/swagger.json

File diff suppressed because it is too large Load Diff

384
cmd/server/docs/swagger.yaml

File diff suppressed because it is too large Load Diff

5
frontend/src/api/interface/process.ts

@ -0,0 +1,5 @@
export namespace Process {
export interface StopReq {
PID: number;
}
}

6
frontend/src/api/modules/process.ts

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

5
frontend/src/components/complex-table/index.vue

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

36
frontend/src/lang/modules/en.ts

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

36
frontend/src/lang/modules/zh.ts

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

11
frontend/src/routers/modules/host.ts

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

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

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

@ -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…
Cancel
Save