helpers: Add an async dialog box impl (#9919)

Adds an async dialog box, way safer than our previous local solution for ANR
This commit is contained in:
Vaxry 2025-04-06 17:31:58 +02:00 committed by GitHub
parent e96b8ce4cc
commit 3c128679ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 215 additions and 76 deletions

View File

@ -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(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.8.0)
pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=0.3.2) 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(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) pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.1)
string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION}) string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})

18
flake.lock generated
View File

@ -128,11 +128,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1743549251, "lastModified": 1743714874,
"narHash": "sha256-yf+AXt0RkAkCyF6iSnJt6EJAnNG/l6qv70CVzhRP6Bg=", "narHash": "sha256-yt8F7NhMFCFHUHy/lNjH/pjZyIDFNk52Q4tivQ31WFo=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprland-protocols", "repo": "hyprland-protocols",
"rev": "4ab17ccac08456cb5e00e8bd323de2efd30612be", "rev": "3a5c2bda1c1a4e55cc1330c782547695a93f05b2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -238,11 +238,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1742984269, "lastModified": 1743950287,
"narHash": "sha256-uz9FaCIbga/gQ5ZG1Hb4HVVjTWT1qjjCAFlCXiaefxg=", "narHash": "sha256-/6IAEWyb8gC/NKZElxiHChkouiUOrVYNq9YqG0Pzm4Y=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "7248194a2ce0106ae647b70d0526a96dc9d6ad60", "rev": "f2dc70e448b994cef627a157ee340135bd68fbc6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -276,11 +276,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1743583204, "lastModified": 1743827369,
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", "narHash": "sha256-rpqepOZ8Eo1zg+KJeWoq1HAOgoMCDloqv5r2EAa9TSA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", "rev": "42a1c966be226125b48c384171c44c651c236c22",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -35,7 +35,7 @@ aquamarine = dependency('aquamarine', version: '>=0.8.0')
hyprcursor = dependency('hyprcursor', version: '>=0.1.7') hyprcursor = dependency('hyprcursor', version: '>=0.1.7')
hyprgraphics = dependency('hyprgraphics', version: '>= 0.1.1') hyprgraphics = dependency('hyprgraphics', version: '>= 0.1.1')
hyprlang = dependency('hyprlang', version: '>= 0.3.2') 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('.') aquamarine_version_list = aquamarine.version().split('.')
add_project_arguments(['-DAQUAMARINE_VERSION="@0@"'.format(aquamarine.version())], language: 'cpp') 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') add_project_arguments(['-DAQUAMARINE_VERSION_MAJOR=@0@'.format(aquamarine_version_list.get(0))], language: 'cpp')

View File

@ -0,0 +1,122 @@
#include "AsyncDialogBox.hpp"
#include "./fs/FsUtils.hpp"
#include <csignal>
#include "../managers/eventLoop/EventLoopManager.hpp"
using namespace Hyprutils::OS;
SP<CAsyncDialogBox> CAsyncDialogBox::create(const std::string& title, const std::string& description, std::vector<std::string> buttons) {
if (!NFsUtils::executableExistsInPath("hyprland-dialog")) {
Debug::log(ERR, "CAsyncDialogBox: cannot create, no hyprland-dialog");
return nullptr;
}
auto dialog = SP<CAsyncDialogBox>(new CAsyncDialogBox(title, description, buttons));
dialog->m_selfWeakReference = dialog;
return dialog;
}
CAsyncDialogBox::CAsyncDialogBox(const std::string& title, const std::string& description, std::vector<std::string> 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<char, 1024> 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<void(std::string)> 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<std::string>{"--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;
}

View File

@ -0,0 +1,45 @@
#pragma once
#include "../macros.hpp"
#include "./memory/Memory.hpp"
#include <vector>
#include <functional>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/os/FileDescriptor.hpp>
struct wl_event_source;
class CAsyncDialogBox {
public:
static SP<CAsyncDialogBox> create(const std::string& title, const std::string& description, std::vector<std::string> buttons);
CAsyncDialogBox(const CAsyncDialogBox&) = delete;
CAsyncDialogBox(CAsyncDialogBox&&) = delete;
CAsyncDialogBox& operator=(const CAsyncDialogBox&) = delete;
CAsyncDialogBox& operator=(CAsyncDialogBox&&) = delete;
void open(std::function<void(std::string)> 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<std::string> buttons);
pid_t m_dialogPid = 0;
wl_event_source* m_readEventSource = nullptr;
std::function<void(std::string)> m_onResolution;
Hyprutils::OS::CFileDescriptor m_pipeReadFd;
std::string m_stdout = "";
const std::string m_title;
const std::string m_description;
const std::vector<std::string> m_buttons;
// WARNING: cyclic reference. This will be removed once the event source is removed to avoid dangling pointers
SP<CAsyncDialogBox> m_selfReference;
WP<CAsyncDialogBox> m_selfWeakReference;
};

View File

@ -39,8 +39,6 @@ CANRManager::CANRManager() {
} }
void CANRManager::onTick() { void CANRManager::onTick() {
std::erase_if(m_data, [](const auto& e) { return e->isDefunct(); });
static auto PENABLEANR = CConfigValue<Hyprlang::INT>("misc:enable_anr_dialog"); static auto PENABLEANR = CConfigValue<Hyprlang::INT>("misc:enable_anr_dialog");
static auto PANRTHRESHOLD = CConfigValue<Hyprlang::INT>("misc:anr_missed_pings"); static auto PANRTHRESHOLD = CConfigValue<Hyprlang::INT>("misc:anr_missed_pings");
@ -68,7 +66,7 @@ void CANRManager::onTick() {
continue; continue;
if (data->missedResponses >= *PANRTHRESHOLD) { 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()); data->runDialog("Application Not Responding", firstWindow->m_szTitle, firstWindow->m_szClass, data->getPid());
for (const auto& w : g_pCompositor->m_vWindows) { for (const auto& w : g_pCompositor->m_vWindows) {
@ -81,11 +79,11 @@ void CANRManager::onTick() {
*w->m_notRespondingTint = 0.2F; *w->m_notRespondingTint = 0.2F;
} }
} }
} else if (data->isThreadRunning()) } else if (data->isRunning())
data->killDialog(); data->killDialog();
if (data->missedResponses == 0) if (data->missedResponses == 0)
data->dialogThreadSaidWait = false; data->dialogSaidWait = false;
data->missedResponses++; data->missedResponses++;
@ -115,7 +113,7 @@ void CANRManager::onResponse(SP<CXWaylandSurface> pXwaylandSurface) {
void CANRManager::onResponse(SP<CANRManager::SANRData> data) { void CANRManager::onResponse(SP<CANRManager::SANRData> data) {
data->missedResponses = 0; data->missedResponses = 0;
if (data->isThreadRunning()) if (data->isRunning())
data->killDialog(); data->killDialog();
} }
@ -158,64 +156,39 @@ CANRManager::SANRData::SANRData(PHLWINDOW pWindow) :
} }
CANRManager::SANRData::~SANRData() { CANRManager::SANRData::~SANRData() {
if (dialogThread.joinable()) { if (dialogBox && dialogBox->isRunning())
killDialog(); 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) { 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(); killDialog();
// dangerous: might lock if the above failed!! dialogBox = CAsyncDialogBox::create(title,
if (dialogThread.joinable()) std::format("Application {} with class of {} is not responding.\nWhat do you want to do with it?", appName.empty() ? "unknown" : appName,
dialogThread.join(); appClass.empty() ? "unknown" : appClass),
std::vector<std::string>{"Terminate", "Wait"});
dialogThreadExited = false; dialogBox->open([dialogWmPID, this](std::string result) {
dialogThreadSaidWait = false; if (result.starts_with("Terminate"))
dialogThread = std::thread([title, appName, appClass, dialogWmPID, this]() { ::kill(dialogWmPID, SIGKILL);
SP<CProcess> proc = makeShared<CProcess>("hyprland-dialog", else if (result.starts_with("Wait"))
std::vector<std::string>{"--title", title, "--text", dialogSaidWait = true;
std::format("Application {} with class of {} is not responding.\nWhat do you want to do with it?", else
appName.empty() ? "unknown" : appName, appClass.empty() ? "unknown" : appClass), Debug::log(ERR, "CANRManager::SANRData::runDialog: lambda: unrecognized result: {}", result);
"--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;
}); });
} }
bool CANRManager::SANRData::isThreadRunning() { bool CANRManager::SANRData::isRunning() {
if (dialogThread.native_handle() == 0) return dialogBox && dialogBox->isRunning();
return false;
if (dialogThreadExited)
return false;
return pthread_kill(dialogThread.native_handle(), 0) != ESRCH;
} }
void CANRManager::SANRData::killDialog() { void CANRManager::SANRData::killDialog() {
if (!dialogProc) if (!dialogBox)
return; return;
if (!dialogProc->pid()) { dialogBox->kill();
Debug::log(ERR, "ANR: cannot kill dialogProc, as it doesn't have a pid."); dialogBox = nullptr;
dialogProc = nullptr;
return;
}
kill(dialogProc->pid(), SIGKILL);
} }
bool CANRManager::SANRData::fitsWindow(PHLWINDOW pWindow) const { bool CANRManager::SANRData::fitsWindow(PHLWINDOW pWindow) const {

View File

@ -7,8 +7,7 @@
#include <hyprutils/os/FileDescriptor.hpp> #include <hyprutils/os/FileDescriptor.hpp>
#include "./eventLoop/EventLoopTimer.hpp" #include "./eventLoop/EventLoopTimer.hpp"
#include "../helpers/signal/Signal.hpp" #include "../helpers/signal/Signal.hpp"
#include <atomic> #include "../helpers/AsyncDialogBox.hpp"
#include <thread>
#include <vector> #include <vector>
class CXDGWMBase; class CXDGWMBase;
@ -32,22 +31,21 @@ class CANRManager {
SANRData(PHLWINDOW pWindow); SANRData(PHLWINDOW pWindow);
~SANRData(); ~SANRData();
WP<CXWaylandSurface> xwaylandSurface; WP<CXWaylandSurface> xwaylandSurface;
WP<CXDGWMBase> xdgBase; WP<CXDGWMBase> xdgBase;
int missedResponses = 0; int missedResponses = 0;
std::thread dialogThread;
SP<Hyprutils::OS::CProcess> dialogProc;
std::atomic<bool> dialogThreadExited = false;
std::atomic<bool> dialogThreadSaidWait = false;
void runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID); bool dialogSaidWait = false;
bool isThreadRunning(); SP<CAsyncDialogBox> dialogBox;
void killDialog();
bool isDefunct() const; void runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID);
bool fitsWindow(PHLWINDOW pWindow) const; bool isRunning();
pid_t getPid() const; void killDialog();
void ping(); bool isDefunct() const;
bool fitsWindow(PHLWINDOW pWindow) const;
pid_t getPid() const;
void ping();
}; };
void onResponse(SP<SANRData> data); void onResponse(SP<SANRData> data);

View File

@ -68,6 +68,7 @@ class CEventLoopManager {
wl_event_source* m_configWatcherInotifySource = nullptr; wl_event_source* m_configWatcherInotifySource = nullptr;
friend class CSyncTimeline; friend class CSyncTimeline;
friend class CAsyncDialogBox;
}; };
inline UP<CEventLoopManager> g_pEventLoopManager; inline UP<CEventLoopManager> g_pEventLoopManager;