From 016eb7a23db54cb33ed722f682c7171027a91945 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:40:03 +0000 Subject: [PATCH] start: init start-hyprland and safe mode (#12484) --- CMakeLists.txt | 5 +- example/hyprland.conf | 12 ++- example/hyprland.desktop | 2 +- hyprland.pc.in | 2 +- hyprpm/src/core/PluginManager.cpp | 1 + nix/default.nix | 1 + src/Compositor.cpp | 62 ++++++++++- src/Compositor.hpp | 30 +++--- src/config/ConfigDescriptions.hpp | 6 ++ src/config/ConfigManager.cpp | 39 +++++-- src/config/ConfigManager.hpp | 2 +- src/i18n/Engine.cpp | 35 +++++++ src/i18n/Engine.hpp | 7 ++ src/main.cpp | 24 ++++- src/plugins/PluginSystem.hpp | 1 + start/CMakeLists.txt | 24 +++++ start/src/core/Instance.cpp | 165 ++++++++++++++++++++++++++++++ start/src/core/Instance.hpp | 36 +++++++ start/src/core/State.hpp | 13 +++ start/src/helpers/Logger.hpp | 9 ++ start/src/helpers/Memory.hpp | 15 +++ start/src/main.cpp | 87 ++++++++++++++++ 22 files changed, 550 insertions(+), 28 deletions(-) create mode 100644 start/CMakeLists.txt create mode 100644 start/src/core/Instance.cpp create mode 100644 start/src/core/Instance.hpp create mode 100644 start/src/core/State.hpp create mode 100644 start/src/helpers/Logger.hpp create mode 100644 start/src/helpers/Memory.hpp create mode 100644 start/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 78a76b21..81b84adc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,13 +119,13 @@ set(GLES_VERSION "GLES3") find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION}) set(AQUAMARINE_MINIMUM_VERSION 0.9.3) -set(HYPERLANG_MINIMUM_VERSION 0.3.2) +set(HYPRLANG_MINIMUM_VERSION 0.6.7) set(HYPRCURSOR_MINIMUM_VERSION 0.1.7) set(HYPRUTILS_MINIMUM_VERSION 0.10.2) set(HYPRGRAPHICS_MINIMUM_VERSION 0.1.6) pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION}) -pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=${HYPERLANG_MINIMUM_VERSION}) +pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=${HYPRLANG_MINIMUM_VERSION}) pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=${HYPRCURSOR_MINIMUM_VERSION}) pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=${HYPRUTILS_MINIMUM_VERSION}) pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=${HYPRGRAPHICS_MINIMUM_VERSION}) @@ -545,6 +545,7 @@ protocolwayland() # tools add_subdirectory(hyprctl) +add_subdirectory(start) if(NO_HYPRPM) message(STATUS "hyprpm is disabled") diff --git a/example/hyprland.conf b/example/hyprland.conf index 07b372c1..4d46134b 100644 --- a/example/hyprland.conf +++ b/example/hyprland.conf @@ -27,7 +27,7 @@ monitor=,preferred,auto,auto # Set programs that you use $terminal = kitty $fileManager = dolphin -$menu = wofi --show drun +$menu = hyprlauncher ################# @@ -329,3 +329,13 @@ windowrule { no_focus = true } + +# Hyprland-run windowrule +windowrule { + name = move-hyprland-run + + match:class = hyprland-run + + move = 20 monitor_h-120 + float = yes +} diff --git a/example/hyprland.desktop b/example/hyprland.desktop index bb2801a9..c81e0216 100644 --- a/example/hyprland.desktop +++ b/example/hyprland.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=Hyprland Comment=An intelligent dynamic tiling Wayland compositor -Exec=Hyprland +Exec=start-hyprland Type=Application DesktopNames=Hyprland Keywords=tiling;wayland;compositor; diff --git a/hyprland.pc.in b/hyprland.pc.in index 6f867d32..661a7f88 100644 --- a/hyprland.pc.in +++ b/hyprland.pc.in @@ -4,5 +4,5 @@ Name: Hyprland URL: https://github.com/hyprwm/Hyprland Description: Hyprland header files Version: @HYPRLAND_VERSION@ -Requires: aquamarine >= @AQUAMARINE_MINIMUM_VERSION@, hyprcursor >= @HYPRCURSOR_MINIMUM_VERSION@, hyprgraphics >= @HYPRGRAPHICS_MINIMUM_VERSION@, hyprlang >= @HYPERLANG_MINIMUM_VERSION@, hyprutils >= @HYPRUTILS_MINIMUM_VERSION@, libdrm, egl, cairo, xkbcommon >= @XKBCOMMMON_MINIMUM_VERSION@, libinput >= @LIBINPUT_MINIMUM_VERSION@, wayland-server >= @WAYLAND_SERVER_MINIMUM_VERSION@@PKGCONFIG_XWAYLAND_DEPENDENCIES@ +Requires: aquamarine >= @AQUAMARINE_MINIMUM_VERSION@, hyprcursor >= @HYPRCURSOR_MINIMUM_VERSION@, hyprgraphics >= @HYPRGRAPHICS_MINIMUM_VERSION@, hyprlang >= @HYPRLANG_MINIMUM_VERSION@, hyprutils >= @HYPRUTILS_MINIMUM_VERSION@, libdrm, egl, cairo, xkbcommon >= @XKBCOMMMON_MINIMUM_VERSION@, libinput >= @LIBINPUT_MINIMUM_VERSION@, wayland-server >= @WAYLAND_SERVER_MINIMUM_VERSION@@PKGCONFIG_XWAYLAND_DEPENDENCIES@ Cflags: -I${prefix} -I${prefix}/hyprland/protocols -I${prefix}/hyprland diff --git a/hyprpm/src/core/PluginManager.cpp b/hyprpm/src/core/PluginManager.cpp index 25e9f5cd..ed952eec 100644 --- a/hyprpm/src/core/PluginManager.cpp +++ b/hyprpm/src/core/PluginManager.cpp @@ -585,6 +585,7 @@ bool CPluginManager::updateHeaders(bool force) { std::filesystem::remove_all(WORKINGDIR); auto HEADERSVALID = headersValid(); + if (HEADERSVALID == HEADERS_OK || HEADERSVALID == HEADERS_MISMATCHED || HEADERSVALID == HEADERS_ABI_MISMATCH) { progress.printMessageAbove(successString("installed headers")); progress.m_iSteps = 5; diff --git a/nix/default.nix b/nix/default.nix index c8c4b044..1531a7a2 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -93,6 +93,7 @@ in ../LICENSE ../protocols ../src + ../start ../systemd ../VERSION (fs.fileFilter (file: file.hasExt "1") ../docs) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 39aaee37..49cfb561 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -142,6 +142,10 @@ static void aqLog(Aquamarine::eBackendLogLevel level, std::string msg) { Debug::log(aqLevelToHl(level), "[AQ] {}", msg); } +void CCompositor::setWatchdogFd(int fd) { + m_watchdogWriteFd = Hyprutils::OS::CFileDescriptor{fd}; +} + void CCompositor::bumpNofile() { if (!getrlimit(RLIMIT_NOFILE, &m_originalNofile)) Debug::log(LOG, "Old rlimit: soft -> {}, hard -> {}", m_originalNofile.rlim_cur, m_originalNofile.rlim_max); @@ -543,6 +547,9 @@ void CCompositor::cleanup() { if (!m_wlDisplay) return; + if (m_watchdogWriteFd.isValid()) + write(m_watchdogWriteFd.get(), "end", 3); + signal(SIGABRT, SIG_DFL); signal(SIGSEGV, SIG_DFL); @@ -796,6 +803,8 @@ void CCompositor::startCompositor() { createLockFile(); EMIT_HOOK_EVENT("ready", nullptr); + if (m_watchdogWriteFd.isValid()) + write(m_watchdogWriteFd.get(), "vax", 3); // This blocks until we are done. Debug::log(LOG, "Hyprland is ready, running the event loop!"); @@ -2459,6 +2468,7 @@ std::vector CCompositor::getWorkspacesCopy() { void CCompositor::performUserChecks() { static auto PNOCHECKXDG = CConfigValue("misc:disable_xdg_env_checks"); static auto PNOCHECKGUIUTILS = CConfigValue("misc:disable_hyprland_guiutils_check"); + static auto PNOWATCHDOG = CConfigValue("misc:disable_watchdog_warning"); if (!*PNOCHECKXDG) { const auto CURRENT_DESKTOP_ENV = getenv("XDG_CURRENT_DESKTOP"); @@ -2470,15 +2480,63 @@ void CCompositor::performUserChecks() { } if (!*PNOCHECKGUIUTILS) { - if (!NFsUtils::executableExistsInPath("hyprland-dialog")) { + if (!NFsUtils::executableExistsInPath("hyprland-dialog")) g_pHyprNotificationOverlay->addNotification(I18n::i18nEngine()->localize(I18n::TXT_KEY_NOTIF_NO_GUIUTILS), CHyprColor{}, 15000, ICON_WARNING); - } } if (g_pHyprOpenGL->m_failedAssetsNo > 0) { g_pHyprNotificationOverlay->addNotification(I18n::i18nEngine()->localize(I18n::TXT_KEY_NOTIF_FAILED_ASSETS, {{"count", std::to_string(g_pHyprOpenGL->m_failedAssetsNo)}}), CHyprColor{1.0, 0.1, 0.1, 1.0}, 15000, ICON_ERROR); } + + if (!m_watchdogWriteFd.isValid() && !*PNOWATCHDOG) + g_pHyprNotificationOverlay->addNotification(I18n::i18nEngine()->localize(I18n::TXT_KEY_NOTIF_NO_WATCHDOG), CHyprColor{1.0, 0.1, 0.1, 1.0}, 15000, ICON_WARNING); + + if (m_safeMode) + openSafeModeBox(); +} + +void CCompositor::openSafeModeBox() { + const auto OPT_LOAD = I18n::i18nEngine()->localize(I18n::TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG); + const auto OPT_OPEN = I18n::i18nEngine()->localize(I18n::TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR); + const auto OPT_OK = I18n::i18nEngine()->localize(I18n::TXT_KEY_SAFE_MODE_BUTTON_UNDERSTOOD); + + auto box = CAsyncDialogBox::create(I18n::i18nEngine()->localize(I18n::TXT_KEY_SAFE_MODE_TITLE), I18n::i18nEngine()->localize(I18n::TXT_KEY_SAFE_MODE_DESCRIPTION), + { + OPT_LOAD, + OPT_OPEN, + OPT_OK, + }); + + box->open()->then([OPT_LOAD, OPT_OK, OPT_OPEN, this](SP> result) { + if (result->hasError()) + return; + + const auto RES = result->result(); + + if (RES.starts_with(OPT_LOAD)) { + m_safeMode = false; + g_pConfigManager->reload(); + } else if (RES.starts_with(OPT_OPEN)) { + std::string reportPath; + const auto HOME = getenv("HOME"); + const auto CACHE_HOME = getenv("XDG_CACHE_HOME"); + + if (CACHE_HOME && CACHE_HOME[0] != '\0') { + reportPath += CACHE_HOME; + reportPath += "/hyprland/"; + } else if (HOME && HOME[0] != '\0') { + reportPath += HOME; + reportPath += "/.cache/hyprland/"; + } + Hyprutils::OS::CProcess proc("xdg-open", {reportPath}); + + proc.runAsync(); + + // reopen + openSafeModeBox(); + } + }); } void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWorkspace) { diff --git a/src/Compositor.hpp b/src/Compositor.hpp index 77627a84..af06059f 100644 --- a/src/Compositor.hpp +++ b/src/Compositor.hpp @@ -40,6 +40,7 @@ class CCompositor { } m_drmRenderNode; bool m_initialized = false; + bool m_safeMode = false; SP m_aqBackend; std::string m_hyprTempDataRoot = ""; @@ -65,6 +66,7 @@ class CCompositor { void cleanup(); void bumpNofile(); void restoreNofile(); + void setWatchdogFd(int fd); bool m_readyToProcess = false; bool m_sessionActive = true; @@ -167,21 +169,23 @@ class CCompositor { std::string m_explicitConfigPath; private: - void initAllSignals(); - void removeAllSignals(); - void cleanEnvironment(); - void setRandomSplash(); - void initManagers(eManagersInitStage stage); - void prepareFallbackOutput(); - void createLockFile(); - void removeLockFile(); - void setMallocThreshold(); + void initAllSignals(); + void removeAllSignals(); + void cleanEnvironment(); + void setRandomSplash(); + void initManagers(eManagersInitStage stage); + void prepareFallbackOutput(); + void createLockFile(); + void removeLockFile(); + void setMallocThreshold(); + void openSafeModeBox(); - uint64_t m_hyprlandPID = 0; - wl_event_source* m_critSigSource = nullptr; - rlimit m_originalNofile = {}; + uint64_t m_hyprlandPID = 0; + wl_event_source* m_critSigSource = nullptr; + rlimit m_originalNofile = {}; + Hyprutils::OS::CFileDescriptor m_watchdogWriteFd; - std::vector m_workspaces; + std::vector m_workspaces; }; inline UP g_pCompositor; diff --git a/src/config/ConfigDescriptions.hpp b/src/config/ConfigDescriptions.hpp index a30b7b3d..85655dfd 100644 --- a/src/config/ConfigDescriptions.hpp +++ b/src/config/ConfigDescriptions.hpp @@ -1321,6 +1321,12 @@ inline static const std::vector CONFIG_OPTIONS = { .type = CONFIG_OPTION_BOOL, .data = SConfigOptionDescription::SBoolData{false}, }, + SConfigOptionDescription{ + .value = "misc:disable_watchdog_warning", + .description = "whether to disable the warning about not using start-hyprland.", + .type = CONFIG_OPTION_BOOL, + .data = SConfigOptionDescription::SBoolData{false}, + }, SConfigOptionDescription{ .value = "misc:lockdead_screen_delay", .description = "the delay in ms after the lockdead screen appears if the lock screen did not appear after a lock event occurred.", diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index d39a33ee..bde4ebc0 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -498,6 +498,7 @@ CConfigManager::CConfigManager() { registerConfigVar("misc:render_unfocused_fps", Hyprlang::INT{15}); registerConfigVar("misc:disable_xdg_env_checks", Hyprlang::INT{0}); registerConfigVar("misc:disable_hyprland_guiutils_check", Hyprlang::INT{0}); + registerConfigVar("misc:disable_watchdog_warning", Hyprlang::INT{0}); registerConfigVar("misc:lockdead_screen_delay", Hyprlang::INT{1000}); registerConfigVar("misc:enable_anr_dialog", Hyprlang::INT{1}); registerConfigVar("misc:anr_missed_pings", Hyprlang::INT{5}); @@ -914,7 +915,7 @@ void CConfigManager::reloadRuleConfigs() { } } -std::optional CConfigManager::generateConfig(std::string configPath) { +std::optional CConfigManager::generateConfig(std::string configPath, bool safeMode) { std::string parentPath = std::filesystem::path(configPath).parent_path(); if (!parentPath.empty()) { @@ -931,7 +932,14 @@ std::optional CConfigManager::generateConfig(std::string configPath Debug::log(WARN, "No config file found; attempting to generate."); std::ofstream ofs; ofs.open(configPath, std::ios::trunc); - ofs << AUTOGENERATED_PREFIX << EXAMPLE_CONFIG; + if (!safeMode) { + ofs << AUTOGENERATED_PREFIX; + ofs << EXAMPLE_CONFIG; + } else { + std::string n = std::string{EXAMPLE_CONFIG}; + replaceInString(n, "\n$menu = hyprlauncher\n", "\n$menu = hyprland-run\n"); + ofs << n; + } ofs.close(); if (ofs.fail()) @@ -941,7 +949,16 @@ std::optional CConfigManager::generateConfig(std::string configPath } std::string CConfigManager::getMainConfigPath() { - static std::string CONFIG_PATH = [this]() -> std::string { + static bool lastSafeMode = g_pCompositor->m_safeMode; + static auto getCfgPath = [this]() -> std::string { + lastSafeMode = g_pCompositor->m_safeMode; + m_firstExecDispatched = false; + + if (g_pCompositor->m_safeMode) { + const auto CONFIGPATH = g_pCompositor->m_instancePath + "/recoverycfg.conf"; + return generateConfig(CONFIGPATH, false).value(); + } + if (!g_pCompositor->m_explicitConfigPath.empty()) return g_pCompositor->m_explicitConfigPath; @@ -956,7 +973,13 @@ std::string CConfigManager::getMainConfigPath() { return generateConfig(CONFIGPATH).value(); } else throw std::runtime_error("Neither HOME nor XDG_CONFIG_HOME are set in the environment. Could not find config in XDG_CONFIG_DIRS or /etc/xdg."); - }(); + }; + static std::string CONFIG_PATH = getCfgPath(); + + if (lastSafeMode != g_pCompositor->m_safeMode) { + CONFIG_PATH = getCfgPath(); + m_config->changeRootPath(CONFIG_PATH.c_str()); + } return CONFIG_PATH; } @@ -1415,7 +1438,6 @@ void CConfigManager::init() { reload(); }); - const std::string CONFIGPATH = getMainConfigPath(); reload(); m_isFirstLaunch = false; @@ -1637,7 +1659,12 @@ void CConfigManager::dispatchExecOnce() { g_pInputManager->setTabletConfigs(); // check for user's possible errors with their setup and notify them if needed - g_pCompositor->performUserChecks(); + // this is additionally guarded because exiting safe mode will re-run this. + static bool once = true; + if (once) { + g_pCompositor->performUserChecks(); + once = false; + } } void CConfigManager::dispatchExecShutdown() { diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index a13547b5..1055e5f2 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -317,7 +317,7 @@ class CConfigManager { // internal methods void setDefaultAnimationVars(); std::optional resetHLConfig(); - std::optional generateConfig(std::string configPath); + std::optional generateConfig(std::string configPath, bool safeMode = false); std::optional verifyConfigExists(); void reloadRuleConfigs(); diff --git a/src/i18n/Engine.cpp b/src/i18n/Engine.cpp index 62406df3..818fa75e 100644 --- a/src/i18n/Engine.cpp +++ b/src/i18n/Engine.cpp @@ -99,6 +99,18 @@ I18n::CI18nEngine::CI18nEngine() { huEngine->registerEntry("en_US", TXT_KEY_NOTIF_FAILED_TO_LOAD_PLUGIN, "Failed to load plugin {name}: {error}"); huEngine->registerEntry("en_US", TXT_KEY_NOTIF_CM_RELOAD_FAILED, "CM shader reload failed, falling back to rgba/rgbx."); huEngine->registerEntry("en_US", TXT_KEY_NOTIF_WIDE_COLOR_NOT_10B, "Monitor {name}: wide color gamut is enabled but the display is not in 10-bit mode."); + huEngine->registerEntry("en_US", TXT_KEY_NOTIF_NO_WATCHDOG, + "Hyprland was started without start-hyprland. This is highly not recommended unless you are in a debugging environment."); + + huEngine->registerEntry("en_US", TXT_KEY_SAFE_MODE_TITLE, "Safe Mode"); + huEngine->registerEntry("en_US", TXT_KEY_SAFE_MODE_DESCRIPTION, + "Hyprland has been launched in safe mode, which means your last session crashed.\nSafe mode prevents your config from being loaded. You can " + "troubleshoot in this environment, or load your config with the button below.\nDefault keybinds apply: SUPER+Q for kitty, SUPER+R for a basic runner, " + "SUPER+M to exit.\nRestarting " + "Hyprland will launch in normal mode again."); + huEngine->registerEntry("en_US", TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG, "Load config"); + huEngine->registerEntry("en_US", TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR, "Open crash report directory"); + huEngine->registerEntry("en_US", TXT_KEY_SAFE_MODE_BUTTON_UNDERSTOOD, "Ok, close this"); // as_IN (Assamese) huEngine->registerEntry("as_IN", TXT_KEY_ANR_TITLE, "এপ্লিকেচনে উত্তৰ দিয়া নাই"); @@ -619,6 +631,17 @@ I18n::CI18nEngine::CI18nEngine() { huEngine->registerEntry("ja_JP", TXT_KEY_NOTIF_FAILED_TO_LOAD_PLUGIN, "プラグイン{name}のロード失敗: {error}"); huEngine->registerEntry("ja_JP", TXT_KEY_NOTIF_CM_RELOAD_FAILED, "CMシェーダーのリロード失敗、rgba/rgbxを使いました。"); huEngine->registerEntry("ja_JP", TXT_KEY_NOTIF_WIDE_COLOR_NOT_10B, "画面{name}:広い色域は設定していますけど、画面は10ビットモードに設定されていません。"); + huEngine->registerEntry("ja_JP", TXT_KEY_NOTIF_NO_WATCHDOG, "Hyprlandはstart-hyprlandなしで実行されました。これはデバグ環境以外でおすすめしません。"); + + huEngine->registerEntry("ja_JP", TXT_KEY_SAFE_MODE_TITLE, "安全モード"); + huEngine->registerEntry( + "ja_JP", TXT_KEY_SAFE_MODE_DESCRIPTION, + "Hyprlandは安全モードに実行しました。これは、Hyprlandはクラッシュしましたから。\n安全モードはコンフィグをロードしなくて、問題を修正できる環境です。下のボタンでコンフィグを" + "ロードできます。\nデフォルトなキーバインドがあります。SUPER+Qはkitty、SUPER+Rは簡素なランチャー、SUPER+" + "MはHyprlandから退出。\nHyprlandを再び実行すれば、普通モードで実行します。"); + huEngine->registerEntry("ja_JP", TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG, "コンフィグをロード"); + huEngine->registerEntry("ja_JP", TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR, "クラッシュレポートフォルダーを開く"); + huEngine->registerEntry("ja_JP", TXT_KEY_SAFE_MODE_BUTTON_UNDERSTOOD, "分かりました、このウィンドウをクローズ"); // lv_LV (Latvian) huEngine->registerEntry("lv_LV", TXT_KEY_ANR_TITLE, "Lietotne nereaģē"); @@ -903,6 +926,18 @@ I18n::CI18nEngine::CI18nEngine() { huEngine->registerEntry("pl_PL", TXT_KEY_NOTIF_FAILED_TO_LOAD_PLUGIN, "Nie udało się załadować plugin'a {name}: {error}"); huEngine->registerEntry("pl_PL", TXT_KEY_NOTIF_CM_RELOAD_FAILED, "Nie udało się przeładować shader'a CM, użyto rgba/rgbx."); huEngine->registerEntry("pl_PL", TXT_KEY_NOTIF_WIDE_COLOR_NOT_10B, "Monitor {name}: skonfigurowano szeroką głębię barw, ale monitor nie jest w trybie 10-bit."); + huEngine->registerEntry("pl_PL", TXT_KEY_NOTIF_NO_WATCHDOG, + "Hyprland został uruchomiony bez start-hyprland. Nie jest to zalecane, chyba, że jest to środowisko do debugowania."); + + huEngine->registerEntry("pl_PL", TXT_KEY_SAFE_MODE_TITLE, "Tryb Bezpieczny"); + huEngine->registerEntry("pl_PL", TXT_KEY_SAFE_MODE_DESCRIPTION, + "Hyprland został uruchomiony w trybie bezpiecznym, co oznacza, że twoja ostatnia sesja uległa awarii.\nTryb bezpieczny zapobiega ładowaniu twojej " + "konfiguracji. Możesz próbować rozwiązać" + "problem w tym środowisku, lub załadować swoją konfigurację przyciskiem poniżej.\nDomyślne skróty klawiszowe są dostępne: SUPER+Q uruchamia kitty, " + "SUPER+R otwiera podstawowy launcher, SUPER+M zamyka Hyprland.\nUruchomienie ponowne Hyprland'a uruchomi go w trybie normalnym."); + huEngine->registerEntry("pl_PL", TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG, "Załaduj konfigurację"); + huEngine->registerEntry("pl_PL", TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR, "Otwórz folder z raportami awarii"); + huEngine->registerEntry("pl_PL", TXT_KEY_SAFE_MODE_BUTTON_UNDERSTOOD, "Ok, zamknij to okno"); // pt_PT (Portuguese Portugal) huEngine->registerEntry("pt_PT", TXT_KEY_ANR_TITLE, "A aplicação não está a responder"); diff --git a/src/i18n/Engine.hpp b/src/i18n/Engine.hpp index d1182632..c3892546 100644 --- a/src/i18n/Engine.hpp +++ b/src/i18n/Engine.hpp @@ -36,6 +36,13 @@ namespace I18n { TXT_KEY_NOTIF_FAILED_TO_LOAD_PLUGIN, TXT_KEY_NOTIF_CM_RELOAD_FAILED, TXT_KEY_NOTIF_WIDE_COLOR_NOT_10B, + TXT_KEY_NOTIF_NO_WATCHDOG, + + TXT_KEY_SAFE_MODE_TITLE, + TXT_KEY_SAFE_MODE_DESCRIPTION, + TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR, + TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG, + TXT_KEY_SAFE_MODE_BUTTON_UNDERSTOOD, }; class CI18nEngine { diff --git a/src/main.cpp b/src/main.cpp index 3e21c965..ed436934 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -68,7 +68,8 @@ int main(int argc, char** argv) { std::string configPath; std::string socketName; int socketFd = -1; - bool ignoreSudo = false, verifyConfig = false; + bool ignoreSudo = false, verifyConfig = false, safeMode = false; + int watchdogFd = -1; if (argc > 1) { std::span args{argv + 1, sc(argc - 1)}; @@ -152,6 +153,23 @@ int main(int argc, char** argv) { } else if (value == "--verify-config") { verifyConfig = true; continue; + } else if (value == "--safe-mode") { + safeMode = true; + continue; + } else if (value == "--watchdog-fd") { + if (std::next(it) == args.end()) { + help(); + return 1; + } + + try { + watchdogFd = std::stoi(*std::next(it)); + it++; + } catch (...) { + std::println(stderr, "[ ERROR ] Invalid fd for watchdog fd"); + help(); + return 1; + } } else { std::println(stderr, "[ ERROR ] Unknown option '{}' !", value); help(); @@ -193,6 +211,10 @@ int main(int argc, char** argv) { reapZombieChildrenAutomatically(); + if (watchdogFd > 0) + g_pCompositor->setWatchdogFd(watchdogFd); + if (safeMode) + g_pCompositor->m_safeMode = true; g_pCompositor->initServer(socketName, socketFd); if (verifyConfig) diff --git a/src/plugins/PluginSystem.hpp b/src/plugins/PluginSystem.hpp index 7d062a9b..ed421960 100644 --- a/src/plugins/PluginSystem.hpp +++ b/src/plugins/PluginSystem.hpp @@ -2,6 +2,7 @@ #include "../defines.hpp" #include "../helpers/defer/Promise.hpp" +#include "../helpers/time/Timer.hpp" #include "PluginAPI.hpp" #include "../managers/permissions/DynamicPermissionManager.hpp" #include diff --git a/start/CMakeLists.txt b/start/CMakeLists.txt new file mode 100644 index 00000000..00b1fded --- /dev/null +++ b/start/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.19) + +project(start-hyprland DESCRIPTION "Hyprland watchdog binary") + +include(GNUInstallDirs) + +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(starthyprland_deps REQUIRED IMPORTED_TARGET hyprutils>=0.10.3) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") + +add_executable(start-hyprland ${SRCFILES}) + +target_link_libraries(start-hyprland PUBLIC PkgConfig::starthyprland_deps) + +install(TARGETS start-hyprland) + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + set(CMAKE_EXECUTABLE_ENABLE_EXPORTS TRUE) +endif() diff --git a/start/src/core/Instance.cpp b/start/src/core/Instance.cpp new file mode 100644 index 00000000..2ff53279 --- /dev/null +++ b/start/src/core/Instance.cpp @@ -0,0 +1,165 @@ +#include "Instance.hpp" +#include "State.hpp" +#include "../helpers/Logger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Hyprutils::OS; +using namespace std::string_literals; + +// +void CHyprlandInstance::runHyprlandThread(bool safeMode) { + std::vector argsStd; + argsStd.emplace_back("--watchdog-fd"); + argsStd.emplace_back(std::format("{}", m_toHlPid.get())); + if (safeMode) + argsStd.emplace_back("--safe-mode"); + + for (const auto& a : g_state->rawArgvNoBinPath) { + argsStd.emplace_back(a); + } + + // spawn a process manually. Hyprutils' Async is detached, while Sync redirects stdout + // TODO: make Sync respect fds? + + std::vector args = {strdup(g_state->customPath.value_or("Hyprland").c_str())}; + for (const auto& a : argsStd) { + args.emplace_back(strdup(a.c_str())); + } + args.emplace_back(nullptr); + + int forkRet = fork(); + if (forkRet == 0) { + // Make hyprland die on our SIGKILL + prctl(PR_SET_PDEATHSIG, SIGKILL); + + execvp(g_state->customPath.value_or("Hyprland").c_str(), args.data()); + + g_logger->log(Hyprutils::CLI::LOG_ERR, "fork(): execvp failed: {}", strerror(errno)); + std::fflush(stdout); + exit(1); + } else + m_hlPid = forkRet; + + m_hlThread = std::thread([this] { + while (true) { + int status = 0; + int ret = waitpid(m_hlPid, &status, 0); + if (ret == -1) { + g_logger->log(Hyprutils::CLI::LOG_ERR, "Couldn't waitpid for hyprland: {}", strerror(errno)); + break; + } + + if (WIFEXITED(status)) + break; + } + + write(m_wakeupWrite.get(), "vax", 3); + + std::fflush(stdout); + std::fflush(stderr); + }); +} + +void CHyprlandInstance::forceQuit() { + m_hyprlandExiting = true; + kill(m_hlPid, SIGTERM); // gracefully, can get stuck but it's unlikely +} + +void CHyprlandInstance::clearFd(const Hyprutils::OS::CFileDescriptor& fd) { + static std::array buf; + read(fd.get(), buf.data(), 1023); +} + +void CHyprlandInstance::dispatchHyprlandEvent() { + std::string recvd = ""; + static std::array buf; + ssize_t n = read(m_fromHlPid.get(), buf.data(), 4096); + if (n < 0) { + g_logger->log(Hyprutils::CLI::LOG_ERR, "Failed dispatching hl events"); + return; + } + + recvd.append(buf.data(), n); + + if (recvd.empty()) + return; + + for (const auto& s : std::views::split(recvd, '\n')) { + const std::string_view sv = std::string_view{s}; + if (sv == "vax") { + // init passed + m_hyprlandInitialized = true; + continue; + } + + if (sv == "end") { + // exiting + m_hyprlandExiting = true; + continue; + } + } +} + +bool CHyprlandInstance::run(bool safeMode) { + int pipefds[2]; + pipe(pipefds); + + m_fromHlPid = CFileDescriptor{pipefds[0]}; + m_toHlPid = CFileDescriptor{pipefds[1]}; + + pipe(pipefds); + + m_wakeupRead = CFileDescriptor{pipefds[0]}; + m_wakeupWrite = CFileDescriptor{pipefds[1]}; + + runHyprlandThread(safeMode); + + pollfd pollfds[2] = { + { + .fd = m_wakeupRead.get(), + .events = POLLIN, + .revents = 0, + }, + { + .fd = m_fromHlPid.get(), + .events = POLLIN, + .revents = 0, + }, + }; + + while (true) { + int ret = poll(pollfds, 2, -1); + + if (ret < 0) { + g_logger->log(Hyprutils::CLI::LOG_ERR, "poll() failed, exiting"); + exit(1); + } + + if (pollfds[1].revents & POLLIN) { + g_logger->log(Hyprutils::CLI::LOG_DEBUG, "got an event from hyprland"); + dispatchHyprlandEvent(); + continue; + } + + if (pollfds[0].revents & POLLIN) { + g_logger->log(Hyprutils::CLI::LOG_DEBUG, "hyprland exit, breaking poll, checking state"); + clearFd(m_wakeupRead); + break; + } + } + + m_hlThread.join(); + + return !m_hyprlandInitialized || m_hyprlandExiting; +} \ No newline at end of file diff --git a/start/src/core/Instance.hpp b/start/src/core/Instance.hpp new file mode 100644 index 00000000..2c72dc12 --- /dev/null +++ b/start/src/core/Instance.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "../helpers/Memory.hpp" + +class CHyprlandInstance { + public: + CHyprlandInstance() = default; + ~CHyprlandInstance() = default; + + CHyprlandInstance(const CHyprlandInstance&) = delete; + CHyprlandInstance(CHyprlandInstance&) = delete; + CHyprlandInstance(CHyprlandInstance&&) = delete; + + bool run(bool safeMode = false); // if returns false, restart. + void forceQuit(); + + private: + void runHyprlandThread(bool safeMode); + void clearFd(const Hyprutils::OS::CFileDescriptor& fd); + void dispatchHyprlandEvent(); + + int m_hlPid = -1; + + Hyprutils::OS::CFileDescriptor m_fromHlPid, m_toHlPid; + Hyprutils::OS::CFileDescriptor m_wakeupRead, m_wakeupWrite; + + bool m_hyprlandInitialized = false; + bool m_hyprlandExiting = false; + + std::thread m_hlThread; +}; + +inline UP g_instance; diff --git a/start/src/core/State.hpp b/start/src/core/State.hpp new file mode 100644 index 00000000..6cf73a96 --- /dev/null +++ b/start/src/core/State.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "../helpers/Memory.hpp" + +#include +#include + +struct SState { + std::span rawArgvNoBinPath; + std::optional customPath; +}; + +inline UP g_state = makeUnique(); \ No newline at end of file diff --git a/start/src/helpers/Logger.hpp b/start/src/helpers/Logger.hpp new file mode 100644 index 00000000..ae771203 --- /dev/null +++ b/start/src/helpers/Logger.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include "Memory.hpp" + +// we do this to add a from start-hyprland to the logs +inline UP g_loggerMain = makeUnique(); +inline UP g_logger; diff --git a/start/src/helpers/Memory.hpp b/start/src/helpers/Memory.hpp new file mode 100644 index 00000000..66ba2c1f --- /dev/null +++ b/start/src/helpers/Memory.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +using namespace Hyprutils::Memory; + +template +using SP = Hyprutils::Memory::CSharedPointer; +template +using WP = Hyprutils::Memory::CWeakPointer; +template +using UP = Hyprutils::Memory::CUniquePointer; +template +using ASP = Hyprutils::Memory::CAtomicSharedPointer; diff --git a/start/src/main.cpp b/start/src/main.cpp new file mode 100644 index 00000000..78fbf0f4 --- /dev/null +++ b/start/src/main.cpp @@ -0,0 +1,87 @@ +#include +#include + +#include "helpers/Logger.hpp" +#include "core/State.hpp" +#include "core/Instance.hpp" + +using namespace Hyprutils::CLI; + +#define ASSERT(expr) \ + if (!(expr)) { \ + g_logger->log(LOG_CRIT, "Failed assertion at line {} in {}: {} was false", __LINE__, \ + ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find("/src/") + 1); })(), #expr); \ + std::abort(); \ + } + +constexpr const char* HELP_INFO = R"#(start-hyprland - A binary to properly start Hyprland via a watchdog process. +Any arguments after -- are passed to Hyprland. For Hyprland help, run start-hyprland -- --help or Hyprland --help)#"; + +// +static void onSignal(int sig) { + if (!g_instance) + return; + + g_instance->forceQuit(); + g_instance.reset(); + + exit(0); +} + +int main(int argc, const char** argv, const char** envp) { + g_logger = makeUnique(*g_loggerMain); + g_logger->setName("start-hyprland"); + g_logger->setLogLevel(Hyprutils::CLI::LOG_DEBUG); + + signal(SIGTERM, ::onSignal); + signal(SIGINT, ::onSignal); + signal(SIGKILL, ::onSignal); + + int startArgv = -1; + + for (int i = 1; i < argc; ++i) { + std::string_view arg = argv[i]; + + if (arg == "--") { + startArgv = i + 1; + break; + } + if (arg == "-h" || arg == "--help") { + std::println("{}", HELP_INFO); + return 0; + } + if (arg == "--path" || arg == "-p") { + if (i + 1 >= argc) { + std::println("{} requires a path", arg); + return 1; + } + + g_state->customPath = argv[++i]; + continue; + } + } + + if (startArgv != -1) + g_state->rawArgvNoBinPath = std::span{argv + startArgv, argc - startArgv}; + + if (!g_state->rawArgvNoBinPath.empty()) + g_logger->log(Hyprutils::CLI::LOG_WARN, "Arguments after -- are passed to Hyprland"); + + bool safeMode = false; + while (true) { + g_instance = makeUnique(); + const bool RET = g_instance->run(safeMode); + g_instance.reset(); + + if (!RET) { + g_logger->log(Hyprutils::CLI::LOG_ERR, "Hyprland exit not-cleanly, restarting"); + safeMode = true; + continue; + } + + g_logger->log(Hyprutils::CLI::LOG_DEBUG, "Hyprland exit cleanly."); + break; + } + + return 0; +} \ No newline at end of file