mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-12 14:05:38 +08:00
Add multi-instance support and fix stop to kill all running instances
- Add allow_multiple_instances field to Crontab model (default: 0 for single instance) - Add validation for new field in commonCronSchema - Add getAllPids and killAllTasks utility functions - Update stop method to kill ALL running instances of a task - Update runCron to respect allow_multiple_instances config - Backward compatible: defaults to single instance mode Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
02fa5b1703
commit
e6719a3490
|
|
@ -417,6 +417,27 @@ export async function getPid(cmd: string) {
|
|||
return pid ? Number(pid) : undefined;
|
||||
}
|
||||
|
||||
export async function getAllPids(cmd: string): Promise<number[]> {
|
||||
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}'`;
|
||||
const pidsStr = await promiseExec(taskCommand);
|
||||
if (!pidsStr) return [];
|
||||
return pidsStr
|
||||
.split('\n')
|
||||
.map((p) => Number(p.trim()))
|
||||
.filter((p) => !isNaN(p) && p > 0);
|
||||
}
|
||||
|
||||
export async function killAllTasks(cmd: string): Promise<void> {
|
||||
const pids = await getAllPids(cmd);
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
await killTask(pid);
|
||||
} catch (error) {
|
||||
// Ignore errors if process already terminated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IVersion {
|
||||
version: string;
|
||||
changeLogLink: string;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class Crontab {
|
|||
task_before?: string;
|
||||
task_after?: string;
|
||||
log_name?: string;
|
||||
allow_multiple_instances?: 1 | 0;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
|
|
@ -47,6 +48,7 @@ export class Crontab {
|
|||
this.task_before = options.task_before;
|
||||
this.task_after = options.task_after;
|
||||
this.log_name = options.log_name;
|
||||
this.allow_multiple_instances = options.allow_multiple_instances || 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,4 +89,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|||
task_before: DataTypes.STRING,
|
||||
task_after: DataTypes.STRING,
|
||||
log_name: DataTypes.STRING,
|
||||
allow_multiple_instances: DataTypes.NUMBER,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
getFileContentByName,
|
||||
fileExist,
|
||||
killTask,
|
||||
killAllTasks,
|
||||
getUniqPath,
|
||||
safeJSONParse,
|
||||
isDemoEnv,
|
||||
|
|
@ -57,7 +58,9 @@ export default class CronService {
|
|||
}
|
||||
let uniqPath = await getUniqPath(command, `${id}`);
|
||||
if (log_name) {
|
||||
const normalizedLogName = log_name.startsWith('/') ? log_name : path.join(config.logPath, log_name);
|
||||
const normalizedLogName = log_name.startsWith('/')
|
||||
? log_name
|
||||
: path.join(config.logPath, log_name);
|
||||
if (normalizedLogName.startsWith(config.logPath)) {
|
||||
uniqPath = log_name;
|
||||
}
|
||||
|
|
@ -462,12 +465,17 @@ export default class CronService {
|
|||
public async stop(ids: number[]) {
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
// Kill all running instances of this task
|
||||
try {
|
||||
await killTask(doc.pid);
|
||||
const command = this.makeCommand(doc);
|
||||
await killAllTasks(command);
|
||||
this.logger.info(
|
||||
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
this.logger.error(
|
||||
`[panel][停止任务失败] 任务ID: ${doc.id}, 错误: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -498,7 +506,10 @@ export default class CronService {
|
|||
|
||||
let { id, command, log_name } = cron;
|
||||
|
||||
const uniqPath = log_name === '/dev/null' ? (await getUniqPath(command, `${id}`)) : log_name;
|
||||
const uniqPath =
|
||||
log_name === '/dev/null'
|
||||
? await getUniqPath(command, `${id}`)
|
||||
: log_name;
|
||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||
await fs.mkdir(logDirPath, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@ import { killTask } from '../config/util';
|
|||
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(cron, () => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
// Check if the cron is already running and stop it
|
||||
// Check if the cron is already running and stop it (only if multiple instances are not allowed)
|
||||
try {
|
||||
const existingCron = await CrontabModel.findOne({
|
||||
where: { id: Number(cron.id) },
|
||||
});
|
||||
|
||||
// Default to single instance mode (0) for backward compatibility
|
||||
const allowMultipleInstances =
|
||||
existingCron?.allow_multiple_instances === 1;
|
||||
|
||||
if (
|
||||
!allowMultipleInstances &&
|
||||
existingCron &&
|
||||
existingCron.pid &&
|
||||
(existingCron.status === CrontabStatus.running ||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ export const commonCronSchema = {
|
|||
return value;
|
||||
}
|
||||
|
||||
if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) {
|
||||
if (
|
||||
!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(
|
||||
value,
|
||||
)
|
||||
) {
|
||||
return helpers.error('string.pattern.base');
|
||||
}
|
||||
if (value.length > 100) {
|
||||
|
|
@ -77,4 +81,5 @@ export const commonCronSchema = {
|
|||
'string.max': '日志名称不能超过100个字符',
|
||||
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
||||
}),
|
||||
allow_multiple_instances: Joi.number().optional().valid(0, 1),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user