fix: resolve wifi crashes, shell injections, race conditions, and add debug mode

- Remove transient NEED_AUTH/DISCONNECTED error paths from wifi connect
- Guard double-callback in connect_wifi_bssid/connect_wifi_with_password
- Shell-quote user paths in wallpaper, screenshot, screenrecorder services
- Fix OSD hide-timer use-after-free and applet_window stale source ID
- Fix brightness poll timer leak on read failure
- Fix spotlight search stale idle source removal
- Lock dbusmenu _cache mutation under _registry_lock
- Fix entry.revision written outside _registry_lock scope
- Add fabric-cli exec wrapper for lock screen (lock_screen.lock())
- Replace hyprlock with lock screen in power menu and dropdown
- Add debug mode toggle in settings (controls log verbosity)
- Add diagnostic logging to globalmenu click chain
This commit is contained in:
sankalp committed 2026-07-16 11:47:53 +05:45
1 parent 4378959131
commit a47abda55a
26 files changed
+224 -57

No files matched your search

+2 -1
View File
@@ -93,7 +93,8 @@ class Brightness(Service):
return True
except Exception as e:
logger.error(f"Error checking brightness file: {e}")
return True
self._poll_timer_id = None
return False
def _detect_backend(self, backend):
"""Detect appropriate backend for brightness control."""
+9
View File
@@ -59,6 +59,15 @@ class ConfigService:
"""Set a config value (local state only)."""
self._config[key] = value
def save(self) -> None:
"""Persist current in-memory config to disk."""
try:
doc = self._dict_to_toml(self._config)
with open(self._config_file, "w") as f:
toml_dump(doc, f)
except Exception as e:
logger.error(f"[ConfigService] Failed to save config: {e}")
def register_reload_callback(
self, callback: Callable[[Dict[str, Any], Dict[str, Any]], None]
) -> None:
+4 -2
View File
@@ -327,12 +327,14 @@ class PlayerService(Service):
if metadata is None:
try:
metadata = self._player.props.metadata
except Exception:
except Exception as e:
logger.debug(f"[mpris] Failed to read metadata: {e}")
metadata = None
if metadata is not None:
try:
dur = metadata["mpris:length"] / 1_000_000
except Exception:
except Exception as e:
logger.debug(f"[mpris] Failed to read duration: {e}")
dur = 0
# Browser players (YouTube in Firefox/Chromium) populate
# `mpris:artUrl` lazily and don't reliably emit the `metadata`
+32 -13
View File
@@ -1,3 +1,4 @@
import shlex
from types import SimpleNamespace
from typing import List, Literal
@@ -105,7 +106,8 @@ class Wifi(Service):
try:
self._device.request_scan_async(None, _on_scan_finish)
except Exception:
except Exception as e:
logger.warning(f"[network] request_scan_async failed: {e}")
self._scanning = False
def notifier(self, name: str, *args):
@@ -401,26 +403,33 @@ class NetworkClient(Service):
callback(False, "No wifi device")
return
if not bssid:
if callback:
callback(False, "No bssid")
return
device = self.wifi_device._device
handler_id: list[int] = []
_fired = [False]
def _on_state_changed(_dev, new_state, _old_state, reason):
if not callback:
if _fired[0]:
return
state = NM.DeviceState(new_state)
if state == NM.DeviceState.ACTIVATED:
_fired[0] = True
_cleanup()
callback(True, "Connected")
elif state in (NM.DeviceState.FAILED, NM.DeviceState.DISCONNECTED):
if callback:
callback(True, "Connected")
elif state == NM.DeviceState.FAILED:
_fired[0] = True
_cleanup()
reason_str = NM.DeviceStateReason(reason).value_nick
if "secret" in reason_str or "auth" in reason_str:
callback(False, "Wrong password")
else:
callback(False, f"Failed ({reason_str})")
elif state == NM.DeviceState.NEED_AUTH:
_cleanup()
callback(False, "Wrong password")
if callback:
if "secret" in reason_str or "auth" in reason_str:
callback(False, "Wrong password")
else:
callback(False, f"Failed ({reason_str})")
def _cleanup():
if handler_id:
@@ -433,7 +442,7 @@ class NetworkClient(Service):
handler_id.append(device.connect("state-changed", _on_state_changed))
exec_shell_command_async(
f"nmcli device wifi connect {bssid}",
f"nmcli device wifi connect {shlex.quote(bssid)}",
lambda *args: logger.debug(f"connect result: {args}"),
)
@@ -442,15 +451,24 @@ class NetworkClient(Service):
callback(False, "No wifi device")
return
if not bssid:
callback(False, "No bssid")
return
device = self.wifi_device._device
handler_id: list[int] = []
_fired = [False]
def _on_state_changed(_dev, new_state, _old_state, reason):
if _fired[0]:
return
state = NM.DeviceState(new_state)
if state == NM.DeviceState.ACTIVATED:
_fired[0] = True
_cleanup()
callback(True, "Connected")
elif state in (NM.DeviceState.FAILED, NM.DeviceState.DISCONNECTED):
_fired[0] = True
_cleanup()
reason_str = NM.DeviceStateReason(reason).value_nick
if "secret" in reason_str or "auth" in reason_str:
@@ -458,6 +476,7 @@ class NetworkClient(Service):
else:
callback(False, f"Failed ({reason_str})")
elif state == NM.DeviceState.NEED_AUTH:
_fired[0] = True
_cleanup()
callback(False, "Wrong password")
@@ -471,6 +490,6 @@ class NetworkClient(Service):
handler_id.append(device.connect("state-changed", _on_state_changed))
exec_shell_command_async(
f"nmcli device wifi connect {bssid} password {password!r}",
f"nmcli device wifi connect {shlex.quote(bssid)} password {shlex.quote(password)}",
lambda *args: logger.debug(f"connect_with_password result: {args}"),
)
+5 -2
View File
@@ -5,6 +5,7 @@ Recording state is held entirely in memory as Fabric properties -- no
temporary files are used.
"""
import shlex
import time
from datetime import datetime
from pathlib import Path
@@ -261,9 +262,11 @@ class ScreenRecorder(Service):
def _handle_action(self, action: str, path: str) -> None:
match action:
case "files":
exec_shell_command_async(f"xdg-open {self.recordings_dir}")
exec_shell_command_async(
f"xdg-open {shlex.quote(str(self.recordings_dir))}"
)
case "view":
exec_shell_command_async(f"xdg-open {path}")
exec_shell_command_async(f"xdg-open {shlex.quote(path)}")
def _notify_cancelled(self) -> None:
self._notifier.notify(
+6 -3
View File
@@ -6,6 +6,7 @@ into a concrete request, hands it to a :class:`CaptureBackend`, then raises
notifications and signals based on the result.
"""
import shlex
from datetime import datetime
from pathlib import Path
@@ -157,11 +158,13 @@ class Screenshot(Service):
def _handle_action(self, action: str, path: str) -> None:
match action:
case "files":
exec_shell_command_async(f"xdg-open {self.screenshots_dir}")
exec_shell_command_async(
f"xdg-open {shlex.quote(str(self.screenshots_dir))}"
)
case "view":
exec_shell_command_async(f"xdg-open {path}")
exec_shell_command_async(f"xdg-open {shlex.quote(path)}")
case "edit":
exec_shell_command_async(f"satty -f {path}")
exec_shell_command_async(f"satty -f {shlex.quote(path)}")
def _notify_cancelled(self) -> None:
self._notifier.notify(
+20 -8
View File
@@ -1,18 +1,18 @@
import hashlib
import mimetypes
import shlex
from fabric.core import Service, Signal
from fabric.utils import GdkPixbuf as gdk_pixbuf
from fabric.utils import exec_shell_command_async, logger, os
from services.config import on_config_change
from shared.data import (
WALLPAPER_PATH,
WALLPAPER_THUMBS_PATH,
WALLPAPERS_THUMBNAILS_SIZE,
_get_wallpaper_path,
)
os.makedirs(WALLPAPER_PATH, exist_ok=True)
os.makedirs(WALLPAPER_THUMBS_PATH, exist_ok=True)
os.makedirs(_get_wallpaper_path(), exist_ok=True)
def get_thumbnail_filename(image_path: str) -> str:
@@ -64,14 +64,23 @@ class WallpaperService(Service):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._file_cache: list[str] | None = None
on_config_change(self._on_config_change)
def _on_config_change(self, new_config, old_config):
if new_config.get("wallpapers_dir") != old_config.get("wallpapers_dir"):
self._file_cache = None
path = _get_wallpaper_path()
os.makedirs(path, exist_ok=True)
os.makedirs(f"{path}/.thumbnails", exist_ok=True)
def get_all_wallpapers(self, use_cache: bool = True) -> list[str]:
if use_cache and self._file_cache is not None:
return self._file_cache
wallpapers: list[str] = []
wallpaper_path = _get_wallpaper_path()
with os.scandir(WALLPAPER_PATH) as entries:
with os.scandir(wallpaper_path) as entries:
for entry in entries:
if not entry.is_file():
continue
@@ -87,8 +96,9 @@ class WallpaperService(Service):
self._file_cache = None
def get_thumbnail_path(self, image_path: str) -> str:
thumbs_dir = f"{_get_wallpaper_path()}/.thumbnails"
return os.path.join(
WALLPAPER_THUMBS_PATH,
thumbs_dir,
get_thumbnail_filename(image_path),
)
@@ -97,7 +107,7 @@ class WallpaperService(Service):
def set_wallpaper(self, image_path: str) -> None:
exec_shell_command_async(
f'awww img "{image_path}" '
f"awww img {shlex.quote(image_path)} "
"--transition-type none "
"--transition-duration 0 "
"--transition-fps 60"
@@ -112,7 +122,9 @@ class WallpaperService(Service):
os.symlink(os.path.abspath(image_path), link_path)
exec_shell_command_async(f'matugen image "{image_path}" --source-color-index 0')
exec_shell_command_async(
f"matugen image {shlex.quote(image_path)} --source-color-index 0"
)
self.colors_generated(image_path)