LiteLoaderBDS-1.16.40/ScriptEngine/Main/NodeJsHelper.cpp
2022-09-21 19:47:03 +08:00

523 lines
17 KiB
C++

#pragma warning(disable : 4251)
#include "Configs.h"
#if defined(LLSE_BACKEND_NODEJS)
#include "Global.hpp"
#include <ScheduleAPI.h>
#include <API/EventAPI.h>
#include <API/CommandCompatibleAPI.h>
#include <API/CommandAPI.h>
#include <Engine/RemoteCall.h>
#include <LLAPI.h>
#include <Utils/StringHelper.h>
#include "NodeJsHelper.h"
#include "Engine/EngineManager.h"
#include "Engine/EngineOwnData.h"
#include <NodeJs/include/uv/uv.h>
#include <NodeJs/include/v8/v8.h>
// pre-declare
extern void BindAPIs(ScriptEngine* engine);
namespace NodeJsHelper {
bool nodeJsInited = false;
std::vector<std::string> args;
std::vector<std::string> exec_args;
std::unique_ptr<node::MultiIsolatePlatform> platform = nullptr;
std::unordered_map<script::ScriptEngine*, node::Environment*> environments;
std::unordered_map<script::ScriptEngine*, std::unique_ptr<node::CommonEnvironmentSetup>>* setups = new std::unordered_map<script::ScriptEngine*, std::unique_ptr<node::CommonEnvironmentSetup>>();
std::unordered_map<node::Environment*, bool> isRunning;
std::unordered_map<node::Environment*, ScheduleTask> uvLoopTask;
bool initNodeJs() {
// Init NodeJs
WCHAR buf[MAX_PATH];
GetCurrentDirectory(MAX_PATH, buf);
auto path = wstr2str(buf) + "\\bedrock_server_mod.exe";
char* cPath = (char*)path.c_str();
uv_setup_args(1, &cPath);
args = {path};
std::vector<std::string> errors;
auto exitCode = node::InitializeNodeWithArgs(&args, &exec_args, &errors);
if (exitCode != 0) {
logger.error("Failed to initialize node! NodeJs plugins won't be loaded");
return false;
}
// Init V8
using namespace v8;
platform = node::MultiIsolatePlatform::Create(std::thread::hardware_concurrency());
V8::InitializePlatform(platform.get());
V8::Initialize();
nodeJsInited = true;
return true;
}
void shutdownNodeJs() {
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
}
script::ScriptEngine* newEngine() {
if (!nodeJsInited && !initNodeJs()) {
return nullptr;
}
std::vector<std::string> errors;
std::unique_ptr<node::CommonEnvironmentSetup> setup =
node::CommonEnvironmentSetup::Create(platform.get(), &errors, args, exec_args, node::EnvironmentFlags::kOwnsProcessState);
//if kOwnsInspector set, inspector_agent.cc:681 CHECK_EQ(start_io_thread_async_initialized.exchange(true), false) fail!
if (!setup) {
for (const std::string& err : errors)
logger.error("CommonEnvironmentSetup Error: {}", err.c_str());
return nullptr;
}
v8::Isolate* isolate = setup->isolate();
node::Environment* env = setup->env();
v8::Locker locker(isolate);
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(setup->context());
script::ScriptEngine* engine = new script::ScriptEngineImpl({}, isolate, setup->context(), false);
logger.debug("Initialize ScriptEngine for node.js [{}]", (void*)engine);
environments[engine] = env;
(*setups)[engine] = std::move(setup);
isRunning[env] = true;
node::AddEnvironmentCleanupHook(
isolate,
[](void* arg) {
static_cast<script::ScriptEngine*>(arg)->destroy();
logger.debug("Destory ScriptEngine for node.js [{}]", arg);
logger.debug("Destroy EnvironmentCleanupHook");
},
engine);
return engine;
}
bool loadPluginCode(script::ScriptEngine* engine, std::string entryScriptPath, std::string pluginDirPath)
{
auto mainScripts = ReadAllFile(entryScriptPath);
if (!mainScripts) {
return false;
}
// Process requireDir
if (!EndsWith(pluginDirPath, "/"))
pluginDirPath += "/";
pluginDirPath = ReplaceStr(pluginDirPath, "\\", "/");
// Find setup
auto it = setups->find(engine);
if (it == setups->end())
return false;
auto isolate = it->second->isolate();
auto env = it->second->env();
try
{
using namespace v8;
EngineScope enter(engine);
string executeJs =
"const __LLSE_PublicRequire = require('module').createRequire(process.cwd() + '/" + pluginDirPath + "');"
+ "const __LLSE_PublicModule = require('module'); __LLSE_PublicModule.exports = {};"
+ "(function (exports, require, module, __filename, __dirname) { "
+ *mainScripts + "\n})({}, __LLSE_PublicRequire, __LLSE_PublicModule, '"
+ entryScriptPath + "', '" + pluginDirPath + "'); "; // TODO __filename & __dirname need to be reviewed
//TODO: ESM Support
// Set exit handler
node::SetProcessExitHandler(env, [](node::Environment* env_, int exit_code){
stopEngine(getEngine(env_));
});
// Load code
MaybeLocal<v8::Value> loadenv_ret = node::LoadEnvironment(env, executeJs.c_str());
if (loadenv_ret.IsEmpty()) // There has been a JS exception.
{
node::Stop(env);
uv_stop(it->second->event_loop());
return false;
}
// Start libuv event loop
uvLoopTask[env] = Schedule::repeat([engine, env, isRunningMap{&isRunning}, eventLoop{it->second->event_loop()}]() {
if (!LL::isServerStopping() && (*isRunningMap)[env]) {
EngineScope enter(engine);
uv_run(eventLoop, UV_RUN_NOWAIT);
}
if (LL::isServerStopping()) {
uv_stop(eventLoop);
logger.debug("Destroy ServerStopping");
}
},2);
return true;
}
catch (...)
{
return false;
}
}
node::Environment* getEnvironmentOf(script::ScriptEngine* engine) {
auto it = environments.find(engine);
if (it == environments.end()) return nullptr;
return it->second;
}
v8::Isolate* getIsolateOf(script::ScriptEngine* engine) {
auto it = setups->find(engine);
if (it == setups->end())
return nullptr;
return it->second->isolate();
}
bool stopEngine(node::Environment* env)
{
if (!env)
return false;
try
{
// Set flag
isRunning[env] = false;
// Stop code executing
node::Stop(env);
// Stop libuv event loop
auto it = uvLoopTask.find(env);
if (it != uvLoopTask.end())
{
it->second.cancel();
}
return true;
}
catch (...)
{
logger.error("Fail to stop engine {}", (void*)env);
return false;
}
}
bool stopEngine(script::ScriptEngine* engine) {
logger.info("NodeJs plugin {} exited.", ENGINE_GET_DATA(engine)->pluginName);
auto env = NodeJsHelper::getEnvironmentOf(engine);
return stopEngine(env);
}
script::ScriptEngine* getEngine(node::Environment* env)
{
for (auto& [engine, environment] : environments)
if (env == environment)
return engine;
return nullptr;
}
// Load NodeJs plugin
// This function must be called in correct backend
bool loadNodeJsPlugin(std::string dirPath, const std::string& packagePath, bool isHotLoad) {
// "dirPath" is always public temp dir (LLSE_PLUGIN_PACKAGE_TEMP_DIR)
if (dirPath == LLSE_PLUGIN_PACKAGE_TEMP_DIR)
{
// Need to copy from temp dir to installed dir
if (std::filesystem::exists(LLSE_PLUGIN_PACKAGE_TEMP_DIR "/package.json")) {
auto pluginName = NodeJsHelper::getPluginPackageName(LLSE_PLUGIN_PACKAGE_TEMP_DIR);
if (pluginName.empty())
{
pluginName = UTF82String(filesystem::path(packagePath).filename().replace_extension("").u8string());
}
auto dest = std::filesystem::path(LLSE_PLUGINS_ROOT_DIR).append(pluginName);
// copy files
std::error_code ec;
//if (filesystem::exists(dest))
// filesystem::remove_all(dest, ec);
std::filesystem::copy(LLSE_PLUGIN_PACKAGE_TEMP_DIR "/", dest,
filesystem::copy_options::overwrite_existing | filesystem::copy_options::recursive, ec);
// reset dirPath
dirPath = UTF82String(dest.u8string());
}
// remove temp dir
std::error_code ec;
std::filesystem::remove_all(LLSE_PLUGIN_PACKAGE_TEMP_DIR, ec);
}
std::string entryPath = NodeJsHelper::findEntryScript(dirPath);
if (entryPath.empty())
return false;
std::string pluginName = NodeJsHelper::getPluginPackageName(dirPath);
// Run "npm install" if needed
if (NodeJsHelper::doesPluginPackHasDependency(dirPath) && !filesystem::exists(filesystem::path(dirPath) / "node_modules")) {
int exitCode = 0;
logger.info(tr("llse.loader.nodejs.executeNpmInstall.start", fmt::arg("name", UTF82String(filesystem::path(dirPath).filename().u8string()))));
if ((exitCode = NodeJsHelper::executeNpmCommand("npm install", dirPath)) == 0)
logger.info(tr("llse.loader.nodejs.executeNpmInstall.success"));
else
logger.error(tr("llse.loader.nodejs.executeNpmInstall.fail", fmt::arg("code", exitCode)));
}
// Create engine & Load plugin
ScriptEngine* engine = nullptr;
node::Environment* env = nullptr;
try {
engine = EngineManager::newEngine();
{
EngineScope enter(engine);
// setData
ENGINE_OWN_DATA()->pluginName = pluginName;
ENGINE_OWN_DATA()->pluginFileOrDirPath = dirPath;
ENGINE_OWN_DATA()->logger.title = pluginName;
// bindAPIs
BindAPIs(engine);
}
if (!NodeJsHelper::loadPluginCode(engine, entryPath, dirPath))
throw "Uncaught exception thrown in code";
if (!PluginManager::getPlugin(pluginName)) {
// Plugin did't register itself. Help to register it
string description = pluginName;
LL::Version ver(1, 0, 0);
std::map<string, string> others = {};
// Read information from package.json
try {
std::filesystem::path packageFilePath = std::filesystem::path(dirPath) / "package.json";
std::ifstream file(UTF82String(packageFilePath.make_preferred().u8string()));
nlohmann::json j;
file >> j;
file.close();
// description
if (j.contains("description")) {
description = j["description"].get<std::string>();
}
// version
if (j.contains("version") && j["version"].is_string()) {
ver = LL::Version::parse(j["version"].get<std::string>());
}
// license
if (j.contains("license") && j["license"].is_string()) {
others["License"] = j["license"].get<std::string>();
}
else if (j["license"].is_object() && j["license"].contains("url")) {
others["License"] = j["license"]["url"].get<std::string>();
}
// repository
if (j.contains("repository") && j["repository"].is_string()) {
others["Repository"] = j["repository"].get<std::string>();
}
else if (j["repository"].is_object() && j["repository"].contains("url")) {
others["Repository"] = j["repository"]["url"].get<std::string>();
}
}
catch (...) {
logger.warn(tr("llse.loader.nodejs.register.fail", fmt::arg("name", pluginName)));
}
// register
PluginManager::registerPlugin(dirPath, pluginName, description, ver, others);
}
// Call necessary events when at hot load
if (isHotLoad)
LLSECallEventsOnHotLoad(engine);
// Success
logger.info(tr("llse.loader.loadMain.loadedPlugin",
fmt::arg("type", "Node.js"),
fmt::arg("name", pluginName)));
return true;
}
catch (const Exception& e) {
logger.error("Fail to load " + dirPath + "!");
if (engine) {
EngineScope enter(engine);
logger.error("In Plugin: " + ENGINE_OWN_DATA()->pluginName);
PrintException(e);
ExitEngineScope exit;
// NodeJs use his own setTimeout, so no need to remove
// LLSERemoveTimeTaskData(engine);
LLSERemoveAllEventListeners(engine);
LLSERemoveCmdRegister(engine);
LLSERemoveCmdCallback(engine);
LLSERemoveAllExportedFuncs(engine);
engine->getData().reset();
EngineManager::unRegisterEngine(engine);
}
if (engine) {
NodeJsHelper::stopEngine(engine);
}
}
catch (const std::exception& e) {
logger.error("Fail to load " + dirPath + "!");
logger.error(TextEncoding::toUTF8(e.what()));
}
catch (...) {
logger.error("Fail to load " + dirPath + "!");
}
return false;
}
std::string findEntryScript(const std::string& dirPath)
{
auto dirPath_obj = std::filesystem::path(dirPath);
std::filesystem::path packageFilePath = dirPath_obj / "package.json";
if (!std::filesystem::exists(packageFilePath))
return "";
try {
std::ifstream file(UTF82String(packageFilePath.make_preferred().u8string()));
nlohmann::json j;
file >> j;
std::string entryFile = "index.js";
if (j.contains("main")) {
entryFile = j["main"].get<std::string>();
}
auto entryPath = std::filesystem::canonical(dirPath_obj / std::filesystem::path(entryFile));
if (!std::filesystem::exists(entryPath))
return "";
else
return UTF82String(entryPath.u8string());
}
catch (...)
{
return "";
}
}
std::string getPluginPackageName(const std::string& dirPath)
{
auto dirPath_obj = std::filesystem::path(dirPath);
std::filesystem::path packageFilePath = dirPath_obj / std::filesystem::path("package.json");
if (!std::filesystem::exists(packageFilePath))
return "";
try {
std::ifstream file(UTF82String(packageFilePath.make_preferred().u8string()));
nlohmann::json j;
file >> j;
std::string packageName = "";
if (j.contains("name")) {
packageName = j["name"].get<std::string>();
}
return packageName;
}
catch (...)
{
return "";
}
}
bool doesPluginPackHasDependency(const std::string& dirPath)
{
auto dirPath_obj = std::filesystem::path(dirPath);
std::filesystem::path packageFilePath = dirPath_obj / std::filesystem::path("package.json");
if (!std::filesystem::exists(packageFilePath))
return false;
try {
std::ifstream file(UTF82String(packageFilePath.make_preferred().u8string()));
nlohmann::json j;
file >> j;
if (j.contains("dependencies")) {
return true;
}
return false;
}
catch (...)
{
return false;
}
}
bool processConsoleNpmCmd(const std::string& cmd)
{
#ifdef LLSE_BACKEND_NODEJS
if (StartsWith(cmd, "npm "))
{
executeNpmCommand(cmd);
return false;
}
else
return true;
#else
return true;
#endif
}
int executeNpmCommand(std::string cmd, std::string workingDir)
{
if (!nodeJsInited && !initNodeJs()) {
return -1;
}
std::vector<std::string> errors;
std::unique_ptr<node::CommonEnvironmentSetup> setup =
node::CommonEnvironmentSetup::Create(platform.get(), &errors, args, exec_args, node::EnvironmentFlags::kOwnsProcessState);
//if kOwnsInspector set, inspector_agent.cc:681 CHECK_EQ(start_io_thread_async_initialized.exchange(true), false) fail!
if (!setup) {
for (const std::string& err : errors)
logger.error("CommonEnvironmentSetup Error: {}", err.c_str());
return -1;
}
v8::Isolate* isolate = setup->isolate();
node::Environment* env = setup->env();
int exit_code = 0;
//Process workingDir
workingDir = ReplaceStr(workingDir, "\\", "/");
{
using namespace v8;
v8::Locker locker(isolate);
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(setup->context());
string executeJs = "const oldCwd = process.cwd();"
"const publicRequire = require('module').createRequire(oldCwd + '/plugins/lib/');"
"require('process').chdir('" + workingDir + "');"
+ "publicRequire('npm-js-interface')('" + cmd + "');"
+ "require('process').chdir(oldCwd);";
try
{
node::SetProcessExitHandler(env, [&](node::Environment* env_, int exit_code) {
node::Stop(env);
});
MaybeLocal<v8::Value> loadenv_ret = node::LoadEnvironment(env, executeJs.c_str());
if (loadenv_ret.IsEmpty()) // There has been a JS exception.
throw "error";
exit_code = node::SpinEventLoop(env).FromMaybe(1);
}
catch(...)
{
logger.error("Fail to execute NPM command. Error occurs");
}
}
node::Stop(env);
return exit_code;
}
} // namespace NodeJsHelper
#endif