diff options
Diffstat (limited to 'rcm-server-kdb.c')
-rw-r--r-- | rcm-server-kdb.c | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/rcm-server-kdb.c b/rcm-server-kdb.c new file mode 100644 index 0000000..14d4a56 --- /dev/null +++ b/rcm-server-kdb.c @@ -0,0 +1,702 @@ +#include <stdio.h> +#include <stdint.h> +#include <stdbool.h> +#include <stdarg.h> +#include <inttypes.h> +#include <sys/types.h> +#include <dirent.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <sqlite3.h> +#include <systemd/sd-bus.h> +#include <sys/inotify.h> + +#include "utils.h" +#include "shared.h" +#include "rcm-server-main.h" +#include "rcm-server-kdb.h" +#include "rcm-server-keymap.h" + +struct inotify_dir { + int iwd; + int dfd; + int64_t dirid; + struct list_head list; +}; + +static LIST_HEAD(inotify_dir_list); + +DEFINE_TRIVIAL_CLEANUP_FUNC(sqlite3_stmt *, sqlite3_finalize); +#define _cleanup_finalize_stmt_ _cleanup_(sqlite3_finalizep) + +DEFINE_TRIVIAL_CLEANUP_FUNC(sqlite3 *, sqlite3_close); +#define _cleanup_sqlite3_close_ _cleanup_(sqlite3_closep) + +static int +_sql_do_prep(sqlite3 *db, sqlite3_stmt **stmt_out, const char *sql, int nbyte, va_list ap) +{ + _cleanup_finalize_stmt_ sqlite3_stmt *stmt = NULL; + unsigned i; + int r; + + r = sqlite3_prepare_v2(db, sql, nbyte, &stmt, NULL); + if (r != SQLITE_OK) { + fprintf(stderr, "SQL error: %s - %s\n", sql, sqlite3_errmsg(db)); + return -EIO; + } + + for (i = 0; i < sqlite3_bind_parameter_count(stmt); i++) { + int type; + + type = va_arg(ap, int); + switch (type) { + case SQLITE_INTEGER: + r = sqlite3_bind_int64(stmt, i + 1, va_arg(ap, int64_t)); + break; + case SQLITE_TEXT: + r = sqlite3_bind_text(stmt, i + 1, va_arg(ap, char *), -1, SQLITE_STATIC); + break; + default: + fprintf(stderr, "SQL error: %s - invalid type (%i)\n", sql, type); + return -EINVAL; + } + + if (r != SQLITE_OK) { + fprintf(stderr, "SQL error: %s - %s\n", sql, sqlite3_errmsg(db)); + return -EIO; + } + } + + *stmt_out = stmt; + stmt = NULL; + return 0; +} + +static int +_sql_prep(sqlite3 *db, sqlite3_stmt **stmt_out, const char *sql, int nbyte, ...) +{ + va_list ap; + int r; + + va_start(ap, nbyte); + r = _sql_do_prep(db, stmt_out, sql, nbyte, ap); + va_end(ap); + return r; +} +#define sql_prep(db, stmt, sql, ...) _sql_prep((db), (stmt), (sql), strlen((sql)) + 1, __VA_ARGS__) + +static int +_sql_get_row(sqlite3_stmt *stmt, va_list ap) +{ + unsigned i; + int64_t *vint; + const char **vchar; + int r; + + + r = sqlite3_step(stmt); + if (r == SQLITE_DONE) + return 0; + else if (r != SQLITE_ROW) + return -r; + + for (i = 0; i < sqlite3_column_count(stmt); i++) { + int type = sqlite3_column_type(stmt, i); + + switch (type) { + case SQLITE_INTEGER: + vint = va_arg(ap, int64_t *); + *vint = sqlite3_column_int64(stmt, i); + break; + case SQLITE_TEXT: + vchar = va_arg(ap, const char **); + *vchar = (const char *)sqlite3_column_text(stmt, i); + break; + default: + fprintf(stderr, "SQL error: invalid type (%i)\n", type); + r = -SQLITE_ERROR; + goto out; + } + } + + return 1; + +out: + return r; +} + +static int +sql_get_row(sqlite3_stmt *stmt, ...) +{ + va_list ap; + int r; + + va_start(ap, stmt); + r = _sql_get_row(stmt, ap); + va_end(ap); + + return r; +} + +static int +sql_get_single_row(sqlite3_stmt *stmt, ...) +{ + va_list ap; + int r; + + va_start(ap, stmt); + r = _sql_get_row(stmt, ap); + va_end(ap); + + if (r < 1) + return r; + + r = sqlite3_step(stmt); + if (r != SQLITE_DONE) + return -EIO; + + return 1; +} + +static int +_sql_exec(sqlite3 *db, const char *sql, int nbyte, ...) +{ + _cleanup_finalize_stmt_ sqlite3_stmt *stmt = NULL; + va_list ap; + int r; + + va_start(ap, nbyte); + r = _sql_do_prep(db, &stmt, sql, nbyte, ap); + va_end(ap); + + if (r < 0 ) + return r; + + r = sqlite3_step(stmt); + + if (r != SQLITE_DONE) { + fprintf(stderr, "sql_exec: unexpected return code (%i) for query %s\n", r, sql); + return -EIO; + } + + return 0; +} +#define sql_exec(db, sql, ...) _sql_exec((db), (sql), strlen((sql)) + 1, __VA_ARGS__) + +static uint64_t +mtime_in_nsec(struct stat *stat) +{ + return (uint64_t)stat->st_atim.tv_sec * 1000000000 + + (uint64_t)stat->st_atim.tv_nsec; +} + +static int +get_directory(sqlite3 *db, const char *path, int64_t *dirid, uint64_t *mtime) +{ + _cleanup_finalize_stmt_ sqlite3_stmt *stmt = NULL; + int r; + + r = sql_prep(db, &stmt, + "SELECT dirid, mtime FROM directories WHERE abspath = :PATH;", + SQLITE_TEXT, path); + if (r < 0) + return r; + + r = sql_get_single_row(stmt, dirid, mtime); + return r; +} + +static void +kdb_check_file(struct manager *mgr, int dfd, int64_t dirid, int64_t fileid, const char *filename, uint64_t db_mtime) +{ + struct stat stat; + uint64_t file_mtime; + _cleanup_keymap_free_ struct keymap *keymap = NULL; + int r; + + printf("%s: check file %s (id %" PRIi64 " dir %" PRIi64 " dirfd %i db_mtime %" PRIu64 ")\n", + __FUNCTION__, filename, fileid, dirid, dfd, db_mtime); + + if (dfd < 0 || !filename) { + printf("Invalid"); + return; + } + + if (fstatat(dfd, filename, &stat, AT_SYMLINK_NOFOLLOW) < 0) { + fprintf(stderr, "\tfstatat(%s) - failed: %m\n", filename); + goto out; + } + + if ((stat.st_mode & S_IFMT) != S_IFREG) { + printf("\t%s - not a regular file\n", filename); + goto out; + } + + file_mtime = mtime_in_nsec(&stat); + if (fileid >= 0) { + if (file_mtime <= db_mtime) { + printf("\t%s - file not older than db entry, skipped\n", filename); + return; + } + + r = sql_exec(mgr->db, + "DELETE FROM keymap WHERE fileid = :FILEID;", + SQLITE_INTEGER, fileid); + if (r < 0) { + printf("\t%s - failed to clean out old entries\n", filename); + goto out; + } + } + + keymap = keymap_read(dfd, ".", filename); + if (!keymap) { + printf("\t%s - unable to parse keymap, skipped\n", filename); + goto out; + } + + if (fileid >= 0) { + r = sql_exec(mgr->db, "UPDATE files SET name = :NAME, desc = :DESC, scanned = 1, mtime = :MTIME" + " WHERE fileid = :FILEID", + SQLITE_TEXT, keymap->name, SQLITE_TEXT, keymap->description, + SQLITE_INTEGER, file_mtime, SQLITE_INTEGER, fileid); + if (r < 0) { + printf("\t%s - failed to update old file entry\n", filename); + goto out; + } + + } else { + r = sql_exec(mgr->db, "INSERT INTO files (dirid, filename, name, desc, scanned, mtime)" + " VALUES (:DIRID, :FILENAME, :NAME, :DESC, 1, :MTIME);", + SQLITE_INTEGER, dirid, SQLITE_TEXT, filename, + SQLITE_TEXT, keymap->name, SQLITE_TEXT, keymap->description, + SQLITE_INTEGER, file_mtime); + if (r < 0) { + printf("\t%s - failed to create new file entry\n", filename); + goto out; + } + + fileid = sqlite3_last_insert_rowid(mgr->db); + } + + for (unsigned i = 0; i < keymap->keycode_count; i++) { + printf("\t%s - inserting entry %u/%u 0x%08" PRIx64 ":0x%08" PRIx64 ":0x%08" PRIx64 "\n", + filename, i, keymap->keycode_count, + (int64_t)keymap->keycodes[i].protocol, + (int64_t)keymap->keycodes[i].scancode, + (int64_t)keymap->keycodes[i].lik->value); + + r = sql_exec(mgr->db, "INSERT INTO keymap (fileid, protocol, scancode, keycode)" + " VALUES (:FILEID, :PROTOCOL, :SCANCODE, :KEYCODE);", + SQLITE_INTEGER, fileid, + SQLITE_INTEGER, (int64_t)keymap->keycodes[i].protocol, + SQLITE_INTEGER, (int64_t)keymap->keycodes[i].scancode, + SQLITE_INTEGER, (int64_t)keymap->keycodes[i].lik->value); + if (r < 0) { + printf("\t%s - failed to insert keymap entry\n", filename); + goto out; + } + } + + printf("%s - inserted into db with %u entries\n", filename, keymap->keycode_count); + return; + +out: + if (fileid >= 0) { + printf("\t%s - Deleting row from DB\n", filename); + sql_exec(mgr->db, + "DELETE FROM files WHERE fileid = :FILEID;", + SQLITE_INTEGER, fileid); + } +} + +int +kdb_add_directory(struct manager *mgr, const char *path) +{ + sqlite3 *db = mgr->db; + _cleanup_free_ char *abspath = NULL; + _cleanup_free_ struct inotify_dir *in_dir = NULL; + _cleanup_closedir_ DIR *dir = NULL; + _cleanup_finalize_stmt_ sqlite3_stmt *stmt = NULL; + struct dirent *dent; + struct stat stat; + uint64_t mtime; + uint64_t old_mtime; + int64_t dirid; + int dfd; + int r; + + abspath = realpath(path, NULL); + if (!abspath) { + r = -errno; + fprintf(stderr, "realpath(%s) failed: %m\n", path); + goto out; + } + + printf("Scanning directory: %s (%s)\n", abspath, path); + + in_dir = malloc(sizeof(*in_dir)); + if (!in_dir) { + r = -ENOMEM; + fprintf(stderr, "malloc failed: %m\n"); + goto out; + } + in_dir->iwd = -1; + in_dir->dfd = -1; + in_dir->dirid = -1; + + dfd = open(abspath, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (dfd < 0) { + r = -errno; + fprintf(stderr, "open(%s) failed: %m\n", abspath); + goto out; + } + in_dir->dfd = dfd; + + dfd = fcntl(in_dir->dfd, F_DUPFD_CLOEXEC); + if (dfd < 0) { + r = -errno; + fprintf(stderr, "fcntl(%i, F_DUPFD_CLOEXEC) failed: %m\n", in_dir->dfd); + goto out; + } + + dir = fdopendir(dfd); + if (!dir) { + close(dfd); + r = -errno; + fprintf(stderr, "fdopendir(%i) failed: %m\n", dfd); + goto out; + } + + if (fstat(dirfd(dir), &stat) < 0) { + r = -errno; + fprintf(stderr, "fstat(%s) failed: %s\n", abspath, strerror(errno)); + goto out; + } + + mtime = mtime_in_nsec(&stat); + + r = inotify_add_watch(mgr->db_ifd, abspath, IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE | IN_ONLYDIR); + if (r < 0) { + r = -errno; + fprintf(stderr, "inotify_add_watch(%s) failed: %m\n", abspath); + goto out; + } + in_dir->iwd = r; + + r = get_directory(db, abspath, &dirid, &old_mtime); + if (r < 0) { + fprintf(stderr, "get_directory(%s) failed: %i\n", abspath, r); + goto out; + + } else if (r == 0) { + printf("Adding new directory: %s\n", abspath); + + r = sql_exec(db, + "INSERT INTO directories (abspath, mtime) VALUES (:PATH, 0);", + SQLITE_TEXT, abspath); + if (r < 0) + goto out; + + dirid = (int64_t)sqlite3_last_insert_rowid(db); + old_mtime = 0; + + } else { + printf("Marking files in directory %s for a rescan\n", abspath); + + r = sql_exec(db, + "UPDATE files SET scanned = 0 WHERE dirid = :ID;", + SQLITE_INTEGER, dirid); + if (r < 0) + goto out; + } + + in_dir->dirid = dirid; + + if (mtime > old_mtime) { + /* Look for added files */ + printf("Directory %s modified, scanning for new files\n", abspath); + errno = 0; + + while ((dent = readdir(dir)) != NULL) { + if (dent->d_name[0] == '\0' || dent->d_name[0] == '.') { + printf("\t%s - skipped\n", dent->d_name); + errno = 0; + continue; + } + + switch (dent->d_type) { + case DT_UNKNOWN: + if (fstatat(dirfd(dir), dent->d_name, &stat, AT_SYMLINK_NOFOLLOW) < 0) { + fprintf(stderr, "\tfstatat(%s) - failed: %s\n", dent->d_name, strerror(errno)); + errno = 0; + continue; + } + + if ((stat.st_mode & S_IFMT) != S_IFREG) { + printf("\t%s - not a regular file\n", dent->d_name); + errno = 0; + continue; + } + + /* fall through */ + + case DT_REG: + printf("\t%s - add (possibly) to DB\n", dent->d_name); + r = sql_exec(db, + "INSERT OR IGNORE INTO" + " files (dirid, filename, name, scanned, mtime)" + " VALUES (:DIRID, :FILENAME, '', 0, 0);", + SQLITE_INTEGER, dirid, + SQLITE_TEXT, dent->d_name); + if (r < 0) + goto out; + + break; + + default: + printf("\t%s - not a regular file\n", dent->d_name); + break; + } + + errno = 0; + } + + if (errno != 0) + fprintf(stderr, "readdir error: %s\n", strerror(errno)); + } + + /* check all files in db */ + r = sql_prep(db, &stmt, + "SELECT fileid, filename, mtime FROM files WHERE dirid = :DIRID AND scanned = 0;", + SQLITE_INTEGER, dirid); + if (r < 0) + goto out; + else { + int64_t fileid; + char *filename; + uint64_t mtime; + + while ((r = sql_get_row(stmt, &fileid, &filename, &mtime)) > 0) + kdb_check_file(mgr, in_dir->dfd, dirid, fileid, filename, mtime); + + if (r < 0) + goto out; + } + + /* delete files not found during scan */ + r = sql_exec(db, + "DELETE FROM files WHERE dirid = :DIRID AND scanned = 0;", + SQLITE_INTEGER, dirid); + if (r < 0) + goto out; + + list_add(&in_dir->list, &inotify_dir_list); + in_dir = NULL; + return 0; + +out: + if (in_dir && in_dir->iwd >= 0) + inotify_rm_watch(mgr->db_ifd, in_dir->iwd); + if (in_dir && in_dir->dfd >= 0) + close(in_dir->dfd); + + return r; +} + +static int +create_tables(sqlite3 *db) +{ + int r; + + r = sql_exec(db, + "CREATE TABLE IF NOT EXISTS directories(" + " dirid INTEGER PRIMARY KEY NOT NULL," + " abspath TEXT UNIQUE NOT NULL," + " mtime INTEGER NOT NULL" + ");", NULL); + if (r != SQLITE_OK) + goto out; + + r = sql_exec(db, + "CREATE TABLE IF NOT EXISTS files(" + " dirid INTEGER NOT NULL REFERENCES directories(dirid) ON UPDATE CASCADE ON DELETE CASCADE," + " fileid INTEGER PRIMARY KEY NOT NULL," + " filename TEXT NOT NULL," + " name TEXT NOT NULL," + " desc TEXT," + " scanned BOOLEAN NOT NULL CHECK (scanned IN (0,1))," + " mtime INTEGER NOT NULL," + " UNIQUE(dirid, filename)" + ");", NULL); + if (r != SQLITE_OK) + goto out; + + r = sql_exec(db, + "CREATE TABLE IF NOT EXISTS keymap(" + " fileid INTEGER NOT NULL REFERENCES files(fileid) ON UPDATE CASCADE ON DELETE CASCADE," + " protocol INTEGER NOT NULL," + " scancode INTEGER NOT NULL," + " keycode INTEGER NOT NULL," + " UNIQUE(protocol, scancode)" + ");", NULL); + if (r != SQLITE_OK) + goto out; + + return SQLITE_OK; + +out: + return r; +} + +static int +enable_foreign_keys(sqlite3 *db) +{ + sqlite3_stmt *stmt; + bool supported = false; + int r; + + sql_exec(db, "PRAGMA foreign_keys = ON;", NULL); + + r = sql_prep(db, &stmt, "PRAGMA foreign_keys;", NULL); + if (r == SQLITE_OK) { + int64_t value; + + r = sql_get_single_row(stmt, &value); + if (r > 0 && value > 0) + supported = true; + } + + sqlite3_finalize(stmt); + return supported ? SQLITE_OK : SQLITE_ERROR; +} + +static int +inotify_read(sd_event_source *s, int fd, uint32_t revents, void *userdata) +{ + struct manager *mgr = userdata; + char buf[sizeof(struct inotify_event) + NAME_MAX + 1] __attribute__ ((aligned(__alignof__(struct inotify_event)))); + struct inotify_event *ev = (struct inotify_event *)buf; + ssize_t r; + + if (revents & EPOLLHUP) { + fprintf(stderr, "kdb inotify connection closed!\n"); + return 0; + } + + if (!(revents & EPOLLIN)) { + fprintf(stderr, "unexpected udev event: %" PRIu32 "\n", revents); + return 0; + } + + while ((r = read(mgr->db_ifd, buf, sizeof(buf))) > 0) { + struct inotify_dir *idir; + sqlite3_stmt *stmt; + int64_t fileid; + uint64_t mtime; + + printf("Read inotify event:\n"); + printf("\tWD: %i\n", ev->wd); + printf("\tMask: %08" PRIx32 "\n", ev->mask); + printf("\tCookie: %08" PRIx32 "\n", ev->cookie); + printf("\tLen: %" PRIu32 "\n", ev->len); + printf("\tName: %s\n", ev->name); + + list_for_each_entry(idir, &inotify_dir_list, list) { + + if (idir->iwd != ev->wd) + continue; + + printf("\tSQL dirid: %" PRIi64 "\n", idir->dirid); + r = sql_prep(mgr->db, &stmt, + "SELECT fileid, mtime FROM files WHERE dirid = :DIRID AND filename = :FILENAME;", + SQLITE_INTEGER, idir->dirid, SQLITE_TEXT, ev->name); + if (r < 0) + break; + + fileid = -1; + mtime = 0; + r = sql_get_row(stmt, &fileid, &mtime); + sqlite3_finalize(stmt); + if (r < 0) + break; + + printf("\tSQL fileid: %" PRIi64 "\n", fileid); + printf("\tSQL mtime: %" PRIu64 "\n", mtime); + kdb_check_file(mgr, idir->dfd, idir->dirid, fileid, ev->name, mtime); + break; + } + } + + return 1; +}; + +int +kdb_init(struct manager *mgr, const char *path) +{ + _cleanup_close_ int ifd = -1; + _cleanup_event_source_unref_ sd_event_source *ev = NULL; + _cleanup_sqlite3_close_ sqlite3 *db = NULL; + int r; + + ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (ifd < 0) { + r = errno; + fprintf(stderr, "Failed to create inotify instance: %s\n", strerror(r)); + return -r; + } + + r = sd_event_add_io(mgr->event, &ev, ifd, EPOLLIN, inotify_read, mgr); + if (r < 0) { + fprintf(stderr, "Failed to add kdb inotify instance to event loop: %m\n"); + return r; + } + + r = sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + if (r != SQLITE_OK) { + fprintf(stderr, "Can't open database: (%i) %s\n", r, sqlite3_errmsg(db)); + return -1; + } + + r = enable_foreign_keys(db); + if (r != SQLITE_OK) { + fprintf(stderr, "Failed to enable foreign keys\n"); + return -1; + } + + r = create_tables(db); + if (r != SQLITE_OK) { + fprintf(stderr, "Failed to create tables\n"); + return -1; + } + + mgr->db_ifd_ev = ev; + mgr->db = db; + mgr->db_ifd = ifd; + ev = NULL; + db = NULL; + ifd = -1; + return 0; +} + +void +kdb_close(struct manager *mgr) +{ + struct inotify_dir *idir; + struct inotify_dir *tmp; + + list_for_each_entry_safe(idir, tmp, &inotify_dir_list, list) { + if (idir->dfd >= 0) + close(idir->dfd); + list_del(&idir->list); + free(idir); + } + + sqlite3_close(mgr->db); + close(mgr->db_ifd); + sd_event_source_unref(mgr->db_ifd_ev); +} + |