| /* SPDX-License-Identifier: GPL-2.0+ */ |
| /* |
| * Collect variables across events. |
| * |
| * usage: collect [--add|--remove] <checkpoint> <id> <idlist> |
| * |
| * Adds ID <id> to the list governed by <checkpoint>. |
| * <id> must be part of the ID list <idlist>. |
| * If all IDs given by <idlist> are listed (ie collect has been |
| * invoked for each ID in <idlist>) collect returns 0, the |
| * number of missing IDs otherwise. |
| * A negative number is returned on error. |
| * |
| * Copyright © 2007, Hannes Reinecke <hare@suse.de> |
| * |
| * This program is free software: you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation, either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| */ |
| |
| #include <errno.h> |
| #include <getopt.h> |
| #include <stddef.h> |
| #include <stdio.h> |
| |
| #include "alloc-util.h" |
| #include "libudev-private.h" |
| #include "macro.h" |
| #include "stdio-util.h" |
| #include "string-util.h" |
| #include "udev-util.h" |
| |
| #define BUFSIZE 16 |
| #define UDEV_ALARM_TIMEOUT 180 |
| |
| enum collect_state { |
| STATE_NONE, |
| STATE_OLD, |
| STATE_CONFIRMED, |
| }; |
| |
| struct _mate { |
| struct udev_list_node node; |
| char *name; |
| enum collect_state state; |
| }; |
| |
| static struct udev_list_node bunch; |
| static int debug; |
| |
| /* This can increase dynamically */ |
| static size_t bufsize = BUFSIZE; |
| |
| static inline struct _mate *node_to_mate(struct udev_list_node *node) |
| { |
| return container_of(node, struct _mate, node); |
| } |
| |
| _noreturn_ static void sig_alrm(int signo) |
| { |
| exit(4); |
| } |
| |
| static void usage(void) |
| { |
| printf("%s [options] <checkpoint> <id> <idlist>\n\n" |
| "Collect variables across events.\n\n" |
| " -h --help Print this message\n" |
| " -a --add Add ID <id> to the list <idlist>\n" |
| " -r --remove Remove ID <id> from the list <idlist>\n" |
| " -d --debug Debug to stderr\n\n" |
| " Adds ID <id> to the list governed by <checkpoint>.\n" |
| " <id> must be part of the list <idlist>.\n" |
| " If all IDs given by <idlist> are listed (ie collect has been\n" |
| " invoked for each ID in <idlist>) collect returns 0, the\n" |
| " number of missing IDs otherwise.\n" |
| " On error a negative number is returned.\n\n" |
| , program_invocation_short_name); |
| } |
| |
| /* |
| * prepare |
| * |
| * Prepares the database file |
| */ |
| static int prepare(char *dir, char *filename) |
| { |
| char buf[PATH_MAX]; |
| int r, fd; |
| |
| r = mkdir(dir, 0700); |
| if (r < 0 && errno != EEXIST) |
| return -errno; |
| |
| snprintf(buf, sizeof buf, "%s/%s", dir, filename); |
| |
| fd = open(buf, O_RDWR|O_CREAT|O_CLOEXEC, S_IRUSR|S_IWUSR); |
| if (fd < 0) |
| fprintf(stderr, "Cannot open %s: %m\n", buf); |
| |
| if (lockf(fd,F_TLOCK,0) < 0) { |
| if (debug) |
| fprintf(stderr, "Lock taken, wait for %d seconds\n", UDEV_ALARM_TIMEOUT); |
| if (IN_SET(errno, EAGAIN, EACCES)) { |
| alarm(UDEV_ALARM_TIMEOUT); |
| lockf(fd, F_LOCK, 0); |
| if (debug) |
| fprintf(stderr, "Acquired lock on %s\n", buf); |
| } else { |
| if (debug) |
| fprintf(stderr, "Could not get lock on %s: %m\n", buf); |
| } |
| } |
| |
| return fd; |
| } |
| |
| /* |
| * Read checkpoint file |
| * |
| * Tricky reading this. We allocate a buffer twice as large |
| * as we're going to read. Then we read into the upper half |
| * of that buffer and start parsing. |
| * Once we do _not_ find end-of-work terminator (whitespace |
| * character) we move the upper half to the lower half, |
| * adjust the read pointer and read the next bit. |
| * Quite clever methinks :-) |
| * I should become a programmer ... |
| * |
| * Yes, one could have used fgets() for this. But then we'd |
| * have to use freopen etc which I found quite tedious. |
| */ |
| static int checkout(int fd) |
| { |
| int len; |
| _cleanup_free_ char *buf = NULL; |
| char *ptr, *word = NULL; |
| struct _mate *him; |
| |
| restart: |
| len = bufsize >> 1; |
| buf = malloc(bufsize + 1); |
| if (!buf) |
| return log_oom(); |
| memset(buf, ' ', bufsize); |
| buf[bufsize] = '\0'; |
| |
| ptr = buf + len; |
| while ((read(fd, buf + len, len)) > 0) { |
| while (ptr && *ptr) { |
| word = ptr; |
| ptr = strpbrk(word," \n\t\r"); |
| if (!ptr && word < (buf + len)) { |
| bufsize = bufsize << 1; |
| if (debug) |
| fprintf(stderr, "ID overflow, restarting with size %zu\n", bufsize); |
| lseek(fd, 0, SEEK_SET); |
| goto restart; |
| } |
| if (ptr) { |
| *ptr = '\0'; |
| ptr++; |
| if (isempty(word)) |
| continue; |
| |
| if (debug) |
| fprintf(stderr, "Found word %s\n", word); |
| him = malloc(sizeof (struct _mate)); |
| if (!him) |
| return log_oom(); |
| him->name = strdup(word); |
| if (!him->name) { |
| free(him); |
| return log_oom(); |
| } |
| him->state = STATE_OLD; |
| udev_list_node_append(&him->node, &bunch); |
| word = NULL; |
| } |
| } |
| memcpy(buf, buf + len, len); |
| memset(buf + len, ' ', len); |
| |
| if (!ptr) |
| ptr = word; |
| ptr -= len; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * invite |
| * |
| * Adds a new ID 'us' to the internal list, |
| * marks it as confirmed. |
| */ |
| static void invite(char *us) |
| { |
| struct udev_list_node *him_node; |
| struct _mate *who = NULL; |
| |
| if (debug) |
| fprintf(stderr, "Adding ID '%s'\n", us); |
| |
| udev_list_node_foreach(him_node, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| if (streq(him->name, us)) { |
| him->state = STATE_CONFIRMED; |
| who = him; |
| } |
| } |
| if (debug && !who) |
| fprintf(stderr, "ID '%s' not in database\n", us); |
| |
| } |
| |
| /* |
| * reject |
| * |
| * Marks the ID 'us' as invalid, |
| * causing it to be removed when the |
| * list is written out. |
| */ |
| static void reject(char *us) |
| { |
| struct udev_list_node *him_node; |
| struct _mate *who = NULL; |
| |
| if (debug) |
| fprintf(stderr, "Removing ID '%s'\n", us); |
| |
| udev_list_node_foreach(him_node, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| if (streq(him->name, us)) { |
| him->state = STATE_NONE; |
| who = him; |
| } |
| } |
| if (debug && !who) |
| fprintf(stderr, "ID '%s' not in database\n", us); |
| } |
| |
| /* |
| * kickout |
| * |
| * Remove all IDs in the internal list which are not part |
| * of the list passed via the command line. |
| */ |
| static void kickout(void) |
| { |
| struct udev_list_node *him_node; |
| struct udev_list_node *tmp; |
| |
| udev_list_node_foreach_safe(him_node, tmp, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| if (him->state == STATE_OLD) { |
| udev_list_node_remove(&him->node); |
| free(him->name); |
| free(him); |
| } |
| } |
| } |
| |
| /* |
| * missing |
| * |
| * Counts all missing IDs in the internal list. |
| */ |
| static int missing(int fd) |
| { |
| char *buf; |
| int ret = 0; |
| struct udev_list_node *him_node; |
| |
| buf = malloc(bufsize); |
| if (!buf) |
| return log_oom(); |
| |
| udev_list_node_foreach(him_node, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| if (him->state == STATE_NONE) { |
| ret++; |
| } else { |
| while (strlen(him->name)+1 >= bufsize) { |
| char *tmpbuf; |
| |
| bufsize = bufsize << 1; |
| tmpbuf = realloc(buf, bufsize); |
| if (!tmpbuf) { |
| free(buf); |
| return log_oom(); |
| } |
| buf = tmpbuf; |
| } |
| snprintf(buf, strlen(him->name)+2, "%s ", him->name); |
| if (write(fd, buf, strlen(buf)) < 0) { |
| free(buf); |
| return -1; |
| } |
| } |
| } |
| |
| free(buf); |
| return ret; |
| } |
| |
| /* |
| * everybody |
| * |
| * Prints out the status of the internal list. |
| */ |
| static void everybody(void) |
| { |
| struct udev_list_node *him_node; |
| const char *state = ""; |
| |
| udev_list_node_foreach(him_node, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| switch (him->state) { |
| case STATE_NONE: |
| state = "none"; |
| break; |
| case STATE_OLD: |
| state = "old"; |
| break; |
| case STATE_CONFIRMED: |
| state = "confirmed"; |
| break; |
| } |
| fprintf(stderr, "ID: %s=%s\n", him->name, state); |
| } |
| } |
| |
| int main(int argc, char **argv) { |
| static const struct option options[] = { |
| { "add", no_argument, NULL, 'a' }, |
| { "remove", no_argument, NULL, 'r' }, |
| { "debug", no_argument, NULL, 'd' }, |
| { "help", no_argument, NULL, 'h' }, |
| {} |
| }; |
| int argi; |
| char *checkpoint, *us; |
| int fd; |
| int i; |
| int ret = EXIT_SUCCESS; |
| int prune = 0; |
| char tmpdir[UTIL_PATH_SIZE]; |
| |
| log_set_target(LOG_TARGET_AUTO); |
| udev_parse_config(); |
| log_parse_environment(); |
| log_open(); |
| |
| for (;;) { |
| int option; |
| |
| option = getopt_long(argc, argv, "ardh", options, NULL); |
| if (option == -1) |
| break; |
| |
| switch (option) { |
| case 'a': |
| prune = 0; |
| break; |
| case 'r': |
| prune = 1; |
| break; |
| case 'd': |
| debug = 1; |
| break; |
| case 'h': |
| usage(); |
| return 0; |
| default: |
| return 1; |
| } |
| } |
| |
| argi = optind; |
| if (argi + 2 > argc) { |
| printf("Missing parameter(s)\n"); |
| return 1; |
| } |
| checkpoint = argv[argi++]; |
| us = argv[argi++]; |
| |
| if (signal(SIGALRM, sig_alrm) == SIG_ERR) { |
| fprintf(stderr, "Cannot set SIGALRM: %m\n"); |
| return 2; |
| } |
| |
| udev_list_node_init(&bunch); |
| |
| if (debug) |
| fprintf(stderr, "Using checkpoint '%s'\n", checkpoint); |
| |
| strscpyl(tmpdir, sizeof(tmpdir), "/run/udev/collect", NULL); |
| fd = prepare(tmpdir, checkpoint); |
| if (fd < 0) { |
| ret = 3; |
| goto out; |
| } |
| |
| if (checkout(fd) < 0) { |
| ret = 2; |
| goto out; |
| } |
| |
| for (i = argi; i < argc; i++) { |
| struct udev_list_node *him_node; |
| struct _mate *who; |
| |
| who = NULL; |
| udev_list_node_foreach(him_node, &bunch) { |
| struct _mate *him = node_to_mate(him_node); |
| |
| if (streq(him->name, argv[i])) |
| who = him; |
| } |
| if (!who) { |
| struct _mate *him; |
| |
| if (debug) |
| fprintf(stderr, "ID %s: not in database\n", argv[i]); |
| him = new(struct _mate, 1); |
| if (!him) { |
| ret = ENOMEM; |
| goto out; |
| } |
| |
| him->name = strdup(argv[i]); |
| if (!him->name) { |
| free(him); |
| ret = ENOMEM; |
| goto out; |
| } |
| |
| him->state = STATE_NONE; |
| udev_list_node_append(&him->node, &bunch); |
| } else { |
| if (debug) |
| fprintf(stderr, "ID %s: found in database\n", argv[i]); |
| who->state = STATE_CONFIRMED; |
| } |
| } |
| |
| if (prune) |
| reject(us); |
| else |
| invite(us); |
| |
| if (debug) { |
| everybody(); |
| fprintf(stderr, "Prune lists\n"); |
| } |
| kickout(); |
| |
| lseek(fd, 0, SEEK_SET); |
| ftruncate(fd, 0); |
| ret = missing(fd); |
| |
| lockf(fd, F_ULOCK, 0); |
| close(fd); |
| out: |
| if (debug) |
| everybody(); |
| if (ret >= 0) |
| printf("COLLECT_%s=%d\n", checkpoint, ret); |
| return ret; |
| } |