summaryrefslogtreecommitdiffstats
path: root/src/shared/watchdog.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/watchdog.c')
-rw-r--r--src/shared/watchdog.c504
1 files changed, 504 insertions, 0 deletions
diff --git a/src/shared/watchdog.c b/src/shared/watchdog.c
new file mode 100644
index 0000000..2d79f71
--- /dev/null
+++ b/src/shared/watchdog.c
@@ -0,0 +1,504 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <linux/watchdog.h>
+
+#include "devnum-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "log.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "time-util.h"
+#include "watchdog.h"
+
+static int watchdog_fd = -EBADF;
+static char *watchdog_device = NULL;
+static usec_t watchdog_timeout = 0; /* 0 → close device and USEC_INFINITY → don't change timeout */
+static usec_t watchdog_pretimeout = 0; /* 0 → disable pretimeout and USEC_INFINITY → don't change pretimeout */
+static usec_t watchdog_last_ping = USEC_INFINITY;
+static bool watchdog_supports_pretimeout = false; /* Depends on kernel state that might change at runtime */
+static char *watchdog_pretimeout_governor = NULL;
+
+/* Starting from kernel version 4.5, the maximum allowable watchdog timeout is
+ * UINT_MAX/1000U seconds (since internal calculations are done in milliseconds
+ * using unsigned integers. However, the kernel's userspace API for the watchdog
+ * uses signed integers for its ioctl parameters (even for timeout values and
+ * bit flags) so this is why we must consider the maximum signed integer value
+ * as well.
+ */
+#define WATCHDOG_TIMEOUT_MAX_SEC (CONST_MIN(UINT_MAX/1000U, (unsigned)INT_MAX))
+
+#define WATCHDOG_GOV_NAME_MAXLEN 20 /* From the kernel watchdog driver */
+
+static int saturated_usec_to_sec(usec_t val) {
+ usec_t t = DIV_ROUND_UP(val, USEC_PER_SEC);
+ return MIN(t, (usec_t) WATCHDOG_TIMEOUT_MAX_SEC); /* Saturate to watchdog max */
+}
+
+static int get_watchdog_sysfs_path(const char *filename, char **ret_path) {
+ struct stat st;
+
+ if (watchdog_fd < 0)
+ return -EBADF;
+
+ if (fstat(watchdog_fd, &st))
+ return -errno;
+
+ if (!S_ISCHR(st.st_mode))
+ return -EBADF;
+
+ if (asprintf(ret_path, "/sys/dev/char/"DEVNUM_FORMAT_STR"/%s", DEVNUM_FORMAT_VAL(st.st_rdev), filename) < 0)
+ return -ENOMEM;
+
+ return 0;
+}
+
+static int get_pretimeout_governor(char **ret_gov) {
+ _cleanup_free_ char *sys_fn = NULL;
+ int r;
+
+ r = get_watchdog_sysfs_path("pretimeout_governor", &sys_fn);
+ if (r < 0)
+ return r;
+
+ log_info("Watchdog: reading from %s", sys_fn);
+
+ r = read_virtual_file(sys_fn, WATCHDOG_GOV_NAME_MAXLEN - 1, ret_gov, NULL);
+ if (r < 0)
+ return r;
+
+ delete_trailing_chars(*ret_gov, WHITESPACE);
+
+ return 0;
+}
+
+static int set_pretimeout_governor(const char *governor) {
+ _cleanup_free_ char *sys_fn = NULL;
+ int r;
+
+ if (isempty(governor))
+ return 0; /* Nothing to do */
+
+ r = get_watchdog_sysfs_path("pretimeout_governor", &sys_fn);
+ if (r < 0)
+ return r;
+
+ log_info("Watchdog: setting pretimeout_governor to '%s' via '%s'", governor, sys_fn);
+
+ r = write_string_file(sys_fn,
+ governor,
+ WRITE_STRING_FILE_DISABLE_BUFFER | WRITE_STRING_FILE_VERIFY_ON_FAILURE | WRITE_STRING_FILE_VERIFY_IGNORE_NEWLINE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set pretimeout_governor to '%s': %m", governor);
+
+ return r;
+}
+
+static int watchdog_set_enable(bool enable) {
+ int flags = enable ? WDIOS_ENABLECARD : WDIOS_DISABLECARD;
+
+ assert(watchdog_fd >= 0);
+
+ if (ioctl(watchdog_fd, WDIOC_SETOPTIONS, &flags) < 0) {
+ if (!enable)
+ return log_warning_errno(errno, "Failed to disable hardware watchdog, ignoring: %m");
+
+ /* ENOTTY means the watchdog is always enabled so we're fine */
+ log_full_errno(ERRNO_IS_NOT_SUPPORTED(errno) ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to enable hardware watchdog, ignoring: %m");
+ if (!ERRNO_IS_NOT_SUPPORTED(errno))
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int watchdog_read_timeout(void) {
+ int sec = 0;
+
+ assert(watchdog_fd >= 0);
+
+ if (ioctl(watchdog_fd, WDIOC_GETTIMEOUT, &sec) < 0)
+ return -errno;
+
+ assert(sec > 0);
+ watchdog_timeout = sec * USEC_PER_SEC;
+
+ return 0;
+}
+
+static int watchdog_set_timeout(void) {
+ int sec;
+
+ assert(watchdog_fd >= 0);
+ assert(timestamp_is_set(watchdog_timeout));
+
+ sec = saturated_usec_to_sec(watchdog_timeout);
+
+ if (ioctl(watchdog_fd, WDIOC_SETTIMEOUT, &sec) < 0)
+ return -errno;
+
+ assert(sec > 0); /* buggy driver ? */
+ watchdog_timeout = sec * USEC_PER_SEC;
+
+ return 0;
+}
+
+static int watchdog_read_pretimeout(void) {
+ int sec = 0;
+
+ assert(watchdog_fd >= 0);
+
+ if (ioctl(watchdog_fd, WDIOC_GETPRETIMEOUT, &sec) < 0) {
+ watchdog_pretimeout = 0;
+ return log_full_errno(ERRNO_IS_NOT_SUPPORTED(errno) ? LOG_DEBUG : LOG_WARNING, errno, "Failed to get pretimeout value, ignoring: %m");
+ }
+
+ watchdog_pretimeout = sec * USEC_PER_SEC;
+
+ return 0;
+}
+
+static int watchdog_set_pretimeout(void) {
+ int sec;
+
+ assert(watchdog_fd >= 0);
+ assert(watchdog_pretimeout != USEC_INFINITY);
+
+ sec = saturated_usec_to_sec(watchdog_pretimeout);
+
+ if (ioctl(watchdog_fd, WDIOC_SETPRETIMEOUT, &sec) < 0) {
+ watchdog_pretimeout = 0;
+
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_info("Watchdog does not support pretimeouts.");
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to set pretimeout to %s: %m", FORMAT_TIMESPAN(sec, USEC_PER_SEC));
+ }
+
+ /* The set ioctl does not return the actual value set so get it now. */
+ (void) watchdog_read_pretimeout();
+
+ return 0;
+}
+
+usec_t watchdog_get_last_ping(clockid_t clock) {
+ return map_clock_usec(watchdog_last_ping, CLOCK_BOOTTIME, clock);
+}
+
+static int watchdog_ping_now(void) {
+ assert(watchdog_fd >= 0);
+
+ if (ioctl(watchdog_fd, WDIOC_KEEPALIVE, 0) < 0)
+ return log_warning_errno(errno, "Failed to ping hardware watchdog, ignoring: %m");
+
+ watchdog_last_ping = now(CLOCK_BOOTTIME);
+
+ return 0;
+}
+
+static int update_pretimeout(void) {
+ _cleanup_free_ char *governor = NULL;
+ int r, t_sec, pt_sec;
+
+ if (watchdog_fd < 0)
+ return 0;
+
+ if (watchdog_timeout == USEC_INFINITY || watchdog_pretimeout == USEC_INFINITY)
+ return 0;
+
+ if (!watchdog_supports_pretimeout && watchdog_pretimeout == 0)
+ return 0; /* Nothing to do */
+
+ /* The configuration changed, do not assume it can still work, as the module(s)
+ * might have been unloaded. */
+ watchdog_supports_pretimeout = false;
+
+ /* Update the pretimeout governor as well */
+ (void) set_pretimeout_governor(watchdog_pretimeout_governor);
+
+ r = get_pretimeout_governor(&governor);
+ if (r < 0)
+ return log_warning_errno(r, "Watchdog: failed to read pretimeout governor: %m");
+ if (isempty(governor))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Watchdog: no pretimeout governor detected - is the required kernel module loaded?");
+
+ /* If we have a pretimeout governor, then pretimeout is supported. Without a governor
+ * pretimeout does not work at all.
+ * Note that this might require a kernel module that is not autoloaded, so we don't
+ * cache this, but we check every time the configuration changes. */
+ watchdog_supports_pretimeout = true;
+
+ /* Determine if the pretimeout is valid for the current watchdog timeout. */
+ t_sec = saturated_usec_to_sec(watchdog_timeout);
+ pt_sec = saturated_usec_to_sec(watchdog_pretimeout);
+ if (pt_sec >= t_sec) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Cannot set watchdog pretimeout to %is (%s watchdog timeout of %is)",
+ pt_sec, pt_sec == t_sec ? "same as" : "longer than", t_sec);
+ (void) watchdog_read_pretimeout();
+ } else
+ r = watchdog_set_pretimeout();
+
+ if (watchdog_pretimeout == 0)
+ log_info("Watchdog pretimeout is disabled.");
+ else
+ log_info("Watchdog running with a pretimeout of %s with governor '%s'.",
+ FORMAT_TIMESPAN(watchdog_pretimeout, 0),
+ governor);
+
+ return r;
+}
+
+static int update_timeout(void) {
+ int r;
+ usec_t previous_timeout;
+
+ assert(watchdog_timeout > 0);
+
+ if (watchdog_fd < 0)
+ return 0;
+
+ previous_timeout = watchdog_timeout;
+
+ if (watchdog_timeout != USEC_INFINITY) {
+ r = watchdog_set_timeout();
+ if (r < 0) {
+ if (!ERRNO_IS_NOT_SUPPORTED(r))
+ return log_error_errno(r, "Failed to set timeout to %s: %m",
+ FORMAT_TIMESPAN(watchdog_timeout, 0));
+
+ log_info("Modifying watchdog timeout is not supported, reusing the programmed timeout.");
+ watchdog_timeout = USEC_INFINITY;
+ }
+ }
+
+ if (watchdog_timeout == USEC_INFINITY) {
+ r = watchdog_read_timeout();
+ if (r < 0) {
+ if (!ERRNO_IS_NOT_SUPPORTED(r))
+ return log_error_errno(r, "Failed to query watchdog HW timeout: %m");
+ log_info("Reading watchdog timeout is not supported, reusing the configured timeout.");
+ watchdog_timeout = previous_timeout;
+ }
+ }
+
+ /* If the watchdog timeout was changed, the pretimeout could have been
+ * changed as well by the driver or the kernel so we need to update the
+ * pretimeout now. Or if the watchdog is being configured for the first
+ * time, we want to configure the pretimeout before it is enabled. */
+ (void) update_pretimeout();
+
+ r = watchdog_set_enable(true);
+ if (r < 0)
+ return r;
+
+ log_info("Watchdog running with a timeout of %s.", FORMAT_TIMESPAN(watchdog_timeout, 0));
+
+ return watchdog_ping_now();
+}
+
+static int open_watchdog(void) {
+ struct watchdog_info ident;
+ char **try_order;
+ int r;
+
+ if (watchdog_fd >= 0)
+ return 0;
+
+ /* Let's prefer new-style /dev/watchdog0 (i.e. kernel 3.5+) over classic /dev/watchdog. The former
+ * has the benefit that we can easily find the matching directory in sysfs from it, as the relevant
+ * sysfs attributes can only be found via /sys/dev/char/<major>:<minor> if the new-style device
+ * major/minor is used, not the old-style. */
+ try_order = !watchdog_device || PATH_IN_SET(watchdog_device, "/dev/watchdog", "/dev/watchdog0") ?
+ STRV_MAKE("/dev/watchdog0", "/dev/watchdog") : STRV_MAKE(watchdog_device);
+
+ STRV_FOREACH(wd, try_order) {
+ watchdog_fd = open(*wd, O_WRONLY|O_CLOEXEC);
+ if (watchdog_fd >= 0) {
+ if (free_and_strdup(&watchdog_device, *wd) < 0) {
+ r = log_oom_debug();
+ goto close_and_fail;
+ }
+
+ break;
+ }
+
+ if (errno != ENOENT)
+ return log_debug_errno(errno, "Failed to open watchdog device %s: %m", *wd);
+ }
+
+ if (watchdog_fd < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to open watchdog device %s: %m", watchdog_device ?: "auto");
+
+ if (ioctl(watchdog_fd, WDIOC_GETSUPPORT, &ident) < 0)
+ log_debug_errno(errno, "Hardware watchdog %s does not support WDIOC_GETSUPPORT ioctl, ignoring: %m", watchdog_device);
+ else
+ log_info("Using hardware watchdog '%s', version %x, device %s",
+ ident.identity,
+ ident.firmware_version,
+ watchdog_device);
+
+ r = update_timeout();
+ if (r < 0)
+ goto close_and_fail;
+
+ return 0;
+
+close_and_fail:
+ watchdog_close(/* disarm= */ true);
+ return r;
+}
+
+const char *watchdog_get_device(void) {
+ return watchdog_device;
+}
+
+int watchdog_set_device(const char *path) {
+ int r;
+
+ r = free_and_strdup(&watchdog_device, path);
+ if (r > 0) /* watchdog_device changed */
+ watchdog_close(/* disarm= */ true);
+
+ return r;
+}
+
+int watchdog_setup(usec_t timeout) {
+ usec_t previous_timeout;
+ int r;
+
+ /* timeout=0 closes the device whereas passing timeout=USEC_INFINITY opens it (if needed)
+ * without configuring any particular timeout and thus reuses the programmed value (therefore
+ * it's a nop if the device is already opened). */
+
+ if (timeout == 0) {
+ watchdog_close(true);
+ return 0;
+ }
+
+ /* Let's shortcut duplicated requests */
+ if (watchdog_fd >= 0 && (timeout == watchdog_timeout || timeout == USEC_INFINITY))
+ return 0;
+
+ /* Initialize the watchdog timeout with the caller value. This value is going to be updated by
+ * update_timeout() with the closest value supported by the driver */
+ previous_timeout = watchdog_timeout;
+ watchdog_timeout = timeout;
+
+ if (watchdog_fd < 0)
+ return open_watchdog();
+
+ r = update_timeout();
+ if (r < 0)
+ watchdog_timeout = previous_timeout;
+
+ return r;
+}
+
+int watchdog_setup_pretimeout(usec_t timeout) {
+ /* timeout=0 disables the pretimeout whereas timeout=USEC_INFINITY is a nop. */
+ if ((watchdog_fd >= 0 && timeout == watchdog_pretimeout) || timeout == USEC_INFINITY)
+ return 0;
+
+ /* Initialize the watchdog timeout with the caller value. This value is
+ * going to be updated by update_pretimeout() with the running value,
+ * even if it fails to update the timeout. */
+ watchdog_pretimeout = timeout;
+
+ return update_pretimeout();
+}
+
+int watchdog_setup_pretimeout_governor(const char *governor) {
+ if (free_and_strdup(&watchdog_pretimeout_governor, governor) < 0)
+ return -ENOMEM;
+
+ return set_pretimeout_governor(watchdog_pretimeout_governor);
+}
+
+static usec_t calc_timeout(void) {
+ /* Calculate the effective timeout which accounts for the watchdog
+ * pretimeout if configured and supported. */
+ if (watchdog_supports_pretimeout && timestamp_is_set(watchdog_pretimeout) && watchdog_timeout >= watchdog_pretimeout)
+ return watchdog_timeout - watchdog_pretimeout;
+ else
+ return watchdog_timeout;
+}
+
+usec_t watchdog_runtime_wait(void) {
+ usec_t timeout = calc_timeout();
+ if (!timestamp_is_set(timeout))
+ return USEC_INFINITY;
+
+ /* Sleep half the watchdog timeout since the last successful ping at most */
+ if (timestamp_is_set(watchdog_last_ping)) {
+ usec_t ntime = now(CLOCK_BOOTTIME);
+
+ assert(ntime >= watchdog_last_ping);
+ return usec_sub_unsigned(watchdog_last_ping + (timeout / 2), ntime);
+ }
+
+ return timeout / 2;
+}
+
+int watchdog_ping(void) {
+ usec_t ntime, timeout;
+
+ if (watchdog_timeout == 0)
+ return 0;
+
+ if (watchdog_fd < 0)
+ /* open_watchdog() will automatically ping the device for us if necessary */
+ return open_watchdog();
+
+ ntime = now(CLOCK_BOOTTIME);
+ timeout = calc_timeout();
+
+ /* Never ping earlier than watchdog_timeout/4 and try to ping
+ * by watchdog_timeout/2 plus scheduling latencies at the latest */
+ if (timestamp_is_set(watchdog_last_ping)) {
+ assert(ntime >= watchdog_last_ping);
+ if ((ntime - watchdog_last_ping) < (timeout / 4))
+ return 0;
+ }
+
+ return watchdog_ping_now();
+}
+
+void watchdog_close(bool disarm) {
+
+ /* Once closed, pinging the device becomes a NOP and we request a new
+ * call to watchdog_setup() to open the device again. */
+ watchdog_timeout = 0;
+
+ if (watchdog_fd < 0)
+ return;
+
+ if (disarm) {
+ (void) watchdog_set_enable(false);
+
+ /* To be sure, use magic close logic, too */
+ for (;;) {
+ static const char v = 'V';
+
+ if (write(watchdog_fd, &v, 1) > 0)
+ break;
+
+ if (errno != EINTR) {
+ log_warning_errno(errno, "Failed to disarm watchdog timer, ignoring: %m");
+ break;
+ }
+ }
+ }
+
+ watchdog_fd = safe_close(watchdog_fd);
+}