diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fe93e4..7538c6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,10 @@ jobs: sudo apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Configure CMake - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCOREDECK_BUILD_TESTS=ON + env: + COREDECK_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCOREDECK_BUILD_TESTS=ON -DCOREDECK_SENTRY_DSN=$COREDECK_SENTRY_DSN + shell: bash - name: Build run: cmake --build build --config ${{ matrix.build_type }} --parallel diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10a831f..eeb563b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,10 @@ jobs: sudo apt-get install -y --no-install-recommends build-essential libcurl4-openssl-dev libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Configure CMake - run: cmake -B build -DCMAKE_BUILD_TYPE=Release + env: + COREDECK_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DCOREDECK_SENTRY_DSN=$COREDECK_SENTRY_DSN + shell: bash - name: Build run: cmake --build build --config Release --parallel diff --git a/.gitmodules b/.gitmodules index c9d77df..f50e780 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,6 @@ [submodule "extern/catch2"] path = extern/catch2 url = https://github.com/catchorg/Catch2.git +[submodule "extern/sentry-native"] + path = extern/sentry-native + url = https://github.com/getsentry/sentry-native.git diff --git a/CMakeLists.txt b/CMakeLists.txt index fff42ae..5002fcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,18 @@ set(TINYFD_DIR ${CMAKE_SOURCE_DIR}/extern/tinyfiledialogs) add_library(tinyfiledialogs STATIC ${TINYFD_DIR}/tinyfiledialogs.c) target_include_directories(tinyfiledialogs PUBLIC ${TINYFD_DIR}) +set(COREDECK_SENTRY_DSN "" CACHE STRING "Sentry DSN for crash reporting") +if (COREDECK_SENTRY_DSN) + set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(SENTRY_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(SENTRY_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SENTRY_BACKEND "crashpad" CACHE STRING "" FORCE) + add_subdirectory(extern/sentry-native) + message(STATUS "Sentry: Enabled") +else () + message(STATUS "Sentry: Disabled") +endif () + find_package(OpenGL REQUIRED) find_package(Threads REQUIRED) @@ -96,7 +108,9 @@ endif () add_library(coredeck_core STATIC src/core/app_settings.cpp src/core/avd.cpp + src/core/crash_reporter.cpp src/core/emulator.cpp + src/core/emulator_console.cpp src/core/file_dialog.cpp src/core/log_buffer.cpp src/core/options.cpp @@ -113,6 +127,7 @@ target_precompile_headers(coredeck_core PRIVATE src/pch.h) target_link_libraries(coredeck_core PUBLIC reflectcpp tinyfiledialogs) target_compile_definitions(coredeck_core PUBLIC + COREDECK_TITLE="CoreDeck" COREDECK_VERSION="${PROJECT_VERSION_FULL}" COREDECK_BUILD_NUMBER="${PROJECT_BUILD_NUMBER}" COREDECK_VENDOR="${APP_VENDOR}" @@ -125,9 +140,14 @@ target_compile_definitions(coredeck_core PUBLIC COREDECK_COPYRIGHT="${APP_COPYRIGHT}" ) +if (COREDECK_SENTRY_DSN) + target_compile_definitions(coredeck_core PUBLIC COREDECK_SENTRY_DSN="${COREDECK_SENTRY_DSN}") + target_link_libraries(coredeck_core PUBLIC sentry) +endif () + if (WIN32) target_compile_definitions(coredeck_core PUBLIC WIN32_LEAN_AND_MEAN NOMINMAX) - target_link_libraries(coredeck_core PUBLIC shell32 comdlg32 ole32 winhttp) + target_link_libraries(coredeck_core PUBLIC shell32 comdlg32 ole32 winhttp ws2_32) elseif (UNIX AND NOT APPLE) target_link_libraries(coredeck_core PUBLIC ${CMAKE_DL_LIBS} Threads::Threads CURL::libcurl) else () @@ -191,6 +211,23 @@ if (UNIX AND NOT APPLE) ) endif () +if (COREDECK_SENTRY_DSN) + if (APPLE) + target_link_options(${PROJECT_NAME} PRIVATE "-Wl,-no_warn_duplicate_libraries") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/MacOS/crashpad_handler + ) + else () + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/$ + ) + endif () +endif () + if (WIN32) configure_file(${CMAKE_SOURCE_DIR}/resources.rc.in ${CMAKE_BINARY_DIR}/resources.rc @ONLY) target_link_options(${PROJECT_NAME} PRIVATE "/SUBSYSTEM:WINDOWS" "/ENTRY:mainCRTStartup") @@ -237,6 +274,10 @@ install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION . ) +if (COREDECK_SENTRY_DSN AND NOT APPLE) + install(PROGRAMS $ DESTINATION .) +endif () + if (NOT APPLE) install(DIRECTORY ${CMAKE_SOURCE_DIR}/assets/fonts DESTINATION assets) endif () diff --git a/extern/sentry-native b/extern/sentry-native new file mode 160000 index 0000000..197e123 --- /dev/null +++ b/extern/sentry-native @@ -0,0 +1 @@ +Subproject commit 197e12332251cfd34064d5b7ae719a70144aa315 diff --git a/resources.rc.in b/resources.rc.in index 3ad541f..40c2dfd 100644 --- a/resources.rc.in +++ b/resources.rc.in @@ -1,6 +1,7 @@ +#pragma code_page(65001) #include -IDI_ICON1 ICON "@CMAKE_SOURCE_DIR@/assets/icons/icon.ico" +1 ICON "@CMAKE_SOURCE_DIR@/assets/icons/icon.ico" VS_VERSION_INFO VERSIONINFO FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_BUILD_NUMBER@ @@ -16,12 +17,13 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "@APP_VENDOR@" - VALUE "FileDescription", "@APP_DESCRIPTION_SHORT@" + VALUE "FileDescription", "@PROJECT_NAME@" VALUE "FileVersion", "@PROJECT_VERSION@.@PROJECT_BUILD_NUMBER@" VALUE "ProductName", "@PROJECT_NAME@" VALUE "ProductVersion", "@PROJECT_VERSION@" VALUE "LegalCopyright", "@APP_COPYRIGHT@" VALUE "OriginalFilename", "@PROJECT_NAME@.exe" + VALUE "Comments", "@APP_DESCRIPTION_SHORT@" END END BLOCK "VarFileInfo" diff --git a/src/core/avd.cpp b/src/core/avd.cpp index 9a652da..b842e83 100644 --- a/src/core/avd.cpp +++ b/src/core/avd.cpp @@ -10,7 +10,6 @@ #include "avd.h" #include "paths.h" #include "process.h" -#include "utilities.h" namespace CoreDeck { static std::unordered_map ParseConfigFile(const std::string &path) { @@ -129,8 +128,7 @@ namespace CoreDeck { std::vector avds; if (!sdk.IsFound) return avds; - const std::string cmd = StrConcat("\"", sdk.EmulatorPath, "\" -list-avds"); - const std::string output = RunCommand(cmd); + const std::string output = RunCommandArgs(sdk.EmulatorPath, {"-list-avds"}); std::istringstream stream(output); std::string line; while (std::getline(stream, line)) { @@ -144,13 +142,14 @@ namespace CoreDeck { if (sdk.AvdManagerPath.empty()) return false; if (data.Name.empty() || data.SystemImagePackagePath.empty()) return false; - std::string cmd = StrConcat( - "echo no | \"", sdk.AvdManagerPath, "\" create avd -n \"", data.Name, "\" -k \"", - data.SystemImagePackagePath, "\"" - ); - - if (!data.DeviceId.empty()) cmd = StrConcat(cmd, " -d \"", data.DeviceId, "\""); - RunCommand(cmd); + std::vector args = { + "create", "avd", "-n", data.Name, "-k", data.SystemImagePackagePath + }; + if (!data.DeviceId.empty()) { + args.emplace_back("-d"); + args.push_back(data.DeviceId); + } + RunCommandArgs(sdk.AvdManagerPath, args, "no\n"); const std::string avdDir = Paths::GetAvdDirectory(); const std::string configPath = Paths::JoinPaths({avdDir, data.Name + ".avd", "config.ini"}); @@ -179,23 +178,9 @@ namespace CoreDeck { } bool DeleteAvd(const SdkInfo &sdk, const std::string &avdName) { - if (!sdk.AvdManagerPath.empty()) { - const std::string cmd = StrConcat("\"", sdk.AvdManagerPath, "\" delete avd -n ", avdName); - RunCommand(cmd); - } else { - // Fallback: manually delete the AVD files - const std::string avdDir = Paths::GetAvdDirectory(); - if (avdDir.empty()) return false; - - const std::string avdFolder = Paths::JoinPaths({avdDir, avdName + ".avd"}); - const std::string avdIni = Paths::JoinPaths({avdDir, avdName + ".ini"}); - - std::error_code ec; - std::filesystem::remove_all(avdFolder, ec); - if (ec) return false; + if (sdk.AvdManagerPath.empty()) return false; - std::filesystem::remove(avdIni, ec); - } + RunCommandArgs(sdk.AvdManagerPath, {"delete", "avd", "-n", avdName}); const std::string avdDir = Paths::GetAvdDirectory(); const std::string avdFolder = Paths::JoinPaths({avdDir, avdName + ".avd"}); diff --git a/src/core/crash_reporter.cpp b/src/core/crash_reporter.cpp new file mode 100644 index 0000000..a921292 --- /dev/null +++ b/src/core/crash_reporter.cpp @@ -0,0 +1,107 @@ +// +// Created by AbdulMuaz Aqeel on 27/04/2026. +// + +#include "crash_reporter.h" + +#ifdef COREDECK_SENTRY_DSN + +#include +#include + +#include "paths.h" + +namespace CoreDeck::CrashReporter { + static sentry_level_t ToSentryLevel(const Level level) { + switch (level) { + case Level::Debug: return SENTRY_LEVEL_DEBUG; + case Level::Info: return SENTRY_LEVEL_INFO; + case Level::Warning: return SENTRY_LEVEL_WARNING; + case Level::Error: return SENTRY_LEVEL_ERROR; + case Level::Fatal: return SENTRY_LEVEL_FATAL; + } + return SENTRY_LEVEL_INFO; + } + + static std::string ToString(const std::string_view sv) { + return std::string(sv); + } + + bool Init() { + sentry_options_t *opts = sentry_options_new(); + sentry_options_set_dsn(opts, COREDECK_SENTRY_DSN); + sentry_options_set_release(opts, "coredeck@" COREDECK_VERSION); + sentry_options_set_dist(opts, COREDECK_BUILD_NUMBER); + sentry_options_set_database_path(opts, Paths::GetAppConfigPath("sentry-db").c_str()); + + const char *handlerName = +#ifdef _WIN32 + "crashpad_handler.exe"; +#else + "crashpad_handler"; +#endif + const std::string handlerPath = Paths::JoinPaths( + {Paths::GetExecutableDirectory(), handlerName} + ); + sentry_options_set_handler_path(opts, handlerPath.c_str()); + + return sentry_init(opts) == 0; + } + + void Shutdown() { + sentry_close(); + } + + bool IsEnabled() { return true; } + + void CaptureMessage(const Level level, const std::string_view message) { + sentry_capture_event(sentry_value_new_message_event( + ToSentryLevel(level), + nullptr, + ToString(message).c_str() + ) + ); + } + + void CaptureException(const std::string_view type, const std::string_view message) { + const sentry_value_t event = sentry_value_new_event(); + const sentry_value_t exc = sentry_value_new_exception( + ToString(type).c_str(), + ToString(message).c_str() + ); + sentry_value_set_stacktrace(exc, nullptr, 0); + sentry_event_add_exception(event, exc); + sentry_capture_event(event); + } + + void AddBreadcrumb(const std::string_view category, const std::string_view message) { + const sentry_value_t crumb = sentry_value_new_breadcrumb( + nullptr, + ToString(message).c_str() + ); + sentry_value_set_by_key(crumb, "category", sentry_value_new_string(ToString(category).c_str())); + sentry_add_breadcrumb(crumb); + } +} + +#else + +namespace CoreDeck::CrashReporter { + bool Init() { return false; } + + void Shutdown() { + } + + bool IsEnabled() { return false; } + + void CaptureMessage(Level, std::string_view) { + } + + void CaptureException(std::string_view, std::string_view) { + } + + void AddBreadcrumb(std::string_view, std::string_view) { + } +} + +#endif diff --git a/src/core/crash_reporter.h b/src/core/crash_reporter.h new file mode 100644 index 0000000..469e97c --- /dev/null +++ b/src/core/crash_reporter.h @@ -0,0 +1,32 @@ +// +// Created by AbdulMuaz Aqeel on 27/04/2026. +// + +#ifndef COREDECK_CRASH_REPORTER_H +#define COREDECK_CRASH_REPORTER_H + +#include + +namespace CoreDeck::CrashReporter { + enum class Level { + Debug, + Info, + Warning, + Error, + Fatal + }; + + bool Init(); + + void Shutdown(); + + bool IsEnabled(); + + void CaptureMessage(Level level, std::string_view message); + + void CaptureException(std::string_view type, std::string_view message); + + void AddBreadcrumb(std::string_view category, std::string_view message); +} + +#endif //COREDECK_CRASH_REPORTER_H \ No newline at end of file diff --git a/src/core/emulator.cpp b/src/core/emulator.cpp index 1c6eb5d..ab2feff 100644 --- a/src/core/emulator.cpp +++ b/src/core/emulator.cpp @@ -8,9 +8,12 @@ #include #include #include +#include +#include #include "emulator.h" #include "process.h" +#include "emulator_console.h" #ifdef _WIN32 #include @@ -24,18 +27,32 @@ namespace CoreDeck { } EmulatorManager::~EmulatorManager() { - std::lock_guard lock(m_Mutex); + std::vector pendingStops; { + std::lock_guard lock(m_Mutex); + for (auto &instance: m_Instances | std::views::values) { + if (instance.StopThread.joinable()) { + pendingStops.push_back(std::move(instance.StopThread)); + } + } + } + for (auto &t: pendingStops) t.join(); + std::lock_guard lock(m_Mutex); for (auto &instance: m_Instances | std::views::values) { if (instance.IsRunning) { - if (instance.StopRequested) { - instance.StopRequested->store(true); + if (instance.ConsolePort > 0) { + EmulatorConsole::SendKill(instance.ConsolePort, 1000); + } + if (!WaitForProcessExit(instance.Pid, 2000)) { + TerminateProcessTree(instance.Pid); } - - KillProcess(instance.Pid); instance.IsRunning = false; } + if (instance.StopRequested) { + instance.StopRequested->store(true); + } + if (instance.ReaderThread.joinable()) { instance.ReaderThread.join(); } @@ -49,8 +66,15 @@ namespace CoreDeck { } } + const int consolePort = EmulatorConsole::FindFreePort(); + std::vector finalArgs = args; + if (consolePort > 0) { + finalArgs.emplace_back("-port"); + finalArgs.emplace_back(std::to_string(consolePort)); + } + int outputFd = -1; - const ProcessId pid = SpawnProcessWithPipe(m_Sdk.EmulatorPath, args, outputFd); + const ProcessId pid = SpawnProcessWithPipe(m_Sdk.EmulatorPath, finalArgs, outputFd); #ifdef _WIN32 if (pid == 0) return false; @@ -61,30 +85,51 @@ namespace CoreDeck { { auto log = std::make_shared(); auto stopFlag = std::make_shared >(false); - std::thread reader([outputFd, log, stopFlag]() { + std::thread reader([outputFd, log, stopFlag] { std::array buf{}; std::string partial; + auto flushLines = [&](const char *data, const std::size_t n) { + partial.append(data, n); + std::size_t pos; + while ((pos = partial.find('\n')) != std::string::npos) { + if (auto line = partial.substr(0, pos); !line.empty()) { + log->Push(line); + } + partial.erase(0, pos + 1); + } + }; + while (!stopFlag->load()) { #ifdef _WIN32 - if (const int n = _read(outputFd, buf.data(), buf.size()); n > 0) { + const HANDLE h = reinterpret_cast(_get_osfhandle(outputFd)); + DWORD nRead = 0; + if (ReadFile(h, buf.data(), static_cast(buf.size()), &nRead, nullptr)) { + if (nRead > 0) { + flushLines(buf.data(), nRead); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } else { + const DWORD err = GetLastError(); + if (err == ERROR_BROKEN_PIPE || err == ERROR_HANDLE_EOF) break; + if (err == ERROR_NO_DATA) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } else { + break; + } + } #else if (const ssize_t n = read(outputFd, buf.data(), buf.size()); n > 0) { -#endif - partial.append(buf.data(), n); - - std::size_t pos; - while ((pos = partial.find('\n')) != std::string::npos) { - if (auto line = partial.substr(0, pos); !line.empty()) { - log->Push(line); - } - partial.erase(0, pos + 1); - } + flushLines(buf.data(), static_cast(n)); } else if (n == 0) { break; - } else { + } else if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } else { + break; } +#endif } if (!partial.empty()) { @@ -98,21 +143,27 @@ namespace CoreDeck { #endif }); - std::lock_guard lock(m_Mutex); - - if (const auto existing = m_Instances.find(avdName); existing != m_Instances.end()) { - if (existing->second.StopRequested) { - existing->second.StopRequested->store(true); - } - if (existing->second.ReaderThread.joinable()) { - existing->second.ReaderThread.join(); + std::thread oldStopThread; + std::thread oldReaderThread; { + std::lock_guard lock(m_Mutex); + if (const auto existing = m_Instances.find(avdName); existing != m_Instances.end()) { + if (existing->second.StopRequested) { + existing->second.StopRequested->store(true); + } + oldStopThread = std::move(existing->second.StopThread); + oldReaderThread = std::move(existing->second.ReaderThread); + m_Instances.erase(existing); } - m_Instances.erase(existing); } + if (oldStopThread.joinable()) oldStopThread.join(); + if (oldReaderThread.joinable()) oldReaderThread.join(); + + std::lock_guard lock(m_Mutex); EmulatorInstance instance; instance.AvdName = avdName; instance.Pid = pid; + instance.ConsolePort = consolePort; instance.IsRunning = true; instance.Log = log; instance.ReaderThread = std::move(reader); @@ -124,26 +175,64 @@ namespace CoreDeck { } bool EmulatorManager::Stop(const std::string &avdName) { - std::lock_guard lock(m_Mutex); - const auto it = m_Instances.find(avdName); - if (it == m_Instances.end() || !it->second.IsRunning) { - return false; + ProcessId pid; + int consolePort; + std::shared_ptr > stopFlag; + std::thread readerThread; + std::thread oldStopThread; { + std::lock_guard lock(m_Mutex); + const auto it = m_Instances.find(avdName); + if (it == m_Instances.end() || !it->second.IsRunning || it->second.Stopping) { + return false; + } + pid = it->second.Pid; + consolePort = it->second.ConsolePort; + stopFlag = it->second.StopRequested; + readerThread = std::move(it->second.ReaderThread); + oldStopThread = std::move(it->second.StopThread); + it->second.Stopping = true; } + if (oldStopThread.joinable()) oldStopThread.join(); - // Signal the thread to stop - if (it->second.StopRequested) { - it->second.StopRequested->store(true); - } + std::thread worker( + [this, avdName, pid, consolePort, stopFlag,reader = std::move(readerThread)]() mutable { + bool exited = false; + if (consolePort > 0 && EmulatorConsole::SendKill(consolePort)) { + exited = WaitForProcessExit(pid, 5000); + } + if (!exited) { + KillProcess(pid); + exited = WaitForProcessExit(pid, 2000); + } + if (!exited) { + TerminateProcessTree(pid); + } - const bool killed = KillProcess(it->second.Pid); - if (killed) { - it->second.IsRunning = false; + if (stopFlag) stopFlag->store(true); + if (reader.joinable()) reader.join(); - if (it->second.ReaderThread.joinable()) { - it->second.ReaderThread.join(); + std::lock_guard lock(m_Mutex); + if (const auto it = m_Instances.find(avdName); it != m_Instances.end()) { + it->second.IsRunning = false; + it->second.Stopping = false; + } } + ); + + std::lock_guard lock(m_Mutex); + if (const auto it = m_Instances.find(avdName); it != m_Instances.end()) { + it->second.StopThread = std::move(worker); + } else { + worker.detach(); } - return killed; + return true; + } + + bool EmulatorManager::IsStopping(const std::string &avdName) { + std::lock_guard lock(m_Mutex); + const auto it = m_Instances.find(avdName); + if (it == m_Instances.end()) return false; + return it->second.Stopping; } bool EmulatorManager::IsRunning(const std::string &avdName) { diff --git a/src/core/emulator.h b/src/core/emulator.h index c8b7af9..0175583 100644 --- a/src/core/emulator.h +++ b/src/core/emulator.h @@ -19,21 +19,27 @@ namespace CoreDeck { struct EmulatorInstance { std::string AvdName; ProcessId Pid; + int ConsolePort = 0; bool IsRunning = false; + bool Stopping = false; std::shared_ptr Log; std::thread ReaderThread; + std::thread StopThread; std::shared_ptr > StopRequested; }; class EmulatorManager { public: explicit EmulatorManager(SdkInfo sdk); + ~EmulatorManager(); bool Launch(const std::string &avdName, const std::vector &args); bool Stop(const std::string &avdName); + bool IsStopping(const std::string &avdName); + bool IsRunning(const std::string &avdName); std::shared_ptr GetLog(const std::string &avdName); diff --git a/src/core/emulator_console.cpp b/src/core/emulator_console.cpp new file mode 100644 index 0000000..289ffec --- /dev/null +++ b/src/core/emulator_console.cpp @@ -0,0 +1,157 @@ +#include "emulator_console.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +using socket_t = SOCKET; +static constexpr socket_t s_InvalidSocket = INVALID_SOCKET; +#else +#include +#include +#include +#include +#include +using socket_t = int; +static constexpr socket_t s_InvalidSocket = -1; +#endif + +namespace CoreDeck::EmulatorConsole { +#ifdef _WIN32 + struct WsaInit { + WsaInit() { + WSADATA d; + WSAStartup(MAKEWORD(2, 2), &d); + } + + ~WsaInit() { WSACleanup(); } + }; + void EnsureWsa() { static WsaInit init; } + void CloseSock(const socket_t s) { closesocket(s); } + void SetNonBlocking(const socket_t s) { + u_long m = 1; + ioctlsocket(s, FIONBIO, &m); + } + int LastErr() { return WSAGetLastError(); } + bool ErrIsWouldBlock(const int e) { return e == WSAEWOULDBLOCK || e == WSAEINPROGRESS; } +#else + void EnsureWsa() { + } + + void CloseSock(const socket_t s) { close(s); } + + void SetNonBlocking(const socket_t s) { + const int f = fcntl(s, F_GETFL, 0); + fcntl(s, F_SETFL, f | O_NONBLOCK); + } + + int LastErr() { return errno; } + bool ErrIsWouldBlock(const int e) { return e == EWOULDBLOCK || e == EAGAIN || e == EINPROGRESS; } +#endif + + std::string ReadAuthToken() { + const char *home = +#ifdef _WIN32 + std::getenv("USERPROFILE"); +#else + std::getenv("HOME"); +#endif + if (!home) return ""; + const std::filesystem::path p = std::filesystem::path(home) / ".emulator_console_auth_token"; + std::ifstream f(p); + if (!f) return ""; + std::stringstream ss; + ss << f.rdbuf(); + std::string token = ss.str(); + while (!token.empty() && (token.back() == '\n' || token.back() == '\r' || token.back() == ' ' || token.back() == + '\t')) { + token.pop_back(); + } + return token; + } + + bool WaitWritable(const socket_t s, const int timeoutMs) { + fd_set wf; + FD_ZERO(&wf); + FD_SET(s, &wf); + timeval tv{timeoutMs / 1000, (timeoutMs % 1000) * 1000}; + return select(static_cast(s) + 1, nullptr, &wf, nullptr, &tv) > 0; + } + + + int FindFreePort(const int startPort, const int endPort) { + EnsureWsa(); + for (int port = startPort; port <= endPort; port += 2) { + const socket_t s = socket(AF_INET, SOCK_STREAM, 0); + if (s == s_InvalidSocket) continue; + sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(static_cast(port)); + const int rc = bind(s, reinterpret_cast(&addr), sizeof(addr)); + CloseSock(s); + if (rc == 0) return port; + } + return -1; + } + + bool SendKill(const int port, const int timeoutMs) { + EnsureWsa(); + const socket_t s = socket(AF_INET, SOCK_STREAM, 0); + if (s == s_InvalidSocket) return false; + + SetNonBlocking(s); + + sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(static_cast(port)); + + const int rc = connect(s, reinterpret_cast(&addr), sizeof(addr)); + if (rc != 0 && !ErrIsWouldBlock(LastErr())) { + CloseSock(s); + return false; + } + if (rc != 0 && !WaitWritable(s, timeoutMs)) { + CloseSock(s); + return false; + } + + std::string payload; + if (const std::string token = ReadAuthToken(); !token.empty()) { + payload = "auth " + token + "\r\n"; + } + payload += "kill\r\n"; + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + size_t sent = 0; + while (sent < payload.size()) { +#ifdef _WIN32 + const int n = send(s, payload.data() + sent, static_cast(payload.size() - sent), 0); +#else + const ssize_t n = send(s, payload.data() + sent, payload.size() - sent, 0); +#endif + if (n > 0) { + sent += static_cast(n); + continue; + } + if (n < 0 && !ErrIsWouldBlock(LastErr())) { + CloseSock(s); + return false; + } + if (std::chrono::steady_clock::now() >= deadline) { + CloseSock(s); + return false; + } + if (!WaitWritable(s, 100)) continue; + } + + CloseSock(s); + return true; + } +} diff --git a/src/core/emulator_console.h b/src/core/emulator_console.h new file mode 100644 index 0000000..997d2fc --- /dev/null +++ b/src/core/emulator_console.h @@ -0,0 +1,10 @@ +#ifndef COREDECK_EMULATOR_CONSOLE_H +#define COREDECK_EMULATOR_CONSOLE_H + +namespace CoreDeck::EmulatorConsole { + int FindFreePort(int startPort = 5554, int endPort = 5584); + + bool SendKill(int port, int timeoutMs = 2000); +} + +#endif diff --git a/src/core/process.cpp b/src/core/process.cpp index 57c077a..80deea6 100644 --- a/src/core/process.cpp +++ b/src/core/process.cpp @@ -3,12 +3,13 @@ // #include "process.h" -#include "paths.h" +#include #include #include #ifdef _WIN32 #include +#include #include #include #include @@ -17,102 +18,198 @@ #include #include #include +#include +#include #endif namespace CoreDeck { - std::string RunCommand(const std::string &cmd) { #ifdef _WIN32 - FILE *pipe = _popen(cmd.c_str(), "r"); - if (!pipe) return ""; - - std::string result; - std::array buffer{}; - - while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { - result += buffer.data(); + static std::string QuoteArg(const std::string &arg) { + if (!arg.empty() && arg.find_first_of(" \t\"") == std::string::npos) return arg; + std::string out = "\""; + for (size_t i = 0; i < arg.size(); ++i) { + size_t backslashes = 0; + while (i < arg.size() && arg[i] == '\\') { + ++backslashes; + ++i; + } + if (i == arg.size()) { + out.append(backslashes * 2, '\\'); + break; + } + if (arg[i] == '"') { + out.append(backslashes * 2 + 1, '\\'); + out.push_back('"'); + } else { + out.append(backslashes, '\\'); + out.push_back(arg[i]); + } } + out.push_back('"'); + return out; + } - _pclose(pipe); - return result; -#else - FILE *pipe = popen(cmd.c_str(), "r"); - if (!pipe) return ""; - - std::string result; - std::array buffer{}; + static bool IsBatchFile(const std::string &path) { + if (path.size() < 4) return false; + std::string ext = path.substr(path.size() - 4); + std::ranges::transform(ext, ext.begin(), ::tolower); + return ext == ".bat" || ext == ".cmd"; + } - while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { - result += buffer.data(); + static std::string BuildCommandLine(const std::string &path, const std::vector &args) { + std::string cmd = QuoteArg(path); + for (const auto &arg: args) { + cmd.push_back(' '); + cmd += QuoteArg(arg); } - - pclose(pipe); - return result; -#endif + if (IsBatchFile(path)) return "cmd.exe /S /C \"" + cmd + "\""; + return cmd; } +#endif - ProcessId SpawnProcess(const std::string &path, const std::vector &args) { + void StreamCommandArgs( + const std::string &path, + const std::vector &args, + const std::string &stdinData, + const std::function &onLine + ) { #ifdef _WIN32 - std::string cmdLine = "\"" + path + "\""; - for (const auto &arg: args) { - cmdLine += " \"" + arg + "\""; + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE hOutR = nullptr, hOutW = nullptr; + HANDLE hInR = nullptr, hInW = nullptr; + if (!CreatePipe(&hOutR, &hOutW, &sa, 0)) return; + SetHandleInformation(hOutR, HANDLE_FLAG_INHERIT, 0); + if (!CreatePipe(&hInR, &hInW, &sa, 0)) { + CloseHandle(hOutR); + CloseHandle(hOutW); + return; } + SetHandleInformation(hInW, HANDLE_FLAG_INHERIT, 0); + + std::string cmdLine = BuildCommandLine(path, args); STARTUPINFOA si = {}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = hInR; + si.hStdOutput = hOutW; + si.hStdError = hOutW; - const std::string nullDevice = Paths::GetNullDevice(); - HANDLE hNull = CreateFileA(nullDevice.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (hNull != INVALID_HANDLE_VALUE) { - si.hStdOutput = hNull; - si.hStdError = hNull; + PROCESS_INFORMATION pi = {}; + if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, + TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { + CloseHandle(hOutR); + CloseHandle(hOutW); + CloseHandle(hInR); + CloseHandle(hInW); + return; } - PROCESS_INFORMATION pi = {}; + CloseHandle(hOutW); + CloseHandle(hInR); - if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, - TRUE, 0, nullptr, nullptr, &si, &pi)) { - if (hNull != INVALID_HANDLE_VALUE) CloseHandle(hNull); - return 0; + if (!stdinData.empty()) { + DWORD written = 0; + WriteFile(hInW, stdinData.data(), static_cast(stdinData.size()), &written, nullptr); } + CloseHandle(hInW); + + std::string partial; + std::array buf{}; + DWORD read = 0; + while (ReadFile(hOutR, buf.data(), static_cast(buf.size()), &read, nullptr) && read > 0) { + partial.append(buf.data(), read); + std::size_t pos; + while ((pos = partial.find_first_of("\n\r")) != std::string::npos) { + if (auto line = partial.substr(0, pos); !line.empty() && onLine) onLine(line); + auto next = partial.find_first_not_of("\n\r", pos); + partial = (next == std::string::npos) ? std::string() : partial.substr(next); + } + } + if (!partial.empty() && onLine) onLine(partial); + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); CloseHandle(pi.hThread); - if (hNull != INVALID_HANDLE_VALUE) CloseHandle(hNull); - - return pi.dwProcessId; + CloseHandle(hOutR); #else - const pid_t pid = fork(); + int outPipe[2]; + int inPipe[2]; + if (pipe(outPipe) == -1) return; + if (pipe(inPipe) == -1) { + close(outPipe[0]); + close(outPipe[1]); + return; + } + const pid_t pid = fork(); if (pid < 0) { - return -1; + close(outPipe[0]); + close(outPipe[1]); + close(inPipe[0]); + close(inPipe[1]); + return; } if (pid == 0) { + close(outPipe[0]); + close(inPipe[1]); + dup2(outPipe[1], STDOUT_FILENO); + dup2(outPipe[1], STDERR_FILENO); + dup2(inPipe[0], STDIN_FILENO); + close(outPipe[1]); + close(inPipe[0]); + std::vector argv; argv.push_back(path.c_str()); - - for (const auto &arg: args) { - argv.push_back(arg.c_str()); - } + for (const auto &a: args) argv.push_back(a.c_str()); argv.push_back(nullptr); + execvp(path.c_str(), const_cast(argv.data())); + _exit(127); + } - const std::string nullDevice = Paths::GetNullDevice(); - if (const int devnull = open(nullDevice.c_str(), O_WRONLY); devnull != -1) { - dup2(devnull, STDOUT_FILENO); - dup2(devnull, STDERR_FILENO); - close(devnull); - } - - execvp(path.c_str(), const_cast(argv.data())); + close(outPipe[1]); + close(inPipe[0]); - _exit(1); + if (!stdinData.empty()) { + [[maybe_unused]] ssize_t w = write(inPipe[1], stdinData.data(), stdinData.size()); } + close(inPipe[1]); + + std::string partial; + std::array buf{}; + ssize_t r; + while ((r = read(outPipe[0], buf.data(), buf.size())) > 0) { + partial.append(buf.data(), r); + std::size_t pos; + while ((pos = partial.find_first_of("\n\r")) != std::string::npos) { + if (auto line = partial.substr(0, pos); !line.empty() && onLine) onLine(line); + auto next = partial.find_first_not_of("\n\r", pos); + partial = (next == std::string::npos) ? std::string() : partial.substr(next); + } + } + if (!partial.empty() && onLine) onLine(partial); - return pid; + close(outPipe[0]); + int status; + waitpid(pid, &status, 0); #endif } + std::string RunCommandArgs(const std::string &path, + const std::vector &args, + const std::string &stdinData) { + std::string out; + StreamCommandArgs(path, args, stdinData, [&out](const std::string &line) { + out += line; + out.push_back('\n'); + }); + return out; + } + ProcessId SpawnProcessWithPipe(const std::string &path, const std::vector &args, int &outputFd) { #ifdef _WIN32 HANDLE hReadPipe, hWritePipe; @@ -130,10 +227,7 @@ namespace CoreDeck { return 0; } - std::string cmdLine = "\"" + path + "\""; - for (const auto &arg: args) { - cmdLine += " \"" + arg + "\""; - } + std::string cmdLine = BuildCommandLine(path, args); STARTUPINFOA si = {}; si.cb = sizeof(si); @@ -144,7 +238,7 @@ namespace CoreDeck { PROCESS_INFORMATION pi = {}; if (!CreateProcessA(nullptr, const_cast(cmdLine.c_str()), nullptr, nullptr, - TRUE, 0, nullptr, nullptr, &si, &pi)) { + TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { CloseHandle(hReadPipe); CloseHandle(hWritePipe); return 0; @@ -153,6 +247,9 @@ namespace CoreDeck { CloseHandle(pi.hThread); CloseHandle(hWritePipe); + DWORD pipeMode = PIPE_NOWAIT; + SetNamedPipeHandleState(hReadPipe, &pipeMode, nullptr, nullptr); + outputFd = _open_osfhandle(reinterpret_cast(hReadPipe), _O_RDONLY); if (outputFd == -1) { CloseHandle(hReadPipe); @@ -175,6 +272,7 @@ namespace CoreDeck { } if (pid == 0) { + setpgid(0, 0); close(pipeFd[0]); dup2(pipeFd[1], STDOUT_FILENO); @@ -242,6 +340,63 @@ namespace CoreDeck { #endif } + bool WaitForProcessExit(const ProcessId pid, const int timeoutMs) { +#ifdef _WIN32 + if (pid == 0) return false; + HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, pid); + if (hProcess == nullptr) return true; + const DWORD r = WaitForSingleObject(hProcess, timeoutMs < 0 ? INFINITE : static_cast(timeoutMs)); + CloseHandle(hProcess); + return r == WAIT_OBJECT_0; +#else + if (pid <= 0) return false; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (true) { + int status; + const pid_t r = waitpid(pid, &status, WNOHANG); + if (r == pid || r == -1) return true; + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +#endif + } + + bool TerminateProcessTree(const ProcessId pid) { +#ifdef _WIN32 + if (pid == 0) return false; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap != INVALID_HANDLE_VALUE) { + std::vector children; + PROCESSENTRY32 pe = {}; + pe.dwSize = sizeof(pe); + if (Process32First(snap, &pe)) { + do { + if (pe.th32ParentProcessID == pid) children.push_back(pe.th32ProcessID); + } while (Process32Next(snap, &pe)); + } + CloseHandle(snap); + for (const DWORD child: children) TerminateProcessTree(child); + } + + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid); + if (hProcess == nullptr) return true; + const bool ok = TerminateProcess(hProcess, 1) != 0; + if (ok) WaitForSingleObject(hProcess, 2000); + CloseHandle(hProcess); + return ok; +#else + if (pid <= 0) return false; + if (kill(-pid, SIGKILL) == 0) { + WaitForProcessExit(pid, 2000); + return true; + } + const bool ok = kill(pid, SIGKILL) == 0; + if (ok) WaitForProcessExit(pid, 2000); + return ok; +#endif + } + bool IsProcessRunning(const ProcessId pid) { #ifdef _WIN32 if (pid == 0) return false; diff --git a/src/core/process.h b/src/core/process.h index 3b8b22e..5a0fec8 100644 --- a/src/core/process.h +++ b/src/core/process.h @@ -5,26 +5,40 @@ #ifndef EMU_LAUNCHER_PROCESS_H #define EMU_LAUNCHER_PROCESS_H +#include #include #include #ifdef _WIN32 - #include - using ProcessId = DWORD; +#include +using ProcessId = DWORD; #else - #include - using ProcessId = pid_t; +#include +using ProcessId = pid_t; #endif namespace CoreDeck { - std::string RunCommand(const std::string &cmd); - - ProcessId SpawnProcess(const std::string &path, const std::vector &args); + std::string RunCommandArgs( + const std::string &path, + const std::vector &args, + const std::string &stdinData = "" + ); + + void StreamCommandArgs( + const std::string &path, + const std::vector &args, + const std::string &stdinData, + const std::function &onLine + ); ProcessId SpawnProcessWithPipe(const std::string &path, const std::vector &args, int &outputFd); bool KillProcess(ProcessId pid); + bool TerminateProcessTree(ProcessId pid); + + bool WaitForProcessExit(ProcessId pid, int timeoutMs); + bool IsProcessRunning(ProcessId pid); } diff --git a/src/core/sdk.cpp b/src/core/sdk.cpp index 1f0f03b..a3e99c7 100644 --- a/src/core/sdk.cpp +++ b/src/core/sdk.cpp @@ -8,6 +8,19 @@ #include "paths.h" namespace CoreDeck { + static std::string FindCmdlineTool(const std::string &binDir, const std::string &name) { +#ifdef _WIN32 + for (const auto *ext: {".bat", ".exe"}) { + const std::string candidate = Paths::JoinPaths({binDir, name + ext}); + if (std::filesystem::exists(candidate)) return candidate; + } + return ""; +#else + const std::string candidate = Paths::JoinPaths({binDir, name}); + return std::filesystem::exists(candidate) ? candidate : ""; +#endif + } + SdkInfo DetectAndroidSdk() { SdkInfo sdk; @@ -30,21 +43,17 @@ namespace CoreDeck { sdk.EmulatorPath = Paths::JoinPaths({sdk.SdkPath, "emulator", "emulator" + Paths::GetExecutableExtension()}); - const std::string cmdlineLatest = Paths::JoinPaths({ - sdk.SdkPath, "cmdline-tools", "latest", "bin", "avdmanager" + Paths::GetExecutableExtension() - }); + const std::string latestBin = Paths::JoinPaths({sdk.SdkPath, "cmdline-tools", "latest", "bin"}); + sdk.AvdManagerPath = FindCmdlineTool(latestBin, "avdmanager"); - if (std::filesystem::exists(cmdlineLatest)) { - sdk.AvdManagerPath = cmdlineLatest; - } else { + if (sdk.AvdManagerPath.empty()) { const std::string cmdlineRoot = Paths::JoinPaths({sdk.SdkPath, "cmdline-tools"}); if (std::filesystem::exists(cmdlineRoot) && std::filesystem::is_directory(cmdlineRoot)) { for (const auto &entry: std::filesystem::directory_iterator(cmdlineRoot)) { if (!entry.is_directory()) continue; - const std::string candidate = Paths::JoinPaths({ - entry.path().string(), "bin", "avdmanager" + Paths::GetExecutableExtension() - }); - if (std::filesystem::exists(candidate)) { + const std::string candidate = FindCmdlineTool( + Paths::JoinPaths({entry.path().string(), "bin"}), "avdmanager"); + if (!candidate.empty()) { sdk.AvdManagerPath = candidate; break; } @@ -53,13 +62,8 @@ namespace CoreDeck { } if (!sdk.AvdManagerPath.empty()) { - std::filesystem::path avdMgrPath(sdk.AvdManagerPath); - const std::string sdkMgrCandidate = Paths::JoinPaths({ - avdMgrPath.parent_path().string(), "sdkmanager" + Paths::GetExecutableExtension() - }); - if (std::filesystem::exists(sdkMgrCandidate)) { - sdk.SdkManagerPath = sdkMgrCandidate; - } + const std::filesystem::path avdMgrPath(sdk.AvdManagerPath); + sdk.SdkManagerPath = FindCmdlineTool(avdMgrPath.parent_path().string(), "sdkmanager"); } if (std::filesystem::exists(sdk.EmulatorPath)) sdk.IsFound = true; diff --git a/src/core/system_image.cpp b/src/core/system_image.cpp index dde04cb..86e58ad 100644 --- a/src/core/system_image.cpp +++ b/src/core/system_image.cpp @@ -3,10 +3,6 @@ // #include -#include -#include -#include -#include #include #include #include @@ -59,8 +55,7 @@ namespace CoreDeck { if (sdk.AvdManagerPath.empty()) return devices; - const std::string cmd = StrConcat("\"", sdk.AvdManagerPath, "\" list device -c"); - const std::string output = RunCommand(cmd); + const std::string output = RunCommandArgs(sdk.AvdManagerPath, {"list", "device", "-c"}); std::istringstream stream(output); std::string line; while (std::getline(stream, line)) { @@ -140,8 +135,7 @@ namespace CoreDeck { std::vector results; if (sdk.SdkManagerPath.empty()) return results; - const std::string cmd = StrConcat("\"", sdk.SdkManagerPath, "\" --list 2>&1"); - const std::string output = RunCommand(cmd); + const std::string output = RunCommandArgs(sdk.SdkManagerPath, {"--list"}); std::unordered_map installedSet; for (const auto &img: installedImages) { @@ -213,57 +207,19 @@ namespace CoreDeck { ) { if (sdk.SdkManagerPath.empty() || packagePath.empty()) return false; - const std::string acceptCmd = StrConcat( -#ifdef _WIN32 - "echo y| \"", sdk.SdkManagerPath, "\" --licenses 2>&1" -#else - "yes | \"", sdk.SdkManagerPath, "\" --licenses 2>&1" -#endif - ); - RunCommand(acceptCmd); - if (progress) { std::lock_guard lock(progress->Mutex); progress->StatusText = "Starting download..."; progress->Percent = 0.0f; } - const std::string cmd = StrConcat("\"", sdk.SdkManagerPath, "\" --install \"", packagePath, "\" 2>&1"); -#ifdef _WIN32 - FILE *pipe = _popen(cmd.c_str(), "r"); -#else - FILE *pipe = popen(cmd.c_str(), "r"); -#endif - if (!pipe) return false; - - std::array buf{}; - std::string partial; - - while (fgets(buf.data(), buf.size(), pipe) != nullptr) { - partial += buf.data(); - - std::size_t pos; - while ((pos = partial.find_first_of("\n\r")) != std::string::npos) { - if (auto line = partial.substr(0, pos); !line.empty()) { - ParseProgressLine(line, progress); - } - if (auto next = partial.find_first_not_of("\n\r", pos); next == std::string::npos) { - partial.clear(); - } else { - partial = partial.substr(next); - } + StreamCommandArgs( + sdk.SdkManagerPath, + {"--install", packagePath}, "", + [&progress](const std::string &line) { + ParseProgressLine(line, progress); } - } - - if (!partial.empty()) { - ParseProgressLine(partial, progress); - } - -#ifdef _WIN32 - _pclose(pipe); -#else - pclose(pipe); -#endif + ); // Verify std::string fsPath = packagePath; @@ -285,17 +241,36 @@ namespace CoreDeck { bool UninstallSystemImage(const SdkInfo &sdk, const std::string &packagePath) { if (sdk.SdkManagerPath.empty() || packagePath.empty()) return false; -#ifdef _WIN32 - const std::string cmd = StrConcat("echo y| \"", sdk.SdkManagerPath, "\" --uninstall \"", packagePath, - "\" 2>&1"); -#else - const std::string cmd = StrConcat("yes | \"", sdk.SdkManagerPath, "\" --uninstall \"", packagePath, "\" 2>&1"); -#endif - RunCommand(cmd); + RunCommandArgs(sdk.SdkManagerPath, {"--uninstall", packagePath}, "y\n"); std::string fsPath = packagePath; std::ranges::replace(fsPath, ';', '/'); const std::string sysImg = Paths::JoinPaths({sdk.SdkPath, fsPath, "system.img"}); return !std::filesystem::exists(sysImg); } + + LicenseStatus CheckSdkLicenses(const SdkInfo &sdk) { + if (sdk.SdkManagerPath.empty()) return LicenseStatus::CheckFailed; + + const std::string output = RunCommandArgs(sdk.SdkManagerPath, {"--licenses"}, "N\n"); + + if (output.find("All SDK package licenses accepted") != std::string::npos) { + return LicenseStatus::AllAccepted; + } + if (output.find("licenses not accepted") != std::string::npos) { + return LicenseStatus::SomeUnaccepted; + } + return LicenseStatus::CheckFailed; + } + + bool AcceptSdkLicenses(const SdkInfo &sdk) { + if (sdk.SdkManagerPath.empty()) return false; + + std::string yes; + yes.reserve(64 * 2); + for (int i = 0; i < 64; ++i) yes += "y\n"; + + const std::string output = RunCommandArgs(sdk.SdkManagerPath, {"--licenses"}, yes); + return output.find("All SDK package licenses accepted") != std::string::npos; + } } diff --git a/src/core/system_image.h b/src/core/system_image.h index afe83e1..ee014f2 100644 --- a/src/core/system_image.h +++ b/src/core/system_image.h @@ -60,6 +60,16 @@ namespace CoreDeck { ); bool UninstallSystemImage(const SdkInfo &sdk, const std::string &packagePath); + + enum class LicenseStatus { + AllAccepted, + SomeUnaccepted, + CheckFailed, + }; + + LicenseStatus CheckSdkLicenses(const SdkInfo &sdk); + + bool AcceptSdkLicenses(const SdkInfo &sdk); } #endif //COREDECK_SYSTEM_IMAGE_H diff --git a/src/core/utilities.cpp b/src/core/utilities.cpp index 260bfe8..f92722c 100644 --- a/src/core/utilities.cpp +++ b/src/core/utilities.cpp @@ -7,18 +7,23 @@ #include #include +#ifdef _WIN32 +#include +#include +#endif + +#include "process.h" #include "utilities.h" namespace CoreDeck { void OpenUrl(const char *url) { #if defined(_WIN32) - const std::string cmd = std::string("start ") + url; + ShellExecuteA(nullptr, "open", url, nullptr, nullptr, SW_SHOWNORMAL); #elif defined(__APPLE__) - const std::string cmd = std::string("open ") + url; + RunCommandArgs("/usr/bin/open", {url}); #else - const std::string cmd = std::string("xdg-open ") + url; + RunCommandArgs("xdg-open", {url}); #endif - [[maybe_unused]] int ret = std::system(cmd.c_str()); } std::uintmax_t GetDirectorySize(const std::string &path) { diff --git a/src/gui/application.cpp b/src/gui/application.cpp index f62013b..55344b7 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -9,14 +9,24 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#include +#define GLFW_EXPOSE_NATIVE_WIN32 #endif #include - +#include +#include #include "imgui.h" #include "imgui_internal.h" +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" +#include +#ifdef _WIN32 +#include +#endif #include "application.h" +#include "theme.h" #include "../core/app_settings.h" #include "../core/paths.h" #include "windows/about.h" @@ -36,6 +46,15 @@ #include "../core/version_check.h" namespace CoreDeck { + static void ShowFatalError(const char *title, const char *message) { +#ifdef _WIN32 + MessageBoxA(nullptr, message, title, MB_OK | MB_ICONERROR); +#else + (void) title; + std::fprintf(stderr, "%s\n", message); +#endif + } + Application::Application() : m_Context(DetectAndroidSdk()) { EnsureOptionsConfigDirectoryExists(); ApplyAppSettingsToContext(m_Context, LoadAppSettings()); @@ -48,25 +67,40 @@ namespace CoreDeck { } } + Application::~Application() { + Shutdown(); + } + + int Application::Run() { + if (!InitPlatform()) return 1; + if (!CreateMainWindow()) return 1; + InitImGui(); + LoadFonts(); + + ImGui::StyleColorsDark(); + ApplyCustomImGuiTheme(); + + const auto glsl_version = "#version 330"; + ImGui_ImplGlfw_InitForOpenGL(m_Window, true); + ImGui_ImplOpenGL3_Init(glsl_version); + m_ImGuiBackendsInitialized = true; + + SetupCallbacks(); + RunLoop(); + Shutdown(); + + return 0; + } + void Application::Build() { if (m_Context.Flow.CurrentScreen == Screen::Onboarding) { BuildOnboardingWindow(m_Context); return; } -#ifdef __APPLE__ - constexpr ImGuiKeyChord kPrimaryMod = ImGuiMod_Super; -#else - constexpr ImGuiKeyChord kPrimaryMod = ImGuiMod_Ctrl; -#endif - if (ImGui::Shortcut(kPrimaryMod | ImGuiKey_R) || ImGui::Shortcut(ImGuiKey_F5)) { - RefreshAvds(m_Context); - } - if (ImGui::Shortcut(kPrimaryMod | ImGuiKey_Comma)) { - m_Context.UI.ShowPreferences = true; - } - +#ifdef NDEBUG PollUpdateCheckIfNeeded(); +#endif if (m_Context.Catalog.SelectedAvd != m_Context.Catalog.PreviousSelectedAvd) { if (m_Context.Catalog.SelectedAvd >= 0 && m_Context.Catalog.SelectedAvd < m_Context.Catalog.Avds.size()) { @@ -140,6 +174,155 @@ namespace CoreDeck { m_Context.Host.Manager.Update(); } + bool Application::InitPlatform() { + if (!glfwInit()) { + ShowFatalError(COREDECK_TITLE, "Failed to initialize GLFW."); + return false; + } + m_GlfwInitialized = true; + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + return true; + } + + bool Application::CreateMainWindow() { + m_Window = glfwCreateWindow(1200, 800, COREDECK_TITLE, nullptr, nullptr); + if (!m_Window) { + ShowFatalError(COREDECK_TITLE, "Failed to create window.\nYour system may not support OpenGL 3.3."); + return false; + } + + glfwMakeContextCurrent(m_Window); + glfwSwapInterval(1); + +#ifdef _WIN32 + HWND hwnd = glfwGetWin32Window(m_Window); + HICON icon = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(1)); + SendMessage(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(icon)); + SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(icon)); +#endif + + m_Context.UI.MainWindow = m_Window; + return true; + } + + void Application::InitImGui() { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + m_ImGuiContextCreated = true; + + ImGuiIO &io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + static std::string imguiIniPath = Paths::GetAppConfigPath("imgui.ini"); + io.IniFilename = imguiIniPath.c_str(); + } + + void Application::LoadFonts() { + const ImGuiIO &io = ImGui::GetIO(); + + const std::string resourcesDir = Paths::GetResourcesDirectory(); + const std::string textFontPath = Paths::JoinPaths( + {resourcesDir, "assets", "fonts", "JetBrainsMono-Regular.ttf"} + ); + const std::string iconFontPath = Paths::JoinPaths( + {resourcesDir, "assets", "fonts", "FontAwesome7Free-Solid-900.otf"} + ); + + if (std::filesystem::exists(textFontPath)) { + static constexpr ImWchar textRanges[] = {0x0020, 0x00FF, 0x2000, 0x206F, 0,}; + io.Fonts->AddFontFromFileTTF(textFontPath.c_str(), 16.0f, nullptr, textRanges); + } + + if (std::filesystem::exists(iconFontPath)) { + ImFontConfig iconConfig; + iconConfig.MergeMode = true; + iconConfig.PixelSnapH = true; + iconConfig.GlyphMinAdvanceX = 16.0f; + + static constexpr ImWchar iconRanges[] = {0xf000, 0xf8ff, 0}; + io.Fonts->AddFontFromFileTTF(iconFontPath.c_str(), 12.0f, &iconConfig, iconRanges); + } + } + + void Application::SetupCallbacks() { + glfwSetWindowUserPointer(m_Window, this); + + glfwSetScrollCallback(m_Window, [](GLFWwindow *, const double x, const double y) { + ImGuiIO &imGuiIO = ImGui::GetIO(); + imGuiIO.AddMouseWheelEvent(static_cast(x) * 0.3f, static_cast(y) * 0.3f); + }); + + glfwSetFramebufferSizeCallback(m_Window, [](GLFWwindow *w, const int width, const int height) { + if (width == 0 || height == 0) return; + + auto *self = static_cast(glfwGetWindowUserPointer(w)); + + glViewport(0, 0, width, height); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + self->Build(); + + ImGui::Render(); + glClearColor(0.06f, 0.06f, 0.07f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(w); + }); + } + + void Application::RunLoop() { + while (!glfwWindowShouldClose(m_Window)) { + const bool focused = glfwGetWindowAttrib(m_Window, GLFW_FOCUSED); + const bool hovered = glfwGetWindowAttrib(m_Window, GLFW_HOVERED); + const double timeout = focused && hovered ? 1.0 / 60.0 : 0.25; + glfwWaitEventsTimeout(timeout); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + Build(); + + ImGui::Render(); + int display_w, display_h; + glfwGetFramebufferSize(m_Window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(m_Window); + } + } + + void Application::Shutdown() { + if (m_ImGuiBackendsInitialized) { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + m_ImGuiBackendsInitialized = false; + } + if (m_ImGuiContextCreated) { + ImGui::DestroyContext(); + m_ImGuiContextCreated = false; + } + if (m_Window) { + glfwDestroyWindow(m_Window); + m_Window = nullptr; + } + if (m_GlfwInitialized) { + glfwTerminate(); + m_GlfwInitialized = false; + } + } + void Application::PollUpdateCheckIfNeeded() { if (m_UpdateCheckFuture.valid()) { if (m_UpdateCheckFuture.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { @@ -182,10 +365,6 @@ namespace CoreDeck { } } - void Application::SetMainWindow(GLFWwindow *const window) { - m_Context.UI.MainWindow = window; - } - AppSettings CaptureAppSettingsFromContext(const Context &context) { AppSettings s; s.SchemaVersion = 1; diff --git a/src/gui/application.h b/src/gui/application.h index 8d96269..bf1665f 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -17,21 +17,51 @@ struct GLFWwindow; namespace CoreDeck { class Application { public: - Application(); + explicit Application(); - void Build(); + ~Application(); + + Application(const Application &) = delete; + + Application &operator=(const Application &) = delete; - void SetMainWindow(GLFWwindow *window); + int Run(); private: + void Build(); + + bool InitPlatform(); + + bool CreateMainWindow(); + + void InitImGui(); + + static void LoadFonts(); + + void SetupCallbacks(); + + void RunLoop(); + + void Shutdown(); + void PollUpdateCheckIfNeeded(); Context m_Context; - std::future> m_UpdateCheckFuture; + GLFWwindow *m_Window = nullptr; + bool m_GlfwInitialized = false; + bool m_ImGuiContextCreated = false; + bool m_ImGuiBackendsInitialized = false; + std::future > m_UpdateCheckFuture; bool m_AutoUpdateCheckStarted = false; bool m_UpdateCheckWasManual = false; }; + AppSettings CaptureAppSettingsFromContext(const Context &context); + + void ApplyAppSettingsToContext(Context &context, const AppSettings &settings); + + void PersistAppSettings(const Context &context); + void RefreshAvds(Context &context); void LoadAvdOptions(Context &context, const std::string &avdName); @@ -39,12 +69,6 @@ namespace CoreDeck { void SaveAvdOptions(Context &context, const std::string &avdName); std::vector &GetDefaultAvdOptions(Context &context); - - AppSettings CaptureAppSettingsFromContext(const Context &context); - - void ApplyAppSettingsToContext(Context &context, const AppSettings &settings); - - void PersistAppSettings(const Context &context); } #endif //EMU_LAUNCHER_RENDERER_H diff --git a/src/gui/context.h b/src/gui/context.h index 51b7dac..18d0670 100644 --- a/src/gui/context.h +++ b/src/gui/context.h @@ -120,6 +120,12 @@ namespace CoreDeck { std::atomic Installing{false}; std::shared_ptr Progress; std::future InstallFuture; + bool AwaitingLicenseConsent = false; + std::atomic LicenseBusy{false}; + std::future LicenseCheckFuture; + std::future LicenseAcceptFuture; + std::string PendingPackagePath; + std::string LicenseError; } ImageInstallationWork; struct Jobs { diff --git a/src/gui/windows/about.cpp b/src/gui/windows/about.cpp index fdd0e1b..5ec970b 100644 --- a/src/gui/windows/about.cpp +++ b/src/gui/windows/about.cpp @@ -27,9 +27,9 @@ namespace CoreDeck { if (ImGui::BeginPopupModal("About CoreDeck", &context.UI.ShowAboutDialog, flags)) { ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - const float titleWidth = ImGui::CalcTextSize("CoreDeck").x; + const float titleWidth = ImGui::CalcTextSize(COREDECK_TITLE).x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - titleWidth) * 0.5f); - ImGui::TextColored(HexColor("#F2F2F2"), "CoreDeck"); + ImGui::TextColored(HexColor("#F2F2F2"), COREDECK_TITLE); ImGui::PopFont(); const std::string versionText = "Version " COREDECK_VERSION " (Build " COREDECK_BUILD_NUMBER ")"; diff --git a/src/gui/windows/avd_list.cpp b/src/gui/windows/avd_list.cpp index 81adcdc..b845c52 100644 --- a/src/gui/windows/avd_list.cpp +++ b/src/gui/windows/avd_list.cpp @@ -140,7 +140,9 @@ namespace CoreDeck { ImGui::SameLine(); if (isRunning) { - if (NegativeButton(IconWithLabel(Icons::Stop, "Stop").c_str())) { + const bool isStopping = context.Host.Manager.IsStopping(avd.Name); + const std::string label = IconWithLabel(Icons::Stop, isStopping ? "Stopping..." : "Stop"); + if (NegativeButton(label.c_str(), !isStopping) && !isStopping) { context.Host.Manager.Stop(avd.Name); } } else { diff --git a/src/gui/windows/install_image.cpp b/src/gui/windows/install_image.cpp index e482fb0..6b2887c 100644 --- a/src/gui/windows/install_image.cpp +++ b/src/gui/windows/install_image.cpp @@ -9,8 +9,24 @@ #include "../application.h" #include "../widgets.h" #include "../theme.h" +#include "../../core/utilities.h" namespace CoreDeck { + static void StartInstall(Context &context, const std::string &pkgPath) { + auto &work = context.ImageInstallationWork; + work.Progress = std::make_shared(); + work.Installing = true; + auto progress = work.Progress; + work.InstallFuture = std::async( + std::launch::async, + [&context, pkgPath, progress] { + const bool ok = InstallSystemImage(context.Host.Sdk, pkgPath, progress); + context.ImageInstallationWork.Installing = false; + return ok; + } + ); + } + void BuildInstallImageWindow(Context &context) { if (context.UI.ShowInstallImageDialog) { constexpr auto title = "Install System Image###InstallImageDialog"; @@ -37,6 +53,79 @@ namespace CoreDeck { const bool isLoading = work.Prefetch.Loading.load(); const bool isInstalling = installing; + if (work.LicenseCheckFuture.valid() && + work.LicenseCheckFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + const LicenseStatus status = work.LicenseCheckFuture.get(); + work.LicenseBusy = false; + if (status == LicenseStatus::AllAccepted) { + StartInstall(context, work.PendingPackagePath); + work.PendingPackagePath.clear(); + } else if (status == LicenseStatus::SomeUnaccepted) { + work.AwaitingLicenseConsent = true; + } else { + work.LicenseError = "Could not query license state. Check that the SDK Manager is working."; + work.PendingPackagePath.clear(); + } + } + + if (work.LicenseAcceptFuture.valid() && + work.LicenseAcceptFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + const bool ok = work.LicenseAcceptFuture.get(); + work.LicenseBusy = false; + work.AwaitingLicenseConsent = false; + if (ok && !work.PendingPackagePath.empty()) { + StartInstall(context, work.PendingPackagePath); + work.PendingPackagePath.clear(); + } else { + work.LicenseError = "License acceptance failed. Try again or accept via Android Studio."; + work.PendingPackagePath.clear(); + } + } + + if (work.AwaitingLicenseConsent) { + const bool licenseBusy = work.LicenseBusy.load(); + + ImGui::Text("Accept Android SDK License Terms"); + ImGui::Spacing(); + ImGui::TextWrapped( + "Some Android SDK package licenses have not been accepted yet. " + "To install this system image, you must agree to Google's Android " + "SDK license terms. By clicking Agree, you confirm that you have " + "read and accept the current terms." + ); + ImGui::Spacing(); + if (PrimaryButton("Open license terms in browser")) { + OpenUrl("https://developer.android.com/studio/terms"); + } + + if (licenseBusy) { + ImGui::Spacing(); + ImGui::TextDisabled("Recording acceptance with the SDK Manager..."); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + const float spacing2 = ImGui::GetStyle().ItemSpacing.x; + const float halfWidth2 = (ImGui::GetContentRegionAvail().x - spacing2) * 0.5f; + + if (PositiveButton("Agree & Install", !licenseBusy, ImVec2(halfWidth2, 0))) { + work.LicenseBusy = true; + work.LicenseAcceptFuture = std::async(std::launch::async, [&context] { + return AcceptSdkLicenses(context.Host.Sdk); + }); + } + ImGui::SameLine(); + if (NegativeButton("Cancel", !licenseBusy, ImVec2(halfWidth2, 0))) { + work.AwaitingLicenseConsent = false; + work.PendingPackagePath.clear(); + } + + ImGui::EndPopup(); + return; + } + if (!isInstalling && work.InstallFuture.valid()) { if (work.InstallFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { @@ -152,25 +241,30 @@ namespace CoreDeck { const float spacing = ImGui::GetStyle().ItemSpacing.x; const float halfWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; + const bool licenseBusy = work.LicenseBusy.load(); + + if (!work.LicenseError.empty()) { + ImGui::TextColored(HexColor("#E64D40"), "%s", work.LicenseError.c_str()); + ImGui::Spacing(); + } + if (isInstalling) { ImGui::BeginDisabled(); PositiveButton("Installing...", false, ImVec2(halfWidth, 0)); ImGui::EndDisabled(); + } else if (licenseBusy) { + ImGui::BeginDisabled(); + PositiveButton("Checking licenses...", false, ImVec2(halfWidth, 0)); + ImGui::EndDisabled(); } else { if (PositiveButton("Install", canInstall, ImVec2(halfWidth, 0))) { const auto &img = work.RemoteImages[work.SelectedImage]; - work.Progress = std::make_shared(); - work.Installing = true; - const std::string pkgPath = img.PackagePath; - auto progress = work.Progress; - work.InstallFuture = std::async( - std::launch::async, - [&context, pkgPath, progress] { - const bool ok = InstallSystemImage(context.Host.Sdk, pkgPath, progress); - context.ImageInstallationWork.Installing = false; - return ok; - } - ); + work.PendingPackagePath = img.PackagePath; + work.LicenseError.clear(); + work.LicenseBusy = true; + work.LicenseCheckFuture = std::async(std::launch::async, [&context] { + return CheckSdkLicenses(context.Host.Sdk); + }); } } diff --git a/src/gui/windows/onboarding.cpp b/src/gui/windows/onboarding.cpp index d84320d..ce67c9d 100644 --- a/src/gui/windows/onboarding.cpp +++ b/src/gui/windows/onboarding.cpp @@ -34,7 +34,7 @@ namespace CoreDeck { VerticalCenter(260.0f); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - CenteredText("CoreDeck", HexColor("#F2F2F2")); + CenteredText(COREDECK_TITLE, HexColor("#F2F2F2")); ImGui::PopFont(); ImGui::Spacing(); diff --git a/src/main.cpp b/src/main.cpp index f4b0594..d5335b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,167 +1,10 @@ -#ifdef _WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#define GLFW_EXPOSE_NATIVE_WIN32 -#endif - -#include "imgui.h" -#include "imgui_impl_glfw.h" -#include "imgui_impl_opengl3.h" -#include -#ifdef _WIN32 -#include -#endif -#include -#include - #include "gui/application.h" -#include "gui/theme.h" -#include "core/paths.h" +#include "core/crash_reporter.h" int main() { - if (!glfwInit()) { -#ifdef _WIN32 - MessageBoxA(nullptr, "Failed to initialize GLFW.", "CoreDeck", MB_OK | MB_ICONERROR); -#else - std::fprintf(stderr, "Failed to initialize GLFW\n"); -#endif - return 1; - } - - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - -#ifdef __APPLE__ - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); -#endif - - GLFWwindow *window = glfwCreateWindow(1200, 800, "CoreDeck", nullptr, nullptr); - if (!window) { -#ifdef _WIN32 - MessageBoxA(nullptr, "Failed to create window.\nYour system may not support OpenGL 3.3.", "CoreDeck", - MB_OK | MB_ICONERROR); -#else - std::fprintf(stderr, "Failed to create GLFW window\n"); -#endif - glfwTerminate(); - return 1; - } - - glfwMakeContextCurrent(window); - glfwSwapInterval(1); - -#ifdef _WIN32 - HWND hwnd = glfwGetWin32Window(window); - HICON icon = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(1)); - SendMessage(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(icon)); - SendMessage(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(icon)); -#endif - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO &io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - - static std::string imguiIniPath = CoreDeck::Paths::GetAppConfigPath("imgui.ini"); - io.IniFilename = imguiIniPath.c_str(); - - const std::string resourcesDir = CoreDeck::Paths::GetResourcesDirectory(); - const std::string textFontPath = CoreDeck::Paths::JoinPaths( - {resourcesDir, "assets", "fonts", "JetBrainsMono-Regular.ttf"} - ); - const std::string iconFontPath = CoreDeck::Paths::JoinPaths( - {resourcesDir, "assets", "fonts", "FontAwesome7Free-Solid-900.otf"} - ); - - if (std::filesystem::exists(textFontPath)) { - io.Fonts->AddFontFromFileTTF(textFontPath.c_str(), 16.0f); - } - - if (std::filesystem::exists(iconFontPath)) { - ImFontConfig iconConfig; - iconConfig.MergeMode = true; - iconConfig.PixelSnapH = true; - iconConfig.GlyphMinAdvanceX = 16.0f; - - static constexpr ImWchar iconRanges[] = {0xf000, 0xf8ff, 0}; - io.Fonts->AddFontFromFileTTF( - iconFontPath.c_str(), - 12.0f, - &iconConfig, - iconRanges - ); - } - - ImGui::StyleColorsDark(); - CoreDeck::ApplyCustomImGuiTheme(); + CoreDeck::CrashReporter::Init(); CoreDeck::Application app; - app.SetMainWindow(window); - - const auto glsl_version = "#version 330"; - ImGui_ImplGlfw_InitForOpenGL(window, true); - ImGui_ImplOpenGL3_Init(glsl_version); - - glfwSetWindowUserPointer(window, &app); - - glfwSetScrollCallback(window, [](GLFWwindow *w, const double x, const double y) { - ImGuiIO &imGuiIO = ImGui::GetIO(); - imGuiIO.AddMouseWheelEvent(static_cast(x) * 0.3f, static_cast(y) * 0.3f); - }); - - glfwSetFramebufferSizeCallback(window, [](GLFWwindow *w, const int width, const int height) { - if (width == 0 || height == 0) return; - - auto *a = static_cast(glfwGetWindowUserPointer(w)); - - glViewport(0, 0, width, height); - - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - - a->Build(); - - ImGui::Render(); - glClearColor(0.06f, 0.06f, 0.07f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - glfwSwapBuffers(w); - }); - - while (!glfwWindowShouldClose(window)) { - const bool focused = glfwGetWindowAttrib(window, GLFW_FOCUSED); - const bool hovered = glfwGetWindowAttrib(window, GLFW_HOVERED); - const double timeout = focused && hovered ? 1.0 / 60.0 : 0.25; - glfwWaitEventsTimeout(timeout); - - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - - app.Build(); - - ImGui::Render(); - int display_w, display_h; - glfwGetFramebufferSize(window, &display_w, &display_h); - glViewport(0, 0, display_w, display_h); - glClearColor(0.1f, 0.1f, 0.1f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - glfwSwapBuffers(window); - } - - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - - glfwDestroyWindow(window); - glfwTerminate(); - - return 0; -} + const int code = app.Run(); + CoreDeck::CrashReporter::Shutdown(); + return code; +} \ No newline at end of file diff --git a/src/pch.h b/src/pch.h index ebf6646..cc564f6 100644 --- a/src/pch.h +++ b/src/pch.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/tools/audit_pch.sh b/tools/audit_pch.sh new file mode 100755 index 0000000..787e3e9 --- /dev/null +++ b/tools/audit_pch.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Inclusion criteria for PCH: +# - Used in ≥5 translation units +# - Cross-platform (no , , etc.) +# - Standard library or stable third-party (no project headers) + +set -eu + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC_DIR="${REPO_ROOT}/src" +PCH_FILE="${SRC_DIR}/pch.h" + +ADD_THRESHOLD=5 +REMOVE_THRESHOLD=3 + +PLATFORM_PATTERN='^(windows\.h|unistd\.h|io\.h|fcntl\.h|sys/.*|pwd\.h|dirent\.h|dlfcn\.h|pthread\.h|signal\.h|spawn\.h|CoreFoundation/.*|mach/.*|libgen\.h)$' +NONSTD_PATTERN='\.(h|hpp|hxx)$|/' + +if [ ! -f "${PCH_FILE}" ]; then + echo "error: ${PCH_FILE} not found" >&2 + exit 2 +fi + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TMPDIR}"' EXIT + +grep -rh "^#include <" "${SRC_DIR}" 2>/dev/null \ + | sed -E 's/^#include <([^>]+)>.*$/\1/' \ + | sort | uniq -c | awk '{$1=$1; print}' \ + | sort -rn > "${TMPDIR}/usage.txt" + +# Headers currently in pch.h. +grep -E '^#include <' "${PCH_FILE}" \ + | sed -E 's/^#include <([^>]+)>.*$/\1/' \ + | sort -u > "${TMPDIR}/in_pch.txt" + +awk -v threshold="${ADD_THRESHOLD}" \ + -v platform_re="${PLATFORM_PATTERN}" \ + -v nonstd_re="${NONSTD_PATTERN}" ' + { + count = $1 + header = $2 + if (count < threshold) next + if (header ~ platform_re) next + if (header ~ nonstd_re) next + print count " " header + } +' "${TMPDIR}/usage.txt" > "${TMPDIR}/eligible.txt" + +awk 'NR==FNR { in_pch[$0]=1; next } !($2 in in_pch)' \ + "${TMPDIR}/in_pch.txt" "${TMPDIR}/eligible.txt" > "${TMPDIR}/add_candidates.txt" + +awk -v threshold="${REMOVE_THRESHOLD}" ' + NR==FNR { count[$2] = $1; next } + { + c = ($0 in count) ? count[$0] : 0 + if (c < threshold) print c " " $0 + } +' "${TMPDIR}/usage.txt" "${TMPDIR}/in_pch.txt" \ + | sort -n > "${TMPDIR}/remove_candidates.txt" + +EXIT_CODE=0 + +if [ -s "${TMPDIR}/add_candidates.txt" ]; then + echo "Headers to consider ADDING to pch.h (used >= ${ADD_THRESHOLD} times, currently missing):" + awk '{ printf " %3d uses <%s>\n", $1, $2 }' "${TMPDIR}/add_candidates.txt" + echo + EXIT_CODE=1 +fi + +if [ -s "${TMPDIR}/remove_candidates.txt" ]; then + echo "Headers to consider REMOVING from pch.h (used < ${REMOVE_THRESHOLD} times):" + awk '{ printf " %3d uses <%s>\n", $1, $2 }' "${TMPDIR}/remove_candidates.txt" + echo + EXIT_CODE=1 +fi + +if [ "${EXIT_CODE}" -eq 0 ]; then + echo "pch.h looks healthy — no suggestions." + echo +fi + +echo "Top 25 standard headers by usage:" +awk -v platform_re="${PLATFORM_PATTERN}" -v nonstd_re="${NONSTD_PATTERN}" ' + $2 ~ platform_re { next } + $2 ~ nonstd_re { next } + { print } +' "${TMPDIR}/usage.txt" | head -25 \ + | awk -v in_pch_file="${TMPDIR}/in_pch.txt" ' + BEGIN { + while ((getline line < in_pch_file) > 0) in_pch[line] = 1 + close(in_pch_file) + } + { + marker = ($2 in in_pch) ? "[in pch]" : " " + printf " %s %3d uses <%s>\n", marker, $1, $2 + } + ' + +exit "${EXIT_CODE}" \ No newline at end of file