Skip to content

Commit

Permalink
定时任务支持 @once@boot 任务
Browse files Browse the repository at this point in the history
  • Loading branch information
whyour committed Feb 19, 2025
1 parent 4969181 commit 8173075
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 119 deletions.
48 changes: 11 additions & 37 deletions back/api/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Logger } from 'winston';
import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
import { commonCronSchema } from '../validation/schedule';

const route = Router();

export default (app: Router) => {
Expand Down Expand Up @@ -170,27 +171,14 @@ export default (app: Router) => {
route.post(
'/',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional(),
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
}),
body: Joi.object(commonCronSchema),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
Expand Down Expand Up @@ -331,30 +319,16 @@ export default (app: Router) => {
'/',
celebrate({
body: Joi.object({
labels: Joi.array().optional().allow(null),
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
...commonCronSchema,
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
Expand Down Expand Up @@ -418,7 +392,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.import_crontab();
const data = await cronService.importCrontab();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
Expand Down
1 change: 1 addition & 0 deletions back/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async function startServer() {
Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
require('./loaders/bootAfter').default();
})
.on('error', (err) => {
Logger.error(err);
Expand Down
8 changes: 8 additions & 0 deletions back/loaders/bootAfter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Container from 'typedi';
import CronService from '../services/cron';

export default async () => {
const cronService = Container.get(CronService);

await cronService.bootTask();
};
72 changes: 56 additions & 16 deletions back/services/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,24 @@ export default class CronService {
return false;
}

private isOnceSchedule(schedule?: string) {
return schedule?.startsWith('@once');
}

private isBootSchedule(schedule?: string) {
return schedule?.startsWith('@boot');
}

private isSpecialSchedule(schedule?: string) {
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
}

public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
if (this.isNodeCron(doc)) {

if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
await cronClient.addCron([
{
name: doc.name || '',
Expand All @@ -50,7 +63,8 @@ export default class CronService {
},
]);
}
await this.set_crontab();

await this.setCrontab();
return doc;
}

Expand All @@ -63,13 +77,16 @@ export default class CronService {
const tab = new Crontab({ ...doc, ...payload });
tab.saved = false;
const newDoc = await this.updateDb(tab);

if (doc.isDisabled === 1) {
return newDoc;
}

if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]);
}
if (this.isNodeCron(newDoc)) {

if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
await cronClient.addCron([
{
name: doc.name || '',
Expand All @@ -80,7 +97,8 @@ export default class CronService {
},
]);
}
await this.set_crontab();

await this.setCrontab();
return newDoc;
}

Expand Down Expand Up @@ -135,7 +153,7 @@ export default class CronService {
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
await this.setCrontab();
}

public async pin(ids: number[]) {
Expand Down Expand Up @@ -381,7 +399,7 @@ export default class CronService {
try {
const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count({ where: query });
return { data: result, total: count };
return { data: result.map((x) => x.get({ plain: true })), total: count };
} catch (error) {
throw error;
}
Expand Down Expand Up @@ -502,7 +520,7 @@ export default class CronService {
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
await this.setCrontab();
}

public async enabled(ids: number[]) {
Expand All @@ -518,7 +536,7 @@ export default class CronService {
extra_schedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
await this.setCrontab();
}

public async log(id: number) {
Expand Down Expand Up @@ -586,15 +604,16 @@ export default class CronService {
return crontab_job_string;
}

private async set_crontab(data?: { data: Crontab[]; total: number }) {
private async setCrontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs());
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (
tab.isDisabled === 1 ||
_schedule!.length !== 5 ||
tab.extra_schedules?.length
tab.extra_schedules?.length ||
this.isSpecialSchedule(tab.schedule)
) {
crontab_string += '# ';
crontab_string += tab.schedule;
Expand All @@ -615,7 +634,7 @@ export default class CronService {
await CrontabModel.update({ saved: true }, { where: {} });
}

public import_crontab() {
public importCrontab() {
exec('crontab -l', (error, stdout, stderr) => {
const lines = stdout.split('\n');
const namePrefix = new Date().getTime();
Expand Down Expand Up @@ -651,17 +670,38 @@ export default class CronService {

public async autosave_crontab() {
const tabs = await this.crontabs();
this.set_crontab(tabs);

const sixCron = tabs.data
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
this.setCrontab(tabs);

const regularCrons = tabs.data
.filter(
(x) =>
this.isNodeCron(x) &&
x.isDisabled !== 1 &&
!this.isSpecialSchedule(x.schedule),
)
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extra_schedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
await cronClient.addCron(regularCrons);
}

public async bootTask() {
const tabs = await this.crontabs();
const bootTasks = tabs.data.filter(
(x) => !x.isDisabled && this.isBootSchedule(x.schedule),
);
if (bootTasks.length > 0) {
await CrontabModel.update(
{ status: CrontabStatus.queued },
{ where: { id: bootTasks.map((t) => t.id!) } },
);
for (const task of bootTasks) {
await this.runSingle(task.id!);
}
}
}
}
36 changes: 36 additions & 0 deletions back/validation/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Joi } from 'celebrate';
import cron_parser from 'cron-parser';

const validateSchedule = (value: string, helpers: any) => {
if (value.startsWith('@once') || value.startsWith('@boot')) {
return value;
}

try {
if (cron_parser.parseExpression(value).hasNext()) {
return value;
}
} catch (e) {
return helpers.error('any.invalid');
}
return helpers.error('any.invalid');
};

export const scheduleSchema = Joi.string()
.required()
.custom(validateSchedule)
.messages({
'any.invalid': '无效的定时规则',
'string.empty': '定时规则不能为空',
});

export const commonCronSchema = {
name: Joi.string().optional(),
command: Joi.string().required(),
schedule: scheduleSchema,
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
};
2 changes: 1 addition & 1 deletion shell/share.sh
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ handle_task_end() {
[[ "$diff_time" == 0 ]] && diff_time=1

if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time")
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})"
fi
Expand Down
2 changes: 1 addition & 1 deletion shell/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ main() {
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
[[ $ID ]] && update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time"

if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
Expand Down
5 changes: 4 additions & 1 deletion src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,5 +496,8 @@
"NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux Mirror Source",
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute",
"常规定时": "Normal Timing",
"手动运行": "Manual Run",
"开机运行": "Boot Run"
}
6 changes: 5 additions & 1 deletion src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,5 +496,9 @@
"NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行",
"常规定时": "常规定时",
"手动运行": "手动运行",
"开机运行": "开机运行"
}

10 changes: 8 additions & 2 deletions src/pages/crontab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ const Crontab = () => {
},
},
render: (text, record) => {
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
return record.nextRunTime
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
: '-';
},
},
{
Expand Down Expand Up @@ -396,9 +398,13 @@ const Crontab = () => {

setValue(
data.map((x) => {
const specialSchedules = ['@once', '@boot'];
const nextRunTime = specialSchedules.includes(x.schedule)
? null
: getCrontabsNextDate(x.schedule, x.extra_schedules);
return {
...x,
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
nextRunTime,
subscription: subscriptionMap?.[x.sub_id],
};
}),
Expand Down
Loading

0 comments on commit 8173075

Please sign in to comment.