Skip to content

Commit

Permalink
core: add an ANR dialog
Browse files Browse the repository at this point in the history
for xdg-shell, we can ping the wm_base, and thus render an ANR dialog if an app dies

for XWayland, there probably is a similar method, but I don't know about it and don't care.
  • Loading branch information
vaxerski committed Feb 18, 2025
1 parent 3352317 commit fb8eaba
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 6 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.4.5)
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.0)
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.5.1)
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.1)

string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})
Expand Down
5 changes: 5 additions & 0 deletions src/Compositor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "managers/SeatManager.hpp"
#include "managers/VersionKeeperManager.hpp"
#include "managers/DonationNagManager.hpp"
#include "managers/ANRManager.hpp"
#include "managers/eventLoop/EventLoopManager.hpp"
#include <algorithm>
#include <aquamarine/output/Output.hpp>
Expand Down Expand Up @@ -592,6 +593,7 @@ void CCompositor::cleanup() {
g_pEventLoopManager.reset();
g_pVersionKeeperMgr.reset();
g_pDonationNagManager.reset();
g_pANRManager.reset();
g_pConfigWatcher.reset();

if (m_pAqBackend)
Expand Down Expand Up @@ -694,6 +696,9 @@ void CCompositor::initManagers(eManagersInitStage stage) {
Debug::log(LOG, "Creating the DonationNag!");
g_pDonationNagManager = makeUnique<CDonationNagManager>();

Debug::log(LOG, "Creating the ANRManager!");
g_pANRManager = makeUnique<CANRManager>();

Debug::log(LOG, "Starting XWayland");
g_pXWayland = makeUnique<CXWayland>(g_pCompositor->m_bWantsXwayland);
} break;
Expand Down
1 change: 1 addition & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ CConfigManager::CConfigManager() {
m_pConfig->addConfigValue("misc:disable_xdg_env_checks", Hyprlang::INT{0});
m_pConfig->addConfigValue("misc:disable_hyprland_qtutils_check", Hyprlang::INT{0});
m_pConfig->addConfigValue("misc:lockdead_screen_delay", Hyprlang::INT{1000});
m_pConfig->addConfigValue("misc:enable_anr_dialog", Hyprlang::INT{1});

m_pConfig->addConfigValue("group:insert_after_current", Hyprlang::INT{1});
m_pConfig->addConfigValue("group:focus_removed_window", Hyprlang::INT{1});
Expand Down
10 changes: 10 additions & 0 deletions src/desktop/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "../config/ConfigValue.hpp"
#include "../managers/TokenManager.hpp"
#include "../managers/AnimationManager.hpp"
#include "../managers/ANRManager.hpp"
#include "../protocols/XDGShell.hpp"
#include "../protocols/core/Compositor.hpp"
#include "../protocols/ContentType.hpp"
Expand Down Expand Up @@ -48,6 +49,7 @@ PHLWINDOW CWindow::create(SP<CXWaylandSurface> surface) {
g_pAnimationManager->createAnimation(0.f, pWindow->m_fDimPercent, g_pConfigManager->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_fMovingToWorkspaceAlpha, g_pConfigManager->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_fMovingFromWorkspaceAlpha, g_pConfigManager->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, g_pConfigManager->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE);

pWindow->addWindowDeco(makeUnique<CHyprDropShadowDecoration>(pWindow));
pWindow->addWindowDeco(makeUnique<CHyprBorderDecoration>(pWindow));
Expand All @@ -71,6 +73,7 @@ PHLWINDOW CWindow::create(SP<CXDGSurfaceResource> resource) {
g_pAnimationManager->createAnimation(0.f, pWindow->m_fDimPercent, g_pConfigManager->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_fMovingToWorkspaceAlpha, g_pConfigManager->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_fMovingFromWorkspaceAlpha, g_pConfigManager->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, g_pConfigManager->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE);

pWindow->addWindowDeco(makeUnique<CHyprDropShadowDecoration>(pWindow));
pWindow->addWindowDeco(makeUnique<CHyprBorderDecoration>(pWindow));
Expand Down Expand Up @@ -1796,3 +1799,10 @@ void CWindow::deactivateGroupMembers() {
break;
}
}

bool CWindow::isNotResponding() {
if (!m_pXDGSurface)
return false;

return g_pANRManager->isNotResponding(m_pXDGSurface->owner.lock());
}
4 changes: 4 additions & 0 deletions src/desktop/Window.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ class CWindow {
// window tags
CTagKeeper m_tags;

// ANR
PHLANIMVAR<float> m_notRespondingTint;

// For the list lookup
bool operator==(const CWindow& rhs) const {
return m_pXDGSurface == rhs.m_pXDGSurface && m_pXWaylandSurface == rhs.m_pXWaylandSurface && m_vPosition == rhs.m_vPosition && m_vSize == rhs.m_vSize &&
Expand Down Expand Up @@ -480,6 +483,7 @@ class CWindow {
NContentType::eContentType getContentType();
void setContentType(NContentType::eContentType contentType);
void deactivateGroupMembers();
bool isNotResponding();

CBox getWindowMainSurfaceBox() const {
return {m_vRealPosition->value().x, m_vRealPosition->value().y, m_vRealSize->value().x, m_vRealSize->value().y};
Expand Down
173 changes: 173 additions & 0 deletions src/managers/ANRManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include "ANRManager.hpp"
#include "../helpers/fs/FsUtils.hpp"
#include "../debug/Log.hpp"
#include "../macros.hpp"
#include "HookSystemManager.hpp"
#include "../Compositor.hpp"
#include "../protocols/XDGShell.hpp"
#include "./eventLoop/EventLoopManager.hpp"
#include "../config/ConfigValue.hpp"

using namespace Hyprutils::OS;

static constexpr auto TIMER_TIMEOUT = std::chrono::milliseconds(1500);

CANRManager::CANRManager() {
if (!NFsUtils::executableExistsInPath("hyprland-dialog")) {
Debug::log(ERR, "hyprland-dialog missing from PATH, cannot start ANRManager");
return;
}

m_timer = makeShared<CEventLoopTimer>(TIMER_TIMEOUT, [this](SP<CEventLoopTimer> self, void* data) { onTick(); }, this);
g_pEventLoopManager->addTimer(m_timer);

m_active = true;

static auto P = g_pHookSystem->hookDynamic("openWindow", [this](void* self, SCallbackInfo& info, std::any data) {
auto window = std::any_cast<PHLWINDOW>(data);

if (window->m_bIsX11)
return;

for (const auto& w : g_pCompositor->m_vWindows) {
if (w->m_bIsX11 || w == window || !w->m_pXDGSurface)
continue;

if (w->m_pXDGSurface->owner == window->m_pXDGSurface->owner)
return;
}

m_data[window->m_pXDGSurface->owner] = makeShared<SANRData>();
});

m_timer->updateTimeout(TIMER_TIMEOUT);
}

void CANRManager::onTick() {
std::erase_if(m_data, [](const auto& e) { return e.first.expired(); });

static auto PENABLEANR = CConfigValue<Hyprlang::INT>("misc:enable_anr_dialog");

if (!*PENABLEANR) {
m_timer->updateTimeout(TIMER_TIMEOUT * 10);
return;
}

for (auto& [wmBase, data] : m_data) {
PHLWINDOW firstWindow;
int count = 0;
for (const auto& w : g_pCompositor->m_vWindows) {
if (!w->m_bIsMapped || w->m_bIsX11 || !w->m_pXDGSurface)
continue;

if (w->m_pXDGSurface->owner != wmBase)
continue;

count++;
if (!firstWindow)
firstWindow = w;
}

if (count == 0)
continue;

if (data->missedResponses > 0) {
if (!data->isThreadRunning() && !data->dialogThreadSaidWait) {
pid_t pid = 0;
wl_client_get_credentials(wmBase->client(), &pid, nullptr, nullptr);
data->runDialog("Application Not Responding", firstWindow->m_szTitle, firstWindow->m_szClass, pid);

for (const auto& w : g_pCompositor->m_vWindows) {
if (!w->m_bIsMapped || w->m_bIsX11 || !w->m_pXDGSurface)
continue;

if (w->m_pXDGSurface->owner != wmBase)
continue;

*w->m_notRespondingTint = 0.2F;
}
}
} else if (data->isThreadRunning())
data->killDialog();

if (data->missedResponses == 0)
data->dialogThreadSaidWait = false;

data->missedResponses++;

wmBase->ping();
}

m_timer->updateTimeout(TIMER_TIMEOUT);
}

void CANRManager::onResponse(SP<CXDGWMBase> wmBase) {
if (!m_data.contains(wmBase))
return;

auto& data = m_data.at(wmBase);
data->missedResponses = 0;
if (data->isThreadRunning())
data->killDialog();
}

void CANRManager::SANRData::runDialog(const std::string& title, const std::string& appName, const std::string appClass, pid_t dialogWmPID) {
if (!dialogThreadExited)
killDialog();

// dangerous: might lock if the above failed!!
if (dialogThread.joinable())
dialogThread.join();

dialogThreadExited = false;
dialogThreadSaidWait = false;
dialogThread = std::thread([title, appName, appClass, dialogWmPID, this]() {
SP<CProcess> proc =
makeShared<CProcess>("hyprland-dialog",
std::vector<std::string>{"--title", title, "--text",
std::format("Application {} with class of {} is not responding.\nWhat do you want to do with it?", appName, 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;
});
}

bool CANRManager::SANRData::isThreadRunning() {
if (dialogThread.native_handle() == 0)
return false;
if (dialogThreadExited)
return false;
return pthread_kill(dialogThread.native_handle(), 0) != ESRCH;
}

void CANRManager::SANRData::killDialog() const {
if (!dialogProc)
return;

kill(dialogProc->pid(), SIGKILL);
}

CANRManager::SANRData::~SANRData() {
if (dialogThread.joinable()) {
killDialog();
// dangerous: might lock if the above failed!!
dialogThread.join();
}
}

bool CANRManager::isNotResponding(SP<CXDGWMBase> wmBase) {
if (!m_data.contains(wmBase))
return false;
return m_data[wmBase]->missedResponses > 1;
}
46 changes: 46 additions & 0 deletions src/managers/ANRManager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include "../helpers/memory/Memory.hpp"
#include "../desktop/DesktopTypes.hpp"
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/os/FileDescriptor.hpp>
#include <map>
#include "./eventLoop/EventLoopTimer.hpp"
#include "../helpers/signal/Signal.hpp"
#include <atomic>
#include <thread>

class CXDGWMBase;

class CANRManager {
public:
CANRManager();

void onResponse(SP<CXDGWMBase> wmBase);
bool isNotResponding(SP<CXDGWMBase> wmBase);

private:
bool m_active = false;
SP<CEventLoopTimer> m_timer;

void onTick();

struct SANRData {
~SANRData();

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 isThreadRunning();
void killDialog() const;
};

std::map<WP<CXDGWMBase>, SP<SANRData>> m_data;
};

inline UP<CANRManager> g_pANRManager;
10 changes: 10 additions & 0 deletions src/protocols/XDGShell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include "../Compositor.hpp"
#include "../managers/SeatManager.hpp"
#include "../managers/ANRManager.hpp"
#include "../helpers/Monitor.hpp"
#include "core/Seat.hpp"
#include "core/Compositor.hpp"
Expand Down Expand Up @@ -740,6 +741,11 @@ CXDGWMBase::CXDGWMBase(SP<CXdgWmBase> resource_) : resource(resource_) {

LOGM(LOG, "New xdg_surface at {:x}", (uintptr_t)RESOURCE.get());
});

resource->setPong([this](CXdgWmBase* r, uint32_t serial) {
g_pANRManager->onResponse(self.lock());
events.pong.emit();
});
}

bool CXDGWMBase::good() {
Expand All @@ -750,6 +756,10 @@ wl_client* CXDGWMBase::client() {
return pClient;
}

void CXDGWMBase::ping() {
resource->sendPing(1337);
}

CXDGShellProtocol::CXDGShellProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
grab = makeShared<CSeatGrab>();
grab->keyboard = true;
Expand Down
5 changes: 5 additions & 0 deletions src/protocols/XDGShell.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,17 @@ class CXDGWMBase {

bool good();
wl_client* client();
void ping();

std::vector<WP<CXDGPositionerResource>> positioners;
std::vector<WP<CXDGSurfaceResource>> surfaces;

WP<CXDGWMBase> self;

struct {
CSignal pong;
} events;

private:
SP<CXdgWmBase> resource;
wl_client* pClient = nullptr;
Expand Down
16 changes: 11 additions & 5 deletions src/render/OpenGL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1396,12 +1396,18 @@ void CHyprOpenGLImpl::renderTextureInternalWithDamage(SP<CTexture> tex, const CB
glUniform1f(shader->roundingPower, roundingPower);

if (allowDim && m_RenderData.currentWindow) {
glUniform1i(shader->applyTint, 1);
const auto DIM = m_RenderData.currentWindow->m_fDimPercent->value();
glUniform3f(shader->tint, 1.f - DIM, 1.f - DIM, 1.f - DIM);
} else {
if (m_RenderData.currentWindow->m_notRespondingTint->value() > 0) {
const auto DIM = m_RenderData.currentWindow->m_notRespondingTint->value();
glUniform1i(shader->applyTint, 1);
glUniform3f(shader->tint, 1.f - DIM, 1.f - DIM, 1.f - DIM);
} else if (m_RenderData.currentWindow->m_fDimPercent->value() > 0) {
glUniform1i(shader->applyTint, 1);
const auto DIM = m_RenderData.currentWindow->m_fDimPercent->value();
glUniform3f(shader->tint, 1.f - DIM, 1.f - DIM, 1.f - DIM);
} else
glUniform1i(shader->applyTint, 0);
} else
glUniform1i(shader->applyTint, 0);
}
}

const float verts[] = {
Expand Down
Loading

4 comments on commit fb8eaba

@nnyyxxxx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we handle cases where we cannot grab the application name and class?

@nnyyxxxx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. we could put "unnamed" as a placeholder, if fetching fails

@vaxerski
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could

@nnyyxxxx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright ill add it to #9436 then

Please sign in to comment.