BililiveRecorder/BililiveRecorder.Core/Scripting/UserScriptRunner.cs

227 lines
7.9 KiB
C#
Raw Normal View History

2022-05-11 00:20:57 +08:00
using System;
using System.Threading;
using BililiveRecorder.Core.Config.V3;
using BililiveRecorder.Core.Scripting.Runtime;
using Esprima;
using Esprima.Ast;
using Jint;
2022-05-11 19:07:21 +08:00
using Jint.Native;
2022-05-11 00:20:57 +08:00
using Jint.Native.Function;
using Jint.Native.Object;
2022-05-11 19:07:21 +08:00
using Jint.Runtime.Interop;
2022-05-11 00:20:57 +08:00
using Serilog;
namespace BililiveRecorder.Core.Scripting
{
public class UserScriptRunner
{
2022-05-11 19:07:21 +08:00
private const string RecorderEvents = "recorderEvents";
private static readonly JsValue RecorderEventsString = RecorderEvents;
2022-05-11 00:20:57 +08:00
private static int ExecutionId = 0;
private readonly GlobalConfig config;
private readonly Options jintOptions;
private static readonly Script setupScript;
private string? cachedScriptSource;
private Script? cachedScript;
static UserScriptRunner()
{
setupScript = new JavaScriptParser(@"
globalThis.recorderEvents = {};
2022-05-11 19:07:21 +08:00
", new ParserOptions(@"internalSetup.js")).ParseScript();
2022-05-11 00:20:57 +08:00
}
public UserScriptRunner(GlobalConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.jintOptions = new Options()
.CatchClrExceptions()
.LimitRecursion(100)
.RegexTimeoutInterval(TimeSpan.FromSeconds(2))
.Configure(engine =>
{
engine.Realm.GlobalObject.FastAddProperty("dns", new JintDns(engine), writable: false, enumerable: false, configurable: false);
engine.Realm.GlobalObject.FastAddProperty("dotnet", new JintDotnet(engine), writable: false, enumerable: false, configurable: false);
engine.Realm.GlobalObject.FastAddProperty("fetchSync", new JintFetchSync(engine), writable: false, enumerable: false, configurable: false);
});
}
private Script? GetParsedScript()
{
var source = this.config.UserScript;
if (this.cachedScript is not null)
{
if (string.IsNullOrWhiteSpace(source))
{
this.cachedScript = null;
this.cachedScriptSource = null;
return null;
}
else if (this.cachedScriptSource == source)
{
return this.cachedScript;
}
}
if (string.IsNullOrWhiteSpace(source))
{
return null;
}
2022-05-17 00:53:37 +08:00
var parser = new JavaScriptParser(source!, new ParserOptions("userscript.js"));
2022-05-11 00:20:57 +08:00
var script = parser.ParseScript();
this.cachedScript = script;
this.cachedScriptSource = source;
return script;
}
private Engine CreateJintEngine(ILogger logger)
{
var engine = new Engine(this.jintOptions);
engine.Realm.GlobalObject.FastAddProperty("console", new JintConsole(engine, logger), writable: false, enumerable: false, configurable: false);
engine.Execute(setupScript);
return engine;
}
2022-05-11 19:07:21 +08:00
private static ILogger BuildLogger(ILogger logger)
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
var id = Interlocked.Increment(ref ExecutionId);
return logger.ForContext<UserScriptRunner>().ForContext(nameof(ExecutionId), id);
}
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
private FunctionInstance? ExecuteScriptThenGetEventHandler(ILogger logger, string functionName)
{
2022-05-11 00:20:57 +08:00
var script = this.GetParsedScript();
if (script is null)
2022-05-11 19:07:21 +08:00
return null;
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
var engine = this.CreateJintEngine(logger);
2022-05-11 00:20:57 +08:00
engine.Execute(script);
2022-05-11 19:07:21 +08:00
if (engine.Realm.GlobalObject.Get(RecorderEventsString) is not ObjectInstance events)
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
logger.Warning("[Script] recorderEvents 被修改为非 object");
return null;
2022-05-11 00:20:57 +08:00
}
2022-05-11 19:07:21 +08:00
return events.Get(functionName) as FunctionInstance;
}
public void CallOnTest(ILogger logger, Action<string>? alert)
{
const string callbackName = "onTest";
var log = BuildLogger(logger);
try
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
if (func is null) return;
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
_ = func.Engine.Call(func, new DelegateWrapper(func.Engine, alert ?? delegate { }));
}
catch (Exception ex)
{
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
return;
}
}
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
/// <summary>
/// 获取直播流 URL
/// </summary>
/// <param name="logger">logger</param>
/// <param name="roomid">房间号</param>
/// <returns>直播流 URL</returns>
public string? CallOnFetchStreamUrl(ILogger logger, int roomid, int[] qnSetting)
{
const string callbackName = "onFetchStreamUrl";
var log = BuildLogger(logger);
try
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
if (func is null) return null;
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
var input = new ObjectInstance(func.Engine);
input.Set("roomid", roomid);
input.Set("qn", JsValue.FromObject(func.Engine, qnSetting));
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
var result = func.Engine.Call(func, input);
2022-05-11 00:20:57 +08:00
2022-05-11 19:07:21 +08:00
switch (result)
{
case JsString jsString:
return jsString.ToString();
case JsUndefined or JsNull:
return null;
default:
log.Warning($"{RecorderEvents}.{callbackName}() 返回了不支持的类型: {{ValueType}}", result.Type);
return null;
}
2022-05-11 00:20:57 +08:00
}
2022-05-11 19:07:21 +08:00
catch (Exception ex)
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
return null;
2022-05-11 00:20:57 +08:00
}
}
2022-05-11 19:07:21 +08:00
/// <summary>
/// 在发送请求之前修改直播流 URL
/// </summary>
/// <param name="logger">logger</param>
/// <param name="originalUrl">原直播流地址</param>
/// <returns>url: 新直播流地址<br/>ip: 可选的强制使用的 IP 地址</returns>
public (string url, string? ip)? CallOnTransformStreamUrl(ILogger logger, string originalUrl)
2022-05-11 00:20:57 +08:00
{
2022-05-11 19:07:21 +08:00
const string callbackName = "onTransformStreamUrl";
var log = BuildLogger(logger);
try
{
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
if (func is null) return null;
var result = func.Engine.Call(func, originalUrl);
switch (result)
{
case JsString jsString:
return (jsString.ToString(), null);
case ObjectInstance obj:
{
var url = obj.Get("url");
if (url is not JsString urlString)
{
log.Warning($"{RecorderEvents}.{callbackName}() 返回的 object 缺少 url 属性");
return null;
}
var ip = obj.Get("ip") as JsString;
return (urlString.ToString(), ip?.ToString());
}
case JsUndefined or JsNull:
return null;
default:
log.Warning($"{RecorderEvents}.{callbackName}() 返回了不支持的类型: {{ValueType}}", result.Type);
return null;
}
}
catch (Exception ex)
{
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
return null;
}
2022-05-11 00:20:57 +08:00
}
}
}