diff options
Diffstat (limited to 'contrib/ntp/ntpd/ntp_ppsdev.c')
-rw-r--r-- | contrib/ntp/ntpd/ntp_ppsdev.c | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/contrib/ntp/ntpd/ntp_ppsdev.c b/contrib/ntp/ntpd/ntp_ppsdev.c new file mode 100644 index 000000000000..d2d9727e36a7 --- /dev/null +++ b/contrib/ntp/ntpd/ntp_ppsdev.c @@ -0,0 +1,459 @@ +/* + * 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" + +#ifdef REFCLOCK + +#if defined(HAVE_UNISTD_H) +# include <unistd.h> +#endif +#if defined(HAVE_FCNTL_H) +# include <fcntl.h> +#endif + +#include <stdlib.h> + +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ +#if defined(__linux__) && defined(HAVE_OPENAT) && defined(HAVE_FDOPENDIR) +#define WITH_PPSDEV_MATCH +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ + +#include <stdio.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 a piece of allocated + * memory and trim any trailing whitespace. + * + * The issue here is that several files in the 'sysfs' tree claim a size + * of 4096 bytes when you 'stat' them -- but reading gives EOF after a + * few chars. (I *can* understand why the kernel takes this shortcut. + * it's just a bit unwieldy...) + */ +static char* +readFileAt( + int rfd , + const char *path) +{ + struct stat sb; + char *ret = NULL; + ssize_t rdlen; + int dfd; + + if (-1 == (dfd = openat(rfd, path, OModeF)) || -1 == fstat(dfd, &sb)) + goto fail; + if ((sb.st_size > 0x2000) || (NULL == (ret = malloc(sb.st_size + 1)))) + goto fail; + if (1 > (rdlen = read(dfd, ret, sb.st_size))) + goto fail; + close(dfd); + + while (rdlen > 0 && ret[rdlen - 1] <= ' ') + --rdlen; + ret[rdlen] = '\0'; + return ret; + + fail: + free(ret); + if (-1 != dfd) + close(dfd); + return NULL; +} + +/* -------------------------------------------------------------------- + * 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")) + goto done; + + 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); + + done: + 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; + + /* 'path' contains the primary path to the associated TTY: + * we 'stat()' for the device id in 'st_rdev'. + */ + if (NULL == (bufp = readFileAt(fdDir, "path"))) + goto done; + if ((-1 == stat(bufp, &sb)) || !S_ISCHR(sb.st_mode)) + goto done; + *pTty = sb.st_rdev; + free(bufp); + + /* 'dev' holds the device ID of the PPS device as 'major:minor' + * in text format. *sigh* couldn't that simply be the name of + * the PPS device itself, as in 'path' above??? But nooooo.... + */ + if (NULL == (bufp = readFileAt(fdDir, "dev"))) + goto done; + dmaj = strtoul((scan = bufp), &endp, 10); + if ((endp == scan) || (*endp != ':') || (dmaj >= 256)) + goto done; + dmin = strtoul((scan = endp + 1), &endp, 10); + if ((endp == scan) || (*endp >= ' ') || (dmin >= 256)) + goto done; + *pPps = makedev((unsigned int)dmaj, (unsigned int)dmin); + rc = TRUE; + + done: + 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 ClassDir; + struct dirent *dent; + dev_t othId, ppsId; + int fdDevDir; + + if (!xdirOpenAt(&ClassDir, AT_FDCWD, "/sys/class/pps")) + goto done; + + while (!found && (dent = readdir(ClassDir.dir))) { + + /* If the entry is not a referring to a PPS device or + * if we can't open the directory for reading, skipt it: + */ + if (strncmp("pps", dent->d_name, 3)) + continue; + fdDevDir = openat(ClassDir.dfd, dent->d_name, OModeD); + if (-1 == fdDevDir) + continue; + + /* get the data and check if device ID for the TTY + * is what we're looking for: + */ + found = getPpsTuple(fdDevDir, &othId, &ppsId) + && (ttyId == othId); + close(fdDevDir); + } + + xdirClose(&ClassDir); + + if (found) + *pPpsId = ppsId; + done: + 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 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 */ +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ + +#include "ntp_clockdev.h" + +int +ppsdev_reopen( + const sockaddr_u *srcadr, + int ttyfd , /* current tty FD, or -1 */ + int ppsfd , /* current pps FD, or -1 */ + const char *ppspath, /* path to pps device, or NULL */ + int omode , /* open mode for pps device */ + int oflags ) /* openn flags for pps device */ +{ + int retfd = -1; + const char *altpath; + + /* avoid 'unused' warnings: we might not use all args, no + * thanks to conditional compiling:) + */ + (void)ppspath; + (void)omode; + (void)oflags; + + if (NULL != (altpath = clockdev_lookup(srcadr, 1))) + ppspath = altpath; + +# 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) && (-1 != ttyfd)) { + 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 = ppsfd; + if (-1 == retfd) + retfd = ttyfd; + + /* Close the old pps FD, but only if the new pps FD is neither + * the tty FD nor the existing pps FD! + */ + if ((retfd != ttyfd) && (retfd != ppsfd)) + ppsdev_close(ttyfd, ppsfd); + + return retfd; +} + +void +ppsdev_close( + int ttyfd, /* current tty FD, or -1 */ + int ppsfd) /* current pps FD, or -1 */ +{ + /* The pps fd might be the same as the tty fd. We close the pps + * channel only if it's valid and _NOT_ the tty itself: + */ + if ((-1 != ppsfd) && (ttyfd != ppsfd)) + close(ppsfd); +} +/* --*-- that's all folks --*-- */ +#else +NONEMPTY_TRANSLATION_UNIT +#endif /* !defined(REFCLOCK) */ |