summaryrefslogtreecommitdiff
path: root/rcm-server-kdb.c
diff options
context:
space:
mode:
Diffstat (limited to 'rcm-server-kdb.c')
-rw-r--r--rcm-server-kdb.c702
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);
+}
+