diff --git a/modules/home/wayland/apps/eww/bar/css/_net.scss b/modules/home/wayland/apps/eww/bar/css/_net.scss index 6126071..88020f6 100644 --- a/modules/home/wayland/apps/eww/bar/css/_net.scss +++ b/modules/home/wayland/apps/eww/bar/css/_net.scss @@ -34,3 +34,10 @@ .bt-device-btn:hover { @include background-active; } .net-toggle-on, .bt-btn-on { @include color-base; } .net-toggle-off, .bt-btn-off { @include color-inactive; } + +// WireGuard +.wg-accent { background-color: $base0E; } +.wg-row { margin-bottom: $popup-scale * 4pt; } +.wg-name { font-size: 0.78em; font-weight: bold; } +.wg-up { @include color-base; } +.wg-down { @include color-inactive; } diff --git a/modules/home/wayland/apps/eww/bar/modules/clock.yuck b/modules/home/wayland/apps/eww/bar/modules/clock.yuck index c5ff4f2..e8fff72 100644 --- a/modules/home/wayland/apps/eww/bar/modules/clock.yuck +++ b/modules/home/wayland/apps/eww/bar/modules/clock.yuck @@ -4,8 +4,8 @@ (defwidget clock-mod [] (module (eventbox - :onhover "${EWW_CMD} update date_rev=true" - :onhoverlost "${EWW_CMD} update date_rev=false" + :onhover "${EWW_CMD} --no-daemonize update date_rev=true" + :onhoverlost "${EWW_CMD} --no-daemonize update date_rev=false" :onclick "scripts/panel-toggle clock" :onrightclick "scripts/powermenu-toggle" (box diff --git a/modules/home/wayland/apps/eww/bar/modules/net.yuck b/modules/home/wayland/apps/eww/bar/modules/net.yuck index 2a9c7e3..57581b2 100644 --- a/modules/home/wayland/apps/eww/bar/modules/net.yuck +++ b/modules/home/wayland/apps/eww/bar/modules/net.yuck @@ -1,5 +1,5 @@ (deflisten net - :initial '{"wifi":{"connected":false,"enabled":false,"icon":"󰤮","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""}}' + :initial '{"wifi":{"connected":false,"enabled":false,"icon":"󰤮","ssid":"","ip":"","freq":0,"band":"","gen":"","signal":0},"ethernet":{"connected":false,"interface":"","ip":"","speed":""},"usb":{"connected":false,"interface":"","ip":""},"wireguard":[],"vpn":{"up":false,"name":""}}' "scripts/net/net") (deflisten bt @@ -20,6 +20,11 @@ :class "net-icon net-active" :tooltip "USB: Connected" :text "󰌷") + (label + :visible {net.vpn.up} + :class "net-icon net-active" + :tooltip "VPN: ${net.vpn.name}" + :text "󰖂") (label :class "net-icon ${net.wifi.connected ? 'net-active' : 'net-dim'}" :tooltip {net.wifi.connected ? "WiFi: ${net.wifi.ssid}" : "WiFi: Disconnected"} diff --git a/modules/home/wayland/apps/eww/bar/scripts/_lib.sh b/modules/home/wayland/apps/eww/bar/scripts/_lib.sh index 8ed1d86..4086653 100644 --- a/modules/home/wayland/apps/eww/bar/scripts/_lib.sh +++ b/modules/home/wayland/apps/eww/bar/scripts/_lib.sh @@ -3,6 +3,13 @@ # top of every wrapped script, right after setting PATH. # --------------------------------------------------------------------------- +# Every eww client call goes through this wrapper. A client waits only 100 ms +# for the daemon's reply; a slower command (opening the popup) would otherwise +# make a plain `eww open` fork a second daemon that takes over the socket — +# the "two bars" bug. With --no-daemonize it is just an error, and the daemon +# still carries out the command. +eww() { command eww --no-daemonize "$@"; } + # Print the payload only when it differs from the previous one. emit() { [[ $1 == "${EMIT_LAST-}" ]] && return 0 diff --git a/modules/home/wayland/apps/eww/bar/scripts/net/net b/modules/home/wayland/apps/eww/bar/scripts/net/net index e345953..a64596e 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/net/net +++ b/modules/home/wayland/apps/eww/bar/scripts/net/net @@ -70,8 +70,60 @@ state() { "$w_connected" "$w_enabled" "$w_icon" "$(jstr "$w_ssid")" "$w_ip" "${w_freq:-0}" "$(band "$w_freq")" "$w_gen" "${w_signal:-0}" printf '"ethernet":{"connected":%s,"interface":"%s","ip":"%s","speed":"%s"},' \ "$e_connected" "$eth" "$e_ip" "$e_speed" - printf '"usb":{"connected":%s,"interface":"%s","ip":"%s"}}' \ + printf '"usb":{"connected":%s,"interface":"%s","ip":"%s"},' \ "$u_connected" "$usb" "$u_ip" + wg_json + printf '}' +} + +# --- WireGuard: interfaces from the wireguard-.service units (so a +# tunnel that is down still shows up) plus any live wireguard-type link. +# `wg show` needs root, so peer names and the endpoint come from the peer +# units instead. + +wg_ifaces() { + { + systemctl list-units --all --plain --no-legend 'wireguard-*.service' 2>/dev/null \ + | awk '$1 !~ /-peer-/ { sub(/^wireguard-/, "", $1); sub(/\.service$/, "", $1); print $1 }' + ip -d -o link 2>/dev/null | awk '/ wireguard / { sub(":", "", $2); print $2 }' + } | sort -u +} + +human_bytes() { + awk -v b="${1:-0}" 'BEGIN { + split("B KiB MiB GiB TiB", u, " "); i = 1 + while (b >= 1024 && i < 5) { b /= 1024; i++ } + printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }' +} + +wg_json() { # prints the "wireguard" array and the "vpn" summary + local name up ip rx tx peers endpoint peer_unit peer_script sep="" out="[" vpn_up=false vpn_name="" + while read -r name; do + [[ -n $name ]] || continue + up=false ip="" rx=0 tx=0 endpoint="" + if [[ -d /sys/class/net/$name ]]; then + ip -o link show "$name" 2>/dev/null | grep -q '<[^>]*UP' && up=true + ip=$(ipv4 "$name") + rx=$(<"/sys/class/net/$name/statistics/rx_bytes") + tx=$(<"/sys/class/net/$name/statistics/tx_bytes") + fi + peers=$(systemctl list-units --all --plain --no-legend "wireguard-$name-peer-*.service" 2>/dev/null \ + | awk '{ print $1 }' | sed "s/^wireguard-$name-peer-//; s/\.service\$//" | paste -sd, -) + peer_unit=$(systemctl list-units --all --plain --no-legend "wireguard-$name-peer-*.service" 2>/dev/null \ + | awk 'NR == 1 { print $1 }') + # NixOS puts `wg set … endpoint "host:port"` in the peer unit's start script + if [[ -n $peer_unit ]]; then + peer_script=$(systemctl show -p ExecStart --value "$peer_unit" 2>/dev/null \ + | sed -n 's/.*path=\([^ ;]*\).*/\1/p' | awk 'NR == 1') + [[ -r $peer_script ]] && endpoint=$(grep -o 'endpoint "[^"]*"' "$peer_script" 2>/dev/null \ + | awk -F'"' 'NR == 1 { print $2 }') + fi + if [[ $up == true && $vpn_up == false ]]; then vpn_up=true vpn_name=$name; fi + [[ -z $vpn_name ]] && vpn_name=$name + out+="$sep{\"name\":\"$(jstr "$name")\",\"up\":$up,\"ip\":\"$ip\",\"rx\":\"$(human_bytes "$rx")\",\"tx\":\"$(human_bytes "$tx")\",\"peers\":\"$(jstr "$peers")\",\"endpoint\":\"$(jstr "$endpoint")\"}" + sep=, + done < <(wg_ifaces) + printf '"wireguard":%s],"vpn":{"up":%s,"name":"%s"}' "$out" "$vpn_up" "$(jstr "$vpn_name")" } refresh() { emit "$(state)"; } diff --git a/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle b/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle new file mode 100644 index 0000000..13c1a4e --- /dev/null +++ b/modules/home/wayland/apps/eww/bar/scripts/net/wg-toggle @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Bring the WireGuard tunnel $1 down (stop its service, which takes the peer +# units with it) or up (start its target, which pulls service and peers). +# Needs the polkit rule from modules/nixos/system/network/wireguard. +name=$1 +[[ -n $name ]] || exit 64 +if [[ -d /sys/class/net/$name ]]; then + systemctl stop "wireguard-$name.service" +else + systemctl start "wireguard-$name.target" +fi diff --git a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle index e72a773..f1d2819 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle @@ -4,6 +4,11 @@ # The popup hosts every panel behind revealers, so switching panels while it # is open is just a variable update (animated, no scripts restarted). It is # only closed and re-opened when it has to move to another monitor. +# +# `eww open popup` takes longer than the client's 100 ms reply deadline, so +# its exit status and error output are meaningless and ignored (the daemon +# carries the command out anyway; the lib's eww wrapper makes sure a slow +# reply can never spawn a second daemon). panel=$1 [[ -n $panel ]] || exit 64 @@ -12,7 +17,7 @@ panel=$1 exec 9>"${XDG_RUNTIME_DIR:-/tmp}/eww-panel-toggle.lock" flock -n 9 || exit 0 -ew() { timeout 5 eww "$@" 9>&-; } +ew() { eww "$@" 9>&-; } current=$(ew get active-panel 2>/dev/null | tr -d '"') popup_screen=$(ew get popup-screen 2>/dev/null | tr -d '"') @@ -21,15 +26,16 @@ if ew active-windows 2>/dev/null | grep -qx 'popup: popup'; then open=1; else op if ((open)) && [[ $current == "$panel" ]]; then ew update active-panel= - ew close popup + ew close popup 2>/dev/null elif ((open)) && [[ -n $screen && $popup_screen == "$screen" ]]; then ew update active-panel="$panel" else - ((open)) && ew close popup + ((open)) && ew close popup 2>/dev/null ew update active-panel="$panel" popup-screen="$screen" if [[ -n $screen ]]; then - ew open popup --screen "$screen" + ew open popup --screen "$screen" >/dev/null 2>&1 else - ew open popup + ew open popup >/dev/null 2>&1 fi fi +exit 0 diff --git a/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle b/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle index 0e08e30..1a6b9b0 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/powermenu-toggle @@ -1,11 +1,12 @@ #!/usr/bin/env bash -# Toggle the full-screen power menu on the focused monitor. -ew() { timeout 5 eww "$@"; } -ew close popup 2>/dev/null -ew update active-panel= +# Toggle the full-screen power menu on the focused monitor. The open's exit +# status is ignored: it exceeds the client's 100 ms reply deadline. +eww close popup 2>/dev/null +eww update active-panel= 2>/dev/null screen=$(eww-bar focused) if [[ -n $screen ]]; then - ew open powermenu --toggle --screen "$screen" + eww open powermenu --toggle --screen "$screen" >/dev/null 2>&1 else - ew open powermenu --toggle + eww open powermenu --toggle >/dev/null 2>&1 fi +exit 0 diff --git a/modules/home/wayland/apps/eww/bar/windows/net.yuck b/modules/home/wayland/apps/eww/bar/windows/net.yuck index e3e81bd..10492a5 100644 --- a/modules/home/wayland/apps/eww/bar/windows/net.yuck +++ b/modules/home/wayland/apps/eww/bar/windows/net.yuck @@ -83,6 +83,29 @@ :visible {!bt.powered} :text "Disabled"))) +; --- WireGuard --- + +(defwidget wg-row [wg] + (box :orientation "v" :space-evenly false :class "wg-row" + (box :orientation "h" :space-evenly false :valign "center" + (label :class "wg-name ${wg.up ? 'wg-up' : 'wg-down'}" :halign "start" :hexpand true :text {wg.name}) + (button + :class "net-toggle-btn ${wg.up ? 'net-toggle-on' : 'net-toggle-off'}" + :onclick "scripts/net/wg-toggle ${wg.name}" + :tooltip {wg.up ? "Disconnect" : "Connect"} + (label :text {wg.up ? "󰖂" : "󰦞"}))) + (box :orientation "v" :space-evenly false :visible {wg.up} + (netinfo-row :label "IP" :value {wg.ip}) + (netinfo-row :label "Peer" :value {wg.endpoint != "" ? "${wg.peers} · ${wg.endpoint}" : wg.peers}) + (netinfo-row :label "Traffic" :value "󰇚 ${wg.rx} 󰕒 ${wg.tx}")) + (label :class "netinfo-dim" :halign "start" :visible {!wg.up} :text "Down"))) + +(defwidget wireguard-net-section [] + (box :orientation "v" :space-evenly false :class "sys-section" + (section-header :title "WireGuard" :accent "wg-accent") + (for wg in {net.wireguard} + (wg-row :wg {wg})))) + ; --- Root --- (defwidget net-win [] @@ -94,4 +117,7 @@ (box :class "section-sep") (wifi-net-section) (box :class "section-sep") + (box :visible {arraylength(net.wireguard) > 0} :space-evenly false :orientation "v" + (wireguard-net-section) + (box :class "section-sep")) (bluetooth-net-section))) diff --git a/modules/home/wayland/apps/eww/bar/windows/powermenu.yuck b/modules/home/wayland/apps/eww/bar/windows/powermenu.yuck index 5e284a0..6f1902e 100644 --- a/modules/home/wayland/apps/eww/bar/windows/powermenu.yuck +++ b/modules/home/wayland/apps/eww/bar/windows/powermenu.yuck @@ -22,7 +22,7 @@ (pm-btn :icon "󰗼" :label "Sign out" :onclick "hyprctl eval \"hl.dispatch(hl.dsp.exit())\"") (pm-btn :icon "󰅖" :label "Cancel" - :onclick "eww close powermenu"))))))) + :onclick "${EWW_CMD} --no-daemonize close powermenu"))))))) (defwindow powermenu :monitor 0 diff --git a/modules/home/wayland/apps/eww/default.nix b/modules/home/wayland/apps/eww/default.nix index e305859..866debc 100755 --- a/modules/home/wayland/apps/eww/default.nix +++ b/modules/home/wayland/apps/eww/default.nix @@ -20,6 +20,7 @@ let "scripts/net/net" = mkScript "net" ./bar/scripts/net/net [ iproute2 wpa_supplicant ]; "scripts/net/bt" = mkScript "bt" ./bar/scripts/net/bt [ jq glib ]; "scripts/net/bt-toggle" = mkScript "bt-toggle" ./bar/scripts/net/bt-toggle [ bluez ]; + "scripts/net/wg-toggle" = mkScript "wg-toggle" ./bar/scripts/net/wg-toggle [ ]; "scripts/sys/gpu" = mkScript "gpu" ./bar/scripts/sys/gpu [ custom.amdgpu_top jq ]; "scripts/sys/memory" = mkScript "memory" ./bar/scripts/sys/memory [ ]; "scripts/sys/battery" = mkScript "battery" ./bar/scripts/sys/battery [ ]; diff --git a/modules/nixos/system/network/wireguard/default.nix b/modules/nixos/system/network/wireguard/default.nix index ee1cf8f..0ff2e1e 100644 --- a/modules/nixos/system/network/wireguard/default.nix +++ b/modules/nixos/system/network/wireguard/default.nix @@ -36,6 +36,20 @@ in }; }; + # Let the desktop (eww network panel, scripts/net/wg-toggle) bring the + # tunnel up and down. eww runs as a user service outside the login + # session, where polkit cannot ask for a password, so wheel users may + # start/stop wireguard-* units without one. + security.polkit.extraConfig = '' + polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && + subject.isInGroup("wheel") && + /^wireguard-/.test(action.lookup("unit") || "")) { + return polkit.Result.YES; + } + }); + ''; + systemd.services."wireguard-wg0-peer-vpn-helcel" = { after = [ "network-online.target" "nss-lookup.target" ]; bindsTo = [ "network-online.target" ]; diff --git a/packages/eww-bar/default.nix b/packages/eww-bar/default.nix index 36d0184..d2875d3 100644 --- a/packages/eww-bar/default.nix +++ b/packages/eww-bar/default.nix @@ -4,6 +4,7 @@ , gnugrep , gnused , hyprland +, iproute2 , jq , procps , systemd @@ -15,6 +16,6 @@ # profiles and the user (`eww-bar status`, `eww-bar reset`). writeShellApplication { name = "eww-bar"; - runtimeInputs = [ coreutils eww gnugrep gnused hyprland jq procps systemd util-linux ]; + runtimeInputs = [ coreutils eww gnugrep gnused hyprland iproute2 jq procps systemd util-linux ]; text = builtins.readFile ./eww-bar.sh; } diff --git a/packages/eww-bar/eww-bar.sh b/packages/eww-bar/eww-bar.sh index 440878b..2116819 100644 --- a/packages/eww-bar/eww-bar.sh +++ b/packages/eww-bar/eww-bar.sh @@ -1,9 +1,17 @@ # eww-bar — lifecycle manager for the eww daemon and the bar window. # -# The daemon is owned by the user unit eww.service. Every window open goes -# through `ensure` first: a plain `eww open` against a dead or stale socket -# silently forks a *second* daemon that takes over the socket path and orphans -# the first daemon's surfaces ("a bar/popup I can't close"). +# The daemon is owned by the user unit eww.service. Two eww facts shape this +# script: +# +# * A client waits only 100 ms for the daemon's reply. Any slower command +# (opening the popup, reload) fails on the client side, and a plain +# `eww open` then forks a SECOND daemon that takes over the socket path and +# orphans the first one's surfaces ("two bars", "a popup I can't close"). +# Every client call here passes --no-daemonize, which turns that into a +# harmless error; outcomes are verified with `active-windows`, never with +# the exit status. +# * `eww ping` is answered by the IPC thread; `eww active-windows` by the GTK +# main loop. Only the latter proves the UI is alive. # # SCREEN is a GDK monitor index (0), a connector name (DP-1) or # desc:. Descriptions @@ -23,7 +31,7 @@ usage: eww-bar COMMAND [ARGS] check health check run by eww-watchdog.timer clean ExecStartPre: kill stray daemons, wipe stale sockets focused print the name of the focused Hyprland monitor - status show unit state, open windows and the bar's screen + status show unit state, socket owner, open windows and screen USAGE } @@ -42,6 +50,25 @@ mkdir -p "$state_dir" log() { printf 'eww-bar: %s\n' "$*" >&2; } +# ---------------------------------------------------------------- client --- + +# fd 9 (the lock) is closed for every client call so nothing can inherit it. +ewwc() { timeout 5 eww --no-daemonize "$@" 9>&-; } + +ping_daemon() { ewwc ping >/dev/null 2>&1; } +list_windows() { ewwc active-windows 2>/dev/null || true; } +bar_open() { list_windows | grep -qx 'bar: bar'; } + +# wait_for_window NAME [SECONDS] +wait_for_window() { + local i n=$((${2:-5} * 5)) + for ((i = 0; i < n; i++)); do + list_windows | grep -qx "$1: $1" && return 0 + sleep 0.2 + done + return 1 +} + # ---------------------------------------------------------------- daemon --- # nixpkgs wraps eww (wrapGAppsHook), so the running process is `.eww-wrapped`; @@ -60,18 +87,33 @@ unit_running() { *) return 1 ;; esac } +main_pid() { systemctl --user show -p MainPID --value "$unit" 2>/dev/null || echo 0; } -# `eww ping` is answered by the IPC thread and only proves the socket is alive. -ping_daemon() { timeout 3 eww ping >/dev/null 2>&1 9>&-; } +# pids listening on an eww socket path. After a rival rebinds the path, ss +# lists BOTH sockets under the same name (the old one is merely unlinked), so +# every pid matters, not just the first. +socket_owners() { + ss -xlp 2>/dev/null | grep -F "$runtime_dir/eww-server_" \ + | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | sort -u | tr '\n' ' ' +} -# `active-windows` is answered from the GTK main loop, so it also detects a -# frozen UI: the reply is then empty instead of listing the windows. -# fd 9 is closed for every client call so a daemonizing client (which can -# only happen when the socket is dead) can never inherit our lock. -list_windows() { timeout 5 eww active-windows 2>/dev/null 9>&- || true; } -bar_open() { list_windows | grep -qx 'bar: bar'; } +# The socket file is gone while the daemon lives on: a rival that crashed at +# startup unlinked it, and no client can reach the daemon any more. +socket_missing() { ! ls "$runtime_dir"/eww-server_* >/dev/null 2>&1; } +daemon_age() { ps -o etimes= -p "$(main_pid)" 2>/dev/null | tr -d " " || echo 0; } -healthy() { unit_running && ping_daemon; } +# pids of daemons other than the service's main process listening on the socket +rival_pids() { + local main pid + main=$(main_pid) + for pid in $(socket_owners); do + [[ $pid != "$main" ]] && printf '%s ' "$pid" + done + return 0 +} +rival_daemon() { [[ -n $(rival_pids) ]]; } + +healthy() { unit_running && ping_daemon && ! rival_daemon; } wait_for_daemon() { local i @@ -98,10 +140,13 @@ ensure() { ( flock -x 9 healthy && exit 0 - # The unit is (re)starting — possibly running us from ExecStartPost — - # so give the daemon time to bind before declaring it dead. - if unit_running && wait_for_daemon; then exit 0; fi - log "daemon unreachable, restarting $unit" + if rival_daemon; then + log "a second eww daemon (pid $(rival_pids)) owns the socket" + elif unit_running && wait_for_daemon && ! rival_daemon; then + # The unit was (re)starting — possibly running us from ExecStartPost. + exit 0 + fi + log "daemon unhealthy, restarting $unit" systemctl --user stop "$unit" >/dev/null 2>&1 || true kill_stray_daemons start_unit @@ -142,9 +187,9 @@ current_screen() { cat "$current_file" 2>/dev/null || true; } close_popups() { local w for w in popup powermenu; do - timeout 3 eww close "$w" >/dev/null 2>&1 9>&- || true + list_windows | grep -qx "$w: $w" && { ewwc close "$w" >/dev/null 2>&1 || true; } done - timeout 3 eww update active-panel= >/dev/null 2>&1 9>&- || true + ewwc update active-panel= >/dev/null 2>&1 || true } # open_bar [--force] [SPEC] @@ -161,17 +206,21 @@ open_bar() { exit 0 fi # Popups may sit on an output that just vanished; `eww open` itself - # replaces an already-open bar. + # replaces an already-open bar. The exit status is meaningless (100 ms + # client deadline), so the result is verified instead. close_popups - if ! timeout 10 eww open bar --screen "$screen" >/dev/null 9>&-; then - if [[ $screen == "$default_screen" ]]; then - log "eww open failed, restarting $unit" + ewwc open bar --screen "$screen" >/dev/null 2>&1 || true + if ! wait_for_window bar 5; then + if [[ $screen != "$default_screen" ]]; then + log "cannot open the bar on '$screen', using screen $default_screen" + screen=$default_screen + ewwc open bar --screen "$screen" >/dev/null 2>&1 || true + fi + if ! wait_for_window bar 5; then + log "the bar does not come up, restarting $unit" restart_unit exit 1 fi - log "cannot open the bar on '$screen', using screen $default_screen" - screen=$default_screen - timeout 10 eww open bar --screen "$screen" >/dev/null 9>&- || { restart_unit; exit 1; } fi printf '%s\n' "$spec" >"$screen_file" printf '%s\n' "$screen" >"$current_file" @@ -181,16 +230,20 @@ open_bar() { close_bar() { close_popups - timeout 3 eww close bar >/dev/null 2>&1 9>&- || true + ewwc close bar >/dev/null 2>&1 || true rm -f "$current_file" } reload() { ensure || return 1 - if ! timeout 15 eww reload >/dev/null 9>&-; then - log "reload failed, restarting $unit" + # Slow: the client always hits its deadline, so ignore the status and + # verify that the daemon is still with us afterwards. + ewwc reload >/dev/null 2>&1 || true + sleep 1 + if ! wait_for_daemon; then + log "daemon lost during reload, restarting $unit" restart_unit - return + return 0 fi open_bar --force } @@ -234,18 +287,28 @@ check() { return 0 fi [[ $(unit_state) == active ]] || return 0 # still starting; ExecStartPost handles it + if rival_daemon; then + log "a second eww daemon (pid $(rival_pids)) owns the socket, restarting $unit" + restart_unit + return 0 + fi + if socket_missing && (($(daemon_age) > 15)); then + log "socket file is gone, restarting $unit" + restart_unit + return 0 + fi if ! ping_daemon; then strike "daemon does not answer" return 0 fi - if bar_open; then + if wait_for_window bar 3; then rm -f "$fail_file" return 0 fi # Either nothing is open or the main loop is frozen (empty reply). Try to # open the bar: a frozen daemon still won't list it afterwards. open_bar || true - if bar_open; then + if wait_for_window bar 3; then rm -f "$fail_file" log "bar was missing, reopened" else @@ -255,7 +318,8 @@ check() { status() { systemctl --user --no-pager status "$unit" 2>&1 | head -n 12 || true - printf '\nwindows:\n' + printf '\nsocket listeners: %s(service main pid %s)\n' "$(socket_owners)" "$(main_pid)" + printf 'windows:\n' list_windows | sed 's/^/ /' printf 'screen: %s (requested: %s)\n' "$(current_screen)" "$(saved_screen)" printf 'strikes: %s\n' "$(cat "$fail_file" 2>/dev/null || echo 0)"