Skip to content

Commit

Permalink
wl: do not handle SIGCHLD when initializing xwayland
Browse files Browse the repository at this point in the history
the wayland backend via wlroots wants to double fork an X server for
XWayland, and expects to waitpid() on the middle fork to make sure things
were successful. our SIGCHLD handler races with this process and
interferes, causing things like:

    waitpid for Xwayland fork failed: No child processes

let's delay installing our SIGCHLD handler until after XWayland is
initialized to hopefully dodge this error.

Fixes qtile#5101

Signed-off-by: Tycho Andersen <[email protected]>
  • Loading branch information
tych0 committed Nov 27, 2024
1 parent 1dacffb commit 862febb
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 22 deletions.
14 changes: 12 additions & 2 deletions libqtile/backend/wayland/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import asyncio
import contextlib
import os
import signal
import time
from collections import defaultdict
from typing import TYPE_CHECKING, cast
Expand Down Expand Up @@ -101,7 +102,7 @@
from libqtile.command.base import expose_command
from libqtile.config import ScreenRect
from libqtile.log_utils import logger
from libqtile.utils import QtileError
from libqtile.utils import QtileError, reap_zombies

try:
# Continue if ffi not built, so that docs can be built without wayland deps.
Expand Down Expand Up @@ -371,12 +372,20 @@ def __init__(self) -> None:
self._on_xdg_activation_v1_request_activate,
)

# Set up XWayland
# Set up XWayland. wlroots wants to fork() and waitpid() for the
# xwayland server:
# https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/871646d22522141c45db2c0bfa1528d595bb69df
# so we need to delay installing our SIGCHLD handler so they can
# actually waitpid(). we install it in _on_xwayland_ready() or the
# exception handler, whichever is executed. This can be reverted if/when:
# https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4926
# is merged.
self._xwayland: xwayland.XWayland | None = None
try:
self._xwayland = xwayland.XWayland(self.display, self.compositor, True)
except RuntimeError:
logger.info("Failed to set up XWayland. Continuing without.")
asyncio.get_running_loop().add_signal_handler(signal.SIGCHLD, reap_zombies)
else:
os.environ["DISPLAY"] = self._xwayland.display_name or ""
logger.info("Set up XWayland with DISPLAY=%s", os.environ["DISPLAY"])
Expand Down Expand Up @@ -954,6 +963,7 @@ def _on_new_toplevel_decoration(

def _on_xwayland_ready(self, _listener: Listener, _data: Any) -> None:
logger.debug("Signal: xwayland ready")
asyncio.get_running_loop().add_signal_handler(signal.SIGCHLD, reap_zombies)
assert self._xwayland is not None
self._xwayland.set_seat(self.seat)
self.xwayland_atoms: dict[int, str] = wlrq.get_xwayland_atoms(self._xwayland)
Expand Down
32 changes: 12 additions & 20 deletions libqtile/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,19 @@ async def async_loop(self) -> None:
faulthandler.register(signal.SIGUSR2, all_threads=True)

try:
signals = {
signal.SIGTERM: self.stop,
signal.SIGINT: self.stop,
signal.SIGHUP: self.stop,
signal.SIGUSR1: self.reload_config,
}
if self.core.name == "x11":
# the wayland backend installs its own SIGCHLD handler after
# the XWayland X server has initialized (as a workaround). the
# x11 backend can just do it here.
signals[signal.SIGCHLD] = utils.reap_zombies
async with (
LoopContext(
{
signal.SIGTERM: self.stop,
signal.SIGINT: self.stop,
signal.SIGHUP: self.stop,
signal.SIGUSR1: self.reload_config,
signal.SIGCHLD: self.reap_zombies,
}
),
LoopContext(signals),
ipc.Server(
self._prepare_socket_path(self.socket_path),
self.server.call,
Expand All @@ -241,17 +244,6 @@ async def async_loop(self) -> None:
self.finalize()
self.core.remove_listener()

def reap_zombies(self) -> None:
try:
# One signal might mean mulitple children have exited. Reap everything
# that has exited, until there's nothing left.
while True:
wait_result = os.waitid(os.P_ALL, 0, os.WEXITED | os.WNOHANG)
if wait_result is None:
return
except ChildProcessError:
pass

def stop(self, exitcode: int = 0) -> None:
hook.fire("shutdown")
lifecycle.behavior = lifecycle.behavior.TERMINATE
Expand Down
15 changes: 15 additions & 0 deletions libqtile/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,18 @@ def remove_dbus_rules() -> None:
# We need to manually close the socket until https://github.com/altdesktop/python-dbus-next/pull/148
# gets merged. There's no error on multiple calls to 'close()'.
bus._sock.close()


def reap_zombies() -> None:
"""
A SIGCHLD handler that reaps all zombies until there are no more.
"""
try:
# One signal might mean mulitple children have exited. Reap everything
# that has exited, until there's nothing left.
while True:
wait_result = os.waitid(os.P_ALL, 0, os.WEXITED | os.WNOHANG)
if wait_result is None:
return
except ChildProcessError:
pass

0 comments on commit 862febb

Please sign in to comment.