diff --git a/noname/init/index.js b/noname/init/index.js index 78464e3fa..b27b443b4 100644 --- a/noname/init/index.js +++ b/noname/init/index.js @@ -11,6 +11,7 @@ import { promiseErrorHandlerMap } from "../util/browser.js"; import { importCardPack, importCharacterPack, importExtension, importMode } from "./import.js"; import { onload } from "./onload.js"; import { initializeSandboxRealms } from "../util/initRealms.js"; +import { ErrorManager } from "../util/error.js"; // 判断是否从file协议切换到http/s协议 export function canUseHttpProtocol() { @@ -96,7 +97,7 @@ export function sendUpdate() { const cp = require("child_process"); cp.exec( `start /min ${__dirname}\\noname-server.exe -platform=electron`, - (err, stdout, stderr) => {} + (err, stdout, stderr) => { } ); return `http://localhost:8089/app.html?sendUpdate=true`; } @@ -664,7 +665,7 @@ export async function boot() { if (isFirstStartAfterUpdate && extErrorList.length) { const stacktraces = extErrorList.map(e => e instanceof Error ? e.stack : String(e)).join("\n\n") // game.saveConfig("update_first_log", stacktraces); - if(confirm(`扩展加载出错!是否重新载入游戏?\n本次更新可能导致了扩展出现了错误:\n\n${stacktraces}`)){ + if (confirm(`扩展加载出错!是否重新载入游戏?\n本次更新可能导致了扩展出现了错误:\n\n${stacktraces}`)) { game.reload(); clearTimeout(resetGameTimeout); return; @@ -1021,137 +1022,141 @@ async function setOnError() { window.onerror = function (msg, src, line, column, err) { if (promiseErrorHandler.onErrorPrepare) promiseErrorHandler.onErrorPrepare(); - const winPath = window.__dirname - ? "file:///" + (__dirname.replace(new RegExp("\\\\", "g"), "/") + "/") - : ""; - let str = `错误文件: ${ - typeof src == "string" - ? decodeURI(src).replace(lib.assetURL, "").replace(winPath, "") - : "未知文件" - }`; - str += `\n错误信息: ${msg}`; - const tip = lib.getErrorTip(msg); - if (tip) str += `\n错误提示: ${tip}`; - str += `\n行号: ${line}`; - str += `\n列号: ${column}`; - const version = typeof lib.version != "undefined" ? lib.version : ""; - const reg = /[^\d.]/; - const match = version.match(reg) != null; - str += "\n" + `${match ? "游戏" : "无名杀"}版本: ${version || "未知版本"}`; - if (match) - str += - "\n⚠️您使用的游戏代码不是源于libccy/noname无名杀官方仓库,请自行寻找您所使用的游戏版本开发者反馈!"; - if (_status && _status.event) { - let evt = _status.event; - str += `\nevent.name: ${evt.name}\nevent.step: ${evt.step}`; - // @ts-ignore - if (evt.parent) - str += `\nevent.parent.name: ${evt.parent.name}\nevent.parent.step: ${evt.parent.step}`; - // @ts-ignore - if (evt.parent && evt.parent.parent) - str += `\nevent.parent.parent.name: ${evt.parent.parent.name}\nevent.parent.parent.step: ${evt.parent.parent.step}`; - if (evt.player || evt.target || evt.source || evt.skill || evt.card) { - str += "\n-------------"; - } - if (evt.player) { - if (lib.translate[evt.player.name]) - str += `\nplayer: ${lib.translate[evt.player.name]}[${evt.player.name}]`; - else str += "\nplayer: " + evt.player.name; - let distance = get.distance(_status.roundStart, evt.player, "absolute"); - if (distance != Infinity) { - str += `\n座位号: ${distance + 1}`; + const errorReporter = ErrorManager.getErrorReporter(err); + if (errorReporter) game.print(errorReporter.report("沙盒内部执行的代码出现错误")); + else { + const winPath = window.__dirname + ? "file:///" + (__dirname.replace(new RegExp("\\\\", "g"), "/") + "/") + : ""; + let str = `错误文件: ${ + typeof src == "string" + ? decodeURI(src).replace(lib.assetURL, "").replace(winPath, "") + : "未知文件" + }`; + str += `\n错误信息: ${msg}`; + const tip = lib.getErrorTip(msg); + if (tip) str += `\n错误提示: ${tip}`; + str += `\n行号: ${line}`; + str += `\n列号: ${column}`; + const version = typeof lib.version != "undefined" ? lib.version : ""; + const reg = /[^\d.]/; + const match = version.match(reg) != null; + str += "\n" + `${match ? "游戏" : "无名杀"}版本: ${version || "未知版本"}`; + if (match) + str += + "\n⚠️您使用的游戏代码不是源于libccy/noname无名杀官方仓库,请自行寻找您所使用的游戏版本开发者反馈!"; + if (_status && _status.event) { + let evt = _status.event; + str += `\nevent.name: ${evt.name}\nevent.step: ${evt.step}`; + // @ts-ignore + if (evt.parent) + str += `\nevent.parent.name: ${evt.parent.name}\nevent.parent.step: ${evt.parent.step}`; + // @ts-ignore + if (evt.parent && evt.parent.parent) + str += `\nevent.parent.parent.name: ${evt.parent.parent.name}\nevent.parent.parent.step: ${evt.parent.parent.step}`; + if (evt.player || evt.target || evt.source || evt.skill || evt.card) { + str += "\n-------------"; + } + if (evt.player) { + if (lib.translate[evt.player.name]) + str += `\nplayer: ${lib.translate[evt.player.name]}[${evt.player.name}]`; + else str += "\nplayer: " + evt.player.name; + let distance = get.distance(_status.roundStart, evt.player, "absolute"); + if (distance != Infinity) { + str += `\n座位号: ${distance + 1}`; + } + } + if (evt.target) { + if (lib.translate[evt.target.name]) + str += `\ntarget: ${lib.translate[evt.target.name]}[${evt.target.name}]`; + else str += "\ntarget: " + evt.target.name; + } + if (evt.source) { + if (lib.translate[evt.source.name]) + str += `\nsource: ${lib.translate[evt.source.name]}[${evt.source.name}]`; + else str += "\nsource: " + evt.source.name; + } + if (evt.skill) { + if (lib.translate[evt.skill]) str += `\nskill: ${lib.translate[evt.skill]}[${evt.skill}]`; + else str += "\nskill: " + evt.skill; + } + if (evt.card) { + if (lib.translate[evt.card.name]) + str += `\ncard: ${lib.translate[evt.card.name]}[${evt.card.name}]`; + else str += "\ncard: " + evt.card.name; } } - if (evt.target) { - if (lib.translate[evt.target.name]) - str += `\ntarget: ${lib.translate[evt.target.name]}[${evt.target.name}]`; - else str += "\ntarget: " + evt.target.name; - } - if (evt.source) { - if (lib.translate[evt.source.name]) - str += `\nsource: ${lib.translate[evt.source.name]}[${evt.source.name}]`; - else str += "\nsource: " + evt.source.name; - } - if (evt.skill) { - if (lib.translate[evt.skill]) str += `\nskill: ${lib.translate[evt.skill]}[${evt.skill}]`; - else str += "\nskill: " + evt.skill; - } - if (evt.card) { - if (lib.translate[evt.card.name]) - str += `\ncard: ${lib.translate[evt.card.name]}[${evt.card.name}]`; - else str += "\ncard: " + evt.card.name; - } - } - str += "\n-------------"; - if ( - typeof line == "number" && - (typeof Reflect.get(game, "readFile") == "function" || location.origin != "file://") - ) { - const createShowCode = function (lines) { - let showCode = ""; - if (lines.length >= 10) { - if (line > 4) { - for (let i = line - 5; i < line + 6 && i < lines.length; i++) { - showCode += `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${lines[i]}\n`; + str += "\n-------------"; + if ( + typeof line == "number" && + (typeof Reflect.get(game, "readFile") == "function" || location.origin != "file://") + ) { + const createShowCode = function (lines) { + let showCode = ""; + if (lines.length >= 10) { + if (line > 4) { + for (let i = line - 5; i < line + 6 && i < lines.length; i++) { + showCode += `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${lines[i]}\n`; + } + } else { + for (let i = 0; i < line + 6 && i < lines.length; i++) { + showCode += `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${lines[i]}\n`; + } } } else { - for (let i = 0; i < line + 6 && i < lines.length; i++) { - showCode += `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${lines[i]}\n`; + showCode = lines + .map((_line, i) => `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${_line}\n`) + .toString(); + } + return showCode; + }; + //协议名须和html一致(网页端防跨域),且文件是js + if (typeof src == "string" && src.startsWith(location.protocol) && src.endsWith(".js")) { + //获取代码 + const codes = lib.init.reqSync( + "local:" + decodeURI(src).replace(lib.assetURL, "").replace(winPath, "") + ); + if (codes) { + const lines = codes.split("\n"); + str += "\n" + createShowCode(lines); + str += "\n-------------"; + } + } + //解析parsex里的content fun内容(通常是技能content) + // @ts-ignore + else if ( + err && + err.stack && + ["at Object.eval [as content]", "at Proxy.content"].some((str) => { + let stackSplit1 = err.stack.split("\n")[1]; + if (stackSplit1) { + return stackSplit1.trim().startsWith(str); } + return false; + }) + ) { + const codes = _status.event.content; + if (typeof codes == "function") { + const lines = codes.toString().split("\n"); + str += "\n" + createShowCode(lines); + str += "\n-------------"; } - } else { - showCode = lines - .map((_line, i) => `${i + 1}| ${line == i + 1 ? "⚠️" : ""}${_line}\n`) - .toString(); - } - return showCode; - }; - //协议名须和html一致(网页端防跨域),且文件是js - if (typeof src == "string" && src.startsWith(location.protocol) && src.endsWith(".js")) { - //获取代码 - const codes = lib.init.reqSync( - "local:" + decodeURI(src).replace(lib.assetURL, "").replace(winPath, "") - ); - if (codes) { - const lines = codes.split("\n"); - str += "\n" + createShowCode(lines); - str += "\n-------------"; - } - } - //解析parsex里的content fun内容(通常是技能content) - // @ts-ignore - else if ( - err && - err.stack && - ["at Object.eval [as content]", "at Proxy.content"].some((str) => { - let stackSplit1 = err.stack.split("\n")[1]; - if (stackSplit1) { - return stackSplit1.trim().startsWith(str); - } - return false; - }) - ) { - const codes = _status.event.content; - if (typeof codes == "function") { - const lines = codes.toString().split("\n"); - str += "\n" + createShowCode(lines); - str += "\n-------------"; } } + if (err && err.stack) + str += + "\n" + + decodeURI(err.stack) + .replace(new RegExp(lib.assetURL, "g"), "") + .replace(new RegExp(winPath, "g"), ""); + alert(str); + game.print(str); } - if (err && err.stack) - str += - "\n" + - decodeURI(err.stack) - .replace(new RegExp(lib.assetURL, "g"), "") - .replace(new RegExp(winPath, "g"), ""); - alert(str); Reflect.set(window, "ea", Array.from(arguments)); Reflect.set(window, "em", msg); Reflect.set(window, "el", line); Reflect.set(window, "ec", column); Reflect.set(window, "eo", err); - game.print(str); if (promiseErrorHandler.onErrorFinish) promiseErrorHandler.onErrorFinish(); // @ts-ignore if (!lib.config.errstop && (_status && _status.event && !(_status.event.content instanceof AsyncFunction))) { diff --git a/noname/util/error.js b/noname/util/error.js new file mode 100644 index 000000000..bf4b617d5 --- /dev/null +++ b/noname/util/error.js @@ -0,0 +1,244 @@ +class CodeSnippet { + /** @type {Array} */ + static #snippetStack = []; + + /** @type {string} */ + #code; + /** @type {number} */ + #erroff; + + /** + * ```plain + * 构造一个代码片段对象 + * + * 通过 `erroff` 指定在发生错误时,错误信息指出的行与实际代码行的偏移量 + * ``` + * @param {string} code + * @param {number} erroff + */ + constructor(code, erroff = 0) { + this.#code = String(code); + this.#erroff = parseInt(String(erroff)) || 0; + } + + /** @type {string} */ + get code() { + return this.#code; + } + + /** @type {Array} */ + get lines() { + return this.code.split(/\r?\n/); + } + + /** + * ```plain + * 给定错误行号来获取错误代码片段 + * ``` + * + * @param {number} lineno + * @returns {string} + */ + viewCode(lineno) { + if (!Number.isInteger(lineno)) + throw new TypeError("错误行号必须是一个整数"); + + const index = lineno - this.#erroff; + const lines = this.lines; + const width = String(index + 4).length; + + let codeView = ""; + + for (let i = index - 4; i < index + 5; i++) { + if (i < 0 || i >= lines.length) + continue; + + codeView += String(i + 1).padStart(width, "0"); + codeView += `|${i == index ? "⚠️" : " "}${lines[i]}\n`; + } + + return codeView; + } + + /** + * ```plain + * 获取当前代码片段 + * ``` + * + * @type {CodeSnippet} + */ + static get currentSnippet() { + if (!this.#snippetStack.length) + throw new Error("代码片段栈为空"); + + return this.#snippetStack[this.#snippetStack.length - 1]; + } + + /** + * ```plain + * 压入一个代码片段作为当前代码片段 + * ``` + * + * @param {CodeSnippet} snippet + */ + static pushSnippet(snippet) { + if (!(snippet instanceof CodeSnippet)) + throw new TypeError("参数必须是一个代码片段对象"); + + this.#snippetStack.push(snippet); + } + + /** + * ```plain + * 弹出当前代码片段 + * ``` + * + * @returns {CodeSnippet} + */ + static popSnippet() { + if (!this.#snippetStack.length) + throw new Error("代码片段栈为空"); + + // @ts-ignore // eslint好不智能哦 + return this.#snippetStack.pop(); + } +} + +class ErrorReporter { + static #topAlert = window.alert.bind(null); + static #errorLineNoPatterns = [ + /:(\d+):\d+\)/, + /at :(\d+):\d+/, + /eval:(\d+):\d+/, + /Function:(\d+):\d+/, + /:(\d+):\d+/, + ]; + + /** @type {CodeSnippet} */ + #snippet; + /** @type {string} */ + #message; + /** @type {string} */ + #stack; + + /** + * ```plain + * 构造一个错误报告对象 + * 以此来保存错误相关信息 + * ``` + * + * @param {Error} error + * @param {CodeSnippet} snippet + */ + constructor(error, snippet = CodeSnippet.currentSnippet) { + if (!("stack" in error)) + throw new TypeError("传入的对象不是一个错误对象"); + + this.#snippet = snippet; + this.#message = String(error); + this.#stack = String(error.stack); + } + + get message() { + return this.#message; + } + + get stack() { + return this.#stack; + } + + static #findLineNo = function (line) { + for (const pattern of ErrorReporter.#errorLineNoPatterns) { + const match = pattern.exec(line); + + if (match) + return parseInt(match[1]); + } + + return NaN; + } + + viewCode() { + const stack = this.#stack; + const line = stack.split("\n")[1]; + const lineno = ErrorReporter.#findLineNo(line); + + if (!isNaN(lineno)) + return this.#snippet.viewCode(lineno); + + return null; + } + + /** + * ```plain + * 向用户报告错误信息 + * ``` + * + * @param {string} title + * @returns {string} + */ + report(title) { + const codeView = this.viewCode() || "#没有代码预览#"; + let errorInfo = `${title}:\n\t${this.#message}\n`; + errorInfo += `----------\n${codeView.trim()}\n`; + errorInfo += `----------\n调用堆栈:\n${this.#stack}`; + ErrorReporter.#topAlert(errorInfo); + return errorInfo; + } + + /** + * ```plain + * 向用户报告错误信息 + * ``` + * + * @param {Error} error + * @param {string} title + */ + static reportError(error, title = "发生错误") { + new ErrorReporter(error).report(title); + } +} + +class ErrorManager { + /** @type {WeakMap} */ + static #errorReporters = new WeakMap(); + + /** + * ```plain + * 设置错误报告器 + * + * 在报告错误时可以从此处获取错误报告器来直接报告错误 + * ``` + * + * @param {Object} obj + * @param {ErrorReporter?} reporter + */ + static setErrorReporter(obj, reporter = null) { + if (obj !== Object(obj)) + throw new TypeError("参数必须是一个对象"); + if (!(reporter instanceof ErrorReporter)) + reporter = new ErrorReporter(obj); + if (ErrorManager.#errorReporters.has(obj)) + throw new Error("对象已存在错误报告器"); + + ErrorManager.#errorReporters.set(obj, reporter); + } + + /** + * ```plain + * 获取设置的错误报告器 + * ``` + * + * @param {Object} obj + * @returns {ErrorReporter?} + */ + static getErrorReporter(obj) { + return ErrorManager.#errorReporters.get(obj) || null; + } +} + +export { + CodeSnippet, + ErrorReporter, + ErrorManager, +}; \ No newline at end of file diff --git a/noname/util/initRealms.js b/noname/util/initRealms.js index 1701c176f..bfb5504c8 100644 --- a/noname/util/initRealms.js +++ b/noname/util/initRealms.js @@ -1,3 +1,5 @@ +import { CodeSnippet, ErrorReporter, ErrorManager } from "./error.js"; + // 方便开关确定沙盒的问题喵 // 当此处为true、debug模式为启用、设备非苹果时,沙盒生效 let SANDBOX_ENABLED = true; @@ -77,7 +79,7 @@ async function initializeSandboxRealms(enabled) { }, }); - // 传递顶级变量域、上下文执行器 + // 传递顶级变量域、上下文执行器、错误管理器 // @ts-ignore iframe.contentWindow.replacedGlobal = window; // @ts-ignore @@ -86,6 +88,8 @@ async function initializeSandboxRealms(enabled) { iframe.contentWindow.replacedCI2 = ContextInvoker2; // @ts-ignore iframe.contentWindow.replacedCIC = ContextInvokerCreator; + // @ts-ignore + iframe.contentWindow.replacedErrors = { CodeSnippet, ErrorReporter, ErrorManager }; // 重新以新的变量域载入当前脚本 const script = iframe.contentWindow.document.createElement("script"); diff --git a/noname/util/sandbox.js b/noname/util/sandbox.js index 148259267..d6ef55ee7 100644 --- a/noname/util/sandbox.js +++ b/noname/util/sandbox.js @@ -41,6 +41,13 @@ const SandboxSignal_ListMonitor = Symbol("ListMonitor"); const SandboxSignal_ExposeInfo = Symbol("ExposeInfo"); const SandboxSignal_TryFunctionRefs = Symbol("TryFunctionRefs"); +/** @type {typeof import("./error.js").CodeSnippet} */ +let CodeSnippet; +/** @type {typeof import("./error.js").ErrorReporter} */ +let ErrorReporter; +/** @type {typeof import("./error.js").ErrorManager} */ +let ErrorManager; + // 用于适配 < Chrome 84 的设备 const WeakRef = window.WeakRef || class WeakRef { /** @@ -2306,12 +2313,25 @@ class Marshal { if (mappedCtor) { const newError = new mappedCtor(); - const stack = String(target.stack); + const stack = (function () { + try { + return String(target.stack); + } catch (e) { + return "无法获取错误"; + } + })(); + Reflect.defineProperty(newError, 'stack', { get: () => () => stack, set: () => { }, configurable: false, }); + + // 继承原本的错误信息 + const errorReporter = ErrorManager.getErrorReporter(target); + if (errorReporter) + ErrorManager.setErrorReporter(newError, errorReporter); + return newError; } } @@ -2866,7 +2886,7 @@ class Domain { * 检查对象是否来自于当前的运行域 * ``` * - * @param {Object} obj + * @param {Object?} obj * @returns {boolean} */ isFrom(obj) { @@ -2885,7 +2905,7 @@ class Domain { * 检查对象是否来自于当前的运行域的Promise * ``` * - * @param {Promise} promise + * @param {Promise?} promise * @returns {boolean} */ isPromise(promise) { @@ -2902,7 +2922,7 @@ class Domain { * 检查对象是否来自于当前的运行域的Error * ``` * - * @param {Error} error + * @param {Error?} error * @returns {boolean} */ isError(error) { @@ -2919,7 +2939,7 @@ class Domain { * 检查对象是否来自于当前的运行域的危险对象 * ``` * - * @param {Object} obj + * @param {Object?} obj * @returns {boolean} */ isUnsafe(obj) { @@ -3582,6 +3602,7 @@ class Sandbox { let wrappedEval; const raw = new thiz.#domainFunction("_", `with(_){with(window){with(${contextName}){return(${applyName}(function(${parameters}){"use strict";\n// 沙盒代码起始\n${code}\n// 沙盒代码结束\n},${contextName}.this,${argsName}))}}}`); + const snippet = new CodeSnippet(code, 5); // 错误信息的行号从 5 开始 (即错误信息的前 5 行是不属于 `code` 的范围) const domain = thiz.#domain; const domainWindow = thiz.#domainWindow; @@ -3637,6 +3658,8 @@ class Sandbox { // 指定执行域 // 方便后续新的函数来继承 Sandbox.#executingScope.push(scope); + // 指定当前的代码片段 + CodeSnippet.pushSnippet(snippet); try { // 传递 `this`、以及函数参数 @@ -3652,27 +3675,17 @@ class Sandbox { // 封送返回结果 return Marshal[SandboxExposer2] (SandboxSignal_Marshal, result, prevDomain); - // } catch (e) { - // // 立即报告错误 - // const window = Domain.topDomain[SandboxExposer](SandboxSignal_GetWindow); - // // @ts-ignore - // const stack = String(e.stack); - // const line = stack.split("\n")[1]; - // const match = /:(\d+):\d+\)/.exec(line); - // if (match) { - // const index = parseInt(match[1]) - 5; - // const lines = code.split("\n"); - // let codeView = ""; - // for (let i = index - 4; i < index + 5; i++) { - // if (i < 0 || i >= lines.length) - // continue; - // codeView += `${i + 1}|${i == index ? "⚠️" : " "}${lines[i]}\n`; - // } - // // @ts-ignore - // window.alert(`Sandbox内执行的代码出现错误:\n${stack}\n----------\n${codeView}\n----------`); - // } - // throw e; // 不再向上抛出异常 + } catch (e) { + // @ts-ignore + if (!domain.isError(e)) + throw e; // 非错误对象无法读取堆栈,继续向上抛出 + + // 保存当前错误信息 + // 这样无论几次重抛都可以复现最原始的错误信息 + ErrorManager.setErrorReporter(e); + throw e; // 继续向上抛出(由于JS不支持rethrow只能这样喵) } finally { + CodeSnippet.popSnippet(); Sandbox.#executingScope.pop(); } }; @@ -3974,6 +3987,16 @@ if (SANDBOX_ENABLED) { // 改为此处初始化,防止多次初始化 Domain[SandboxExposer2](SandboxSignal_InitDomain); + // 获取顶级域的错误管理器 + // @ts-ignore + ({ + CodeSnippet, + ErrorReporter, + ErrorManager, + } + // @ts-ignore + = window.replacedErrors); + // 向顶级运行域暴露导出 // @ts-ignore window.SANDBOX_EXPORT = {