main: Add a 'dbus' set of sink actions

The sysrq sink actions are are intended for use with kdump, which will
capture relevant kernel and userspace memory. It's implementation is
thus pretty straight forward.

Most BMCs don't use kdump, so implement a set of sink actions that talk
via D-Bus to generate a dump and gracefully reboot the BMC.

Signed-off-by: Andrew Jeffery <andrew@aj.id.au>
Change-Id: I126b0118faaa793011268a785eeb955139739eaf
diff --git a/main.c b/main.c
index c064035..d63d74c 100644
--- a/main.c
+++ b/main.c
@@ -10,7 +10,8 @@
  *
  * Options:
  *  --sink-actions=ACTION
- *	Set the class of sink action(s) to be used. Defaults to 'sysrq'
+ *	Set the class of sink action(s) to be used. Can take the value of 'sysrq' or 'dbus'.
+ *	Defaults to 'sysrq'.
  *
  * Examples:
  *  debug-trigger
@@ -26,7 +27,15 @@
  *	'sysrq' set of sink actions. When 'D' is read from /dev/serio_raw0 'c' will be written to
  *	/proc/sysrq-trigger, causing a kernel panic. When 'R' is read from /dev/serio_raw0 'b' will
  *	be written to /proc/sysrq-trigger, causing an immediate reboot of the system.
+ *
+ *  dbug-trigger --sink-actions=dbus /dev/serio_raw0
+ *	Open /dev/serio_raw0 as the source and configure the 'dbus' set of sink actions. When 'D' is
+ *	read from /dev/serio_raw0 create a dump via phosphor-debug-collector by calling through its
+ *	D-Bus interface, then reboot the system by starting systemd's 'reboot.target'
  */
+#define _GNU_SOURCE
+
+#include "config.h"
 
 #include <err.h>
 #include <errno.h>
@@ -35,6 +44,9 @@
 #include <libgen.h>
 #include <limits.h>
 #include <linux/reboot.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/reboot.h>
@@ -42,6 +54,10 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0]))
+
+struct sd_bus;
+
 struct debug_source_ops {
 	int (*poll)(void *ctx, char *op);
 };
@@ -55,6 +71,13 @@
 	int source;
 };
 
+struct debug_source_dbus {
+	struct sd_bus *bus;
+#define DBUS_SOURCE_PFD_SOURCE	0
+#define DBUS_SOURCE_PFD_DBUS	1
+	struct pollfd pfds[2];
+};
+
 struct debug_sink_ops {
 	void (*debug)(void *ctx);
 	void (*reboot)(void *ctx);
@@ -69,6 +92,10 @@
 	int sink;
 };
 
+struct debug_sink_dbus {
+	struct sd_bus *bus;
+};
+
 static void sysrq_sink_debug(void *ctx)
 {
 	struct debug_sink_sysrq *sysrq = ctx;
@@ -107,11 +134,6 @@
 	}
 }
 
-const struct debug_sink_ops sysrq_sink_ops = {
-	.debug = sysrq_sink_debug,
-	.reboot = sysrq_sink_reboot,
-};
-
 static int basic_source_poll(void *ctx, char *op)
 {
 	struct debug_source_basic *basic = ctx;
@@ -130,10 +152,317 @@
 	return 0;
 }
 
+const struct debug_sink_ops sysrq_sink_ops = {
+	.debug = sysrq_sink_debug,
+	.reboot = sysrq_sink_reboot,
+};
+
 const struct debug_source_ops basic_source_ops = {
 	.poll = basic_source_poll,
 };
 
+#if HAVE_SYSTEMD
+#include <systemd/sd-bus.h>
+
+static void dbus_sink_reboot(void *ctx);
+static int dbus_sink_dump_progress(sd_bus_message *m, void *userdata,
+				   sd_bus_error *ret_error __attribute__((unused)))
+{
+	struct debug_sink_dbus *dbus = userdata;
+	const char *status;
+	const char *iface;
+	int rc;
+
+	// sa{sv}as
+	rc = sd_bus_message_read_basic(m, 's', &iface);
+	if (rc < 0) {
+		warnx("Failed to extract interface from PropertiesChanged signal: %s",
+		      strerror(-rc));
+		return rc;
+	}
+
+	/* Bail if it's not an update to the Progress interface */
+	if (strcmp(iface, "xyz.openbmc_project.Common.Progress"))
+		return 0;
+
+	rc = sd_bus_message_enter_container(m, 'a', "{sv}");
+	if (rc < 0)
+		return rc;
+
+	if (!rc)
+		return 0;
+
+	status = NULL;
+	while (1) {
+		const char *member;
+
+		rc = sd_bus_message_enter_container(m, 'e', "sv");
+		if (rc < 0)
+			return rc;
+
+		if (!rc)
+			break;
+
+		rc = sd_bus_message_read_basic(m, 's', &member);
+		if (rc < 0) {
+			warnx("Failed to extract member name from PropertiesChanged signal: %s",
+			      strerror(-rc));
+			return rc;
+		}
+
+		if (!strcmp(member, "Status")) {
+			rc = sd_bus_message_enter_container(m, 'v', "s");
+			if (rc < 0) {
+				warnx("Failed to enter variant container in PropertiesChanged signal: %s",
+				      strerror(-rc));
+				return rc;
+			}
+
+			if (!rc)
+				goto exit_dict_container;
+
+			rc = sd_bus_message_read_basic(m, 's', &status);
+			if (rc < 0) {
+				warnx("Failed to extract status value from PropertiesChanged signal: %s",
+				      strerror(-rc));
+				return rc;
+			}
+
+			sd_bus_message_exit_container(m);
+		} else {
+			rc = sd_bus_message_skip(m, "v");
+			if (rc < 0) {
+				warnx("Failed to skip variant for unrecognised member %s in PropertiesChanged signal: %s",
+				      member, strerror(-rc));
+				return rc;
+			}
+		}
+
+exit_dict_container:
+		sd_bus_message_exit_container(m);
+	}
+
+	sd_bus_message_exit_container(m);
+
+	if (!status)
+		return 0;
+
+	printf("Dump progress on %s: %s\n", sd_bus_message_get_path(m), status);
+
+	/* If we're finished with the dump, reboot the system */
+	if (!strcmp(status, "xyz.openbmc_project.Common.Progress.OperationStatus.Completed")) {
+		sd_bus_slot *slot = sd_bus_get_current_slot(dbus->bus);
+		sd_bus_slot_unref(slot);
+		dbus_sink_reboot(userdata);
+	}
+
+	return 0;
+}
+
+static void dbus_sink_debug(void *ctx)
+{
+	sd_bus_error ret_error = SD_BUS_ERROR_NULL;
+	struct debug_sink_dbus *dbus = ctx;
+	sd_bus_message *reply;
+	sd_bus_slot *slot;
+	const char *path;
+	char *status;
+	int rc;
+
+	/* Start a BMC dump */
+	rc = sd_bus_call_method(dbus->bus,
+				"xyz.openbmc_project.Dump.Manager",
+				"/xyz/openbmc_project/dump/bmc",
+				"xyz.openbmc_project.Dump.Create",
+				"CreateDump",
+				&ret_error,
+				&reply, "a{sv}", 0);
+	if (rc < 0) {
+		warnx("Failed to call CreateDump: %s", strerror(-rc));
+		return;
+	}
+
+	/* Extract the dump path */
+	rc = sd_bus_message_read_basic(reply, 'o', &path);
+	if (rc < 0) {
+		warnx("Failed to extract dump object path: %s", strerror(-rc));
+		goto cleanup_reply;
+	}
+
+	/* Set up a match watching for completion of the dump */
+	rc = sd_bus_match_signal(dbus->bus,
+				 &slot,
+				 "xyz.openbmc_project.Dump.Manager",
+				 path,
+				 "org.freedesktop.DBus.Properties",
+				 "PropertiesChanged",
+				 dbus_sink_dump_progress,
+				 ctx);
+	if (rc < 0) {
+		warnx("Failed to add signal match for progress status on dump object %s: %s",
+		      path, strerror(-rc));
+		goto cleanup_reply;
+	}
+
+	/*
+	 * Mark the slot as 'floating'. If a slot is _not_ marked as floating it holds a reference
+	 * to the bus, and the bus will stay alive so long as the slot is referenced. If the slot is
+	 * instead marked floating the relationship is inverted: The lifetime of the slot is defined
+	 * in terms of the bus, which means we relieve ourselves of having to track the lifetime of
+	 * the slot.
+	 *
+	 * For more details see `man 3 sd_bus_slot_set_floating`, also documented here:
+	 *
+	 * https://www.freedesktop.org/software/systemd/man/sd_bus_slot_set_floating.html
+	 */
+	rc = sd_bus_slot_set_floating(slot, 0);
+	if (rc < 0) {
+		warnx("Failed to mark progress match slot on %s as floating: %s",
+		      path, strerror(-rc));
+		goto cleanup_reply;
+	}
+
+	printf("Registered progress match on dump object %s\n", path);
+
+	/* Now that the match is set up, check the current value in case we missed any updates */
+	rc = sd_bus_get_property_string(dbus->bus,
+					"xyz.openbmc_project.Dump.Manager",
+					path,
+					"xyz.openbmc_project.Common.Progress",
+					"Status",
+					&ret_error,
+					&status);
+	if (rc < 0) {
+		warnx("Failed to get progress status property on dump object %s: %s",
+		      path, strerror(-rc));
+		sd_bus_slot_unref(slot);
+		goto cleanup_reply;
+	}
+
+	printf("Dump state for %s is currently %s\n", path, status);
+
+	/*
+	 * If we're finished with the dump, reboot the system. If the dump isn't finished the reboot
+	 * will instead take place via the dbus_sink_dump_progress() callback on the match.
+	 */
+	if (!strcmp(status, "xyz.openbmc_project.Common.Progress.OperationStatus.Completed")) {
+		sd_bus_slot_unref(slot);
+		dbus_sink_reboot(ctx);
+	}
+
+cleanup_reply:
+	sd_bus_message_unref(reply);
+}
+
+static void dbus_sink_reboot(void *ctx)
+{
+	sd_bus_error ret_error = SD_BUS_ERROR_NULL;
+	struct debug_sink_dbus *dbus = ctx;
+	sd_bus_message *reply;
+	int rc;
+
+	warnx("Rebooting the system");
+
+	rc = sd_bus_call_method(dbus->bus,
+				"org.freedesktop.systemd1",
+				"/org/freedesktop/systemd1",
+				"org.freedesktop.systemd1.Manager",
+				"StartUnit",
+				&ret_error,
+				&reply,
+				"ss",
+				"reboot.target",
+				"replace-irreversibly");
+	if (rc < 0) {
+		warnx("Failed to start reboot.target: %s", strerror(-rc));
+	}
+}
+
+static int dbus_source_poll(void *ctx, char *op)
+{
+	struct debug_source_dbus *dbus = ctx;
+	int rc;
+
+	while (1) {
+		struct timespec tsto, *ptsto;
+		uint64_t dbusto;
+
+		/* See SD_BUS_GET_FD(3) */
+		dbus->pfds[DBUS_SOURCE_PFD_DBUS].fd = sd_bus_get_fd(dbus->bus);
+		dbus->pfds[DBUS_SOURCE_PFD_DBUS].events = sd_bus_get_events(dbus->bus);
+		rc = sd_bus_get_timeout(dbus->bus, &dbusto);
+		if (rc < 0)
+			return rc;
+
+		if (dbusto == UINT64_MAX) {
+			ptsto = NULL;
+		} else if (dbus->pfds[DBUS_SOURCE_PFD_DBUS].events == 0) {
+			ptsto = NULL;
+		} else {
+#define MSEC_PER_SEC 1000U
+#define USEC_PER_SEC (MSEC_PER_SEC * 1000U)
+#define NSEC_PER_SEC (USEC_PER_SEC * 1000U)
+#define NSEC_PER_USEC (NSEC_PER_SEC / USEC_PER_SEC)
+			tsto.tv_sec = dbusto / USEC_PER_SEC;
+			tsto.tv_nsec = (dbusto % USEC_PER_SEC) * NSEC_PER_USEC;
+			ptsto = &tsto;
+		}
+
+		if ((rc = ppoll(dbus->pfds, ARRAY_SIZE(dbus->pfds), ptsto, NULL)) < 0) {
+			warn("Failed polling source fds");
+			return -errno;
+		}
+
+		if (dbus->pfds[DBUS_SOURCE_PFD_SOURCE].revents) {
+			ssize_t ingress;
+
+			if ((ingress = read(dbus->pfds[DBUS_SOURCE_PFD_SOURCE].fd, op, 1)) != 1) {
+				if (ingress < 0) {
+					warn("Failed to read from basic source");
+					return -errno;
+				}
+
+				errx(EXIT_FAILURE, "Bad read, requested 1 got %zd", ingress);
+			}
+
+			return 0;
+		}
+
+		if (dbus->pfds[DBUS_SOURCE_PFD_DBUS].revents) {
+			if ((rc = sd_bus_process(dbus->bus, NULL)) < 0) {
+				warnx("Failed processing inbound D-Bus messages: %s",
+				      strerror(-rc));
+				return rc;
+			}
+		}
+	}
+}
+#else
+static void dbus_sink_debug(void *ctx)
+{
+	warnx("%s: Configured without systemd, dbus sinks disabled", __func__);
+}
+
+static void dbus_sink_reboot(void *ctx)
+{
+	warnx("%s: Configured without systemd, dbus sinks disabled", __func__);
+}
+
+static int dbus_source_poll(void *ctx, char *op)
+{
+	errx(EXIT_FAILURE, "Configured without systemd, dbus sources disabled", __func__);
+}
+#endif
+
+const struct debug_sink_ops dbus_sink_ops = {
+	.debug = dbus_sink_debug,
+	.reboot = dbus_sink_reboot,
+};
+
+const struct debug_source_ops dbus_source_ops = {
+	.poll = dbus_source_poll,
+};
+
 static int process(struct debug_source *source, struct debug_sink *sink)
 {
 	char command;
@@ -160,9 +489,11 @@
 
 int main(int argc, char * const argv[])
 {
+	struct debug_source_basic basic_source;
+	struct debug_source_dbus dbus_source;
+	struct debug_sink_sysrq sysrq_sink;
+	struct debug_sink_dbus dbus_sink;
 	const char *sink_actions = NULL;
-	struct debug_source_basic basic;
-	struct debug_sink_sysrq sysrq;
 	struct debug_source source;
 	struct debug_sink sink;
 	char devnode[PATH_MAX];
@@ -239,18 +570,46 @@
 			optind++;
 		}
 
-		sysrq.sink = sinkfd;
+		basic_source.source = sourcefd;
+		source.ops = &basic_source_ops;
+		source.ctx = &basic_source;
+
+		sysrq_sink.sink = sinkfd;
 		sink.ops = &sysrq_sink_ops;
-		sink.ctx = &sysrq;
+		sink.ctx = &sysrq_sink;
+	}
+
+	/* Set up the dbus sink actions if requested via --sink-actions=dbus */
+	if (sink_actions && !strcmp("dbus", sink_actions)) {
+		sd_bus *bus;
+		int rc;
+
+		rc = sd_bus_open_system(&bus);
+		if (rc < 0) {
+			errx(EXIT_FAILURE, "Failed to connect to the system bus: %s",
+			       strerror(-rc));
+		}
+
+		dbus_source.bus = bus;
+		dbus_source.pfds[DBUS_SOURCE_PFD_SOURCE].fd = sourcefd;
+		dbus_source.pfds[DBUS_SOURCE_PFD_SOURCE].events = POLLIN;
+		source.ops = &dbus_source_ops;
+		source.ctx = &dbus_source;
+
+		dbus_sink.bus = bus;
+		sink.ops = &dbus_sink_ops;
+		sink.ctx = &dbus_sink;
 	}
 
 	/* Check we're done with the command-line */
 	if (optind < argc)
-		err(EXIT_FAILURE, "Found %d unexpected arguments", argc - optind);
+		errx(EXIT_FAILURE, "Found %d unexpected arguments", argc - optind);
 
-	basic.source = sourcefd;
-	source.ops = &basic_source_ops;
-	source.ctx = &basic;
+	if (!(source.ops && source.ctx))
+		errx(EXIT_FAILURE, "Invalid source configuration");
+
+	if (!(sink.ops && sink.ctx))
+		errx(EXIT_FAILURE, "Unrecognised sink: %s", sink_actions);
 
 	/* Trigger the actions on the sink when we receive an event from the source */
 	if (process(&source, &sink) < 0)
diff --git a/meson.build b/meson.build
index 96f437e..a75e122 100644
--- a/meson.build
+++ b/meson.build
@@ -6,7 +6,9 @@
 		'werror=true',
 		'c_std=gnu18',
 	])
-executable('debug-trigger', 'main.c', install: true)
+
+config = configuration_data()
+debug_trigger_deps = []
 
 if get_option('systemd')
 	systemd = dependency('systemd')
@@ -15,8 +17,12 @@
 		       output: 'debug-trigger@.service',
 		       copy: true,
 		       install_dir: unitdir)
+	debug_trigger_deps += dependency('libsystemd')
 endif
 
+config.set10('HAVE_SYSTEMD', get_option('systemd'))
+config_h = configure_file(configuration: config, output: 'config.h')
+
 udev = dependency('udev')
 udevdir = udev.get_pkgconfig_variable('udevdir')
 
@@ -27,3 +33,7 @@
 	rulesdir = udevdir + '/rules.d'
 	configure_file(input: src, output: dst, copy: true, install_dir: rulesdir)
 endforeach
+
+executable('debug-trigger', 'main.c', config_h,
+	   dependencies: debug_trigger_deps,
+	   install: true)