修复调试脚本日志丢失

This commit is contained in:
whyour 2023-09-22 00:46:16 +08:00
parent ab3fc9b5f1
commit a864a56917
14 changed files with 336 additions and 176 deletions

View File

@ -34,7 +34,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let result = []; let result = [];
const blacklist = ['node_modules', '.git']; const blacklist = ['node_modules', '.git', '.pnpm'];
if (req.query.path) { if (req.query.path) {
const targetPath = path.join( const targetPath = path.join(
config.scriptPath, config.scriptPath,

View File

@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
export default class SockService { export default class SockService {
private clients: Connection[] = []; private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
public getClients() { public getClients() {
return this.clients; return this.clients;

View File

@ -273,13 +273,13 @@ update_qinglong() {
exit_status=$? exit_status=$?
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "更新青龙源文件成功...\n"
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp} unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
update_qinglong_static update_qinglong_static
else else
echo -e "\n更新青龙源文件失败,请检查网络...\n" echo -e "更新青龙源文件失败,请检查网络...\n"
fi fi
} }
@ -288,28 +288,29 @@ update_qinglong_static() {
exit_status=$? exit_status=$?
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n" echo -e "更新青龙静态资源成功...\n"
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp} unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
check_update_dep check_update_dep
else else
echo -e "\n更新青龙静态资源失败,请检查网络...\n" echo -e "更新青龙静态资源失败,请检查网络...\n"
fi fi
} }
check_update_dep() { check_update_dep() {
echo -e "\n开始检测依赖...\n" echo -e "\n开始检测依赖...\n"
if [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}" npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n" echo -e "\n依赖检测安装成功...\n"
echo -e "\n更新包下载成功...\n" echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then if [[ "$needRestart" == 'true' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/ cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/

View File

@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
@ -29,9 +29,9 @@ import {
MenuProps, MenuProps,
} from 'antd'; } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import WebSocketManager from '../utils/websocket';
export interface SharedContext { export interface SharedContext {
headerStyle: React.CSSProperties; headerStyle: React.CSSProperties;
@ -40,7 +40,6 @@ export interface SharedContext {
user: any; user: any;
reloadUser: (needLoading?: boolean) => void; reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void; reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo; systemInfo: TSystemInfo;
} }
@ -60,8 +59,6 @@ export default function () {
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>(); const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true); const [initLoading, setInitLoading] = useState<boolean>(true);
const { const {
@ -180,32 +177,14 @@ export default function () {
useEffect(() => { useEffect(() => {
if (!user || !user.username) return; if (!user || !user.username) return;
ws.current = new SockJS( const ws = WebSocketManager.getInstance(
`${window.location.origin}/api/ws?token=${localStorage.getItem( `${window.location.origin}/api/ws?token=${localStorage.getItem(
config.authKey, config.authKey,
)}`, )}`,
); );
ws.current.onmessage = (e: any) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('WS connection succeeded !!!');
} else {
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
} catch (error) {
console.log('websocket连接失败', e);
}
};
const wsCurrent = ws.current;
return () => { return () => {
wsCurrent.close(); ws.close();
}; };
}, [user]); }, [user]);
@ -387,7 +366,6 @@ export default function () {
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage,
systemInfo, systemInfo,
}} }}
/> />

View File

@ -35,6 +35,7 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import WebSocketManager from '@/utils/websocket';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@ -87,8 +88,7 @@ const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
}; };
const Dependence = () => { const Dependence = () => {
const { headerStyle, isPhone, socketMessage } = const { headerStyle, isPhone } = useOutletContext<SharedContext>();
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: intl.get('序号'), title: intl.get('序号'),
@ -109,11 +109,12 @@ const Dependence = () => {
width: 120, width: 120,
dataIndex: 'status', dataIndex: 'status',
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
console.log(record.status);
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
<Tag <Tag
color={StatusMap[record.status].color} color={StatusMap[record.status]?.color}
icon={StatusMap[record.status].icon} icon={StatusMap[record.status]?.icon}
style={{ marginRight: 0 }} style={{ marginRight: 0 }}
> >
{intl.get(Status[record.status])} {intl.get(Status[record.status])}
@ -395,63 +396,62 @@ const Dependence = () => {
} }
}, [logDependence]); }, [logDependence]);
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) return; const { message, references } = payload;
const { type, message, references } = socketMessage; let status: number | undefined = undefined;
if ( if (message.includes('开始时间') && references.length > 0) {
type === 'installDependence' && status = message.includes('安装') ? Status.安装中 : Status.删除中;
message.includes('开始时间') &&
references.length > 0
) {
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status: message.includes('安装') ? Status.安装中 : Status.删除中,
});
}
}
setValue(result);
} }
if ( if (message.includes('结束时间') && references.length > 0) {
type === 'installDependence' &&
message.includes('结束时间') &&
references.length > 0
) {
let status;
if (message.includes('安装')) { if (message.includes('安装')) {
status = message.includes('成功') ? Status.已安装 : Status.安装失败; status = message.includes('成功') ? Status.已安装 : Status.安装失败;
} else { } else {
status = message.includes('成功') ? Status.已删除 : Status.删除失败; status = message.includes('成功') ? Status.已删除 : Status.删除失败;
} }
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status,
});
}
}
setValue(result);
if (status === Status.) { if (status === Status.) {
setTimeout(() => { setTimeout(() => {
const _result = [...value]; setValue((p) => {
for (let i = 0; i < references.length; i++) { const _result = [...p];
const index = value.findIndex((x) => x.id === references[i]); for (let i = 0; i < references.length; i++) {
if (index !== -1) { const index = p.findIndex((x) => x.id === references[i]);
_result.splice(index, 1); if (index !== -1) {
_result.splice(index, 1);
}
} }
} return _result;
setValue(_result); });
}, 5000); }, 5000);
return;
} }
} }
}, [socketMessage]); if (typeof status === 'number') {
setValue((p) => {
const result = [...p];
for (let i = 0; i < references.length; i++) {
const index = p.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...p[index],
status,
});
}
}
return result;
});
}
}, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage);
ws.subscribe('uninstallDependence', handleMessage);
return () => {
ws.unsubscribe('installDependence', handleMessage);
ws.unsubscribe('uninstallDependence', handleMessage);
};
}, []);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]); setSelectedRowIds([]);
@ -548,24 +548,25 @@ const Dependence = () => {
dependence={editedDependence} dependence={editedDependence}
defaultType={type} defaultType={type}
/> />
<DependenceLogModal {logDependence && (
visible={isLogModalVisible} <DependenceLogModal
handleCancel={(needRemove?: boolean) => { visible={isLogModalVisible}
setIsLogModalVisible(false); handleCancel={(needRemove?: boolean) => {
if (needRemove) { setIsLogModalVisible(false);
const index = value.findIndex((x) => x.id === logDependence.id); if (needRemove) {
const result = [...value]; const index = value.findIndex((x) => x.id === logDependence.id);
if (index !== -1) { const result = [...value];
result.splice(index, 1); if (index !== -1) {
setValue(result); result.splice(index, 1);
setValue(result);
}
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
getDependenceDetail(logDependence);
} }
} else if ([...value].map((x) => x.id).includes(logDependence.id)) { }}
getDependenceDetail(logDependence); dependence={logDependence}
} />
}} )}
socketMessage={socketMessage}
dependence={logDependence}
/>
</PageContainer> </PageContainer>
); );
}; };

View File

@ -9,17 +9,16 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import WebSocketManager from '@/utils/websocket';
const DependenceLogModal = ({ const DependenceLogModal = ({
dependence, dependence,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
dependence?: any; dependence?: any;
visible: boolean; visible: boolean;
handleCancel: (needRemove?: boolean) => void; handleCancel: (needRemove?: boolean) => void;
socketMessage: any;
}) => { }) => {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
@ -54,7 +53,7 @@ const DependenceLogModal = ({
code === 200 && code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id) localStorage.getItem('logDependence') === String(dependence.id)
) { ) {
const log = (data.log || []).join('') as string; const log = (data?.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败')); setIsRemoveFailed(log.includes('删除失败'));
@ -95,21 +94,25 @@ const DependenceLogModal = ({
} }
}, [dependence]); }, [dependence]);
useEffect(() => { const handleMessage = (payload: any) => {
if (!socketMessage || !dependence) return; const { message, references } = payload;
const { type, message, references } = socketMessage; if (references.length > 0 && references.includes(dependence.id)) {
if (
type === 'installDependence' &&
references.length > 0 &&
references.includes(dependence.id)
) {
if (message.includes('结束时间')) { if (message.includes('结束时间')) {
setExecuting(false); setExecuting(false);
setIsRemoveFailed(message.includes('删除失败')); setIsRemoveFailed(message.includes('删除失败'));
} }
setValue(`${value}${message}`); setValue((p) => `${p}${message}`);
} }
}, [socketMessage]); };
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage);
return () => {
ws.unsubscribe('installDependence', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);

View File

@ -1,5 +1,11 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, {
useEffect,
useState,
useRef,
useCallback,
useReducer,
} from 'react';
import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd'; import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@ -9,6 +15,7 @@ import SaveModal from './saveModal';
import SettingModal from './setting'; import SettingModal from './setting';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { getEditorMode, logEnded } from '@/utils'; import { getEditorMode, logEnded } from '@/utils';
import WebSocketManager from '@/utils/websocket';
const { Option } = Select; const { Option } = Select;
@ -18,12 +25,10 @@ const EditModal = ({
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
treeData?: any; treeData?: any;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any;
currentNode: any; currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
@ -34,12 +39,11 @@ const EditModal = ({
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentPid, setCurrentPid] = useState(null); const [currentPid, setCurrentPid] = useState(null);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
}; };
@ -104,28 +108,25 @@ const EditModal = ({
}); });
}; };
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) { let { message: _message } = payload;
return;
}
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (logEnded(_message)) { if (logEnded(_message)) {
setTimeout(() => { setTimeout(() => {
setIsRunning(false); setIsRunning(false);
}, 300); }, 300);
} }
if (log) { setLog(p=>`${p}${_message}`);
_message = `\n${_message}`; }, []);
}
setLog(`${log}${_message}`); useEffect(() => {
}, [socketMessage]); const ws = WebSocketManager.getInstance();
ws.subscribe('manuallyRunScript', handleMessage);
return () => {
ws.unsubscribe('manuallyRunScript', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
setLog(''); setLog('');

View File

@ -48,7 +48,7 @@ import { langs } from '@uiw/codemirror-extensions-langs';
const { Text } = Typography; const { Text } = Typography;
const Script = () => { const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } = const { headerStyle, isPhone, theme } =
useOutletContext<SharedContext>(); useOutletContext<SharedContext>();
const [value, setValue] = useState(intl.get('请选择脚本文件')); const [value, setValue] = useState(intl.get('请选择脚本文件'));
const [select, setSelect] = useState<string>(''); const [select, setSelect] = useState<string>('');
@ -591,16 +591,15 @@ const Script = () => {
}} }}
/> />
)} )}
<EditModal {isLogModalVisible && <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentNode={currentNode} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />}
<EditScriptNameModal <EditScriptNameModal
visible={isAddFileModalVisible} visible={isAddFileModalVisible}
treeData={data} treeData={data}

View File

@ -1,12 +1,14 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd'; import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import WebSocketManager from '@/utils/websocket';
import Ansi from 'ansi-to-react';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage, systemInfo }: any) => { const CheckUpdate = ({ systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
@ -149,17 +151,8 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
}; };
useEffect(() => { useEffect(() => {
if (!modalRef.current || !socketMessage) { if (!value) return;
return; const updateFailed = value.includes('失败,请检查');
}
const { type, message: _message, references } = socketMessage;
if (type !== 'updateSystemVersion') {
return;
}
const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败');
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed, maskClosable: updateFailed,
@ -167,29 +160,46 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
okButtonProps: { disabled: !updateFailed }, okButtonProps: { disabled: !updateFailed },
content: ( content: (
<> <>
<pre>{newMessage}</pre> <pre>
<Ansi>{value}</Ansi>
</pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div> <div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</> </>
), ),
}); });
}, [value]);
if (updateFailed && !value.includes('失败,请检查')) { const handleMessage = useCallback((payload: any) => {
let { message: _message } = payload;
const updateFailed = _message.includes('失败,请检查');
if (updateFailed) {
message.error(intl.get('更新失败,请检查网络及日志或稍后再试')); message.error(intl.get('更新失败,请检查网络及日志或稍后再试'));
} }
setValue(newMessage); setTimeout(() => {
document.getElementById('log-identifier') &&
document document
.getElementById('log-identifier')! .querySelector('#log-identifier')!
.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); .scrollIntoView({ behavior: 'smooth' });
}, 600);
if (_message.includes('更新包下载成功')) { if (_message.includes('更新包下载成功')) {
setTimeout(() => { setTimeout(() => {
showReloadModal(); showReloadModal();
}, 1000); }, 1000);
} }
}, [socketMessage]);
setValue((p) => `${p}${_message}`);
}, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('updateSystemVersion', handleMessage);
return () => {
ws.unsubscribe('updateSystemVersion', handleMessage);
};
}, []);
return ( return (
<> <>

View File

@ -46,7 +46,6 @@ const Setting = () => {
theme, theme,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage,
systemInfo, systemInfo,
} = useOutletContext<SharedContext>(); } = useOutletContext<SharedContext>();
const columns = [ const columns = [
@ -376,7 +375,6 @@ const Setting = () => {
children: ( children: (
<Other <Other
reloadTheme={reloadTheme} reloadTheme={reloadTheme}
socketMessage={socketMessage}
systemInfo={systemInfo} systemInfo={systemInfo}
/> />
), ),

View File

@ -24,9 +24,8 @@ import useProgress from './progress';
const Other = ({ const Other = ({
systemInfo, systemInfo,
socketMessage,
reloadTheme, reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => { }: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [systemConfig, setSystemConfig] = useState<{ const [systemConfig, setSystemConfig] = useState<{
logRemoveFrequency?: number | null; logRemoveFrequency?: number | null;
@ -274,7 +273,7 @@ const Other = ({
</Upload> </Upload>
</Form.Item> </Form.Item>
<Form.Item label={intl.get('检查更新')} name="update"> <Form.Item label={intl.get('检查更新')} name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} /> <CheckUpdate systemInfo={systemInfo} />
</Form.Item> </Form.Item>
</Form> </Form>
); );

View File

@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { import {
Button, Button,
message, message,
@ -36,6 +36,7 @@ import './index.less';
import SubscriptionLogModal from './logModal'; import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import WebSocketManager from '@/utils/websocket';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@ -61,8 +62,7 @@ export enum SubscriptionType {
} }
const Subscription = () => { const Subscription = () => {
const { headerStyle, isPhone, socketMessage } = const { headerStyle, isPhone } = useOutletContext<SharedContext>();
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
@ -508,23 +508,31 @@ const Subscription = () => {
: 'subscription'; : 'subscription';
}; };
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) return; const { message, references } = payload;
const { type, message, references } = socketMessage; setValue((p) => {
if (type === 'runSubscriptionEnd' && references.length > 0) { const result = [...p];
const result = [...value];
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]); const index = p.findIndex((x) => x.id === references[i]);
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...value[index], ...p[index],
status: SubscriptionStatus.idle, status: SubscriptionStatus.idle,
}); });
} }
} }
setValue(result); return result;
} });
}, [socketMessage]); }, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('runSubscriptionEnd', handleMessage);
return () => {
ws.unsubscribe('runSubscriptionEnd', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
if (logSubscription) { if (logSubscription) {

8
src/utils/type.ts Normal file
View File

@ -0,0 +1,8 @@
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd'
| 'reloadSystem';

154
src/utils/websocket.ts Normal file
View File

@ -0,0 +1,154 @@
import SockJS from 'sockjs-client';
import { SockMessageType } from './type';
class WebSocketManager {
private static instance: WebSocketManager | null = null;
private url: string;
private socket: WebSocket | null = null;
private subscriptions: Map<SockMessageType, Set<(p: any) => void>> = new Map();
private options: {
maxReconnectAttempts: number;
reconnectInterval: number;
heartbeatInterval: number;
};
private reconnectAttempts: number = 0;
private heartbeatTimeout: NodeJS.Timeout | null = null;
private state: 'closed' | 'connecting' | 'open' = 'closed';
constructor(url: string, options: Partial<typeof WebSocketManager.prototype.options> = {}) {
this.url = url;
this.options = {
maxReconnectAttempts: options.maxReconnectAttempts || 5,
reconnectInterval: options.reconnectInterval || 3000,
heartbeatInterval: options.heartbeatInterval || 30000,
};
this.init();
}
public static getInstance(url: string = '', options?: Partial<typeof WebSocketManager.prototype.options>): WebSocketManager {
if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocketManager(url, options);
}
return WebSocketManager.instance;
}
private async init() {
try {
this.state = 'connecting';
this.emit('connecting');
while (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.socket = new SockJS(this.url);
this.setupEventListeners();
this.startHeartbeat();
await this.waitForClose();
this.stopHeartbeat();
this.socket = null;
this.reconnectAttempts++;
await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval));
}
} catch (error) {
this.handleError(error);
}
}
private setupEventListeners() {
if (!this.socket) return;
this.socket.onopen = () => {
this.state = 'open';
this.emit('open');
};
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
this.dispatchMessage(message);
};
this.socket.onclose = () => {
this.state = 'closed';
this.emit('close');
};
}
private async waitForClose() {
while (this.socket?.readyState !== SockJS.CLOSED) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
public subscribe(topic: SockMessageType, callback: (v: any) => void) {
const topicSubscriptions = this.subscriptions.get(topic) || new Set();
if (!topicSubscriptions.has(callback)) {
topicSubscriptions.add(callback);
this.subscriptions.set(topic, topicSubscriptions);
const subscriptionMessage = { action: 'subscribe', topic };
this.send(subscriptionMessage);
}
}
public unsubscribe(topic: SockMessageType, callback: (v: any) => void) {
const topicSubscriptions = this.subscriptions.get(topic) || new Set();
if (topicSubscriptions.has(callback)) {
topicSubscriptions.delete(callback);
const unsubscribeMessage = { action: 'unsubscribe', topic };
this.send(unsubscribeMessage);
}
}
public send(message: any) {
if (this.socket?.readyState === SockJS.OPEN) {
this.socket.send(JSON.stringify(message));
}
}
private dispatchMessage(message: any) {
const { type, ...others } = message;
const topicSubscriptions = this.subscriptions.get(type) || new Set();
[...topicSubscriptions].forEach((callback) => callback(others));
}
private startHeartbeat() {
this.heartbeatTimeout = setInterval(() => {
if (this.socket?.readyState === SockJS.OPEN) {
this.socket.send(JSON.stringify({ type: 'heartbeat' }));
}
}, this.options.heartbeatInterval);
}
private stopHeartbeat() {
if (this.heartbeatTimeout) {
clearInterval(this.heartbeatTimeout);
}
}
public close() {
if (this.socket) {
this.state = 'closed';
this.stopHeartbeat();
this.socket.close();
this.emit('close');
}
}
private handleError(error: any) {
console.error('WebSocket错误:', error);
this.emit('error', error);
}
public on(event: string, listener: Function) {
// this.addListener(event, listener);
}
public emit(event: string, data?: any) {
// this.listeners(event).forEach((listener) => listener(data));
}
}
export default WebSocketManager;