diff mbox series

[libgpiod,v2,v3,4/5] tools: add gpiowatch

Message ID 20221011002909.26987-5-warthog618@gmail.com
State Superseded
Headers show
Series tools: improvements for v2 | expand

Commit Message

Kent Gibson Oct. 11, 2022, 12:29 a.m. UTC
Add a gpiowatch tool, based on gpiomon, to report line info change
events read from chip file descriptors.

Inspired by the gpio-watch tool in the linux kernel, but with gpiomon
features such as custom formatted output, filtering events of
interest and exiting after a number of events, so more useful for
scripting.

Default output is minimalist, so just time, event type and line id.
Full event details are available using the custom formatted output.

Signed-off-by: Kent Gibson <warthog618@gmail.com>
---

Changes v2 -> v3:
   - Minimise the default output to more closely match gpiomon.
   - Add --format option for when more detail is required.
   - Add --num-events option to exit after a number of events.
   - Add --event option to report only specific event types.
   - Add --quiet option to not print events.
   - fix monotonic to realtime conversion on 32 bit platforms.

 man/Makefile.am   |   2 +-
 tools/.gitignore  |   1 +
 tools/Makefile.am |   4 +-
 tools/gpiowatch.c | 433 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 438 insertions(+), 2 deletions(-)
 create mode 100644 tools/gpiowatch.c

Comments

Bartosz Golaszewski Nov. 8, 2022, 3 p.m. UTC | #1
On Tue, Oct 11, 2022 at 2:29 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> Add a gpiowatch tool, based on gpiomon, to report line info change
> events read from chip file descriptors.
>
> Inspired by the gpio-watch tool in the linux kernel, but with gpiomon
> features such as custom formatted output, filtering events of
> interest and exiting after a number of events, so more useful for
> scripting.
>
> Default output is minimalist, so just time, event type and line id.
> Full event details are available using the custom formatted output.
>
> Signed-off-by: Kent Gibson <warthog618@gmail.com>
> ---
>
> Changes v2 -> v3:
>    - Minimise the default output to more closely match gpiomon.
>    - Add --format option for when more detail is required.
>    - Add --num-events option to exit after a number of events.
>    - Add --event option to report only specific event types.
>    - Add --quiet option to not print events.
>    - fix monotonic to realtime conversion on 32 bit platforms.
>

Nice and clean, I don't have any issues other than the regular
coding-style bikeshedding.

What happened to the idea we've been floating about creating a single,
busyboxy executable with links rather than separate executables? Have
we ever agreed on it?

Bart
Kent Gibson Nov. 8, 2022, 3:38 p.m. UTC | #2
On Tue, Nov 08, 2022 at 04:00:08PM +0100, Bartosz Golaszewski wrote:
> On Tue, Oct 11, 2022 at 2:29 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > Add a gpiowatch tool, based on gpiomon, to report line info change
> > events read from chip file descriptors.
> >
> > Inspired by the gpio-watch tool in the linux kernel, but with gpiomon
> > features such as custom formatted output, filtering events of
> > interest and exiting after a number of events, so more useful for
> > scripting.
> >
> > Default output is minimalist, so just time, event type and line id.
> > Full event details are available using the custom formatted output.
> >
> > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > ---
> >
> > Changes v2 -> v3:
> >    - Minimise the default output to more closely match gpiomon.
> >    - Add --format option for when more detail is required.
> >    - Add --num-events option to exit after a number of events.
> >    - Add --event option to report only specific event types.
> >    - Add --quiet option to not print events.
> >    - fix monotonic to realtime conversion on 32 bit platforms.
> >
> 
> Nice and clean, I don't have any issues other than the regular
> coding-style bikeshedding.
> 

Will be renamed to gpionotify for v5, ok?

> What happened to the idea we've been floating about creating a single,
> busyboxy executable with links rather than separate executables? Have
> we ever agreed on it?
> 

Yeah, last we spoke on it we agreed it was of dubious value and a low
priority, so I didn't go anywhere with it.  You've reconsidered?

Cheers,
Kent.
Bartosz Golaszewski Nov. 8, 2022, 6:04 p.m. UTC | #3
On Tue, Nov 8, 2022 at 4:38 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Tue, Nov 08, 2022 at 04:00:08PM +0100, Bartosz Golaszewski wrote:
> > On Tue, Oct 11, 2022 at 2:29 AM Kent Gibson <warthog618@gmail.com> wrote:
> > >
> > > Add a gpiowatch tool, based on gpiomon, to report line info change
> > > events read from chip file descriptors.
> > >
> > > Inspired by the gpio-watch tool in the linux kernel, but with gpiomon
> > > features such as custom formatted output, filtering events of
> > > interest and exiting after a number of events, so more useful for
> > > scripting.
> > >
> > > Default output is minimalist, so just time, event type and line id.
> > > Full event details are available using the custom formatted output.
> > >
> > > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > > ---
> > >
> > > Changes v2 -> v3:
> > >    - Minimise the default output to more closely match gpiomon.
> > >    - Add --format option for when more detail is required.
> > >    - Add --num-events option to exit after a number of events.
> > >    - Add --event option to report only specific event types.
> > >    - Add --quiet option to not print events.
> > >    - fix monotonic to realtime conversion on 32 bit platforms.
> > >
> >
> > Nice and clean, I don't have any issues other than the regular
> > coding-style bikeshedding.
> >
>
> Will be renamed to gpionotify for v5, ok?

Yes, sure, just like discussed.

>
> > What happened to the idea we've been floating about creating a single,
> > busyboxy executable with links rather than separate executables? Have
> > we ever agreed on it?
> >
>
> Yeah, last we spoke on it we agreed it was of dubious value and a low
> priority, so I didn't go anywhere with it.  You've reconsidered?
>

I'm seeing that the tools-common.c file grew quite a bit after your
rework (which is good, a lot of stuff has been generalized) which
makes me think it wouldn't be a bad idea to not include it 6 times
separately. It's either a libgpio-tools or putting this stuff into the
same executable?

Anyway, we can do it later.

Bart
Kent Gibson Nov. 9, 2022, 1:57 a.m. UTC | #4
On Tue, Nov 08, 2022 at 07:04:36PM +0100, Bartosz Golaszewski wrote:
> On Tue, Nov 8, 2022 at 4:38 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Tue, Nov 08, 2022 at 04:00:08PM +0100, Bartosz Golaszewski wrote:
> > > On Tue, Oct 11, 2022 at 2:29 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > >
> > > > Add a gpiowatch tool, based on gpiomon, to report line info change
> > > > events read from chip file descriptors.
> > > >
> > > > Inspired by the gpio-watch tool in the linux kernel, but with gpiomon
> > > > features such as custom formatted output, filtering events of
> > > > interest and exiting after a number of events, so more useful for
> > > > scripting.
> > > >
> > > > Default output is minimalist, so just time, event type and line id.
> > > > Full event details are available using the custom formatted output.
> > > >
> > > > Signed-off-by: Kent Gibson <warthog618@gmail.com>
> > > > ---
> > > >
> > > > Changes v2 -> v3:
> > > >    - Minimise the default output to more closely match gpiomon.
> > > >    - Add --format option for when more detail is required.
> > > >    - Add --num-events option to exit after a number of events.
> > > >    - Add --event option to report only specific event types.
> > > >    - Add --quiet option to not print events.
> > > >    - fix monotonic to realtime conversion on 32 bit platforms.
> > > >
> > >
> > > Nice and clean, I don't have any issues other than the regular
> > > coding-style bikeshedding.
> > >
> >
> > Will be renamed to gpionotify for v5, ok?
> 
> Yes, sure, just like discussed.
> 
> >
> > > What happened to the idea we've been floating about creating a single,
> > > busyboxy executable with links rather than separate executables? Have
> > > we ever agreed on it?
> > >
> >
> > Yeah, last we spoke on it we agreed it was of dubious value and a low
> > priority, so I didn't go anywhere with it.  You've reconsidered?
> >
> 
> I'm seeing that the tools-common.c file grew quite a bit after your
> rework (which is good, a lot of stuff has been generalized) which
> makes me think it wouldn't be a bad idea to not include it 6 times
> separately. It's either a libgpio-tools or putting this stuff into the
> same executable?
> 

Fair enough - for some reason I was thinking tools-common did get linked
into a libgpiod-tools lib, but I guess not.

The bulk of the rework is the resolver, and I was thinking that some
form of that should go in core libgpiod - that would also reduce the
duplication problem.

> Anyway, we can do it later.
> 

Agreed - not for this series.

Cheers,
Kent.
diff mbox series

Patch

diff --git a/man/Makefile.am b/man/Makefile.am
index 8d1d9b3..032ed4e 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -3,7 +3,7 @@ 
 
 if WITH_MANPAGES
 
-dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiomon.man
+dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiomon.man gpiowatch.man
 
 %.man: $(top_builddir)/tools/$(*F)
 	help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
diff --git a/tools/.gitignore b/tools/.gitignore
index d6b2f44..ad0800b 100644
--- a/tools/.gitignore
+++ b/tools/.gitignore
@@ -6,3 +6,4 @@  gpioinfo
 gpioget
 gpioset
 gpiomon
+gpiowatch
diff --git a/tools/Makefile.am b/tools/Makefile.am
index c956314..1097fea 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -9,7 +9,7 @@  libtools_common_la_SOURCES = tools-common.c tools-common.h
 
 LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la $(LIBEDIT_LIBS)
 
-bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon
+bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiowatch
 
 gpiodetect_SOURCES = gpiodetect.c
 
@@ -21,6 +21,8 @@  gpioset_SOURCES = gpioset.c
 
 gpiomon_SOURCES = gpiomon.c
 
+gpiowatch_SOURCES = gpiowatch.c
+
 EXTRA_DIST = gpio-tools-test gpio-tools-test.bats
 
 if WITH_TESTS
diff --git a/tools/gpiowatch.c b/tools/gpiowatch.c
new file mode 100644
index 0000000..dfcf14a
--- /dev/null
+++ b/tools/gpiowatch.c
@@ -0,0 +1,433 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "tools-common.h"
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <line>...\n", get_progname());
+	printf("\n");
+	printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
+	printf("\n");
+	printf("Lines are specified by name, or optionally by offset if the chip option\n");
+	printf("is provided.\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("      --banner\t\tdisplay a banner on successful startup\n");
+	printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+	printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+	printf("  -e, --event <event>\tspecify the events to monitor.\n");
+	printf("\t\t\tPossible values: 'requested', 'released', 'reconfigured'.\n");
+	printf("\t\t\t(default is all events)\n");
+	printf("  -h, --help\t\tdisplay this help and exit\n");
+	printf("  -F, --format <fmt>\tspecify a custom output format\n");
+	printf("      --localtime\tconvert event timestamps to local time\n");
+	printf("  -n, --num-events <num>\n");
+	printf("\t\t\texit after processing num events\n");
+	printf("  -q, --quiet\t\tdon't generate any output\n");
+	printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+	printf("      --utc\t\tconvert event timestamps to UTC\n");
+	printf("  -v, --version\t\toutput version information and exit\n");
+	print_chip_help();
+	printf("\n");
+	printf("Format specifiers:\n");
+	printf("  %%o   GPIO line offset\n");
+	printf("  %%l   GPIO line name\n");
+	printf("  %%c   GPIO chip name\n");
+	printf("  %%e   numeric info event type ('1' - requested, '2' - released or '3' - reconfigured)\n");
+	printf("  %%E   info event type ('requested', 'released' or 'reconfigured')\n");
+	printf("  %%a   line attributes\n");
+	printf("  %%C   consumer\n");
+	printf("  %%S   event timestamp as seconds\n");
+	printf("  %%U   event timestamp as UTC\n");
+	printf("  %%L   event timestamp as local time\n");
+}
+
+static int parse_event_type_or_die(const char *option)
+{
+	if (strcmp(option, "requested") == 0)
+		return GPIOD_INFO_EVENT_LINE_REQUESTED;
+	if (strcmp(option, "released") == 0)
+		return GPIOD_INFO_EVENT_LINE_RELEASED;
+	if (strcmp(option, "reconfigured") != 0)
+		die("invalid edge: %s", option);
+	return GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED;
+}
+
+struct config {
+	bool quiet;
+	bool strict;
+	int event_type;
+	int events_wanted;
+	const char *chip_id;
+	const char *fmt;
+	int by_name;
+	int timestamp_fmt;
+	int banner;
+};
+
+int parse_config(int argc, char **argv, struct config *cfg)
+{
+	int opti, optc;
+	const char *const shortopts = "+b:c:C:e:hF:ln:p:qshv";
+	const struct option longopts[] = {
+		{ "banner",	no_argument,	&cfg->banner,	1 },
+		{ "by-name",	no_argument,	&cfg->by_name,	1 },
+		{ "chip",	required_argument, NULL,	'c' },
+		{ "event",	required_argument, NULL,	'e' },
+		{ "format",	required_argument, NULL,	'F' },
+		{ "help",	no_argument,	NULL,		'h' },
+		{ "localtime",	no_argument,	&cfg->timestamp_fmt,	2 },
+		{ "num-events",	required_argument, NULL,	'n' },
+		{ "quiet",	no_argument,	NULL,		'q' },
+		{ "silent",	no_argument,	NULL,		'q' },
+		{ "strict",	no_argument,	NULL,		's' },
+		{ "utc",	no_argument,	&cfg->timestamp_fmt,	1 },
+		{ "version",	no_argument,	NULL,		'v' },
+		{ GETOPT_NULL_LONGOPT },
+	};
+
+	memset(cfg, 0, sizeof(*cfg));
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'c':
+			cfg->chip_id = optarg;
+			break;
+		case 'e':
+			cfg->event_type = parse_event_type_or_die(optarg);
+			break;
+		case 'F':
+			cfg->fmt = optarg;
+			break;
+		case 'n':
+			cfg->events_wanted = parse_uint_or_die(optarg);
+			break;
+		case 'q':
+			cfg->quiet = true;
+			break;
+		case 's':
+			cfg->strict = true;
+			break;
+		case 'h':
+			print_help();
+			exit(EXIT_SUCCESS);
+		case 'v':
+			print_version();
+			exit(EXIT_SUCCESS);
+		case '?':
+			die("try %s --help", get_progname());
+		case 0:
+			break;
+		default:
+			abort();
+		}
+	}
+
+	return optind;
+}
+
+static void print_banner(int num_lines, char **lines)
+{
+	int i;
+
+	if (num_lines > 1) {
+		printf("Watching lines ");
+
+		for (i = 0; i < num_lines - 1; i++)
+			printf("%s, ", lines[i]);
+
+		printf("and %s...\n", lines[i]);
+	} else {
+		printf("Watching line %s...\n", lines[0]);
+	}
+}
+
+static void print_event_type(int evtype)
+{
+	switch (evtype) {
+	case GPIOD_INFO_EVENT_LINE_REQUESTED:
+		fputs("requested", stdout);
+		break;
+	case GPIOD_INFO_EVENT_LINE_RELEASED:
+		fputs("released", stdout);
+		break;
+	case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+		fputs("reconfigured", stdout);
+		break;
+	default:
+		fputs("unknown", stdout);
+		break;
+	}
+}
+
+/*
+ * A convenience function to map clock monotonic to realtime, as uAPI only
+ * supports CLOCK_MONOTONIC.
+ *
+ * Samples the realtime clock on either side of a monotonic sample and averages
+ * the realtime samples to estimate the offset between the two clocks.
+ * Any time shifts between the two realtime samples will result in the
+ * monotonic time being mapped to the average of the before and after, so
+ * half way between the old and new times.
+ *
+ * Any CPU suspension between the event being generated and converted will
+ * result in the returned time being shifted by the period of suspension.
+ */
+static uint64_t monotonic_to_realtime(uint64_t evtime)
+{
+	struct timespec ts;
+	uint64_t before, after, mono;
+
+	clock_gettime(CLOCK_REALTIME, &ts);
+	before = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+	mono = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+	clock_gettime(CLOCK_REALTIME, &ts);
+	after = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+	evtime += (after/2 - mono + before/2);
+	return evtime;
+}
+
+static void event_print_formatted(struct gpiod_info_event *event,
+			struct line_resolver *resolver, int chip_num,
+			struct config *cfg)
+{
+	struct gpiod_line_info *info;
+	const char *lname, *prev, *curr, *consumer;
+	char  fmt;
+	uint64_t evtime;
+	int evtype;
+	unsigned int offset;
+
+	info = gpiod_info_event_get_line_info(event);
+	evtime = gpiod_info_event_get_timestamp_ns(event);
+	evtype = gpiod_info_event_get_event_type(event);
+	offset = gpiod_line_info_get_offset(info);
+
+	for (prev = curr = cfg->fmt;;) {
+		curr = strchr(curr, '%');
+		if (!curr) {
+			fputs(prev, stdout);
+			break;
+		}
+
+		if (prev != curr)
+			fwrite(prev, curr - prev, 1, stdout);
+
+		fmt = *(curr + 1);
+
+		switch (fmt) {
+		case 'a':
+			print_line_attributes(info);
+			break;
+		case 'c':
+			fputs(get_chip_name(resolver, chip_num), stdout);
+			break;
+		case 'C':
+			if (!gpiod_line_info_is_used(info)) {
+				consumer = "unused";
+			} else {
+				consumer = gpiod_line_info_get_consumer(info);
+				if (!consumer)
+					consumer = "kernel";
+			}
+			fputs(consumer, stdout);
+			break;
+		case 'e':
+			printf("%d", evtype);
+			break;
+		case 'E':
+			print_event_type(evtype);
+			break;
+		case 'l':
+			lname = gpiod_line_info_get_name(info);
+			if (!lname)
+				lname = "unnamed";
+			fputs(lname, stdout);
+			break;
+		case 'L':
+			print_event_time(monotonic_to_realtime(evtime), 2);
+			break;
+		case 'o':
+			printf("%u", offset);
+			break;
+		case 'S':
+			print_event_time(evtime, 0);
+			break;
+		case 'U':
+			print_event_time(monotonic_to_realtime(evtime), 1);
+			break;
+		case '%':
+			fputc('%', stdout);
+			break;
+		case '\0':
+			fputc('%', stdout);
+			goto end;
+		default:
+			fwrite(curr, 2, 1, stdout);
+			break;
+		}
+
+		curr += 2;
+		prev = curr;
+	}
+
+end:
+	fputc('\n', stdout);
+}
+
+static void event_print_human_readable(struct gpiod_info_event *event,
+			struct line_resolver *resolver, int chip_num,
+			struct config *cfg)
+{
+	struct gpiod_line_info *info;
+	uint64_t evtime;
+	int evtype;
+	unsigned int offset;
+	char *evname;
+
+	info = gpiod_info_event_get_line_info(event);
+	evtime = gpiod_info_event_get_timestamp_ns(event);
+	evtype = gpiod_info_event_get_event_type(event);
+	offset = gpiod_line_info_get_offset(info);
+
+	switch (evtype) {
+	case GPIOD_INFO_EVENT_LINE_REQUESTED:
+		evname = "requested";
+		break;
+	case GPIOD_INFO_EVENT_LINE_RELEASED:
+		evname = "released";
+		break;
+	case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+		evname = "reconfigured";
+		break;
+	default:
+		evname = "unknown";
+	}
+
+	if (cfg->timestamp_fmt)
+		evtime = monotonic_to_realtime(evtime);
+
+	print_event_time(evtime, cfg->timestamp_fmt);
+	printf("\t%s\t", evname);
+	print_line_id(resolver, chip_num, offset, cfg->chip_id);
+	fputc('\n', stdout);
+}
+
+static void event_print(struct gpiod_info_event *event,
+			struct line_resolver *resolver, int chip_num,
+			struct config *cfg)
+{
+	if (cfg->quiet)
+		return;
+
+	if (cfg->fmt)
+		event_print_formatted(event, resolver, chip_num, cfg);
+	else
+		event_print_human_readable(event, resolver, chip_num, cfg);
+}
+
+int main(int argc, char **argv)
+{
+	int i, j, events_done = 0, evtype;
+	struct gpiod_chip **chips;
+	struct pollfd *pollfds;
+	struct gpiod_chip *chip;
+	struct line_resolver *resolver;
+	struct gpiod_info_event *event;
+	struct config cfg;
+
+	i = parse_config(argc, argv, &cfg);
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 1)
+		die("at least one GPIO line must be specified");
+
+	if (argc > 64)
+		die("too many lines given");
+
+	resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict,
+				 cfg.by_name);
+	validate_resolution(resolver, cfg.chip_id);
+	chips = calloc(resolver->num_chips, sizeof(*chips));
+	pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
+	if (!pollfds)
+		die("out of memory");
+
+	for (i = 0; i < resolver->num_chips; i++) {
+		chip = gpiod_chip_open(resolver->chips[i].path);
+		if (!chip)
+			die_perror("unable to open chip '%s'",
+				   resolver->chips[i].path);
+
+		for (j = 0; j < resolver->num_lines; j++)
+			if ((resolver->lines[j].chip_num == i) &&
+			    !gpiod_chip_watch_line_info(
+					chip, resolver->lines[j].offset))
+				die_perror("unable to watch line on chip '%s'",
+					   resolver->chips[i].path);
+
+		chips[i] = chip;
+		pollfds[i].fd = gpiod_chip_get_fd(chip);
+		pollfds[i].events = POLLIN;
+	}
+
+	if (cfg.banner)
+		print_banner(argc, argv);
+
+	for (;;) {
+		fflush(stdout);
+
+		if (poll(pollfds, resolver->num_chips, -1) < 0)
+			die_perror("error polling for events");
+
+		for (i = 0; i < resolver->num_chips; i++) {
+			if (pollfds[i].revents == 0)
+				continue;
+
+			event = gpiod_chip_read_info_event(chips[i]);
+			if (!event)
+				die_perror("unable to retrieve chip event");
+
+			if (cfg.event_type) {
+				evtype = gpiod_info_event_get_event_type(event);
+				if (evtype != cfg.event_type)
+					continue;
+			}
+
+			event_print(event, resolver, i, &cfg);
+
+			events_done++;
+
+			if (cfg.events_wanted &&
+			    events_done >= cfg.events_wanted)
+				goto done;
+		}
+	}
+done:
+	for (i = 0; i < resolver->num_chips; i++)
+		gpiod_chip_close(chips[i]);
+
+	free(chips);
+	free_line_resolver(resolver);
+
+	return EXIT_SUCCESS;
+}