From 719de33bec22893d8322850ade86293571027114 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Thu, 28 Nov 2024 23:55:11 +0700 Subject: [PATCH] fix: support path with special characters on windows (#1730) * fix: utf8 * fix: uft8 for cli * fix: codecvt_utf8_utf16 is deprecated * fix: more * feat: support wstring string conversion * fix: build * fix: engine path env * fix: wstring * fix: cli start server * fix: utf8 file * fix: get env * fix: db * fix: e2e * fix: e2e * fix: cli delete * fix: comment * fix: e2e windows * fix: e2e windows continue * fix: e2e windows skip because of progress bar log issue * fix: add sleep in case of cuda for e2e * fix: import --------- Co-authored-by: vansangpfiev --- engine/CMakeLists.txt | 4 + engine/cli/CMakeLists.txt | 4 + engine/cli/commands/server_start_cmd.cc | 42 ++++++---- engine/cli/main.cc | 19 ++++- engine/database/database.h | 2 +- engine/e2e-test/test_api_engine_uninstall.py | 31 +++---- engine/e2e-test/test_api_model_start.py | 30 ++++--- engine/e2e-test/test_api_model_stop.py | 14 +++- engine/e2e-test/test_cli_engine_install.py | 3 + engine/e2e-test/test_cli_engine_uninstall.py | 2 +- engine/e2e-test/test_cli_model_delete.py | 18 ++-- engine/main.cc | 88 +++++++++++++------- engine/migrations/migration_helper.cc | 10 ++- engine/migrations/migration_helper.h | 7 +- engine/migrations/migration_manager.cc | 12 +-- engine/services/engine_service.cc | 26 ++++-- engine/services/hardware_service.cc | 40 +++++---- engine/services/model_service.cc | 11 ++- engine/utils/config_yaml_utils.h | 3 + engine/utils/cortex_utils.h | 26 +++--- engine/utils/engine_constants.h | 2 +- engine/utils/file_manager_utils.h | 33 ++++++-- engine/utils/system_info_utils.h | 4 +- engine/utils/widechar_conv.h | 49 +++++++++++ 24 files changed, 340 insertions(+), 140 deletions(-) create mode 100644 engine/utils/widechar_conv.h diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index bdbb67ed8..b53eb7fdf 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -28,6 +28,10 @@ if(MSVC) $<$:/MTd> #---|-- Statically link the runtime libraries $<$:/MT> #--| ) + + add_compile_options(/utf-8) + add_definitions(-DUNICODE -D_UNICODE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DUNICODE /D_UNICODE") endif() if(NOT DEFINED CORTEX_VARIANT) diff --git a/engine/cli/CMakeLists.txt b/engine/cli/CMakeLists.txt index ce6f254ca..c69e7e150 100644 --- a/engine/cli/CMakeLists.txt +++ b/engine/cli/CMakeLists.txt @@ -26,6 +26,10 @@ if(MSVC) $<$:/MTd> #---|-- Statically link the runtime libraries $<$:/MT> #--| ) + + add_compile_options(/utf-8) + add_definitions(-DUNICODE -D_UNICODE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DUNICODE /D_UNICODE") endif() if(NOT DEFINED CORTEX_VARIANT) diff --git a/engine/cli/commands/server_start_cmd.cc b/engine/cli/commands/server_start_cmd.cc index e039e5329..5ba972463 100644 --- a/engine/cli/commands/server_start_cmd.cc +++ b/engine/cli/commands/server_start_cmd.cc @@ -2,6 +2,7 @@ #include "commands/cortex_upd_cmd.h" #include "utils/cortex_utils.h" #include "utils/file_manager_utils.h" +#include "utils/widechar_conv.h" namespace commands { @@ -57,24 +58,32 @@ bool ServerStartCmd::Exec(const std::string& host, int port, ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - std::string params = "--start-server"; - params += " --config_file_path " + get_config_file_path(); - params += " --data_folder_path " + get_data_folder_path(); - params += " --loglevel " + log_level_; - std::string cmds = cortex_utils::GetCurrentPath() + "/" + exe + " " + params; + std::wstring params = L"--start-server"; + params += L" --config_file_path " + + file_manager_utils::GetConfigurationPath().wstring(); + params += L" --data_folder_path " + + file_manager_utils::GetCortexDataPath().wstring(); + params += L" --loglevel " + cortex::wc::Utf8ToWstring(log_level_); + std::wstring exe_w = cortex::wc::Utf8ToWstring(exe); + std::wstring current_path_w = + file_manager_utils::GetExecutableFolderContainerPath().wstring(); + std::wstring wcmds = current_path_w + L"/" + exe_w + L" " + params; + CTL_DBG("wcmds: " << wcmds); + std::vector mutable_cmds(wcmds.begin(), wcmds.end()); + mutable_cmds.push_back(L'\0'); // Create child process if (!CreateProcess( NULL, // No module name (use command line) - const_cast( - cmds.c_str()), // Command line (replace with your actual executable) - NULL, // Process handle not inheritable - NULL, // Thread handle not inheritable - FALSE, // Set handle inheritance to FALSE - 0, // No creation flags - NULL, // Use parent's environment block - NULL, // Use parent's starting directory - &si, // Pointer to STARTUPINFO structure - &pi)) // Pointer to PROCESS_INFORMATION structure + mutable_cmds + .data(), // Command line (replace with your actual executable) + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Set handle inheritance + 0, // No creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi)) // Pointer to PROCESS_INFORMATION structure { std::cout << "Could not start server: " << GetLastError() << std::endl; return false; @@ -115,7 +124,8 @@ bool ServerStartCmd::Exec(const std::string& host, int port, std::string p = cortex_utils::GetCurrentPath() + "/" + exe; execl(p.c_str(), exe.c_str(), "--start-server", "--config_file_path", get_config_file_path().c_str(), "--data_folder_path", - get_data_folder_path().c_str(), "--loglevel", log_level_.c_str(), (char*)0); + get_data_folder_path().c_str(), "--loglevel", log_level_.c_str(), + (char*)0); } else { // Parent process if (!TryConnectToServer(host, port)) { diff --git a/engine/cli/main.cc b/engine/cli/main.cc index 49cdf4be9..a03c5adf0 100644 --- a/engine/cli/main.cc +++ b/engine/cli/main.cc @@ -25,6 +25,9 @@ #error "Unsupported platform!" #endif +#include +#include + void RemoveBinaryTempFileIfExists() { auto temp = file_manager_utils::GetExecutableFolderContainerPath() / "cortex_temp"; @@ -40,11 +43,20 @@ void RemoveBinaryTempFileIfExists() { void SetupLogger(trantor::FileLogger& async_logger, bool verbose) { if (!verbose) { auto config = file_manager_utils::GetCortexConfig(); + std::filesystem::create_directories( +#if defined(_WIN32) + std::filesystem::u8path(config.logFolderPath) / +#else std::filesystem::path(config.logFolderPath) / +#endif std::filesystem::path(cortex_utils::logs_folder)); - async_logger.setFileName(config.logFolderPath + "/" + - cortex_utils::logs_cli_base_name); + + // Do not need to use u8path here because trantor handles itself + async_logger.setFileName( + (std::filesystem::path(config.logFolderPath) / + std::filesystem::path(cortex_utils::logs_cli_base_name)) + .string()); async_logger.setMaxLines(config.maxLogLines); // Keep last 100000 lines async_logger.startLogging(); trantor::Logger::setOutputFunction( @@ -192,8 +204,7 @@ int main(int argc, char* argv[]) { // Check if server exists, if not notify to user to install server auto exe = commands::GetCortexServerBinary(); auto server_binary_path = - std::filesystem::path(cortex_utils::GetCurrentPath()) / - std::filesystem::path(exe); + file_manager_utils::GetExecutableFolderContainerPath() / exe; if (!std::filesystem::exists(server_binary_path)) { std::cout << CORTEX_CPP_VERSION << " requires server binary, to install server, run: " diff --git a/engine/database/database.h b/engine/database/database.h index 27c75e923..dbe58cc4b 100644 --- a/engine/database/database.h +++ b/engine/database/database.h @@ -20,7 +20,7 @@ class Database { private: Database() - : db_(file_manager_utils::GetCortexDataPath().string() + "/cortex.db", + : db_(file_manager_utils::GetCortexDataPath() / "cortex.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE) {} SQLite::Database db_; }; diff --git a/engine/e2e-test/test_api_engine_uninstall.py b/engine/e2e-test/test_api_engine_uninstall.py index 06c3c241c..2a491d07a 100644 --- a/engine/e2e-test/test_api_engine_uninstall.py +++ b/engine/e2e-test/test_api_engine_uninstall.py @@ -1,4 +1,5 @@ import pytest +import time import requests from test_runner import ( run, @@ -21,26 +22,26 @@ def setup_and_teardown(self): # Teardown stop_server() - - def test_engines_uninstall_llamacpp_should_be_successful(self): - # install first, using cli for synchronously - run( - "Install Engine", - ["engines", "install", "llama-cpp"], - timeout=120, - capture=False, - ) + + @pytest.mark.asyncio + async def test_engines_uninstall_llamacpp_should_be_successful(self): + response = requests.post("http://localhost:3928/v1/engines/llama-cpp/install") + assert response.status_code == 200 + await wait_for_websocket_download_success_event(timeout=None) + time.sleep(30) + response = requests.delete("http://localhost:3928/v1/engines/llama-cpp/install") assert response.status_code == 200 - def test_engines_uninstall_llamacpp_with_only_version_should_be_failed(self): + @pytest.mark.asyncio + async def test_engines_uninstall_llamacpp_with_only_version_should_be_failed(self): # install first - run( - "Install Engine", - ["engines", "install", "llama-cpp", "-v", "v0.1.35"], - timeout=None, - capture=False, + data = {"variant": "mac-arm64"} + install_response = requests.post( + "http://127.0.0.1:3928/v1/engines/llama-cpp/install", json=data ) + await wait_for_websocket_download_success_event(timeout=120) + assert install_response.status_code == 200 data = {"version": "v0.1.35"} response = requests.delete( diff --git a/engine/e2e-test/test_api_model_start.py b/engine/e2e-test/test_api_model_start.py index d6a98a78b..b3e33d113 100644 --- a/engine/e2e-test/test_api_model_start.py +++ b/engine/e2e-test/test_api_model_start.py @@ -1,8 +1,10 @@ import pytest +import time import requests from test_runner import run, start_server, stop_server - - +from test_runner import ( + wait_for_websocket_download_success_event +) class TestApiModelStart: @pytest.fixture(autouse=True) @@ -12,20 +14,28 @@ def setup_and_teardown(self): success = start_server() if not success: raise Exception("Failed to start server") - run("Install engine", ["engines", "install", "llama-cpp"], 5 * 60) run("Delete model", ["models", "delete", "tinyllama:gguf"]) - run( - "Pull model", - ["pull", "tinyllama:gguf"], - timeout=None, - ) yield # Teardown stop_server() - - def test_models_start_should_be_successful(self): + + @pytest.mark.asyncio + async def test_models_start_should_be_successful(self): + response = requests.post("http://localhost:3928/v1/engines/llama-cpp/install") + assert response.status_code == 200 + await wait_for_websocket_download_success_event(timeout=None) + # TODO(sang) need to fix for cuda download + time.sleep(30) + + json_body = { + "model": "tinyllama:gguf" + } + response = requests.post("http://localhost:3928/v1/models/pull", json=json_body) + assert response.status_code == 200, f"Failed to pull model: tinyllama:gguf" + await wait_for_websocket_download_success_event(timeout=None) + json_body = {"model": "tinyllama:gguf"} response = requests.post( "http://localhost:3928/v1/models/start", json=json_body diff --git a/engine/e2e-test/test_api_model_stop.py b/engine/e2e-test/test_api_model_stop.py index dc3b6b77b..4fc7a55e2 100644 --- a/engine/e2e-test/test_api_model_stop.py +++ b/engine/e2e-test/test_api_model_stop.py @@ -1,7 +1,10 @@ import pytest +import time import requests from test_runner import run, start_server, stop_server - +from test_runner import ( + wait_for_websocket_download_success_event +) class TestApiModelStop: @@ -13,14 +16,19 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - run("Install engine", ["engines", "install", "llama-cpp"], 5 * 60) yield run("Uninstall engine", ["engines", "uninstall", "llama-cpp"]) # Teardown stop_server() - def test_models_stop_should_be_successful(self): + @pytest.mark.asyncio + async def test_models_stop_should_be_successful(self): + response = requests.post("http://localhost:3928/v1/engines/llama-cpp/install") + assert response.status_code == 200 + await wait_for_websocket_download_success_event(timeout=None) + time.sleep(30) + json_body = {"model": "tinyllama:gguf"} response = requests.post( "http://localhost:3928/v1/models/start", json=json_body diff --git a/engine/e2e-test/test_cli_engine_install.py b/engine/e2e-test/test_cli_engine_install.py index 380334222..a998f3183 100644 --- a/engine/e2e-test/test_cli_engine_install.py +++ b/engine/e2e-test/test_cli_engine_install.py @@ -19,6 +19,7 @@ def setup_and_teardown(self): # Teardown stop_server() + @pytest.mark.skipif(platform.system() == "Windows", reason="Progress bar log issue on Windows") def test_engines_install_llamacpp_should_be_successfully(self): exit_code, output, error = run( "Install Engine", @@ -46,6 +47,7 @@ def test_engines_install_onnx_on_tensorrt_should_be_failed(self): assert "is not supported on" in output, "Should display error message" assert exit_code == 0, f"Install engine failed with error: {error}" + @pytest.mark.skipif(platform.system() == "Windows", reason="Progress bar log issue on Windows") def test_engines_install_pre_release_llamacpp(self): engine_version = "v0.1.29" exit_code, output, error = run( @@ -67,6 +69,7 @@ def test_engines_install_pre_release_llamacpp(self): assert is_engine_version_exist, f"Engine version {engine_version} is not found" assert exit_code == 0, f"Install engine failed with error: {error}" + @pytest.mark.skipif(platform.system() == "Windows", reason="Progress bar log issue on Windows") def test_engines_should_fallback_to_download_llamacpp_engine_if_not_exists(self): exit_code, output, error = run( "Install Engine", diff --git a/engine/e2e-test/test_cli_engine_uninstall.py b/engine/e2e-test/test_cli_engine_uninstall.py index ede5e9758..fcc5f5c73 100644 --- a/engine/e2e-test/test_cli_engine_uninstall.py +++ b/engine/e2e-test/test_cli_engine_uninstall.py @@ -25,7 +25,7 @@ def setup_and_teardown(self): @pytest.mark.asyncio async def test_engines_uninstall_llamacpp_should_be_successfully(self): requests.post("http://127.0.0.1:3928/v1/engines/llama-cpp/install") - await wait_for_websocket_download_success_event(timeout=120) + await wait_for_websocket_download_success_event(timeout=None) exit_code, output, error = run( "Uninstall engine", ["engines", "uninstall", "llama-cpp"] ) diff --git a/engine/e2e-test/test_cli_model_delete.py b/engine/e2e-test/test_cli_model_delete.py index f7ab53058..d0ba43ec1 100644 --- a/engine/e2e-test/test_cli_model_delete.py +++ b/engine/e2e-test/test_cli_model_delete.py @@ -1,6 +1,10 @@ import pytest +import requests from test_runner import popen, run from test_runner import start_server, stop_server +from test_runner import ( + wait_for_websocket_download_success_event +) class TestCliModelDelete: @@ -11,10 +15,6 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - # Pull model - - run("Pull model", ["pull", "tinyllama:gguf"], timeout=None,) - yield # Teardown @@ -22,7 +22,15 @@ def setup_and_teardown(self): run("Delete model", ["models", "delete", "tinyllama:gguf"]) stop_server() - def test_models_delete_should_be_successful(self): + @pytest.mark.asyncio + async def test_models_delete_should_be_successful(self): + json_body = { + "model": "tinyllama:gguf" + } + response = requests.post("http://localhost:3928/v1/models/pull", json=json_body) + assert response.status_code == 200, f"Failed to pull model: tinyllama:gguf" + await wait_for_websocket_download_success_event(timeout=None) + exit_code, output, error = run( "Delete model", ["models", "delete", "tinyllama:gguf"] ) diff --git a/engine/main.cc b/engine/main.cc index afce8f3d3..b39c4c6e2 100644 --- a/engine/main.cc +++ b/engine/main.cc @@ -22,6 +22,7 @@ #include "utils/file_manager_utils.h" #include "utils/logging_utils.h" #include "utils/system_info_utils.h" +#include "utils/widechar_conv.h" #if defined(__APPLE__) && defined(__MACH__) #include // for dirname() @@ -66,7 +67,11 @@ void RunServer(std::optional port, bool ignore_cout) { } // Create logs/ folder and setup log to file std::filesystem::create_directories( +#if defined(_WIN32) + std::filesystem::u8path(config.logFolderPath) / +#else std::filesystem::path(config.logFolderPath) / +#endif std::filesystem::path(cortex_utils::logs_folder)); static trantor::FileLogger asyncFileLogger; asyncFileLogger.setFileName( @@ -200,7 +205,11 @@ void RunServer(std::optional port, bool ignore_cout) { } } +#if defined(_WIN32) +int wmain(int argc, wchar_t* argv[]) { +#else int main(int argc, char* argv[]) { +#endif // Stop the program if the system is not supported auto system_info = system_info_utils::GetSystemInfo(); if (system_info->arch == system_info_utils::kUnsupported || @@ -213,17 +222,30 @@ int main(int argc, char* argv[]) { // avoid printing logs to terminal is_server = true; - // check if migration is needed - if (auto res = cortex::migr::MigrationManager( - cortex::db::Database::GetInstance().db()) - .Migrate(); - res.has_error()) { - CLI_LOG("Error: " << res.error()); - return 1; - } - std::optional server_port; bool ignore_cout_log = false; +#if defined(_WIN32) + for (int i = 0; i < argc; i++) { + std::wstring command = argv[i]; + if (command == L"--config_file_path") { + std::wstring v = argv[i + 1]; + file_manager_utils::cortex_config_file_path = + cortex::wc::WstringToUtf8(v); + } else if (command == L"--data_folder_path") { + std::wstring v = argv[i + 1]; + file_manager_utils::cortex_data_folder_path = + cortex::wc::WstringToUtf8(v); + } else if (command == L"--port") { + server_port = std::stoi(argv[i + 1]); + } else if (command == L"--ignore_cout") { + ignore_cout_log = true; + } else if (command == L"--loglevel") { + std::wstring v = argv[i + 1]; + std::string log_level = cortex::wc::WstringToUtf8(v); + logging_utils_helper::SetLogLevel(log_level, ignore_cout_log); + } + } +#else for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--config_file_path") == 0) { file_manager_utils::cortex_config_file_path = argv[i + 1]; @@ -238,6 +260,7 @@ int main(int argc, char* argv[]) { logging_utils_helper::SetLogLevel(log_level, ignore_cout_log); } } +#endif { auto result = file_manager_utils::CreateConfigFileIfNotExist(); @@ -263,6 +286,15 @@ int main(int argc, char* argv[]) { } } + // check if migration is needed + if (auto res = cortex::migr::MigrationManager( + cortex::db::Database::GetInstance().db()) + .Migrate(); + res.has_error()) { + CLI_LOG("Error: " << res.error()); + return 1; + } + // Delete temporary file if it exists auto temp = file_manager_utils::GetExecutableFolderContainerPath() / "cortex_temp"; @@ -274,26 +306,26 @@ int main(int argc, char* argv[]) { } } - // Check if this process is for python execution - if (argc > 1) { - if (strcmp(argv[1], "--run_python_file") == 0) { - std::string py_home_path = (argc > 3) ? argv[3] : ""; - std::unique_ptr dl; - try { - std::string abs_path = - cortex_utils::GetCurrentPath() + kPythonRuntimeLibPath; - dl = std::make_unique(abs_path, "engine"); - } catch (const cortex_cpp::dylib::load_error& e) { - LOG_ERROR << "Could not load engine: " << e.what(); - return 1; - } + // // Check if this process is for python execution + // if (argc > 1) { + // if (strcmp(argv[1], "--run_python_file") == 0) { + // std::string py_home_path = (argc > 3) ? argv[3] : ""; + // std::unique_ptr dl; + // try { + // std::string abs_path = + // cortex_utils::GetCurrentPath() + kPythonRuntimeLibPath; + // dl = std::make_unique(abs_path, "engine"); + // } catch (const cortex_cpp::dylib::load_error& e) { + // LOG_ERROR << "Could not load engine: " << e.what(); + // return 1; + // } - auto func = dl->get_function("get_engine"); - auto e = func(); - e->ExecutePythonFile(argv[0], argv[2], py_home_path); - return 0; - } - } + // auto func = dl->get_function("get_engine"); + // auto e = func(); + // e->ExecutePythonFile(argv[0], argv[2], py_home_path); + // return 0; + // } + // } RunServer(server_port, ignore_cout_log); return 0; diff --git a/engine/migrations/migration_helper.cc b/engine/migrations/migration_helper.cc index afebae5aa..f2b39d77e 100644 --- a/engine/migrations/migration_helper.cc +++ b/engine/migrations/migration_helper.cc @@ -2,12 +2,13 @@ namespace cortex::migr { cpp::result MigrationHelper::BackupDatabase( - const std::string& src_db_path, const std::string& backup_db_path) { + const std::filesystem::path& src_db_path, + const std::string& backup_db_path) { try { SQLite::Database src_db(src_db_path, SQLite::OPEN_READONLY); sqlite3* backup_db; - if (sqlite3_open(backup_db_path.c_str(), &backup_db) != SQLITE_OK) { + if (sqlite3_open16(backup_db_path.c_str(), &backup_db) != SQLITE_OK) { throw std::runtime_error("Failed to open backup database"); } @@ -35,13 +36,14 @@ cpp::result MigrationHelper::BackupDatabase( } cpp::result MigrationHelper::RestoreDatabase( - const std::string& backup_db_path, const std::string& target_db_path) { + const std::string& backup_db_path, + const std::filesystem::path& target_db_path) { try { SQLite::Database target_db(target_db_path, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); sqlite3* backup_db; - if (sqlite3_open(backup_db_path.c_str(), &backup_db) != SQLITE_OK) { + if (sqlite3_open16(backup_db_path.c_str(), &backup_db) != SQLITE_OK) { throw std::runtime_error("Failed to open backup database"); } diff --git a/engine/migrations/migration_helper.h b/engine/migrations/migration_helper.h index ff0ee5075..cdf7b8f55 100644 --- a/engine/migrations/migration_helper.h +++ b/engine/migrations/migration_helper.h @@ -2,6 +2,7 @@ #include #include +#include #include "utils/logging_utils.h" #include "utils/result.hpp" @@ -9,9 +10,11 @@ namespace cortex::migr { class MigrationHelper { public: cpp::result BackupDatabase( - const std::string& src_db_path, const std::string& backup_db_path); + const std::filesystem::path& src_db_path, + const std::string& backup_db_path); cpp::result RestoreDatabase( - const std::string& backup_db_path, const std::string& target_db_path); + const std::string& backup_db_path, + const std::filesystem::path& target_db_path); }; } // namespace cortex::migr diff --git a/engine/migrations/migration_manager.cc b/engine/migrations/migration_manager.cc index b2920722f..f4b4f8046 100644 --- a/engine/migrations/migration_manager.cc +++ b/engine/migrations/migration_manager.cc @@ -38,9 +38,9 @@ cpp::result MigrationManager::Migrate() { return true; // Back up all data before migrating if (std::filesystem::exists(fmu::GetCortexDataPath() / kCortexDb)) { - auto src_db_path = (fmu::GetCortexDataPath() / kCortexDb).string(); - auto backup_db_path = (fmu::GetCortexDataPath() / kCortexDbBackup).string(); - if (auto res = mgr_helper_.BackupDatabase(src_db_path, backup_db_path); + auto src_db_path = (fmu::GetCortexDataPath() / kCortexDb); + auto backup_db_path = (fmu::GetCortexDataPath() / kCortexDbBackup); + if (auto res = mgr_helper_.BackupDatabase(src_db_path, backup_db_path.string()); res.has_error()) { CTL_INF("Error: backup database failed!"); return res; @@ -60,9 +60,9 @@ cpp::result MigrationManager::Migrate() { }); auto restore_db = [this]() -> cpp::result { - auto src_db_path = (fmu::GetCortexDataPath() / kCortexDb).string(); - auto backup_db_path = (fmu::GetCortexDataPath() / kCortexDbBackup).string(); - return mgr_helper_.BackupDatabase(src_db_path, backup_db_path); + auto src_db_path = (fmu::GetCortexDataPath() / kCortexDb); + auto backup_db_path = (fmu::GetCortexDataPath() / kCortexDbBackup); + return mgr_helper_.BackupDatabase(src_db_path, backup_db_path.string()); }; // Backup folder structure diff --git a/engine/services/engine_service.cc b/engine/services/engine_service.cc index 40356f163..4eebff669 100644 --- a/engine/services/engine_service.cc +++ b/engine/services/engine_service.cc @@ -4,6 +4,7 @@ #include #include "algorithm" #include "utils/archive_utils.h" +#include "utils/cortex_utils.h" #include "utils/engine_constants.h" #include "utils/engine_matcher_utils.h" #include "utils/file_manager_utils.h" @@ -664,14 +665,17 @@ cpp::result EngineService::LoadEngine( CTL_INF("Selected engine variant: " << json_helper::DumpJsonString(selected_engine_variant->ToJson())); - +#if defined(_WIN32) + auto user_defined_engine_path = _wgetenv(L"ENGINE_PATH"); +#else auto user_defined_engine_path = getenv("ENGINE_PATH"); +#endif + CTL_DBG("user defined engine path: " << user_defined_engine_path); const std::filesystem::path engine_dir_path = [&] { if (user_defined_engine_path != nullptr) { - return std::filesystem::path(user_defined_engine_path + - GetEnginePath(ne)) / - selected_engine_variant->variant / + return std::filesystem::path(user_defined_engine_path) / + GetEnginePath(ne) / selected_engine_variant->variant / selected_engine_variant->version; } else { return file_manager_utils::GetEnginesContainerPath() / ne / @@ -701,9 +705,9 @@ cpp::result EngineService::LoadEngine( // Do nothing, llamacpp can re-use tensorrt-llm dependencies (need to be tested careful) // 3. Add dll directory if met other conditions - auto add_dll = [this](const std::string& e_type, const std::string& p) { - auto ws = std::wstring(p.begin(), p.end()); - if (auto cookie = AddDllDirectory(ws.c_str()); cookie != 0) { + auto add_dll = [this](const std::string& e_type, + const std::filesystem::path& p) { + if (auto cookie = AddDllDirectory(p.c_str()); cookie != 0) { CTL_DBG("Added dll directory: " << p); engines_[e_type].cookie = cookie; } else { @@ -720,7 +724,11 @@ cpp::result EngineService::LoadEngine( } }; +#if defined(_WIN32) + if (bool should_use_dll_search_path = !(_wgetenv(L"ENGINE_PATH")); +#else if (bool should_use_dll_search_path = !(getenv("ENGINE_PATH")); +#endif should_use_dll_search_path) { if (IsEngineLoaded(kLlamaRepo) && ne == kTrtLlmRepo && should_use_dll_search_path) { @@ -736,11 +744,11 @@ cpp::result EngineService::LoadEngine( CTL_DBG("Removed cuda dll directory: " << kLlamaRepo); } - add_dll(ne, engine_dir_path.string()); + add_dll(ne, engine_dir_path); } else if (IsEngineLoaded(kTrtLlmRepo) && ne == kLlamaRepo) { // Do nothing } else { - add_dll(ne, engine_dir_path.string()); + add_dll(ne, engine_dir_path); } } #endif diff --git a/engine/services/hardware_service.cc b/engine/services/hardware_service.cc index 02693a48d..16ae234b4 100644 --- a/engine/services/hardware_service.cc +++ b/engine/services/hardware_service.cc @@ -10,6 +10,7 @@ #include "database/hardware.h" #include "services/engine_service.h" #include "utils/cortex_utils.h" +#include "utils/widechar_conv.h" namespace services { @@ -115,24 +116,33 @@ bool HardwareService::Restart(const std::string& host, int port) { ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); - std::string params = "--ignore_cout"; - params += " --config_file_path " + get_config_file_path(); - params += " --data_folder_path " + get_data_folder_path(); - params += " --loglevel " + luh::LogLevelStr(luh::global_log_level); - std::string cmds = cortex_utils::GetCurrentPath() + "/" + exe + " " + params; + // TODO (sang) write a common function for this and server_start_cmd + std::wstring params = L"--ignore_cout"; + params += L" --config_file_path " + + file_manager_utils::GetConfigurationPath().wstring(); + params += L" --data_folder_path " + + file_manager_utils::GetCortexDataPath().wstring(); + params += L" --loglevel " + + cortex::wc::Utf8ToWstring(luh::LogLevelStr(luh::global_log_level)); + std::wstring exe_w = cortex::wc::Utf8ToWstring(exe); + std::wstring current_path_w = + file_manager_utils::GetExecutableFolderContainerPath().wstring(); + std::wstring wcmds = current_path_w + L"/" + exe_w + L" " + params; + std::vector mutable_cmds(wcmds.begin(), wcmds.end()); + mutable_cmds.push_back(L'\0'); // Create child process if (!CreateProcess( NULL, // No module name (use command line) - const_cast( - cmds.c_str()), // Command line (replace with your actual executable) - NULL, // Process handle not inheritable - NULL, // Thread handle not inheritable - TRUE, // Handle inheritance - 0, // No creation flags - NULL, // Use parent's environment block - NULL, // Use parent's starting directory - &si, // Pointer to STARTUPINFO structure - &pi)) // Pointer to PROCESS_INFORMATION structure + mutable_cmds + .data(), // Command line (replace with your actual executable) + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + TRUE, // Handle inheritance + 0, // No creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi)) // Pointer to PROCESS_INFORMATION structure { std::cout << "Could not start server: " << GetLastError() << std::endl; return false; diff --git a/engine/services/model_service.cc b/engine/services/model_service.cc index d6e66a717..1ec1a68cf 100644 --- a/engine/services/model_service.cc +++ b/engine/services/model_service.cc @@ -9,12 +9,14 @@ #include "hardware_service.h" #include "httplib.h" #include "utils/cli_selection_utils.h" +#include "utils/cortex_utils.h" #include "utils/engine_constants.h" #include "utils/file_manager_utils.h" #include "utils/huggingface_utils.h" #include "utils/logging_utils.h" #include "utils/result.hpp" #include "utils/string_utils.h" +#include "utils/widechar_conv.h" namespace { void ParseGguf(const DownloadItem& ggufDownloadItem, @@ -458,7 +460,8 @@ ModelService::DownloadModelFromCortexsoAsync( return; } auto url_obj = url_parser::FromUrlString(model_yml_item->downloadUrl); - CTL_INF("Adding model to modellist with branch: " << branch); + CTL_INF("Adding model to modellist with branch: " + << branch << ", path: " << model_yml_item->localPath.string()); config::YamlHandler yaml_handler; yaml_handler.ModelConfigFromFile(model_yml_item->localPath.string()); auto mc = yaml_handler.GetModelConfig(); @@ -666,9 +669,13 @@ cpp::result ModelService::StartModel( json_data = mc.ToJson(); if (mc.files.size() > 0) { - // TODO(sang) support multiple files +#if defined(_WIN32) + json_data["model_path"] = cortex::wc::WstringToUtf8( + fmu::ToAbsoluteCortexDataPath(fs::path(mc.files[0])).wstring()); +#else json_data["model_path"] = fmu::ToAbsoluteCortexDataPath(fs::path(mc.files[0])).string(); +#endif } else { LOG_WARN << "model_path is empty"; return StartModelResult{.success = false}; diff --git a/engine/utils/config_yaml_utils.h b/engine/utils/config_yaml_utils.h index 187e1b4ef..3176339a0 100644 --- a/engine/utils/config_yaml_utils.h +++ b/engine/utils/config_yaml_utils.h @@ -86,6 +86,9 @@ class CortexConfigMgr { if (!out_file) { throw std::runtime_error("Failed to open output file."); } + // Workaround to save file as utf8 BOM + const unsigned char utf8_bom[] = {0xEF, 0xBB, 0xBF}; + out_file.write(reinterpret_cast(utf8_bom), sizeof(utf8_bom)); YAML::Node node; node["logFolderPath"] = config.logFolderPath; node["logLlamaCppPath"] = config.logLlamaCppPath; diff --git a/engine/utils/cortex_utils.h b/engine/utils/cortex_utils.h index eb142d6b5..895217250 100644 --- a/engine/utils/cortex_utils.h +++ b/engine/utils/cortex_utils.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -17,10 +18,16 @@ #include #endif -#if __APPLE__ +#if defined(__APPLE__) #include #endif +#if defined(_WIN32) +#include +#include +#include +#endif + namespace cortex_utils { inline std::string logs_folder = "./logs"; inline std::string logs_base_name = "./logs/cortex.log"; @@ -74,20 +81,19 @@ inline drogon::HttpResponsePtr CreateCortexStreamResponse( return res; } + + #if defined(_WIN32) inline std::string GetCurrentPath() { - wchar_t path[MAX_PATH]; - DWORD result = GetModuleFileNameW(NULL, path, MAX_PATH); + char path[MAX_PATH]; + DWORD result = GetModuleFileNameA(NULL, path, MAX_PATH); if (result == 0) { - std::wcerr << L"Error getting module file name." << std::endl; + std::cerr << "Error getting module file name." << std::endl; return ""; } - std::wstring::size_type pos = std::wstring(path).find_last_of(L"\\/"); - auto ws = std::wstring(path).substr(0, pos); - std::string res; - std::transform(ws.begin(), ws.end(), std::back_inserter(res), - [](wchar_t c) { return (char)c; }); - return res; + + std::string::size_type pos = std::string(path).find_last_of("\\/"); + return std::string(path).substr(0, pos); } #else inline std::string GetCurrentPath() { diff --git a/engine/utils/engine_constants.h b/engine/utils/engine_constants.h index c63a58ab9..5dab49936 100644 --- a/engine/utils/engine_constants.h +++ b/engine/utils/engine_constants.h @@ -9,7 +9,7 @@ constexpr const auto kLlamaRepo = "cortex.llamacpp"; constexpr const auto kTrtLlmRepo = "cortex.tensorrt-llm"; constexpr const auto kPythonRuntimeRepo = "cortex.python"; -constexpr const auto kLlamaLibPath = "/engines/cortex.llamacpp"; +constexpr const auto kLlamaLibPath = "./engines/cortex.llamacpp"; constexpr const auto kPythonRuntimeLibPath = "/engines/cortex.python"; constexpr const auto kOnnxLibPath = "/engines/cortex.onnx"; constexpr const auto kTensorrtLlmPath = "/engines/cortex.tensorrt-llm"; diff --git a/engine/utils/file_manager_utils.h b/engine/utils/file_manager_utils.h index 399afcfa6..72310385c 100644 --- a/engine/utils/file_manager_utils.h +++ b/engine/utils/file_manager_utils.h @@ -7,6 +7,7 @@ #include "utils/config_yaml_utils.h" #include "utils/engine_constants.h" #include "utils/result.hpp" +#include "utils/widechar_conv.h" #if defined(__APPLE__) && defined(__MACH__) #include @@ -14,6 +15,8 @@ #include #elif defined(_WIN32) #include +#include +#include #endif namespace file_manager_utils { @@ -55,8 +58,8 @@ inline std::filesystem::path GetExecutableFolderContainerPath() { return std::filesystem::current_path(); } #elif defined(_WIN32) - char buffer[MAX_PATH]; - GetModuleFileNameA(NULL, buffer, MAX_PATH); + wchar_t buffer[MAX_PATH]; + GetModuleFileNameW(NULL, buffer, MAX_PATH); // CTL_DBG("Executable path: " << buffer); return std::filesystem::path{buffer}.parent_path(); #else @@ -67,11 +70,11 @@ inline std::filesystem::path GetExecutableFolderContainerPath() { inline std::filesystem::path GetHomeDirectoryPath() { #ifdef _WIN32 - const char* homeDir = std::getenv("USERPROFILE"); + const wchar_t* homeDir = _wgetenv(L"USERPROFILE"); if (!homeDir) { // Fallback if USERPROFILE is not set - const char* homeDrive = std::getenv("HOMEDRIVE"); - const char* homePath = std::getenv("HOMEPATH"); + const wchar_t* homeDrive = _wgetenv(L"HOMEDRIVE"); + const wchar_t* homePath = _wgetenv(L"HOMEPATH"); if (homeDrive && homePath) { return std::filesystem::path(homeDrive) / std::filesystem::path(homePath); } else { @@ -103,8 +106,12 @@ inline std::filesystem::path GetConfigurationPath() { } if (config_file_path != kDefaultConfigurationPath) { - // CTL_INF("Config file path: " + config_file_path); +// CTL_INF("Config file path: " + config_file_path); +#if defined(_WIN32) + return std::filesystem::u8path(config_file_path); +#else return std::filesystem::path(config_file_path); +#endif } std::string variant{CORTEX_VARIANT}; @@ -162,11 +169,21 @@ inline config_yaml_utils::CortexConfig GetDefaultConfig() { : std::filesystem::path(cortex_data_folder_path); return config_yaml_utils::CortexConfig{ +#if defined(_WIN32) + .logFolderPath = + cortex::wc::WstringToUtf8(default_data_folder_path.wstring()), +#else .logFolderPath = default_data_folder_path.string(), +#endif .logLlamaCppPath = kLogsLlamacppBaseName, .logTensorrtLLMPath = kLogsTensorrtllmBaseName, .logOnnxPath = kLogsOnnxBaseName, +#if defined(_WIN32) + .dataFolderPath = + cortex::wc::WstringToUtf8(default_data_folder_path.wstring()), +#else .dataFolderPath = default_data_folder_path.string(), +#endif .maxLogLines = config_yaml_utils::kDefaultMaxLines, .apiServerHost = config_yaml_utils::kDefaultHost, .apiServerPort = config_yaml_utils::kDefaultPort, @@ -220,7 +237,11 @@ inline std::filesystem::path GetCortexDataPath() { auto config = GetCortexConfig(); std::filesystem::path data_folder_path; if (!config.dataFolderPath.empty()) { +#if defined(_WIN32) + data_folder_path = std::filesystem::u8path(config.dataFolderPath); +#else data_folder_path = std::filesystem::path(config.dataFolderPath); +#endif } else { auto home_path = GetHomeDirectoryPath(); data_folder_path = home_path / kCortexFolderName; diff --git a/engine/utils/system_info_utils.h b/engine/utils/system_info_utils.h index 6183c3095..013069699 100644 --- a/engine/utils/system_info_utils.h +++ b/engine/utils/system_info_utils.h @@ -87,8 +87,8 @@ inline std::unique_ptr GetSystemInfo() { inline bool IsNvidiaSmiAvailable() { #ifdef _WIN32 // Check if nvidia-smi.exe exists in the PATH on Windows - char buffer[MAX_PATH]; - if (SearchPath(NULL, "nvidia-smi.exe", NULL, MAX_PATH, buffer, NULL) != 0) { + wchar_t buffer[MAX_PATH]; + if (SearchPath(NULL, L"nvidia-smi.exe", NULL, MAX_PATH, buffer, NULL) != 0) { return true; } else { return false; diff --git a/engine/utils/widechar_conv.h b/engine/utils/widechar_conv.h new file mode 100644 index 000000000..e979be3c1 --- /dev/null +++ b/engine/utils/widechar_conv.h @@ -0,0 +1,49 @@ +#pragma once + +#if defined(_WIN32) +#include +#include + +namespace cortex::wc { + +inline std::string WstringToUtf8(const std::wstring& wstr) { + if (wstr.empty()) { + return std::string(); + } + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), NULL, 0, NULL, NULL); + if (size_needed <= 0) { + throw std::runtime_error("WideCharToMultiByte() failed: " + std::to_string(GetLastError())); + } + + std::string result(size_needed, 0); + int bytes_written = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), &result[0], size_needed, NULL, NULL); + if (bytes_written <= 0) { + throw std::runtime_error("WideCharToMultiByte() failed: " + std::to_string(GetLastError())); + } + + return result; +} + +inline std::wstring Utf8ToWstring(const std::string& str) { + if (str.empty()) { + return std::wstring(); + } + + int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), NULL, 0); + if (size_needed <= 0) { + throw std::runtime_error("MultiByteToWideChar() failed: " + std::to_string(GetLastError())); + } + + std::wstring result(size_needed, 0); + int chars_written = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], size_needed); + if (chars_written <= 0) { + throw std::runtime_error("MultiByteToWideChar() failed: " + std::to_string(GetLastError())); + } + + return result; +} + +}; + +#endif \ No newline at end of file