Skip to content

Commit 16551df

Browse files
committed
✨ 后台脚本增加重试逻辑
1 parent fc2134b commit 16551df

File tree

9 files changed

+229
-20
lines changed

9 files changed

+229
-20
lines changed

eslint/linter-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ const config = {
4949
globalReturn: true,
5050
},
5151
},
52+
globals: {
53+
CATRetryError: "readonly",
54+
CAT_fileStorage: "readonly",
55+
CAT_userConfig: "readonly",
56+
},
5257
rules: {
5358
"constructor-super": ["error"],
5459
"for-direction": ["error"],

example/error_retry.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// ==UserScript==
2+
// @name 重试示例
3+
// @namespace https://bbs.tampermonkey.net.cn/
4+
// @version 0.1.0
5+
// @description try to take over the world!
6+
// @author You
7+
// @crontab * * once * *
8+
// @grant GM_notification
9+
// ==/UserScript==
10+
11+
return new Promise((resolve, reject) => {
12+
// Your code here...
13+
GM_notification({
14+
title: "retry",
15+
text: "10秒后重试"
16+
});
17+
reject(new CATRetryError("xxx错误", 10));
18+
});

src/app/repo/scripts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,12 @@ export interface Script {
7070
status: SCRIPT_STATUS; // 脚本状态 1:启用 2:禁用 3:错误 4:初始化
7171
sort: number; // 脚本顺序位置
7272
runStatus: SCRIPT_RUN_STATUS; // 脚本运行状态,后台脚本才会有此状态 running:运行中 complete:完成 error:错误 retry:重试
73-
error?: string; // 运行错误信息
73+
error?: { error: string } | string; // 运行错误信息
7474
createtime: number; // 脚本创建时间戳
7575
updatetime?: number; // 脚本更新时间戳
7676
checktime: number; // 脚本检查更新时间戳
7777
lastruntime?: number; // 脚本最后一次运行时间戳
78+
nextruntime?: number; // 脚本下一次运行时间戳
7879
}
7980

8081
// 脚本运行时的资源,包含已经编译好的脚本与脚本需要的资源

src/runtime/background/runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,12 @@ export default class Runtime extends Manager {
537537
// 监听沙盒发送的脚本运行状态消息
538538
this.message.setHandler(
539539
"scriptRunStatus",
540-
(action, [scriptId, runStatus]: any) => {
540+
(action, [scriptId, runStatus, error, nextruntime]: any) => {
541541
this.scriptDAO.update(scriptId, {
542542
runStatus,
543543
lastruntime: new Date().getTime(),
544+
nextruntime,
545+
error,
544546
});
545547
Runtime.hook.trigger("runStatus", scriptId, runStatus);
546548
}
@@ -673,6 +675,8 @@ export default class Runtime extends Manager {
673675
loadBackgroundScript(script: ScriptRunResouce): Promise<boolean> {
674676
this.runBackScript.set(script.id, script);
675677
return new Promise((resolve, reject) => {
678+
// 清除重试数据
679+
script.nextruntime = 0;
676680
this.messageSandbox
677681
?.syncSend("enable", script)
678682
.then(() => {

src/runtime/content/exec_script.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export default class ExecScript {
4040
constructor(
4141
scriptRes: ScriptRunResouce,
4242
message: MessageManager,
43-
scriptFunc?: ScriptFunc
43+
scriptFunc?: ScriptFunc,
44+
thisContext?: { [key: string]: any }
4445
) {
4546
this.scriptRes = scriptRes;
4647
this.logger = LoggerCore.getInstance().logger({
@@ -58,15 +59,19 @@ export default class ExecScript {
5859
}
5960
if (scriptRes.grantMap.none) {
6061
// 不注入任何GM api
61-
this.proxyContent = window;
62+
this.proxyContent = global;
6263
} else {
6364
// 构建脚本GM上下文
6465
this.sandboxContent = createContext(
6566
scriptRes,
6667
this.GM_info,
6768
this.proxyMessage
6869
);
69-
this.proxyContent = proxyContext(window, this.sandboxContent);
70+
this.proxyContent = proxyContext(
71+
global,
72+
this.sandboxContent,
73+
thisContext
74+
);
7075
}
7176
}
7277

src/runtime/content/exec_warp.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* eslint-disable func-names */
2+
/* eslint-disable max-classes-per-file */
3+
import { MessageManager } from "@App/app/message/message";
4+
import { ScriptRunResouce } from "@App/app/repo/scripts";
5+
import ExecScript from "./exec_script";
6+
7+
export class CATRetryError {
8+
msg: string;
9+
10+
time: Date;
11+
12+
constructor(msg: string, time: number | Date) {
13+
this.msg = msg;
14+
if (typeof time === "number") {
15+
this.time = new Date(Date.now() + time * 1000);
16+
} else {
17+
this.time = time;
18+
}
19+
}
20+
}
21+
22+
export class BgExecScriptWarp extends ExecScript {
23+
setTimeout: Map<number, boolean>;
24+
25+
setInterval: Map<number, boolean>;
26+
27+
constructor(scriptRes: ScriptRunResouce, message: MessageManager) {
28+
const thisContext: { [key: string]: any } = {};
29+
const setTimeout = new Map<number, any>();
30+
const setInterval = new Map<number, any>();
31+
thisContext.setTimeout = function (
32+
handler: () => void,
33+
timeout: number | undefined,
34+
...args: any
35+
) {
36+
const t = global.setTimeout(
37+
function () {
38+
setTimeout.delete(t);
39+
if (typeof handler === "function") {
40+
handler();
41+
}
42+
},
43+
timeout,
44+
...args
45+
);
46+
setTimeout.set(t, true);
47+
return t;
48+
};
49+
thisContext.clearTimeout = function (t: number | undefined) {
50+
global.clearTimeout(t);
51+
};
52+
thisContext.setInterval = function (
53+
handler: () => void,
54+
timeout: number | undefined,
55+
...args: any
56+
) {
57+
const t = global.setInterval(
58+
function () {
59+
setInterval.delete(t);
60+
if (typeof handler === "function") {
61+
handler();
62+
}
63+
},
64+
timeout,
65+
...args
66+
);
67+
setInterval.set(t, true);
68+
return t;
69+
};
70+
thisContext.clearInterval = function (t: number | undefined) {
71+
global.clearInterval(t);
72+
};
73+
// @ts-ignore
74+
thisContext.CATRetryError = CATRetryError;
75+
super(scriptRes, message, undefined, thisContext);
76+
this.setTimeout = setTimeout;
77+
this.setInterval = setInterval;
78+
}
79+
80+
stop() {
81+
this.setTimeout.forEach((_, t) => {
82+
global.clearTimeout(t);
83+
});
84+
this.setTimeout.clear();
85+
this.setInterval.forEach((_, t) => {
86+
global.clearInterval(t);
87+
});
88+
this.setInterval.clear();
89+
return super.stop();
90+
}
91+
}

src/runtime/content/sandbox.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { CronJob } from "cron";
1313
import IoC from "@App/app/ioc";
1414
import ExecScript from "./exec_script";
15+
import { BgExecScriptWarp, CATRetryError } from "./exec_warp";
1516

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

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

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

32+
retryList: {
33+
script: ScriptRunResouce;
34+
retryTime: number;
35+
}[] = [];
36+
3137
constructor(message: MessageSandbox) {
3238
this.message = message;
3339
this.logger = LoggerCore.getInstance().logger({ component: "sandbox" });
40+
// 重试队列,5s检查一次
41+
setInterval(() => {
42+
if (!this.retryList.length) {
43+
return;
44+
}
45+
const now = Date.now();
46+
const retryList = [];
47+
for (let i = 0; i < this.retryList.length; i += 1) {
48+
const item = this.retryList[i];
49+
if (item.retryTime < now) {
50+
this.retryList.splice(i, 1);
51+
i -= 1;
52+
retryList.push(item.script);
53+
}
54+
}
55+
retryList.forEach((script) => {
56+
script.nextruntime = 0;
57+
this.execScript(script);
58+
});
59+
}, 5000);
60+
}
61+
62+
joinRetryList(script: ScriptRunResouce) {
63+
if (script.nextruntime) {
64+
this.retryList.push({
65+
script,
66+
retryTime: script.nextruntime,
67+
});
68+
this.retryList.sort((a, b) => a.retryTime - b.retryTime);
69+
}
70+
}
71+
72+
removeRetryList(scriptId: number) {
73+
for (let i = 0; i < this.retryList.length; i += 1) {
74+
if (this.retryList[i].script.id === scriptId) {
75+
this.retryList.splice(i, 1);
76+
i -= 1;
77+
}
78+
}
3479
}
3580

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

56101
// 直接运行脚本
57102
start(script: ScriptRunResouce): Promise<boolean> {
58-
return this.execScript(script);
103+
return this.execScript(script, true);
59104
}
60105

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

102145
// 停止计时器
@@ -111,14 +154,14 @@ export default class SandboxRuntime {
111154
}
112155

113156
// 执行脚本
114-
execScript(script: ScriptRunResouce) {
157+
execScript(script: ScriptRunResouce, execOnce?: boolean) {
115158
const logger = this.logger.with({ scriptId: script.id, name: script.name });
116159
if (this.execScripts.has(script.id)) {
117160
// 释放掉资源
118161
// 暂未实现执行完成后立马释放,会在下一次执行时释放
119162
this.stop(script.id);
120163
}
121-
const exec = new ExecScript(script, this.message);
164+
const exec = new BgExecScriptWarp(script, this.message);
122165
this.execScripts.set(script.id, exec);
123166
this.message.send("scriptRunStatus", [
124167
exec.scriptRes.id,
@@ -141,11 +184,25 @@ export default class SandboxRuntime {
141184
})
142185
.catch((err) => {
143186
// 发送执行完成+错误消息
144-
logger.error("exec script error", Logger.E(err));
187+
let errMsg;
188+
let nextruntime = 0;
189+
if (err instanceof CATRetryError) {
190+
errMsg = { error: err.msg };
191+
if (!execOnce) {
192+
// 下一次执行时间
193+
nextruntime = err.time.getTime();
194+
script.nextruntime = nextruntime;
195+
this.joinRetryList(script);
196+
}
197+
} else {
198+
errMsg = Logger.E(err);
199+
}
200+
logger.error("exec script error", errMsg);
145201
this.message.send("scriptRunStatus", [
146202
exec.scriptRes.id,
147203
SCRIPT_RUN_STATUS_ERROR,
148-
Logger.E(err),
204+
errMsg,
205+
nextruntime,
149206
]);
150207
// 错误还是抛出,方便排查
151208
throw err;
@@ -161,6 +218,8 @@ export default class SandboxRuntime {
161218
if (!script.metadata.crontab) {
162219
throw new Error("错误的crontab表达式");
163220
}
221+
// 如果有nextruntime,则加入重试队列
222+
this.joinRetryList(script);
164223
let flag = false;
165224
const cronJobList: Array<CronJob> = [];
166225
script.metadata.crontab.forEach((val) => {

src/runtime/content/utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,19 @@ Object.keys(descs).forEach((key) => {
142142
});
143143

144144
// 拦截上下文
145-
export function proxyContext(global: any, context: any) {
145+
export function proxyContext(
146+
global: any,
147+
context: any,
148+
thisContext?: { [key: string]: any }
149+
) {
146150
const special = Object.assign(writables);
147151
// 处理某些特殊的属性
148152
// 后台脚本要不要考虑不能使用eval?
149-
const thisContext: { [key: string]: any } = {
150-
eval: global.eval,
151-
define: undefined,
152-
};
153+
if (!thisContext) {
154+
thisContext = {};
155+
}
156+
thisContext.eval = global.eval;
157+
thisContext.define = undefined;
153158
// keyword是与createContext时同步的,避免访问到context的内部变量
154159
const contextKeyword: { [key: string]: any } = {
155160
message: 1,

src/types/scriptcat.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,27 @@ declare function CAT_fileStorage(
240240
): void;
241241
declare function CAT_fileStorage(action: "config"): void;
242242

243+
/**
244+
* 脚本猫定时脚本重试错误, 当你的脚本出现错误时, 可以reject返回此错误, 以便脚本猫重试
245+
* 重试时间请注意不要与脚本执行时间冲突, 否则可能会导致重复执行, 最小重试时间为5s
246+
* @class CATRetryError
247+
*/
248+
declare class CATRetryError {
249+
/**
250+
* constructor 构造函数
251+
* @param {string} message 错误信息
252+
* @param {number} seconds x秒后重试, 单位秒
253+
*/
254+
constructor(message: string, seconds: number);
255+
256+
/**
257+
* constructor 构造函数
258+
* @param {string} message 错误信息
259+
* @param {Date} date 重试时间, 指定时间后重试
260+
*/
261+
constructor(message: string, date: Date);
262+
}
263+
243264
declare namespace CATType {
244265
interface ProxyRule {
245266
proxyServer: ProxyServer;

0 commit comments

Comments
 (0)