Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions src/app/service/sandbox/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,35 +229,49 @@ export class Runtime {
// 如果有nextruntime,则加入重试队列
this.joinRetryList(script);
this.crontabSripts.push(script);
let flag = false;

const ERROR_MESSAGES: Record<number, string> = {
0: "crontabScript: cron expression failed",
2: "crontabScript: onTick creation failed",
4: "crontabScript: create cronjob failed",
6: "crontabScript: cronjob start failed",
};

const logError = (ok: number, val: string, e: unknown) =>
this.logger.error(
ERROR_MESSAGES[ok] ?? "crontabScript: execution failed",
{ uuid: script.uuid, crontab: val },
Logger.E(e)
);

const cronJobList: Array<CronJob> = [];
script.metadata.crontab.forEach((val) => {
const { cronExpr, oncePos } = extractCronExpr(val);
let ok = 0;
try {
const cron = new CronJob(cronExpr, this.crontabExec(script, oncePos));
const { cronExpr, oncePos } = extractCronExpr(val);
ok = 2;
const onTick = this.crontabExec(script, oncePos);
ok = 4;
const cron = new CronJob(cronExpr, onTick);
ok = 6;
cron.start();
ok = 8;
cronJobList.push(cron);
} catch (e) {
flag = true;
this.logger.error(
"create cronjob failed",
{
uuid: script.uuid,
crontab: val,
},
Logger.E(e)
);
logError(ok, val, e);
}
});
if (cronJobList.length !== script.metadata.crontab.length) {

const allSucceeded = cronJobList.length === script.metadata.crontab.length;
if (allSucceeded) {
this.cronJob.set(script.uuid, cronJobList);
} else {
// 有表达式失败了
for (const crontab of cronJobList) {
crontab.stop();
}
} else {
this.cronJob.set(script.uuid, cronJobList);
}
return !flag;
return allSucceeded;
}

crontabExec(script: ScriptLoadInfo, oncePos: number) {
Expand Down
12 changes: 9 additions & 3 deletions src/pkg/utils/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,18 @@ type NextTimeResult = {
* 对外展示用方法。
*
* - 若为 once cron,返回「下次在 xx 执行一次」的国际化文案
* - 若表达式无效,返回本地化的错误提示文案
* - 否则直接返回下一次执行时间字符串
*/
export const nextTimeDisplay = (crontab: string, date = new Date()): string => {
const res = nextTimeInfo(crontab, date);
const nextTimeFormatted = res.next.toFormat(res.format);
return res.once ? t(`cron_oncetype.${res.once}`, { next: nextTimeFormatted }) : nextTimeFormatted;
try {
const res = nextTimeInfo(crontab, date);
const nextTimeFormatted = res.next.toFormat(res.format);
return res.once ? t(`cron_oncetype.${res.once}`, { next: nextTimeFormatted }) : nextTimeFormatted;
} catch (e) {
console.error(`nextTimeDisplay: Invalid cron expression "${crontab}"`, e);
return t("cron_invalid_expr");
}
};

/**
Expand Down
6 changes: 3 additions & 3 deletions src/pkg/utils/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@App/app/repo/scripts";
import type { Subscribe } from "@App/app/repo/subscribe";
import { SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
import { nextTimeDisplay } from "./cron";
import { extractCronExpr } from "./cron";
import { parseUserConfig } from "./yaml";
import { t as i18n_t } from "@App/locales/locales";
import { readBlobContent } from "@App/pkg/utils/encoding";
Expand Down Expand Up @@ -81,7 +81,7 @@ export function parseScriptFromCode(code: string, origin: string, uuid?: string)
if (metadata.crontab !== undefined) {
type = SCRIPT_TYPE_CRONTAB;
try {
nextTimeDisplay(metadata.crontab[0]);
extractCronExpr(metadata.crontab[0]);
} catch {
throw new Error(i18n_t("error_cron_invalid", { expr: metadata.crontab[0] }));
}
Expand Down Expand Up @@ -136,7 +136,7 @@ export async function prepareScriptByCode(
dao?: ScriptDAO,
options?: {
byEditor?: boolean; // 是否通过编辑器导入
byWebRequest?: boolean; // 是否通过網頁連結安裝或更新
byWebRequest?: boolean; // 是否通过网页连结安装或更新
}
): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> {
dao = dao ?? new ScriptDAO();
Expand Down
17 changes: 17 additions & 0 deletions src/pkg/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ const assertNextTimeInfo = (expr: string, date: Date, expected: any) => {
.toEqual(expected);
};

describe.concurrent("nextTimeDisplay ERROR SAFE", () => {
it.concurrent.each([
["* * * once * once"],
["* * once * once"],
["* once(2,4) once(4-5) * *"],
["* * 1 A *"],
["* once 1.2 * *"],
["* 3 1**2 * *"],
["* 1^2 F * *"],
["1 1 * *"],
["* 3"],
])("错误Cron表达式: %s", (expr) => {
// 确保无效表达式不会抛出异常
expect(() => nextTimeDisplay(expr)).not.toThrow();
});
});

describe.concurrent("nextTimeInfo1", () => {
const date = new Date("2025-12-17T11:47:17.629"); // 2025-12-17 11:47:17.629 (本地时区)

Expand Down
Loading