1
0
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:
zhengkunwang223 2023-06-28 14:50:10 +08:00 committed by GitHub
parent c403eb55b1
commit 38c0d290e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1652 additions and 619 deletions

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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