runtime
Some checks failed
test / Run tests (push) Failing after 17s
build / Build (push) Failing after 24s
Some checks failed
test / Run tests (push) Failing after 17s
build / Build (push) Failing after 24s
This commit is contained in:
@ -1,6 +1,24 @@
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { sendMessage } from "../utils";
|
||||
import { SCRIPT_RUN_STATUS } from "@App/app/repo/scripts";
|
||||
|
||||
export function preparationSandbox(msg: WindowMessage) {
|
||||
return sendMessage(msg, "preparationSandbox");
|
||||
}
|
||||
|
||||
// 代理发送消息到ServiceWorker
|
||||
export function sendMessageToServiceWorker(msg: WindowMessage, action: string, data?: any) {
|
||||
return sendMessage(msg, "sendMessageToServiceWorker", { action, data });
|
||||
}
|
||||
|
||||
// 代理连接ServiceWorker
|
||||
export function connectServiceWorker(msg: WindowMessage) {
|
||||
return sendMessage(msg, "connectServiceWorker");
|
||||
}
|
||||
|
||||
export function proxyUpdateRunStatus(
|
||||
msg: WindowMessage,
|
||||
data: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: any; nextruntime?: number }
|
||||
) {
|
||||
return sendMessageToServiceWorker(msg, "updateRunStatus", data);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { Logger, LoggerDAO } from "@App/app/repo/logger";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { ExtensionMessage } from "@Packages/message/extension_message";
|
||||
import { ServiceWorkerClient } from "../service_worker/client";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
|
||||
// offscreen环境的管理器
|
||||
export class OffscreenManager {
|
||||
@ -31,11 +32,16 @@ export class OffscreenManager {
|
||||
serviceWorker.preparationOffscreen();
|
||||
}
|
||||
|
||||
sendMessageToServiceWorker(data: { action: string; data: any }) {
|
||||
return sendMessage(data.action, data.data);
|
||||
}
|
||||
|
||||
initManager() {
|
||||
// 监听消息
|
||||
const group = this.api.group("offscreen");
|
||||
this.windowApi.on("logger", this.logger.bind(this));
|
||||
this.windowApi.on("preparationSandbox", this.preparationSandbox.bind(this));
|
||||
this.windowApi.on("sendMessageToServiceWorker", this.sendMessageToServiceWorker.bind(this));
|
||||
const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker);
|
||||
script.init();
|
||||
}
|
||||
|
@ -3,13 +3,23 @@ import Logger from "@App/app/logger/logger";
|
||||
import { Broker, MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { ScriptClient, subscribeScriptEnable } from "../service_worker/client";
|
||||
import { SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
|
||||
import {
|
||||
ResourceClient,
|
||||
ScriptClient,
|
||||
subscribeScriptEnable,
|
||||
subscribeScriptInstall,
|
||||
ValueClient,
|
||||
} from "../service_worker/client";
|
||||
import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
|
||||
import { disableScript, enableScript } from "../sandbox/client";
|
||||
|
||||
export class ScriptService {
|
||||
logger: Logger;
|
||||
|
||||
scriptClient: ScriptClient = new ScriptClient();
|
||||
resourceClient: ResourceClient = new ResourceClient();
|
||||
valueClient: ValueClient = new ValueClient();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue,
|
||||
@ -21,16 +31,26 @@ export class ScriptService {
|
||||
|
||||
async init() {
|
||||
subscribeScriptEnable(this.broker, async (data) => {
|
||||
const info = await new ScriptClient().info(data.uuid);
|
||||
const info = await this.scriptClient.info(data.uuid);
|
||||
if (info.type === SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
if (data.enable) {
|
||||
// 发送给沙盒运行
|
||||
enableScript(this.windowMessage, info);
|
||||
// 构造脚本运行资源,发送给沙盒运行
|
||||
enableScript(this.windowMessage, await this.scriptClient.getScriptRunResource(info));
|
||||
} else {
|
||||
// 发送给沙盒停止
|
||||
disableScript(this.windowMessage, info);
|
||||
disableScript(this.windowMessage, info.uuid);
|
||||
}
|
||||
});
|
||||
subscribeScriptInstall(this.broker, async (data) => {
|
||||
// 判断是开启还是关闭
|
||||
if (data.script.status === SCRIPT_STATUS_ENABLE) {
|
||||
// 构造脚本运行资源,发送给沙盒运行
|
||||
enableScript(this.windowMessage, await this.scriptClient.getScriptRunResource(data.script));
|
||||
} else {
|
||||
// 发送给沙盒停止
|
||||
disableScript(this.windowMessage, data.script.uuid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { sendMessage } from "../utils";
|
||||
|
||||
export function enableScript(msg: WindowMessage, data: Script) {
|
||||
export function enableScript(msg: WindowMessage, data: ScriptRunResouce) {
|
||||
return sendMessage(msg, "enableScript", data);
|
||||
}
|
||||
|
||||
export function disableScript(msg: WindowMessage, data: Script) {
|
||||
return sendMessage(msg, "disableScript", data);
|
||||
export function disableScript(msg: WindowMessage, uuid: string) {
|
||||
return sendMessage(msg, "disableScript", uuid);
|
||||
}
|
||||
|
@ -1,10 +1,18 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Script, SCRIPT_TYPE_BACKGROUND, ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import {
|
||||
SCRIPT_RUN_STATUS_COMPLETE,
|
||||
SCRIPT_RUN_STATUS_ERROR,
|
||||
SCRIPT_RUN_STATUS_RUNNING,
|
||||
SCRIPT_TYPE_BACKGROUND,
|
||||
ScriptRunResouce,
|
||||
} from "@App/app/repo/scripts";
|
||||
import ExecScript from "@App/runtime/content/exec_script";
|
||||
import { BgExecScriptWarp, CATRetryError } from "@App/runtime/content/exec_warp";
|
||||
import { Server } from "@Packages/message/server";
|
||||
import { WindowMessage } from "@Packages/message/window_message";
|
||||
import { CronJob } from "cron";
|
||||
import { proxyUpdateRunStatus } from "../offscreen/client";
|
||||
|
||||
export class Runtime {
|
||||
cronJob: Map<string, Array<CronJob>> = new Map();
|
||||
@ -55,34 +63,219 @@ export class Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
removeRetryList(scriptId: number) {
|
||||
removeRetryList(uuid: string) {
|
||||
for (let i = 0; i < this.retryList.length; i += 1) {
|
||||
if (this.retryList[i].script.id === scriptId) {
|
||||
if (this.retryList[i].script.uuid === uuid) {
|
||||
this.retryList.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableScript(data: Script) {
|
||||
// 开启脚本, 判断脚本是后台脚本还是定时脚本
|
||||
if (data.type === SCRIPT_TYPE_BACKGROUND) {
|
||||
async enableScript(script: ScriptRunResouce) {
|
||||
// 开启脚本
|
||||
// 如果正在运行,先释放
|
||||
if (this.execScripts.has(script.uuid)) {
|
||||
await this.disableScript(script.uuid);
|
||||
}
|
||||
if (script.type === SCRIPT_TYPE_BACKGROUND) {
|
||||
// 后台脚本直接运行起来
|
||||
return this.execScript(script);
|
||||
} else {
|
||||
// 定时脚本加入定时任务
|
||||
return this.crontabScript(script);
|
||||
}
|
||||
eval("console.log('hello')");
|
||||
console.log("enableScript", data);
|
||||
}
|
||||
|
||||
disableScript(data: Script) {
|
||||
// 关闭脚本, 判断脚本是后台脚本还是定时脚本
|
||||
if (data.type === SCRIPT_TYPE_BACKGROUND) {
|
||||
// 后台脚本直接停止
|
||||
} else {
|
||||
// 定时脚本停止定时任务
|
||||
disableScript(uuid: string) {
|
||||
// 关闭脚本
|
||||
// 停止定时任务
|
||||
this.stopCronJob(uuid);
|
||||
// 移除重试队列
|
||||
this.removeRetryList(uuid);
|
||||
// 发送运行状态变更
|
||||
proxyUpdateRunStatus(this.windowMessage, { uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE });
|
||||
// 停止脚本运行
|
||||
return this.stopScript(uuid);
|
||||
}
|
||||
|
||||
// 执行脚本
|
||||
async execScript(script: ScriptRunResouce, execOnce?: boolean) {
|
||||
const logger = this.logger.with({ script: script.uuid, name: script.name });
|
||||
if (this.execScripts.has(script.uuid)) {
|
||||
// 释放掉资源
|
||||
// 暂未实现执行完成后立马释放,会在下一次执行时释放
|
||||
await this.stopScript(script.uuid);
|
||||
}
|
||||
console.log("disableScript", data);
|
||||
const exec = new BgExecScriptWarp(script);
|
||||
this.execScripts.set(script.uuid, exec);
|
||||
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_RUNNING });
|
||||
// 修改掉脚本掉最后运行时间, 数据库也需要修改
|
||||
script.lastruntime = new Date().getTime();
|
||||
const ret = exec.exec();
|
||||
if (ret instanceof Promise) {
|
||||
ret
|
||||
.then((resp) => {
|
||||
// 发送执行完成消息
|
||||
proxyUpdateRunStatus(this.windowMessage, { uuid: script.uuid, runStatus: SCRIPT_RUN_STATUS_COMPLETE });
|
||||
logger.info("exec script complete", {
|
||||
value: resp,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// 发送执行完成+错误消息
|
||||
let errMsg;
|
||||
let nextruntime = 0;
|
||||
if (err instanceof CATRetryError) {
|
||||
// @ts-ignore
|
||||
errMsg = { error: err.msg };
|
||||
if (!execOnce) {
|
||||
// 下一次执行时间
|
||||
// @ts-ignore
|
||||
nextruntime = err.time.getTime();
|
||||
script.nextruntime = nextruntime;
|
||||
this.joinRetryList(script);
|
||||
}
|
||||
} else {
|
||||
errMsg = Logger.E(err);
|
||||
}
|
||||
logger.error("exec script error", errMsg);
|
||||
proxyUpdateRunStatus(this.windowMessage, {
|
||||
uuid: script.uuid,
|
||||
runStatus: SCRIPT_RUN_STATUS_ERROR,
|
||||
error: errMsg,
|
||||
nextruntime,
|
||||
});
|
||||
// 错误还是抛出,方便排查
|
||||
throw err;
|
||||
});
|
||||
} else {
|
||||
logger.warn("backscript return not promise");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
crontabScript(script: ScriptRunResouce) {
|
||||
// 执行定时脚本 运行表达式
|
||||
if (!script.metadata.crontab) {
|
||||
throw new Error("错误的crontab表达式");
|
||||
}
|
||||
// 如果有nextruntime,则加入重试队列
|
||||
this.joinRetryList(script);
|
||||
let flag = false;
|
||||
const cronJobList: Array<CronJob> = [];
|
||||
script.metadata.crontab.forEach((val) => {
|
||||
let oncePos = 0;
|
||||
let crontab = val;
|
||||
if (crontab.indexOf("once") !== -1) {
|
||||
const vals = crontab.split(" ");
|
||||
vals.forEach((item, index) => {
|
||||
if (item === "once") {
|
||||
oncePos = index;
|
||||
}
|
||||
});
|
||||
if (vals.length === 5) {
|
||||
oncePos += 1;
|
||||
}
|
||||
crontab = crontab.replace(/once/g, "*");
|
||||
}
|
||||
try {
|
||||
const cron = new CronJob(crontab, this.crontabExec(script, oncePos));
|
||||
cron.start();
|
||||
cronJobList.push(cron);
|
||||
} catch (e) {
|
||||
flag = true;
|
||||
this.logger.error(
|
||||
"create cronjob failed",
|
||||
{
|
||||
uuid: script.uuid,
|
||||
crontab: val,
|
||||
},
|
||||
Logger.E(e)
|
||||
);
|
||||
}
|
||||
});
|
||||
if (cronJobList.length !== script.metadata.crontab.length) {
|
||||
// 有表达式失败了
|
||||
cronJobList.forEach((crontab) => {
|
||||
crontab.stop();
|
||||
});
|
||||
} else {
|
||||
this.cronJob.set(script.uuid, cronJobList);
|
||||
}
|
||||
return Promise.resolve(!flag);
|
||||
}
|
||||
|
||||
crontabExec(script: ScriptRunResouce, oncePos: number) {
|
||||
if (oncePos) {
|
||||
return () => {
|
||||
// 没有最后一次执行时间表示之前都没执行过,直接执行
|
||||
if (!script.lastruntime) {
|
||||
this.execScript(script);
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
const last = new Date(script.lastruntime);
|
||||
let flag = false;
|
||||
// 根据once所在的位置去判断执行
|
||||
switch (oncePos) {
|
||||
case 1: // 每分钟
|
||||
flag = last.getMinutes() !== now.getMinutes();
|
||||
break;
|
||||
case 2: // 每小时
|
||||
flag = last.getHours() !== now.getHours();
|
||||
break;
|
||||
case 3: // 每天
|
||||
flag = last.getDay() !== now.getDay();
|
||||
break;
|
||||
case 4: // 每月
|
||||
flag = last.getMonth() !== now.getMonth();
|
||||
break;
|
||||
case 5: // 每周
|
||||
flag = this.getWeek(last) !== this.getWeek(now);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (flag) {
|
||||
this.execScript(script);
|
||||
}
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
this.execScript(script);
|
||||
};
|
||||
}
|
||||
|
||||
// 获取本周是第几周
|
||||
getWeek(date: Date) {
|
||||
const nowDate = new Date(date);
|
||||
const firstDay = new Date(date);
|
||||
firstDay.setMonth(0); // 设置1月
|
||||
firstDay.setDate(1); // 设置1号
|
||||
const diffDays = Math.ceil((nowDate.getTime() - firstDay.getTime()) / (24 * 60 * 60 * 1000));
|
||||
const week = Math.ceil(diffDays / 7);
|
||||
return week === 0 ? 1 : week;
|
||||
}
|
||||
|
||||
// 停止计时器
|
||||
stopCronJob(uuid: string) {
|
||||
const list = this.cronJob.get(uuid);
|
||||
if (list) {
|
||||
list.forEach((val) => {
|
||||
val.stop();
|
||||
});
|
||||
this.cronJob.delete(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
stopScript(uuid: string) {
|
||||
const exec = this.execScripts.get(uuid);
|
||||
if (!exec) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
exec.stop();
|
||||
this.execScripts.delete(uuid);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { Script, ScriptCode, ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { Client } from "@Packages/message/client";
|
||||
import { InstallSource } from ".";
|
||||
import { Broker } from "@Packages/message/message_queue";
|
||||
import { Resource } from "@App/app/repo/resource";
|
||||
|
||||
export class ServiceWorkerClient extends Client {
|
||||
constructor() {
|
||||
@ -23,8 +24,8 @@ export class ScriptClient extends Client {
|
||||
return this.do("getInstallInfo", uuid);
|
||||
}
|
||||
|
||||
install(script: Script, upsertBy: InstallSource = "user") {
|
||||
return this.do("install", { script, upsertBy });
|
||||
install(script: Script, code: string, upsertBy: InstallSource = "user") {
|
||||
return this.do("install", { script, code, upsertBy });
|
||||
}
|
||||
|
||||
delete(uuid: string) {
|
||||
@ -38,6 +39,34 @@ export class ScriptClient extends Client {
|
||||
info(uuid: string): Promise<Script> {
|
||||
return this.do("fetchInfo", uuid);
|
||||
}
|
||||
|
||||
getCode(uuid: string): Promise<ScriptCode | undefined> {
|
||||
return this.do("getCode", uuid);
|
||||
}
|
||||
|
||||
getScriptRunResource(script: Script): Promise<ScriptRunResouce> {
|
||||
return this.do("getScriptRunResource", script);
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker/resource");
|
||||
}
|
||||
|
||||
getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
||||
return this.do("getScriptResources", script);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueClient extends Client {
|
||||
constructor() {
|
||||
super("serviceWorker/value");
|
||||
}
|
||||
|
||||
getScriptValue(script: Script) {
|
||||
return this.do("getScriptValue", script);
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeScriptInstall(
|
||||
|
@ -2,6 +2,9 @@ import { Server } from "@Packages/message/server";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { ScriptService } from "./script";
|
||||
import { ExtensionMessage } from "@Packages/message/extension_message";
|
||||
import { ResourceService } from "./resource";
|
||||
import { ValueService } from "./value";
|
||||
import { RuntimeService } from "./runtime";
|
||||
|
||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||
|
||||
@ -19,7 +22,14 @@ export default class ServiceWorkerManager {
|
||||
// 准备好环境
|
||||
this.mq.emit("preparationOffscreen", {});
|
||||
});
|
||||
const script = new ScriptService(group.group("script"), this.mq);
|
||||
|
||||
const resource = new ResourceService(group.group("resource"), this.mq);
|
||||
resource.init();
|
||||
const value = new ValueService(group.group("value"), this.mq);
|
||||
value.init();
|
||||
const script = new ScriptService(group.group("script"), this.mq, value, resource);
|
||||
script.init();
|
||||
const runtime = new RuntimeService(group.group("runtime"), this.mq);
|
||||
runtime.init();
|
||||
}
|
||||
}
|
||||
|
285
src/app/service/service_worker/resource.ts
Normal file
285
src/app/service/service_worker/resource.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import crypto from "crypto-js";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Resource, ResourceDAO, ResourceHash, ResourceType } from "@App/app/repo/resource";
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { isText } from "@App/pkg/utils/istextorbinary";
|
||||
import { blobToBase64 } from "@App/pkg/utils/script";
|
||||
|
||||
export class ResourceService {
|
||||
logger: Logger;
|
||||
resourceDAO: ResourceDAO = new ResourceDAO();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue
|
||||
) {
|
||||
this.logger = LoggerCore.logger().with({ service: "resource" });
|
||||
}
|
||||
|
||||
public async getResource(uuid: string, url: string, type: ResourceType): Promise<Resource | undefined> {
|
||||
let res = await this.getResourceModel(url);
|
||||
if (res) {
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
try {
|
||||
res = await this.addResource(url, uuid, type);
|
||||
if (res) {
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// ignore
|
||||
this.logger.error("get resource failed", { uuid, url }, Logger.E(e));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
||||
return Promise.resolve({
|
||||
...((await this.getResourceByType(script, "require")) || {}),
|
||||
...((await this.getResourceByType(script, "require-css")) || {}),
|
||||
...((await this.getResourceByType(script, "resource")) || {}),
|
||||
});
|
||||
}
|
||||
|
||||
async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
|
||||
if (!script.metadata[type]) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const ret: { [key: string]: Resource } = {};
|
||||
await Promise.allSettled(
|
||||
script.metadata[type].map(async (u) => {
|
||||
if (type === "resource") {
|
||||
const split = u.split(/\s+/);
|
||||
if (split.length === 2) {
|
||||
const res = await this.getResource(script.uuid, split[1], "resource");
|
||||
if (res) {
|
||||
ret[split[0]] = res;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await this.getResource(script.uuid, u, type);
|
||||
if (res) {
|
||||
ret[u] = res;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
// 更新资源
|
||||
async checkScriptResource(script: Script) {
|
||||
return Promise.resolve({
|
||||
...((await this.checkResourceByType(script, "require")) || {}),
|
||||
...((await this.checkResourceByType(script, "require-css")) || {}),
|
||||
...((await this.checkResourceByType(script, "resource")) || {}),
|
||||
});
|
||||
}
|
||||
|
||||
async checkResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
|
||||
if (!script.metadata[type]) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const ret: { [key: string]: Resource } = {};
|
||||
await Promise.allSettled(
|
||||
script.metadata[type].map(async (u) => {
|
||||
if (type === "resource") {
|
||||
const split = u.split(/\s+/);
|
||||
if (split.length === 2) {
|
||||
const res = await this.checkResource(script.uuid, split[1], "resource");
|
||||
if (res) {
|
||||
ret[split[0]] = res;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await this.checkResource(script.uuid, u, type);
|
||||
if (res) {
|
||||
ret[u] = res;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
async checkResource(uuid: string, url: string, type: ResourceType) {
|
||||
let res = await this.getResourceModel(url);
|
||||
if (res) {
|
||||
// 判断1分钟过期
|
||||
if ((res.updatetime || 0) > new Date().getTime() - 1000 * 60) {
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
}
|
||||
try {
|
||||
res = await this.updateResource(url, uuid, type);
|
||||
if (res) {
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// ignore
|
||||
this.logger.error("check resource failed", { uuid, url }, Logger.E(e));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async updateResource(url: string, uuid: string, type: ResourceType) {
|
||||
// 重新加载
|
||||
const u = this.parseUrl(url);
|
||||
let result = await this.getResourceModel(u.url);
|
||||
try {
|
||||
const resource = await this.loadByUrl(u.url, type);
|
||||
resource.updatetime = new Date().getTime();
|
||||
if (!result) {
|
||||
// 资源不存在,保存
|
||||
resource.createtime = new Date().getTime();
|
||||
resource.link = { uuid: true };
|
||||
await this.resourceDAO.save(resource);
|
||||
result = resource;
|
||||
this.logger.info("reload new resource success", { url: u.url });
|
||||
} else {
|
||||
result.base64 = resource.base64;
|
||||
result.content = resource.content;
|
||||
result.contentType = resource.contentType;
|
||||
result.hash = resource.hash;
|
||||
result.updatetime = resource.updatetime;
|
||||
result.link[uuid] = true;
|
||||
await this.resourceDAO.update(result.url, result);
|
||||
this.logger.info("reload resource success", {
|
||||
url: u.url,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error("load resource error", { url: u.url }, Logger.E(e));
|
||||
throw e;
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
public async addResource(url: string, uuid: string, type: ResourceType): Promise<Resource> {
|
||||
const u = this.parseUrl(url);
|
||||
let result = await this.getResourceModel(u.url);
|
||||
// 资源不存在,重新加载
|
||||
if (!result) {
|
||||
try {
|
||||
const resource = await this.loadByUrl(u.url, type);
|
||||
resource.link[uuid] = true;
|
||||
resource.createtime = new Date().getTime();
|
||||
resource.updatetime = new Date().getTime();
|
||||
await this.resourceDAO.save(resource);
|
||||
result = resource;
|
||||
this.logger.info("load resource success", { url: u.url });
|
||||
} catch (e) {
|
||||
this.logger.error("load resource error", { url: u.url }, Logger.E(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
async getResourceModel(url: string) {
|
||||
const u = this.parseUrl(url);
|
||||
const resource = await this.resourceDAO.get(u.url);
|
||||
if (resource) {
|
||||
// 校验hash
|
||||
if (u.hash) {
|
||||
if (
|
||||
(u.hash.md5 && u.hash.md5 !== resource.hash.md5) ||
|
||||
(u.hash.sha1 && u.hash.sha1 !== resource.hash.sha1) ||
|
||||
(u.hash.sha256 && u.hash.sha256 !== resource.hash.sha256) ||
|
||||
(u.hash.sha384 && u.hash.sha384 !== resource.hash.sha384) ||
|
||||
(u.hash.sha512 && u.hash.sha512 !== resource.hash.sha512)
|
||||
) {
|
||||
resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`;
|
||||
}
|
||||
}
|
||||
return Promise.resolve(resource);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
calculateHash(blob: Blob): Promise<ResourceHash> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(blob);
|
||||
reader.onloadend = () => {
|
||||
if (!reader.result) {
|
||||
resolve({
|
||||
md5: "",
|
||||
sha1: "",
|
||||
sha256: "",
|
||||
sha384: "",
|
||||
sha512: "",
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
md5: crypto.MD5(<string>reader.result).toString(),
|
||||
sha1: crypto.SHA1(<string>reader.result).toString(),
|
||||
sha256: crypto.SHA256(<string>reader.result).toString(),
|
||||
sha384: crypto.SHA384(<string>reader.result).toString(),
|
||||
sha512: crypto.SHA512(<string>reader.result).toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
loadByUrl(url: string, type: ResourceType): Promise<Resource> {
|
||||
const u = this.parseUrl(url);
|
||||
return fetch(u.url)
|
||||
.then(async (resp) => {
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`resource response status not 200:${resp.status}`);
|
||||
}
|
||||
return {
|
||||
data: await resp.blob(),
|
||||
headers: resp.headers,
|
||||
};
|
||||
})
|
||||
.then(async (response) => {
|
||||
const resource: Resource = {
|
||||
url: u.url,
|
||||
content: "",
|
||||
contentType: (response.headers.get("content-type") || "application/octet-stream").split(";")[0],
|
||||
hash: await this.calculateHash(<Blob>response.data),
|
||||
base64: "",
|
||||
link: {},
|
||||
type,
|
||||
createtime: new Date().getTime(),
|
||||
};
|
||||
const arrayBuffer = await (<Blob>response.data).arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
if (isText(uint8Array)) {
|
||||
resource.content = await (<Blob>response.data).text();
|
||||
}
|
||||
resource.base64 = (await blobToBase64(<Blob>response.data)) || "";
|
||||
return resource;
|
||||
});
|
||||
}
|
||||
|
||||
parseUrl(url: string): {
|
||||
url: string;
|
||||
hash?: { [key: string]: string };
|
||||
} {
|
||||
const urls = url.split("#");
|
||||
if (urls.length < 2) {
|
||||
return { url: urls[0], hash: undefined };
|
||||
}
|
||||
const hashs = urls[1].split(/[,;]/);
|
||||
const hash: { [key: string]: string } = {};
|
||||
hashs.forEach((val) => {
|
||||
const kv = val.split("=");
|
||||
if (kv.length < 2) {
|
||||
return;
|
||||
}
|
||||
hash[kv[0]] = kv[1].toLocaleLowerCase();
|
||||
});
|
||||
return { url: urls[0], hash };
|
||||
}
|
||||
|
||||
init() {
|
||||
this.group.on("getScriptResources", this.getScriptResources.bind(this));
|
||||
}
|
||||
}
|
56
src/app/service/service_worker/runtime.ts
Normal file
56
src/app/service/service_worker/runtime.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { ScriptEnableCallbackValue } from "./client";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptDAO } from "@App/app/repo/scripts";
|
||||
|
||||
export class RuntimeService {
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
// 监听脚本开启
|
||||
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
|
||||
const script = await this.scriptDAO.get(data.uuid);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
// 如果是普通脚本, 在service worker中进行注册
|
||||
// 如果是后台脚本, 在offscreen中进行处理
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
// 注册入页面脚本
|
||||
if (data.enable) {
|
||||
this.registryPageScript(script);
|
||||
} else {
|
||||
this.unregistryPageScript(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 将开启的脚本发送一次enable消息
|
||||
const scriptDao = new ScriptDAO();
|
||||
const list = await scriptDao.all();
|
||||
list.forEach((script) => {
|
||||
if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
||||
});
|
||||
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
|
||||
this.mq.addListener("preparationOffscreen", () => {
|
||||
list.forEach((script) => {
|
||||
if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registryPageScript(script: Script) {}
|
||||
|
||||
unregistryPageScript(script: Script) {}
|
||||
}
|
@ -5,24 +5,34 @@ import Logger from "@App/app/logger/logger";
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Cache from "@App/app/cache";
|
||||
import CacheKey from "@App/app/cache_key";
|
||||
import { openInCurrentTab } from "@App/pkg/utils/utils";
|
||||
import { openInCurrentTab, randomString } from "@App/pkg/utils/utils";
|
||||
import {
|
||||
Script,
|
||||
SCRIPT_RUN_STATUS,
|
||||
SCRIPT_STATUS_DISABLE,
|
||||
SCRIPT_STATUS_ENABLE,
|
||||
SCRIPT_TYPE_NORMAL,
|
||||
ScriptCodeDAO,
|
||||
ScriptDAO,
|
||||
ScriptRunResouce,
|
||||
} from "@App/app/repo/scripts";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { InstallSource } from ".";
|
||||
import { ScriptEnableCallbackValue } from "./client";
|
||||
import { ResourceService } from "./resource";
|
||||
import { ValueService } from "./value";
|
||||
import { compileScriptCode } from "@App/runtime/content/utils";
|
||||
|
||||
export class ScriptService {
|
||||
logger: Logger;
|
||||
scriptDAO: ScriptDAO = new ScriptDAO();
|
||||
scriptCodeDAO: ScriptCodeDAO = new ScriptCodeDAO();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue
|
||||
private mq: MessageQueue,
|
||||
private valueService: ValueService,
|
||||
private resourceService: ResourceService
|
||||
) {
|
||||
this.logger = LoggerCore.logger().with({ service: "script" });
|
||||
}
|
||||
@ -143,7 +153,7 @@ export class ScriptService {
|
||||
}
|
||||
|
||||
// 安装脚本
|
||||
async installScript(param: { script: Script; upsertBy: InstallSource }) {
|
||||
async installScript(param: { script: Script; code: string; upsertBy: InstallSource }) {
|
||||
param.upsertBy = param.upsertBy || "user";
|
||||
const { script, upsertBy } = param;
|
||||
const logger = this.logger.with({
|
||||
@ -225,6 +235,45 @@ export class ScriptService {
|
||||
return script;
|
||||
}
|
||||
|
||||
async updateRunStatus(params: { uuid: string; runStatus: SCRIPT_RUN_STATUS; error?: string; nextruntime?: number }) {
|
||||
await new ScriptDAO().update(params.uuid, {
|
||||
runStatus: params.runStatus,
|
||||
lastruntime: new Date().getTime(),
|
||||
error: params.error,
|
||||
nextruntime: params.nextruntime,
|
||||
});
|
||||
this.mq.publish("updateRunStatus", params);
|
||||
}
|
||||
|
||||
getCode(uuid: string) {
|
||||
return this.scriptCodeDAO.get(uuid);
|
||||
}
|
||||
|
||||
async buildScriptRunResource(script: Script): Promise<ScriptRunResouce> {
|
||||
const ret: ScriptRunResouce = <ScriptRunResouce>Object.assign(script);
|
||||
|
||||
// 自定义配置
|
||||
if (ret.selfMetadata) {
|
||||
ret.metadata = { ...ret.metadata };
|
||||
Object.keys(ret.selfMetadata).forEach((key) => {
|
||||
ret.metadata[key] = ret.selfMetadata![key];
|
||||
});
|
||||
}
|
||||
|
||||
ret.value = await this.valueService.getScriptValue(ret);
|
||||
|
||||
ret.resource = await this.resourceService.getScriptResources(ret);
|
||||
|
||||
ret.flag = randomString(16);
|
||||
const code = await this.getCode(ret.uuid);
|
||||
if (!code) {
|
||||
throw new Error("code is null");
|
||||
}
|
||||
ret.code = compileScriptCode(ret, code.code);
|
||||
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.listenerScriptInstall();
|
||||
|
||||
@ -233,51 +282,8 @@ export class ScriptService {
|
||||
this.group.on("delete", this.deleteScript.bind(this));
|
||||
this.group.on("enable", this.enableScript.bind(this));
|
||||
this.group.on("fetchInfo", this.fetchInfo.bind(this));
|
||||
|
||||
this.listenScript();
|
||||
this.group.on("updateRunStatus", this.updateRunStatus.bind(this));
|
||||
this.group.on("getCode", this.getCode.bind(this));
|
||||
this.group.on("getScriptRunResource", this.buildScriptRunResource.bind(this));
|
||||
}
|
||||
|
||||
// 监听脚本
|
||||
async listenScript() {
|
||||
// 监听脚本开启
|
||||
this.mq.addListener("enableScript", async (data: ScriptEnableCallbackValue) => {
|
||||
const script = await new ScriptDAO().findByUUID(data.uuid);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
// 如果是普通脚本, 在service worker中进行注册
|
||||
// 如果是后台脚本, 在offscreen中进行处理
|
||||
if (script.type === SCRIPT_TYPE_NORMAL) {
|
||||
// 注册入页面脚本
|
||||
if (data.enable) {
|
||||
this.registryPageScript(script);
|
||||
} else {
|
||||
this.unregistryPageScript(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 将开启的脚本发送一次enable消息
|
||||
const scriptDao = new ScriptDAO();
|
||||
const list = await scriptDao.all();
|
||||
list.forEach((script) => {
|
||||
if (script.status !== SCRIPT_STATUS_ENABLE || script.type !== SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
||||
});
|
||||
// 监听offscreen环境初始化, 初始化完成后, 再将后台脚本运行起来
|
||||
this.mq.addListener("preparationOffscreen", () => {
|
||||
list.forEach((script) => {
|
||||
if (script.status !== SCRIPT_STATUS_ENABLE || script.type === SCRIPT_TYPE_NORMAL) {
|
||||
return;
|
||||
}
|
||||
this.mq.publish("enableScript", { uuid: script.uuid, enable: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registryPageScript(script: Script) {}
|
||||
|
||||
unregistryPageScript(script: Script) {}
|
||||
}
|
||||
|
37
src/app/service/service_worker/value.ts
Normal file
37
src/app/service/service_worker/value.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { Script } from "@App/app/repo/scripts";
|
||||
import { ValueDAO } from "@App/app/repo/value";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Group } from "@Packages/message/server";
|
||||
|
||||
export class ValueService {
|
||||
logger: Logger;
|
||||
valueDAO: ValueDAO = new ValueDAO();
|
||||
|
||||
constructor(
|
||||
private group: Group,
|
||||
private mq: MessageQueue
|
||||
) {
|
||||
this.logger = LoggerCore.logger().with({ service: "value" });
|
||||
}
|
||||
|
||||
storageKey(script: Script): string {
|
||||
if (script.metadata.storagename) {
|
||||
return script.metadata.storagename[0];
|
||||
}
|
||||
return script.uuid;
|
||||
}
|
||||
|
||||
async getScriptValue(script: Script) {
|
||||
const ret = await this.valueDAO.get(this.storageKey(script));
|
||||
if (!ret) {
|
||||
return {};
|
||||
}
|
||||
return Promise.resolve(ret?.data);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.group.on("getScriptValue", this.getScriptValue.bind(this));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user