/* * Copyright (C) 2013-2024 Eugen Wissner * * 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 #include #include #include #include #include #include #include #include "slack-timedate.h" static void slack_method_call (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { gchar *timezone, *response; gboolean user_interaction, relative, is_localtime, use_ntp; gint64 usec_utc; auto timedate1 = reinterpret_cast(user_data); // Set time zone if (g_strcmp0(method_name, "SetTimezone") == 0) { g_variant_get(parameters, "(&sb)", &timezone, &user_interaction); std::error_code error_code; if (timedate1->set_timezone(timezone, error_code)) { g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Set time zone: %s", error_code ? error_code.message().c_str() : "Zone info is not a regular file"); } g_free (timezone); } else if (g_strcmp0(method_name, "SetTime") == 0) { g_variant_get (parameters, "(xbb)", &usec_utc, &relative, &user_interaction); // Set time //if (!slack_set_time (usec_utc, slack_get_is_localtime ())) { if (slack_set_time(usec_utc, relative)) { g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to set system clock"); } } else if (g_strcmp0(method_name, "SetNTP") == 0) { g_variant_get (parameters, "(bb)", &use_ntp, &user_interaction); // Enable NTP if (slack_set_ntp (use_ntp)) { g_dbus_method_invocation_return_value (invocation, NULL); } else { g_dbus_method_invocation_return_error (invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "Error enabling NTP"); } } else if (g_strcmp0(method_name, "ListTimezones") == 0) { try { auto return_tuple = Glib::Variant>::create_tuple({ timedate1->list_timezones() }); g_dbus_method_invocation_return_value(invocation, return_tuple.gobj()); } catch (const std::exception& exception) { g_dbus_method_invocation_return_error(invocation, G_IO_ERROR, G_IO_ERROR_FAILED, "List time zones: %s", exception.what()); } } } static GVariant *slack_get_property (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *prop_name, GError **slack_err, gpointer user_data) { auto timedate1 = reinterpret_cast(user_data); if (g_strcmp0 ("Timezone", prop_name) == 0) { return Glib::Variant::create(timedate1->timezone()).gobj_copy(); } if (g_strcmp0 ("LocalRTC", prop_name) == 0) { return g_variant_new_boolean (slack_get_is_localtime ()); } if (g_strcmp0 ("NTP", prop_name) == 0) { return g_variant_new_boolean (slack_get_ntp ()); } return NULL; } static void on_timedate_acquired(const Glib::RefPtr& connection, const Glib::ustring& name, gpointer user_data) { Glib::RefPtr introspection_data; static const auto interface_vtable = Gio::DBus::InterfaceVTable( [user_data](const Glib::RefPtr& connection, const Glib::ustring& sender, const Glib::ustring& obj_path, const Glib::ustring& interface_name, const Glib::ustring& method_name, const Glib::VariantContainerBase& parameters, const Glib::RefPtr& invocation) { auto parameters_copy = parameters.gobj_copy(); slack_method_call(connection->gobj(), sender.data(), obj_path.data(), interface_name.data(), method_name.data(), parameters_copy, invocation->gobj(), user_data); g_free(parameters_copy); }, [user_data](Glib::VariantBase& result, const Glib::RefPtr& connection, const Glib::ustring& sender, const Glib::ustring& obj_path, const Glib::ustring& interface_name, const Glib::ustring& prop_name) { auto property_result = slack_get_property(connection->gobj(), sender.data(), obj_path.data(), interface_name.data(), prop_name.data(), nullptr, user_data); result.init(property_result, true); } ); try { introspection_data = Gio::DBus::NodeInfo::create_for_xml(INTROSPECTION_XML); } catch (Glib::Error& slack_err) { g_error("Failed to parse D-Bus introspection XML: %s\n", slack_err.what().data()); } try { guint registration_id = connection->register_object(BUS_PATH, introspection_data->lookup_interface(), interface_vtable); } catch (Glib::Error& slack_err) { g_critical("Failed to register callbacks for the exported object with the D-Bus interface: %s\n", slack_err.what().data()); } } static void on_timedate_lost(const Glib::RefPtr& connection, const Glib::ustring& name) { g_warning("Failed to acquire the service %s.\n", name.data()); exit(1); } gboolean timeout_callback (Glib::RefPtr loop2quit) { loop2quit->quit(); return false; } int main(int argc, char **argv) { Gio::init(); auto timedate1 = std::make_unique(); guint owner_id = Gio::DBus::own_name(Gio::DBus::BUS_TYPE_SYSTEM, BUS_NAME, [&timedate1](const Glib::RefPtr& connection, const Glib::ustring& name) { on_timedate_acquired(connection, name, timedate1.get()); }, Gio::DBus::SlotNameAcquired(), &on_timedate_lost, Gio::DBus::BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | Gio::DBus::BUS_NAME_OWNER_FLAGS_REPLACE); Glib::RefPtr loop = Glib::MainLoop::create(false); Glib::signal_timeout().connect_seconds(sigc::bind(&timeout_callback, loop), DEFAULT_EXIT_SEC); loop->run(); Gio::DBus::unown_name(owner_id); return EXIT_SUCCESS; } namespace dlackware::timedate { void timedate1::list_timezones(const std::string& prefix, std::vector& accumulator) { auto zoneinfo_path = std::filesystem::path(zoneinfo_database) / prefix; for (auto zoneinfo_entry : std::filesystem::directory_iterator(zoneinfo_path)) { auto new_prefix = prefix + zoneinfo_entry.path().filename().string(); if (zoneinfo_entry.is_directory()) { list_timezones(new_prefix + "/", accumulator); } else if (zoneinfo_entry.is_regular_file() && !zoneinfo_entry.path().has_extension() && zoneinfo_entry.path().filename() != "leapseconds") { accumulator.emplace_back(new_prefix); } } } Glib::Variant> timedate1::list_timezones() { std::vector result; list_timezones("", result); return Glib::Variant>::create(result); } Glib::ustring timedate1::timezone() { std::unique_ptr zone_copied_from( g_file_read_link("/etc/localtime", NULL), &g_free); if (zone_copied_from == nullptr) { return nullptr; } return Glib::ustring{ zone_copied_from.get() + strlen(zoneinfo_database) + 1 }; } bool timedate1::set_timezone(gchar *zone, std::error_code& ec) { ec.clear(); auto zone_file = std::filesystem::path("/usr/share/zoneinfo") / zone; if (!std::filesystem::is_regular_file(zone_file, ec) || ec) { return false; } std::filesystem::path etc_localtime = "/etc/localtime"; std::filesystem::remove(etc_localtime); std::filesystem::create_symlink(zone_file, etc_localtime, ec); return !ec; } } gboolean slack_set_time(gint64 seconds_since_epoch, gboolean relative) { timespec ts; if (relative) { if (clock_gettime(CLOCK_REALTIME, &ts) == -1) { return FALSE; } ts.tv_sec += static_cast(seconds_since_epoch / USEC_PER_SEC); ts.tv_nsec += (seconds_since_epoch % USEC_PER_SEC) * NSEC_PER_USEC; if (ts.tv_nsec < 0) { --ts.tv_sec; ts.tv_nsec += USEC_PER_SEC; } } else { ts.tv_sec = static_cast(seconds_since_epoch / USEC_PER_SEC); ts.tv_nsec = (seconds_since_epoch % USEC_PER_SEC) * NSEC_PER_USEC; } return clock_settime (CLOCK_REALTIME, &ts) == 0; } gboolean slack_get_is_localtime() { std::ifstream fh{ "/etc/hardwareclock", std::ios::in }; if (!fh.is_open()) { return false; } std::array time_str; // "localtime" is longer than "UTC" and has 9 letters + \0 while (fh.good()) { fh.getline(time_str.data(), time_str.size()); fh.clear(fh.rdstate() & (~std::ios_base::failbit)); if (std::strncmp(time_str.data(), "localtime", time_str.size()) == 0) { return true; } } return false; } gboolean slack_get_ntp() { return Glib::file_test("/etc/rc.d/rc.ntpd", Glib::FileTest::FILE_TEST_IS_EXECUTABLE); } gboolean slack_set_ntp(gboolean xntp) { std::filesystem::perms rc_mode = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read; if (xntp) { rc_mode |= std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; g_message("Please don't forget to configure the NTP daemon"); /* It doesn't matter if fails. * The ntpdate command is considered obsolete, but "orthodox" ntpd -g * will fail if your system clock is off for more than half an hour. */ g_spawn_command_line_async("/usr/sbin/ntpdate pool.ntp.org", NULL); } if (Glib::file_test("/etc/rc.d/rc.ntpd", Glib::FileTest::FILE_TEST_IS_REGULAR)) { std::error_code ec; std::filesystem::permissions("/etc/rc.d/rc.ntpd", rc_mode, ec); return !ec; } else { g_error("The NTP daemon isn't installed"); return false; } }