diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e0337f44..3a9db5937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial application debugging through Tesseratos (#1303, **@RiscadoA**). - Print stacktrace with *cpptrace* on calls to CUBOS_FAIL (#1172, **@RiscadoA**). - Simple cross-platform multi-process utilities (**@RiscadoA**). +- Project opening and closing on Tesseratos (#1218, **@RiscadoA**). ### Changed diff --git a/tools/tesseratos/CMakeLists.txt b/tools/tesseratos/CMakeLists.txt index d709ceb47..cf2fc5677 100644 --- a/tools/tesseratos/CMakeLists.txt +++ b/tools/tesseratos/CMakeLists.txt @@ -5,6 +5,8 @@ set(TESSERATOS_SOURCE "src/tesseratos/main.cpp" "src/tesseratos/debugger/plugin.cpp" "src/tesseratos/debugger/debugger.cpp" + "src/tesseratos/project/plugin.cpp" + "src/tesseratos/project/manager.cpp" "src/tesseratos/asset_explorer/plugin.cpp" "src/tesseratos/asset_explorer/popup.cpp" "src/tesseratos/scene_editor/plugin.cpp" diff --git a/tools/tesseratos/src/tesseratos/main.cpp b/tools/tesseratos/src/tesseratos/main.cpp index e802e3523..95a2c9fbd 100644 --- a/tools/tesseratos/src/tesseratos/main.cpp +++ b/tools/tesseratos/src/tesseratos/main.cpp @@ -9,6 +9,7 @@ #include "asset_explorer/plugin.hpp" #include "debugger/plugin.hpp" +#include "project/plugin.hpp" #include "scene_editor/plugin.hpp" #include "voxel_palette_editor/plugin.hpp" @@ -34,6 +35,8 @@ int main(int argc, char** argv) cubos.plugin(debuggerPlugin); cubos.plugin(assetExplorerPlugin); + cubos.plugin(projectPlugin); + cubos.plugin(sceneEditorPlugin); cubos.plugin(voxelPaletteEditorPlugin); diff --git a/tools/tesseratos/src/tesseratos/project/manager.cpp b/tools/tesseratos/src/tesseratos/project/manager.cpp new file mode 100644 index 000000000..f1c28590d --- /dev/null +++ b/tools/tesseratos/src/tesseratos/project/manager.cpp @@ -0,0 +1,145 @@ +#include "manager.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::data::FileSystem; +using cubos::core::data::StandardArchive; +using cubos::core::net::Address; + +CUBOS_REFLECT_IMPL(tesseratos::ProjectManager::State) +{ + using namespace cubos::core::reflection; + return Type::create("tesseratos::ProjectManager::State") + .with(ConstructibleTrait::typed().withMoveConstructor().build()); +} + +tesseratos::ProjectManager::ProjectManager(State& state, cubos::engine::Assets& assets, Debugger& debugger) + : mState(state) + , mAssets(assets) + , mDebugger(debugger) +{ +} + +bool tesseratos::ProjectManager::open(std::string projectOSPath, std::string binaryOSPath) +{ + if (projectOSPath.empty()) + { + CUBOS_ERROR("Project path is empty"); + return false; + } + + this->close(); + + // Prepare the project directory for mounting. + auto archive = std::make_unique(projectOSPath, /*isDirectory=*/true, /*readOnly=*/false); + if (!archive->initialized()) + { + CUBOS_ERROR("Could not open project at {}", projectOSPath); + return false; + } + + if (!FileSystem::mount("/project", std::move(archive))) + { + CUBOS_ERROR("Could not mount project archive to /project"); + return false; + } + + // Load asset metadata from the project assets directory. + mAssets.loadMeta("/project/assets"); + + // We managed to open the project's directory, store the paths. + mState.mProjectOSPath = std::move(projectOSPath); + mState.mBinaryOSPath = std::move(binaryOSPath); + + // Try to launch the project. + // If we can't, we can still keep the project open, but we won't be able to use its types. + this->launch(); + + return true; +} + +bool tesseratos::ProjectManager::open() const +{ + return !mState.mProjectOSPath.empty(); +} + +void tesseratos::ProjectManager::close() +{ + if (mState.mProjectOSPath.empty()) + { + return; + } + + this->terminate(); + + // Unload asset metadata from the project. + mAssets.unloadMeta("/project/assets"); + + // Unmount its directory from the virtual file system. + if (!FileSystem::unmount("/project")) + { + CUBOS_ERROR("Failed to unmount previously open project directory"); + } + + mState.mProjectOSPath.clear(); +} + +bool tesseratos::ProjectManager::launch() +{ + // Stop the binary if it is already running. + this->terminate(); + + if (!mState.mProcess.start(mState.mBinaryOSPath, {"--debug", std::to_string(mState.mPort)})) + { + CUBOS_ERROR("Could not start project's process at {}", mState.mBinaryOSPath); + return false; + } + + // Try to connect to the child process's debugger. + for (int i = 1, sleep = 100; i <= 3; ++i, sleep *= 2) + { + if (i > 0) + { + CUBOS_WARN("CCould not connect to project's process debugger in try {}, retrying in {} ms", i - 1, sleep); + std::this_thread::sleep_for(std::chrono::milliseconds(sleep)); + } + + if (mDebugger.connect(Address::LocalHost, mState.mPort)) + { + CUBOS_INFO("Successfully connected to project's process debugger in try {}", i); + break; + } + } + + // Check if we successfully connected to the child process's debugger. + // If we didn't, kill the child process. + if (!mDebugger.isConnected()) + { + CUBOS_ERROR("Could not connect to the project's process debugger, terminating the process"); + mState.mProcess.kill(); + return false; + } + + return true; +} + +void tesseratos::ProjectManager::terminate() +{ + if (mDebugger.isConnected()) + { + mDebugger.close(); // Issue a close command to the process' debugger. + } + else + { + mState.mProcess.kill(); + } + + mState.mProcess.wait(); // Wait for the process to finish. +} diff --git a/tools/tesseratos/src/tesseratos/project/manager.hpp b/tools/tesseratos/src/tesseratos/project/manager.hpp new file mode 100644 index 000000000..580752503 --- /dev/null +++ b/tools/tesseratos/src/tesseratos/project/manager.hpp @@ -0,0 +1,115 @@ +/// @file +/// @brief System argument @ref tesseratos::ProjectManager. +/// @ingroup tesseratos-debugger-plugin + +#pragma once + +#include + +#include +#include + +#include + +#include "../debugger/debugger.hpp" + +namespace tesseratos +{ + /// @brief System argument which can be used to manage the currently loaded project. + class ProjectManager + { + public: + CUBOS_REFLECT; + + /// @brief Resource which holds the state of the project manager. + class State; + + /// @brief Constructs. + /// @param state State of the project manager. + /// @param assets Asset manager. + /// @param debugger Debugger. + ProjectManager(State& state, cubos::engine::Assets& assets, Debugger& debugger); + + /// @brief Opens a project directory. + /// + /// The given project directory is mounted in the /project directory in the virtual file system. + /// Unmounts any previously opened project directory. + /// + /// @param projectOSPath Project's directory path in the operating system. + /// @param binaryOSPath Project's binary directory path in the operating system. + /// @return Whether the project could be opened successfully. + bool open(std::string projectOSPath, std::string binaryOSPath); + + /// @brief Checks whether a project is currently open. + /// @return Whether a project is currently open. + bool open() const; + + /// @brief Closes the currently open project. + void close(); + + /// @brief Launches the project's binary and attaches the debugger. + /// + /// If the binary is already running, it is stopped first. + /// + /// @return Whether the project could be launched successfully. + bool launch(); + + /// @brief Stops the project's binary. + void terminate(); + + private: + State& mState; + cubos::engine::Assets& mAssets; + Debugger& mDebugger; + }; + + class ProjectManager::State + { + public: + CUBOS_REFLECT; + + /// @brief Default constructs. + State() = default; + + private: + friend ProjectManager; + + std::string mProjectOSPath{}; + std::string mBinaryOSPath{}; + cubos::core::thread::Process mProcess{}; + uint16_t mPort{9335}; + }; +} // namespace tesseratos + +namespace cubos::core::ecs +{ + template <> + class SystemFetcher + { + public: + static inline constexpr bool ConsumesOptions = false; + + SystemFetcher state; + SystemFetcher assets; + SystemFetcher debugger; + + SystemFetcher(World& world, const SystemOptions& options) + : state{world, options} + , assets{world, options} + , debugger{world, options} + { + } + + void analyze(SystemAccess& access) const + { + state.analyze(access); + assets.analyze(access); + debugger.analyze(access); + } + + tesseratos::ProjectManager fetch(const SystemContext& ctx) + { + return {state.fetch(ctx), assets.fetch(ctx), debugger.fetch(ctx)}; + } + }; +} // namespace cubos::core::ecs diff --git a/tools/tesseratos/src/tesseratos/project/plugin.cpp b/tools/tesseratos/src/tesseratos/project/plugin.cpp new file mode 100644 index 000000000..bf44e0bcc --- /dev/null +++ b/tools/tesseratos/src/tesseratos/project/plugin.cpp @@ -0,0 +1,85 @@ +#include "plugin.hpp" +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "../debugger/plugin.hpp" + +using namespace cubos::core::data; +using namespace cubos::engine; + +void tesseratos::projectPlugin(Cubos& cubos) +{ + cubos.depends(toolboxPlugin); + cubos.depends(imguiPlugin); + cubos.depends(settingsPlugin); + cubos.depends(assetsPlugin); + cubos.depends(debuggerPlugin); + + cubos.resource(); + + cubos.system("show Project") + .tagged(imguiTag) + .call([](ProjectManager project, Toolbox& toolbox, Settings& settings) { + if (!toolbox.isOpen("Project")) + { + return; + } + + if (!ImGui::Begin("Project")) + { + ImGui::End(); + return; + } + + if (!project.open()) + { + // If the project is not open, only show the open dialog. + auto projectOSPath = settings.getString("project.path", ""); + if (ImGui::InputText("Project Path", &projectOSPath)) + { + settings.setString("project.path", projectOSPath); + } + + auto binaryOSPath = settings.getString("project.binary.path", ""); + if (ImGui::InputText("Binary Path", &binaryOSPath)) + { + settings.setString("project.binary.path", binaryOSPath); + } + + if (ImGui::Button("Open") && !project.open(projectOSPath, binaryOSPath)) + { + CUBOS_ERROR("Failed to open project"); + } + + ImGui::End(); + return; + } + + if (ImGui::Button("Launch")) + { + project.launch(); + } + + if (ImGui::Button("Terminate")) + { + project.terminate(); + } + + if (ImGui::Button("Close")) + { + project.close(); + } + + ImGui::End(); + }); +} diff --git a/tools/tesseratos/src/tesseratos/project/plugin.hpp b/tools/tesseratos/src/tesseratos/project/plugin.hpp new file mode 100644 index 000000000..4ded20a54 --- /dev/null +++ b/tools/tesseratos/src/tesseratos/project/plugin.hpp @@ -0,0 +1,24 @@ +/// @dir +/// @brief @ref tesseratos-project-plugin plugin directory. + +/// @file +/// @brief Plugin entry point. +/// @ingroup tesseratos-project-plugin + +#pragma once + +#include + +#include "manager.hpp" + +namespace tesseratos +{ + /// @defgroup tesseratos-project-plugin Project + /// @ingroup tesseratos + /// @brief Adds a resource used to manage the currently loaded project. + + /// @brief Plugin entry function. + /// @param cubos @b Cubos main class + /// @ingroup tesseratos-project-plugin + void projectPlugin(cubos::engine::Cubos& cubos); +} // namespace tesseratos