Merge pull request #1447 from IceCola97/sandbox-dev

增加沙盒内报错显示,方便调试;修复Audio|Image未定义问题
This commit is contained in:
Spmario233 2024-06-09 13:16:50 +08:00 committed by GitHub
commit fbc72d91fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 514 additions and 153 deletions

View File

@ -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,6 +1022,9 @@ async function setOnError() {
window.onerror = function (msg, src, line, column, err) {
if (promiseErrorHandler.onErrorPrepare) promiseErrorHandler.onErrorPrepare();
const errorReporter = ErrorManager.getErrorReporter(err);
if (errorReporter) game.print(errorReporter.report("沙盒内部执行的代码出现错误"));
else {
const winPath = window.__dirname
? "file:///" + (__dirname.replace(new RegExp("\\\\", "g"), "/") + "/")
: "";
@ -1146,12 +1150,13 @@ async function setOnError() {
.replace(new RegExp(lib.assetURL, "g"), "")
.replace(new RegExp(winPath, "g"), "");
alert(str);
game.print(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))) {

245
noname/util/error.js Normal file
View File

@ -0,0 +1,245 @@
class CodeSnippet {
/** @type {Array<CodeSnippet>} */
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<string>} */
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)
return null;
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 = [
/<anonymous>:(\d+):\d+\)/,
/at <anonymous>:(\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() {
if(!this.#snippet)
return null;
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<Object, ErrorReporter>} */
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);
ErrorManager.#errorReporters.set(obj, reporter);
}
/**
* ```plain
* 获取设置的错误报告器
* ```
*
* @param {Object} obj
* @returns {ErrorReporter?}
*/
static getErrorReporter(obj) {
return ErrorManager.#errorReporters.get(obj) || null;
}
}
export {
CodeSnippet,
ErrorReporter,
ErrorManager,
};

View File

@ -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");
@ -107,6 +111,8 @@ async function initializeSandboxRealms(enabled) {
delete iframe.contentWindow.replacedCI2;
// @ts-ignore
delete iframe.contentWindow.replacedCIC;
// @ts-ignore
delete iframe.contentWindow.replacedErrors;
// @ts-ignore
Object.assign(SANDBOX_EXPORT, iframe.contentWindow.SANDBOX_EXPORT);

View File

@ -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 {
/**
@ -2240,8 +2247,7 @@ class Marshal {
if (descriptor
&& descriptor.value
&& !descriptor.enumerable
&& !descriptor.configurable
&& descriptor.value.constructor === src)
&& !descriptor.configurable)
cloned = function () { };
else
cloned = () => { };
@ -2306,12 +2312,39 @@ class Marshal {
if (mappedCtor) {
const newError = new mappedCtor();
const stack = String(target.stack);
Reflect.defineProperty(newError, 'stack', {
get: () => () => stack,
const silentAccess = (o, p, d) => {
try {
if (typeof p == "function")
return p(o);
else
return o[p];
} catch (e) {
return d;
}
};
const pinValue = (o, p, v) => {
Reflect.defineProperty(o, p, {
get: () => v,
set: () => { },
configurable: false,
});
};
const name = String(silentAccess(target, "name", "#无法获取错误名#"));
const message = String(silentAccess(target, "message", "#无法获取错误消息#"));
const stack = String(silentAccess(target, "stack", "#无法获取调用栈#"));
const string = silentAccess(target, String, "#无法获取错误信息#");
pinValue(newError, "name", name);
pinValue(newError, "message", message);
pinValue(newError, "stack", stack);
pinValue(newError, "toString", () => string);
// 继承原本的错误信息
const errorReporter = ErrorManager.getErrorReporter(target);
ErrorManager.setErrorReporter(newError,
errorReporter || new ErrorReporter(target)); // 无论有没有都捕获当前的错误信息
return newError;
}
}
@ -2866,7 +2899,7 @@ class Domain {
* 检查对象是否来自于当前的运行域
* ```
*
* @param {Object} obj
* @param {Object?} obj
* @returns {boolean}
*/
isFrom(obj) {
@ -2885,7 +2918,7 @@ class Domain {
* 检查对象是否来自于当前的运行域的Promise
* ```
*
* @param {Promise} promise
* @param {Promise?} promise
* @returns {boolean}
*/
isPromise(promise) {
@ -2902,7 +2935,7 @@ class Domain {
* 检查对象是否来自于当前的运行域的Error
* ```
*
* @param {Error} error
* @param {Error?} error
* @returns {boolean}
*/
isError(error) {
@ -2919,7 +2952,7 @@ class Domain {
* 检查对象是否来自于当前的运行域的危险对象
* ```
*
* @param {Object} obj
* @param {Object?} obj
* @returns {boolean}
*/
isUnsafe(obj) {
@ -3144,6 +3177,10 @@ function trapMarshal(srcDomain, dstDomain, obj) {
* ```
*/
class Sandbox {
// @ts-ignore
static #topWindow = window.replacedGlobal || window;
// @ts-ignore
static #topWindowHTMLElement = (window.replacedGlobal || window).HTMLElement;
/** @type {WeakMap<Domain, Sandbox>} */
static #domainMap = new WeakMap();
/** @type {Array} */
@ -3184,6 +3221,20 @@ class Sandbox {
*/
#freeAccess = false;
/**
* ```plain
* 当在当前scope中访问不到变量时
* 是否允许沙盒代码可以穿透到顶级域的全局变量域中
* 去读取DOM类型的构造函数仅读取
* 包括ImageAudio等
*
* 此开关有风险请谨慎使用
* ```
*
* @type {boolean}
*/
#domAccess = false;
/**
* 创建一个新的沙盒
*/
@ -3421,6 +3472,40 @@ class Sandbox {
this.#freeAccess = !!value;
}
/**
* ```plain
* 当在当前scope中访问不到变量时
* 是否允许沙盒代码可以穿透到顶级域的全局变量域中
* 去读取DOM类型的构造函数仅读取
* 包括ImageAudio等
*
* 此开关有风险请谨慎使用
* ```
*
* @type {boolean}
*/
get domAccess() {
Sandbox.#assertOperator(this);
return this.#domAccess;
}
/**
* ```plain
* 当在当前scope中访问不到变量时
* 是否允许沙盒代码可以穿透到顶级域的全局变量域中
* 去读取DOM类型的构造函数仅读取
* 包括ImageAudio等
*
* 此开关有风险请谨慎使用
* ```
*
* @type {boolean}
*/
set domAccess(value) {
Sandbox.#assertOperator(this);
this.#domAccess = !!value;
}
/**
* ```plain
* 向当前域注入内建对象
@ -3582,6 +3667,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 +3723,8 @@ class Sandbox {
// 指定执行域
// 方便后续新的函数来继承
Sandbox.#executingScope.push(scope);
// 指定当前的代码片段
CodeSnippet.pushSnippet(snippet);
try {
// 传递 `this`、以及函数参数
@ -3652,27 +3740,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 = /<anonymous>:(\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();
}
};
@ -3787,10 +3865,18 @@ class Sandbox {
// 暴露非内建的顶级全局变量
if (thiz.#freeAccess
&& !Globals.isBuiltinKey(p)) {
const topWindow = Domain.topDomain[SandboxExposer](SandboxSignal_GetWindow);
const topWindow = Sandbox.#topWindow;
if (p in topWindow)
return trapMarshal(Domain.topDomain, thiz.#domain, topWindow[p]);
} else if (thiz.#domAccess) {
const topWindow = Sandbox.#topWindow;
const accessTarget = topWindow[p];
if (typeof accessTarget == "function"
&& "prototype" in accessTarget
&& accessTarget.prototype instanceof Sandbox.#topWindowHTMLElement)
return trapMarshal(Domain.topDomain, thiz.#domain, accessTarget);
}
return undefined;
@ -3803,6 +3889,14 @@ class Sandbox {
&& !Globals.isBuiltinKey(p)) {
const topWindow = Domain.topDomain[SandboxExposer](SandboxSignal_GetWindow);
return p in topWindow;
} else if (thiz.#domAccess) {
const topWindow = Sandbox.#topWindow;
const accessTarget = topWindow[p];
if (typeof accessTarget == "function"
&& "prototype" in accessTarget
&& accessTarget.prototype instanceof Sandbox.#topWindowHTMLElement)
return true;
}
return false;
@ -3974,6 +4068,16 @@ if (SANDBOX_ENABLED) {
// 改为此处初始化,防止多次初始化
Domain[SandboxExposer2](SandboxSignal_InitDomain);
// 获取顶级域的错误管理器
// @ts-ignore
({
CodeSnippet,
ErrorReporter,
ErrorManager,
}
// @ts-ignore
= window.replacedErrors);
// 向顶级运行域暴露导出
// @ts-ignore
window.SANDBOX_EXPORT = {

View File

@ -583,6 +583,7 @@ function createSandbox() {
const box = new Sandbox();
box.freeAccess = true;
box.domAccess = true;
box.initBuiltins();
// 向沙盒提供顶级运行域的文档对象