#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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); }