diff --git a/CMakeLists.txt b/CMakeLists.txt index b4c36158..65586f5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,7 +108,7 @@ find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION}) pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.8.0) pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=0.3.2) pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=0.1.7) -pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.5.1) +pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.6.0) pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.1) string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION}) diff --git a/flake.lock b/flake.lock index ff2935a2..b7f2f4a2 100644 --- a/flake.lock +++ b/flake.lock @@ -128,11 +128,11 @@ ] }, "locked": { - "lastModified": 1743549251, - "narHash": "sha256-yf+AXt0RkAkCyF6iSnJt6EJAnNG/l6qv70CVzhRP6Bg=", + "lastModified": 1743714874, + "narHash": "sha256-yt8F7NhMFCFHUHy/lNjH/pjZyIDFNk52Q4tivQ31WFo=", "owner": "hyprwm", "repo": "hyprland-protocols", - "rev": "4ab17ccac08456cb5e00e8bd323de2efd30612be", + "rev": "3a5c2bda1c1a4e55cc1330c782547695a93f05b2", "type": "github" }, "original": { @@ -238,11 +238,11 @@ ] }, "locked": { - "lastModified": 1742984269, - "narHash": "sha256-uz9FaCIbga/gQ5ZG1Hb4HVVjTWT1qjjCAFlCXiaefxg=", + "lastModified": 1743950287, + "narHash": "sha256-/6IAEWyb8gC/NKZElxiHChkouiUOrVYNq9YqG0Pzm4Y=", "owner": "hyprwm", "repo": "hyprutils", - "rev": "7248194a2ce0106ae647b70d0526a96dc9d6ad60", + "rev": "f2dc70e448b994cef627a157ee340135bd68fbc6", "type": "github" }, "original": { @@ -276,11 +276,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1743583204, - "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", + "lastModified": 1743827369, + "narHash": "sha256-rpqepOZ8Eo1zg+KJeWoq1HAOgoMCDloqv5r2EAa9TSA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", + "rev": "42a1c966be226125b48c384171c44c651c236c22", "type": "github" }, "original": { diff --git a/meson.build b/meson.build index a40975aa..66ef1539 100644 --- a/meson.build +++ b/meson.build @@ -35,7 +35,7 @@ aquamarine = dependency('aquamarine', version: '>=0.8.0') hyprcursor = dependency('hyprcursor', version: '>=0.1.7') hyprgraphics = dependency('hyprgraphics', version: '>= 0.1.1') hyprlang = dependency('hyprlang', version: '>= 0.3.2') -hyprutils = dependency('hyprutils', version: '>= 0.2.3') +hyprutils = dependency('hyprutils', version: '>= 0.6.0') aquamarine_version_list = aquamarine.version().split('.') add_project_arguments(['-DAQUAMARINE_VERSION="@0@"'.format(aquamarine.version())], language: 'cpp') add_project_arguments(['-DAQUAMARINE_VERSION_MAJOR=@0@'.format(aquamarine_version_list.get(0))], language: 'cpp') diff --git a/src/helpers/AsyncDialogBox.cpp b/src/helpers/AsyncDialogBox.cpp new file mode 100644 index 00000000..2ca99ab9 --- /dev/null +++ b/src/helpers/AsyncDialogBox.cpp @@ -0,0 +1,122 @@ +#include "AsyncDialogBox.hpp" +#include "./fs/FsUtils.hpp" +#include +#include "../managers/eventLoop/EventLoopManager.hpp" + +using namespace Hyprutils::OS; + +SP CAsyncDialogBox::create(const std::string& title, const std::string& description, std::vector buttons) { + if (!NFsUtils::executableExistsInPath("hyprland-dialog")) { + Debug::log(ERR, "CAsyncDialogBox: cannot create, no hyprland-dialog"); + return nullptr; + } + + auto dialog = SP(new CAsyncDialogBox(title, description, buttons)); + + dialog->m_selfWeakReference = dialog; + + return dialog; +} + +CAsyncDialogBox::CAsyncDialogBox(const std::string& title, const std::string& description, std::vector buttons) : + m_title(title), m_description(description), m_buttons(buttons) { + ; +} + +static int onFdWrite(int fd, uint32_t mask, void* data) { + auto box = (CAsyncDialogBox*)data; + + box->onWrite(fd, mask); + + return 0; +} + +void CAsyncDialogBox::onWrite(int fd, uint32_t mask) { + if (mask & WL_EVENT_READABLE) { + std::array buf; + int ret = 0; + + // make the FD nonblock for a moment + // TODO: can we avoid this without risking a blocking read()? + int fdFlags = fcntl(fd, F_GETFL, 0); + if (fcntl(fd, F_SETFL, fdFlags | O_NONBLOCK) < 0) { + Debug::log(ERR, "CAsyncDialogBox::onWrite: fcntl 1 failed!"); + return; + } + + while ((ret = read(m_pipeReadFd.get(), buf.data(), 1023)) > 0) { + m_stdout += std::string_view{(char*)buf.data(), (size_t)ret}; + } + + // restore the flags (otherwise libwayland wont give us a hangup) + if (fcntl(fd, F_SETFL, fdFlags) < 0) { + Debug::log(ERR, "CAsyncDialogBox::onWrite: fcntl 2 failed!"); + return; + } + } + + if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) { + Debug::log(LOG, "CAsyncDialogBox: dialog {:x} hung up, closed."); + + if (m_onResolution) + m_onResolution(m_stdout); + + wl_event_source_remove(m_readEventSource); + m_selfReference.reset(); + return; + } +} + +void CAsyncDialogBox::open(std::function onResolution) { + m_onResolution = onResolution; + + std::string buttonsString = ""; + for (auto& b : m_buttons) { + buttonsString += b + ";"; + } + if (!buttonsString.empty()) + buttonsString.pop_back(); + + CProcess proc("hyprland-dialog", std::vector{"--title", m_title, "--text", m_description, "--buttons", buttonsString}); + + int outPipe[2]; + if (pipe(outPipe)) { + Debug::log(ERR, "CAsyncDialogBox::open: failed to pipe()"); + return; + } + + m_pipeReadFd = CFileDescriptor(outPipe[0]); + + proc.setStdoutFD(outPipe[1]); + + m_readEventSource = wl_event_loop_add_fd(g_pEventLoopManager->m_sWayland.loop, m_pipeReadFd.get(), WL_EVENT_READABLE, ::onFdWrite, this); + + if (!m_readEventSource) { + Debug::log(ERR, "CAsyncDialogBox::open: failed to add read fd to loop"); + return; + } + + m_selfReference = m_selfWeakReference.lock(); + + m_dialogPid = proc.pid(); + + if (!proc.runAsync()) { + Debug::log(ERR, "CAsyncDialogBox::open: failed to run async"); + wl_event_source_remove(m_readEventSource); + return; + } + + // close the write fd, only the dialog owns it now + close(outPipe[1]); +} + +void CAsyncDialogBox::kill() { + if (m_dialogPid <= 0) + return; + + ::kill(m_dialogPid, SIGKILL); +} + +bool CAsyncDialogBox::isRunning() const { + return m_readEventSource; +} \ No newline at end of file diff --git a/src/helpers/AsyncDialogBox.hpp b/src/helpers/AsyncDialogBox.hpp new file mode 100644 index 00000000..d3cc242c --- /dev/null +++ b/src/helpers/AsyncDialogBox.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "../macros.hpp" +#include "./memory/Memory.hpp" + +#include +#include + +#include +#include + +struct wl_event_source; + +class CAsyncDialogBox { + public: + static SP create(const std::string& title, const std::string& description, std::vector buttons); + + CAsyncDialogBox(const CAsyncDialogBox&) = delete; + CAsyncDialogBox(CAsyncDialogBox&&) = delete; + CAsyncDialogBox& operator=(const CAsyncDialogBox&) = delete; + CAsyncDialogBox& operator=(CAsyncDialogBox&&) = delete; + + void open(std::function onResolution); + void kill(); + bool isRunning() const; + + void onWrite(int fd, uint32_t mask); + + private: + CAsyncDialogBox(const std::string& title, const std::string& description, std::vector buttons); + + pid_t m_dialogPid = 0; + wl_event_source* m_readEventSource = nullptr; + std::function m_onResolution; + Hyprutils::OS::CFileDescriptor m_pipeReadFd; + std::string m_stdout = ""; + + const std::string m_title; + const std::string m_description; + const std::vector m_buttons; + + // WARNING: cyclic reference. This will be removed once the event source is removed to avoid dangling pointers + SP m_selfReference; + WP m_selfWeakReference; +}; \ No newline at end of file diff --git a/src/managers/ANRManager.cpp b/src/managers/ANRManager.cpp index 3debcda8..acdb39b7 100644 --- a/src/managers/ANRManager.cpp +++ b/src/managers/ANRManager.cpp @@ -39,8 +39,6 @@ CANRManager::CANRManager() { } void CANRManager::onTick() { - std::erase_if(m_data, [](const auto& e) { return e->isDefunct(); }); - static auto PENABLEANR = CConfigValue("misc:enable_anr_dialog"); static auto PANRTHRESHOLD = CConfigValue("misc:anr_missed_pings"); @@ -68,7 +66,7 @@ void CANRManager::onTick() { continue; if (data->missedResponses >= *PANRTHRESHOLD) { - if (!data->isThreadRunning() && !data->dialogThreadSaidWait) { + if (!data->isRunning() && !data->dialogSaidWait) { data->runDialog("Application Not Responding", firstWindow->m_szTitle, firstWindow->m_szClass, data->getPid()); for (const auto& w : g_pCompositor->m_vWindows) { @@ -81,11 +79,11 @@ void CANRManager::onTick() { *w->m_notRespondingTint = 0.2F; } } - } else if (data->isThreadRunning()) + } else if (data->isRunning()) data->killDialog(); if (data->missedResponses == 0) - data->dialogThreadSaidWait = false; + data->dialogSaidWait = false; data->missedResponses++; @@ -115,7 +113,7 @@ void CANRManager::onResponse(SP pXwaylandSurface) { void CANRManager::onResponse(SP data) { data->missedResponses = 0; - if (data->isThreadRunning()) + if (data->isRunning()) data->killDialog(); } @@ -158,64 +156,39 @@ CANRManager::SANRData::SANRData(PHLWINDOW pWindow) : } CANRManager::SANRData::~SANRData() { - if (dialogThread.joinable()) { + if (dialogBox && dialogBox->isRunning()) killDialog(); - // dangerous: might lock if the above failed!! - dialogThread.join(); - } } void CANRManager::SANRData::runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID) { - if (!dialogThreadExited) + if (dialogBox && dialogBox->isRunning()) killDialog(); - // dangerous: might lock if the above failed!! - if (dialogThread.joinable()) - dialogThread.join(); + dialogBox = CAsyncDialogBox::create(title, + std::format("Application {} with class of {} is not responding.\nWhat do you want to do with it?", appName.empty() ? "unknown" : appName, + appClass.empty() ? "unknown" : appClass), + std::vector{"Terminate", "Wait"}); - dialogThreadExited = false; - dialogThreadSaidWait = false; - dialogThread = std::thread([title, appName, appClass, dialogWmPID, this]() { - SP proc = makeShared("hyprland-dialog", - std::vector{"--title", title, "--text", - std::format("Application {} with class of {} is not responding.\nWhat do you want to do with it?", - appName.empty() ? "unknown" : appName, appClass.empty() ? "unknown" : appClass), - "--buttons", "Terminate;Wait"}); - - dialogProc = proc; - proc->runSync(); - - dialogThreadExited = true; - - if (proc->stdOut().empty()) - return; - - if (proc->stdOut().starts_with("Terminate")) - kill(dialogWmPID, SIGKILL); - if (proc->stdOut().starts_with("Wait")) - dialogThreadSaidWait = true; + dialogBox->open([dialogWmPID, this](std::string result) { + if (result.starts_with("Terminate")) + ::kill(dialogWmPID, SIGKILL); + else if (result.starts_with("Wait")) + dialogSaidWait = true; + else + Debug::log(ERR, "CANRManager::SANRData::runDialog: lambda: unrecognized result: {}", result); }); } -bool CANRManager::SANRData::isThreadRunning() { - if (dialogThread.native_handle() == 0) - return false; - if (dialogThreadExited) - return false; - return pthread_kill(dialogThread.native_handle(), 0) != ESRCH; +bool CANRManager::SANRData::isRunning() { + return dialogBox && dialogBox->isRunning(); } void CANRManager::SANRData::killDialog() { - if (!dialogProc) + if (!dialogBox) return; - if (!dialogProc->pid()) { - Debug::log(ERR, "ANR: cannot kill dialogProc, as it doesn't have a pid."); - dialogProc = nullptr; - return; - } - - kill(dialogProc->pid(), SIGKILL); + dialogBox->kill(); + dialogBox = nullptr; } bool CANRManager::SANRData::fitsWindow(PHLWINDOW pWindow) const { diff --git a/src/managers/ANRManager.hpp b/src/managers/ANRManager.hpp index 4d838f57..f5c0085b 100644 --- a/src/managers/ANRManager.hpp +++ b/src/managers/ANRManager.hpp @@ -7,8 +7,7 @@ #include #include "./eventLoop/EventLoopTimer.hpp" #include "../helpers/signal/Signal.hpp" -#include -#include +#include "../helpers/AsyncDialogBox.hpp" #include class CXDGWMBase; @@ -32,22 +31,21 @@ class CANRManager { SANRData(PHLWINDOW pWindow); ~SANRData(); - WP xwaylandSurface; - WP xdgBase; + WP xwaylandSurface; + WP xdgBase; - int missedResponses = 0; - std::thread dialogThread; - SP dialogProc; - std::atomic dialogThreadExited = false; - std::atomic dialogThreadSaidWait = false; + int missedResponses = 0; - void runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID); - bool isThreadRunning(); - void killDialog(); - bool isDefunct() const; - bool fitsWindow(PHLWINDOW pWindow) const; - pid_t getPid() const; - void ping(); + bool dialogSaidWait = false; + SP dialogBox; + + void runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID); + bool isRunning(); + void killDialog(); + bool isDefunct() const; + bool fitsWindow(PHLWINDOW pWindow) const; + pid_t getPid() const; + void ping(); }; void onResponse(SP data); diff --git a/src/managers/eventLoop/EventLoopManager.hpp b/src/managers/eventLoop/EventLoopManager.hpp index ebeb2160..f240dae9 100644 --- a/src/managers/eventLoop/EventLoopManager.hpp +++ b/src/managers/eventLoop/EventLoopManager.hpp @@ -68,6 +68,7 @@ class CEventLoopManager { wl_event_source* m_configWatcherInotifySource = nullptr; friend class CSyncTimeline; + friend class CAsyncDialogBox; }; inline UP g_pEventLoopManager;