/* * 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 "timedate.h" static void slack_method_call(const Glib::RefPtr& connection, const Glib::ustring& sender, const Glib::ustring& object_path, const Glib::ustring& interface_name, const Glib::ustring& method_name, const Glib::VariantContainerBase& parameters, const Glib::RefPtr& invocation) { // Set time zone if (method_name == "SetTimezone") { Glib::Variant timezone; Glib::Variant user_interaction; parameters.get_child(timezone, 0); parameters.get_child(user_interaction, 1); try { dlackware::timedate::set_timezone(timezone.get(), user_interaction.get()); invocation->return_value(Glib::VariantContainerBase()); } catch (const std::filesystem::filesystem_error& filesystem_error) { invocation->return_error(Gio::DBus::Error{ Gio::DBus::Error::FAILED, filesystem_error.what() }); } } else if (method_name == "SetLocalRTC") { Glib::Variant local_rtc; Glib::Variant fix_system; Glib::Variant user_interaction; parameters.get_child(local_rtc, 0); parameters.get_child(fix_system, 1); parameters.get_child(user_interaction, 2); try { dlackware::timedate::set_local_rtc(local_rtc.get(), fix_system.get(), user_interaction.get()); invocation->return_value(Glib::VariantContainerBase()); } catch (const std::system_error& error) { invocation->return_error(Gio::DBus::Error{ Gio::DBus::Error::FAILED, error.what() }); } catch (const Glib::SpawnError& spawn_error) { invocation->return_error(Gio::DBus::Error{ Gio::DBus::Error::FAILED, spawn_error.what() }); } } else if (method_name == "SetTime") { Glib::Variant usec_utc; Glib::Variant relative; Glib::Variant user_interaction; parameters.get_child(usec_utc, 0); parameters.get_child(relative, 1); parameters.get_child(user_interaction, 2); try { dlackware::timedate::set_time(usec_utc.get(), relative.get(), user_interaction.get()); invocation->return_value(Glib::VariantContainerBase()); } catch (const std::system_error& error) { invocation->return_error(Gio::DBus::Error{ Gio::DBus::Error::FAILED, error.what() }); } } else if (method_name == "SetNTP") { Glib::Variant use_ntp; Glib::Variant user_interaction; parameters.get_child(use_ntp, 0); parameters.get_child(user_interaction, 1); // Enable NTP try { dlackware::timedate::set_ntp(use_ntp.get(), user_interaction.get()); invocation->return_value(Glib::VariantContainerBase()); } catch (const std::filesystem::filesystem_error& filesystem_error) { invocation->return_error(Gio::DBus::Error{ Gio::DBus::Error::FAILED, filesystem_error.what() }); } } else if (method_name == "ListTimezones") { try { auto return_tuple = Glib::VariantContainerBase::create_tuple({ Glib::Variant>::create(dlackware::timedate::list_timezones()) }); invocation->return_value(return_tuple); } catch (const std::exception& exception) { Gio::DBus::Error error{ Gio::DBus::Error::FAILED, exception.what() }; invocation->return_error(error); } } } static void slack_get_property(Glib::VariantBase& result, const Glib::RefPtr& connection, const Glib::ustring& sender, const Glib::ustring& object_path, const Glib::ustring& interface_name, const Glib::ustring& prop_name) { if (prop_name == "Timezone") { result = Glib::Variant::create(dlackware::timedate::timezone()); } else if (prop_name == "LocalRTC") { result = Glib::Variant::create(dlackware::timedate::local_rtc()); } else if (prop_name == "NTP") { result = Glib::Variant::create(dlackware::timedate::ntp()); } else if (prop_name == "CanNTP") { result = Glib::Variant::create(dlackware::timedate::can_ntp()); } else if (prop_name == "NTPSynchronized") { result = Glib::Variant::create(dlackware::timedate::ntp_synchronized()); } else if (prop_name == "TimeUSec") { result = Glib::Variant::create(dlackware::timedate::time_usec()); } else if (prop_name == "RTCTimeUSec") { result = Glib::Variant::create(dlackware::timedate::rtc_time_usec()); } } namespace dlackware::timedate { timedate1::timedate1() : interface_vtable{ &slack_method_call, &slack_get_property } { } void timedate1::on_name_lost(const Glib::RefPtr& connection, const Glib::ustring& name) { g_warning("Failed to acquire the service %s.\n", name.data()); exit(1); } void timedate1::on_bus_acquired(const Glib::RefPtr& connection, const Glib::ustring& name) { Glib::RefPtr introspection_data; try { auto introspection_xml = Glib::file_get_contents( (std::filesystem::path(DATADIR_INTERFACES) / dlackware::timedate::bus_name).concat(".xml") ); 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(), this->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 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); } } } std::vector list_timezones() { std::vector result; list_timezones("", result); return result; } Glib::ustring 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() + zoneinfo_database.size() + 1 }; } void set_timezone(const Glib::ustring& zone, bool) { auto zone_file = std::filesystem::path("/usr/share/zoneinfo") / zone.data(); std::error_code ec{}; if (!std::filesystem::is_regular_file(zone_file, ec)) { throw std::filesystem::filesystem_error("Zone info is not a regular file", zone_file, ec); } std::filesystem::path etc_localtime = "/etc/localtime"; std::filesystem::remove(etc_localtime); std::filesystem::create_symlink(zone_file, etc_localtime); } void set_ntp(bool use_ntp, bool) { try { std::string service_state = use_ntp ? "-u" : "-d"; const auto command_flags = Glib::SpawnFlags::SPAWN_SEARCH_PATH | Glib::SpawnFlags::SPAWN_STDERR_TO_DEV_NULL | Glib::SpawnFlags::SPAWN_STDERR_TO_DEV_NULL; int wait_status{-1}; Glib::spawn_sync("", std::vector{ "s6-rc", service_state, "change", "ntpd" }, command_flags, {}, nullptr, nullptr, &wait_status); if (wait_status == 0 || wait_status == 0x300) // Unknown service name in the arguments. { return; } } catch (const Glib::SpawnError& spawn_error) { } 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 (use_ntp) { rc_mode |= std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; } std::error_code ec; const std::filesystem::path service_path{ "/etc/rc.d/rc.ntpd" }; if (std::filesystem::is_regular_file(service_path, ec)) { std::filesystem::permissions("/etc/rc.d/rc.ntpd", rc_mode); } else { throw std::filesystem::filesystem_error("The NTP daemon isn't installed", service_path, ec); } } void set_time(std::int64_t usec_utc, bool relative, bool) { timespec ts; std::chrono::system_clock::duration current_time_since_epoch; if (relative) { current_time_since_epoch = std::chrono::system_clock::now().time_since_epoch(); current_time_since_epoch += std::chrono::microseconds(usec_utc); } else { current_time_since_epoch = std::chrono::microseconds(usec_utc); } auto seconds_since_epoch = std::chrono::floor(current_time_since_epoch); ts.tv_sec = seconds_since_epoch.count(); ts.tv_nsec = std::chrono::duration_cast( current_time_since_epoch - seconds_since_epoch).count(); if (clock_settime(CLOCK_REALTIME, &ts) == -1) { throw std::system_error(errno, std::generic_category()); } } void set_local_rtc(bool local_rtc, bool fix_system, bool) { std::ofstream hardwareclock{ "/etc/hardwareclock", std::ios::trunc }; std::string hwclock_argument; hardwareclock << "# /etc/hardwareclock" << std::endl << "#" << std::endl << "# Tells how the hardware clock time is stored." << std::endl << "# You should run timeconfig to edit this file." << std::endl << std::endl; if (local_rtc) { hwclock_argument = "--localtime"; hardwareclock << "localtime"; } else { hwclock_argument = "--utc"; hardwareclock << "UTC"; } hardwareclock << std::endl; if (!fix_system) { Glib::spawn_sync("", std::vector{ "/sbin/hwclock", hwclock_argument, "--hctosys" }, Glib::SpawnFlags::SPAWN_STDOUT_TO_DEV_NULL | Glib::SpawnFlags::SPAWN_STDERR_TO_DEV_NULL, {}, nullptr, nullptr, static_cast(nullptr)); } } bool local_rtc() { 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; } bool ntp() { return Glib::file_test("/etc/rc.d/rc.ntpd", Glib::FileTest::FILE_TEST_IS_EXECUTABLE); } bool can_ntp() { std::string standard_output; int wait_status{-1}; try { Glib::spawn_sync("", std::vector{ "s6-rc-db", "type", "ntpd" }, Glib::SpawnFlags::SPAWN_SEARCH_PATH | Glib::SpawnFlags::SPAWN_STDERR_TO_DEV_NULL, {}, &standard_output, nullptr, &wait_status); } catch (const Glib::SpawnError& spawn_error) { return std::filesystem::exists("/etc/rc.d/rc.ntpd"); } return wait_status == 0 && standard_output == "bundle\n"; } bool ntp_synchronized() { timex buffer{ .modes = 0 }; int ntp_result = adjtimex(&buffer); if (ntp_result == -1) { throw std::system_error(errno, std::generic_category()); } return ntp_result != TIME_ERROR && (buffer.status & STA_UNSYNC) == 0; } std::uint64_t time_usec() { timespec spec; if (clock_gettime(CLOCK_REALTIME, &spec) == -1) { throw std::system_error(errno, std::generic_category()); } time_t seconds = spec.tv_sec; tm time; if (local_rtc()) { gmtime_r(&seconds, &time); } else { localtime_r(&seconds, &time); } return std::chrono::duration_cast( std::chrono::seconds(std::mktime(&time)) + std::chrono::nanoseconds(spec.tv_nsec)).count(); } std::uint64_t rtc_time_usec() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); } }