]> git.ipfire.org Git - thirdparty/ntp.git/commitdiff
[Bug 3757] Improve handling of Linux-PPS in NTPD
authorJuergen Perlinger <perlinger@ntp.org>
Mon, 7 Mar 2022 06:54:01 +0000 (07:54 +0100)
committerJuergen Perlinger <perlinger@ntp.org>
Mon, 7 Mar 2022 06:54:01 +0000 (07:54 +0100)
bk: 6225ac09yK5G6Ve4ohnjnnYIIwdpEA

ChangeLog
configure.ac
include/ntp_refclock.h
ntpd/Makefile.am
ntpd/ntp_ppsdev.c [new file with mode: 0644]
ntpd/refclock_nmea.c

index bed65246ac094c9410df6c09e63f0d064d489ea8..73b3d8cc11430b2449f7528300a1a4fe597ab914 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,6 @@
 ---
 * [Bug 3741] 4.2.8p15 can't build with glibc 2.34 <perlinger@ntp.org>
+* [Bug 3757] Improve handling of Linux-PPS in NTPD <perlinger@ntp.org>
 
 ---
 (4.2.8p15) 2020/06/23 Released by Harlan Stenn <stenn@ntp.org>
index 5dc6aee02edc763c02af86a9417269f5b70d045c..f2892997cecb9dbc5e20d2d12d522e52819b679c 100644 (file)
@@ -4295,6 +4295,26 @@ case "$ntp_signd_path" in
     ;;
 esac
 
+dnl check for 'magic pps' for Linux
+AC_MSG_CHECKING([if we want 'magic' PPS support])
+AC_ARG_ENABLE(
+    [magicpps],
+    [AS_HELP_STRING(
+       [--enable-magicpps],
+       [+ try to auto-instantiate PPS devices on Linux]
+    )],
+    [ans=$enableval],
+    [ans=yes]
+)
+AC_MSG_RESULT([$ans])
+case "$ans" in
+ yes)
+    AC_DEFINE([ENABLE_MAGICPPS], [1],
+       [auto-instantiate missing PPS devices on Linux])
+    AC_CHECK_FUNCS([openat fdopendir fstatat])
+    ;;
+esac
+
 AC_CHECK_HEADERS([libscf.h])
 LSCF=
 case "$ac_cv_header_libscf_h" in
index e74e77aa791ab846134e1033c205a2656a81e310..7f88d958638735035cf89fa0656825f3b12d2b3b 100644 (file)
@@ -250,6 +250,12 @@ extern int refclock_ppsaugment(
     const struct refclock_atom*, l_fp *rcvtime ,
     double rcvfudge, double ppsfudge);
 
+#ifdef _WIN32
+#define ppsdev_open(ttyfd, ppspath, mode, flags) (ttyfd)
+#else
+extern int ppsdev_open(int ttyfd, const char *ppspath,
+                      int mode, int flags);
+#endif
 #endif /* REFCLOCK */
 
 #endif /* NTP_REFCLOCK_H */
index b977d5e21cb188738cf571d63f9335545e13a83b..9b993c0fc38232aef986c7f68e7b863e00e94a50 100644 (file)
@@ -242,6 +242,7 @@ libntpd_a_SOURCES =         \
        ntp_monitor.c           \
        ntp_peer.c              \
        ntp_proto.c             \
+       ntp_ppsdev.c            \
        ntp_refclock.c          \
        ntp_request.c           \
        ntp_restrict.c          \
diff --git a/ntpd/ntp_ppsdev.c b/ntpd/ntp_ppsdev.c
new file mode 100644 (file)
index 0000000..17d6db8
--- /dev/null
@@ -0,0 +1,384 @@
+/*
+ * ntp_ppsdev.c - PPS-device support
+ *
+ * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
+ * The contents of 'html/copyright.html' apply.
+ * ---------------------------------------------------------------------
+ * Helper code to work around (or with) a Linux 'specialty': PPS devices
+ * are created via attaching the PPS line discipline to a TTY.  This
+ * creates new pps devices, and the PPS API is *not* available through
+ * the original TTY fd.
+ *
+ * Findig the PPS device associated with a TTY is possible but needs
+ * quite a bit of file system traversal & lookup in the 'sysfs' tree.
+ *
+ * The code below does the job for kernel versions 4 & 5, and will
+ * probably work for older and newer kernels, too... and in any case, if
+ * the device or symlink to the PPS device with the given name exists,
+ * it will take precedence anyway.
+ * ---------------------------------------------------------------------
+ */
+#ifdef __linux__
+# define _GNU_SOURCE
+#endif
+
+#include "config.h"
+#include "ntpd.h"
+
+/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
+#if defined(__linux__) && defined(HAVE_OPENAT) && defined(HAVE_FDOPENDIR)
+#define WITH_PPSDEV_MATCH
+/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <string.h>
+#include <errno.h>
+
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <linux/tty.h>
+
+typedef int BOOL;
+#ifndef TRUE
+# define TRUE 1
+#endif
+#ifndef FALSE
+# define FALSE 0
+#endif
+
+static const int OModeF = O_CLOEXEC|O_RDONLY|O_NOCTTY;
+static const int OModeD = O_CLOEXEC|O_RDONLY|O_DIRECTORY;
+
+/* ------------------------------------------------------------------ */
+/* extended directory stream
+ */
+typedef struct {
+       int  dfd;       /* file descriptor for dir for 'openat()' */
+       DIR *dir;       /* directory stream for iteration         */
+} XDIR;
+
+static void
+xdirClose(
+       XDIR *pxdir)
+{
+       if (NULL != pxdir->dir)
+               closedir(pxdir->dir); /* closes the internal FD, too! */
+       else if (-1 != pxdir->dfd)
+               close(pxdir->dfd);    /* otherwise _we_ have to do it */
+       pxdir->dfd = -1;
+       pxdir->dir = NULL;
+}
+
+static BOOL
+xdirOpenAt(
+       XDIR       *pxdir,
+       int         fdo  ,
+       const char *path )
+{
+       /* Officially, the directory stream owns the file discriptor it
+        * received via 'fdopendir()'.  But for the purpose of 'openat()'
+        * it's ok to keep the value around -- even if we should do
+        * _absolutely_nothing_ with it apart from using it as a path
+        * reference!
+        */
+       pxdir->dir = NULL;
+       if (-1 == (pxdir->dfd = openat(fdo, path, OModeD)))
+               goto fail;
+       if (NULL == (pxdir->dir = fdopendir(pxdir->dfd)))
+               goto fail;
+       return TRUE;
+       
+  fail:
+       xdirClose(pxdir);
+       return FALSE;
+}
+
+/* --------------------------------------------------------------------
+ * read content of a file (with a size limit) into  piece of allocated
+ * memory and trim any trailing whitespace.
+ */
+static char*
+readFileAt(
+       int         rfd ,
+       const char *path)
+{
+       struct stat sb;
+       char *ret = NULL, *tmp;
+       ssize_t rdlen;
+       int dfd;
+       
+       if (-1 == (dfd = openat(rfd, path, OModeF)) || -1 == fstat(dfd, &sb))
+               goto fail;
+       if ((sb.st_size > 0x2000) || (NULL == (tmp = malloc(sb.st_size + 1))))
+               goto fail;
+       if (1 > (rdlen = read(dfd, tmp, sb.st_size)))
+               goto fail;
+       while (rdlen > 0 && tmp[rdlen - 1] <= ' ')
+               --rdlen;
+       tmp[rdlen] = '\0';
+       ret = tmp;
+  fail:
+       if (-1 != dfd)
+               close(dfd);
+       return ret;    
+}
+
+/* --------------------------------------------------------------------
+ * Scan the "/dev" directory for a device with a given major and minor 
+ * device id. Return the path if found.
+ */
+static char*
+findDevByDevId(
+       dev_t rdev)
+{
+       struct stat    sb;
+       struct dirent *dent;
+       XDIR           xdir;
+       char          *name = NULL;
+       
+       if (xdirOpenAt(&xdir, AT_FDCWD, "/dev")) {
+               while (!name && (dent = readdir(xdir.dir))) {
+                       if (-1 == fstatat(xdir.dfd, dent->d_name,
+                                         &sb, AT_SYMLINK_NOFOLLOW))
+                               continue;
+                       if (!S_ISCHR(sb.st_mode))
+                               continue;
+                       if (sb.st_rdev == rdev) {
+                               if (-1 == asprintf(&name, "/dev/%s", dent->d_name))
+                                       name = NULL;
+                       }
+               }
+               xdirClose(&xdir);
+       }
+       return name;
+}
+
+/* --------------------------------------------------------------------
+ * Get the mofor:minor device id for a character device file descriptor
+ */
+static BOOL
+getCharDevId(
+       int          fd ,
+       dev_t       *out,
+       struct stat *psb)
+{
+       BOOL        rc = FALSE;
+       struct stat sb;
+       
+       if (NULL == psb)
+               psb = &sb;
+       if (-1 != fstat(fd, psb)) {
+               rc = S_ISCHR(psb->st_mode);
+               if (rc)
+                       *out = psb->st_rdev;
+               else
+                       errno = EINVAL;
+       }
+       return rc;
+}
+
+/* --------------------------------------------------------------------
+ * given the dir-fd of a pps instance dir in the linux sysfs tree, get
+ * the device IDs for the PPS device and the associated TTY.
+ */
+static BOOL
+getPpsTuple(
+       int   fdDir,
+       dev_t *pTty,
+       dev_t *pPps)
+{
+       BOOL          rc = FALSE;
+       unsigned long dmaj, dmin;
+       struct stat   sb;
+       char         *bufp, *endp, *scan;
+       
+       if (NULL == (bufp = readFileAt(fdDir, "path")))
+               goto fail;
+       if ((-1 == stat(bufp, &sb)) || !S_ISCHR(sb.st_mode))
+               goto fail;
+       *pTty = sb.st_rdev;
+       free(bufp);
+       
+       if (NULL == (bufp = readFileAt(fdDir, "dev")))
+               goto fail;
+       dmaj = strtoul((scan = bufp), &endp, 10);
+       if ((endp == scan) || (*endp != ':') || (dmaj >= 256))
+               goto fail;
+       dmin = strtoul((scan = endp + 1), &endp, 10);
+       if ((endp == scan) || (*endp >= ' ') || (dmin >= 256))
+               goto fail;
+       *pPps = makedev((unsigned int)dmaj, (unsigned int)dmin);
+       rc = TRUE;
+       
+  fail:
+       free(bufp);
+       return rc;      
+}
+
+/* --------------------------------------------------------------------
+ * for a given (TTY) device id, lookup the corresponding PPS device id
+ * by processing the contents of the kernel sysfs tree.
+ * Returns false if no such PS device can be found; otherwise set the
+ * ouput parameter to the PPS dev id and return true...
+ */
+static BOOL
+findPpsDevId(
+       dev_t  ttyId ,
+       dev_t *pPpsId)
+{
+       BOOL found = FALSE;
+       XDIR ClsDir;
+       
+       if (xdirOpenAt(&ClsDir, AT_FDCWD, "/sys/class/pps")) {
+               struct dirent *dent;
+               dev_t          othId, ppsId;
+               
+               while (!found && (dent = readdir(ClsDir.dir))) {
+                       int   fdDevDir;
+                       if (strncmp("pps", dent->d_name, 3))
+                               continue;
+                       if (-1 == (fdDevDir = openat(
+                                          ClsDir.dfd, dent->d_name, OModeD)))
+                               continue;
+                       found = getPpsTuple(fdDevDir, &othId, &ppsId)
+                            && (ttyId == othId);
+                       close(fdDevDir);
+               }
+               if (found)
+                       *pPpsId = ppsId;
+               xdirClose(&ClsDir);
+       }
+       return found;
+}
+
+/* --------------------------------------------------------------------
+ * Return the path to a PPS device related to tghe TT fd given. The
+ * function might even try to instantiate such a PPS device when
+ * running es effective root.  Returns NULL if no PPS device can be
+ * established; otherwise it is a 'malloc()'ed area that should be
+ * 'free()'d after use.
+ */
+static char*
+findMatchingPpsDev(
+       int fdtty)
+{
+       struct stat sb;
+       dev_t       ttyId, ppsId;
+       int         fdpps, ldisc = N_PPS;
+       char       *dpath = NULL;
+
+       /* Without the device identifier of the TTY, we're busted: */
+       if (!getCharDevId(fdtty, &ttyId, &sb))
+               goto done;
+
+       /* If we find a matching PPS device ID, return the path to the
+        * device. It might not open, but it's the best we can get.
+        */
+       if (findPpsDevId(ttyId, &ppsId)) {
+               dpath = findDevByDevId(ppsId);
+               goto done;
+       }
+       
+#   ifdef ENABLE_MAGICPPS
+       /* 'magic' PPS support -- try to instantiate missing PPS devices
+        * on-the-fly.  Our mileage may vary -- running as root at that
+        * moment is vital for success.  (We *can* create the PPS device
+        * as ordnary user, but we won't be able to open it!)
+        */
+       
+       /* If we're root, try to push the PPS LDISC to the tty FD. If
+        * that does not work out, we're busted again:
+        */
+       if ((0 != geteuid()) || (-1 == ioctl(fdtty, TIOCSETD, &ldisc)))
+               goto done;
+       msyslog(LOG_INFO, "auto-instantiated PPS device for device %u:%u",
+               major(ttyId), minor(ttyId));
+
+       /* We really should find a matching PPS device now. And since
+        * we're root (see above!), we should be able to open that device.
+        */
+       if (findPpsDevId(ttyId, &ppsId))
+               dpath = findDevByDevId(ppsId);
+       if (!dpath)
+               goto done;
+
+       /* And since ince we're 'root', we might as well try to clone
+        * the ownership and access rights from the original TTY to the
+        * PPS device.  If that does not work, we just have to live with
+        * what we've got so far...
+        */
+       if (-1 == (fdpps = open(dpath, OModeF))) {
+               msyslog(LOG_ERR, "could not open auto-created '%s': %m", dpath);
+               goto done;
+       }
+       if (-1 == fchmod(fdpps, sb.st_mode)) {
+               msyslog(LOG_ERR, "could not chmod auto-created '%s': %m", dpath);
+       }
+       if (-1 == fchown(fdpps, sb.st_uid, sb.st_gid)) {
+               msyslog(LOG_ERR, "could not chown auto-created '%s': %m", dpath);
+       }
+       close(fdpps);
+#   else
+       (void)ldisc;
+#   endif
+       
+  done:
+       /* Whatever we go so far, that's it. */
+       return dpath;
+}
+
+/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
+#endif /* linux PPS device matcher */
+/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
+
+
+int
+ppsdev_open(
+       int         ttyfd  ,
+       const char *ppspath,
+       int         omode  ,
+       int         oflags )
+{
+       int retfd = -1;
+
+#   if defined(__unix__) && !defined(_WIN32)
+       if (-1 == retfd) {      
+               if (ppspath && *ppspath) {
+                       retfd = open(ppspath, omode, oflags);
+                       msyslog(LOG_INFO, "ppsdev_open(%s) %s",
+                               ppspath, (retfd != -1 ? "succeeded" : "failed"));
+               }
+       }
+#   endif
+       
+#   if defined(WITH_PPSDEV_MATCH)
+       if (-1 == retfd) {      
+               char *xpath = findMatchingPpsDev(ttyfd);
+               if (xpath && *xpath) {
+                       retfd = open(xpath, omode, oflags);
+                       msyslog(LOG_INFO, "ppsdev_open(%s) %s",
+                               xpath, (retfd != -1 ? "succeeded" : "failed"));
+               }
+               free(xpath);
+       }
+#   endif
+       
+       /* BSDs and probably SOLARIS can use the TTY fd for the PPS API,
+        * and so does Windows where the PPS API is implemented via an
+        * IOCTL.  Likewise does the 'SoftPPS' implementation in Windows
+        * based on COM Events.  So, if everything else fails, simply
+        * try the FD given for the TTY/COMport...
+        */
+       if (-1 == retfd)
+               retfd = ttyfd;
+       
+       return retfd;
+}
+
+/* --*-- that's all folks --*-- */
index 1d7997c6aaf99bdbcb7a0957e4f7de5ee23d519d..0b9e2ea7e4ef8920ee7af0b5b6a480a4fbd0b742 100644 (file)
@@ -472,7 +472,7 @@ nmea_shutdown(
 #          ifdef HAVE_PPSAPI
                if (up->ppsapi_lit)
                        time_pps_destroy(up->atom.handle);
-               if (up->ppsapi_tried && up->ppsapi_fd != pp->io.fd)
+               if ((up->ppsapi_fd != -1) && (up->ppsapi_fd != pp->io.fd))
                        close(up->ppsapi_fd);
 #          endif
                free(up);
@@ -510,25 +510,48 @@ nmea_control(
         * PPS control
         *
         * If /dev/gpspps$UNIT can be opened that will be used for
-        * PPSAPI.  Otherwise, the GPS serial device /dev/gps$UNIT
-        * already opened is used for PPSAPI as well. (This might not
-        * work, in which case the PPS API remains unavailable...)
+        * PPSAPI.  On Linux, a PPS device mathing the TTY will be
+        * searched for and possibly created on the fly.  Otherwise, the
+        * GPS serial device /dev/gps$UNIT already opened is used for
+        * PPSAPI as well. (This might not work, in which case the PPS
+        * API remains unavailable...)
         */
 
        /* Light up the PPSAPI interface if not yet attempted. */
        if ((CLK_FLAG1 & pp->sloppyclockflag) && !up->ppsapi_tried) {
+               int ppsfd;
                up->ppsapi_tried = TRUE;
+               /* get FD for the pps device; might be the tty itself! */
                devlen = snprintf(device, sizeof(device), PPSDEV, unit);
                if (devlen < sizeof(device)) {
-                       up->ppsapi_fd = open(device, PPSOPENMODE,
-                                            S_IRUSR | S_IWUSR);
+                       ppsfd = ppsdev_open(
+                               pp->io.fd, device,
+                               PPSOPENMODE, (S_IRUSR|S_IWUSR));
                } else {
-                       up->ppsapi_fd = -1;
+                       ppsfd = pp->io.fd;
                        msyslog(LOG_ERR, "%s PPS device name too long",
                                refnumtoa(&peer->srcadr));
                }
-               if (-1 == up->ppsapi_fd)
-                       up->ppsapi_fd = pp->io.fd;
+               /* Now do a dance to juggle it into place: */
+               if (-1 == up->ppsapi_fd) {
+                       /* no previous FD -- that one is easy. */
+                       up->ppsapi_fd = ppsfd;
+               } else if (ppsfd != pp->io.fd) {
+                       /* new distinct pps FD -- take it! */
+                       if (up->ppsapi_fd != pp->io.fd)
+                               close(up->ppsapi_fd);
+                       up->ppsapi_fd = ppsfd;                  
+               }
+               /* If neither condition above is met, we have to keep
+                * the existing pps handle:  It is either a device we
+                * could not open again since we dropped privs, or it is
+                * the tty handle because there was nothing else to open
+                * right from the beginning.
+                *
+                * note: the PPS I/O handle remains valid until
+                *  - the clock is shut down
+                *  - flag1 is set again after being cleared
+                */
                if (refclock_ppsapi(up->ppsapi_fd, &up->atom)) {
                        /* use the PPS API for our own purposes now. */
                        up->ppsapi_lit = refclock_params(
@@ -540,9 +563,6 @@ nmea_control(
                                        "%s set PPSAPI params fails",
                                        refnumtoa(&peer->srcadr));
                        }
-                       /* note: the PPS I/O handle remains valid until
-                        * flag1 is cleared or the clock is shut down.
-                        */
                } else {
                        msyslog(LOG_WARNING,
                                "%s flag1 1 but PPSAPI fails",
@@ -556,10 +576,7 @@ nmea_control(
                if (up->ppsapi_lit)
                        time_pps_destroy(up->atom.handle);
                up->atom.handle = 0;
-               /* close/drop PPS fd */
-               if (up->ppsapi_fd != pp->io.fd)
-                       close(up->ppsapi_fd);
-               up->ppsapi_fd = -1;
+               /* do !!NOT!! close/drop PPS fd here! */
 
                /* clear markers and peer items */
                up->ppsapi_gate  = FALSE;