/* * Copyright © 2008, 2010, 2011, 2022 Christian Persch * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #define G_SETTINGS_ENABLE_BACKEND #include #include "terminal-debug.hh" #include "terminal-libgsystem.hh" #include "terminal-settings-bridge-backend.hh" #include "terminal-settings-utils.hh" #include "terminal-settings-bridge-generated.h" #include #include struct _TerminalSettingsBridgeBackend { GSettingsBackend parent_instance; TerminalSettingsBridge* bridge; GCancellable* cancellable; GHashTable* cache; }; struct _TerminalSettingsBridgeBackendClass { GSettingsBackendClass parent_class; }; enum { PROP_SETTINGS_BRIDGE = 1, }; #define PRIORITY (10000) // _g_io_modules_ensure_extension_points_registered() is not public, // so just ensure a type that does this call on registration, which // all of the glib-internal settings backends do. However as an added // complication, none of their get_type() functions are public. So // instead we need to create an object using the public function and // immediately delete it again. // However, this *still* does not work; the // g_null_settings_backend_new() call prints the warning about a non- // registered extension point even though its get_type() function calls // _g_io_modules_ensure_extension_points_registered() immediately before. // Therefore we can only use this backend when creating a GSettings // ourself, by explicitly passing it at that time. G_DEFINE_TYPE_WITH_CODE(TerminalSettingsBridgeBackend, terminal_settings_bridge_backend, G_TYPE_SETTINGS_BACKEND, // _g_io_modules_ensure_extension_points_registered(); // { gs_unref_object auto dummy = g_null_settings_backend_new(); } // // g_io_extension_point_implement(G_SETTINGS_BACKEND_EXTENSION_POINT_NAME, // g_define_type_id, "bridge", PRIORITY) ); // Note that since D-Bus doesn't support maybe values, we use arrays // with either zero or one item to send/receive a maybe. // If we get more than one item, just use the first one. /* helper functions */ template inline constexpr auto IMPL(T* that) noexcept { return reinterpret_cast(that); } typedef struct { GVariant* value; bool value_set; bool writable; bool writable_set; } CacheEntry; static auto cache_entry_new(void) { return g_new0(CacheEntry, 1); } static void cache_entry_free(CacheEntry* e) noexcept { if (e->value) g_variant_unref(e->value); g_free(e); } static auto cache_lookup_entry(TerminalSettingsBridgeBackend* impl, char const* key) noexcept { return reinterpret_cast(g_hash_table_lookup(impl->cache, key)); } static auto cache_ensure_entry(TerminalSettingsBridgeBackend* impl, char const* key) noexcept { g_hash_table_insert(impl->cache, g_strdup(key), cache_entry_new()); return cache_lookup_entry(impl, key); } static void cache_insert_value(TerminalSettingsBridgeBackend* impl, char const* key, GVariant* value) noexcept { auto const ce = cache_ensure_entry(impl, key); g_clear_pointer(&ce->value, g_variant_unref); ce->value = value ? g_variant_ref(value) : nullptr; ce->value_set = true; } static void cache_insert_writable(TerminalSettingsBridgeBackend* impl, char const* key, bool writable) noexcept { auto const ce = cache_ensure_entry(impl, key); ce->writable = writable; ce->writable_set = true; } static void cache_remove_path(TerminalSettingsBridgeBackend* impl, char const* path) noexcept { auto iter = GHashTableIter{}; g_hash_table_iter_init(&iter, impl->cache); void* keyp = nullptr; void* valuep = nullptr; while (g_hash_table_iter_next(&iter, &keyp, &valuep)) { auto const key = reinterpret_cast(keyp); if (g_str_has_prefix(key, path)) { auto ce = reinterpret_cast(valuep); g_clear_pointer(&ce->value, g_variant_unref); ce->value_set = false; } } } static void cache_remove_value(TerminalSettingsBridgeBackend* impl, char const* key) noexcept { auto const ce = cache_ensure_entry(impl, key); g_clear_pointer(&ce->value, g_variant_unref); ce->value_set = false; } static void cache_remove_writable(TerminalSettingsBridgeBackend* impl, char const* key) noexcept { auto const ce = cache_ensure_entry(impl, key); ce->writable_set = false; } /* GSettingsBackend class implementation */ static GPermission* terminal_settings_bridge_backend_get_permission(GSettingsBackend* backend, char const* path) noexcept { _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::get_permission\n"); return g_simple_permission_new(true); } static gboolean terminal_settings_bridge_backend_get_writable(GSettingsBackend* backend, char const* key) noexcept { auto const impl = IMPL(backend); auto const ce = cache_lookup_entry(impl, key); if (ce && ce->writable_set) return ce->writable; auto writable = gboolean{false}; auto const r = terminal_settings_bridge_call_get_writable_sync(impl->bridge, key, &writable, impl->cancellable, nullptr); if (r) cache_insert_writable(impl, key, writable); else cache_remove_writable(impl, key); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::get_writable key %s success %d writable %d\n", key, r, writable); return writable; } static GVariant* terminal_settings_bridge_backend_read(GSettingsBackend* backend, char const* key, GVariantType const* type, gboolean default_value) noexcept { if (default_value) return nullptr; auto const impl = IMPL(backend); auto const ce = cache_lookup_entry(impl, key); if (ce && ce->value_set) return ce->value ? g_variant_ref(ce->value) : nullptr; gs_unref_variant GVariant* rv = nullptr; auto r = terminal_settings_bridge_call_read_sync(impl->bridge, key, g_variant_type_peek_string(type), default_value, &rv, impl->cancellable, nullptr); auto const value = r ? terminal_g_variant_unwrap(rv) : nullptr; if (r && value && !g_variant_is_of_type(value, type)) { _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::read key %s got type %s expected type %s\n", key, g_variant_get_type_string(value), g_variant_type_peek_string(type)); g_clear_pointer(&value, g_variant_unref); r = false; } if (r) cache_insert_value(impl, key, value); else cache_remove_value(impl, key); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::read key %s success %d value %s\n", key, r, value ? g_variant_print(value, true) : "(null)"); return value; } static GVariant* terminal_settings_bridge_backend_read_user_value(GSettingsBackend* backend, char const* key, GVariantType const* type) noexcept { auto const impl = IMPL(backend); auto const ce = cache_lookup_entry(impl, key); if (ce && ce->value_set) return ce->value ? g_variant_ref(ce->value) : nullptr; gs_unref_variant GVariant* rv = nullptr; auto r = terminal_settings_bridge_call_read_user_value_sync(impl->bridge, key, g_variant_type_peek_string(type), &rv, impl->cancellable, nullptr); auto const value = r ? terminal_g_variant_unwrap(rv) : nullptr; if (r && value && !g_variant_is_of_type(value, type)) { _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::read_user_value key %s got type %s expected type %s\n", key, g_variant_get_type_string(value), g_variant_type_peek_string(type)); g_clear_pointer(&value, g_variant_unref); r = false; } if (r) cache_insert_value(impl, key, value); else cache_remove_value(impl, key); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::read_user_value key %s success %d value %s\n", key, r, value ? g_variant_print(value, true) : "(null)"); return value; } static void terminal_settings_bridge_backend_reset(GSettingsBackend* backend, char const* key, void* tag) noexcept { auto const impl = IMPL(backend); auto const r = terminal_settings_bridge_call_reset_sync(impl->bridge, key, impl->cancellable, nullptr); cache_remove_value(impl, key); g_settings_backend_changed(backend, key, tag); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::reset key %s success %d\n", key, r); } static void terminal_settings_bridge_backend_sync(GSettingsBackend* backend) noexcept { auto const impl = IMPL(backend); terminal_settings_bridge_call_sync_sync(impl->bridge, impl->cancellable, nullptr); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::sync\n"); } static void terminal_settings_bridge_backend_subscribe(GSettingsBackend* backend, char const* name) noexcept { auto const impl = IMPL(backend); terminal_settings_bridge_call_subscribe_sync(impl->bridge, name, impl->cancellable, nullptr); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::subscribe name %s\n", name); } static void terminal_settings_bridge_backend_unsubscribe(GSettingsBackend* backend, char const* name) noexcept { auto const impl = IMPL(backend); terminal_settings_bridge_call_unsubscribe_sync(impl->bridge, name, impl->cancellable, nullptr); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::unsubscribe name %s\n", name); } static gboolean terminal_settings_bridge_backend_write(GSettingsBackend* backend, char const* key, GVariant* value, void* tag) noexcept { auto const impl = IMPL(backend); gs_unref_variant auto holder = g_variant_ref_sink(value); auto success = gboolean{false}; auto const r = terminal_settings_bridge_call_write_sync(impl->bridge, key, terminal_g_variant_wrap(value), &success, impl->cancellable, nullptr); cache_insert_value(impl, key, value); g_settings_backend_changed(backend, key, tag); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::write key %s value %s success %d\n", key, value ? g_variant_print(value, true) : "(null)", r); return r && success; } static gboolean terminal_settings_bridge_backend_write_tree(GSettingsBackend* backend, GTree* tree, void* tag) noexcept { auto const impl = IMPL(backend); gs_free char* path_prefix = nullptr; gs_free char const** keys = nullptr; gs_free GVariant** values = nullptr; g_settings_backend_flatten_tree(tree, &path_prefix, &keys, &values); auto builder = GVariantBuilder{}; g_variant_builder_init(&builder, G_VARIANT_TYPE("a(smv)")); for (auto i = 0; keys[i]; ++i) { gs_unref_variant auto value = values[i] ? g_variant_ref_sink(values[i]) : nullptr; g_variant_builder_add(&builder, "(smv)", keys[i], value ? g_variant_new_variant(value) : nullptr); gs_free auto wkey = g_strconcat(path_prefix, keys[i], nullptr); // Directory reset? if (g_str_has_suffix(wkey, "/")) { g_warn_if_fail(!value); cache_remove_path(impl, wkey); } else { cache_insert_value(impl, wkey, value); } } auto const tree_value = terminal_g_variant_wrap(g_variant_builder_end(&builder)); auto success = gboolean{false}; auto const r = terminal_settings_bridge_call_write_tree_sync(impl->bridge, path_prefix, tree_value, // consumed &success, impl->cancellable, nullptr); g_settings_backend_changed_tree(backend, tree, tag); _terminal_debug_print(TERMINAL_DEBUG_BRIDGE, "Bridge backend ::write_tree success %d\n", r); return r && success; } /* GObject class implementation */ static void terminal_settings_bridge_backend_init(TerminalSettingsBridgeBackend* backend) /* noexcept */ { auto const impl = IMPL(backend); // Note that unfortunately it appears to be impossible to receive all // change notifications from a GSettingsBackend directly, so we cannot // get forwarded change notifications from the bridge. Instead, we have // to cache written values (since the actual write happens delayed in // the remote backend and the next read may still return the old value // otherwise). impl->cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, GDestroyNotify(cache_entry_free)); } static void terminal_settings_bridge_backend_constructed(GObject* object) noexcept { G_OBJECT_CLASS(terminal_settings_bridge_backend_parent_class)->constructed(object); auto const impl = IMPL(object); assert(impl->bridge); } static void terminal_settings_bridge_backend_finalize(GObject* object) noexcept { auto const impl = IMPL(object); g_clear_pointer(&impl->cache, g_hash_table_unref); g_clear_object(&impl->cancellable); g_clear_object(&impl->bridge); G_OBJECT_CLASS(terminal_settings_bridge_backend_parent_class)->finalize(object); } static void terminal_settings_bridge_backend_set_property(GObject* object, guint prop_id, GValue const* value, GParamSpec* pspec) noexcept { auto const impl = IMPL(object); switch (prop_id) { case PROP_SETTINGS_BRIDGE: impl->bridge = TERMINAL_SETTINGS_BRIDGE(g_value_dup_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void terminal_settings_bridge_backend_class_init(TerminalSettingsBridgeBackendClass* klass) /* noexcept */ { auto const gobject_class = G_OBJECT_CLASS(klass); gobject_class->constructed = terminal_settings_bridge_backend_constructed; gobject_class->finalize = terminal_settings_bridge_backend_finalize; gobject_class->set_property = terminal_settings_bridge_backend_set_property; g_object_class_install_property (gobject_class, PROP_SETTINGS_BRIDGE, g_param_spec_object("settings-bridge", nullptr, nullptr, TERMINAL_TYPE_SETTINGS_BRIDGE, GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS))); auto const backend_class = G_SETTINGS_BACKEND_CLASS(klass); backend_class->get_permission = terminal_settings_bridge_backend_get_permission; backend_class->get_writable = terminal_settings_bridge_backend_get_writable; backend_class->read = terminal_settings_bridge_backend_read; backend_class->read_user_value = terminal_settings_bridge_backend_read_user_value; backend_class->reset = terminal_settings_bridge_backend_reset; backend_class->subscribe = terminal_settings_bridge_backend_subscribe; backend_class->sync = terminal_settings_bridge_backend_sync; backend_class->unsubscribe = terminal_settings_bridge_backend_unsubscribe; backend_class->write = terminal_settings_bridge_backend_write; backend_class->write_tree = terminal_settings_bridge_backend_write_tree; } /* public API */ /** * terminal_settings_bridge_backend_new: * @bridge: a #TerminalSettingsBridge * * Returns: (transfer full): a new #TerminalSettingsBridgeBackend for @bridge */ GSettingsBackend* terminal_settings_bridge_backend_new(TerminalSettingsBridge* bridge) { return reinterpret_cast (g_object_new (TERMINAL_TYPE_SETTINGS_BRIDGE_BACKEND, "settings-bridge", bridge, nullptr)); }