// gKeyMapper - talk to gkeymapper-daemon over the session bus. #include "daemonclient.h" #include #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(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(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(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(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(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(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