feat: Implement process management and network data handling (#11270)

- Introduced ProcessStore for managing process and network data via WebSocket.
- Enhanced TableSearch component to synchronize search parameters with props.
- Updated network and process views to utilize ProcessStore for data fetching and state management.
- Improved data filtering and sorting in network and process views.
- Added WebSocket connection management with polling for real-time updates.
This commit is contained in:
KOMATA 2025-12-09 17:31:23 +08:00 committed by GitHub
parent 38985671c6
commit 38ab0d9ca0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 441 additions and 174 deletions

View file

@ -15,7 +15,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, watch } from 'vue';
defineOptions({ name: 'TableSearch' });
const emit = defineEmits(['search', 'update:searchName']);
@ -26,8 +26,22 @@ const props = defineProps({
type: Boolean,
default: false,
},
searchName: {
type: [String, Number],
default: undefined,
},
});
watch(
() => props.searchName,
(newVal) => {
if (searchInfo.value !== newVal) {
searchInfo.value = newVal;
}
},
{ immediate: true },
);
const search = () => {
emit('update:searchName', searchInfo.value);
emit('search');

View file

@ -4,10 +4,11 @@ import GlobalStore from './modules/global';
import MenuStore from './modules/menu';
import TabsStore from './modules/tabs';
import TerminalStore from './modules/terminal';
import ProcessStore from './modules/process';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export { GlobalStore, MenuStore, TabsStore, TerminalStore };
export { GlobalStore, MenuStore, TabsStore, TerminalStore, ProcessStore };
export default pinia;

View file

@ -0,0 +1,320 @@
import { defineStore } from 'pinia';
import { ref, reactive } from 'vue';
export interface PsSearch {
type: 'ps';
pid: number | undefined;
username: string;
name: string;
}
export interface NetSearch {
type: 'net';
processID: number | undefined;
processName: string;
port: number | undefined;
}
export const ProcessStore = defineStore('ProcessStore', () => {
let websocket: WebSocket | null = null;
let pollingTimer: ReturnType<typeof setInterval> | null = null;
let disconnectTimer: ReturnType<typeof setTimeout> | null = null;
let connectionRefCount = 0;
const isConnected = ref(false);
const isConnecting = ref(false);
const psData = ref<any[]>([]);
const psLoading = ref(false);
const psSearch = reactive<PsSearch>({
type: 'ps',
pid: undefined,
username: '',
name: '',
});
const netData = ref<any[]>([]);
const netLoading = ref(false);
const netSearch = reactive<NetSearch>({
type: 'net',
processID: undefined,
processName: '',
port: undefined,
});
let pendingRequestType: 'ps' | 'net' | null = null;
let queuedRequestType: 'ps' | 'net' | null = null;
const isPsFetching = ref(false);
const isNetFetching = ref(false);
const activePollingType = ref<'ps' | 'net' | null>(null);
const isWsOpen = () => {
return websocket && websocket.readyState === WebSocket.OPEN;
};
const onOpen = () => {
isConnected.value = true;
isConnecting.value = false;
};
const doSendMessage = (type: 'ps' | 'net') => {
pendingRequestType = type;
if (type === 'ps') {
isPsFetching.value = true;
psLoading.value = psData.value.length === 0;
const searchParams = { ...psSearch };
if (typeof searchParams.pid === 'string') {
searchParams.pid = Number(searchParams.pid);
}
websocket!.send(JSON.stringify(searchParams));
} else {
isNetFetching.value = true;
netLoading.value = netData.value.length === 0;
const searchParams = { ...netSearch };
if (typeof searchParams.processID === 'string') {
searchParams.processID = Number(searchParams.processID);
}
if (typeof searchParams.port === 'string') {
searchParams.port = Number(searchParams.port);
}
websocket!.send(JSON.stringify(searchParams));
}
};
const onMessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
const responseType = pendingRequestType;
if (pendingRequestType === 'ps') {
isPsFetching.value = false;
} else if (pendingRequestType === 'net') {
isNetFetching.value = false;
}
pendingRequestType = null;
if (responseType === activePollingType.value) {
if (responseType === 'ps') {
psData.value = data || [];
psLoading.value = false;
} else if (responseType === 'net') {
netData.value = data || [];
netLoading.value = false;
}
}
if (queuedRequestType && isWsOpen()) {
const typeToSend = queuedRequestType;
queuedRequestType = null;
doSendMessage(typeToSend);
}
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
const onError = () => {
console.error('WebSocket error');
};
const onClose = () => {
isConnected.value = false;
isConnecting.value = false;
websocket = null;
};
const initWebSocket = (currentNode: string) => {
if (websocket || isConnecting.value) {
return;
}
isConnecting.value = true;
const href = window.location.href;
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
const ipLocal = href.split('//')[1].split('/')[0];
websocket = new WebSocket(`${protocol}://${ipLocal}/api/v2/process/ws?operateNode=${currentNode}`);
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
};
const closeWebSocket = () => {
stopPolling();
if (websocket) {
websocket.close();
websocket = null;
}
isConnected.value = false;
isConnecting.value = false;
};
const connect = (currentNode: string) => {
if (disconnectTimer) {
clearTimeout(disconnectTimer);
disconnectTimer = null;
}
connectionRefCount++;
if (!websocket && !isConnecting.value) {
initWebSocket(currentNode);
}
};
const disconnect = () => {
connectionRefCount = Math.max(0, connectionRefCount - 1);
if (connectionRefCount === 0) {
disconnectTimer = setTimeout(() => {
if (connectionRefCount === 0) {
closeWebSocket();
}
}, 500);
}
};
const sendPsMessage = () => {
if (!isWsOpen()) {
return;
}
if (pendingRequestType !== null) {
queuedRequestType = 'ps';
return;
}
if (isPsFetching.value) {
return;
}
doSendMessage('ps');
};
const sendNetMessage = () => {
if (!isWsOpen()) {
return;
}
if (pendingRequestType !== null) {
queuedRequestType = 'net';
return;
}
if (isNetFetching.value) {
return;
}
doSendMessage('net');
};
const startPolling = (type: 'ps' | 'net', interval = 3000, initialDelay = 0) => {
stopPolling();
activePollingType.value = type;
const sendInitial = () => {
if (type === 'ps') {
sendPsMessage();
} else {
sendNetMessage();
}
};
const scheduleInitialFetch = () => {
if (initialDelay > 0) {
setTimeout(sendInitial, initialDelay);
} else {
sendInitial();
}
};
if (isWsOpen()) {
scheduleInitialFetch();
} else {
const checkConnection = setInterval(() => {
if (isWsOpen()) {
clearInterval(checkConnection);
scheduleInitialFetch();
}
}, 100);
setTimeout(() => clearInterval(checkConnection), 5000);
}
pollingTimer = setInterval(() => {
if (type === 'ps') {
sendPsMessage();
} else {
sendNetMessage();
}
}, interval);
};
const stopPolling = () => {
if (pollingTimer) {
clearInterval(pollingTimer);
pollingTimer = null;
}
activePollingType.value = null;
};
const updatePsSearch = (params: Partial<Omit<PsSearch, 'type'>>) => {
Object.assign(psSearch, params);
};
const updateNetSearch = (params: Partial<Omit<NetSearch, 'type'>>) => {
Object.assign(netSearch, params);
};
const resetPsSearch = () => {
psSearch.pid = undefined;
psSearch.username = '';
psSearch.name = '';
};
const resetNetSearch = () => {
netSearch.processID = undefined;
netSearch.processName = '';
netSearch.port = undefined;
};
return {
isConnected,
isConnecting,
psData,
psLoading,
psSearch,
netData,
netLoading,
netSearch,
isPsFetching,
isNetFetching,
activePollingType,
isWsOpen,
connect,
disconnect,
initWebSocket,
closeWebSocket,
sendPsMessage,
sendNetMessage,
startPolling,
stopPolling,
updatePsSearch,
updateNetSearch,
resetPsSearch,
resetNetSearch,
};
});
export default ProcessStore;

View file

@ -1,7 +1,7 @@
<template>
<div>
<FireRouter />
<LayoutContent :title="$t('menu.network', 2)" v-loading="loading">
<LayoutContent :title="$t('menu.network', 2)" v-loading="processStore.netLoading">
<template #rightToolBar>
<div class="w-full flex justify-end items-center gap-5">
<el-select
@ -25,17 +25,17 @@
<TableSearch
@search="search()"
:placeholder="$t('process.pid')"
v-model:searchName="netSearch.processID"
v-model:searchName="processStore.netSearch.processID"
/>
<TableSearch
@search="search()"
:placeholder="$t('process.processName')"
v-model:searchName="netSearch.processName"
v-model:searchName="processStore.netSearch.processName"
/>
<TableSearch
@search="search()"
:placeholder="$t('commons.table.port')"
v-model:searchName="netSearch.port"
v-model:searchName="processStore.netSearch.port"
/>
</div>
</template>
@ -62,9 +62,10 @@
<script setup lang="ts">
import FireRouter from '@/views/host/process/index.vue';
import { ref, onMounted, onUnmounted, reactive, watch } from 'vue';
import { GlobalStore } from '@/store';
import { SortBy, TableV2SortOrder } from 'element-plus';
import { ref, onMounted, onUnmounted, watch, h } from 'vue';
import { GlobalStore, ProcessStore } from '@/store';
import { SortBy, TableV2SortOrder, ElIcon } from 'element-plus';
import { Filter } from '@element-plus/icons-vue';
import i18n from '@/lang';
const statusOptions = [
@ -76,24 +77,29 @@ const statusOptions = [
];
const globalStore = GlobalStore();
const processStore = ProcessStore();
const netSearch = reactive({
type: 'net',
processID: undefined,
processName: '',
port: undefined,
});
const quickSearchName = (name: string) => {
processStore.netSearch.processID = undefined;
processStore.netSearch.processName = name;
processStore.netSearch.port = undefined;
search();
};
const quickSearchPort = (port: number) => {
processStore.netSearch.processID = undefined;
processStore.netSearch.processName = '';
processStore.netSearch.port = port;
search();
};
let processSocket = ref(null) as unknown as WebSocket;
const data = ref<any[]>([]);
const oldData = ref<any[]>([]);
const loading = ref(false);
const sortState = ref<SortBy>({
key: 'PID',
order: TableV2SortOrder.ASC,
});
const filters = ref<string[]>([]);
const filters = ref<string[]>(['LISTEN', 'ESTABLISHED']);
const sortByNum = (a: any, b: any, prop: string): number => {
const aVal = parseFloat(a[prop]) || 0;
@ -121,6 +127,23 @@ const columns = ref([
title: i18n.global.t('process.processName'),
dataKey: 'name',
width: 300,
cellRenderer: ({ rowData }) => {
return h('div', { class: 'flex items-center gap-1' }, [
h('span', { class: 'truncate', title: rowData.name }, rowData.name),
h(
ElIcon,
{
class: 'cursor-pointer hover:text-primary ml-1 flex-shrink-0',
size: 14,
onClick: (e: Event) => {
e.stopPropagation();
quickSearchName(rowData.name);
},
},
() => h(Filter),
),
]);
},
},
{
key: 'localaddr',
@ -129,7 +152,25 @@ const columns = ref([
width: 350,
cellRenderer: ({ rowData }) => {
const addr = rowData.localaddr;
return addr?.ip ? `${addr.ip}${addr.port > 0 ? ':' + addr.port : ''}` : '';
const addrStr = addr?.ip ? `${addr.ip}${addr.port > 0 ? ':' + addr.port : ''}` : '';
const hasPort = addr?.port > 0;
return h('div', { class: 'flex items-center gap-1' }, [
h('span', {}, addrStr),
hasPort
? h(
ElIcon,
{
class: 'cursor-pointer hover:text-primary ml-1',
size: 12,
onClick: (e: Event) => {
e.stopPropagation();
quickSearchPort(addr.port);
},
},
() => h(Filter),
)
: null,
]);
},
},
{
@ -152,9 +193,12 @@ const columns = ref([
]);
watch(
[sortState, oldData, filters],
[sortState, () => processStore.netData, filters],
([newState, newData, newFilters]) => {
if (!newData?.length) return;
if (!newData?.length) {
data.value = [];
return;
}
let filtered = newData;
if (newFilters.length > 0) {
@ -187,70 +231,18 @@ const changeSort = ({ key, order }) => {
sortState.value = { key, order };
};
const filterByStatus = () => {
if (filters.value.length > 0) {
return oldData.value.filter((row) => filters.value.includes(row.status));
}
return oldData.value;
};
const isWsOpen = () => processSocket && processSocket.readyState === 1;
const closeSocket = () => {
if (isWsOpen()) processSocket.close();
};
const onOpenProcess = () => {
loading.value = true;
processSocket.send(JSON.stringify(netSearch));
};
const onMessage = (message: any) => {
oldData.value = JSON.parse(message.data);
data.value = filterByStatus();
if (data.value == null) {
data.value = [];
}
loading.value = false;
};
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];
let currentNode = globalStore.currentNode;
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v2/process/ws?operateNode=${currentNode}`);
processSocket.onopen = onOpenProcess;
processSocket.onmessage = onMessage;
processSocket.onerror = onerror;
processSocket.onclose = onClose;
search();
sendMsg();
};
const sendMsg = () => {
setInterval(() => {
search();
}, 3000);
};
const search = () => {
if (isWsOpen()) {
if (typeof netSearch.processID === 'string') {
netSearch.processID = Number(netSearch.processID);
}
if (typeof netSearch.port === 'string') {
netSearch.port = Number(netSearch.port);
}
processSocket.send(JSON.stringify(netSearch));
}
processStore.sendNetMessage();
};
onMounted(() => {
initProcess();
processStore.connect(globalStore.currentNode);
const initialDelay = processStore.netData.length > 0 ? 500 : 0;
processStore.startPolling('net', 3000, initialDelay);
});
onUnmounted(() => {
closeSocket();
processStore.stopPolling();
processStore.disconnect();
});
</script>

View file

@ -1,7 +1,7 @@
<template>
<div>
<FireRouter />
<LayoutContent :title="$t('menu.process', 2)" v-loading="loading">
<LayoutContent :title="$t('menu.process', 2)" v-loading="processStore.psLoading">
<template #rightToolBar>
<div class="w-full flex justify-end items-center gap-5">
<el-select
@ -25,17 +25,17 @@
<TableSearch
@search="search()"
:placeholder="$t('process.pid')"
v-model:searchName="processSearch.pid"
v-model:searchName="processStore.psSearch.pid"
/>
<TableSearch
@search="search()"
:placeholder="$t('commons.table.name')"
v-model:searchName="processSearch.name"
v-model:searchName="processStore.psSearch.name"
/>
<TableSearch
@search="search()"
:placeholder="$t('commons.table.user')"
v-model:searchName="processSearch.username"
v-model:searchName="processStore.psSearch.username"
/>
</div>
</template>
@ -64,13 +64,15 @@
<script setup lang="ts">
import FireRouter from '@/views/host/process/index.vue';
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import { ref, onMounted, onUnmounted, computed, watch, h } from 'vue';
import ProcessDetail from './detail/index.vue';
import i18n from '@/lang';
import { stopProcess } from '@/api/modules/process';
import { GlobalStore } from '@/store';
import { GlobalStore, ProcessStore } from '@/store';
import { SortBy, TableV2SortOrder, ElButton } from 'element-plus';
const globalStore = GlobalStore();
const processStore = ProcessStore();
const statusOptions = computed(() => [
{ text: i18n.global.t('process.running'), value: 'running' },
@ -82,25 +84,15 @@ const statusOptions = computed(() => [
{ text: i18n.global.t('process.zombie'), value: 'zombie' },
]);
const processSearch = reactive({
type: 'ps',
pid: undefined,
username: '',
name: '',
});
const opRef = ref();
const sortState = ref<SortBy>({
key: 'PID',
order: TableV2SortOrder.ASC,
});
let processSocket = ref(null) as unknown as WebSocket;
const data = ref([]);
const loading = ref(false);
const oldData = ref([]);
const data = ref<any[]>([]);
const detailRef = ref();
const isGetData = ref(true);
const filters = ref([]);
const filters = ref<string[]>([]);
const sortByNum = (a: any, b: any, prop: string): number => {
const aVal = parseFloat(a[prop]) || 0;
@ -217,23 +209,32 @@ const columns = ref([
]);
watch(
[sortState, oldData],
([newState, newData]) => {
if (!newData?.length) return;
[sortState, () => processStore.psData, filters],
([newState, newData, newFilters]) => {
if (!newData?.length) {
data.value = [];
return;
}
let filtered = newData;
if (newFilters.length > 0) {
filtered = filtered.filter((re: any) => newFilters.includes(re.status));
}
const { key, order } = newState ?? {};
if (!key || !order) {
data.value = filterByStatus();
data.value = filtered;
return;
}
const currCol = columns.value.find((c) => c.key === key);
if (!currCol) return;
if (!currCol) {
data.value = filtered;
return;
}
const currSortMethod = currCol.sortMethod ?? sortByNum;
const filteredData = filterByStatus();
data.value = filteredData.slice(0).sort((a, b) => {
data.value = filtered.slice(0).sort((a, b) => {
const res = (currSortMethod as any)(a, b, currCol.dataKey);
return order === TableV2SortOrder.ASC ? res : 0 - res;
});
@ -250,72 +251,8 @@ const changeSort = ({ key, order }) => {
sortState.value = { key, order };
};
const isWsOpen = () => {
const readyState = processSocket && processSocket.readyState;
return readyState === 1;
};
const closeSocket = () => {
if (isWsOpen()) {
processSocket && processSocket.close();
}
};
const onOpenProcess = () => {
loading.value = true;
isGetData.value = true;
processSocket.send(JSON.stringify(processSearch));
};
const onMessage = (message: any) => {
isGetData.value = false;
oldData.value = JSON.parse(message.data);
data.value = filterByStatus();
if (data.value == null) {
data.value = [];
}
loading.value = false;
};
const filterByStatus = () => {
if (filters.value.length > 0) {
const newData = oldData.value.filter((re: any) => {
return (filters.value as string[]).indexOf(re.status) > -1;
});
return newData;
} else {
return oldData.value;
}
};
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];
let currentNode = globalStore.currentNode;
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v2/process/ws?operateNode=${currentNode}`);
processSocket.onopen = onOpenProcess;
processSocket.onmessage = onMessage;
processSocket.onerror = onerror;
processSocket.onclose = onClose;
sendMsg();
};
const sendMsg = () => {
setInterval(() => {
search();
}, 3000);
};
const search = () => {
if (isWsOpen() && !isGetData.value) {
isGetData.value = true;
if (typeof processSearch.pid === 'string') {
processSearch.pid = Number(processSearch.pid);
}
processSocket.send(JSON.stringify(processSearch));
}
processStore.sendPsMessage();
};
const stop = async (row: any) => {
@ -333,10 +270,13 @@ const stop = async (row: any) => {
};
onMounted(() => {
initProcess();
processStore.connect(globalStore.currentNode);
const initialDelay = processStore.psData.length > 0 ? 500 : 0;
processStore.startPolling('ps', 3000, initialDelay);
});
onUnmounted(() => {
closeSocket();
processStore.stopPolling();
processStore.disconnect();
});
</script>