summaryrefslogtreecommitdiff
path: root/Userland/Services/ConfigServer/ConnectionFromClient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Userland/Services/ConfigServer/ConnectionFromClient.cpp')
-rw-r--r--Userland/Services/ConfigServer/ConnectionFromClient.cpp274
1 files changed, 274 insertions, 0 deletions
diff --git a/Userland/Services/ConfigServer/ConnectionFromClient.cpp b/Userland/Services/ConfigServer/ConnectionFromClient.cpp
new file mode 100644
index 0000000000..ff5f29371b
--- /dev/null
+++ b/Userland/Services/ConfigServer/ConnectionFromClient.cpp
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "ConnectionFromClient.h"
+#include <ConfigServer/ConfigClientEndpoint.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/FileWatcher.h>
+#include <LibCore/Timer.h>
+
+namespace ConfigServer {
+
+static HashMap<int, RefPtr<ConnectionFromClient>> s_connections;
+
+struct CachedDomain {
+ String domain;
+ NonnullRefPtr<Core::ConfigFile> config;
+ RefPtr<Core::FileWatcher> watcher;
+};
+
+static HashMap<String, NonnullOwnPtr<CachedDomain>> s_cache;
+static constexpr int s_disk_sync_delay_ms = 5'000;
+
+static void for_each_monitoring_connection(String const& domain, ConnectionFromClient* excluded_connection, Function<void(ConnectionFromClient&)> callback)
+{
+ for (auto& it : s_connections) {
+ if (it.value->is_monitoring_domain(domain) && (!excluded_connection || it.value != excluded_connection))
+ callback(*it.value);
+ }
+}
+
+static Core::ConfigFile& ensure_domain_config(String const& domain)
+{
+ auto it = s_cache.find(domain);
+ if (it != s_cache.end())
+ return *it->value->config;
+
+ auto config = Core::ConfigFile::open_for_app(domain, Core::ConfigFile::AllowWriting::Yes).release_value_but_fixme_should_propagate_errors();
+ // FIXME: Use a single FileWatcher with multiple watches inside.
+ auto watcher_or_error = Core::FileWatcher::create(InodeWatcherFlags::Nonblock);
+ VERIFY(!watcher_or_error.is_error());
+ auto result = watcher_or_error.value()->add_watch(config->filename(), Core::FileWatcherEvent::Type::ContentModified);
+ VERIFY(!result.is_error());
+ watcher_or_error.value()->on_change = [config, domain](auto&) {
+ auto new_config = Core::ConfigFile::open(config->filename(), Core::ConfigFile::AllowWriting::Yes).release_value_but_fixme_should_propagate_errors();
+ for (auto& group : config->groups()) {
+ for (auto& key : config->keys(group)) {
+ if (!new_config->has_key(group, key)) {
+ for_each_monitoring_connection(domain, nullptr, [&domain, &group, &key](ConnectionFromClient& connection) {
+ connection.async_notify_removed_key(domain, group, key);
+ });
+ }
+ }
+ }
+ // FIXME: Detect type of keys.
+ for (auto& group : new_config->groups()) {
+ for (auto& key : new_config->keys(group)) {
+ auto old_value = config->read_entry(group, key);
+ auto new_value = new_config->read_entry(group, key);
+ if (old_value != new_value) {
+ for_each_monitoring_connection(domain, nullptr, [&domain, &group, &key, &new_value](ConnectionFromClient& connection) {
+ connection.async_notify_changed_string_value(domain, group, key, new_value);
+ });
+ }
+ }
+ }
+ // FIXME: Refactor this whole thing so that we don't need a cache lookup here.
+ s_cache.get(domain).value()->config = new_config;
+ };
+ auto cache_entry = make<CachedDomain>(domain, config, watcher_or_error.release_value());
+ s_cache.set(domain, move(cache_entry));
+ return *config;
+}
+
+ConnectionFromClient::ConnectionFromClient(NonnullOwnPtr<Core::Stream::LocalSocket> client_socket, int client_id)
+ : IPC::ConnectionFromClient<ConfigClientEndpoint, ConfigServerEndpoint>(*this, move(client_socket), client_id)
+ , m_sync_timer(Core::Timer::create_single_shot(s_disk_sync_delay_ms, [this]() { sync_dirty_domains_to_disk(); }))
+{
+ s_connections.set(client_id, *this);
+}
+
+ConnectionFromClient::~ConnectionFromClient()
+{
+}
+
+void ConnectionFromClient::die()
+{
+ s_connections.remove(client_id());
+ m_sync_timer->stop();
+ sync_dirty_domains_to_disk();
+}
+
+void ConnectionFromClient::pledge_domains(Vector<String> const& domains)
+{
+ if (m_has_pledged) {
+ did_misbehave("Tried to pledge domains twice.");
+ return;
+ }
+ m_has_pledged = true;
+ for (auto& domain : domains)
+ m_pledged_domains.set(domain);
+}
+
+void ConnectionFromClient::monitor_domain(String const& domain)
+{
+ if (m_has_pledged && !m_pledged_domains.contains(domain)) {
+ did_misbehave("Attempt to monitor non-pledged domain");
+ return;
+ }
+
+ m_monitored_domains.set(domain);
+}
+
+bool ConnectionFromClient::validate_access(String const& domain, String const& group, String const& key)
+{
+ if (!m_has_pledged)
+ return true;
+ if (m_pledged_domains.contains(domain))
+ return true;
+ did_misbehave(String::formatted("Blocked attempt to access domain '{}', group={}, key={}", domain, group, key).characters());
+ return false;
+}
+
+void ConnectionFromClient::sync_dirty_domains_to_disk()
+{
+ if (m_dirty_domains.is_empty())
+ return;
+ auto dirty_domains = move(m_dirty_domains);
+ dbgln("Syncing {} dirty domains to disk", dirty_domains.size());
+ for (auto domain : dirty_domains) {
+ auto& config = ensure_domain_config(domain);
+ if (auto result = config.sync(); result.is_error()) {
+ dbgln("Failed to write config '{}' to disk: {}", domain, result.error());
+ // Put it back in the list since it's still dirty.
+ m_dirty_domains.set(domain);
+ }
+ }
+}
+
+Messages::ConfigServer::ListConfigKeysResponse ConnectionFromClient::list_config_keys(String const& domain, String const& group)
+{
+ if (!validate_access(domain, group, ""))
+ return Vector<String> {};
+ auto& config = ensure_domain_config(domain);
+ return { config.keys(group) };
+}
+
+Messages::ConfigServer::ListConfigGroupsResponse ConnectionFromClient::list_config_groups(String const& domain)
+{
+ if (!validate_access(domain, "", ""))
+ return Vector<String> {};
+ auto& config = ensure_domain_config(domain);
+ return { config.groups() };
+}
+
+Messages::ConfigServer::ReadStringValueResponse ConnectionFromClient::read_string_value(String const& domain, String const& group, String const& key)
+{
+ if (!validate_access(domain, group, key))
+ return nullptr;
+
+ auto& config = ensure_domain_config(domain);
+ if (!config.has_key(group, key))
+ return Optional<String> {};
+ return Optional<String> { config.read_entry(group, key) };
+}
+
+Messages::ConfigServer::ReadI32ValueResponse ConnectionFromClient::read_i32_value(String const& domain, String const& group, String const& key)
+{
+ if (!validate_access(domain, group, key))
+ return nullptr;
+
+ auto& config = ensure_domain_config(domain);
+ if (!config.has_key(group, key))
+ return Optional<i32> {};
+ return Optional<i32> { config.read_num_entry(group, key) };
+}
+
+Messages::ConfigServer::ReadBoolValueResponse ConnectionFromClient::read_bool_value(String const& domain, String const& group, String const& key)
+{
+ if (!validate_access(domain, group, key))
+ return nullptr;
+
+ auto& config = ensure_domain_config(domain);
+ if (!config.has_key(group, key))
+ return Optional<bool> {};
+ return Optional<bool> { config.read_bool_entry(group, key) };
+}
+
+void ConnectionFromClient::start_or_restart_sync_timer()
+{
+ if (m_sync_timer->is_active())
+ m_sync_timer->restart();
+ else
+ m_sync_timer->start();
+}
+
+void ConnectionFromClient::write_string_value(String const& domain, String const& group, String const& key, String const& value)
+{
+ if (!validate_access(domain, group, key))
+ return;
+
+ auto& config = ensure_domain_config(domain);
+
+ if (config.has_key(group, key) && config.read_entry(group, key) == value)
+ return;
+
+ config.write_entry(group, key, value);
+ m_dirty_domains.set(domain);
+ start_or_restart_sync_timer();
+
+ for_each_monitoring_connection(domain, this, [&domain, &group, &key, &value](ConnectionFromClient& connection) {
+ connection.async_notify_changed_string_value(domain, group, key, value);
+ });
+}
+
+void ConnectionFromClient::write_i32_value(String const& domain, String const& group, String const& key, i32 value)
+{
+ if (!validate_access(domain, group, key))
+ return;
+
+ auto& config = ensure_domain_config(domain);
+
+ if (config.has_key(group, key) && config.read_num_entry(group, key) == value)
+ return;
+
+ config.write_num_entry(group, key, value);
+ m_dirty_domains.set(domain);
+ start_or_restart_sync_timer();
+
+ for_each_monitoring_connection(domain, this, [&domain, &group, &key, &value](ConnectionFromClient& connection) {
+ connection.async_notify_changed_i32_value(domain, group, key, value);
+ });
+}
+
+void ConnectionFromClient::write_bool_value(String const& domain, String const& group, String const& key, bool value)
+{
+ if (!validate_access(domain, group, key))
+ return;
+
+ auto& config = ensure_domain_config(domain);
+
+ if (config.has_key(group, key) && config.read_bool_entry(group, key) == value)
+ return;
+
+ config.write_bool_entry(group, key, value);
+ m_dirty_domains.set(domain);
+ start_or_restart_sync_timer();
+
+ for_each_monitoring_connection(domain, this, [&domain, &group, &key, &value](ConnectionFromClient& connection) {
+ connection.async_notify_changed_bool_value(domain, group, key, value);
+ });
+}
+
+void ConnectionFromClient::remove_key(String const& domain, String const& group, String const& key)
+{
+ if (!validate_access(domain, group, key))
+ return;
+
+ auto& config = ensure_domain_config(domain);
+ if (!config.has_key(group, key))
+ return;
+
+ config.remove_entry(group, key);
+ m_dirty_domains.set(domain);
+ start_or_restart_sync_timer();
+
+ for_each_monitoring_connection(domain, this, [&domain, &group, &key](ConnectionFromClient& connection) {
+ connection.async_notify_removed_key(domain, group, key);
+ });
+}
+
+}