diff options
Diffstat (limited to 'src/ClockTAI.cpp')
-rw-r--r-- | src/ClockTAI.cpp | 562 |
1 files changed, 0 insertions, 562 deletions
diff --git a/src/ClockTAI.cpp b/src/ClockTAI.cpp deleted file mode 100644 index c376c07..0000000 --- a/src/ClockTAI.cpp +++ /dev/null @@ -1,562 +0,0 @@ -/* - Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, - 2011, 2012 Her Majesty the Queen in Right of Canada (Communications - Research Center Canada) - - Copyright (C) 2019 - Matthias P. Braendli, matthias.braendli@mpb.li - - http://www.opendigitalradio.org - */ -/* - This file is part of ODR-DabMux. - - ODR-DabMux 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. - - ODR-DabMux 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 ODR-DabMux. If not, see <http://www.gnu.org/licenses/>. -*/ - -/* This file downloads the TAI-UTC bulletins from the from IETF and parses them - * so that correct time can be communicated in EDI timestamps. - * - * This file contains self-test code that can be executed by running - * g++ -g -Wall -DTEST -DHAVE_CURL -std=c++11 -lcurl -pthread \ - * ClockTAI.cpp Log.cpp RemoteControl.cpp -lboost_system -o taitest && ./taitest - */ - -#ifdef HAVE_CONFIG_H -# include "config.h" -#endif - -#include "ClockTAI.h" -#include "Log.h" - -#include <time.h> -#include <stdio.h> -#include <errno.h> -#if SUPPORT_SETTING_CLOCK_TAI -# include <sys/timex.h> -#endif -#ifdef HAVE_CURL -# include <curl/curl.h> -#endif -#include <array> -#include <string> -#include <iostream> -#include <algorithm> -#include <regex> - -using namespace std; - -#ifdef TEST -static bool wait_longer = true; -#endif - -constexpr int download_retry_interval_hours = 1; - -// Offset between NTP time and POSIX time: -// timestamp_unix = timestamp_ntp - ntp_unix_offset -const int64_t ntp_unix_offset = 2208988800L; - -// leap seconds insertion bulletin is available from the IETF and in the TZ -// distribution -static array<const char*, 2> default_tai_urls = { - "https://www.ietf.org/timezones/data/leap-seconds.list", - "https://raw.githubusercontent.com/eggert/tz/master/leap-seconds.list", -}; - -// According to the Filesystem Hierarchy Standard, the data in -// /var/tmp "must not be deleted when the system is booted." -static const char *tai_cache_location = "/var/tmp/odr-dabmux-leap-seconds.cache"; - -// read TAI offset from a valid bulletin in IETF format -static int parse_ietf_bulletin(const std::string& bulletin) -{ - // Example Line: - // 3692217600 37 # 1 Jan 2017 - // - // NTP timestamp<TAB>leap seconds<TAB># some comment - // The NTP timestamp starts at epoch 1.1.1900. - // The difference between NTP timestamps and unix epoch is 70 - // years i.e. 2208988800 seconds - - std::regex regex_bulletin(R"(([0-9]+)\s+([0-9]+)\s+#.*)"); - - time_t now = time(nullptr); - - int tai_utc_offset = 0; - - int tai_utc_offset_valid = false; - - stringstream ss(bulletin); - - /* We cannot just take the last line, because it might - * be in the future, announcing an upcoming leap second. - * - * So we need to look at the current date, and compare it - * with the date of the leap second. - */ - for (string line; getline(ss, line); ) { - - std::smatch bulletin_entry; - - bool is_match = std::regex_search(line, bulletin_entry, regex_bulletin); - if (is_match) { - if (bulletin_entry.size() != 3) { - throw runtime_error( - "Incorrect number of matched TAI IETF bulletin entries"); - } - const string bulletin_ntp_timestamp(bulletin_entry[1]); - const string bulletin_offset(bulletin_entry[2]); - - const int64_t timestamp_unix = - std::atoll(bulletin_ntp_timestamp.c_str()) - ntp_unix_offset; - - const int offset = std::atoi(bulletin_offset.c_str()); - // Ignore entries announcing leap seconds in the future - if (timestamp_unix < now) { - tai_utc_offset = offset; - tai_utc_offset_valid = true; - } -#if TEST - else { - cerr << "IETF Ignoring offset " << bulletin_offset << - " at TS " << bulletin_ntp_timestamp << - " in the future" << endl; - } -#endif - } - } - - if (not tai_utc_offset_valid) { - throw runtime_error("No data in TAI bulletin"); - } - - return tai_utc_offset; -} - - -struct bulletin_state { - bool valid = false; - int64_t expiry = 0; - int offset = 0; - - bool usable() const { return valid and expiry > 0; } -}; - -static bulletin_state parse_bulletin(const string& bulletin) -{ - // The bulletin contains one line that specifies an expiration date - // in NTP time. If that point in time is in the future, we consider - // the bulletin valid. - // - // The entry looks like this: - //#@ 3707596800 - - bulletin_state ret; - - std::regex regex_expiration(R"(#@\s+([0-9]+))"); - - time_t now = time(nullptr); - - stringstream ss(bulletin); - - for (string line; getline(ss, line); ) { - std::smatch bulletin_entry; - - bool is_match = std::regex_search(line, bulletin_entry, regex_expiration); - if (is_match) { - if (bulletin_entry.size() != 2) { - throw runtime_error( - "Incorrect number of matched TAI IETF bulletin expiration"); - } - const string expiry_data_str(bulletin_entry[1]); - const int64_t expiry_unix = - std::atoll(expiry_data_str.c_str()) - ntp_unix_offset; - -#ifdef TEST - etiLog.level(info) << "Bulletin expires in " << expiry_unix - now; -#endif - ret.expiry = expiry_unix - now; - try { - ret.offset = parse_ietf_bulletin(bulletin); - ret.valid = true; - } - catch (const runtime_error& e) { - etiLog.level(warn) << "Bulletin expiry ok but parse error: " << e.what(); - } - break; - } - } - return ret; -} - - -// callback that receives data from cURL -static size_t fill_bulletin(char *ptr, size_t size, size_t nmemb, void *ctx) -{ - auto *bulletin = reinterpret_cast<stringstream*>(ctx); - - size_t len = size * nmemb; - for (size_t i = 0; i < len; i++) { - *bulletin << ptr[i]; - } - return len; -} - -static string download_tai_utc_bulletin(const char* url) -{ - stringstream bulletin; - -#ifdef HAVE_CURL - CURL *curl; - CURLcode res; - - curl = curl_easy_init(); - if (curl) { - curl_easy_setopt(curl, CURLOPT_URL, url); - /* Tell libcurl to follow redirection */ - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fill_bulletin); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bulletin); - - res = curl_easy_perform(curl); - /* always cleanup ! */ - curl_easy_cleanup(curl); - - if (res != CURLE_OK) { - throw runtime_error( "TAI-UTC bulletin download failed: " + - string(curl_easy_strerror(res))); - } - } - return bulletin.str(); -#else - throw runtime_error("Cannot download TAI Clock information without cURL"); -#endif // HAVE_CURL -} - -static string load_bulletin_from_file(const char* cache_filename) -{ - // Clear the bulletin - ifstream f(cache_filename); - if (not f.good()) { - return {}; - } - - stringstream ss; - ss << f.rdbuf(); - f.close(); - - return ss.str(); -} - -ClockTAI::ClockTAI(const std::vector<std::string>& bulletin_urls) : - RemoteControllable("clocktai") -{ - RC_ADD_PARAMETER(expiry, "Number of seconds until TAI Bulletin expires"); - - if (bulletin_urls.empty()) { - etiLog.level(debug) << "Initialising default TAI Bulletin URLs"; - for (const auto url : default_tai_urls) { - m_bulletin_urls.push_back(url); - } - } - else { - etiLog.level(debug) << "Initialising user-configured TAI Bulletin URLs"; - m_bulletin_urls = bulletin_urls; - } - - for (const auto url : m_bulletin_urls) { - etiLog.level(info) << "TAI Bulletin URL: '" << url << "'"; - } -} - -int ClockTAI::get_valid_offset() -{ - int offset = 0; - bool offset_valid = false; - - std::unique_lock<std::mutex> lock(m_data_mutex); - - const auto state = parse_bulletin(m_bulletin); - if (state.usable()) { -#if TEST - etiLog.level(info) << "Bulletin already valid"; -#endif - offset = state.offset; - offset_valid = true; - } - else { - const auto cache_bulletin = load_bulletin_from_file(tai_cache_location); - const auto cache_state = parse_bulletin(cache_bulletin); - - if (cache_state.usable()) { - m_bulletin = cache_bulletin; - offset = cache_state.offset; - offset_valid = true; -#if TEST - etiLog.level(info) << "Bulletin from cache valid with offset=" << offset; -#endif - } - else { - for (const auto url : m_bulletin_urls) { - try { -#if TEST - etiLog.level(info) << "Load bulletin from " << url; -#endif - const auto new_bulletin = download_tai_utc_bulletin(url.c_str()); - const auto new_state = parse_bulletin(new_bulletin); - if (new_state.usable()) { - m_bulletin = new_bulletin; - offset = new_state.offset; - offset_valid = true; - - etiLog.level(debug) << "Loaded valid TAI Bulletin from " << - url << " giving offset=" << offset; - } - else { - etiLog.level(debug) << "Skipping invalid TAI bulletin from " - << url; - } - } - catch (const runtime_error& e) { - etiLog.level(warn) << - "TAI-UTC offset could not be retrieved from " << - url << " : " << e.what(); - } - - if (offset_valid) { - update_cache(tai_cache_location); - break; - } - } - } - } - - if (offset_valid) { - // With the current evolution of the offset, we're probably going - // to reach 500 long after DAB gets replaced by another standard. - if (offset < 0 or offset > 500) { - stringstream ss; - ss << "TAI offset " << offset << " out of range"; - throw range_error(ss.str()); - } - - return offset; - } - else { - // Try again later - throw download_failed(); - } -} - - -int ClockTAI::get_offset() -{ - using namespace std::chrono; - const auto time_now = system_clock::now(); - - std::unique_lock<std::mutex> lock(m_data_mutex); - - if (not m_offset_valid) { -#ifdef TEST - // Assume we've downloaded it in the past: - - m_offset = 37; // Valid in early 2017 - m_offset_valid = true; - - // Simulate requiring a new download - m_bulletin_download_time = time_now - hours(24 * 40); -#else - // First time we run we must block until we know - // the offset - lock.unlock(); - try { - m_offset = get_valid_offset(); - } - catch (const download_failed&) { - throw runtime_error("Unable to download TAI bulletin"); - } - lock.lock(); - m_offset_valid = true; - m_bulletin_download_time = time_now; -#endif - etiLog.level(info) << - "Initialised TAI-UTC offset to " << m_offset << "s."; - } - - if (time_now - m_bulletin_download_time > hours(24 * 31)) { - // Refresh if it's older than one month. Leap seconds are - // announced several months in advance - etiLog.level(debug) << "Trying to refresh TAI bulletin"; - - if (m_offset_future.valid()) { - auto state = m_offset_future.wait_for(seconds(0)); - switch (state) { - case future_status::ready: - try { - m_offset = m_offset_future.get(); - m_offset_valid = true; - m_bulletin_download_time = time_now; - - etiLog.level(info) << - "Updated TAI-UTC offset to " << m_offset << "s."; - } - catch (const download_failed&) { - etiLog.level(warn) << - "TAI-UTC download failed, will retry in " << - download_retry_interval_hours << " hour(s)"; - - m_bulletin_download_time += hours(download_retry_interval_hours); - } -#ifdef TEST - wait_longer = false; -#endif - break; - - case future_status::deferred: - case future_status::timeout: - // Not ready yet -#ifdef TEST - etiLog.level(debug) << " async not ready yet"; -#endif - break; - } - } - else { -#ifdef TEST - etiLog.level(debug) << " Launch async"; -#endif - m_offset_future = async(launch::async, &ClockTAI::get_valid_offset, this); - } - } - - return m_offset; -} - -#if SUPPORT_SETTING_CLOCK_TAI -int ClockTAI::update_local_tai_clock(int offset) -{ - struct timex timex_request; - timex_request.modes = ADJ_TAI; - timex_request.constant = offset; - - int err = adjtimex(&timex_request); - if (err == -1) { - perror("adjtimex"); - } - - printf("adjtimex: %d, tai %d\n", err, timex_request.tai); - - return err; -} -#endif - -void ClockTAI::update_cache(const char* cache_filename) -{ - ofstream f(cache_filename); - if (not f.good()) { - throw runtime_error("TAI-UTC bulletin open cache for writing"); - } - - f << m_bulletin; - f.close(); -} - - -void ClockTAI::set_parameter(const string& parameter, const string& value) -{ - if (parameter == "expiry") { - throw ParameterError("Parameter '" + parameter + - "' is not read-only in controllable " + get_rc_name()); - } - else { - throw ParameterError("Parameter '" + parameter + - "' is not exported by controllable " + get_rc_name()); - } -} - -const string ClockTAI::get_parameter(const string& parameter) const -{ - if (parameter == "expiry") { - std::unique_lock<std::mutex> lock(m_data_mutex); - const int64_t expiry = parse_bulletin(m_bulletin).expiry; - if (expiry > 0) { - return to_string(expiry); - } - else { - return "Bulletin expired or invalid!"; - } - } - else { - throw ParameterError("Parameter '" + parameter + - "' is not exported by controllable " + get_rc_name()); - } -} - -#if 0 -// Example testing code -void debug_tai_clk() -{ - struct timespec rt_clk; - - int err = clock_gettime(CLOCK_REALTIME, &rt_clk); - if (err) { - perror("REALTIME clock_gettime failed"); - } - - struct timespec tai_clk; - - err = clock_gettime(CLOCK_TAI, &tai_clk); - if (err) { - perror("TAI clock_gettime failed"); - } - - printf("RT - TAI = %ld\n", rt_clk.tv_sec - tai_clk.tv_sec); - - - struct timex timex_request; - timex_request.modes = 0; // Do not set anything - - err = adjtimex(&timex_request); - if (err == -1) { - perror("adjtimex"); - } - - printf("adjtimex: %d, tai %d\n", err, timex_request.tai); -} -#endif - -#if TEST -int main(int argc, char **argv) -{ - using namespace std; - - ClockTAI tai({}); - - while (wait_longer) { - try { - etiLog.level(info) << - "Offset is " << tai.get_offset(); - } - catch (const exception &e) { - etiLog.level(error) << - "Exception " << e.what(); - } - - this_thread::sleep_for(chrono::seconds(2)); - } - - return 0; -} -#endif - |