I'm having trouble getting dbus to connect:
try:
logging.debug("Attempting to connect to D-Bus.")
self.bus = SessionBus()
self.keepass_service = self.bus.get("org.keepassxc.KeePassXC.MainWindow", "/org/keepassxc/KeePassXC/MainWindow")
# self.keepass_service = self.bus.get("org.keepassxc.KeePassXC", "/org/keepassxc/KeePassXC/")
# self.keepass_service = self.bus.get("org.keepassxc.KeePassXC.MainWindow")
Dbus.Listnames shows:
$ dbus-send --print-reply --dest=org.freedesktop.DBus --type=method_call /org/freedesktop/DBus org.freedesktop.DBus.ListNames
method return time=1729375987.604568 sender=org.freedesktop.DBus -> destination=:1.826 serial=3 reply_serial=2
array [
string "org.freedesktop.DBus"
string ":1.469"
string "org.freedesktop.Notifications"
string "org.freedesktop.PowerManagement"
string ":1.7"
string "org.keepassxc.KeePassXC.MainWindow"
This version produces this error:
self.keepass_service = self.bus.get("org.keepassxc.KeePassXC.MainWindow", "/org/keepassxc/KeePassXC/MainWindow")
ERROR:root:Error message: g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.UnknownObject: No such object path '/org/keepassxc/KeePassXC/MainWindow' (41)
This version produces this error:
self.keepass_service = self.bus.get("org.keepassxc.KeePassXC", "/org/keepassxc/KeePassXC/")
(process:607644): GLib-GIO-CRITICAL **: 16:18:39.599: g_dbus_connection_call_sync_internal: assertion 'object_path != NULL && g_variant_is_object_path (object_path)' failed
ERROR:root:Failed to connect to KeePassXC D-Bus interface.
ERROR:root:Error message: 'no such object; you might need to pass object path as the 2nd argument for get()'
I've tried adding a time delay in case it was a race condition. I've tried with a keepassxc instance already running. I don't know where to go next?
Here's the code in full context:
from pydbus import SessionBus
import logging
import os
import subprocess
from gi.repository import GLib
import time
# Set up logging configuration
logging.basicConfig(level=logging.DEBUG) # Set logging level to debug
class KeePassXCManager:
def __init__(self, db_path, password=None, keyfile=None, appimage_path=None):
logging.debug("Initializing KeePassXCManager")
self.db_path = db_path
self.password = password
self.keyfile = keyfile
self.kp = None
self.keepass_command = []
# Set default path to the KeePassXC AppImage in ~/Applications
self.appimage_path = appimage_path or os.path.expanduser("~/Applications/KeePassXC.appimage")
logging.debug(f"AppImage path set to: {self.appimage_path}")
# Determine the KeePassXC launch command
self._set_keepassxc_command()
self._ensure_keepassxc_running()
# Set up the D-Bus connection to KeePassXC
self.bus = SessionBus()
self.keepass_service = None
self._connect_to_dbus()
# Open the database once the manager is initialized
if not self.open_database():
logging.error("Failed to open the database during initialization.")
def _set_keepassxc_command(self):
"""Sets the command to launch KeePassXC."""
try:
if self._is_keepassxc_installed():
logging.info("Using installed KeePassXC version.")
self.keepass_command = ["keepassxc"]
elif os.path.isfile(self.appimage_path) and os.access(self.appimage_path, os.X_OK):
logging.info(f"KeePassXC AppImage is executable at {self.appimage_path}")
self.keepass_command = [self.appimage_path]
else:
logging.error("KeePassXC is not installed or AppImage is not executable.")
raise RuntimeError("KeePassXC is not installed. Please install it or provide a valid AppImage.")
logging.debug(f"Final KeePassXC command set: {self.keepass_command}")
except Exception as e:
logging.error(f"Error setting KeePassXC command: {e}")
raise
def _is_keepassxc_installed(self):
"""Checks if KeePassXC is installed on the system."""
logging.debug("Checking if KeePassXC is installed via package manager")
try:
result = subprocess.run(["which", "keepassxc"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode == 0:
logging.info(f"KeePassXC found at {result.stdout.decode().strip()}")
return True
else:
logging.warning("KeePassXC is not installed via package manager.")
return False
except Exception as e:
logging.error(f"Error checking KeePassXC installation: {e}")
return False
def _ensure_keepassxc_running(self):
"""Checks if KeePassXC is running and starts it if not."""
logging.debug("Checking if KeePassXC is running")
try:
# Check if KeePassXC is running using pgrep
result = subprocess.run(["pgrep", "-x", "keepassxc"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
logging.info("KeePassXC is not running. Starting KeePassXC.")
# Start KeePassXC
subprocess.Popen(self.keepass_command)
# Optionally, wait for a short time to allow KeePassXC to start
GLib.idle_add(lambda: None) # Allows the GUI to initialize
else:
logging.info("KeePassXC is already running.")
except Exception as e:
logging.error(f"Error checking or starting KeePassXC: {e}")
def _construct_open_command(self):
"""Constructs the command to open the KeePassXC database."""
command = [self.keepass_command[0], self.db_path]
if self.password:
command.append("--pw-stdin")
logging.debug(f"Command includes password for opening database: {self.db_path}")
if self.keyfile:
command.append(f"--keyfile={self.keyfile}")
logging.debug(f"Command includes keyfile for opening database: {self.keyfile}")
logging.debug(f"Final command to open KeePassXC database: {command}")
return command if self.password or self.keyfile else None
def _clear_sensitive_data(self):
"""Clears sensitive data from memory."""
logging.debug("Clearing sensitive data from memory")
self.password = None
self.keyfile = None
self.db_path = None
def _connect_to_dbus(self):
"""Connects to the KeePassXC D-Bus interface."""
try:
logging.debug("Attempting to connect to D-Bus.")
self.bus = SessionBus()
# self.keepass_service = self.bus.get("org.keepassxc.KeePassXC.MainWindow", "/org/keepassxc/KeePassXC/MainWindow")
self.keepass_service = self.bus.get("org.keepassxc.KeePassXC", "/org/keepassxc/KeePassXC/")
# self.keepass_service = self.bus.get("org.keepassxc.KeePassXC.MainWindow")
# self.keepass_service = self.bus.get("org.KeePassXC.MainWindow", "/org/KeePassXC/MainWindow")
if self.keepass_service:
logging.info("Successfully connected to KeePassXC D-Bus interface.")
else:
logging.error("KeePassXC D-Bus interface is not available.")
except Exception as e:
logging.error("Failed to connect to KeePassXC D-Bus interface.")
logging.error(f"Error message: {e}")
services = self.bus.get_services()
logging.error(f"Available D-Bus services: {services}")
def open_database(self):
"""Opens the KeePassXC database using D-Bus."""
try:
if not self.keepass_service:
logging.error("KeePassXC D-Bus service is not available.")
return False
logging.info(f"Opening database: {self.db_path}")
# Prepare parameters for the D-Bus call
password = self.password or ""
keyfile = self.keyfile or ""
# Call the D-Bus method with parameters directly
response = self.keepass_service.openDatabase(self.db_path, password, keyfile)
if response:
logging.info("Database opened successfully via D-Bus.")
return True
else:
logging.error("Failed to open database via D-Bus.")
return False
except Exception as e:
logging.error(f"An error occurred while opening the database: {e}")
return False
def unlock_database(self):
"""Unlocks the KeePassXC database with the password via D-Bus."""
try:
if not self.keepass_service:
logging.error("KeePassXC D-Bus service is not available.")
return False
logging.info("Unlocking database with the provided password.")
response = self.keepass_service.unlockDatabase(self.password)
if response:
logging.info("Database unlocked successfully via D-Bus.")
return True
else:
logging.error("Failed to unlock database via D-Bus.")
return False
except Exception as e:
logging.error(f"An error occurred while unlocking the database: {e}")
return False
You are assuming that the object path always follows the naming of the service itself. That's not always the case – a service can export many different object paths, and does not strictly need to follow any naming style (i.e. there isn't an enforced rule that all object paths start with the service name, much less that there be one that exactly matches it; both are merely conventions).
KeePassXC is a Qt-based app, and many of those famously do not care to follow the usual D-Bus convention of using the service name as the "base" for object paths; instead, the old KDE3 DCOP (pre-D-Bus) style with all objects rooted directly at /
remains common among Qt programs.
Looking through busctl or D-Spy (or the older D-Feet), it seems that KeePassXC does not follow the D-Bus conventions of object naming, and the only object it exposes is at the path /keepassxc
.
$ busctl --user tree org.keepassxc.KeePassXC.MainWindow
└─ /keepassxc
$ gdbus introspect -e -d org.keepassxc.KeePassXC.MainWindow -o / -r -p
node / {
node /keepassxc {
};
};
So you need to call:
bus.get("org.keepassxc.KeePassXC.MainWindow", "/keepassxc")
Note that since an object can have methods under several interfaces, some D-Bus bindings automatically use introspection to resolve unambiguous method names to the correct interface, but e.g. dbus-python would require you to explicitly use dbus.Interface(obj…).Foo(…)
or obj.Foo(…, dbus_interface=…)
.
Likewise the interface names don't need to match the service name – although it seems that KeePassXC has used the same string for both, but the .MainWindow
suffix is pretty odd for a service name to have when all windows of an app belong to the same process (while being a perfectly normal name for an interface that holds main window-related methods).
Generally instead of dbus-send I'd suggest the slightly less verbose systemd or GLib2 tools (both of which support showing the introspection XML from the service):
$ busctl --user introspect org.keepassxc.KeePassXC.MainWindow /keepassxc
$ busctl --user call ...
$ gdbus introspect -e -d org.keepassxc.KeePassXC.MainWindow -o /keepassxc
$ gdbus call -e ...