依赖管理支持取消安装和状态筛选

This commit is contained in:
whyour 2024-02-13 22:42:22 +08:00
parent 892d91edc4
commit 14cb1f7788
9 changed files with 209 additions and 51 deletions

View File

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

View File

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

View File

@ -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<Dependence[]> {
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<Dependence[]> {
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

View File

@ -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<T> {
(): Promise<T>;
dependency?: Dependence;
}
class TaskLimit {
private oneLimit = new PQueue({ concurrency: 1 });
private dependenyLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]);
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<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
public async runWithCronLimit<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.cronLimit.add(fn, options);
}
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.oneLimit.add(fn, options);
public runDependeny<T>(
dependency: Dependence,
fn: IDependencyFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
this.queuedDependencyIds.add(dependency.id!);
fn.dependency = dependency;
return this.dependenyLimit.add(fn, options);
}
public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
public updateDepLog<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.updateLogLimit.add(fn, options);
}
}

View File

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

View File

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

View File

@ -106,6 +106,7 @@
"创建时间": "创建时间",
"确认删除依赖": "确认删除依赖",
"确认重新安装": "确认重新安装",
"确认取消安装": "确认取消安装",
"确认删除选中的依赖吗": "确认删除选中的依赖吗",
"确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗",
"请输入名称": "请输入名称",
@ -394,6 +395,7 @@
"系统": "系统",
"个人": "个人",
"重新安装": "重新安装",
"取消安装": "取消安装",
"强制删除": "强制删除",
"全部任务": "全部任务",
"关联订阅": "关联订阅",

View File

@ -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 (
<Space size="middle" style={{ cursor: 'text' }}>
@ -163,26 +195,32 @@ const Dependence = () => {
<FileTextOutlined />
</a>
</Tooltip>
{record.status !== Status. &&
record.status !== Status. && (
<>
<Tooltip title={isPc ? intl.get('重新安装') : ''}>
<a onClick={() => reInstallDependence(record, index)}>
<BugOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('删除') : ''}>
<a onClick={() => deleteDependence(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('强制删除') : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</>
)}
{[Status., Status.].includes(record.status) && (
<Tooltip title={isPc ? intl.get('取消安装') : ''}>
<a onClick={() => cancelDependence(record)}>
<IconFont type="ql-icon-quxiaoanzhuang" />
</a>
</Tooltip>
)}
{![Status., Status.].includes(record.status) && (
<>
<Tooltip title={isPc ? intl.get('重新安装') : ''}>
<a onClick={() => reInstallDependence(record, index)}>
<BugOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('删除') : ''}>
<a onClick={() => deleteDependence(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('强制删除') : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</>
)}
</Space>
);
},
@ -200,11 +238,15 @@ const Dependence = () => {
const tableRef = useRef<HTMLDivElement>(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('确认取消安装')}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
{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[]);
}}
/>
</DndProvider>
</div>

View File

@ -0,0 +1,9 @@
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
'queued',
}