mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-28 09:02:12 +08:00
fix: isolate invalid cron rules during scheduler recovery
This commit is contained in:
@@ -1,30 +1,15 @@
|
||||
import { ServerUnaryCall, sendUnaryData, status } from '@grpc/grpc-js';
|
||||
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import CronExpressionParser from 'cron-parser';
|
||||
import { isValidCronSchedule } from '../shared/cronSchedule';
|
||||
import { scheduleStacks } from './data';
|
||||
import { runCron } from '../shared/runCron';
|
||||
import Logger from '../loaders/logger';
|
||||
import { tf } from '../shared/i18n';
|
||||
|
||||
/**
|
||||
* 预校验 cron 表达式,检测 node-schedule 会拒绝但 cron-parser 会接受的 pattern。
|
||||
* node-schedule 对 bare /N(字段以 / 开头,如前无星号/数字前缀的 /6)返回 null,
|
||||
* 提前拦截避免走 scheduleJob 后才发现无效。
|
||||
*/
|
||||
// Validate the entire batch before replacing any existing jobs.
|
||||
const isValidCronField = (cron: string): boolean => {
|
||||
// 检测 bare /N 模式:字段以 / 开头如 "/6",或空格后紧跟 "/6"
|
||||
// node-schedule 会对这种字段返回 null
|
||||
if (/\s\/\d/.test(cron) || /^\/\d/.test(cron)) {
|
||||
return false;
|
||||
}
|
||||
// 日期和星期字段的 ? 是合法通配符。解析完整表达式,避免将单独的
|
||||
// ? 等无效规则放行后,在替换恢复快照时清除已有任务。
|
||||
try {
|
||||
return CronExpressionParser.parse(cron).hasNext();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return isValidCronSchedule(cron);
|
||||
};
|
||||
|
||||
const addCron = (
|
||||
|
||||
+22
-4
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getInvalidCronSchedules } from '../shared/cronSchedule';
|
||||
import {
|
||||
withSchedulerMutation,
|
||||
schedulerRegistrationError,
|
||||
@@ -46,10 +47,13 @@ export default class CronService {
|
||||
|
||||
private isNodeCron(cron: Crontab) {
|
||||
const { schedule, extra_schedules } = cron;
|
||||
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// System crontab only receives portable numeric five-field expressions.
|
||||
// Extended syntax, macros and legacy shorthand belong to node-schedule.
|
||||
return (
|
||||
schedule?.trim().split(/\s+/).length !== 5 ||
|
||||
/[^\d\s*,/\-]/.test(schedule || '') ||
|
||||
Boolean(extra_schedules?.length)
|
||||
);
|
||||
}
|
||||
|
||||
private get schedulerMode(): 'system' | 'node' {
|
||||
@@ -1075,6 +1079,20 @@ export default class CronService {
|
||||
public async autosave_crontab(requireScheduler = false) {
|
||||
return withSchedulerMutation(async () => {
|
||||
const tabs = await this.crontabs();
|
||||
// A bad persisted rule must not block startup or all other schedules.
|
||||
// Keep the DB row editable; omit it from both runtime scheduler snapshots.
|
||||
tabs.data = tabs.data.filter((doc) => {
|
||||
if (doc.isDisabled === 1) return true;
|
||||
const invalidSchedules = getInvalidCronSchedules(doc);
|
||||
if (invalidSchedules.length === 0) return true;
|
||||
this.logger.warn(
|
||||
'[crontab] Skipping task with invalid schedule (id=%s, name=%s): %s',
|
||||
doc.id,
|
||||
doc.name || '',
|
||||
JSON.stringify(invalidSchedules),
|
||||
);
|
||||
return false;
|
||||
});
|
||||
const regularCrons = tabs.data
|
||||
.filter(
|
||||
(x) =>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import cronParser from 'cron-parser-v4';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
|
||||
// Keep validation aligned with node-schedule's locked cron-parser version.
|
||||
// cron-parser 5 accepts syntax (e.g. H) that the scheduler cannot execute.
|
||||
export function isValidCronSchedule(schedule: unknown): schedule is string {
|
||||
if (typeof schedule !== 'string' || !schedule.trim()) return false;
|
||||
try {
|
||||
return cronParser.parseExpression(schedule).hasNext();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getInvalidCronSchedules(cron: {
|
||||
schedule?: string;
|
||||
extra_schedules?: Array<{ schedule: string }>;
|
||||
}): unknown[] {
|
||||
if (
|
||||
cron.schedule?.startsWith(ScheduleType.ONCE) ||
|
||||
cron.schedule?.startsWith(ScheduleType.BOOT)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [cron.schedule, ...(cron.extra_schedules || []).map((x) => x.schedule)]
|
||||
.filter((schedule) => !isValidCronSchedule(schedule));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Joi } from 'celebrate';
|
||||
import CronExpressionParser from 'cron-parser';
|
||||
import { isValidCronSchedule } from '../shared/cronSchedule';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
import path from 'path';
|
||||
import config from '../config';
|
||||
@@ -12,18 +12,7 @@ const validateSchedule = (value: string, helpers: any) => {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 检测裸 /N 模式:cron-parser 会接受,但 node-schedule 会返回 null
|
||||
// 提前拦截,避免任务入库后调度器注册失败
|
||||
if (/\s\/\d/.test(value) || /^\/\d/.test(value)) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
try {
|
||||
if (CronExpressionParser.parse(value).hasNext()) {
|
||||
return value;
|
||||
}
|
||||
} catch (e) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
if (isValidCronSchedule(value)) return value;
|
||||
return helpers.error('any.invalid');
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user