202 lines
7.0 KiB
C++
202 lines
7.0 KiB
C++
// 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;
|
|
}
|
|
|
|
if (g_strcmp0(signalName, "ConfigReloaded") == 0) {
|
|
// The daemon keeps the previous mappings when the file cannot be read,
|
|
// so remember that and let the footer show it. It stays set until a
|
|
// reload succeeds.
|
|
gboolean ok = FALSE;
|
|
g_variant_get(parameters, "(b)", &ok);
|
|
self->configOk_ = ok != FALSE;
|
|
}
|
|
|
|
// DeviceChanged and the config state carry nothing GetStatus does not
|
|
// report, so a single status refresh covers both.
|
|
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.configOk = self->configOk_;
|
|
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
|