From 8c5fe88d9741ce771a71925d640d6f1fc7de4d81 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Tue, 10 Sep 2024 11:40:05 +0700 Subject: [PATCH] feat: nightly updater (#1175) --- engine/CMakeLists.txt | 2 +- engine/commands/cortex_upd_cmd.cc | 145 +++++++++++------- engine/commands/cortex_upd_cmd.h | 115 +++++++++++++- engine/controllers/command_line_parser.cc | 58 +++---- engine/services/download_service.cc | 1 + engine/test/components/test_cortex_upd_cmd.cc | 56 +++++++ engine/vcpkg.json | 35 +++-- 7 files changed, 293 insertions(+), 119 deletions(-) create mode 100644 engine/test/components/test_cortex_upd_cmd.cc diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 48f5a5ead..8a72e5040 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -86,7 +86,7 @@ if(DEFINED CMAKE_JS_INC) # define NPI_VERSION add_compile_definitions(NAPI_VERSION=8) endif() - + add_compile_definitions(CORTEX_VARIANT="${CORTEX_VARIANT}") add_compile_definitions(CORTEX_CPP_VERSION="${CORTEX_CPP_VERSION}") add_compile_definitions(CORTEX_CONFIG_FILE_PATH="${CORTEX_CONFIG_FILE_PATH}") diff --git a/engine/commands/cortex_upd_cmd.cc b/engine/commands/cortex_upd_cmd.cc index 9027203e7..a4343e55c 100644 --- a/engine/commands/cortex_upd_cmd.cc +++ b/engine/commands/cortex_upd_cmd.cc @@ -4,6 +4,7 @@ #include "cortex_upd_cmd.h" #include "httplib.h" #include "nlohmann/json.hpp" +#include "server_stop_cmd.h" #include "services/download_service.h" #include "utils/archive_utils.h" #include "utils/file_manager_utils.h" @@ -12,21 +13,38 @@ namespace commands { -namespace { -const std::string kCortexBinary = "cortex-cpp"; -} - CortexUpdCmd::CortexUpdCmd() {} void CortexUpdCmd::Exec(std::string v) { - // TODO(sang) stop server if it is running + { + auto config = file_manager_utils::GetCortexConfig(); + httplib::Client cli(config.apiServerHost + ":" + config.apiServerPort); + auto res = cli.Get("/health/healthz"); + if (res) { + CLI_LOG("Server is running. Stopping server before updating!"); + commands::ServerStopCmd ssc(config.apiServerHost, + std::stoi(config.apiServerPort)); + ssc.Exec(); + } + } + if (CORTEX_VARIANT == file_manager_utils::kNightlyVariant) { + if (!GetNightly(v)) + return; + } else { + if (!GetStableAndBeta(v)) + return; + } + CLI_LOG("Update cortex sucessfully"); +} + +bool CortexUpdCmd::GetStableAndBeta(const std::string& v) { // Check if the architecture and OS are supported auto system_info = system_info_utils::GetSystemInfo(); if (system_info.arch == system_info_utils::kUnsupported || system_info.os == system_info_utils::kUnsupported) { CTL_ERR("Unsupported OS or architecture: " << system_info.os << ", " << system_info.arch); - return; + return false; } CTL_INF("OS: " << system_info.os << ", Arch: " << system_info.arch); @@ -50,7 +68,7 @@ void CortexUpdCmd::Exec(std::string v) { std::string matched_variant = ""; for (auto& asset : assets) { auto asset_name = asset["name"].get(); - if (asset_name.find("cortex-cpp") != std::string::npos && + if (asset_name.find(kCortexBinary) != std::string::npos && asset_name.find(os_arch) != std::string::npos) { matched_variant = asset_name; break; @@ -59,7 +77,7 @@ void CortexUpdCmd::Exec(std::string v) { } if (matched_variant.empty()) { CTL_ERR("No variant found for " << os_arch); - return; + return false; } CTL_INF("Matched variant: " << matched_variant); @@ -99,14 +117,6 @@ void CortexUpdCmd::Exec(std::string v) { archive_utils::ExtractArchive(download_path.string(), extract_path.string()); - // remove the downloaded file - // TODO(any) Could not delete file on Windows because it is currently hold by httplib(?) - // Not sure about other platforms - try { - std::filesystem::remove(absolute_path); - } catch (const std::exception& e) { - CTL_WRN("Could not delete file: " << e.what()); - } CTL_INF("Finished!"); }); break; @@ -114,58 +124,75 @@ void CortexUpdCmd::Exec(std::string v) { } } catch (const nlohmann::json::parse_error& e) { std::cerr << "JSON parse error: " << e.what() << std::endl; - return; + return false; } } else { CTL_ERR("HTTP error: " << res->status); - return; + return false; } } else { auto err = res.error(); CTL_ERR("HTTP error: " << httplib::to_string(err)); - return; + return false; } -#if defined(_WIN32) - auto executable_path = file_manager_utils::GetExecutableFolderContainerPath(); - auto temp = executable_path / "cortex_tmp.exe"; - remove(temp.string().c_str()); // ignore return code - - auto src = - executable_path / "cortex" / kCortexBinary / (kCortexBinary + ".exe"); - auto dst = executable_path / (kCortexBinary + ".exe"); - // Rename - rename(dst.string().c_str(), temp.string().c_str()); - // Update - CopyFile(const_cast(src.string().c_str()), - const_cast(dst.string().c_str()), false); - auto download_folder = executable_path / "cortex"; - remove(download_folder); - remove(temp.string().c_str()); -#else + + // Replace binary file auto executable_path = file_manager_utils::GetExecutableFolderContainerPath(); - auto temp = executable_path / "cortex_tmp"; - auto src = executable_path / "cortex" / kCortexBinary / kCortexBinary; - auto dst = executable_path / kCortexBinary; - if (std::rename(dst.string().c_str(), temp.string().c_str())) { - CTL_ERR("Failed to rename from " << dst.string() << " to " - << temp.string()); - return; - } - try { - std::filesystem::copy_file( - src, dst, std::filesystem::copy_options::overwrite_existing); - std::filesystem::permissions(dst, std::filesystem::perms::owner_all | - std::filesystem::perms::group_all | - std::filesystem::perms::others_read | - std::filesystem::perms::others_exec); - std::filesystem::remove(temp); - auto download_folder = executable_path / "cortex/"; - std::filesystem::remove_all(download_folder); - } catch (const std::exception& e) { - CTL_WRN("Something wrong happened: " << e.what()); - return; + auto src = executable_path / "cortex" / kCortexBinary / GetCortexBinary(); + auto dst = executable_path / GetCortexBinary(); + return ReplaceBinaryInflight(src, dst); +} + +bool CortexUpdCmd::GetNightly(const std::string& v) { + // Check if the architecture and OS are supported + auto system_info = system_info_utils::GetSystemInfo(); + if (system_info.arch == system_info_utils::kUnsupported || + system_info.os == system_info_utils::kUnsupported) { + CTL_ERR("Unsupported OS or architecture: " << system_info.os << ", " + << system_info.arch); + return false; } -#endif - CLI_LOG("Update cortex sucessfully"); + CTL_INF("OS: " << system_info.os << ", Arch: " << system_info.arch); + + // Download file + std::string version = v.empty() ? "latest" : std::move(v); + std::ostringstream release_path; + release_path << "cortex/" << version << "/" << system_info.os << "-" + << system_info.arch << "/" << kNightlyFileName; + CTL_INF("Engine release path: " << kNightlyHost << release_path.str()); + + auto download_task = DownloadTask{.id = "cortex", + .type = DownloadType::Cortex, + .error = std::nullopt, + .items = {DownloadItem{ + .id = "cortex", + .host = kNightlyHost, + .fileName = kNightlyFileName, + .type = DownloadType::Cortex, + .path = release_path.str(), + }}}; + + DownloadService download_service; + download_service.AddDownloadTask( + download_task, [this](const std::string& absolute_path, bool unused) { + // try to unzip the downloaded file + std::filesystem::path download_path{absolute_path}; + CTL_INF("Downloaded engine path: " << download_path.string()); + + std::filesystem::path extract_path = + download_path.parent_path().parent_path(); + + archive_utils::ExtractArchive(download_path.string(), + extract_path.string()); + + CTL_INF("Finished!"); + }); + + // Replace binay file + auto executable_path = file_manager_utils::GetExecutableFolderContainerPath(); + auto src = executable_path / "cortex" / GetCortexBinary(); + auto dst = executable_path / GetCortexBinary(); + return ReplaceBinaryInflight(src, dst); } + } // namespace commands \ No newline at end of file diff --git a/engine/commands/cortex_upd_cmd.h b/engine/commands/cortex_upd_cmd.h index 2606dbbd9..396c6cdaf 100644 --- a/engine/commands/cortex_upd_cmd.h +++ b/engine/commands/cortex_upd_cmd.h @@ -1,13 +1,124 @@ #pragma once -#include #include +#include + +#include "httplib.h" +#include "nlohmann/json.hpp" +#include "utils/file_manager_utils.h" +#include "utils/logging_utils.h" namespace commands { +#ifndef CORTEX_VARIANT +#define CORTEX_VARIANT file_manager_utils::kProdVariant +#endif +constexpr const auto kNightlyHost = "https://delta.jan.ai"; +constexpr const auto kNightlyFileName = "cortex-nightly.tar.gz"; +const std::string kCortexBinary = "cortex"; + +inline std::string GetCortexBinary() { +#if defined(_WIN32) + constexpr const bool has_exe = true; +#else + constexpr const bool has_exe = false; +#endif + if (CORTEX_VARIANT == file_manager_utils::kNightlyVariant) { + return has_exe ? kCortexBinary + "-nightly.exe" + : kCortexBinary + "-nightly"; + } else if (CORTEX_VARIANT == file_manager_utils::kBetaVariant) { + return has_exe ? kCortexBinary + "-beta.exe" : kCortexBinary + "-beta"; + } else { + return has_exe ? kCortexBinary + ".exe" : kCortexBinary; + } +} + +inline std::string GetHostName() { + if (CORTEX_VARIANT == file_manager_utils::kNightlyVariant) { + return "https://delta.jan.ai"; + } else { + return "https://api.github.com"; + } +} + +inline std::string GetReleasePath() { + if (CORTEX_VARIANT == file_manager_utils::kNightlyVariant) { + return "/cortex/latest/version.json"; + } else { + return "/repos/janhq/cortex.cpp/releases/latest"; + } +} + +inline void CheckNewUpdate() { + auto host_name = GetHostName(); + auto release_path = GetReleasePath(); + CTL_INF("Engine release path: " << host_name << release_path); -class CortexUpdCmd{ + httplib::Client cli(host_name); + if (auto res = cli.Get(release_path)) { + if (res->status == httplib::StatusCode::OK_200) { + try { + auto json_res = nlohmann::json::parse(res->body); + std::string latest_version = json_res["tag_name"].get(); + std::string current_version = CORTEX_CPP_VERSION; + if (current_version != latest_version) { + CLI_LOG("\nA new release of cortex is available: " + << current_version << " -> " << latest_version); + CLI_LOG("To upgrade, run: cortex update"); + // CLI_LOG(json_res["html_url"].get()); + } + } catch (const nlohmann::json::parse_error& e) { + CTL_INF("JSON parse error: " << e.what()); + } + } else { + CTL_INF("HTTP error: " << res->status); + } + } else { + auto err = res.error(); + CTL_INF("HTTP error: " << httplib::to_string(err)); + } +} + +inline bool ReplaceBinaryInflight(const std::filesystem::path& src, + const std::filesystem::path& dst) { + if (src == dst) { + // Already has the newest + return true; + } + std::filesystem::path temp = std::filesystem::temp_directory_path() / "cortex_temp"; + + try { + if (std::filesystem::exists(temp)) { + std::filesystem::remove(temp); + } + + std::rename(dst.string().c_str(), temp.string().c_str()); + std::filesystem::copy_file( + src, dst, std::filesystem::copy_options::overwrite_existing); + std::filesystem::permissions(dst, std::filesystem::perms::owner_all | + std::filesystem::perms::group_all | + std::filesystem::perms::others_read | + std::filesystem::perms::others_exec); + auto download_folder = src.parent_path(); + std::filesystem::remove_all(download_folder); + } catch (const std::exception& e) { + CTL_ERR("Something wrong happened: " << e.what()); + if (std::filesystem::exists(temp)) { + std::rename(temp.string().c_str(), dst.string().c_str()); + CLI_LOG("Restored binary file"); + } + return false; + } + + return true; +} + +class CortexUpdCmd { public: CortexUpdCmd(); void Exec(std::string version); + + private: + bool GetStableAndBeta(const std::string& v); + bool GetNightly(const std::string& v); }; } // namespace commands \ No newline at end of file diff --git a/engine/controllers/command_line_parser.cc b/engine/controllers/command_line_parser.cc index 0c8f3e26e..987aa693a 100644 --- a/engine/controllers/command_line_parser.cc +++ b/engine/controllers/command_line_parser.cc @@ -23,7 +23,7 @@ CommandLineParser::CommandLineParser() : app_("Cortex.cpp CLI"), engine_service_{EngineService()} {} bool CommandLineParser::SetupCommand(int argc, char** argv) { - + auto config = file_manager_utils::GetCortexConfig(); std::string model_id; // Models group commands @@ -33,7 +33,7 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { auto start_cmd = models_cmd->add_subcommand("start", "Start a model by ID"); start_cmd->add_option("model_id", model_id, ""); - start_cmd->callback([&model_id]() { + start_cmd->callback([&model_id, &config]() { commands::CmdInfo ci(model_id); std::string model_file = ci.branch == "main" ? ci.model_name : ci.model_name + "-" + ci.branch; @@ -41,7 +41,8 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { yaml_handler.ModelConfigFromFile( file_manager_utils::GetModelsContainerPath().string() + "/" + model_file + ".yaml"); - commands::ModelStartCmd msc("127.0.0.1", 3928, + commands::ModelStartCmd msc(config.apiServerHost, + std::stoi(config.apiServerPort), yaml_handler.GetModelConfig()); msc.Exec(); }); @@ -49,7 +50,7 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { auto stop_model_cmd = models_cmd->add_subcommand("stop", "Stop a model by ID"); stop_model_cmd->add_option("model_id", model_id, ""); - stop_model_cmd->callback([&model_id]() { + stop_model_cmd->callback([&model_id, &config]() { commands::CmdInfo ci(model_id); std::string model_file = ci.branch == "main" ? ci.model_name : ci.model_name + "-" + ci.branch; @@ -57,7 +58,8 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { yaml_handler.ModelConfigFromFile( file_manager_utils::GetModelsContainerPath().string() + "/" + model_file + ".yaml"); - commands::ModelStopCmd smc("127.0.0.1", 3928, + commands::ModelStopCmd smc(config.apiServerHost, + std::stoi(config.apiServerPort), yaml_handler.GetModelConfig()); smc.Exec(); }); @@ -104,7 +106,7 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { chat_cmd->add_option("model_id", model_id, ""); chat_cmd->add_option("-m,--message", msg, "Message to chat with model"); - chat_cmd->callback([&model_id, &msg] { + chat_cmd->callback([&model_id, &msg, &config] { commands::CmdInfo ci(model_id); std::string model_file = ci.branch == "main" ? ci.model_name : ci.model_name + "-" + ci.branch; @@ -112,7 +114,9 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { yaml_handler.ModelConfigFromFile( file_manager_utils::GetModelsContainerPath().string() + "/" + model_file + ".yaml"); - commands::ChatCmd cc("127.0.0.1", 3928, yaml_handler.GetModelConfig()); + commands::ChatCmd cc(config.apiServerHost, + std::stoi(config.apiServerPort), + yaml_handler.GetModelConfig()); cc.Exec(msg); }); } @@ -158,17 +162,18 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { app_.add_subcommand("run", "Shortcut to start a model and chat"); std::string model_id; run_cmd->add_option("model_id", model_id, ""); - run_cmd->callback([&model_id] { - commands::RunCmd rc("127.0.0.1", 3928, model_id); + run_cmd->callback([&model_id, &config] { + commands::RunCmd rc(config.apiServerHost, std::stoi(config.apiServerPort), + model_id); rc.Exec(); }); } auto stop_cmd = app_.add_subcommand("stop", "Stop the API server"); - stop_cmd->callback([] { - // TODO get info from config file - commands::ServerStopCmd ssc("127.0.0.1", 3928); + stop_cmd->callback([&config] { + commands::ServerStopCmd ssc(config.apiServerHost, + std::stoi(config.apiServerPort)); ssc.Exec(); }); @@ -202,34 +207,7 @@ bool CommandLineParser::SetupCommand(int argc, char** argv) { // Check new update, only check for stable release for now #ifdef CORTEX_CPP_VERSION if (check_update) { - constexpr auto github_host = "https://api.github.com"; - std::ostringstream release_path; - release_path << "/repos/janhq/cortex.cpp/releases/latest"; - CTL_INF("Engine release path: " << github_host << release_path.str()); - - httplib::Client cli(github_host); - if (auto res = cli.Get(release_path.str())) { - if (res->status == httplib::StatusCode::OK_200) { - try { - auto json_res = nlohmann::json::parse(res->body); - std::string latest_version = json_res["tag_name"].get(); - std::string current_version = CORTEX_CPP_VERSION; - if (current_version != latest_version) { - CLI_LOG("\nA new release of cortex is available: " - << current_version << " -> " << latest_version); - CLI_LOG("To upgrade, run: cortex update"); - CLI_LOG(json_res["html_url"].get()); - } - } catch (const nlohmann::json::parse_error& e) { - CTL_INF("JSON parse error: " << e.what()); - } - } else { - CTL_INF("HTTP error: " << res->status); - } - } else { - auto err = res.error(); - CTL_INF("HTTP error: " << httplib::to_string(err)); - } + commands::CheckNewUpdate(); } #endif diff --git a/engine/services/download_service.cc b/engine/services/download_service.cc index ffb25f02d..78a0c4757 100644 --- a/engine/services/download_service.cc +++ b/engine/services/download_service.cc @@ -81,6 +81,7 @@ void DownloadService::StartDownloadItem( } if (current == total) { outputFile.flush(); + outputFile.close(); CLI_LOG("Done download: " << static_cast(total) / 1024 / 1024 << " MiB"); if (callback.has_value()) { diff --git a/engine/test/components/test_cortex_upd_cmd.cc b/engine/test/components/test_cortex_upd_cmd.cc new file mode 100644 index 000000000..9b8f3e764 --- /dev/null +++ b/engine/test/components/test_cortex_upd_cmd.cc @@ -0,0 +1,56 @@ +#include "commands/cortex_upd_cmd.h" +#include "gtest/gtest.h" + +namespace { +constexpr const auto kNewReleaseFolder = "./cortex-release"; +constexpr const auto kNewReleaseFile = "./cortex-release/cortexexe"; +constexpr const auto kCurReleaseFile = "./cortexexe"; +std::filesystem::path GetCortexTemp() { + return std::filesystem::temp_directory_path() / "cortex_temp"; +} +} // namespace + +class CortexUpdCmdTest : public ::testing::Test { + void SetUp() { + // Create new release folder and files + std::filesystem::path folder_path(kNewReleaseFolder); + std::filesystem::create_directory(folder_path); + std::ofstream src(kNewReleaseFile); + src.close(); + std::ofstream dst(kCurReleaseFile); + dst.close(); + } + + void TearDown() { + std::filesystem::path folder_path(kNewReleaseFolder); + if (std::filesystem::exists(folder_path)) { + std::filesystem::remove_all(folder_path); + } + + if (std::filesystem::exists(kCurReleaseFile)) { + std::filesystem::remove(kCurReleaseFile); + } + + if (std::filesystem::exists(GetCortexTemp())) { + std::filesystem::remove(GetCortexTemp()); + } + } +}; + +TEST_F(CortexUpdCmdTest, return_true_if_self_replace) { + EXPECT_TRUE(commands::ReplaceBinaryInflight("test", "test")); +} + +TEST_F(CortexUpdCmdTest, replace_binary_successfully) { + std::filesystem::path new_binary(kNewReleaseFile); + std::filesystem::path cur_binary(kCurReleaseFile); + EXPECT_TRUE(commands::ReplaceBinaryInflight(new_binary, cur_binary)); + EXPECT_TRUE(std::filesystem::exists(GetCortexTemp())); +} + +TEST_F(CortexUpdCmdTest, should_restore_old_binary_if_has_error) { + std::filesystem::path new_binary("Non-exist"); + std::filesystem::path cur_binary(kCurReleaseFile); + EXPECT_FALSE(commands::ReplaceBinaryInflight(new_binary, cur_binary)); + EXPECT_FALSE(std::filesystem::exists(GetCortexTemp())); +} \ No newline at end of file diff --git a/engine/vcpkg.json b/engine/vcpkg.json index e7a74d2dc..fe4783ec8 100644 --- a/engine/vcpkg.json +++ b/engine/vcpkg.json @@ -1,19 +1,20 @@ { - "dependencies": [ - "gtest", - "cli11", - { - "name": "cpp-httplib", - "features": ["openssl"] - }, - "drogon", - "jinja2cpp", - "jsoncpp", - "minizip", - "nlohmann-json", - "yaml-cpp", - "libarchive", - "tabulate" - ] + "dependencies": [ + "gtest", + "cli11", + { + "name": "cpp-httplib", + "features": [ + "openssl" + ] + }, + "drogon", + "jinja2cpp", + "jsoncpp", + "minizip", + "nlohmann-json", + "yaml-cpp", + "libarchive", + "tabulate" + ] } -