From 94c1f95a4c7dcc055b62d6b20ebf94832d09cbeb Mon Sep 17 00:00:00 2001 From: Aleksander Cynarski Date: Wed, 7 Jul 2021 23:44:05 +0200 Subject: [PATCH] domizianie --- .config/mako/config | 9 + .config/sway/bin/assign-cgroups.py | 269 ++++++++++++++++++++++++ .config/sway/bin/session.sh | 26 +++ .config/sway/sway.d/07-hotkeys.conf | 5 + .config/sway/sway.d/10-autostart.conf | 6 +- .config/swayr/config.toml | 21 ++ .config/wal/templates/colors-waybar.css | 6 + .config/wal/templates/mako | 9 + .config/waybar/config | 1 + .config/waybar/style.css | 47 +++-- 10 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 .config/mako/config create mode 100755 .config/sway/bin/assign-cgroups.py create mode 100755 .config/sway/bin/session.sh create mode 100644 .config/swayr/config.toml create mode 100644 .config/wal/templates/colors-waybar.css create mode 100644 .config/wal/templates/mako diff --git a/.config/mako/config b/.config/mako/config new file mode 100644 index 0000000..0e7a44c --- /dev/null +++ b/.config/mako/config @@ -0,0 +1,9 @@ +# https://github.com/altercation/solarized#the-values +font=Terminus 10 +background-color=#0C130C +text-color=#dedbcb +border-color=#606855 +default-timeout=5000 + +[urgency=high] +text-color=#CB4B16 \ No newline at end of file diff --git a/.config/sway/bin/assign-cgroups.py b/.config/sway/bin/assign-cgroups.py new file mode 100755 index 0000000..c67faca --- /dev/null +++ b/.config/sway/bin/assign-cgroups.py @@ -0,0 +1,269 @@ +#!/usr/bin/python3 +""" +Automatically assign a dedicated systemd scope to the GUI applications +launched in the same cgroup as the compositor. This could be helpful for +implementing cgroup-based resource management and would be necessary when +`systemd-oomd` is in use. + +Limitations: The script is using i3ipc window:new event to detect application +launches and would fail to detect background apps or special surfaces. +Therefore it's recommended to supplement the script with use of systemd user +services for such background apps. + +Dependencies: dbus-next, i3ipc, psutil, tenacity, python-xlib +""" +import argparse +import asyncio +import logging +import socket +import struct +from typing import Optional + +from dbus_next import Variant +from dbus_next.aio import MessageBus +from dbus_next.errors import DBusError +from i3ipc import Event +from i3ipc.aio import Con, Connection +from psutil import Process +from tenacity import retry, retry_if_exception_type, stop_after_attempt +from Xlib import X +from Xlib.display import Display + +try: + # requires python-xlib >= 0.30 + from Xlib.ext import res as XRes +except ImportError: + XRes = None + + +LOG = logging.getLogger("assign-cgroups") +SD_BUS_NAME = "org.freedesktop.systemd1" +SD_OBJECT_PATH = "/org/freedesktop/systemd1" +SD_SLICE_FORMAT = "app-{app_id}.slice" +SD_UNIT_FORMAT = "app-{app_id}-{unique}.scope" +# Ids of known launcher applications that are not special surfaces. When the app is +# started using one of those, it should be moved to a new cgroup. +# Launcher should only be listed here if it creates cgroup of its own. +LAUNCHER_APPS = ["nwggrid"] + + +def get_cgroup(pid: int) -> Optional[str]: + """ + Get cgroup identifier for the process specified by pid. + Assumes cgroups v2 unified hierarchy. + """ + try: + with open(f"/proc/{pid}/cgroup", "r") as file: + cgroup = file.read() + return cgroup.strip().split(":")[-1] + except OSError: + LOG.exception("Error geting cgroup info") + return None + + +def get_pid_by_socket(sockpath: str) -> int: + """ + getsockopt (..., SO_PEERCRED, ...) returns the following structure + struct ucred + { + pid_t pid; /* s32: PID of sending process. */ + uid_t uid; /* u32: UID of sending process. */ + gid_t gid; /* u32: GID of sending process. */ + }; + See also: socket(7), unix(7) + """ + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(sockpath) + ucred = sock.getsockopt( + socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("iII") + ) + pid, _, _ = struct.unpack("iII", ucred) + return pid + + +def escape_app_id(app_id: str) -> str: + """Escape app_id for systemd APIs""" + return app_id.replace("-", "\\x2d") + + +class XlibHelper: + """Utility class for some X11-specific logic""" + + def __init__(self): + self.display = Display() + self.use_xres = self._try_init_xres() + + def _try_init_xres(self) -> bool: + if XRes is None or self.display.query_extension(XRes.extname) is None: + LOG.warning( + "X-Resource extension is not supported. " + "Process identification for X11 applications will be less reliable." + ) + return False + ver = self.display.res_query_version() + LOG.info( + "X-Resource version %d.%d", + ver.server_major, + ver.server_minor, + ) + return (ver.server_major, ver.server_minor) >= (1, 2) + + def get_net_wm_pid(self, wid: int) -> int: + """Get PID from _NET_WM_PID property of X11 window""" + window = self.display.create_resource_object("window", wid) + net_wm_pid = self.display.get_atom("_NET_WM_PID") + pid = window.get_full_property(net_wm_pid, X.AnyPropertyType) + + if pid is None: + raise Exception("Failed to get PID from _NET_WM_PID") + return int(pid.value.tolist()[0]) + + def get_xres_client_id(self, wid: int) -> int: + """Get PID from X server via X-Resource extension""" + res = self.display.res_query_client_ids( + [{"client": wid, "mask": XRes.LocalClientPIDMask}] + ) + for cid in res.ids: + if cid.spec.client > 0 and cid.spec.mask == XRes.LocalClientPIDMask: + for value in cid.value: + return value + raise Exception("Failed to get PID via X-Resource extension") + + def get_window_pid(self, wid: int) -> Optional[int]: + """Get PID of X11 window""" + if self.use_xres: + return self.get_xres_client_id(wid) + + return self.get_net_wm_pid(wid) + + +class CGroupHandler: + """Main logic: handle i3/sway IPC events and start systemd transient units.""" + + def __init__(self, bus: MessageBus, conn: Connection): + self._bus = bus + self._conn = conn + self._xhelper: Optional[XlibHelper] = None + try: + self._xhelper = XlibHelper() + # pylint: disable=broad-except + except Exception as exc: + LOG.warning("Failed to connect to X11 display: %s", exc) + + async def connect(self): + """asynchronous initialization code""" + # pylint: disable=attribute-defined-outside-init + introspection = await self._bus.introspect(SD_BUS_NAME, SD_OBJECT_PATH) + self._sd_proxy = self._bus.get_proxy_object( + SD_BUS_NAME, SD_OBJECT_PATH, introspection + ) + self._sd_manager = self._sd_proxy.get_interface(f"{SD_BUS_NAME}.Manager") + + self._compositor_pid = get_pid_by_socket(self._conn.socket_path) + self._compositor_cgroup = get_cgroup(self._compositor_pid) + assert self._compositor_cgroup is not None + LOG.info("compositor:%s %s", self._compositor_pid, self._compositor_cgroup) + + self._conn.on(Event.WINDOW_NEW, self._on_new_window) + return self + + def get_pid(self, con: Con) -> Optional[int]: + """Get PID from IPC response (sway), X-Resource or _NET_WM_PID (i3)""" + if isinstance(con.pid, int) and con.pid > 0: + return con.pid + + if con.window is not None and self._xhelper is not None: + return self._xhelper.get_window_pid(con.window) + + return None + + def cgroup_change_needed(self, cgroup: Optional[str]) -> bool: + """Check criteria for assigning current app into an isolated cgroup""" + if cgroup is None: + return False + for launcher in LAUNCHER_APPS: + if SD_SLICE_FORMAT.format(app_id=launcher) in cgroup: + return True + return cgroup == self._compositor_cgroup + + @retry( + reraise=True, + retry=retry_if_exception_type(DBusError), + stop=stop_after_attempt(3), + ) + async def assign_scope(self, app_id: str, proc: Process): + """ + Assign process (and all unassigned children) to the + app-{app_id}.slice/app{app_id}-{pid}.scope cgroup + """ + app_id = escape_app_id(app_id) + sd_slice = SD_SLICE_FORMAT.format(app_id=app_id) + sd_unit = SD_UNIT_FORMAT.format(app_id=app_id, unique=proc.pid) + # Collect child processes as systemd assigns a scope only to explicitly + # specified PIDs. + # There's a risk of race as the child processes may exit by the time dbus call + # reaches systemd, hence the @retry decorator is applied to the method. + pids = [proc.pid] + [ + x.pid + for x in proc.children(recursive=True) + if self.cgroup_change_needed(get_cgroup(x.pid)) + ] + + await self._sd_manager.call_start_transient_unit( + sd_unit, + "fail", + [["PIDs", Variant("au", pids)], ["Slice", Variant("s", sd_slice)]], + [], + ) + LOG.debug( + "window %s successfully assigned to cgroup %s/%s", app_id, sd_slice, sd_unit + ) + + async def _on_new_window(self, _: Connection, event: Event): + """window:new IPC event handler""" + con = event.container + app_id = con.app_id if con.app_id else con.window_class + try: + pid = self.get_pid(con) + if pid is None: + LOG.warning("Failed to get pid for %s", app_id) + return + proc = Process(pid) + cgroup = get_cgroup(proc.pid) + # some X11 apps don't set WM_CLASS. fallback to process name + if app_id is None: + app_id = proc.name() + LOG.debug("window %s(%s) cgroup %s", app_id, proc.pid, cgroup) + if self.cgroup_change_needed(cgroup): + await self.assign_scope(app_id, proc) + # pylint: disable=broad-except + except Exception: + LOG.exception("Failed to modify cgroup for %s", app_id) + + +async def main(): + """Async entrypoint""" + bus = await MessageBus().connect() + conn = await Connection(auto_reconnect=False).connect() + await CGroupHandler(bus, conn).connect() + try: + await conn.main() + except (ConnectionError, EOFError): + logging.exception("Connection to the Sway IPC was lost") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Assign CGroups to apps in compositors with i3 IPC protocol support" + ) + parser.add_argument( + "-l", + "--loglevel", + choices=["critical", "error", "warning", "info", "debug"], + default="info", + dest="loglevel", + help="set logging level", + ) + args = parser.parse_args() + logging.basicConfig(level=args.loglevel.upper()) + asyncio.run(main()) diff --git a/.config/sway/bin/session.sh b/.config/sway/bin/session.sh new file mode 100755 index 0000000..5784bca --- /dev/null +++ b/.config/sway/bin/session.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +export XDG_CURRENT_DESKTOP=sway +export XDG_SESSION_TYPE=wayland +export _JAVA_AWT_WM_NONREPARENTING=1 +VARIABLES="DISPLAY I3SOCK SWAYSOCK WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE _JAVA_AWT_WM_NONREPARENTING" +SESSION_TARGET="sway-session.target" + +if hash dbus-update-activation-environment 2>/dev/null; then + dbus-update-activation-environment --systemd $VARIABLES +fi + +systemctl --user import-environment $VARIABLES +systemctl --user start "$SESSION_TARGET" + +if [ "$1" != "--with-cleanup" ] || + [ -z "$SWAYSOCK" ] || + ! hash swaymsg 2>/dev/null +then + exit 0; +fi + +swaymsg -t subscribe '["shutdown"]' + +systemctl --user stop "$SESSION_TARGET" +systemctl --user unset-environment $VARIABLES diff --git a/.config/sway/sway.d/07-hotkeys.conf b/.config/sway/sway.d/07-hotkeys.conf index 1e40975..0b5a0d8 100644 --- a/.config/sway/sway.d/07-hotkeys.conf +++ b/.config/sway/sway.d/07-hotkeys.conf @@ -191,3 +191,8 @@ bindsym $mod+o exec --no-startup-id ~/.config/rofi/scripts/sound.sh bindsym $mod+y exec clipman pick -t rofi +bindsym $mod+u exec env RUST_BACKTRACE=1 swayr switch-window >> /tmp/swayr.log 2>&1 + +bindsym $mod+Tab exec env RUST_BACKTRACE=1 swayr switch-to-urgent-or-lru-window >> /tmp/swayr.log 2>&1 + +bindsym $mod+$alt+Space exec env RUST_BACKTRACE=1 swayr switch-workspace-or-window >> /tmp/swayr.log 2>&1 \ No newline at end of file diff --git a/.config/sway/sway.d/10-autostart.conf b/.config/sway/sway.d/10-autostart.conf index bf33bb4..f7faf81 100644 --- a/.config/sway/sway.d/10-autostart.conf +++ b/.config/sway/sway.d/10-autostart.conf @@ -11,7 +11,5 @@ exec_always { gsettings set org.gnome.desktop.wm.preferences button-layout '' } -exec "hash dbus-update-activation-environment 2>/dev/null; \ - dbus-update-activation-environment --all; \ - dbus-update-activation-environment --systemd; \ - systemctl --user start sway-session.target" +exec ~/.config/sway/bin/session.sh --with-cleanup +exec ~/.config/sway/bin/assign-cgroups.py \ No newline at end of file diff --git a/.config/swayr/config.toml b/.config/swayr/config.toml new file mode 100644 index 0000000..1f0ceec --- /dev/null +++ b/.config/swayr/config.toml @@ -0,0 +1,21 @@ +[menu] +executable = 'wofi' +args = [ + '--show=dmenu', + '--allow-markup', + '--allow-images', + '--insensitive', + '--cache-file=/dev/null', + '--parse-search', + '--prompt={prompt}', +] + +[format] +window_format = '{urgency_start}“{title}”{urgency_end} — {app_name} on workspace {workspace_name} ({id})' +workspace_format = 'Workspace {name} ({id})' +urgency_start = '' +urgency_end = '' +icon_dirs = [ + '/usr/share/icons/hicolor/48x48/apps', + '/usr/share/pixmaps', +] diff --git a/.config/wal/templates/colors-waybar.css b/.config/wal/templates/colors-waybar.css new file mode 100644 index 0000000..761f7f4 --- /dev/null +++ b/.config/wal/templates/colors-waybar.css @@ -0,0 +1,6 @@ +@define-color fore {cursor}; +@define-color back {background}; + +@define-color inactive {color1}; +@define-color dark {color2}; +@define-color active {color7}; diff --git a/.config/wal/templates/mako b/.config/wal/templates/mako new file mode 100644 index 0000000..2926f26 --- /dev/null +++ b/.config/wal/templates/mako @@ -0,0 +1,9 @@ +# https://github.com/altercation/solarized#the-values +font=Terminus 10 +background-color={background} +text-color={foreground} +border-color={color1} +default-timeout=5000 + +[urgency=high] +text-color=#CB4B16 \ No newline at end of file diff --git a/.config/waybar/config b/.config/waybar/config index 729b035..83d1de6 100644 --- a/.config/waybar/config +++ b/.config/waybar/config @@ -125,6 +125,7 @@ "modules-center": [ ], "modules-right": [ + "custom/left-arrow-dark", "tray" ], diff --git a/.config/waybar/style.css b/.config/waybar/style.css index 86c547f..1661768 100644 --- a/.config/waybar/style.css +++ b/.config/waybar/style.css @@ -1,21 +1,24 @@ +@import "../../.cache/wal/colors-waybar.css"; + * { font-size: 10px; font-family: Terminus; } window#waybar { - background: #292b2e; - color: #fdf6e3; + background: @dark; + color: @back; } #custom-right-arrow-dark, #custom-left-arrow-dark { - color: #1a1a1a; + color: @back; + background: @dark; } #custom-right-arrow-light, #custom-left-arrow-light { - color: #292b2e; - background: #1a1a1a; + color: @dark; + background: @back; } #workspaces, @@ -28,7 +31,17 @@ window#waybar { #battery, #disk, #tray { - background: #1a1a1a; + background: @back; + color: @dark; +} + +#workspaces { + padding: 0 10px 0 10px; +} + +#tray { + background: @back; + padding: 0 10px 0 10px; } #custom-temperature, @@ -36,41 +49,43 @@ window#waybar { #custom-pressure, #custom-pm_25, #custom-pm_10 { - background: #1a1a1a; + background: @back; + color: @dark; padding: 2px 5px; } #workspaces button { padding: 0 2px; - color: #fdf6e3; + color: @inactive; } + #workspaces button.focused { - color: #268bd2; + color: @invactive; } #workspaces button:hover { box-shadow: inherit; text-shadow: inherit; } #workspaces button:hover { - background: #1a1a1a; - border: #1a1a1a; + background: @background; + border: @foreground; padding: 0 3px; } #pulseaudio { - color: #268bd2; + color: @dark; } #memory { - color: #2aa198; + color: @dark; } #cpu { - color: #6c71c4; + color: @dark; } #battery { - color: #859900; + color: @dark; } #disk { - color: #b58900; + color: @dark; } #clock,