Blob Blame History Raw
/**
 * @file timemaster.c
 * @brief Program to run NTP with PTP as reference clocks.
 * @note Copyright (C) 2014 Miroslav Lichvar <mlichvar@redhat.com>
 *
 * 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 <ctype.h>
#include <errno.h>
#include <libgen.h>
#include <limits.h>
#include <linux/net_tstamp.h>
#include <net/if.h>
#include <signal.h>
#include <spawn.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "print.h"
#include "rtnl.h"
#include "sk.h"
#include "util.h"
#include "version.h"

#define DEFAULT_RUNDIR "/var/run/timemaster"

#define DEFAULT_FIRST_SHM_SEGMENT 0
#define DEFAULT_RESTART_PROCESSES 1

#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;
	char *ntp_options;
};

struct ptp_domain {
	int domain;
	int ntp_poll;
	int phc2sys_poll;
	double delay;
	char **interfaces;
	char **ptp4l_settings;
	char *ntp_options;
};

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;
	int first_shm_segment;
	int restart_processes;
	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;
	int **command_groups;
	int restart_groups;
	int no_restart_group;
};

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, xstrdup(*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, xstrdup(w));
	}
}

static void replace_string(char *s, char **str)
{
	if (*str)
		free(*str);
	*str = xstrdup(s);
}

static char *parse_section_name(char *s)
{
	char *s1, *s2;

	s1 = s + 1;
	for (s2 = s1; *s2 && *s2 != ']'; s2++)
		;
	*s2 = '\0';

	return xstrdup(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);
		free(source->ntp.ntp_options);
		break;
	case PTP_DOMAIN:
		free_parray((void **)source->ptp.interfaces);
		free_parray((void **)source->ptp.ptp4l_settings);
		free(source->ptp.ntp_options);
		break;
	}
	free(source);
}

static struct source *source_ntp_parse(char *parameter, char **settings)
{
	char *name, *value;
	struct source *source;
	int r = 0;

	if (!*parameter) {
		pr_err("missing address for ntp_server");
		return NULL;
	}

	source = xmalloc(sizeof(*source));
	source->type = NTP_SERVER;
	source->ntp.address = xstrdup(parameter);
	source->ntp.minpoll = DEFAULT_NTP_MINPOLL;
	source->ntp.maxpoll = DEFAULT_NTP_MAXPOLL;
	source->ntp.iburst = 0;
	source->ntp.ntp_options = xstrdup("");

	for (; *settings; settings++) {
		parse_setting(*settings, &name, &value);
		if (!strcasecmp(name, "minpoll")) {
			r = parse_int(value, &source->ntp.minpoll);
		} else if (!strcasecmp(name, "maxpoll")) {
			r = parse_int(value, &source->ntp.maxpoll);
		} else if (!strcasecmp(name, "iburst")) {
			r = parse_bool(value, &source->ntp.iburst);
		} else if (!strcasecmp(name, "ntp_options")) {
			replace_string(value, &source->ntp.ntp_options);
		} else {
			pr_err("unknown ntp_server setting %s", name);
			goto failed;
		}
		if (r) {
			pr_err("invalid value %s for %s", value, name);
			goto failed;
		}
	}

	return source;
failed:
	source_destroy(source);
	return NULL;
}

static struct source *source_ptp_parse(char *parameter, char **settings)
{
	char *name, *value;
	struct source *source;
	int r = 0;

	source = xmalloc(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();
	source->ptp.ntp_options = xstrdup("");

	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,
				      xstrdup(value));
		} else if (!strcasecmp(name, "ntp_options")) {
			replace_string(value, &source->ptp.ntp_options);
		} 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;
	int r = 0;

	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 if (!strcasecmp(name, "first_shm_segment")) {
			r = parse_int(value, &config->first_shm_segment);
		} else if (!strcasecmp(name, "restart_processes")) {
			r = parse_int(value, &config->restart_processes);
		} else {
			pr_err("unknown timemaster setting %s", name);
			return 1;
		}
		if (r) {
			pr_err("invalid value %s for %s", value, 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 = xstrdup(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, xstrdup(s));
	while ((s = va_arg(ap, const char *)))
		parray_append((void ***)&config->settings, xstrdup(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 = xcalloc(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 = xstrdup(DEFAULT_RUNDIR);
	config->first_shm_segment = DEFAULT_FIRST_SHM_SEGMENT;
	config->restart_processes = DEFAULT_RESTART_PROCESSES;

	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 ***)&section_lines, xstrdup(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, xstrdup(config->path));
	extend_string_array(&command, config->options);
	parray_extend((void ***)&command,
		      xstrdup("-f"), xstrdup(file->path),
		      xstrdup(hw_ts ? "-H" : "-S"), NULL);

	for (; *interfaces; interfaces++)
		parray_extend((void ***)&command,
			      xstrdup("-i"), xstrdup(*interfaces), NULL);

	return command;
}

static char **get_phc2sys_command(struct program_config *config, int domain,
				  int poll, int shm_segment, char *uds_path,
				  char *message_tag)
{
	char **command = (char **)parray_new();

	parray_append((void ***)&command, xstrdup(config->path));
	extend_string_array(&command, config->options);
	parray_extend((void ***)&command,
		      xstrdup("-a"), xstrdup("-r"),
		      xstrdup("-R"), string_newf("%.2f", poll > 0 ?
						1.0 / (1 << poll) : 1 << -poll),
		      xstrdup("-z"), xstrdup(uds_path),
		      xstrdup("-t"), xstrdup(message_tag),
		      xstrdup("-n"), string_newf("%d", domain),
		      xstrdup("-E"), xstrdup("ntpshm"),
		      xstrdup("-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_command(char **command, int command_group,
			struct script *script)
{
	int *group;

	parray_append((void ***)&script->commands, command);

	group = xmalloc(sizeof(int));
	*group = command_group;
	parray_append((void ***)&script->command_groups, group);
}

static void add_shm_source(int shm_segment, int poll, int dpoll, double delay,
			   char *ntp_options, 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 %s\n",
			       shm_segment, poll, dpoll, refid, delay,
			       ntp_options);
		break;
	case NTPD:
		string_appendf(ntp_config,
			       "server 127.127.28.%d minpoll %d maxpoll %d "
			       "mode 1 %s\n"
			       "fudge 127.127.28.%d refid %s\n",
			       shm_segment, poll, poll, ntp_options,
			       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 %s\n",
		       source->address, source->minpoll, source->maxpoll,
		       source->iburst ? "iburst" : "", source->ntp_options);
	return 0;
}

static int add_ptp_source(struct ptp_domain *source,
			  struct timemaster_config *config, int *shm_segment,
			  int *command_group, int ***allocated_phcs,
			  char **ntp_config, struct script *script)
{
	struct config_file *config_file;
	char **command, *uds_path, **interfaces, *message_tag;
	char ts_interface[IF_NAMESIZE];
	int i, j, num_interfaces, *phc, *phcs, hw_ts, sw_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;
	sw_ts = SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_RX_SOFTWARE |
		SOF_TIMESTAMPING_SOFTWARE;

	for (num_interfaces = 0;
	     source->interfaces[num_interfaces]; num_interfaces++)
		;

	if (!num_interfaces)
		return 0;

	/* get PHCs used by specified interfaces */
	phcs = xmalloc(num_interfaces * sizeof(int));
	for (i = 0; i < num_interfaces; i++) {
		phcs[i] = -1;

		/*
		 * if it is a bonded interface, use the name of the active
		 * slave interface (which will be timestamping packets)
		 */
		if (!rtnl_get_ts_device(source->interfaces[i], ts_interface)) {
			pr_debug("slave interface of %s: %s",
				 source->interfaces[i], ts_interface);
		} else {
			snprintf(ts_interface, sizeof(ts_interface), "%s",
				 source->interfaces[i]);
		}

		/* check if the interface has a usable PHC */
		if (sk_get_ts_info(ts_interface, &ts_info)) {
			pr_err("failed to get time stamping info for %s",
			       ts_interface);
			free(phcs);
			return 1;
		}

		if (((ts_info.so_timestamping & hw_ts) != hw_ts)) {
			pr_debug("interface %s: no PHC", ts_interface);
			if ((ts_info.so_timestamping & sw_ts) != sw_ts) {
				pr_err("time stamping not supported on %s",
				       ts_interface);
				free(phcs);
				return 1;
			}
			continue;
		}

		pr_debug("interface %s: PHC %d", ts_interface,
			 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 = xmalloc(sizeof(int));
			*phc = phcs[i];
			parray_append((void ***)allocated_phcs, phc);
		}

		uds_path = string_newf("%s/ptp4l.%d.socket",
				       config->rundir, *shm_segment);

		message_tag = string_newf("[%d", source->domain);
		for (j = 0; interfaces[j]; j++)
			string_appendf(&message_tag, "%s%s", j ? "+" : ":",
				       interfaces[j]);
		string_appendf(&message_tag, "]");

		config_file = xmalloc(sizeof(*config_file));
		config_file->path = string_newf("%s/ptp4l.%d.conf",
						config->rundir, *shm_segment);
		config_file->content = xstrdup("[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"
			       "message_tag %s\n",
			       source->domain, uds_path, message_tag);

		if (phcs[i] >= 0) {
			/* HW time stamping */
			command = get_ptp4l_command(&config->ptp4l, config_file,
						    interfaces, 1);
			add_command(command, *command_group, script);

			command = get_phc2sys_command(&config->phc2sys,
						      source->domain,
						      source->phc2sys_poll,
						      *shm_segment, uds_path,
						      message_tag);
			add_command(command, (*command_group)++, script);
		} else {
			/* SW time stamping */
			command = get_ptp4l_command(&config->ptp4l, config_file,
						    interfaces, 0);
			add_command(command, (*command_group)++, script);

			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,
			       source->ntp_options, "PTP", config, ntp_config);

		(*shm_segment)++;

		free(message_tag);
		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, xstrdup(config->path));
	extend_string_array(&command, config->options);
	parray_extend((void ***)&command, xstrdup("-n"),
		      xstrdup("-f"), xstrdup(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, xstrdup(config->path));
	extend_string_array(&command, config->options);
	parray_extend((void ***)&command, xstrdup("-n"),
		      xstrdup("-c"), xstrdup(file->path), NULL);

	return command;
}

static struct config_file *add_ntp_program(struct timemaster_config *config,
					   struct script *script,
					   int command_group)
{
	struct config_file *ntp_config = xmalloc(sizeof(*ntp_config));
	char **command = NULL;

	ntp_config->content = xstrdup("");

	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);
	add_command(command, command_group, script);

	return ntp_config;
}

static void script_destroy(struct script *script)
{
	char ***commands, **command;
	int **groups;
	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);

	for (groups = script->command_groups; *groups; groups++)
		free(*groups);
	free(script->command_groups);

	free(script);
}

static struct script *script_create(struct timemaster_config *config)
{
	struct script *script = xmalloc(sizeof(*script));
	struct source *source, **sources;
	struct config_file *ntp_config = NULL;
	int **allocated_phcs = (int **)parray_new();
	int ret = 0, shm_segment, command_group = 0;

	script->configs = (struct config_file **)parray_new();
	script->commands = (char ***)parray_new();
	script->command_groups = (int **)parray_new();
	script->no_restart_group = command_group;
	script->restart_groups = config->restart_processes;

	ntp_config = add_ntp_program(config, script, command_group++);
	shm_segment = config->first_shm_segment;

	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,
					   &command_group, &allocated_phcs,
					   &ntp_config->content, script))
				ret = 1;
			break;
		}
	}

	free_parray((void **)allocated_phcs);

	if (ret) {
		script_destroy(script);
		return NULL;
	}

	return script;
}

static pid_t 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 0;
	}

	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 0;
	}

	posix_spawnattr_destroy(&attr);
#else
	pid = fork();

	if (pid < 0) {
		pr_err("fork() failed: %m");
		return 0;
	}

	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 = xstrdup(""), arg = command; *arg; arg++)
		string_appendf(&s, "%s ", *arg);

	pr_info("process %d started: %s", pid, s);

	free(s);

	return pid;
}

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 = xstrdup(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)
{
	struct timespec ts_start, ts_now;
	sigset_t mask, old_mask;
	siginfo_t info;
	pid_t pid, *pids;
	int i, group, num_commands, status, quit = 0, ret = 0;

	for (num_commands = 0; script->commands[num_commands]; num_commands++)
		;

	if (!num_commands) {
		/* nothing to do */
		return 0;
	}

	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;
	}

	pids = xcalloc(num_commands, sizeof(*pids));

	for (i = 0; i < num_commands; i++) {
		pids[i] = start_program(script->commands[i], &old_mask);
		if (!pids[i]) {
			kill(getpid(), SIGTERM);
			break;
		}
	}

	clock_gettime(CLOCK_MONOTONIC, &ts_start);

	/* process the blocked signals */
	while (1) {
		if (sigwaitinfo(&mask, &info) < 0) {
			if (errno == EINTR)
				continue;
			pr_err("sigwaitinfo() failed: %m");
			break;
		}

		clock_gettime(CLOCK_MONOTONIC, &ts_now);

		if (info.si_signo != SIGCHLD) {
			if (quit)
				continue;

			quit = 1;
			pr_debug("exiting on signal %d", info.si_signo);

			/* terminate remaining processes */
			for (i = 0; i < num_commands; i++) {
				if (pids[i] > 0) {
					pr_debug("killing process %d", pids[i]);
					kill(pids[i], SIGTERM);
				}
			}

			continue;
		}

		/* wait for all terminated processes */
		while (1) {
			pid = waitpid(-1, &status, WNOHANG);
			if (pid <= 0)
				break;

			if (!WIFEXITED(status)) {
				pr_info("process %d terminated abnormally",
					pid);
			} else {
				pr_info("process %d terminated with status %d",
					pid, WEXITSTATUS(status));
			}

			for (i = 0; i < num_commands; i++) {
				if (pids[i] == pid)
					pids[i] = 0;
			}
		}

		/* wait for all processes to terminate when exiting */
		if (quit) {
			for (i = 0; i < num_commands; i++) {
				if (pids[i])
					break;
			}
			if (i == num_commands)
				break;

			pr_debug("waiting for other processes to terminate");
			continue;
		}

		/*
		 * terminate (and then restart if allowed) all processes in
		 * groups that have a terminated process
		 */
		for (group = 0; group < num_commands; group++) {
			int terminated = 0, running = 0;

			for (i = 0; i < num_commands; i++) {
				if (*(script->command_groups[i]) != group)
					continue;
				if (pids[i])
					running++;
				else
					terminated++;
			}

			if (!terminated)
				continue;

			/*
			 * exit with a non-zero status if the group should not
			 * be restarted (i.e. chronyd/ntpd), timemaster is
			 * running only for a short time (and it is likely a
			 * configuration error), or restarting is disabled
			 * completely
			 */
			if (group == script->no_restart_group ||
			    ts_now.tv_sec - ts_start.tv_sec <= 1 ||
			    !script->restart_groups) {
				kill(getpid(), SIGTERM);
				ret = 1;
				break;
			}

			for (i = 0; i < num_commands; i++) {
				if (*(script->command_groups[i]) != group)
					continue;

				/* terminate all processes in the group first */
				if (running && pids[i]) {
					pr_debug("killing process %d", pids[i]);
					kill(pids[i], SIGTERM);
				} else if (!running && !pids[i]) {
					pids[i] = start_program(script->commands[i],
								&old_mask);
					if (!pids[i])
						kill(getpid(), SIGTERM);

					/* limit restarting rate */
					sleep(1);
				}
			}
		}
	}

	free(pids);

	if (remove_config_files(script->configs))
		return 1;

	return ret;
}

static void script_print(struct script *script)
{
	char ***commands, **command;
	int **groups;
	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, groups = script->command_groups;
	     *commands; commands++, groups++) {
		fprintf(stderr, "[%d] ", **groups);
		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;
}