diff --git a/packages/message/extension_message.ts b/packages/message/extension_message.ts new file mode 100644 index 0000000..84c2e3e --- /dev/null +++ b/packages/message/extension_message.ts @@ -0,0 +1,57 @@ +import { Message, MessageConnect } from "./server"; + +export class ExtensionMessage implements Message { + onConnect(callback: (data: any, con: MessageConnect) => void) { + chrome.runtime.onConnect.addListener((port) => { + const handler = (msg: any) => { + port.onMessage.removeListener(handler); + callback(msg, new ExtensionMessageConnect(port)); + }; + port.onMessage.addListener(handler); + }); + } + + connect(data: any): Promise { + return new Promise((resolve) => { + const con = chrome.runtime.connect(); + con.postMessage(data); + resolve(new ExtensionMessageConnect(con)); + }); + } + + // 注意chrome.runtime.onMessage.addListener的回调函数需要返回true才能处理异步请求 + onMessage(callback: (data: any, sendResponse: (data: any) => void) => void) { + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + return callback(msg, sendResponse); + }); + } + + // 发送消息 注意不进行回调的内存泄漏 + sendMessage(data: any): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage(data, (resp) => { + resolve(resp); + }); + }); + } +} + +export class ExtensionMessageConnect implements MessageConnect { + constructor(private con: chrome.runtime.Port) {} + + sendMessage(data: any) { + this.con.postMessage(data); + } + + onMessage(callback: (data: any) => void) { + this.con.onMessage.addListener(callback); + } + + disconnect() { + this.con.disconnect(); + } + + onDisconnect(callback: () => void) { + this.con.onDisconnect.addListener(callback); + } +} diff --git a/packages/message/message_queue.ts b/packages/message/message_queue.ts index 7be383a..ee79151 100644 --- a/packages/message/message_queue.ts +++ b/packages/message/message_queue.ts @@ -1,16 +1,15 @@ import EventEmitter from "eventemitter3"; -import { connect } from "./client"; -import { ApiFunction, Server } from "./server"; +import { ApiFunction, Message, MessageConnect, Server } from "./server"; export type SubscribeCallback = (message: any) => void; export class Broker { - constructor() {} + constructor(private msg: Message) {} // 订阅 - async subscribe(topic: string, handler: SubscribeCallback): Promise { - const con = await connect("messageQueue", { action: "subscribe", topic }); - con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => { + async subscribe(topic: string, handler: SubscribeCallback): Promise { + const con = await this.msg.connect({ action: "messageQueue", data: { action: "subscribe", topic } }); + con.onMessage((msg: { action: string; topic: string; message: any }) => { if (msg.action === "message") { handler(msg.message); } @@ -26,7 +25,7 @@ export class Broker { // 消息队列 export class MessageQueue { - topicConMap: Map = new Map(); + topicConMap: Map = new Map(); private EE: EventEmitter = new EventEmitter(); @@ -44,7 +43,7 @@ export class MessageQueue { } switch (action) { case "subscribe": - this.subscribe(topic, con as chrome.runtime.Port); + this.subscribe(topic, con as MessageConnect); break; case "publish": this.publish(topic, message); @@ -55,14 +54,14 @@ export class MessageQueue { }; } - private subscribe(topic: string, con: chrome.runtime.Port) { + private subscribe(topic: string, con: MessageConnect) { let list = this.topicConMap.get(topic); if (!list) { list = []; this.topicConMap.set(topic, list); } list.push({ name: topic, con }); - con.onDisconnect.addListener(() => { + con.onDisconnect(() => { let list = this.topicConMap.get(topic); // 移除断开连接的con list = list!.filter((item) => item.con !== con); @@ -73,7 +72,7 @@ export class MessageQueue { publish(topic: string, message: any) { const list = this.topicConMap.get(topic); list?.forEach((item) => { - item.con.postMessage({ action: "message", topic, message }); + item.con.sendMessage({ action: "message", topic, message }); }); this.EE.emit(topic, message); } diff --git a/packages/message/server.ts b/packages/message/server.ts index 78ad6c6..d4d378f 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -1,19 +1,36 @@ -export type ApiFunction = (params: any, con: chrome.runtime.Port | chrome.runtime.MessageSender) => any; +export interface Message { + onConnect(callback: (data: any, con: MessageConnect) => void): void; + onMessage(callback: (data: any, sendResponse: (data: any) => void) => void): void; + connect(data: any): Promise; + sendMessage(data: any): Promise; +} + +export interface MessageConnect { + onMessage(callback: (data: any) => void): void; + sendMessage(data: any): void; + disconnect(): void; + onDisconnect(callback: () => void): void; +} + +export type MessageSender = { + tabId: number; +}; + +export type ApiFunction = (params: any, con: MessageConnect | null) => any; export class Server { private apiFunctionMap: Map = new Map(); - constructor(private env: string) { - chrome.runtime.onConnect.addListener((port) => { - const handler = (msg: { action: string; data: any }) => { - port.onMessage.removeListener(handler); - this.connectHandle(msg.action, msg.data, port); - }; - port.onMessage.addListener(handler); + constructor( + private env: string, + message: Message + ) { + message.onConnect((msg: any, con: MessageConnect) => { + this.connectHandle(msg.action, msg.data, con); }); - chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - this.messageHandle(msg.action, msg.data, sender, sendResponse); + message.onMessage((msg, sendResponse) => { + return this.messageHandle(msg.action, msg.data, sendResponse); }); } @@ -25,24 +42,26 @@ export class Server { this.apiFunctionMap.set(name, func); } - private connectHandle(msg: string, params: any, con: chrome.runtime.Port) { + private connectHandle(msg: string, params: any, con: MessageConnect) { const func = this.apiFunctionMap.get(msg); if (func) { func(params, con); } } - private messageHandle( - msg: string, - params: any, - sender: chrome.runtime.MessageSender, - sendResponse: (response: any) => void - ) { + private messageHandle(msg: string, params: any, sendResponse: (response: any) => void) { const func = this.apiFunctionMap.get(msg); if (func) { try { - const ret = func(params, sender); - sendResponse({ code: 0, data: ret }); + const ret = func(params, null); + if (ret instanceof Promise) { + ret.then((data) => { + sendResponse({ code: 0, data }); + }); + return true; + } else { + sendResponse({ code: 0, data: ret }); + } } catch (e: any) { sendResponse({ code: -1, message: e.message }); } diff --git a/packages/message/window_message.ts b/packages/message/window_message.ts index 4f57c82..85b16eb 100644 --- a/packages/message/window_message.ts +++ b/packages/message/window_message.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from "uuid"; +import { Message, MessageConnect } from "./server"; // 通过 window.postMessage/onmessage 实现通信 @@ -7,11 +8,11 @@ import EventEmitter from "eventemitter3"; // 消息体 export type WindowMessageBody = { messageId: string; // 消息id - type: "sendMessage" | "respMessage" | "connect"; // 消息类型 + type: "sendMessage" | "respMessage" | "connect" | "disconnect" | "connectMessage"; // 消息类型 data: any; // 消息数据 }; -export class WindowMessage { +export class WindowMessage implements Message { EE: EventEmitter = new EventEmitter(); // source: Window 消息来源 @@ -22,7 +23,7 @@ export class WindowMessage { ) { // 监听消息 this.source.addEventListener("message", (e) => { - if (e.source === this.target) { + if (e.source === this.target || e.source === this.source) { this.messageHandle(e.data); } }); @@ -47,22 +48,22 @@ export class WindowMessage { } else if (data.type === "connect") { this.EE.emit("connect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); } else if (data.type === "disconnect") { - this.EE.emit("disconnect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); + this.EE.emit("disconnect:" + data.messageId); } else if (data.type === "connectMessage") { - this.EE.emit("connectMessage", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target)); + this.EE.emit("connectMessage:" + data.messageId, data.data); } } - onConnect(callback: (data: any, con: WindowMessageConnect) => void) { + onConnect(callback: (data: any, con: MessageConnect) => void) { this.EE.addListener("connect", callback); } - connect(action: string, data?: any): Promise { + connect(data: any): Promise { return new Promise((resolve) => { const body: WindowMessageBody = { messageId: uuidv4(), type: "connect", - data: { action, data }, + data, }; this.target.postMessage(body, "*"); resolve(new WindowMessageConnect(body.messageId, this.EE, this.target)); @@ -73,12 +74,13 @@ export class WindowMessage { this.EE.addListener("message", callback); } - sendMessage(action: string, data?: any): Promise { + // 发送消息 注意不进行回调的内存泄漏 + sendMessage(data: any): Promise { return new Promise((resolve) => { const body: WindowMessageBody = { messageId: uuidv4(), type: "sendMessage", - data: { action, data }, + data, }; const callback = (body: WindowMessageBody) => { this.EE.removeListener("response:" + body.messageId, callback); @@ -90,10 +92,42 @@ export class WindowMessage { } } -export class WindowMessageConnect { +export class WindowMessageConnect implements MessageConnect { constructor( private messageId: string, private EE: EventEmitter, private target: Window - ) {} + ) { + this.onDisconnect(() => { + // 移除所有监听 + this.EE.removeAllListeners("connectMessage:" + this.messageId); + this.EE.removeAllListeners("disconnect:" + this.messageId); + }); + } + + sendMessage(data: any) { + const body: WindowMessageBody = { + messageId: this.messageId, + type: "connectMessage", + data, + }; + this.target.postMessage(body, "*"); + } + + onMessage(callback: (data: any) => void) { + this.EE.addListener("connectMessage:" + this.messageId, callback); + } + + disconnect() { + const body: WindowMessageBody = { + messageId: this.messageId, + type: "disconnect", + data: null, + }; + this.target.postMessage(body); + } + + onDisconnect(callback: () => void) { + this.EE.addListener("disconnect:" + this.messageId, callback); + } } diff --git a/src/app/logger/message_writer.ts b/src/app/logger/message_writer.ts index 00c28d6..1b75bf5 100644 --- a/src/app/logger/message_writer.ts +++ b/src/app/logger/message_writer.ts @@ -1,29 +1,24 @@ -import MessageCenter from "../message/center"; -import { MessageManager } from "../message/message"; -import { Logger, LoggerDAO } from "../repo/logger"; +import { WindowMessage } from "@Packages/message/window_message"; import { LogLabel, LogLevel, Writer } from "./core"; // 通过通讯机制写入日志 export default class MessageWriter implements Writer { - connect: MessageManager; + connect: WindowMessage; - constructor(connect: MessageManager) { + constructor(connect: WindowMessage) { this.connect = connect; } write(level: LogLevel, message: string, label: LogLabel): void { - this.connect.send("log", { - id: 0, - level, - message, - label, - createtime: new Date().getTime(), + this.connect.sendMessage({ + action: "logger", + data: { + id: 0, + level, + message, + label, + createtime: new Date().getTime(), + }, }); } } - -export function ListenerMessage(db: LoggerDAO, connect: MessageCenter) { - connect.setHandler("log", (action, data: Logger) => { - db.save(data); - }); -} diff --git a/src/app/service/offscreen/client.ts b/src/app/service/offscreen/client.ts new file mode 100644 index 0000000..538843a --- /dev/null +++ b/src/app/service/offscreen/client.ts @@ -0,0 +1,6 @@ +import { WindowMessage } from "@Packages/message/window_message"; +import { sendMessage } from "../utils"; + +export function preparationSandbox(msg: WindowMessage) { + return sendMessage(msg, "preparationSandbox"); +} diff --git a/src/app/service/offscreen/index.ts b/src/app/service/offscreen/index.ts index 50a50b1..6fb35c0 100644 --- a/src/app/service/offscreen/index.ts +++ b/src/app/service/offscreen/index.ts @@ -1,17 +1,42 @@ import { Server } from "@Packages/message/server"; import { ScriptService } from "./script"; -import { MessageQueue } from "@Packages/message/message_queue"; +import { Broker, MessageQueue } from "@Packages/message/message_queue"; +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"; // offscreen环境的管理器 export class OffscreenManager { - private api: Server = new Server("offscreen"); + private extensionMessage = new ExtensionMessage(); + + private api: Server = new Server("offscreen", this.extensionMessage); + + private windowMessage = new WindowMessage(window, sandbox); + + private windowApi: Server = new Server("offscreen-window", this.windowMessage); private mq: MessageQueue = new MessageQueue(this.api); + private broker: Broker = new Broker(this.extensionMessage); + + logger(data: Logger) { + const dao = new LoggerDAO(); + dao.save(data); + } + + preparationSandbox() { + // 通知初始化好环境了 + const serviceWorker = new ServiceWorkerClient(); + serviceWorker.preparationOffscreen(); + } + initManager() { // 监听消息 - const group = this.api.group("serviceWorker"); - const script = new ScriptService(group.group("script"), this.mq); + const group = this.api.group("offscreen"); + this.windowApi.on("logger", this.logger.bind(this)); + this.windowApi.on("preparationSandbox", this.preparationSandbox.bind(this)); + const script = new ScriptService(group.group("script"), this.mq, this.windowMessage, this.broker); script.init(); } } diff --git a/src/app/service/offscreen/script.ts b/src/app/service/offscreen/script.ts index 846772c..43b1937 100644 --- a/src/app/service/offscreen/script.ts +++ b/src/app/service/offscreen/script.ts @@ -1,20 +1,37 @@ import LoggerCore from "@App/app/logger/core"; import Logger from "@App/app/logger/logger"; -import { MessageQueue } from "@Packages/message/message_queue"; +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 { disableScript, enableScript } from "../sandbox/client"; export class ScriptService { logger: Logger; constructor( private group: Group, - private mq: MessageQueue + private mq: MessageQueue, + private windowMessage: WindowMessage, + private broker: Broker ) { this.logger = LoggerCore.logger().with({ service: "script" }); } - init() { - // 初始化, 执行所有的后台脚本, 设置定时脚本计时器 - + async init() { + subscribeScriptEnable(this.broker, async (data) => { + const info = await new ScriptClient().info(data.uuid); + if (info.type === SCRIPT_TYPE_NORMAL) { + return; + } + if (data.enable) { + // 发送给沙盒运行 + enableScript(this.windowMessage, info); + } else { + // 发送给沙盒停止 + disableScript(this.windowMessage, info); + } + }); } } diff --git a/src/app/service/sandbox/client.ts b/src/app/service/sandbox/client.ts new file mode 100644 index 0000000..7c18af4 --- /dev/null +++ b/src/app/service/sandbox/client.ts @@ -0,0 +1,11 @@ +import { Script } from "@App/app/repo/scripts"; +import { WindowMessage } from "@Packages/message/window_message"; +import { sendMessage } from "../utils"; + +export function enableScript(msg: WindowMessage, data: Script) { + return sendMessage(msg, "enableScript", data); +} + +export function disableScript(msg: WindowMessage, data: Script) { + return sendMessage(msg, "disableScript", data); +} diff --git a/src/app/service/sandbox/index.ts b/src/app/service/sandbox/index.ts new file mode 100644 index 0000000..b084b95 --- /dev/null +++ b/src/app/service/sandbox/index.ts @@ -0,0 +1,29 @@ +import { Server } from "@Packages/message/server"; +import { WindowMessage } from "@Packages/message/window_message"; +import { preparationSandbox } from "../offscreen/client"; +import { Script, SCRIPT_TYPE_BACKGROUND } from "@App/app/repo/scripts"; + +// sandbox环境的管理器 +export class SandboxManager { + api: Server = new Server("sandbox", this.windowMessage); + + constructor(private windowMessage: WindowMessage) {} + + enableScript(data: Script) { + // 开启脚本, 判断脚本是后台脚本还是定时脚本 + if(data.type === SCRIPT_TYPE_BACKGROUND) { + // 后台脚本直接运行起来 + }else{ + // 定时脚本加入定时任务 + } + eval("console.log('hello')"); + console.log("enableScript", data); + } + + initManager() { + this.api.on("enableScript", this.enableScript.bind(this)); + + // 通知初始化好环境了 + preparationSandbox(this.windowMessage); + } +} diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index c31e063..4c5408f 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -3,6 +3,16 @@ import { Client } from "@Packages/message/client"; import { InstallSource } from "."; import { Broker } from "@Packages/message/message_queue"; +export class ServiceWorkerClient extends Client { + constructor() { + super("serviceWorker"); + } + + preparationOffscreen() { + return this.do("preparationOffscreen"); + } +} + export class ScriptClient extends Client { constructor() { super("serviceWorker/script"); @@ -24,6 +34,10 @@ export class ScriptClient extends Client { enable(uuid: string, enable: boolean) { return this.do("enable", { uuid, enable }); } + + info(uuid: string): Promise diff --git a/src/pages/options/main.tsx b/src/pages/options/main.tsx index 536b8ba..d79e422 100644 --- a/src/pages/options/main.tsx +++ b/src/pages/options/main.tsx @@ -1,27 +1,18 @@ import React from "react"; import ReactDOM from "react-dom/client"; import MainLayout from "../components/layout/MainLayout.tsx"; +import Sider from "../components/layout/Sider.tsx"; +import { Provider } from "react-redux"; +import { store } from "@App/store/store.ts"; import "@arco-design/web-react/dist/css/arco.css"; import "@App/locales/locales"; import "@App/index.css"; import "./index.css"; -import { Provider } from "react-redux"; -import { store } from "@App/store/store.ts"; -import { Broker } from "@Packages/message/message_queue.ts"; -import Sider from "../components/layout/Sider.tsx"; - -// // 测试监听广播 - -// const border = new Broker(); - -// border.subscribe("installScript", (message) => { -// console.log(message); -// }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + diff --git a/src/pages/options/routes/ScriptList.tsx b/src/pages/options/routes/ScriptList.tsx index 7acc28a..b360ccc 100644 --- a/src/pages/options/routes/ScriptList.tsx +++ b/src/pages/options/routes/ScriptList.tsx @@ -83,6 +83,8 @@ import { import { selectScriptListColumnWidth } from "@App/store/features/setting"; import { Broker } from "@Packages/message/message_queue"; import { subscribeScriptDelete, subscribeScriptInstall } from "@App/app/service/service_worker/client"; +import { ExtensionMessage } from "@Packages/message/extension_message"; +import { MessageConnect } from "@Packages/message/server"; type ListType = Script & { loading?: boolean }; @@ -109,8 +111,9 @@ function ScriptList() { useEffect(() => { dispatch(fetchAndSortScriptList()); // 监听脚本安装/运行 - const border = new Broker(); - const subCon: chrome.runtime.Port[] = []; + const msg = new ExtensionMessage(); + const border = new Broker(msg); + const subCon: MessageConnect[] = []; subscribeScriptInstall(border, (message) => { dispatch(upsertScript(message.script)); diff --git a/src/sandbox.ts b/src/sandbox.ts index 01bd09d..f3fcffb 100644 --- a/src/sandbox.ts +++ b/src/sandbox.ts @@ -1,22 +1,22 @@ +import { WindowMessage } from "@Packages/message/window_message"; import LoggerCore from "./app/logger/core"; -import DBWriter from "./app/logger/db_writer"; import MessageWriter from "./app/logger/message_writer"; -import { LoggerDAO } from "./app/repo/logger"; -import { OffscreenManager } from "./app/service/offscreen"; +import { SandboxManager } from "./app/service/sandbox"; function main() { - // 建立与offscreen页面的连接 - + // 建立与offscreen页面的连接 + const msg = new WindowMessage(window, parent); + // 初始化日志组件 const loggerCore = new LoggerCore({ debug: process.env.NODE_ENV === "development", - writer: new MessageWriter(connectSandbox), + writer: new MessageWriter(msg), labels: { env: "sandbox" }, }); loggerCore.logger().debug("offscreen start"); // 初始化管理器 - const manager = new OffscreenManager(); + const manager = new SandboxManager(msg); manager.initManager(); } diff --git a/src/service_worker.ts b/src/service_worker.ts index a8a3d3f..3adb2a6 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -51,11 +51,11 @@ async function main() { labels: { env: "background" }, }); loggerCore.logger().debug("background start"); - // 初始化沙盒环境 - await setupOffscreenDocument(); // 初始化管理器 const manager = new ServiceWorkerManager(); manager.initManager(); + // 初始化沙盒环境 + await setupOffscreenDocument(); } main(); diff --git a/src/types/main.d.ts b/src/types/main.d.ts index c576d2a..b271ac0 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -2,3 +2,5 @@ declare module "@App/types/scriptcat.d.ts"; declare module "*.tpl"; declare module "*.json"; declare module "*.yaml"; + +declare let sandbox: Window;