Skip to content

Commit

Permalink
✨ 后台脚本增加重试逻辑
Browse files Browse the repository at this point in the history
  • Loading branch information
CodFrm committed Jun 14, 2023
1 parent fc2134b commit 16551df
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 20 deletions.
5 changes: 5 additions & 0 deletions eslint/linter-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ const config = {
globalReturn: true,
},
},
globals: {
CATRetryError: "readonly",
CAT_fileStorage: "readonly",
CAT_userConfig: "readonly",
},
rules: {
"constructor-super": ["error"],
"for-direction": ["error"],
Expand Down
18 changes: 18 additions & 0 deletions example/error_retry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// ==UserScript==
// @name 重试示例
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description try to take over the world!
// @author You
// @crontab * * once * *
// @grant GM_notification
// ==/UserScript==

return new Promise((resolve, reject) => {
// Your code here...
GM_notification({
title: "retry",
text: "10秒后重试"
});
reject(new CATRetryError("xxx错误", 10));
});
3 changes: 2 additions & 1 deletion src/app/repo/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ export interface Script {
status: SCRIPT_STATUS; // 脚本状态 1:启用 2:禁用 3:错误 4:初始化
sort: number; // 脚本顺序位置
runStatus: SCRIPT_RUN_STATUS; // 脚本运行状态,后台脚本才会有此状态 running:运行中 complete:完成 error:错误 retry:重试
error?: string; // 运行错误信息
error?: { error: string } | string; // 运行错误信息
createtime: number; // 脚本创建时间戳
updatetime?: number; // 脚本更新时间戳
checktime: number; // 脚本检查更新时间戳
lastruntime?: number; // 脚本最后一次运行时间戳
nextruntime?: number; // 脚本下一次运行时间戳
}

// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源
Expand Down
6 changes: 5 additions & 1 deletion src/runtime/background/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,10 +537,12 @@ export default class Runtime extends Manager {
// 监听沙盒发送的脚本运行状态消息
this.message.setHandler(
"scriptRunStatus",
(action, [scriptId, runStatus]: any) => {
(action, [scriptId, runStatus, error, nextruntime]: any) => {
this.scriptDAO.update(scriptId, {
runStatus,
lastruntime: new Date().getTime(),
nextruntime,
error,
});
Runtime.hook.trigger("runStatus", scriptId, runStatus);
}
Expand Down Expand Up @@ -673,6 +675,8 @@ export default class Runtime extends Manager {
loadBackgroundScript(script: ScriptRunResouce): Promise<boolean> {
this.runBackScript.set(script.id, script);
return new Promise((resolve, reject) => {
// 清除重试数据
script.nextruntime = 0;
this.messageSandbox
?.syncSend("enable", script)
.then(() => {
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/content/exec_script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export default class ExecScript {
constructor(
scriptRes: ScriptRunResouce,
message: MessageManager,
scriptFunc?: ScriptFunc
scriptFunc?: ScriptFunc,
thisContext?: { [key: string]: any }
) {
this.scriptRes = scriptRes;
this.logger = LoggerCore.getInstance().logger({
Expand All @@ -58,15 +59,19 @@ export default class ExecScript {
}
if (scriptRes.grantMap.none) {
// 不注入任何GM api
this.proxyContent = window;
this.proxyContent = global;
} else {
// 构建脚本GM上下文
this.sandboxContent = createContext(
scriptRes,
this.GM_info,
this.proxyMessage
);
this.proxyContent = proxyContext(window, this.sandboxContent);
this.proxyContent = proxyContext(
global,
this.sandboxContent,
thisContext
);
}
}

Expand Down
91 changes: 91 additions & 0 deletions src/runtime/content/exec_warp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-disable func-names */
/* eslint-disable max-classes-per-file */
import { MessageManager } from "@App/app/message/message";
import { ScriptRunResouce } from "@App/app/repo/scripts";
import ExecScript from "./exec_script";

export class CATRetryError {
msg: string;

time: Date;

constructor(msg: string, time: number | Date) {
this.msg = msg;
if (typeof time === "number") {
this.time = new Date(Date.now() + time * 1000);
} else {
this.time = time;
}
}
}

export class BgExecScriptWarp extends ExecScript {
setTimeout: Map<number, boolean>;

setInterval: Map<number, boolean>;

constructor(scriptRes: ScriptRunResouce, message: MessageManager) {
const thisContext: { [key: string]: any } = {};
const setTimeout = new Map<number, any>();
const setInterval = new Map<number, any>();
thisContext.setTimeout = function (
handler: () => void,
timeout: number | undefined,
...args: any
) {
const t = global.setTimeout(
function () {
setTimeout.delete(t);
if (typeof handler === "function") {
handler();
}
},
timeout,
...args
);
setTimeout.set(t, true);
return t;
};
thisContext.clearTimeout = function (t: number | undefined) {
global.clearTimeout(t);
};
thisContext.setInterval = function (
handler: () => void,
timeout: number | undefined,
...args: any
) {
const t = global.setInterval(
function () {
setInterval.delete(t);
if (typeof handler === "function") {
handler();
}
},
timeout,
...args
);
setInterval.set(t, true);
return t;
};
thisContext.clearInterval = function (t: number | undefined) {
global.clearInterval(t);
};
// @ts-ignore
thisContext.CATRetryError = CATRetryError;
super(scriptRes, message, undefined, thisContext);
this.setTimeout = setTimeout;
this.setInterval = setInterval;
}

stop() {
this.setTimeout.forEach((_, t) => {
global.clearTimeout(t);
});
this.setTimeout.clear();
this.setInterval.forEach((_, t) => {
global.clearInterval(t);
});
this.setInterval.clear();
return super.stop();
}
}
79 changes: 69 additions & 10 deletions src/runtime/content/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { CronJob } from "cron";
import IoC from "@App/app/ioc";
import ExecScript from "./exec_script";
import { BgExecScriptWarp, CATRetryError } from "./exec_warp";

type SandboxEvent = "enable" | "disable" | "start" | "stop";

Expand All @@ -28,9 +29,53 @@ export default class SandboxRuntime {

execScripts: Map<number, ExecScript> = new Map();

retryList: {
script: ScriptRunResouce;
retryTime: number;
}[] = [];

constructor(message: MessageSandbox) {
this.message = message;
this.logger = LoggerCore.getInstance().logger({ component: "sandbox" });
// 重试队列,5s检查一次
setInterval(() => {
if (!this.retryList.length) {
return;
}
const now = Date.now();
const retryList = [];
for (let i = 0; i < this.retryList.length; i += 1) {
const item = this.retryList[i];
if (item.retryTime < now) {
this.retryList.splice(i, 1);
i -= 1;
retryList.push(item.script);
}
}
retryList.forEach((script) => {
script.nextruntime = 0;
this.execScript(script);
});
}, 5000);
}

joinRetryList(script: ScriptRunResouce) {
if (script.nextruntime) {
this.retryList.push({
script,
retryTime: script.nextruntime,
});
this.retryList.sort((a, b) => a.retryTime - b.retryTime);
}
}

removeRetryList(scriptId: number) {
for (let i = 0; i < this.retryList.length; i += 1) {
if (this.retryList[i].script.id === scriptId) {
this.retryList.splice(i, 1);
i -= 1;
}
}
}

listenEvent(event: SandboxEvent, handler: Handler) {
Expand All @@ -55,7 +100,7 @@ export default class SandboxRuntime {

// 直接运行脚本
start(script: ScriptRunResouce): Promise<boolean> {
return this.execScript(script);
return this.execScript(script, true);
}

stop(scriptId: number): Promise<boolean> {
Expand Down Expand Up @@ -92,11 +137,9 @@ export default class SandboxRuntime {
// 现期对于正在运行的脚本仅仅是在background中判断是否运行
// 未运行的脚本不处理GMApi的请求
this.stopCronJob(id);
const exec = this.execScripts.get(id);
if (!exec) {
return Promise.resolve(false);
}
return Promise.resolve(true);
// 移除重试队列
this.removeRetryList(id);
return this.stop(id);
}

// 停止计时器
Expand All @@ -111,14 +154,14 @@ export default class SandboxRuntime {
}

// 执行脚本
execScript(script: ScriptRunResouce) {
execScript(script: ScriptRunResouce, execOnce?: boolean) {
const logger = this.logger.with({ scriptId: script.id, name: script.name });
if (this.execScripts.has(script.id)) {
// 释放掉资源
// 暂未实现执行完成后立马释放,会在下一次执行时释放
this.stop(script.id);
}
const exec = new ExecScript(script, this.message);
const exec = new BgExecScriptWarp(script, this.message);
this.execScripts.set(script.id, exec);
this.message.send("scriptRunStatus", [
exec.scriptRes.id,
Expand All @@ -141,11 +184,25 @@ export default class SandboxRuntime {
})
.catch((err) => {
// 发送执行完成+错误消息
logger.error("exec script error", Logger.E(err));
let errMsg;
let nextruntime = 0;
if (err instanceof CATRetryError) {
errMsg = { error: err.msg };
if (!execOnce) {
// 下一次执行时间
nextruntime = err.time.getTime();
script.nextruntime = nextruntime;
this.joinRetryList(script);
}
} else {
errMsg = Logger.E(err);
}
logger.error("exec script error", errMsg);
this.message.send("scriptRunStatus", [
exec.scriptRes.id,
SCRIPT_RUN_STATUS_ERROR,
Logger.E(err),
errMsg,
nextruntime,
]);
// 错误还是抛出,方便排查
throw err;
Expand All @@ -161,6 +218,8 @@ export default class SandboxRuntime {
if (!script.metadata.crontab) {
throw new Error("错误的crontab表达式");
}
// 如果有nextruntime,则加入重试队列
this.joinRetryList(script);
let flag = false;
const cronJobList: Array<CronJob> = [];
script.metadata.crontab.forEach((val) => {
Expand Down
15 changes: 10 additions & 5 deletions src/runtime/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,19 @@ Object.keys(descs).forEach((key) => {
});

// 拦截上下文
export function proxyContext(global: any, context: any) {
export function proxyContext(
global: any,
context: any,
thisContext?: { [key: string]: any }
) {
const special = Object.assign(writables);
// 处理某些特殊的属性
// 后台脚本要不要考虑不能使用eval?
const thisContext: { [key: string]: any } = {
eval: global.eval,
define: undefined,
};
if (!thisContext) {
thisContext = {};
}
thisContext.eval = global.eval;
thisContext.define = undefined;
// keyword是与createContext时同步的,避免访问到context的内部变量
const contextKeyword: { [key: string]: any } = {
message: 1,
Expand Down
21 changes: 21 additions & 0 deletions src/types/scriptcat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ declare function CAT_fileStorage(
): void;
declare function CAT_fileStorage(action: "config"): void;

/**
* 脚本猫定时脚本重试错误, 当你的脚本出现错误时, 可以reject返回此错误, 以便脚本猫重试
* 重试时间请注意不要与脚本执行时间冲突, 否则可能会导致重复执行, 最小重试时间为5s
* @class CATRetryError
*/
declare class CATRetryError {
/**
* constructor 构造函数
* @param {string} message 错误信息
* @param {number} seconds x秒后重试, 单位秒
*/
constructor(message: string, seconds: number);

/**
* constructor 构造函数
* @param {string} message 错误信息
* @param {Date} date 重试时间, 指定时间后重试
*/
constructor(message: string, date: Date);
}

declare namespace CATType {
interface ProxyRule {
proxyServer: ProxyServer;
Expand Down

0 comments on commit 16551df

Please sign in to comment.