374 lines
12 KiB
C++
374 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2013-2024 Eugen Wissner <belka@caraus.de>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <sys/timex.h>
|
|
#include "timedate.h"
|
|
|
|
static void slack_method_call(const Glib::RefPtr<Gio::DBus::Connection>& 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<Gio::DBus::MethodInvocation>& invocation)
|
|
{
|
|
// Set time zone
|
|
if (method_name == "SetTimezone")
|
|
{
|
|
Glib::Variant<Glib::ustring> timezone;
|
|
Glib::Variant<bool> 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 == "SetTime")
|
|
{
|
|
Glib::Variant<gint64> usec_utc;
|
|
Glib::Variant<bool> relative;
|
|
Glib::Variant<bool> 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<bool> use_ntp;
|
|
Glib::Variant<bool> 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<std::vector<Glib::ustring>>::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<Gio::DBus::Connection>& 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<Glib::ustring>::create(dlackware::timedate::timezone());
|
|
}
|
|
else if (prop_name == "LocalRTC")
|
|
{
|
|
result = Glib::Variant<bool>::create(dlackware::timedate::local_rtc());
|
|
}
|
|
else if (prop_name == "NTP")
|
|
{
|
|
result = Glib::Variant<bool>::create(dlackware::timedate::ntp());
|
|
}
|
|
else if (prop_name == "CanNTP")
|
|
{
|
|
result = Glib::Variant<bool>::create(dlackware::timedate::can_ntp());
|
|
}
|
|
else if (prop_name == "NTPSynchronized")
|
|
{
|
|
result = Glib::Variant<bool>::create(dlackware::timedate::ntp_synchronized());
|
|
}
|
|
else if (prop_name == "TimeUSec")
|
|
{
|
|
result = Glib::Variant<std::uint64_t>::create(dlackware::timedate::time_usec());
|
|
}
|
|
else if (prop_name == "RTCTimeUSec")
|
|
{
|
|
result = Glib::Variant<std::uint64_t>::create(dlackware::timedate::rtc_time_usec());
|
|
}
|
|
}
|
|
|
|
namespace dlackware::timedate
|
|
{
|
|
constexpr const std::uint64_t usec_per_sec = 1000000ULL;
|
|
constexpr const std::uint64_t nsec_per_usec = 1000ULL;
|
|
|
|
timedate1::timedate1()
|
|
: interface_vtable{ &slack_method_call, &slack_get_property }
|
|
{
|
|
}
|
|
|
|
void timedate1::on_name_lost(const Glib::RefPtr<Gio::DBus::Connection>& 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<Gio::DBus::Connection>& connection, const Glib::ustring& name)
|
|
{
|
|
Glib::RefPtr<Gio::DBus::NodeInfo> 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<Glib::ustring>& 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<Glib::ustring> list_timezones()
|
|
{
|
|
std::vector<Glib::ustring> result;
|
|
|
|
list_timezones("", result);
|
|
return result;
|
|
}
|
|
|
|
Glib::ustring timezone()
|
|
{
|
|
std::unique_ptr<gchar[], decltype(&g_free)> 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)
|
|
{
|
|
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 seconds_since_epoch, bool relative, bool)
|
|
{
|
|
timespec ts;
|
|
|
|
if (relative)
|
|
{
|
|
if (clock_gettime(CLOCK_REALTIME, &ts) == -1)
|
|
{
|
|
throw std::system_error(errno, std::generic_category());
|
|
}
|
|
ts.tv_sec += static_cast<time_t>(seconds_since_epoch / dlackware::timedate::usec_per_sec);
|
|
ts.tv_nsec += (seconds_since_epoch % dlackware::timedate::usec_per_sec) * dlackware::timedate::nsec_per_usec;
|
|
|
|
if (ts.tv_nsec < 0)
|
|
{
|
|
--ts.tv_sec;
|
|
ts.tv_nsec += dlackware::timedate::usec_per_sec;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ts.tv_sec = static_cast<time_t>(seconds_since_epoch / dlackware::timedate::usec_per_sec);
|
|
ts.tv_nsec = (seconds_since_epoch % dlackware::timedate::usec_per_sec) * dlackware::timedate::nsec_per_usec;
|
|
}
|
|
if (clock_settime(CLOCK_REALTIME, &ts) == -1)
|
|
{
|
|
throw std::system_error(errno, std::generic_category());
|
|
}
|
|
}
|
|
|
|
bool local_rtc()
|
|
{
|
|
std::ifstream fh{ "/etc/hardwareclock", std::ios::in };
|
|
if (!fh.is_open())
|
|
{
|
|
return false;
|
|
}
|
|
std::array<char, 10> 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<std::string>{ "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::microseconds>(
|
|
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::microseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
|
}
|
|
}
|