diff --git a/incdefs.sh b/incdefs.sh index 5bbdcea..34e227f 100755 --- a/incdefs.sh +++ b/incdefs.sh @@ -19,20 +19,34 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # -# Look for the clock_adjtime functional prototype in the C library. +# Look for functional prototypes in the C library. # user_flags() { # Needed for vasprintf(). printf " -D_GNU_SOURCE" + # Get list of directories searched for header files. dirs=$(echo "" | ${CROSS_COMPILE}cpp -Wp,-v 2>&1 >/dev/null | grep ^" /") + + # Look for clock_adjtime(). for d in $dirs; do files=$(find $d -type f -name time.h) for f in $files; do if grep -q clock_adjtime $f; then printf " -DHAVE_CLOCK_ADJTIME" - return + break 2 + fi + done + done + + # Look for posix_spawn(). + for d in $dirs; do + files=$(find $d -type f -name spawn.h) + for f in $files; do + if grep -q posix_spawn $f; then + printf " -DHAVE_POSIX_SPAWN" + break 2 fi done done diff --git a/makefile b/makefile index 74a7fe2..5469301 100644 --- a/makefile +++ b/makefile @@ -22,13 +22,14 @@ CC = $(CROSS_COMPILE)gcc VER = -DVER=$(version) CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS) LDLIBS = -lm -lrt $(EXTRA_LDFLAGS) -PRG = ptp4l pmc phc2sys hwstamp_ctl phc_ctl +PRG = ptp4l pmc phc2sys hwstamp_ctl phc_ctl timemaster OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o fault.o \ filter.o fsm.o linreg.o mave.o mmedian.o msg.o ntpshm.o phc.o \ pi.o port.o print.o ptp4l.o raw.o servo.o sk.o stats.o tlv.o \ transport.o udp.o udp6.o uds.o util.o version.o -OBJECTS = $(OBJ) hwstamp_ctl.o phc2sys.o pmc.o pmc_common.o sysoff.o phc_ctl.o +OBJECTS = $(OBJ) hwstamp_ctl.o phc2sys.o phc_ctl.o pmc.o pmc_common.o \ + sysoff.o timemaster.o SRC = $(OBJECTS:.o=.c) DEPEND = $(OBJECTS:.o=.d) srcdir := $(dir $(lastword $(MAKEFILE_LIST))) @@ -56,6 +57,8 @@ hwstamp_ctl: hwstamp_ctl.o version.o phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o +timemaster: print.o sk.o timemaster.o util.o version.o + version.o: .version version.sh $(filter-out version.d,$(DEPEND)) .version: force diff --git a/timemaster.8 b/timemaster.8 new file mode 100644 index 0000000..9a3ddb4 --- /dev/null +++ b/timemaster.8 @@ -0,0 +1,335 @@ +.TH TIMEMASTER 8 "October 2014" "linuxptp" +.SH NAME + +timemaster \- run NTP with PTP as reference clocks + +.SH SYNOPSIS + +.B timemaster +[ +.B \-nmqv +] [ +.BI \-l " print-level" +] +.BI \-f " file" + +.SH DESCRIPTION +\fBtimemaster\fR is a program that uses \fBptp4l\fR and \fBphc2sys\fR in +combination with \fBchronyd\fR or \fBntpd\fR to synchronize the system clock to +NTP and PTP time sources. The PTP time is provided by \fBphc2sys\fR and +\fBptp4l\fR via SHM reference clocks to \fBchronyd\fR/\fBntpd\fR, which +can compare all time sources and use the best sources to synchronize the system +clock. + +On start, \fBtimemaster\fR reads a configuration file that specifies the NTP +and PTP time sources, checks which network interfaces have and share a PTP +hardware clock (PHC), generates configuration files for \fBptp4l\fR and +\fBchronyd\fR/\fBntpd\fR, and start the \fBptp4l\fR, \fBphc2sys\fR, +\fBchronyd\fR/\fBntpd\fR processes as needed. Then, it waits for a signal to +kill the processes, remove the generated configuration files and exit. + +.SH OPTIONS + +.TP +.BI \-f " file" +Specify the path to the \fBtimemaster\fR configuration file. +.TP +.BI \-n +Don't start the programs, only print their configuration files and the commands +that would be executed if this option wasn't specified. +.TP +.BI \-l " level" +Set the maximum syslog level of messages which should be printed or sent to +the system logger. The default value is 6 (LOG_INFO). +.TP +.B \-m +Print messages to the standard output. +.TP +.B \-q +Don't send messages to the system logger. +.TP +.B \-v +Print the software version and exit. +.TP +.BI \-h +Display a help message and exit. + +.SH CONFIGURATION FILE + +The configuration file is divided into sections. Each section starts with a +line containing its name enclosed in brackets and it follows with settings. +Each setting is placed on a separate line, it contains the name of the +option and the value separated by whitespace characters. Empty lines and lines +starting with # are ignored. + +Sections that can used in the configuration file and options that can be set in +them are described below. + +.SS [timemaster] + +.TP +.B ntp_program +Select which NTP implementation should be used. Possible values are +\fBchronyd\fR and \fBntpd\fR. The default value is \fBchronyd\fR. Limitations +of the implementations relevant to the timemaster configuration are listed in +\fBNOTES\fR. + +.TP +.B rundir +Specify the directory where should be generated \fBchronyd\fR, \fBntpd\fR and +\fBptp4l\fR configuration files and sockets. The directory will be created if +it doesn't exist. The default value is \fB/var/run/timemaster\fR. + +.SS [ntp_server address] + +The \fBntp_server\fR section specifies an NTP server that should be used as a +time source. The address of the server is included in the name of the section. + +.TP +.B minpoll +.TQ +.B maxpoll +Specify the minimum and maximum NTP polling interval as powers of two in +seconds. The default values are 6 (64 seconds) and 10 (1024 seconds) +respectively. Shorter polling intervals usually improve the accuracy +significantly, but they should be used only when allowed by the operators of +the NTP service (public NTP servers generally don't allow too frequent +queries). If the NTP server is located on the same LAN, polling intervals +around 4 (16 seconds) might give best accuracy. + +.TP +.B iburst +Enable or disable sending a burst of NTP packets on start to speed up the +initial synchronization. Possible values are 1 and 0. The default value is 0 +(disabled). + +.SS [ptp_domain number] + +The \fBptp_domain\fR section specifies a PTP domain that should be used as a +time source. The PTP domain number is included in the name of the section. The +\fBptp4l\fR instances are configured to run in the \fBslaveOnly\fR mode. In +this section at least the \fBinterfaces\fR option needs to be set, other +options are optional. + +.TP +.B interfaces +Specify which network interfaces should be used for this PTP domain. A separate +\fBptp4l\fR instance will be started for each group of interfaces sharing the +same PHC and for each interface that supports only SW time stamping. HW time +stamping is enabled automatically. If an interface with HW time stamping is +specified also in other PTP domains, only the \fBptp4l\fR instance from the +first PTP domain will be using HW time stamping. + +.TP +.B ntp_poll +Specify the polling interval of the NTP SHM reference clock reading samples +from \fBptp4l\fR or \fBphc2sys\fR. It's specified as a power of two in seconds. +The default value is 2 (4 seconds). + +.TP +.B phc2sys_poll +Specify the polling interval used by \fBphc2sys\fR to read a PTP clock +synchronized by \fBptp4l\fR and update the SHM sample for +\fBchronyd\fR/\fBntpd\fR. It's specified as a power of two in seconds. The +default value is 0 (1 second). + +.TP +.B delay +Specify the maximum assumed roundtrip delay to the primary source of the time +in this PTP domain. This value is included in the distance used by +\fBchronyd\fR in the source selection algorithm to detect falsetickers and +assign weights for source combining. The default value is 1e-4 (100 +microseconds). With \fBntpd\fR, the \fBtos mindist\fR command can be used to +set a limit with similar purpose globally for all time sources. + +.TP +.B ptp4l_option +Specify an extra \fBptp4l\fR option specific to this PTP domain that should be +added to the configuration files generated for \fBptp4l\fR. This option may be +used multiple times in one \fBptp_domain\fR section. + +.SS [chronyd] + +.TP +.B path +Specify the path to the \fBchronyd\fR binary. The default value is +\fBchronyd\fR to search for the binary in \fBPATH\fR. + +.TP +.B options +Specify extra options that should be added to the \fBchronyd\fR command line. +No extra options are added by default. + +.SS [chrony.conf] + +Settings specified in this section are copied directly to the configuration +file generated for \fBchronyd\fR. If this section is not present in the +\fBtimemaster\fR configuration file, the following setting will be added: + +.EX +makestep 1 3 +.EE + +This configures \fBchronyd\fR to step the system clock in the first three +updates if the offset is larger than 1 second. + +.SS [ntpd] + +.TP +.B path +Specify the path to the \fBntpd\fR binary. The default value is \fBntpd\fR to +search for the binary in \fBPATH\fR. + +.TP +.B options +Specify extra options that should be added to the \fBntpd\fR command line. No +extra options are added by default. + +.SS [ntp.conf] + +Settings specified in this section are copied directly to the configuration +file generated for \fBntpd\fR. If this section is not present in the +\fBtimemaster\fR configuration file, the following settings will be added: + +.EX +restrict default nomodify notrap nopeer noquery +restrict 127.0.0.1 +restrict ::1 +.EE + +This configures \fBntpd\fR to use safe default restrictions. + +.SS [phc2sys] + +.TP +.B path +Specify the path to the \fBphc2sys\fR binary. The default value is +\fBphc2sys\fR to search for the binary in \fBPATH\fR. + +.TP +.B options +Specify extra options that should be added to all \fBphc2sys\fR command lines. +By default, \fB-l 5\fR is added to the command lines. + +.SS [ptp4l] + +.TP +.B path +Specify the path to the \fBptp4l\fR binary. The default value is \fBptp4l\fR to +search for the binary in \fBPATH\fR. + +.TP +.B options +Specify extra options that should be added to all \fBptp4l\fR command lines. By +default, \fB-l 5\fR is added to the command lines. + +.SS [ptp4l.conf] +Settings specified in this section are copied directly to the configuration +files generated for all \fBptp4l\fR instances. There is no default content of +this section. + +.SH NOTES +For best accuracy, \fBchronyd\fR is usually preferred over \fBntpd\fR, it also +synchronizes the system clock faster. Both NTP implementations, however, have +some limitations that need to be considered before choosing the one to be used +in a given \fBtimemaster\fR configuration. + +The \fBchronyd\fR limitations are: + +.RS +In version 1.31 and older, the maximum number of reference clocks used at the +same time is 8. This limits the number of PHCs and interfaces using SW time +stamping that can be used for PTP. + +Using polling intervals (\fBminpoll\fR, \fBmaxpoll\fR, \fBntp_poll\fR options) +shorter than 2 (4 seconds) is not recommended with versions before 1.30. With +1.30 and later values of 0 or 1 can be used for NTP sources and negative values +for PTP sources (\fBntp_poll\fR) to specify a subsecond interval. +.RE + +The \fBntpd\fR limitations are: + +.RS +Only the first two shared-memory segments created by the SHM refclock driver +in \fBntpd\fR have owner-only access. Other segments are created with world +access, possibly allowing any user on the system writing to the segments and +disrupting the synchronization. + +The shortest polling interval for all sources is 3 (8 seconds). + +Nanosecond resolution in the SHM refclock driver is supported in version +4.2.7p303 and later, older versions have only microsecond resolution. +.RE + +.SH EXAMPLES + +A minimal configuration file using one NTP source and two PTP sources would be: + +.EX +[ntp_server 10.1.1.1] + +[ptp_domain 0] +interfaces eth0 + +[ptp_domain 1] +interfaces eth1 +.EE + +A more complex example using all \fBtimemaster\fR options would be: + +.EX +[ntp_server 10.1.1.1] +minpoll 3 +maxpoll 4 +iburst 1 + +[ptp_domain 0] +interfaces eth0 eth1 +ntp_poll 0 +phc2sys_poll -2 +delay 10e-6 +ptp4l_option clock_servo linreg +ptp4l_option delay_mechanism P2P + +[timemaster] +ntp_program chronyd +rundir /var/run/timemaster + +[chronyd] +path /usr/sbin/chronyd +options + +[chrony.conf] +makestep 1 3 +logchange 0.5 +rtcsync +driftfile /var/lib/chrony/drift + +[ntpd] +path /usr/sbin/ntpd +options -u ntp:ntp + +[ntp.conf] +restrict default nomodify notrap nopeer noquery +restrict 127.0.0.1 +restrict ::1 +driftfile /var/lib/ntp/drift + +[phc2sys] +path /usr/sbin/phc2sys +options -l 5 + +[ptp4l] +path /usr/sbin/ptp4l +options + +[ptp4l.conf] +logging_level 5 +.EE + +.SH SEE ALSO + +.BR chronyd (8), +.BR ntpd (8), +.BR phc2sys (8), +.BR ptp4l (8) diff --git a/timemaster.c b/timemaster.c new file mode 100644 index 0000000..83a5b83 --- /dev/null +++ b/timemaster.c @@ -0,0 +1,1173 @@ +/** + * @file timemaster.c + * @brief Program to run NTP with PTP as reference clocks. + * @note Copyright (C) 2014 Miroslav Lichvar + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "print.h" +#include "sk.h" +#include "util.h" +#include "version.h" + +#define DEFAULT_RUNDIR "/var/run/timemaster" + +#define DEFAULT_NTP_PROGRAM CHRONYD +#define DEFAULT_NTP_MINPOLL 6 +#define DEFAULT_NTP_MAXPOLL 10 +#define DEFAULT_PTP_DELAY 1e-4 +#define DEFAULT_PTP_NTP_POLL 2 +#define DEFAULT_PTP_PHC2SYS_POLL 0 + +#define DEFAULT_CHRONYD_SETTINGS \ + "makestep 1 3" +#define DEFAULT_NTPD_SETTINGS \ + "restrict default nomodify notrap nopeer noquery", \ + "restrict 127.0.0.1", \ + "restrict ::1" +#define DEFAULT_PTP4L_OPTIONS "-l", "5" +#define DEFAULT_PHC2SYS_OPTIONS "-l", "5" + +enum source_type { + NTP_SERVER, + PTP_DOMAIN, +}; + +enum ntp_program { + CHRONYD, + NTPD, +}; + +struct ntp_server { + char *address; + int minpoll; + int maxpoll; + int iburst; +}; + +struct ptp_domain { + int domain; + int ntp_poll; + int phc2sys_poll; + double delay; + char **interfaces; + char **ptp4l_settings; +}; + +struct source { + enum source_type type; + union { + struct ntp_server ntp; + struct ptp_domain ptp; + }; +}; + +struct program_config { + char *path; + char **options; + char **settings; +}; + +struct timemaster_config { + struct source **sources; + enum ntp_program ntp_program; + char *rundir; + struct program_config chronyd; + struct program_config ntpd; + struct program_config phc2sys; + struct program_config ptp4l; +}; + +struct config_file { + char *path; + char *content; +}; + +struct script { + struct config_file **configs; + char ***commands; +}; + +static void free_parray(void **a) +{ + void **p; + + for (p = a; *p; p++) + free(*p); + free(a); +} + +static void extend_string_array(char ***a, char **strings) +{ + char **s; + + for (s = strings; *s; s++) + parray_append((void ***)a, strdup(*s)); +} + +static void extend_config_string(char **s, char **lines) +{ + for (; *lines; lines++) + string_appendf(s, "%s\n", *lines); +} + +static int parse_bool(char *s, int *b) +{ + if (get_ranged_int(s, b, 0, 1) != PARSED_OK) + return 1; + + return 0; +} + +static int parse_int(char *s, int *i) +{ + if (get_ranged_int(s, i, INT_MIN, INT_MAX) != PARSED_OK) + return 1; + + return 0; +} + +static int parse_double(char *s, double *d) +{ + if (get_ranged_double(s, d, INT_MIN, INT_MAX) != PARSED_OK) + return 1; + + return 0; +} + +static char *parse_word(char *s) +{ + while (*s && !isspace(*s)) + s++; + while (*s && isspace(*s)) + *(s++) = '\0'; + return s; +} + +static void parse_words(char *s, char ***a) +{ + char *w; + + if (**a) { + free_parray((void **)(*a)); + *a = (char **)parray_new(); + } + while (*s) { + w = s; + s = parse_word(s); + parray_append((void ***)a, strdup(w)); + } +} + +static void replace_string(char *s, char **str) +{ + if (*str) + free(*str); + *str = strdup(s); +} + +static char *parse_section_name(char *s) +{ + char *s1, *s2; + + s1 = s + 1; + for (s2 = s1; *s2 && *s2 != ']'; s2++) + ; + *s2 = '\0'; + + return strdup(s1); +} + +static void parse_setting(char *s, char **name, char **value) +{ + *name = s; + for (*value = s; **value && !isspace(**value); (*value)++) + ; + for (; **value && !isspace(**value); (*value)++) + ; + for (; **value && isspace(**value); (*value)++) + **value = '\0'; +} + +static void source_destroy(struct source *source) +{ + switch (source->type) { + case NTP_SERVER: + free(source->ntp.address); + break; + case PTP_DOMAIN: + free_parray((void **)source->ptp.interfaces); + free_parray((void **)source->ptp.ptp4l_settings); + break; + } + free(source); +} + +static struct source *source_ntp_parse(char *parameter, char **settings) +{ + char *name, *value; + struct ntp_server ntp_server; + struct source *source; + int r = 0; + + if (!*parameter) { + pr_err("missing address for ntp_server"); + return NULL; + } + + ntp_server.address = parameter; + ntp_server.minpoll = DEFAULT_NTP_MINPOLL; + ntp_server.maxpoll = DEFAULT_NTP_MAXPOLL; + ntp_server.iburst = 0; + + for (; *settings; settings++) { + parse_setting(*settings, &name, &value); + if (!strcasecmp(name, "minpoll")) { + r = parse_int(value, &ntp_server.minpoll); + } else if (!strcasecmp(name, "maxpoll")) { + r = parse_int(value, &ntp_server.maxpoll); + } else if (!strcasecmp(name, "iburst")) { + r = parse_bool(value, &ntp_server.iburst); + } else { + pr_err("unknown ntp_server setting %s", name); + return NULL; + } + if (r) { + pr_err("invalid value %s for %s", value, name); + return NULL; + } + } + + source = malloc(sizeof(*source)); + source->type = NTP_SERVER; + source->ntp = ntp_server; + source->ntp.address = strdup(source->ntp.address); + + return source; +} + +static struct source *source_ptp_parse(char *parameter, char **settings) +{ + char *name, *value; + struct source *source; + int r = 0; + + source = malloc(sizeof(*source)); + source->type = PTP_DOMAIN; + source->ptp.delay = DEFAULT_PTP_DELAY; + source->ptp.ntp_poll = DEFAULT_PTP_NTP_POLL; + source->ptp.phc2sys_poll = DEFAULT_PTP_PHC2SYS_POLL; + source->ptp.interfaces = (char **)parray_new(); + source->ptp.ptp4l_settings = (char **)parray_new(); + + if (parse_int(parameter, &source->ptp.domain)) { + pr_err("invalid ptp_domain number %s", parameter); + goto failed; + } + + for (; *settings; settings++) { + parse_setting(*settings, &name, &value); + if (!strcasecmp(name, "delay")) { + r = parse_double(value, &source->ptp.delay); + } else if (!strcasecmp(name, "ntp_poll")) { + r = parse_int(value, &source->ptp.ntp_poll); + } else if (!strcasecmp(name, "phc2sys_poll")) { + r = parse_int(value, &source->ptp.phc2sys_poll); + } else if (!strcasecmp(name, "ptp4l_option")) { + parray_append((void ***)&source->ptp.ptp4l_settings, + strdup(value)); + } else if (!strcasecmp(name, "interfaces")) { + parse_words(value, &source->ptp.interfaces); + } else { + pr_err("unknown ptp_domain setting %s", name); + goto failed; + } + + if (r) { + pr_err("invalid value %s for %s", value, name); + goto failed; + } + } + + if (!*source->ptp.interfaces) { + pr_err("no interfaces specified for ptp_domain %d", + source->ptp.domain); + goto failed; + } + + return source; +failed: + source_destroy(source); + return NULL; +} + +static int parse_program_settings(char **settings, + struct program_config *config) +{ + char *name, *value; + + for (; *settings; settings++) { + parse_setting(*settings, &name, &value); + if (!strcasecmp(name, "path")) { + replace_string(value, &config->path); + } else if (!strcasecmp(name, "options")) { + parse_words(value, &config->options); + } else { + pr_err("unknown program setting %s", name); + return 1; + } + } + + return 0; +} + +static int parse_timemaster_settings(char **settings, + struct timemaster_config *config) +{ + char *name, *value; + + for (; *settings; settings++) { + parse_setting(*settings, &name, &value); + if (!strcasecmp(name, "ntp_program")) { + if (!strcasecmp(value, "chronyd")) { + config->ntp_program = CHRONYD; + } else if (!strcasecmp(value, "ntpd")) { + config->ntp_program = NTPD; + } else { + pr_err("unknown ntp program %s", value); + return 1; + } + } else if (!strcasecmp(name, "rundir")) { + replace_string(value, &config->rundir); + } else { + pr_err("unknown timemaster setting %s", name); + return 1; + } + } + + return 0; +} + +static int parse_section(char **settings, char *name, + struct timemaster_config *config) +{ + struct source *source = NULL; + char ***settings_dst = NULL; + char *parameter = parse_word(name); + + if (!strcasecmp(name, "ntp_server")) { + source = source_ntp_parse(parameter, settings); + if (!source) + return 1; + } else if (!strcasecmp(name, "ptp_domain")) { + source = source_ptp_parse(parameter, settings); + if (!source) + return 1; + } else if (!strcasecmp(name, "chrony.conf")) { + settings_dst = &config->chronyd.settings; + } else if (!strcasecmp(name, "ntp.conf")) { + settings_dst = &config->ntpd.settings; + } else if (!strcasecmp(name, "ptp4l.conf")) { + settings_dst = &config->ptp4l.settings; + } else if (!strcasecmp(name, "chronyd")) { + if (parse_program_settings(settings, &config->chronyd)) + return 1; + } else if (!strcasecmp(name, "ntpd")) { + if (parse_program_settings(settings, &config->ntpd)) + return 1; + } else if (!strcasecmp(name, "phc2sys")) { + if (parse_program_settings(settings, &config->phc2sys)) + return 1; + } else if (!strcasecmp(name, "ptp4l")) { + if (parse_program_settings(settings, &config->ptp4l)) + return 1; + } else if (!strcasecmp(name, "timemaster")) { + if (parse_timemaster_settings(settings, config)) + return 1; + } else { + pr_err("unknown section %s", name); + return 1; + } + + if (source) + parray_append((void ***)&config->sources, source); + + if (settings_dst) { + free_parray((void **)*settings_dst); + *settings_dst = (char **)parray_new(); + extend_string_array(settings_dst, settings); + } + + return 0; +} + +static void init_program_config(struct program_config *config, + const char *name, ...) +{ + const char *s; + va_list ap; + + config->path = strdup(name); + config->settings = (char **)parray_new(); + config->options = (char **)parray_new(); + + va_start(ap, name); + + /* add default options and settings */ + while ((s = va_arg(ap, const char *))) + parray_append((void ***)&config->options, strdup(s)); + while ((s = va_arg(ap, const char *))) + parray_append((void ***)&config->settings, strdup(s)); + + va_end(ap); +} + +static void free_program_config(struct program_config *config) +{ + free(config->path); + free_parray((void **)config->settings); + free_parray((void **)config->options); +} + +static void config_destroy(struct timemaster_config *config) +{ + struct source **sources; + + for (sources = config->sources; *sources; sources++) + source_destroy(*sources); + free(config->sources); + + free_program_config(&config->chronyd); + free_program_config(&config->ntpd); + free_program_config(&config->phc2sys); + free_program_config(&config->ptp4l); + + free(config->rundir); + free(config); +} + +static struct timemaster_config *config_parse(char *path) +{ + struct timemaster_config *config = calloc(1, sizeof(*config)); + FILE *f; + char buf[4096], *line, *section_name = NULL; + char **section_lines = NULL; + int ret = 0; + + config->sources = (struct source **)parray_new(); + config->ntp_program = DEFAULT_NTP_PROGRAM; + config->rundir = strdup(DEFAULT_RUNDIR); + + init_program_config(&config->chronyd, "chronyd", + NULL, DEFAULT_CHRONYD_SETTINGS, NULL); + init_program_config(&config->ntpd, "ntpd", + NULL, DEFAULT_NTPD_SETTINGS, NULL); + init_program_config(&config->phc2sys, "phc2sys", + DEFAULT_PHC2SYS_OPTIONS, NULL, NULL); + init_program_config(&config->ptp4l, "ptp4l", + DEFAULT_PTP4L_OPTIONS, NULL, NULL); + + f = fopen(path, "r"); + if (!f) { + pr_err("failed to open %s: %m", path); + free(config); + return NULL; + } + + while (fgets(buf, sizeof(buf), f)) { + /* remove trailing and leading whitespace */ + for (line = buf + strlen(buf) - 1; + line >= buf && isspace(*line); line--) + *line = '\0'; + for (line = buf; *line && isspace(*line); line++) + ; + /* skip comments and empty lines */ + if (!*line || *line == '#') + continue; + + if (*line == '[') { + /* parse previous section before starting another */ + if (section_name) { + if (parse_section(section_lines, section_name, + config)) { + ret = 1; + break; + } + free_parray((void **)section_lines); + free(section_name); + } + section_name = parse_section_name(line); + section_lines = (char **)parray_new(); + continue; + } + + if (!section_lines) { + pr_err("settings outside section"); + ret = 1; + break; + } + + parray_append((void ***)§ion_lines, strdup(line)); + } + + if (!ret && section_name && + parse_section(section_lines, section_name, config)) { + ret = 1; + } + + fclose(f); + + if (section_name) + free(section_name); + if (section_lines) + free_parray((void **)section_lines); + + if (ret) { + config_destroy(config); + return NULL; + } + + return config; +} + +static char **get_ptp4l_command(struct program_config *config, + struct config_file *file, char **interfaces, + int hw_ts) +{ + char **command = (char **)parray_new(); + + parray_append((void ***)&command, strdup(config->path)); + extend_string_array(&command, config->options); + parray_extend((void ***)&command, + strdup("-f"), strdup(file->path), + strdup(hw_ts ? "-H" : "-S"), NULL); + + for (; *interfaces; interfaces++) + parray_extend((void ***)&command, + strdup("-i"), strdup(*interfaces), NULL); + + return command; +} + +static char **get_phc2sys_command(struct program_config *config, int domain, + int poll, int shm_segment, char *uds_path) +{ + char **command = (char **)parray_new(); + + parray_append((void ***)&command, strdup(config->path)); + extend_string_array(&command, config->options); + parray_extend((void ***)&command, + strdup("-a"), strdup("-r"), + strdup("-R"), string_newf("%.2f", poll > 0 ? + 1.0 / (1 << poll) : 1 << -poll), + strdup("-z"), strdup(uds_path), + strdup("-n"), string_newf("%d", domain), + strdup("-E"), strdup("ntpshm"), + strdup("-M"), string_newf("%d", shm_segment), NULL); + + return command; +} + +static char *get_refid(char *prefix, unsigned int number) +{ + if (number < 10) + return string_newf("%.3s%u", prefix, number); + else if (number < 100) + return string_newf("%.2s%u", prefix, number); + else if (number < 1000) + return string_newf("%.1s%u", prefix, number); + return NULL; +}; + +static void add_shm_source(int shm_segment, int poll, int dpoll, double delay, + char *prefix, struct timemaster_config *config, + char **ntp_config) +{ + char *refid = get_refid(prefix, shm_segment); + + switch (config->ntp_program) { + case CHRONYD: + string_appendf(ntp_config, + "refclock SHM %d poll %d dpoll %d " + "refid %s precision 1.0e-9 delay %.1e\n", + shm_segment, poll, dpoll, refid, delay); + break; + case NTPD: + string_appendf(ntp_config, + "server 127.127.28.%d minpoll %d maxpoll %d\n" + "fudge 127.127.28.%d refid %s\n", + shm_segment, poll, poll, shm_segment, refid); + break; + } + + free(refid); +} + +static int add_ntp_source(struct ntp_server *source, char **ntp_config) +{ + pr_debug("adding NTP server %s", source->address); + + string_appendf(ntp_config, "server %s minpoll %d maxpoll %d%s\n", + source->address, source->minpoll, source->maxpoll, + source->iburst ? " iburst" : ""); + return 0; +} + +static int add_ptp_source(struct ptp_domain *source, + struct timemaster_config *config, int *shm_segment, + int ***allocated_phcs, char **ntp_config, + struct script *script) +{ + struct config_file *config_file; + char **command, *uds_path, **interfaces; + int i, j, num_interfaces, *phc, *phcs, hw_ts; + struct sk_ts_info ts_info; + + pr_debug("adding PTP domain %d", source->domain); + + hw_ts = SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RX_HARDWARE | + SOF_TIMESTAMPING_RAW_HARDWARE; + + for (num_interfaces = 0; + source->interfaces[num_interfaces]; num_interfaces++) + ; + + if (!num_interfaces) + return 0; + + /* get PHCs used by specified interfaces */ + phcs = malloc(num_interfaces * sizeof(int)); + for (i = 0; i < num_interfaces; i++) { + phcs[i] = -1; + + /* check if the interface has a usable PHC */ + if (sk_get_ts_info(source->interfaces[i], &ts_info)) { + pr_err("failed to get time stamping info for %s", + source->interfaces[i]); + free(phcs); + return 1; + } + + if (!ts_info.valid || + ((ts_info.so_timestamping & hw_ts) != hw_ts)) { + pr_debug("interface %s: no PHC", source->interfaces[i]); + continue; + } + + pr_debug("interface %s: PHC %d", source->interfaces[i], + ts_info.phc_index); + + /* and the PHC isn't already used in another source */ + for (j = 0; (*allocated_phcs)[j]; j++) { + if (*(*allocated_phcs)[j] == ts_info.phc_index) { + pr_debug("PHC %d already allocated", + ts_info.phc_index); + break; + } + } + if (!(*allocated_phcs)[j]) + phcs[i] = ts_info.phc_index; + } + + for (i = 0; i < num_interfaces; i++) { + /* skip if already used by ptp4l in this domain */ + if (phcs[i] == -2) + continue; + + interfaces = (char **)parray_new(); + parray_append((void ***)&interfaces, source->interfaces[i]); + + /* merge all interfaces sharing PHC to one ptp4l command */ + if (phcs[i] >= 0) { + for (j = i + 1; j < num_interfaces; j++) { + if (phcs[i] == phcs[j]) { + parray_append((void ***)&interfaces, + source->interfaces[j]); + /* mark the interface as used */ + phcs[j] = -2; + } + } + + /* don't use this PHC in other sources */ + phc = malloc(sizeof(int)); + *phc = phcs[i]; + parray_append((void ***)allocated_phcs, phc); + } + + uds_path = string_newf("%s/ptp4l.%d.socket", + config->rundir, *shm_segment); + + config_file = malloc(sizeof(*config_file)); + config_file->path = string_newf("%s/ptp4l.%d.conf", + config->rundir, *shm_segment); + config_file->content = strdup("[global]\n"); + extend_config_string(&config_file->content, + config->ptp4l.settings); + extend_config_string(&config_file->content, + source->ptp4l_settings); + string_appendf(&config_file->content, + "slaveOnly 1\n" + "domainNumber %d\n" + "uds_address %s\n", + source->domain, uds_path); + + if (phcs[i] >= 0) { + /* HW time stamping */ + command = get_ptp4l_command(&config->ptp4l, config_file, + interfaces, 1); + parray_append((void ***)&script->commands, command); + + command = get_phc2sys_command(&config->phc2sys, + source->domain, + source->phc2sys_poll, + *shm_segment, uds_path); + parray_append((void ***)&script->commands, command); + } else { + /* SW time stamping */ + command = get_ptp4l_command(&config->ptp4l, config_file, + interfaces, 0); + parray_append((void ***)&script->commands, command); + + string_appendf(&config_file->content, + "clock_servo ntpshm\n" + "ntpshm_segment %d\n", *shm_segment); + } + + parray_append((void ***)&script->configs, config_file); + + add_shm_source(*shm_segment, source->ntp_poll, + source->phc2sys_poll, source->delay, "PTP", + config, ntp_config); + + (*shm_segment)++; + + free(uds_path); + free(interfaces); + } + + free(phcs); + + return 0; +} + +static char **get_chronyd_command(struct program_config *config, + struct config_file *file) +{ + char **command = (char **)parray_new(); + + parray_append((void ***)&command, strdup(config->path)); + extend_string_array(&command, config->options); + parray_extend((void ***)&command, strdup("-n"), + strdup("-f"), strdup(file->path), NULL); + + return command; +} + +static char **get_ntpd_command(struct program_config *config, + struct config_file *file) +{ + char **command = (char **)parray_new(); + + parray_append((void ***)&command, strdup(config->path)); + extend_string_array(&command, config->options); + parray_extend((void ***)&command, strdup("-n"), + strdup("-c"), strdup(file->path), NULL); + + return command; +} + +static struct config_file *add_ntp_program(struct timemaster_config *config, + struct script *script) +{ + struct config_file *ntp_config = malloc(sizeof(*ntp_config)); + char **command = NULL; + + ntp_config->content = strdup(""); + + switch (config->ntp_program) { + case CHRONYD: + extend_config_string(&ntp_config->content, + config->chronyd.settings); + ntp_config->path = string_newf("%s/chrony.conf", + config->rundir); + command = get_chronyd_command(&config->chronyd, ntp_config); + break; + case NTPD: + extend_config_string(&ntp_config->content, + config->ntpd.settings); + ntp_config->path = string_newf("%s/ntp.conf", config->rundir); + command = get_ntpd_command(&config->ntpd, ntp_config); + break; + } + + parray_append((void ***)&script->configs, ntp_config); + parray_append((void ***)&script->commands, command); + + return ntp_config; +} + +static void script_destroy(struct script *script) +{ + char ***commands, **command; + struct config_file *config, **configs; + + for (configs = script->configs; *configs; configs++) { + config = *configs; + free(config->path); + free(config->content); + free(config); + } + free(script->configs); + + for (commands = script->commands; *commands; commands++) { + for (command = *commands; *command; command++) + free(*command); + free(*commands); + } + free(script->commands); + + free(script); +} + +static struct script *script_create(struct timemaster_config *config) +{ + struct script *script = malloc(sizeof(*script)); + struct source *source, **sources; + struct config_file *ntp_config = NULL; + int **allocated_phcs = (int **)parray_new(); + int ret = 0, shm_segment = 0; + + script->configs = (struct config_file **)parray_new(); + script->commands = (char ***)parray_new(); + + ntp_config = add_ntp_program(config, script); + + for (sources = config->sources; (source = *sources); sources++) { + switch (source->type) { + case NTP_SERVER: + if (add_ntp_source(&source->ntp, &ntp_config->content)) + ret = 1; + break; + case PTP_DOMAIN: + if (add_ptp_source(&source->ptp, config, &shm_segment, + &allocated_phcs, + &ntp_config->content, script)) + ret = 1; + break; + } + } + + free_parray((void **)allocated_phcs); + + if (ret) { + script_destroy(script); + return NULL; + } + + return script; +} + +static int start_program(char **command, sigset_t *mask) +{ + char **arg, *s; + pid_t pid; + +#ifdef HAVE_POSIX_SPAWN + posix_spawnattr_t attr; + + if (posix_spawnattr_init(&attr)) { + pr_err("failed to init spawn attributes: %m"); + return 1; + } + + if (posix_spawnattr_setsigmask(&attr, mask) || + posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) || + posix_spawnp(&pid, command[0], NULL, &attr, command, environ)) { + pr_err("failed to spawn %s: %m", command[0]); + posix_spawnattr_destroy(&attr); + return 1; + } + + posix_spawnattr_destroy(&attr); +#else + pid = fork(); + + if (pid < 0) { + pr_err("fork() failed: %m"); + return 1; + } + + if (!pid) { + /* restore the signal mask */ + if (sigprocmask(SIG_SETMASK, mask, NULL) < 0) { + pr_err("sigprocmask() failed: %m"); + exit(100); + } + + execvp(command[0], (char **)command); + + pr_err("failed to execute %s: %m", command[0]); + + exit(101); + } +#endif + + for (s = strdup(""), arg = command; *arg; arg++) + string_appendf(&s, "%s ", *arg); + + pr_info("process %d started: %s", pid, s); + + free(s); + + return 0; +} + +static int create_config_files(struct config_file **configs) +{ + struct config_file *config; + FILE *file; + char *tmp, *dir; + struct stat st; + + for (; (config = *configs); configs++) { + tmp = strdup(config->path); + dir = dirname(tmp); + if (stat(dir, &st) < 0 && errno == ENOENT && + mkdir(dir, 0755) < 0) { + pr_err("failed to create %s: %m", dir); + free(tmp); + return 1; + } + free(tmp); + + pr_debug("creating %s", config->path); + + file = fopen(config->path, "w"); + if (!file) { + pr_err("failed to open %s: %m", config->path); + return 1; + } + + if (fwrite(config->content, + strlen(config->content), 1, file) != 1) { + pr_err("failed to write to %s", config->path); + fclose(file); + return 1; + } + + fclose(file); + } + + return 0; +} + +static int remove_config_files(struct config_file **configs) +{ + struct config_file *config; + + for (; (config = *configs); configs++) { + pr_debug("removing %s", config->path); + + if (unlink(config->path)) + pr_err("failed to remove %s: %m", config->path); + } + + return 0; +} + +static int script_run(struct script *script) +{ + sigset_t mask, old_mask; + siginfo_t info; + pid_t pid; + int status, ret = 0; + char ***command; + + if (create_config_files(script->configs)) + return 1; + + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGQUIT); + sigaddset(&mask, SIGINT); + + /* block the signals */ + if (sigprocmask(SIG_BLOCK, &mask, &old_mask) < 0) { + pr_err("sigprocmask() failed: %m"); + return 1; + } + + for (command = script->commands; *command; command++) { + if (start_program(*command, &old_mask)) { + kill(getpid(), SIGTERM); + break; + } + } + + /* wait for one of the blocked signals */ + while (1) { + if (sigwaitinfo(&mask, &info) > 0) + break; + if (errno != EINTR) { + pr_err("sigwaitinfo() failed: %m"); + break; + } + } + + pr_info("received signal %d", info.si_signo); + + /* kill the process group */ + kill(0, SIGTERM); + + while ((pid = wait(&status)) >= 0) { + if (!WIFEXITED(status)) { + pr_info("process %d terminated abnormally", pid); + ret = 1; + } else { + if (WEXITSTATUS(status)) + ret = 1; + pr_info("process %d terminated with status %d", pid, + WEXITSTATUS(status)); + } + } + + if (remove_config_files(script->configs)) + return 1; + + return ret; +} + +static void script_print(struct script *script) +{ + char ***commands, **command; + struct config_file *config, **configs; + + for (configs = script->configs; *configs; configs++) { + config = *configs; + fprintf(stderr, "%s:\n\n%s\n", config->path, config->content); + } + + fprintf(stderr, "commands:\n\n"); + for (commands = script->commands; *commands; commands++) { + for (command = *commands; *command; command++) + fprintf(stderr, "%s ", *command); + fprintf(stderr, "\n"); + } +} + +static void usage(char *progname) +{ + fprintf(stderr, + "\nusage: %s [options] -f file\n\n" + " -f file specify path to configuration file\n" + " -n only print generated files and commands\n" + " -l level set logging level (6)\n" + " -m print messages to stdout\n" + " -q do not print messages to syslog\n" + " -v print version and exit\n" + " -h print this message and exit\n", + progname); +} + +int main(int argc, char **argv) +{ + struct timemaster_config *config; + struct script *script; + char *progname, *config_path = NULL; + int c, ret = 0, log_stdout = 0, log_syslog = 1, dry_run = 0; + + progname = strrchr(argv[0], '/'); + progname = progname ? progname + 1 : argv[0]; + + print_set_progname(progname); + print_set_verbose(1); + print_set_syslog(0); + + while (EOF != (c = getopt(argc, argv, "f:nl:mqvh"))) { + switch (c) { + case 'f': + config_path = optarg; + break; + case 'n': + dry_run = 1; + break; + case 'l': + print_set_level(atoi(optarg)); + break; + case 'm': + log_stdout = 1; + break; + case 'q': + log_syslog = 0; + break; + case 'v': + version_show(stdout); + return 0; + case 'h': + usage(progname); + return 0; + default: + usage(progname); + return 1; + } + } + + if (!config_path) { + pr_err("no configuration file specified"); + return 1; + } + + config = config_parse(config_path); + if (!config) + return 1; + + script = script_create(config); + config_destroy(config); + if (!script) + return 1; + + print_set_verbose(log_stdout); + print_set_syslog(log_syslog); + + if (dry_run) + script_print(script); + else + ret = script_run(script); + + script_destroy(script); + + if (!dry_run) + pr_info("exiting"); + + return ret; +}