mirror of
https://github.com/whyour/qinglong.git
synced 2025-08-23 19:56:07 +08:00
依赖管理支持取消安装和状态筛选
This commit is contained in:
parent
892d91edc4
commit
14cb1f7788
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -435,8 +435,8 @@ export async function killTask(pid: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPid(name: string) {
|
export async function getPid(cmd: string) {
|
||||||
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
||||||
const pid = await promiseExec(taskCommand);
|
const pid = await promiseExec(taskCommand);
|
||||||
return pid ? Number(pid) : undefined;
|
return pid ? Number(pid) : undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@ import {
|
||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { FindOptions, Op } from 'sequelize';
|
import { FindOptions, Op } from 'sequelize';
|
||||||
import { fileExist, promiseExecSuccess } from '../config/util';
|
import {
|
||||||
|
fileExist,
|
||||||
|
getPid,
|
||||||
|
killTask,
|
||||||
|
promiseExecSuccess,
|
||||||
|
} from '../config/util';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import taskLimit from '../shared/pLimit';
|
import taskLimit from '../shared/pLimit';
|
||||||
|
|
||||||
|
@ -86,11 +91,21 @@ export default class DependenceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dependencies(
|
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 = {},
|
query: any = {},
|
||||||
): Promise<Dependence[]> {
|
): 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) {
|
if (searchValue) {
|
||||||
const encodeText = encodeURI(searchValue);
|
const encodeText = encodeURI(searchValue);
|
||||||
const reg = {
|
const reg = {
|
||||||
|
@ -106,7 +121,7 @@ export default class DependenceService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition);
|
const result = await this.find(condition, sort);
|
||||||
return result as any;
|
return result as any;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -134,6 +149,18 @@ export default class DependenceService {
|
||||||
return docs;
|
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[]> {
|
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
||||||
const docs = await DependenceModel.findAll({
|
const docs = await DependenceModel.findAll({
|
||||||
where: { ...query },
|
where: { ...query },
|
||||||
|
@ -168,8 +195,14 @@ export default class DependenceService {
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
return taskLimit.runOneByOne(() => {
|
return taskLimit.runDependeny(dependency, () => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
|
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
taskLimit.removeQueuedDependency(dependency);
|
||||||
|
|
||||||
const depIds = [dependency.id!];
|
const depIds = [dependency.id!];
|
||||||
const status = isInstall
|
const status = isInstall
|
||||||
? DependenceStatus.installing
|
? DependenceStatus.installing
|
||||||
|
|
|
@ -2,11 +2,19 @@ import PQueue, { QueueAddOptions } from 'p-queue-cjs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { AuthDataType, SystemModel } from '../data/system';
|
import { AuthDataType, SystemModel } from '../data/system';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
|
import { Dependence } from '../data/dependence';
|
||||||
|
|
||||||
|
interface IDependencyFn<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
dependency?: Dependence;
|
||||||
|
}
|
||||||
class TaskLimit {
|
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 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() {
|
get cronLimitActiveCount() {
|
||||||
return this.cronLimit.pending;
|
return this.cronLimit.pending;
|
||||||
|
@ -16,6 +24,10 @@ class TaskLimit {
|
||||||
return this.cronLimit.size;
|
return this.cronLimit.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get firstDependencyId() {
|
||||||
|
return [...this.queuedDependencyIds.values()][0];
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setCustomLimit();
|
this.setCustomLimit();
|
||||||
this.handleEvents();
|
this.handleEvents();
|
||||||
|
@ -26,21 +38,19 @@ class TaskLimit {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
this.cronLimit.on('active', () => {
|
this.cronLimit.on('active', () => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
`[schedule][开始处理任务] 运行中任务数: ${
|
||||||
);
|
this.cronLimitActiveCount + 1
|
||||||
})
|
}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
this.cronLimit.on('completed', (param) => {
|
|
||||||
Logger.info(
|
|
||||||
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.cronLimit.on('error', error => {
|
this.cronLimit.on('completed', (param) => {
|
||||||
Logger.error(
|
Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`);
|
||||||
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
|
});
|
||||||
);
|
this.cronLimit.on('error', (error) => {
|
||||||
|
Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);
|
||||||
});
|
});
|
||||||
this.cronLimit.on('next', () => {
|
this.cronLimit.on('next', () => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
|
@ -48,12 +58,16 @@ class TaskLimit {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.cronLimit.on('idle', () => {
|
this.cronLimit.on('idle', () => {
|
||||||
Logger.info(
|
Logger.info(`[schedule][任务队列] 空闲中...`);
|
||||||
`[schedule][任务队列] 空闲中...`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeQueuedDependency(dependency: Dependence) {
|
||||||
|
if (this.queuedDependencyIds.has(dependency.id!)) {
|
||||||
|
this.queuedDependencyIds.delete(dependency.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async setCustomLimit(limit?: number) {
|
public async setCustomLimit(limit?: number) {
|
||||||
if (limit) {
|
if (limit) {
|
||||||
this.cronLimit.concurrency = 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);
|
return this.cronLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
public runDependeny<T>(
|
||||||
return this.oneLimit.add(fn, options);
|
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);
|
return this.updateLogLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createFromIconfontCN } from '@ant-design/icons';
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
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;
|
export default IconFont;
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"创建时间": "Creation Time",
|
"创建时间": "Creation Time",
|
||||||
"确认删除依赖": "Confirm to delete the dependency",
|
"确认删除依赖": "Confirm to delete the dependency",
|
||||||
"确认重新安装": "Confirm to reinstall",
|
"确认重新安装": "Confirm to reinstall",
|
||||||
|
"确认取消安装": "Confirm to cancel install",
|
||||||
"确认删除选中的依赖吗": "Confirm to delete the selected dependencies?",
|
"确认删除选中的依赖吗": "Confirm to delete the selected dependencies?",
|
||||||
"确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?",
|
"确认重新安装选中的依赖吗": "Confirm to reinstall the selected dependencies?",
|
||||||
"请输入名称": "Please enter a name",
|
"请输入名称": "Please enter a name",
|
||||||
|
@ -394,6 +395,7 @@
|
||||||
"系统": "System",
|
"系统": "System",
|
||||||
"个人": "Personal",
|
"个人": "Personal",
|
||||||
"重新安装": "Reinstall",
|
"重新安装": "Reinstall",
|
||||||
|
"取消安装": "Cancel Install",
|
||||||
"强制删除": "Force Delete",
|
"强制删除": "Force Delete",
|
||||||
"全部任务": "All Tasks",
|
"全部任务": "All Tasks",
|
||||||
"关联订阅": "Associate Subscription",
|
"关联订阅": "Associate Subscription",
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"创建时间": "创建时间",
|
"创建时间": "创建时间",
|
||||||
"确认删除依赖": "确认删除依赖",
|
"确认删除依赖": "确认删除依赖",
|
||||||
"确认重新安装": "确认重新安装",
|
"确认重新安装": "确认重新安装",
|
||||||
|
"确认取消安装": "确认取消安装",
|
||||||
"确认删除选中的依赖吗": "确认删除选中的依赖吗",
|
"确认删除选中的依赖吗": "确认删除选中的依赖吗",
|
||||||
"确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗",
|
"确认重新安装选中的依赖吗": "确认重新安装选中的依赖吗",
|
||||||
"请输入名称": "请输入名称",
|
"请输入名称": "请输入名称",
|
||||||
|
@ -394,6 +395,7 @@
|
||||||
"系统": "系统",
|
"系统": "系统",
|
||||||
"个人": "个人",
|
"个人": "个人",
|
||||||
"重新安装": "重新安装",
|
"重新安装": "重新安装",
|
||||||
|
"取消安装": "取消安装",
|
||||||
"强制删除": "强制删除",
|
"强制删除": "强制删除",
|
||||||
"全部任务": "全部任务",
|
"全部任务": "全部任务",
|
||||||
"关联订阅": "关联订阅",
|
"关联订阅": "关联订阅",
|
||||||
|
|
|
@ -36,6 +36,8 @@ 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';
|
import WebSocketManager from '@/utils/websocket';
|
||||||
|
import { DependenceStatus } from './type';
|
||||||
|
import IconFont from '@/components/iconfont';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
@ -108,6 +110,36 @@ const Dependence = () => {
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: 120,
|
width: 120,
|
||||||
dataIndex: 'status',
|
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) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Space size="middle" style={{ cursor: 'text' }}>
|
<Space size="middle" style={{ cursor: 'text' }}>
|
||||||
|
@ -163,8 +195,14 @@ const Dependence = () => {
|
||||||
<FileTextOutlined />
|
<FileTextOutlined />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{record.status !== Status.安装中 &&
|
{[Status.队列中, Status.安装中].includes(record.status) && (
|
||||||
record.status !== 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('重新安装') : ''}>
|
<Tooltip title={isPc ? intl.get('重新安装') : ''}>
|
||||||
<a onClick={() => reInstallDependence(record, index)}>
|
<a onClick={() => reInstallDependence(record, index)}>
|
||||||
|
@ -200,11 +238,15 @@ const Dependence = () => {
|
||||||
const tableRef = useRef<HTMLDivElement>(null);
|
const tableRef = useRef<HTMLDivElement>(null);
|
||||||
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
|
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
|
||||||
|
|
||||||
const getDependencies = () => {
|
const getDependencies = (status?: number[]) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(
|
.get(
|
||||||
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
|
`${
|
||||||
|
config.apiPrefix
|
||||||
|
}dependencies?searchValue=${searchText}&type=${type}&status=${
|
||||||
|
status || ''
|
||||||
|
}`,
|
||||||
)
|
)
|
||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200) {
|
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[]) => {
|
const handleCancel = (dependence?: any[]) => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
dependence && handleDependence(dependence);
|
dependence && handleDependence(dependence);
|
||||||
|
@ -538,6 +605,9 @@ const Dependence = () => {
|
||||||
size="middle"
|
size="middle"
|
||||||
scroll={{ x: 768, y: tableScrollHeight }}
|
scroll={{ x: 768, y: tableScrollHeight }}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
onChange={(pagination, filters) => {
|
||||||
|
getDependencies(filters?.status as number[]);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
9
src/pages/dependence/type.ts
Normal file
9
src/pages/dependence/type.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export enum DependenceStatus {
|
||||||
|
'installing',
|
||||||
|
'installed',
|
||||||
|
'installFailed',
|
||||||
|
'removing',
|
||||||
|
'removed',
|
||||||
|
'removeFailed',
|
||||||
|
'queued',
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user