diff --git a/back/api/dependence.ts b/back/api/dependence.ts index 6998ddce..8c9c59fa 100644 --- a/back/api/dependence.ts +++ b/back/api/dependence.ts @@ -134,4 +134,20 @@ export default (app: Router) => { } }, ); + + route.put( + '/cancel', + celebrate({ + body: Joi.array().items(Joi.number().required()), + }), + async (req: Request, res: Response, next: NextFunction) => { + try { + const dependenceService = Container.get(DependenceService); + await dependenceService.cancel(req.body); + return res.send({ code: 200 }); + } catch (e) { + return next(e); + } + }, + ); }; diff --git a/back/config/util.ts b/back/config/util.ts index 50af7cee..0872547a 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -435,8 +435,8 @@ export async function killTask(pid: number) { } } -export async function getPid(name: string) { - const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`; +export async function getPid(cmd: string) { + const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`; const pid = await promiseExec(taskCommand); return pid ? Number(pid) : undefined; } diff --git a/back/services/dependence.ts b/back/services/dependence.ts index 0cb94d00..02af2b25 100644 --- a/back/services/dependence.ts +++ b/back/services/dependence.ts @@ -14,7 +14,12 @@ import { import { spawn } from 'cross-spawn'; import SockService from './sock'; import { FindOptions, Op } from 'sequelize'; -import { fileExist, promiseExecSuccess } from '../config/util'; +import { + fileExist, + getPid, + killTask, + promiseExecSuccess, +} from '../config/util'; import dayjs from 'dayjs'; import taskLimit from '../shared/pLimit'; @@ -86,11 +91,21 @@ export default class DependenceService { } public async dependencies( - { searchValue, type }: { searchValue: string; type: string }, - sort: any = { position: -1 }, + { + searchValue, + type, + status, + }: { searchValue: string; type: string; status: string }, + sort: any = [], query: any = {}, ): Promise { - let condition = { ...query, type: DependenceTypes[type as any] }; + let condition = { + ...query, + type: DependenceTypes[type as any], + }; + if (status) { + condition.status = status.split(',').map(Number); + } if (searchValue) { const encodeText = encodeURI(searchValue); const reg = { @@ -106,7 +121,7 @@ export default class DependenceService { }; } try { - const result = await this.find(condition); + const result = await this.find(condition, sort); return result as any; } catch (error) { throw error; @@ -134,6 +149,18 @@ export default class DependenceService { return docs; } + public async cancel(ids: number[]) { + const docs = await DependenceModel.findAll({ where: { id: ids } }); + for (const doc of docs) { + taskLimit.removeQueuedDependency(doc); + const depRunCommand = InstallDependenceCommandTypes[doc.type]; + const cmd = `${depRunCommand} ${doc.name.trim()}`; + const pid = await getPid(cmd); + pid && (await killTask(pid)); + } + await this.removeDb(ids); + } + private async find(query: any, sort: any = []): Promise { const docs = await DependenceModel.findAll({ where: { ...query }, @@ -168,8 +195,14 @@ export default class DependenceService { isInstall: boolean = true, force: boolean = false, ) { - return taskLimit.runOneByOne(() => { + return taskLimit.runDependeny(dependency, () => { return new Promise(async (resolve) => { + if (taskLimit.firstDependencyId !== dependency.id) { + return resolve(null); + } + + taskLimit.removeQueuedDependency(dependency); + const depIds = [dependency.id!]; const status = isInstall ? DependenceStatus.installing diff --git a/back/shared/pLimit.ts b/back/shared/pLimit.ts index 1aa84104..82a52bb1 100644 --- a/back/shared/pLimit.ts +++ b/back/shared/pLimit.ts @@ -2,11 +2,19 @@ import PQueue, { QueueAddOptions } from 'p-queue-cjs'; import os from 'os'; import { AuthDataType, SystemModel } from '../data/system'; import Logger from '../loaders/logger'; +import { Dependence } from '../data/dependence'; +interface IDependencyFn { + (): Promise; + dependency?: Dependence; +} class TaskLimit { - private oneLimit = new PQueue({ concurrency: 1 }); + private dependenyLimit = new PQueue({ concurrency: 1 }); + private queuedDependencyIds = new Set([]); private updateLogLimit = new PQueue({ concurrency: 1 }); - private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) }); + private cronLimit = new PQueue({ + concurrency: Math.max(os.cpus().length, 4), + }); get cronLimitActiveCount() { return this.cronLimit.pending; @@ -16,6 +24,10 @@ class TaskLimit { return this.cronLimit.size; } + get firstDependencyId() { + return [...this.queuedDependencyIds.values()][0]; + } + constructor() { this.setCustomLimit(); this.handleEvents(); @@ -26,21 +38,19 @@ class TaskLimit { Logger.info( `[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`, ); - }) + }); this.cronLimit.on('active', () => { Logger.info( - `[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`, - ); - }) - this.cronLimit.on('completed', (param) => { - Logger.info( - `[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`, + `[schedule][开始处理任务] 运行中任务数: ${ + this.cronLimitActiveCount + 1 + }, 等待中任务数: ${this.cronLimitPendingCount}`, ); }); - this.cronLimit.on('error', error => { - Logger.error( - `[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`, - ); + this.cronLimit.on('completed', (param) => { + Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`); + }); + this.cronLimit.on('error', (error) => { + Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`); }); this.cronLimit.on('next', () => { Logger.info( @@ -48,12 +58,16 @@ class TaskLimit { ); }); this.cronLimit.on('idle', () => { - Logger.info( - `[schedule][任务队列] 空闲中...`, - ); + Logger.info(`[schedule][任务队列] 空闲中...`); }); } + public removeQueuedDependency(dependency: Dependence) { + if (this.queuedDependencyIds.has(dependency.id!)) { + this.queuedDependencyIds.delete(dependency.id!); + } + } + public async setCustomLimit(limit?: number) { if (limit) { this.cronLimit.concurrency = limit; @@ -68,15 +82,27 @@ class TaskLimit { } } - public async runWithCronLimit(fn: () => Promise, options?: Partial): Promise { + public async runWithCronLimit( + fn: () => Promise, + options?: Partial, + ): Promise { return this.cronLimit.add(fn, options); } - public runOneByOne(fn: () => Promise, options?: Partial): Promise { - return this.oneLimit.add(fn, options); + public runDependeny( + dependency: Dependence, + fn: IDependencyFn, + options?: Partial, + ): Promise { + this.queuedDependencyIds.add(dependency.id!); + fn.dependency = dependency; + return this.dependenyLimit.add(fn, options); } - public updateDepLog(fn: () => Promise, options?: Partial): Promise { + public updateDepLog( + fn: () => Promise, + options?: Partial, + ): Promise { return this.updateLogLimit.add(fn, options); } } diff --git a/src/components/iconfont.tsx b/src/components/iconfont.tsx index 03f9afb1..72411736 100644 --- a/src/components/iconfont.tsx +++ b/src/components/iconfont.tsx @@ -1,7 +1,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; const IconFont = createFromIconfontCN({ - scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'], + scriptUrl: ['//at.alicdn.com/t/c/font_3354854_lc939gab1iq.js'], }); export default IconFont; diff --git a/src/locales/en-US.json b/src/locales/en-US.json index bd833e72..a13d3b1f 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -106,6 +106,7 @@ "创建时间": "Creation Time", "确认删除依赖": "Confirm to delete the dependency", "确认重新安装": "Confirm to reinstall", + "确认取消安装": "Confirm to cancel install", "确认删除选中的依赖吗": "Confirm to delete the selected dependencies?", "确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?", "请输入名称": "Please enter a name", @@ -394,6 +395,7 @@ "系统": "System", "个人": "Personal", "重新安装": "Reinstall", + "取消安装": "Cancel Install", "强制删除": "Force Delete", "全部任务": "All Tasks", "关联订阅": "Associate Subscription", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 90ff37af..facf44e0 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -106,6 +106,7 @@ "创建时间": "创建时间", "确认删除依赖": "确认删除依赖", "确认重新安装": "确认重新安装", + "确认取消安装": "确认取消安装", "确认删除选中的依赖吗": "确认删除选中的依赖吗", "确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗", "请输入名称": "请输入名称", @@ -394,6 +395,7 @@ "系统": "系统", "个人": "个人", "重新安装": "重新安装", + "取消安装": "取消安装", "强制删除": "强制删除", "全部任务": "全部任务", "关联订阅": "关联订阅", diff --git a/src/pages/dependence/index.tsx b/src/pages/dependence/index.tsx index 02f3d527..9824b560 100644 --- a/src/pages/dependence/index.tsx +++ b/src/pages/dependence/index.tsx @@ -36,6 +36,8 @@ import { SharedContext } from '@/layouts'; import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import dayjs from 'dayjs'; import WebSocketManager from '@/utils/websocket'; +import { DependenceStatus } from './type'; +import IconFont from '@/components/iconfont'; const { Text } = Typography; const { Search } = Input; @@ -108,6 +110,36 @@ const Dependence = () => { key: 'status', width: 120, dataIndex: 'status', + filters: [ + { + text: intl.get('队列中'), + value: DependenceStatus.queued, + }, + { + text: intl.get('安装中'), + value: DependenceStatus.installing, + }, + { + text: intl.get('已安装'), + value: DependenceStatus.installed, + }, + { + text: intl.get('安装失败'), + value: DependenceStatus.installFailed, + }, + { + text: intl.get('删除中'), + value: DependenceStatus.removing, + }, + { + text: intl.get('已删除'), + value: DependenceStatus.removed, + }, + { + text: intl.get('删除失败'), + value: DependenceStatus.removeFailed, + }, + ], render: (text: string, record: any, index: number) => { return ( @@ -163,26 +195,32 @@ const Dependence = () => { - {record.status !== Status.安装中 && - record.status !== Status.删除中 && ( - <> - - reInstallDependence(record, index)}> - - - - - deleteDependence(record, index)}> - - - - - deleteDependence(record, index, true)}> - - - - - )} + {[Status.队列中, Status.安装中].includes(record.status) && ( + + cancelDependence(record)}> + + + + )} + {![Status.安装中, Status.删除中].includes(record.status) && ( + <> + + reInstallDependence(record, index)}> + + + + + deleteDependence(record, index)}> + + + + + deleteDependence(record, index, true)}> + + + + + )} ); }, @@ -200,11 +238,15 @@ const Dependence = () => { const tableRef = useRef(null); const tableScrollHeight = useTableScrollHeight(tableRef, 59); - const getDependencies = () => { + const getDependencies = (status?: number[]) => { setLoading(true); request .get( - `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, + `${ + config.apiPrefix + }dependencies?searchValue=${searchText}&type=${type}&status=${ + status || '' + }`, ) .then(({ code, data }) => { if (code === 200) { @@ -289,6 +331,31 @@ const Dependence = () => { }); }; + const cancelDependence = (record: any) => { + Modal.confirm({ + title: intl.get('确认取消安装'), + content: ( + <> + {intl.get('确认取消安装')}{' '} + + {record.name} + {' '} + {intl.get('吗')} + + ), + onOk() { + request + .put(`${config.apiPrefix}dependencies/cancel`, [record.id]) + .then(() => { + getDependencies(); + }); + }, + onCancel() { + console.log('Cancel'); + }, + }); + }; + const handleCancel = (dependence?: any[]) => { setIsModalVisible(false); dependence && handleDependence(dependence); @@ -538,6 +605,9 @@ const Dependence = () => { size="middle" scroll={{ x: 768, y: tableScrollHeight }} loading={loading} + onChange={(pagination, filters) => { + getDependencies(filters?.status as number[]); + }} /> diff --git a/src/pages/dependence/type.ts b/src/pages/dependence/type.ts new file mode 100644 index 00000000..e9913986 --- /dev/null +++ b/src/pages/dependence/type.ts @@ -0,0 +1,9 @@ +export enum DependenceStatus { + 'installing', + 'installed', + 'installFailed', + 'removing', + 'removed', + 'removeFailed', + 'queued', +}