In the spirit of UNIX where one program does one job and does it well
(famous last words), there seems to be a need for a standalone binary
that performs a very trivial task: wait until the PTP synchronization
finishes*.

*The definition we choose for "finishing" is rather primitive: the
offset to master is below a certain threshold. We make no guarantees
that it may not rise above that threshold right after this program
decides that the synchronization ended.

Fundamentally, this program works in the following way: it automates
what you are able to do using the following command:

pmc -u -b 0 'GET TIME_STATUS_NP'
sending: GET TIME_STATUS_NP
        a6f4af.fffe.fdfc73-0 seq 0 RESPONSE MANAGEMENT TIME_STATUS_NP
                master_offset              0              <- this is the 
interesting part
                ingress_time               90010984167360
                cumulativeScaledRateOffset +0.000000000
                scaledLastGmPhaseChange    0
                gmTimeBaseIndicator        0
                lastGmPhaseChange          0x0000'0000000000000000.0000
                gmPresent                  true
                gmIdentity                 001f7b.fffe.630248

The program exits when the "master_offset" becomes <= the given
threshold (passed via the "-x" option). The default value for the
threshold is 0 ns.

but because scripting pmc is rather ugly (I'm not even sure if it was
designed for that), we do it in a small C program.

Opening a PMC agent over the local UNIX domain socket will obviously
only be able to reach the ports that are local to this system.
However, that creates a problem: we may have a back-to-back connection
between a master and a slave, and we haven't turned off the BMCA, so we
don't know beforehand if we're going to be master or slave. But
nonetheless, we want to send some data once the link is synchronized
(i.e. if we're the slave, wait until we're synchronized; if we're the
master, it would be nice to check that the link partner is synchronized).

To support that use case, we actually need yet another PMC agent, this
time one that subscribes for push notifications over the network.

So we create one PMC agent over UDS, and one PMC agent over the network
for each local port, in case that port might become a master.

This new program needs the remote ptp4l instance to be run using
"allow_remote_subscribers" set to 1. If that is turned off, it will work
in a limited mode where it can only detect that the local system has
finished sync, because the local port is a slave and no network agent is
required for that.

There is one more important option to mention, "-i" for the designated
interface. This option can be omitted, and when that happens, checksync
will wait for all detected slave ports to finish sync. When "-i" is
specified, it will wait for a single port to finish. Which port? see below.

The idea is that scripts should be able to do:

system-a $ ptp4l -i eth0 -2 -P ----allow_remote_subscriptions 1 &
system-a $ checksync -i eth0 -m -2 -x 20 && echo "Clock A is synchronized"
system-b $ ptp4l -i eth1 -2 -P ----allow_remote_subscriptions 1 &
system-b $ checksync -i eth1 -m -2 -x 20 && echo "Clock B is synchronized"

otherwise said, run exactly the same script on different systems.
However with PTP, one system may become master and the other may become
slave, and therefore, one checksync instance will need to check the time
status of the local system, and the other instance will need to check
the time status of the remote system.

But we don't want to know the details of the remote system, nor do we
want to log into it using ssh and run checksync there. For all we care,
we should be able to run checksync on a port that is in the MASTER
state, and it should figure out which remote ports to check.

So when "-i" is specified and that interface is a master port, we
actually check the sync status of the remote ports attached to it.

[ note, we actually only check the sync status of the first remote port,
  that was sufficient for the point-to-point use case I was testing
  with, we should check all ports though ]

Signed-off-by: Vladimir Oltean <olte...@gmail.com>
---
 checksync.c | 577 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 makefile    |   8 +-
 2 files changed, 583 insertions(+), 2 deletions(-)
 create mode 100644 checksync.c

diff --git a/checksync.c b/checksync.c
new file mode 100644
index 000000000000..99f85e473702
--- /dev/null
+++ b/checksync.c
@@ -0,0 +1,577 @@
+/**
+ * @file checksync.c
+ * @brief Utility program to wait until an external program synchronizes
+ * @note Copyright 2021 Vladimir Oltean <olte...@gmail.com>
+ * @note SPDX-License-Identifier: GPL-2.0+
+ */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <net/if.h>
+#include <sys/queue.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include "missing.h"
+#include "pmc_agent.h"
+#include "pmc_common.h"
+#include "print.h"
+#include "util.h"
+#include "version.h"
+
+enum port_type {
+       PORT_TYPE_LOCAL,
+       PORT_TYPE_REMOTE,
+};
+
+struct port {
+       LIST_ENTRY(port) list;
+       struct PortIdentity portid;
+       enum port_state state;
+       char name[IFNAMSIZ];
+       int64_t master_offset;
+       struct pmc_agent *agent;
+       const struct pmc_agent *discovered_by_agent;
+       enum port_type type;
+};
+
+struct checksync_private {
+       struct config *cfg;
+       struct pmc_agent *uds_agent;
+       unsigned int threshold;
+       bool state_changed;
+       const char *designated_iface;
+       LIST_HEAD(port_head, port) ports;
+};
+
+static void usage(char *progname)
+{
+       fprintf(stderr,
+               "\n"
+               "usage: %s [options]\n\n"
+               " -f [file]      read configuration from 'file'\n"
+               " -h             prints this message and exits\n"
+               " -l [num]       set the logging level to 'num'\n"
+               " -2             IEEE 802.3\n"
+               " -4             UDP IPV4 (default)\n"
+               " -6             UDP IPV6\n"
+               " -t [hex]       transport specific field, default 0x0\n"
+               " -x [num]       set the sync offset threshold to 'num'\n"
+               " -m             print messages to stdout\n"
+               " -q             do not print messages to the syslog\n"
+               " -v             prints the software version and exits\n"
+               "\n",
+               progname);
+}
+
+/* FIXME: copied from phc2sys */
+static enum port_state normalize_state(enum port_state state)
+{
+       if (state != PS_MASTER && state != PS_SLAVE &&
+           state != PS_PRE_MASTER && state != PS_UNCALIBRATED) {
+               /* treat any other state as "not a master nor a slave" */
+               state = PS_DISABLED;
+       }
+       return state;
+}
+
+static struct port *port_get(struct checksync_private *priv,
+                            const struct PortIdentity *portid)
+{
+       struct port *port;
+
+       LIST_FOREACH(port, &priv->ports, list)
+               if (pid_eq(&port->portid, portid))
+                       return port;
+
+       return NULL;
+}
+
+static struct port *port_add(struct checksync_private *priv,
+                            const struct PortIdentity *portid,
+                            const char *name, enum port_type type)
+{
+       struct port *port;
+
+       port = port_get(priv, portid);
+       if (port)
+               return port;
+
+       port = calloc(1, sizeof(*port));
+       if (!port) {
+               pr_err("failed to allocate memory for a port");
+               return NULL;
+       }
+
+       pr_debug("new port %s (%s)", pid2str(portid), name);
+       port->type = type;
+       port->master_offset = INT64_MAX;
+       memcpy(&port->portid, portid, sizeof(*portid));
+       strcpy(port->name, name);
+       LIST_INSERT_HEAD(&priv->ports, port, list);
+       return port;
+}
+
+static struct port *slave_port_get_by_clockid(struct checksync_private *priv,
+                                             const struct ClockIdentity 
*clockid)
+{
+       struct port *port;
+
+       LIST_FOREACH(port, &priv->ports, list)
+               if (cid_eq(&port->portid.clockIdentity, clockid) &&
+                   port->state == PS_SLAVE)
+                       return port;
+
+       return NULL;
+}
+
+/* If the user has specified an interface through the command line arguments,
+ * the designated port is:
+ * - that interface, if it is a slave.
+ * - one of the remote ports behind it, if it is a master.
+ */
+static struct port *designated_port_get(struct checksync_private *priv)
+{
+       struct port *local_port, *remote_port;
+
+       LIST_FOREACH(local_port, &priv->ports, list) {
+               if (strcmp(local_port->name, priv->designated_iface) != 0 ||
+                   local_port->type != PORT_TYPE_LOCAL)
+                       continue;
+
+               if (local_port->state == PS_SLAVE)
+                       return local_port;
+
+               LIST_FOREACH(remote_port, &priv->ports, list) {
+                       if (remote_port->discovered_by_agent == 
local_port->agent &&
+                           remote_port->state == PS_SLAVE)
+                               return remote_port;
+               }
+       }
+
+       return NULL;
+}
+
+static int checksync_recv_subscribed(void *context, struct ptp_message *msg,
+                                    int excluded)
+{
+       struct checksync_private *priv = context;
+       const struct ClockIdentity *clockid;
+       struct time_status_np *tsn;
+       enum port_state state;
+       struct portDS *pds;
+       struct port *port;
+       int mgt_id;
+
+       mgt_id = management_tlv_id(msg);
+       if (mgt_id == excluded)
+               return 0;
+
+       switch (mgt_id) {
+       case MID_PORT_DATA_SET:
+               pds = management_tlv_data(msg);
+               port = port_get(priv, &pds->portIdentity);
+               if (!port) {
+                       pr_info("received port state data for unknown port %s",
+                               pid2str(&pds->portIdentity));
+                       return 1;
+               }
+               state = normalize_state(pds->portState);
+               if (port->state != state) {
+                       pr_info("port %s (%s) changed state",
+                               pid2str(&pds->portIdentity), port->name);
+                       port->state = state;
+                       priv->state_changed = true;
+               }
+               return 1;
+       case MID_TIME_STATUS_NP:
+               tsn = management_tlv_data(msg);
+
+               clockid = &msg->header.sourcePortIdentity.clockIdentity;
+               port = slave_port_get_by_clockid(priv, clockid);
+               if (!port) {
+                       pr_info("received time sync data for unknown slave port 
%s",
+                               pid2str(&msg->header.sourcePortIdentity));
+                       return 1;
+               }
+               pr_info("port %s (%s) offset %10" PRId64 " to master %s",
+                       pid2str(&port->portid), port->name, tsn->master_offset,
+                       cid2str(&tsn->gmIdentity));
+               port->master_offset = tsn->master_offset;
+               priv->state_changed = true;
+               return 1;
+       }
+
+       return 0;
+}
+
+static int port_create_agent(struct checksync_private *priv,
+                            struct port *port)
+{
+       struct config *cfg = priv->cfg;
+
+       port->agent = pmc_agent_create();
+       if (!port->agent) {
+               pr_err("port %s (%s) failed to create a PMC agent",
+                      pid2str(&port->portid), port->name);
+               return -1;
+       }
+
+       return init_pmc_node(cfg, port->agent,
+                            config_get_int(cfg, NULL, "network_transport"),
+                            port->name,
+                            config_get_int(cfg, NULL, "boundary_hops"),
+                            checksync_recv_subscribed, priv);
+}
+
+static void port_destroy_agent(struct checksync_private *priv,
+                              struct port *port)
+{
+       if (!port->agent)
+               return;
+
+       pmc_agent_destroy(port->agent);
+}
+
+static int checksync_auto_init_ports(struct checksync_private *priv,
+                                    struct pmc_agent *agent,
+                                    enum port_type type)
+{
+       struct PortIdentity portid;
+       struct port *port, *tmp;
+       int number_ports, err;
+       enum port_state state;
+       char iface[IFNAMSIZ];
+       int timestamping;
+       unsigned int i;
+
+       while (1) {
+               if (!is_running())
+                       return -1;
+               err = pmc_agent_query_dds(agent, 1000);
+               if (!err)
+                       break;
+               if (err == -ETIMEDOUT)
+                       pr_notice("Waiting for ptp4l...");
+               else
+                       return -1;
+       }
+
+       err = pmc_agent_get_clock_identity(agent, &portid.clockIdentity);
+       if (err) {
+               pr_err("failed to get clock identity");
+               return -1;
+       }
+
+       number_ports = pmc_agent_get_number_ports(agent);
+       if (number_ports <= 0) {
+               pr_err("failed to get number of ports");
+               return -1;
+       }
+
+       err = pmc_agent_subscribe(agent, 1000, true, true);
+       if (err) {
+               pr_err("PMC failed to subscribe: %d, "
+                      "is ptp4l running with allow_remote_subscriptions?",
+                      err);
+               return -1;
+       }
+
+       for (i = 1; i <= number_ports; i++) {
+               err = pmc_agent_query_port_properties(agent, 1000, i, &state,
+                                                     &timestamping, iface);
+               if (err == -ENODEV) {
+                       /* port does not exist, ignore the port */
+                       continue;
+               }
+               if (err) {
+                       pr_err("failed to get port properties: %d", err);
+                       goto out_free_ports;
+               }
+               if (timestamping == TS_SOFTWARE) {
+                       /* ignore ports with software time stamping */
+                       continue;
+               }
+               portid.portNumber = i;
+               port = port_add(priv, &portid, iface, type);
+               if (!port)
+                       goto out_free_ports;
+               port->discovered_by_agent = agent;
+               port->state = normalize_state(state);
+       }
+
+       return 0;
+
+out_free_ports:
+       LIST_FOREACH_SAFE(port, &priv->ports, list, tmp)
+               free(port);
+
+       return -1;
+}
+
+static int checksync_create(struct checksync_private *priv, int argc,
+                           char **argv)
+{
+       char uds_local[MAX_IFNAME_SIZE + 1];
+       int c, err = 0, index, print_level;
+       char *config = NULL, *progname;
+       struct config *cfg = NULL;
+       struct option *opts;
+
+       cfg = config_create();
+       if (!cfg)
+               return -1;
+
+       opts = config_long_options(cfg);
+
+       /* Process the command line arguments. */
+       progname = strrchr(argv[0], '/');
+       progname = progname ? 1 + progname : argv[0];
+       while ((c = getopt_long(argc, argv, "246b:i:f:hl:mqt:x:v", opts, 
&index)) != EOF) {
+               switch (c) {
+               case 0:
+                       err = config_parse_option(cfg, opts[index].name, 
optarg);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case '2':
+                       err = config_set_int(cfg, "network_transport",
+                                            TRANS_IEEE_802_3);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case '4':
+                       err = config_set_int(cfg, "network_transport",
+                                            TRANS_UDP_IPV4);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case '6':
+                       err = config_set_int(cfg, "network_transport",
+                                            TRANS_UDP_IPV6);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case 'i':
+                       priv->designated_iface = optarg;
+                       break;
+               case 'f':
+                       config = optarg;
+                       break;
+               case 'l':
+                       err = get_arg_val_i(c, optarg, &print_level,
+                                           PRINT_LEVEL_MIN, PRINT_LEVEL_MAX);
+                       if (err)
+                               goto out_config_destroy;
+                       config_set_int(cfg, "logging_level", print_level);
+                       print_set_level(print_level);
+                       break;
+               case 'm':
+                       config_set_int(cfg, "verbose", 1);
+                       print_set_verbose(1);
+                       break;
+               case 'q':
+                       config_set_int(cfg, "use_syslog", 0);
+                       print_set_syslog(0);
+                       break;
+               case 't':
+                       if (sscanf(optarg, "%x", &c) != 1)
+                               goto out_config_destroy;
+
+                       err = config_set_int(cfg, "transportSpecific", c);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case 'x':
+                       err = get_arg_val_ui(c, optarg, &priv->threshold,
+                                            0, UINT_MAX);
+                       if (err)
+                               goto out_config_destroy;
+                       break;
+               case 'v':
+                       version_show(stdout);
+                       return 0;
+               case 'h':
+                       usage(progname);
+                       return -1;
+               case '?':
+               default:
+                       usage(progname);
+                       return -1;
+               }
+       }
+       if (config && (c = config_read(config, cfg))) {
+               fprintf(stderr, "failed to read config\n");
+               goto out_config_destroy;
+       }
+       print_set_progname(progname);
+       print_set_tag(config_get_string(cfg, NULL, "message_tag"));
+       print_set_verbose(config_get_int(cfg, NULL, "verbose"));
+       print_set_syslog(config_get_int(cfg, NULL, "use_syslog"));
+       print_set_level(config_get_int(cfg, NULL, "logging_level"));
+
+       priv->cfg = cfg;
+
+       snprintf(uds_local, sizeof(uds_local), "/var/run/pmc.%d", getpid());
+
+       priv->uds_agent = pmc_agent_create();
+       if (!priv->uds_agent)
+               goto out_config_destroy;
+
+       err = init_pmc_node(cfg, priv->uds_agent, TRANS_UDS, uds_local, 0,
+                           checksync_recv_subscribed, priv);
+       if (err)
+               goto out_agent_destroy;
+
+       return 0;
+
+out_agent_destroy:
+       pmc_agent_destroy(priv->uds_agent);
+       msg_cleanup();
+out_config_destroy:
+       config_destroy(cfg);
+       return -1;
+}
+
+static void checksync_destroy(struct checksync_private *priv)
+{
+       pmc_agent_destroy(priv->uds_agent);
+       msg_cleanup();
+       config_destroy(priv->cfg);
+}
+
+static bool checksync_done(struct checksync_private *priv)
+{
+       struct port *port, *designated_port = NULL;
+       bool have_slave_ports = false;
+
+       if (!priv->state_changed)
+               return false;
+
+       priv->state_changed = false;
+
+       /* Figure out the designated port on which we must wait for the sync to
+        * finish. This may not be a local port and it depends on port state,
+        * so we cannot determine it statically.
+        */
+       if (priv->designated_iface) {
+               designated_port = designated_port_get(priv);
+               if (!designated_port) {
+                       pr_err("could not find port designated to interface %s",
+                              priv->designated_iface);
+                       exit(1);
+               }
+       }
+
+       LIST_FOREACH(port, &priv->ports, list) {
+               /* If we have a designated port, wait just for that.
+                * Otherwise wait for all ports.
+                */
+               if (designated_port && port != designated_port)
+                       continue;
+
+               if (port->state != PS_SLAVE)
+                       continue;
+
+               have_slave_ports = true;
+
+               if (llabs(port->master_offset) > priv->threshold) {
+                       pr_debug("port %s (%s) offset %10" PRId64
+                                " under threshold %d",
+                                pid2str(&port->portid), port->name,
+                                port->master_offset, priv->threshold);
+                       return false;
+               }
+       }
+
+       /* If all ports are masters, sync is not done */
+       return have_slave_ports ? true : false;
+}
+
+static int checksync_update_pmc_agents(struct checksync_private *priv)
+{
+       struct port *port;
+       int err;
+
+       err = pmc_agent_update(priv->uds_agent);
+       if (err < 0)
+               return err;
+
+       LIST_FOREACH(port, &priv->ports, list) {
+               if (!port->agent)
+                       continue;
+
+               err = pmc_agent_update(port->agent);
+               if (err < 0)
+                       return err;
+       }
+
+       return 0;
+}
+
+static int checksync_run(struct checksync_private *priv)
+{
+       struct port *port, *tmp;
+       int err;
+
+       /* Create the PMC agent over UDS */
+       err = checksync_auto_init_ports(priv, priv->uds_agent, PORT_TYPE_LOCAL);
+       if (err)
+               return err;
+
+       /* And then a PMC agent over each local port, in the eventuality that
+        * it may become a master and have other ports behind it, on which we
+        * need to monitor the sync status.
+        */
+       LIST_FOREACH(port, &priv->ports, list) {
+               err = port_create_agent(priv, port);
+               if (err < 0)
+                       goto out_destroy_port_agents;
+
+               pr_debug("auto init ports behind local port %s (%s)",
+                        pid2str(&port->portid), port->name);
+
+               err = checksync_auto_init_ports(priv, port->agent,
+                                               PORT_TYPE_REMOTE);
+               if (err)
+                       goto out_destroy_port_agents;
+       }
+
+       while (is_running()) {
+               /* Collect updates from ptp4l */
+               err = checksync_update_pmc_agents(priv);
+               if (err < 0) {
+                       pr_err("pmc_agent_update returned %d", err);
+                       break;
+               }
+
+               if (checksync_done(priv))
+                       break;
+       }
+
+out_destroy_port_agents:
+       LIST_FOREACH_SAFE(port, &priv->ports, list, tmp) {
+               port_destroy_agent(priv, port);
+               free(port);
+       }
+
+       return err;
+}
+
+int main(int argc, char *argv[])
+{
+       struct checksync_private priv = {0};
+       int err;
+
+       handle_term_signals();
+
+       err = checksync_create(&priv, argc, argv);
+       if (err)
+               return err;
+
+       err = checksync_run(&priv);
+
+       checksync_destroy(&priv);
+
+       return 0;
+}
diff --git a/makefile b/makefile
index 6d49b911153e..5b680a47cc12 100644
--- a/makefile
+++ b/makefile
@@ -22,7 +22,7 @@ CC    = $(CROSS_COMPILE)gcc
 VER     = -DVER=$(version)
 CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS)
 LDLIBS = -lm -lrt -pthread $(EXTRA_LDFLAGS)
-PRG    = ptp4l hwstamp_ctl nsm phc2sys phc_ctl pmc timemaster ts2phc
+PRG    = checksync ptp4l hwstamp_ctl nsm phc2sys phc_ctl pmc timemaster ts2phc
 FILTERS        = filter.o mave.o mmedian.o
 SERVOS = linreg.o ntpshm.o nullf.o pi.o servo.o
 TRANSP = raw.o transport.o udp.o udp6.o uds.o
@@ -34,7 +34,7 @@ OBJ   = bmc.o clock.o clockadj.o clockcheck.o config.o 
designated_fsm.o \
  sk.o stats.o tc.o $(TRANSP) telecom.o tlv.o tsproc.o unicast_client.o \
  unicast_fsm.o unicast_service.o util.o version.o
 
-OBJECTS        = $(OBJ) hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o pmc.o 
pmc_agent.o \
+OBJECTS        = $(OBJ) checksync.o hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o 
pmc.o pmc_agent.o \
  pmc_common.o sysoff.o timemaster.o $(TS2PHC)
 SRC    = $(OBJECTS:.o=.c)
 DEPEND = $(OBJECTS:.o=.d)
@@ -50,6 +50,10 @@ man8dir      = $(mandir)/man8
 
 all: $(PRG)
 
+checksync: config.o checksync.o interface.o hash.o msg.o phc.o pmc_common.o \
+ pmc_agent.o print.o raw.o sk.o tlv.o transport.o udp.o udp6.o uds.o util.o \
+ version.o
+
 ptp4l: $(OBJ)
 
 nsm: config.o $(FILTERS) hash.o interface.o msg.o nsm.o phc.o print.o \
-- 
2.25.1



_______________________________________________
Linuxptp-devel mailing list
Linuxptp-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/linuxptp-devel

Reply via email to