summaryrefslogtreecommitdiff
path: root/libudev/libudev-queue-export.c
diff options
context:
space:
mode:
Diffstat (limited to 'libudev/libudev-queue-export.c')
-rw-r--r--libudev/libudev-queue-export.c473
1 files changed, 473 insertions, 0 deletions
diff --git a/libudev/libudev-queue-export.c b/libudev/libudev-queue-export.c
new file mode 100644
index 0000000000..a36ff5150a
--- /dev/null
+++ b/libudev/libudev-queue-export.c
@@ -0,0 +1,473 @@
+/*
+ * libudev - interface to udev device information
+ *
+ * Copyright (C) 2008 Kay Sievers <kay.sievers@vrfy.org>
+ * Copyright (C) 2009 Alan Jenkins <alan-jenkins@tuffmail.co.uk>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ */
+
+/*
+ * DISCLAIMER - The file format mentioned here is private to udev/libudev,
+ * and may be changed without notice.
+ *
+ *
+ * The udev event queue is exported as a binary log file.
+ * Each log record consists of a sequence number followed by the device path.
+ *
+ * When a new event is queued, its details are appended to the log.
+ * When the event finishes, a second record is appended to the log
+ * with the same sequence number but a null devpath.
+ *
+ * Example:
+ * {1, "/devices/virtual/mem/null" },
+ * {2, "/devices/virtual/mem/zero" },
+ * {1, "" },
+ * Event 2 is still queued, but event 1 has been finished
+ *
+ * The queue does not grow indefinitely. It is periodically re-created
+ * to remove finished events. Atomic rename() makes this transparent to readers.
+ *
+ *
+ * The queue file starts with a single sequence number which specifies the
+ * minimum sequence number in the log that follows. Any events prior to this
+ * sequence number have already finished.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <assert.h>
+
+#include "udev.h"
+
+static int rebuild_queue_file(struct udev_queue_export *udev_queue_export);
+
+struct udev_queue_export {
+ struct udev *udev;
+ int failed_count; /* number of failed events exported */
+ int queued_count; /* number of unfinished events exported in queue file */
+ FILE *queue_file;
+ unsigned long long int seqnum_max; /* earliest sequence number in queue file */
+ unsigned long long int seqnum_min; /* latest sequence number in queue file */
+ int waste_bytes; /* queue file bytes wasted on finished events */
+};
+
+struct udev_queue_export *udev_queue_export_new(struct udev *udev)
+{
+ struct udev_queue_export *udev_queue_export;
+ unsigned long long int initial_seqnum;
+
+ if (udev == NULL)
+ return NULL;
+
+ udev_queue_export = calloc(1, sizeof(struct udev_queue_export));
+ if (udev_queue_export == NULL)
+ return NULL;
+ udev_queue_export->udev = udev;
+
+ initial_seqnum = udev_get_kernel_seqnum(udev);
+ udev_queue_export->seqnum_min = initial_seqnum;
+ udev_queue_export->seqnum_max = initial_seqnum;
+
+ udev_queue_export_cleanup(udev_queue_export);
+ if (rebuild_queue_file(udev_queue_export) != 0) {
+ free(udev_queue_export);
+ return NULL;
+ }
+
+ return udev_queue_export;
+}
+
+void udev_queue_export_unref(struct udev_queue_export *udev_queue_export)
+{
+ if (udev_queue_export == NULL)
+ return;
+ if (udev_queue_export->queue_file != NULL)
+ fclose(udev_queue_export->queue_file);
+ free(udev_queue_export);
+}
+
+void udev_queue_export_cleanup(struct udev_queue_export *udev_queue_export)
+{
+ char filename[UTIL_PATH_SIZE];
+
+ util_strscpyl(filename, sizeof(filename), udev_get_dev_path(udev_queue_export->udev), "/.udev/queue.tmp", NULL);
+ unlink(filename);
+
+ util_strscpyl(filename, sizeof(filename), udev_get_dev_path(udev_queue_export->udev), "/.udev/queue.bin", NULL);
+ unlink(filename);
+}
+
+
+static int skip_to(FILE *file, long offset)
+{
+ long old_offset;
+
+ /* fseek may drop buffered data, avoid it for small seeks */
+ old_offset = ftell(file);
+ if (offset > old_offset && offset - old_offset <= BUFSIZ) {
+ size_t skip_bytes = offset - old_offset;
+ char buf[skip_bytes];
+
+ if (fread(buf, skip_bytes, 1, file) != skip_bytes)
+ return -1;
+ }
+
+ return fseek(file, offset, SEEK_SET);
+}
+
+struct queue_devpaths {
+ unsigned int devpaths_first; /* index of first queued event */
+ unsigned int devpaths_size;
+ long devpaths[]; /* seqnum -> offset of devpath in queue file (or 0) */
+};
+
+/*
+ * Returns a table mapping seqnum to devpath file offset for currently queued events.
+ * devpaths[i] represents the event with seqnum = i + udev_queue_export->seqnum_min.
+ */
+static struct queue_devpaths *build_index(struct udev_queue_export *udev_queue_export)
+{
+ struct queue_devpaths *devpaths;
+ unsigned long long int range;
+ long devpath_offset;
+ ssize_t devpath_len;
+ unsigned long long int seqnum;
+ unsigned long long int n;
+ unsigned int i;
+
+ /* seek to the first event in the file */
+ rewind(udev_queue_export->queue_file);
+ udev_queue_read_seqnum(udev_queue_export->queue_file, &seqnum);
+
+ /* allocate the table */
+ range = udev_queue_export->seqnum_min - udev_queue_export->seqnum_max;
+ if (range - 1 > INT_MAX) {
+ err(udev_queue_export->udev, "queue file overflow\n");
+ return NULL;
+ }
+ devpaths = calloc(1, sizeof(struct queue_devpaths) + (range + 1) * sizeof(long));
+ if (index == NULL)
+ return NULL;
+ devpaths->devpaths_size = range + 1;
+
+ /* read all records and populate the table */
+ while(1) {
+ if (udev_queue_read_seqnum(udev_queue_export->queue_file, &seqnum) < 0)
+ break;
+ n = seqnum - udev_queue_export->seqnum_max;
+ if (n >= devpaths->devpaths_size)
+ goto read_error;
+
+ devpath_offset = ftell(udev_queue_export->queue_file);
+ devpath_len = udev_queue_skip_devpath(udev_queue_export->queue_file);
+ if (devpath_len < 0)
+ goto read_error;
+
+ if (devpath_len > 0)
+ devpaths->devpaths[n] = devpath_offset;
+ else
+ devpaths->devpaths[n] = 0;
+ }
+
+ /* find first queued event */
+ for (i = 0; i < devpaths->devpaths_size; i++) {
+ if (devpaths->devpaths[i] != 0)
+ break;
+ }
+ devpaths->devpaths_first = i;
+
+ return devpaths;
+
+read_error:
+ err(udev_queue_export->udev, "queue file corrupted\n");
+ free(devpaths);
+ return NULL;
+}
+
+static int rebuild_queue_file(struct udev_queue_export *udev_queue_export)
+{
+ unsigned long long int seqnum;
+ struct queue_devpaths *devpaths = NULL;
+ char filename[UTIL_PATH_SIZE];
+ char filename_tmp[UTIL_PATH_SIZE];
+ FILE *new_queue_file = NULL;
+ unsigned int i;
+
+ /* read old queue file */
+ if (udev_queue_export->queue_file != NULL) {
+ dbg(udev_queue_export->udev, "compacting queue file, freeing %d bytes\n",
+ udev_queue_export->waste_bytes);
+
+ devpaths = build_index(udev_queue_export);
+ if (devpaths != NULL)
+ udev_queue_export->seqnum_max += devpaths->devpaths_first;
+ }
+ if (devpaths == NULL) {
+ dbg(udev_queue_export->udev, "creating empty queue file\n");
+ udev_queue_export->queued_count = 0;
+ udev_queue_export->seqnum_max = udev_queue_export->seqnum_min;
+ }
+
+ /* create new queue file */
+ util_strscpyl(filename_tmp, sizeof(filename_tmp), udev_get_dev_path(udev_queue_export->udev), "/.udev/queue.tmp", NULL);
+ new_queue_file = fopen(filename_tmp, "w+");
+ if (new_queue_file == NULL)
+ goto error;
+ seqnum = udev_queue_export->seqnum_max;
+ fwrite(&seqnum, 1, sizeof(unsigned long long int), new_queue_file);
+
+ /* copy unfinished events only to the new file */
+ if (devpaths != NULL) {
+ for (i = devpaths->devpaths_first; i < devpaths->devpaths_size; i++) {
+ char devpath[UTIL_PATH_SIZE];
+ int err;
+ unsigned short devpath_len;
+
+ if (devpaths->devpaths[i] != 0)
+ {
+ skip_to(udev_queue_export->queue_file, devpaths->devpaths[i]);
+ err = udev_queue_read_devpath(udev_queue_export->queue_file, devpath, sizeof(devpath));
+ devpath_len = err;
+
+ fwrite(&seqnum, sizeof(unsigned long long int), 1, new_queue_file);
+ fwrite(&devpath_len, sizeof(unsigned short), 1, new_queue_file);
+ fwrite(devpath, 1, devpath_len, new_queue_file);
+ }
+ seqnum++;
+ }
+ free(devpaths);
+ devpaths = NULL;
+ }
+ fflush(new_queue_file);
+ if (ferror(new_queue_file))
+ goto error;
+
+ /* rename the new file on top of the old one */
+ util_strscpyl(filename, sizeof(filename), udev_get_dev_path(udev_queue_export->udev), "/.udev/queue.bin", NULL);
+ if (rename(filename_tmp, filename) != 0)
+ goto error;
+
+ if (udev_queue_export->queue_file != NULL)
+ fclose(udev_queue_export->queue_file);
+ udev_queue_export->queue_file = new_queue_file;
+ udev_queue_export->waste_bytes = 0;
+
+ return 0;
+
+error:
+ err(udev_queue_export->udev, "failed to create queue file: %m\n");
+ udev_queue_export_cleanup(udev_queue_export);
+
+ if (udev_queue_export->queue_file != NULL) {
+ fclose(udev_queue_export->queue_file);
+ udev_queue_export->queue_file = NULL;
+ }
+ if (new_queue_file != NULL)
+ fclose(new_queue_file);
+
+ if (devpaths != NULL)
+ free(devpaths);
+ udev_queue_export->queued_count = 0;
+ udev_queue_export->waste_bytes = 0;
+ udev_queue_export->seqnum_max = udev_queue_export->seqnum_min;
+
+ return -1;
+}
+
+static int write_queue_record(struct udev_queue_export *udev_queue_export,
+ unsigned long long int seqnum, const char *devpath, size_t devpath_len)
+{
+ unsigned short len;
+
+ if (udev_queue_export->queue_file == NULL) {
+ dbg(udev_queue_export->udev, "can't record event: queue file not available\n");
+ return -1;
+ }
+
+ if (fwrite(&seqnum, sizeof(unsigned long long int), 1, udev_queue_export->queue_file) != 1)
+ goto write_error;
+
+ len = (devpath_len < USHRT_MAX) ? devpath_len : USHRT_MAX;
+ if (fwrite(&len, sizeof(unsigned short), 1, udev_queue_export->queue_file) != 1)
+ goto write_error;
+ if (fwrite(devpath, 1, len, udev_queue_export->queue_file) != len)
+ goto write_error;
+
+ /* *must* flush output; caller may fork */
+ if (fflush(udev_queue_export->queue_file) != 0)
+ goto write_error;
+
+ return 0;
+
+write_error:
+ /* if we failed half way through writing a record to a file,
+ we should not try to write any further records to it. */
+ err(udev_queue_export->udev, "error writing to queue file: %m\n");
+ fclose(udev_queue_export->queue_file);
+ udev_queue_export->queue_file = NULL;
+
+ return -1;
+}
+
+
+enum device_state {
+ DEVICE_QUEUED,
+ DEVICE_FINISHED,
+ DEVICE_FAILED,
+};
+
+static inline size_t queue_record_size(size_t devpath_len)
+{
+ return sizeof(unsigned long long int) + sizeof(unsigned short int) + devpath_len;
+}
+
+static int update_queue(struct udev_queue_export *udev_queue_export,
+ struct udev_device *udev_device, enum device_state state)
+{
+ unsigned long long int seqnum = udev_device_get_seqnum(udev_device);
+ const char *devpath = NULL;
+ size_t devpath_len = 0;
+ int bytes;
+ int err;
+
+ if (state == DEVICE_QUEUED) {
+ devpath = udev_device_get_devpath(udev_device);
+ devpath_len = strlen(devpath);
+ }
+
+ /* recover from an earlier failed rebuild */
+ if (udev_queue_export->queue_file == NULL) {
+ if (rebuild_queue_file(udev_queue_export) != 0)
+ return -1;
+ }
+
+ /* when the queue files grow too large, they must be garbage collected and rebuilt */
+ bytes = ftell(udev_queue_export->queue_file) + queue_record_size(devpath_len);
+
+ /* if we're removing the last event from the queue, that's the best time to rebuild it */
+ if (state != DEVICE_QUEUED && udev_queue_export->queued_count == 1 && bytes > 2048) {
+ /* because we don't need to read the old queue file */
+ fclose(udev_queue_export->queue_file);
+ udev_queue_export->queue_file = NULL;
+ rebuild_queue_file(udev_queue_export);
+ return 0;
+ }
+
+ /* try to rebuild the queue files before they grow larger than one page. */
+ if ((udev_queue_export->waste_bytes > bytes / 2) && bytes > 4096)
+ rebuild_queue_file(udev_queue_export);
+
+ /* don't record a finished event, if we already dropped the event in a failed rebuild */
+ if (seqnum < udev_queue_export->seqnum_max)
+ return 0;
+
+ /* now write to the queue */
+ if (state == DEVICE_QUEUED) {
+ udev_queue_export->queued_count++;
+ udev_queue_export->seqnum_min = seqnum;
+ } else {
+ udev_queue_export->waste_bytes += queue_record_size(devpath_len) + queue_record_size(0);
+ udev_queue_export->queued_count--;
+ }
+ err = write_queue_record(udev_queue_export, seqnum, devpath, devpath_len);
+
+ /* try to handle ENOSPC */
+ if (err != 0 && udev_queue_export->queued_count == 0) {
+ udev_queue_export_cleanup(udev_queue_export);
+ err = rebuild_queue_file(udev_queue_export);
+ }
+
+ return err;
+}
+
+static void update_failed(struct udev_queue_export *udev_queue_export,
+ struct udev_device *udev_device, enum device_state state)
+{
+ struct udev *udev = udev_device_get_udev(udev_device);
+ char filename[UTIL_PATH_SIZE];
+ char *s;
+ size_t l;
+
+ if (state != DEVICE_FAILED && udev_queue_export->failed_count == 0)
+ return;
+
+ /* location of failed file */
+ s = filename;
+ l = util_strpcpyl(&s, sizeof(filename), udev_get_dev_path(udev_queue_export->udev), "/.udev/failed/", NULL);
+ util_path_encode(udev_device_get_devpath(udev_device), s, l);
+
+ switch (state) {
+ case DEVICE_FAILED:
+ /* record event in the failed directory */
+ if (udev_queue_export->failed_count == 0)
+ util_create_path(udev, filename);
+ udev_queue_export->failed_count++;
+
+ udev_selinux_setfscreatecon(udev, filename, S_IFLNK);
+ symlink(udev_device_get_devpath(udev_device), filename);
+ udev_selinux_resetfscreatecon(udev);
+ break;
+
+ case DEVICE_QUEUED:
+ /* delete failed file */
+ if (unlink(filename) == 0) {
+ util_delete_path(udev, filename);
+ udev_queue_export->failed_count--;
+ }
+ break;
+
+ case DEVICE_FINISHED:
+ if (udev_device_get_devpath_old(udev_device) != NULL) {
+ /* "move" event - rename failed file to current name, do not delete failed */
+ char filename_old[UTIL_PATH_SIZE];
+
+ s = filename_old;
+ l = util_strpcpyl(&s, sizeof(filename_old), udev_get_dev_path(udev_queue_export->udev), "/.udev/failed/", NULL);
+ util_path_encode(udev_device_get_devpath_old(udev_device), s, l);
+
+ if (rename(filename_old, filename) == 0)
+ info(udev, "renamed devpath, moved failed state of '%s' to %s'\n",
+ udev_device_get_devpath_old(udev_device), udev_device_get_devpath(udev_device));
+ }
+ break;
+ }
+
+ return;
+}
+
+static int update(struct udev_queue_export *udev_queue_export,
+ struct udev_device *udev_device, enum device_state state)
+{
+ update_failed(udev_queue_export, udev_device, state);
+
+ if (update_queue(udev_queue_export, udev_device, state) != 0)
+ return -1;
+
+ return 0;
+}
+
+int udev_queue_export_device_queued(struct udev_queue_export *udev_queue_export, struct udev_device *udev_device)
+{
+ return update(udev_queue_export, udev_device, DEVICE_QUEUED);
+}
+
+int udev_queue_export_device_finished(struct udev_queue_export *udev_queue_export, struct udev_device *udev_device)
+{
+ return update(udev_queue_export, udev_device, DEVICE_FINISHED);
+}
+
+int udev_queue_export_device_failed(struct udev_queue_export *udev_queue_export, struct udev_device *udev_device)
+{
+ return update(udev_queue_export, udev_device, DEVICE_FAILED);
+}