Files
gKeyMapper/src/daemonclient.cpp

192 lines
6.6 KiB
C++
Raw Normal View History

2026-10-18 02:57:29 +02:00
// gKeyMapper - talk to gkeymapper-daemon over the session bus.
#include "daemonclient.h"
#include <cstdio>
#include "ipc_ids.h"
namespace gkeymapper {
namespace {
// The daemon answers within one loop iteration, but it may have to be started
// first (D-Bus activation), so allow for a slow first reply.
constexpr int kCallTimeoutMs = 5000;
constexpr const char* kDBusName = "org.freedesktop.DBus";
constexpr const char* kDBusPath = "/org/freedesktop/DBus";
} // namespace
DaemonClient::~DaemonClient() {
if (connection_ == nullptr) {
return;
}
if (signal_subscription_ != 0) {
g_dbus_connection_signal_unsubscribe(connection_, signal_subscription_);
}
if (name_subscription_ != 0) {
g_dbus_connection_signal_unsubscribe(connection_, name_subscription_);
}
g_object_unref(connection_);
}
void DaemonClient::init(StatusFn onStatus, KeyFn onKey) {
onStatus_ = std::move(onStatus);
onKey_ = std::move(onKey);
if (started_) {
return; // a second activation reuses the existing connection
}
started_ = true;
g_bus_get(G_BUS_TYPE_SESSION, nullptr, &DaemonClient::on_bus_ready, this);
}
void DaemonClient::on_bus_ready(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
auto* self = static_cast<DaemonClient*>(userData);
GError* error = nullptr;
GDBusConnection* connection = g_bus_get_finish(result, &error);
if (connection == nullptr) {
// No session bus: the GUI still edits and saves the file, it just
// cannot show the daemon state.
std::fprintf(stderr, "gkeymapper: no session bus: %s\n",
error != nullptr ? error->message : "unknown error");
g_clear_error(&error);
self->report_unreachable();
return;
}
self->connection_ = connection;
self->watch_signals();
self->fetch_status();
}
void DaemonClient::watch_signals() {
signal_subscription_ = g_dbus_connection_signal_subscribe(
connection_, ipc::kBusName, ipc::kInterface, nullptr, ipc::kObjectPath, nullptr,
G_DBUS_SIGNAL_FLAGS_NONE, &DaemonClient::on_daemon_signal, this, nullptr);
// Lets the footer follow the daemon: it flips to "not reachable" when it
// stops and back when it (or D-Bus activation) starts it again.
name_subscription_ = g_dbus_connection_signal_subscribe(
connection_, kDBusName, kDBusName, "NameOwnerChanged", kDBusPath, ipc::kBusName,
G_DBUS_SIGNAL_FLAGS_NONE, &DaemonClient::on_name_owner_changed, this, nullptr);
}
void DaemonClient::on_daemon_signal(GDBusConnection* /*connection*/, const gchar* /*sender*/,
const gchar* /*objectPath*/, const gchar* /*interfaceName*/,
const gchar* signalName, GVariant* parameters,
gpointer userData) {
auto* self = static_cast<DaemonClient*>(userData);
if (g_strcmp0(signalName, "KeyPressed") == 0) {
guint32 index = 0;
const gchar* action = nullptr;
g_variant_get(parameters, "(u&s)", &index, &action);
if (self->onKey_) {
self->onKey_(static_cast<int>(index), action != nullptr ? action : "");
}
return;
}
// ConfigReloaded and DeviceChanged carry no state the footer does not
// already get from GetStatus, so just refresh it.
self->fetch_status();
}
void DaemonClient::on_name_owner_changed(GDBusConnection* /*connection*/, const gchar* /*sender*/,
const gchar* /*objectPath*/,
const gchar* /*interfaceName*/,
const gchar* /*signalName*/, GVariant* parameters,
gpointer userData) {
auto* self = static_cast<DaemonClient*>(userData);
const gchar* name = nullptr;
const gchar* oldOwner = nullptr;
const gchar* newOwner = nullptr;
g_variant_get(parameters, "(&s&s&s)", &name, &oldOwner, &newOwner);
if (newOwner == nullptr || *newOwner == '\0') {
self->report_unreachable();
} else {
self->fetch_status();
}
}
void DaemonClient::fetch_status() {
if (connection_ == nullptr) {
return;
}
// A call to a name without an owner makes the bus activate the service, so
// this also brings the daemon up on demand.
g_dbus_connection_call(connection_, ipc::kBusName, ipc::kObjectPath, ipc::kInterface,
"GetStatus", nullptr, G_VARIANT_TYPE("(sbss)"),
G_DBUS_CALL_FLAGS_NONE, kCallTimeoutMs, nullptr,
&DaemonClient::on_status_reply, this);
}
void DaemonClient::on_status_reply(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
auto* self = static_cast<DaemonClient*>(userData);
GError* error = nullptr;
GVariant* reply = g_dbus_connection_call_finish(self->connection_, result, &error);
if (reply == nullptr) {
// Normal when the daemon is not installed or not running.
g_clear_error(&error);
self->report_unreachable();
return;
}
const gchar* version = nullptr;
const gchar* device = nullptr;
const gchar* configPath = nullptr;
gboolean running = FALSE;
g_variant_get(reply, "(&sb&s&s)", &version, &running, &device, &configPath);
g_variant_unref(reply);
DaemonStatus status;
status.reachable = true;
status.version = version != nullptr ? version : "";
status.device = device != nullptr ? device : "";
status.configPath = configPath != nullptr ? configPath : "";
self->publish(status);
}
void DaemonClient::reload() {
if (connection_ == nullptr) {
return;
}
g_dbus_connection_call(connection_, ipc::kBusName, ipc::kObjectPath, ipc::kInterface,
"Reload", nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, kCallTimeoutMs,
nullptr, &DaemonClient::on_reload_reply, this);
}
void DaemonClient::on_reload_reply(GObject* /*source*/, GAsyncResult* result, gpointer userData) {
auto* self = static_cast<DaemonClient*>(userData);
GError* error = nullptr;
GVariant* reply = g_dbus_connection_call_finish(self->connection_, result, &error);
if (reply != nullptr) {
g_variant_unref(reply);
}
if (error != nullptr) {
// The file watch applies the save anyway, so a missing daemon is fine.
g_clear_error(&error);
}
}
void DaemonClient::report_unreachable() {
DaemonStatus status;
status.reachable = false;
publish(status);
}
void DaemonClient::publish(const DaemonStatus& status) {
if (onStatus_) {
onStatus_(status);
}
}
} // namespace gkeymapper