diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 0ec755689..48d79bf8e 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -180,14 +180,34 @@ app.get_content_sources() -> table Returns a list of content sources (paths), in descending priority order. -``lua +```lua app.set_content_sources(sources: table) ``` Sets a list of content sources (paths). Specified in descending priority order. -``lua +```lua app.reset_content_sources() ``` Resets content sources. + +## Sub-instances + +```lua +-- Creates a headless engine instance with the current project and the specified application script. +-- Returns the instance ID. The number of active sub-instances is currently limited to one. +app.start_background_instance( + -- script file + app_script: string, + -- log file + output_file: string | nil +) -> int + +-- Checks if the engine sub-instance is alive. +app.is_instance_alive(handle: int) -> boolean + +-- Stops the engine sub-instance. +-- Returns true if the instance was alive at the time of the call. +app.terminate(handle: int) -> boolean +``` diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index 07e358ef4..bbdf5cd2f 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -142,4 +142,24 @@ app.set_content_sources(sources: table) -- Сбрасывает список источников контента. app.reset_content_sources() -``` \ No newline at end of file +``` + +## Под-экземпляры + +```lua +-- Создаёт headless-экземпляр движка с текущим проектом и указанным сценарием. +-- Возвращает id экземпляра. Число живых под-экземпляров, на данный момент, ограничено одним. +app.start_background_instance( + -- файл сценария + app_script: string, + -- файл лога + output_file: string | nil +) -> int + +-- Проверяет, жив ли под-экземпляр движка. +app.is_instance_alive(handle: int) -> boolean + +-- Останавливает под-экземпляр движка. +-- Возвращает true если экземпляр был жив в момент вызова. +app.terminate_instance(handle: int) -> boolean +``` diff --git a/res/project.toml b/res/project.toml index b641ad00f..281964857 100644 --- a/res/project.toml +++ b/res/project.toml @@ -3,5 +3,6 @@ name = "default" base_packs = ["base"] permissions = [ "network", - "record-audio" + "record-audio", + "sub-instances", ] diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 9793216a2..2869bba1c 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -140,7 +140,20 @@ local __vc_named_coroutines = {} local __vc_next_coroutine = 1 function __vc_start_coroutine(chunk) - local co = coroutine.create(chunk) + local co = coroutine.create(function() + local _, err = xpcall(chunk, function(msg) + local traceback = debug.get_traceback(0) + local s = string.format("%s:", msg) + for i=1,#traceback - 2 do + local frame = traceback[i] + s = s .. "\n\t"..tb_frame_tostring(frame) + end + return s + end) + if err then + error(err) + end + end) local id = __vc_next_coroutine __vc_next_coroutine = __vc_next_coroutine + 1 __vc_coroutines[id] = co diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 2868dd634..e341f68a1 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -10,8 +10,9 @@ vc = { is_client = function() return not _vc_headless end, + get_version = __vc_app.get_version, get_setting = __vc_app.get_setting, - get_setting_str = __vc_app.get_setting_str, + str_setting = __vc_app.str_setting, get_setting_info = __vc_app.get_setting_info, } diff --git a/src/constants.hpp b/src/constants.hpp index f43314f4a..5bacfe078 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -16,6 +16,9 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true; inline const std::string ENGINE_VERSION_STRING = "0.32"; +inline constexpr uint MAX_SUBPROCESSES = 1; +inline constexpr uint MAX_SUBPROCESS_DEPTH = 2; + /// @brief world regions format version inline constexpr uint REGION_FORMAT_VERSION = 3; diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp index bb1a386a2..f565949aa 100644 --- a/src/devtools/AppScriptsControl.cpp +++ b/src/devtools/AppScriptsControl.cpp @@ -8,11 +8,7 @@ static debug::Logger logger("app-scripts"); -AppScriptsControl::AppScriptsControl( - const CoreParameters& params, const Project& project -) - : project(project) { - +AppScriptsControl::AppScriptsControl(const CoreParameters& params) { io::path scriptFile = params.scriptFile.empty() ? "project:start.lua" diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp index 4e44eef60..3531dc2f9 100644 --- a/src/devtools/AppScriptsControl.hpp +++ b/src/devtools/AppScriptsControl.hpp @@ -14,7 +14,7 @@ struct Project; class AppScriptsControl { public: - AppScriptsControl(const CoreParameters& params, const Project& project); + AppScriptsControl(const CoreParameters& params); void tick(); void loadProjectClientScript(); @@ -24,7 +24,6 @@ public: bool isFinished() const; private: - const Project& project; std::unique_ptr clientScript; std::unique_ptr scriptCoroutine; }; diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index daeac7ca0..571a8b2c5 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -1,6 +1,7 @@ #pragma once #include "interfaces/Serializable.hpp" +#include "io/path.hpp" #include #include @@ -12,6 +13,7 @@ struct Permissions { static inline std::string NETWORK = "network"; static inline std::string RECORD_AUDIO = "record-audio"; static inline std::string WRITE_TO_USER = "write-to-user"; + static inline std::string SUB_INSTANCES = "sub-instances"; std::set permissions; @@ -21,6 +23,7 @@ struct Permissions { struct Project : Serializable { std::string name; std::string title; + io::path path; std::vector basePacks; Permissions permissions; diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index b9f585f47..b5d8074ce 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -14,5 +14,6 @@ struct CoreParameters { std::filesystem::path projectFolder; std::string debugServerString; int tps = 20; + int subProcessDepth = 0; std::unordered_map projectArgs; }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 10f8cee6d..f0e20f922 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -192,7 +192,7 @@ void Engine::initialize(CoreParameters coreParameters) { audio::set_input_device(name == "auto" ? "" : name); })); - appScripts = std::make_unique(params, *project); + appScripts = std::make_unique(params); if (params.stdinCommands) { cmd::start_stdin_cmd_reader(*this); @@ -380,6 +380,7 @@ void Engine::loadAssets() { void Engine::loadProject() { io::path projectFile = "project:project.toml"; project = std::make_unique(); + project->path = "project:"; project->deserialize(io::read_object(projectFile)); logger.info() << "loaded project " << util::quote(project->name); } @@ -412,6 +413,7 @@ void Engine::onWorldClosed() { } void Engine::quit() { + logger.info() << "quitSignal set to true"; quitSignal = true; if (!isHeadless()) { window->setShouldClose(true); diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 3a196f522..aa505a9ba 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -43,7 +43,7 @@ void Mainloop::run() { auto& appScripts = engine.getAppScripts(); logger.info() << "main loop started"; - while (!window.isShouldClose()) { + while (!window.isShouldClose() && !engine.isQuitSignal()) { time.update(window.time()); engine.applicationTick(); engine.updateFrontend(); diff --git a/src/io/path.hpp b/src/io/path.hpp index 538431b7c..3ab95300e 100644 --- a/src/io/path.hpp +++ b/src/io/path.hpp @@ -33,6 +33,8 @@ namespace io { } } + path(std::string_view str) : path(std::string(str)) {} + path(const char* str) : path(std::string(str)) {} bool operator==(const std::string& other) const { diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 6442fcbad..9157b33a6 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -20,8 +20,14 @@ #include "window/Window.hpp" #include "world/Level.hpp" +#include + using namespace scripting; +namespace { + static std::array, MAX_SUBPROCESSES> processes; +} + /// @brief Check if content is loaded static int l_is_content_loaded(lua::State* L) { return lua::pushboolean(L, content != nullptr); @@ -337,6 +343,10 @@ static int l_start_debug_instance(lua::State* L) { if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) { throw std::runtime_error("project has no debugging permission"); } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } int port = lua::tointeger(L, 1); if (port == 0) { @@ -357,6 +367,8 @@ static int l_start_debug_instance(lua::State* L) { "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--dbg-server", "tcp:" + std::to_string(port), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), }; if (!projectPath.empty()) { args.emplace_back("--project"); @@ -365,11 +377,77 @@ static int l_start_debug_instance(lua::State* L) { platform::new_engine_instance( std::move(args), - outputPath.empty() ? "" : io::resolve(std::string(outputPath)) + outputPath.empty() ? "" : io::resolve(std::string(outputPath)), + false ); return lua::pushinteger(L, port); } +static int l_start_background_instance(lua::State* L) { + if (!engine->getProject().permissions.has(Permissions::SUB_INSTANCES)) { + throw std::runtime_error("project has no sub-instances permission"); + } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } + + auto scriptPath = lua::require_lstring(L, 1); + io::path outputPath = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : ""; + const auto& paths = engine->getPaths(); + + std::vector args { + "--headless", + "--res", paths.getResourcesFolder().u8string(), + "--dir", paths.getUserFilesFolder().u8string(), + "--script", io::resolve(scriptPath).u8string(), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), + }; + args.emplace_back("--project"); + args.emplace_back(io::resolve(engine->getProject().path).u8string()); + + int handle = -1; + for (int i = 0; i < ::processes.size(); i++) { + if (!::processes[i] || !::processes[i]->isActive()) { + handle = i; + break; + } + } + if (handle == -1) { + throw std::runtime_error("sub-processes limit exceeded"); + } + ::processes[handle] = platform::new_engine_instance( + std::move(args), + outputPath.empty() ? "" : io::resolve(outputPath), + true + ); + return lua::pushinteger(L, handle); +} + +static int l_is_instance_alive(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + return lua::pushboolean(L, process && process->isActive()); +} + +static int l_terminate_instance(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + if (process == nullptr) { + return lua::pushboolean(L, false); + } + bool active = process->isActive(); + process.reset(); + return lua::pushboolean(L, active); +} + const luaL_Reg applib[] = { /// content {"is_content_loaded", lua::wrap}, @@ -402,5 +480,8 @@ const luaL_Reg applib[] = { {"get_version", lua::wrap}, {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, + {"start_background_instance", lua::wrap}, + {"is_instance_alive", lua::wrap}, + {"terminate_instance", lua::wrap}, {nullptr, nullptr} }; diff --git a/src/main.cpp b/src/main.cpp index 77175bd8a..402b829b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { + logger.info() << (signum == SIGTERM ? "SIGTERM" : "SIGINT") << " received"; Engine::getInstance().quit(); } @@ -25,12 +26,16 @@ int main(int argc, char** argv) { if (!parse_cmdline(argc, argv, coreParameters)) { return EXIT_SUCCESS; } + logger.debug() << "sub-process depth: " + << coreParameters.subProcessDepth; } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return EXIT_FAILURE; } std::signal(SIGTERM, sigterm_handler); - +#ifdef NDEBUG + std::signal(SIGINT, sigterm_handler); +#endif debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); platform::configure_encoding(); diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 7f34bcea4..f05231821 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -13,22 +13,22 @@ namespace fs = std::filesystem; class ArgC { - public: - std::string keyword; - std::function execute; - std::string args; - std::string help; - ArgC( - const std::string& keyword, - std::function execute, - const std::string& args, - const std::string& help - ) { - this->keyword = keyword; - this->execute = execute; - this->args = args; - this->help = help; - } +public: + std::string keyword; + std::function execute; + std::string args; + std::string help; + ArgC( + const std::string& keyword, + std::function execute, + const std::string& args, + const std::string& help + ) { + this->keyword = keyword; + this->execute = execute; + this->args = args; + this->help = help; + } }; @@ -36,54 +36,62 @@ static bool perform_keyword( util::ArgsReader& reader, const std::string& keyword, CoreParameters& params ) { static const std::vector argumentsCommandline = { - ArgC("--res", [¶ms, &reader]() -> bool { + ArgC("--res", [](auto& params, auto& reader) -> bool { params.resFolder = reader.next(); return true; }, "", "set resources directory."), - ArgC("--dir", [¶ms, &reader]() -> bool { + ArgC("--dir", [](auto& params, auto& reader) -> bool { params.userFolder = reader.next(); return true; }, "", "set userfiles directory."), - ArgC("--project", [¶ms, &reader]() -> bool { + ArgC("--project", [](auto& params, auto& reader) -> bool { params.projectFolder = reader.next(); return true; }, "", "set project directory."), - ArgC("--test", [¶ms, &reader]() -> bool { + ArgC("--test", [](auto& params, auto& reader) -> bool { params.testMode = true; params.scriptFile = reader.next(); return true; }, "", "test script file."), - ArgC("--script", [¶ms, &reader]() -> bool { + ArgC("--script", [](auto& params, auto& reader) -> bool { params.testMode = false; params.scriptFile = reader.next(); return true; }, "", "main script file."), - ArgC("--headless", [¶ms]() -> bool { + ArgC("--headless", [](auto& params, auto& reader) -> bool { params.headless = true; return true; }, "", "run in headless mode."), - ArgC("--stdin-cmd", [¶ms]() -> bool { + ArgC("--stdin-cmd", [](auto& params, auto& reader) -> bool { params.stdinCommands = true; return true; }, "", "run commands from stdin."), - ArgC("--tps", [¶ms, &reader]() -> bool { + ArgC("--tps", [](auto& params, auto& reader) -> bool { params.tps = reader.nextInt(); return true; }, "", "headless mode tick-rate (default - 20)."), - ArgC("--version", []() -> bool { + ArgC("--version", [](auto&, auto&) -> bool { std::cout << ENGINE_VERSION_STRING << std::endl; return false; }, "", "display the engine version."), - ArgC("--dbg-server", [¶ms, &reader]() -> bool { + ArgC("--dbg-server", [](auto& params, auto& reader) -> bool { params.debugServerString = reader.next(); return true; }, "", "open debugging server where is {transport}:{port}"), - ArgC("--help", []() -> bool { + ArgC("--sub-depth", [](auto& params, auto& reader) -> bool { + params.subProcessDepth = reader.nextInt(); + return true; + }, "", "sub-process depth"), + ArgC("--help", [](auto&, auto&) -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; - for (auto& a : argumentsCommandline) { - std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); - std::cout << "- " << a.help << std::endl; + for (auto& arg : argumentsCommandline) { + if (arg.help.empty()) { + continue; + } + std::cout << std::setw(24) << std::left + << (arg.keyword + " " + arg.args); + std::cout << "- " << arg.help << std::endl; } std::cout << std::endl; return false; @@ -91,7 +99,7 @@ static bool perform_keyword( }; for (auto& a : argumentsCommandline) { if (a.keyword == keyword) { - return a.execute(); + return a.execute(params, reader); } } throw std::runtime_error("unknown argument " + keyword); diff --git a/src/util/platform.cpp b/src/util/platform.cpp index f113641e5..7b08639e9 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -19,6 +19,13 @@ #else #include #include +#include +#include +#include +#endif +#ifdef __linux__ + #include + #include #endif namespace platform::internal { @@ -215,8 +222,102 @@ std::filesystem::path platform::get_executable_path() { #endif } -void platform::new_engine_instance( - const std::vector& args, std::filesystem::path outputFile +class SystemProcess final : public Process { +public: +#ifdef __linux__ + SystemProcess(pid_t pid) : pid(pid) { + } + + ~SystemProcess() { + terminate(); + } +#elif defined(_WIN32) + SystemProcess(const PROCESS_INFORMATION& pi) + : processHandle(pi.hProcess), pid(pi.dwProcessId) { + if (pi.hThread != nullptr) { + CloseHandle(pi.hThread); + } + } + + ~SystemProcess() { + terminate(); + } +#endif + SystemProcess(const SystemProcess&) = delete; + SystemProcess(SystemProcess&&) noexcept = default; + SystemProcess& operator=(const SystemProcess&) = delete; + + SystemProcess& operator=(SystemProcess&& other) noexcept { + if (this != &other) { +#ifdef __linux__ + pid = other.pid; +#elif defined(_WIN32) + if (processHandle != nullptr) { + CloseHandle(processHandle); + } + processHandle = other.processHandle; + pid = other.pid; + other.processHandle = nullptr; + other.pid = 0; +#endif + } + return *this; + } + + bool isActive() const override { +#ifdef __linux__ + if (kill(pid, 0) == 0) { + return true; + } + return errno != ESRCH; +#elif defined(_WIN32) + if (processHandle == nullptr) { + return false; + } + DWORD exitCode; + if (GetExitCodeProcess(processHandle, &exitCode)) { + return exitCode == STILL_ACTIVE; + } + return false; +#endif + } + + void update() override {} + + void waitForEnd() override { +#ifdef __linux__ + waitpid(pid, nullptr, 0); +#elif defined(_WIN32) + if (processHandle != nullptr) { + WaitForSingleObject(processHandle, INFINITE); + } +#endif + }; + + void terminate() final override { +#ifdef __linux__ + logger.info() << "terminating " << pid; + kill(pid, SIGTERM); +#elif defined(_WIN32) + if (processHandle != nullptr) { + TerminateProcess(processHandle, 1); + WaitForSingleObject(processHandle, INFINITE); + } +#endif + } +private: +#ifdef __linux__ + pid_t pid; +#elif defined(_WIN32) + HANDLE processHandle = nullptr; + DWORD pid = 0; +#endif +}; + +std::unique_ptr platform::new_engine_instance( + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ) { auto executable = get_executable_path(); @@ -285,6 +386,38 @@ void platform::new_engine_instance( &si, &pi ); + if (subProcess) { + HANDLE job = CreateJobObjectW(nullptr, nullptr); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + try { + if (!SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &info, + sizeof(info))) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + if (!AssignProcessToJobObject(job, pi.hProcess)) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + return std::make_unique(pi); + } catch (const std::exception& err) { + CloseHandle(job); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return nullptr; + } + } if (success) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); @@ -295,6 +428,46 @@ void platform::new_engine_instance( ); } #else + +// TODO: implement +#ifndef __APPLE__ + if (subProcess) { + pid_t pid = fork(); + + int fd = open( + outputFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644 + ); + + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGTERM); + + if (getppid() == 1) { + _exit(1); + } + + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector argv(args.size() + 2); + argv[0] = const_cast(executable.c_str()); + for (int i = 0; i < args.size(); i++) { + argv[i + 1] = args[i].c_str(); + } + argv[args.size() + 1] = nullptr; + + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + execvp(executable.c_str(), const_cast(argv.data())); + + _exit(127); + } else { + close(fd); + usleep(1000); // waiting for execvp call + return std::make_unique(pid); + } + } +#endif + std::stringstream ss; ss << executable; for (int i = 0; i < args.size(); i++) { @@ -315,6 +488,7 @@ void platform::new_engine_instance( ); } #endif + return nullptr; } bool platform::stdin_has_data() { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 54c56a0c5..18dc052b6 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include "interfaces/Process.hpp" + namespace platform { void configure_encoding(); /// @brief Get Environment locale in ISO format ll_CC @@ -18,8 +21,10 @@ namespace platform { /// @brief Get current process running executable path std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments - void new_engine_instance( - const std::vector& args, std::filesystem::path outputFile + std::unique_ptr new_engine_instance( + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ); /// @brief Open URL in web browser bool open_url(const std::string& url);