| /** |
| * @file tz2alt.c |
| * @note Copyright (C) 2021 Richard Cochran <richardcochran@gmail.com> |
| * @note SPDX-License-Identifier: GPL-2.0+ |
| */ |
| #include <errno.h> |
| #include <stdbool.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <time.h> |
| #include <unistd.h> |
| |
| #include "config.h" |
| #include "lstab.h" |
| #include "pmc_common.h" |
| #include "print.h" |
| #include "sad.h" |
| #include "version.h" |
| #include "tz.h" |
| |
| #define DEFAULT_TZ "PST8PDT" |
| #define DEFAULT_PERIOD 3600 |
| #define DEFAULT_WINDOW (3600 * 24 * 30 * 3) |
| |
| static int key_field, period = DEFAULT_PERIOD, window = DEFAULT_WINDOW; |
| static char uds_local[MAX_IFNAME_SIZE + 1]; |
| static struct lstab *lstab; |
| static struct config *cfg; |
| |
| struct tzinfo { |
| const char *name; |
| char display_name[MAX_TZ_DISPLAY_NAME + 1]; |
| time_t timestamp; |
| |
| /* Following fields populated by get_offsets. */ |
| |
| time_t local_utc_offset; |
| time_t local_tai_offset; |
| int tai_utc_offset; |
| enum lstab_result tai_result; |
| }; |
| |
| static int get_offsets(struct tzinfo *tz); |
| |
| static bool offsets_equal(struct tzinfo *a, struct tzinfo *b) |
| { |
| return a->local_utc_offset == b->local_utc_offset; |
| } |
| |
| static bool find_next_discontinuity(struct tzinfo *tz, struct tzinfo *next) |
| { |
| time_t i, j, n; |
| bool gt; |
| |
| next->timestamp = tz->timestamp + window; |
| get_offsets(next); |
| if (offsets_equal(tz, next)) { |
| return false; |
| } |
| |
| i = 0; |
| j = window; |
| |
| while (1) { |
| next->timestamp = tz->timestamp + i; |
| get_offsets(next); |
| gt = offsets_equal(tz, next); |
| if (gt) { |
| n = j - i - 1; |
| } else { |
| j = i; |
| i = 0; |
| n = j - i - 1; |
| } |
| if (!n) { |
| if (gt) { |
| next->timestamp++; |
| get_offsets(next); |
| } |
| break; |
| } |
| i += (n + 1) / 2; |
| } |
| |
| return true; |
| } |
| |
| static int get_offsets(struct tzinfo *tz) |
| { |
| struct tm tm = {0}; |
| time_t t2; |
| |
| tz->tai_result = lstab_utc2tai(lstab, tz->timestamp, |
| &tz->tai_utc_offset); |
| if (tz->tai_result == LSTAB_UNKNOWN) { |
| pr_err("leap second table is stale"); |
| return -1; |
| } |
| |
| setenv("TZ", tz->name, 1); |
| tzset(); |
| if (!localtime_r(&tz->timestamp, &tm)) { |
| return -1; |
| } |
| |
| setenv("TZ", "UTC", 1); |
| tzset(); |
| t2 = mktime(&tm); |
| tz->local_utc_offset = t2 - tz->timestamp; |
| tz->local_tai_offset = tz->local_utc_offset - tz->tai_utc_offset; |
| |
| return 0; |
| } |
| |
| static int get_unambiguous_time(struct tzinfo *tz) |
| { |
| int err; |
| |
| do { |
| tz->timestamp = time(NULL); |
| err = get_offsets(tz); |
| } while (tz->tai_result == LSTAB_AMBIGUOUS); |
| |
| return err; |
| } |
| |
| static void show_timezone_info(const char *label, struct tzinfo *tz) |
| { |
| pr_debug("%s %s ts %ld local-utc %ld tai-utc %d local-tai %ld %s", |
| label, |
| tz->name, |
| tz->timestamp, |
| tz->local_utc_offset, |
| tz->tai_utc_offset, |
| tz->local_tai_offset, |
| tz->tai_result == LSTAB_OK ? "valid" : "invalid"); |
| } |
| |
| /* Returns true if display name was truncated. */ |
| static bool tz_set_name(struct tzinfo *tz, const char *name) |
| { |
| const char *suffix; |
| int len; |
| |
| memset(tz->display_name, 0, sizeof(tz->display_name)); |
| tz->name = name; |
| |
| len = strlen(name); |
| if (len <= MAX_TZ_DISPLAY_NAME) { |
| strncpy(tz->display_name, name, sizeof(tz->display_name) - 1); |
| return false; |
| } |
| |
| /* |
| * The displayName field is limited to 10 characters, but |
| * there are many valid time zone names like "Europe/Vienna". |
| * Use the suffix if present. |
| */ |
| suffix = strchr(name, '/'); |
| if (suffix) { |
| suffix++; |
| len = strlen(suffix); |
| if (len > 0 && len <= MAX_TZ_DISPLAY_NAME) { |
| strncpy(tz->display_name, suffix, |
| sizeof(tz->display_name) - 1); |
| return true; |
| } |
| } |
| |
| /* No nice suffix to be found, so just truncate. */ |
| strncpy(tz->display_name, name, sizeof(tz->display_name) - 1); |
| |
| return true; |
| } |
| |
| static int update_ptp_serivce(struct tzinfo *tz, struct tzinfo *next) |
| { |
| struct alternate_time_offset_properties atop; |
| struct management_tlv_datum mtd; |
| uint64_t time_of_next_jump; |
| struct pmc *pmc; |
| int err; |
| |
| pmc = pmc_create(cfg, TRANS_UDS, uds_local, |
| config_get_string(cfg, NULL, "uds_address"), 0, |
| config_get_int(cfg, NULL, "domainNumber"), |
| config_get_int(cfg, NULL, "transportSpecific") << 4, 0, 1); |
| if (!pmc) { |
| return -1; |
| } |
| err = pmc_send_set_aton(pmc, MID_ALTERNATE_TIME_OFFSET_NAME, |
| key_field, tz->display_name); |
| if (err) { |
| return err; |
| } |
| memset(&atop, 0, sizeof(atop)); |
| atop.keyField = key_field; |
| atop.currentOffset = tz->local_tai_offset; |
| if (next) { |
| atop.jumpSeconds = next->local_tai_offset - tz->local_tai_offset; |
| time_of_next_jump = (uint64_t) next->timestamp; |
| atop.timeOfNextJump.seconds_lsb = time_of_next_jump & 0xffffffff; |
| atop.timeOfNextJump.seconds_msb = time_of_next_jump >> 32; |
| } |
| err = pmc_send_set_action(pmc, MID_ALTERNATE_TIME_OFFSET_PROPERTIES, |
| &atop, sizeof(atop)); |
| if (err) { |
| return err; |
| } |
| mtd.val = key_field; |
| mtd.reserved = 1; /*enable field*/ |
| err = pmc_send_set_action(pmc, MID_ALTERNATE_TIME_OFFSET_ENABLE, |
| &mtd, sizeof(mtd)); |
| if (err) { |
| return err; |
| } |
| |
| pmc_destroy(pmc); |
| return 0; |
| } |
| |
| static int do_tztool(const char *timezone) |
| { |
| struct tzinfo nx, tz; |
| const char *leapfile; |
| bool pending; |
| char buf[64]; |
| int err; |
| |
| if (key_field > MAX_TIME_ZONES - 1) { |
| pr_err("key field %d exceeds maximum of %d", key_field, |
| MAX_TIME_ZONES - 1); |
| return -1; |
| } |
| |
| tz_set_name(&nx, timezone); |
| if (tz_set_name(&tz, timezone)) { |
| pr_info("truncating time zone display name from %s to %s", |
| tz.name, tz.display_name); |
| } |
| |
| leapfile = config_get_string(cfg, NULL, "leapfile"); |
| if (!leapfile) { |
| pr_err("please specify leap second table with --leapfile"); |
| return -1; |
| } |
| |
| while (is_running()) { |
| |
| /* Read the leap seconds file again as it may have changed. */ |
| lstab = lstab_create(leapfile); |
| if (!lstab) { |
| pr_err("failed to create leap second table"); |
| return -1; |
| } |
| |
| err = get_unambiguous_time(&tz); |
| if (err) { |
| return err; |
| } |
| show_timezone_info("current time = ", &tz); |
| |
| pending = find_next_discontinuity(&tz, &nx); |
| if (pending) { |
| setenv("TZ", nx.name, 1); |
| tzset(); |
| if (ctime_r(&nx.timestamp, buf)) { |
| buf[strlen(buf) - 1] = 0; |
| } |
| show_timezone_info("discontinuity = ", &nx); |
| pr_info("next discontinuity %s %s", buf, nx.name); |
| } else { |
| pr_info("no discontinuity within %d second window", window); |
| } |
| |
| lstab_destroy(lstab); |
| lstab = NULL; |
| |
| err = update_ptp_serivce(&tz, pending ? &nx : NULL); |
| if (err) { |
| pr_err("failed to update PTP service"); |
| return err; |
| } |
| sleep(period); |
| } |
| return 0; |
| } |
| |
| static void usage(char *progname) |
| { |
| fprintf(stderr, |
| "\nusage: %s [options]\n\n" |
| " -f [file] read configuration from 'file'\n" |
| " -h prints this message and exits\n" |
| " -k [num] key field for the ALTERNATE_TIME_OFFSET_INDICATOR TLV\n" |
| " -l [num] set the logging level to 'num'\n" |
| " -p [num] period between updates in seconds, default %d\n" |
| " -v prints the software version and exits\n" |
| " -w [num] look ahead time window in seconds, default %d\n" |
| " -z zone Time zone string, default '%s'\n" |
| " See /usr/share/zoneinfo for valid strings\n" |
| "\n", |
| progname, DEFAULT_PERIOD, DEFAULT_WINDOW, DEFAULT_TZ); |
| } |
| |
| int main(int argc, char *argv[]) |
| { |
| char *config = NULL, *progname, *timezone = DEFAULT_TZ; |
| int c, cmd_line_print_level, err = 0, index; |
| struct option *opts; |
| |
| if (handle_term_signals()) { |
| return -1; |
| } |
| cfg = config_create(); |
| if (!cfg) { |
| return -1; |
| } |
| opts = config_long_options(cfg); |
| print_set_verbose(1); |
| print_set_syslog(0); |
| |
| /* Process the command line arguments. */ |
| progname = strrchr(argv[0], '/'); |
| progname = progname ? 1+progname : argv[0]; |
| while (EOF != (c = getopt_long(argc, argv, "f:hk:l:p:vw:z:", opts, &index))) { |
| switch (c) { |
| case 0: |
| if (config_parse_option(cfg, opts[index].name, optarg)) { |
| config_destroy(cfg); |
| return -1; |
| } |
| break; |
| case 'f': |
| config = optarg; |
| break; |
| case 'k': |
| key_field = atoi(optarg); |
| break; |
| case 'l': |
| if (get_arg_val_i(c, optarg, &cmd_line_print_level, |
| PRINT_LEVEL_MIN, PRINT_LEVEL_MAX)) |
| return -1; |
| config_set_int(cfg, "logging_level", cmd_line_print_level); |
| break; |
| case 'p': |
| period = atoi(optarg); |
| break; |
| case 'v': |
| version_show(stdout); |
| config_destroy(cfg); |
| return 0; |
| case 'w': |
| window = atoi(optarg); |
| break; |
| case 'z': |
| timezone = optarg; |
| break; |
| case 'h': |
| usage(progname); |
| config_destroy(cfg); |
| return 0; |
| case '?': |
| default: |
| usage(progname); |
| config_destroy(cfg); |
| return -1; |
| } |
| } |
| |
| print_set_syslog(0); |
| print_set_verbose(1); |
| print_set_level(config_get_int(cfg, NULL, "logging_level")); |
| |
| if (config && (err = config_read(config, cfg))) { |
| goto out; |
| } |
| |
| if (sad_create(cfg)) { |
| goto out; |
| } |
| |
| print_set_progname(progname); |
| print_set_tag(config_get_string(cfg, NULL, "message_tag")); |
| print_set_level(config_get_int(cfg, NULL, "logging_level")); |
| snprintf(uds_local, sizeof(uds_local), "tztool.%d", getpid()); |
| |
| err = do_tztool(timezone); |
| out: |
| sad_destroy(cfg); |
| config_destroy(cfg); |
| return err; |
| } |