diff options
Diffstat (limited to 'src/bin/lightmediascannerd.c')
-rw-r--r-- | src/bin/lightmediascannerd.c | 1313 |
1 files changed, 1313 insertions, 0 deletions
diff --git a/src/bin/lightmediascannerd.c b/src/bin/lightmediascannerd.c new file mode 100644 index 0000000..606860d --- /dev/null +++ b/src/bin/lightmediascannerd.c @@ -0,0 +1,1313 @@ +#include "lightmediascanner.h" +#include <gio/gio.h> +#include <glib-unix.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <sqlite3.h> + +static char *db_path = NULL; +static char **charsets = NULL; +static GHashTable *categories = NULL; +static int commit_interval = 100; +static int slave_timeout = 60; +static gboolean vacuum = FALSE; +static gboolean startup_scan = FALSE; + +static GDBusNodeInfo *introspection_data = NULL; + +static const char BUS_PATH[] = "/org/lightmediascanner/Scanner1"; +static const char BUS_IFACE[] = "org.lightmediascanner.Scanner1"; + +static const char introspection_xml[] = + "<node>" + " <interface name=\"org.lightmediascanner.Scanner1\">" + " <property name=\"IsScanning\" type=\"b\" access=\"read\" />" + " <property name=\"WriteLocked\" type=\"b\" access=\"read\" />" + " <property name=\"UpdateID\" type=\"t\" access=\"read\" />" + " <property name=\"Categories\" type=\"a{sv}\" access=\"read\" />" + " <method name=\"Scan\">" + " <arg direction=\"in\" type=\"a{sv}\" name=\"specification\" />" + " </method>" + " <method name=\"Stop\" />" + " <method name=\"RequestWriteLock\" />" + " <method name=\"ReleaseWriteLock\" />" + " </interface>" + "</node>"; + +typedef struct scanner_category +{ + char *category; + GArray *parsers; + GArray *dirs; +} scanner_category_t; + +typedef struct scanner_pending +{ + char *category; + GList *paths; +} scanner_pending_t; + +typedef struct scanner { + GDBusConnection *conn; + char *write_lock; + unsigned write_lock_name_watcher; + GDBusMethodInvocation *pending_stop; + GList *pending_scan; /* of scanner_pending_t, see scanner_thread_work */ + GThread *thread; /* see scanner_thread_work */ + unsigned cleanup_thread_idler; /* see scanner_thread_work */ + guint64 update_id; + struct { + unsigned idler; /* not a flag, but g_source tag */ + unsigned is_scanning : 1; + unsigned write_locked : 1; + unsigned update_id : 1; + unsigned categories: 1; + } changed_props; +} scanner_t; + +static scanner_category_t * +scanner_category_new(const char *category) +{ + scanner_category_t *sc = g_new0(scanner_category_t, 1); + + sc->category = g_strdup(category); + sc->parsers = g_array_new(TRUE, TRUE, sizeof(char *)); + sc->dirs = g_array_new(TRUE, TRUE, sizeof(char *)); + + return sc; +} + +static void +scanner_category_destroy(scanner_category_t *sc) +{ + char **itr; + + for (itr = (char **)sc->parsers->data; *itr != NULL; itr++) + g_free(*itr); + for (itr = (char **)sc->dirs->data; *itr != NULL; itr++) + g_free(*itr); + + g_array_free(sc->parsers, TRUE); + g_array_free(sc->dirs, TRUE); + g_free(sc->category); + + g_free(sc); +} + +static void scanner_release_write_lock(scanner_t *scanner); + +static void +scanner_write_lock_vanished(GDBusConnection *conn, const char *name, gpointer data) +{ + scanner_t *scanner = data; + g_warning("Write lock holder %s vanished, release lock\n", name); + scanner_release_write_lock(scanner); +} + +static guint64 +get_update_id(void) +{ + const char sql[] = "SELECT version FROM lms_internal WHERE tab='update_id'"; + sqlite3 *db; + sqlite3_stmt *stmt; + int ret; + guint64 update_id = 0; + + ret = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL); + if (ret != SQLITE_OK) { + g_warning("Couldn't open '%s': %s", db_path, sqlite3_errmsg(db)); + goto end; + } + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { + g_warning("Couldn't get update_id from %s: %s", + db_path, sqlite3_errmsg(db)); + goto end; + } + + ret = sqlite3_step(stmt); + if (ret == SQLITE_DONE) + update_id = 0; + else if (ret == SQLITE_ROW) + update_id = sqlite3_column_int(stmt, 0); + else + g_warning("Couldn't run SQL to get update_id, ret=%d: %s", + ret, sqlite3_errmsg(db)); + + sqlite3_reset(stmt); + sqlite3_finalize(stmt); + +end: + sqlite3_close(db); + + g_debug("update id: %llu", (unsigned long long)update_id); + return update_id; +} + +static void +do_vacuum(void) +{ + const char sql[] = "VACUUM"; + sqlite3 *db; + sqlite3_stmt *stmt; + int ret; + + ret = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, NULL); + if (ret != SQLITE_OK) { + g_warning("Couldn't open '%s': %s", db_path, sqlite3_errmsg(db)); + goto end; + } + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { + g_warning("Couldn't vacuum from %s: %s", + db_path, sqlite3_errmsg(db)); + goto end; + } + + ret = sqlite3_step(stmt); + if (ret != SQLITE_DONE) + g_warning("Couldn't run SQL VACUUM, ret=%d: %s", + ret, sqlite3_errmsg(db)); + + sqlite3_reset(stmt); + sqlite3_finalize(stmt); + +end: + sqlite3_close(db); +} + +static gboolean +check_write_locked(const scanner_t *scanner) +{ + return scanner->write_lock != NULL || scanner->thread != NULL; +} + +static void +category_variant_foreach(gpointer key, gpointer value, gpointer user_data) +{ + scanner_category_t *sc = value; + GVariantBuilder *builder = user_data; + GVariantBuilder *sub, *darr, *parr; + char **itr; + + darr = g_variant_builder_new(G_VARIANT_TYPE("as")); + for (itr = (char **)sc->dirs->data; *itr != NULL; itr++) + g_variant_builder_add(darr, "s", *itr); + + parr = g_variant_builder_new(G_VARIANT_TYPE("as")); + for (itr = (char **)sc->parsers->data; *itr != NULL; itr++) + g_variant_builder_add(parr, "s", *itr); + + sub = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(sub, "{sv}", "dirs", g_variant_builder_end(darr)); + g_variant_builder_add(sub, "{sv}", "parsers", g_variant_builder_end(parr)); + + g_variant_builder_add(builder, "{sv}", sc->category, + g_variant_builder_end(sub)); + + g_variant_builder_unref(sub); + g_variant_builder_unref(parr); + g_variant_builder_unref(darr); +} + +static GVariant * +categories_get_variant(void) +{ + GVariantBuilder *builder; + GVariant *variant; + + builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); + + g_hash_table_foreach(categories, category_variant_foreach, builder); + + variant = g_variant_builder_end(builder); + g_variant_builder_unref(builder); + + return variant; +} + +static gboolean +scanner_dbus_props_changed(gpointer data) +{ + GVariantBuilder *builder; + GError *error = NULL; + scanner_t *scanner = data; + guint64 update_id = 0; + + if (!check_write_locked(scanner)) + update_id = get_update_id(); + if (update_id > 0 && update_id != scanner->update_id) { + scanner->changed_props.update_id = TRUE; + scanner->update_id = update_id; + } + + builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + + if (scanner->changed_props.is_scanning) { + scanner->changed_props.is_scanning = FALSE; + g_variant_builder_add(builder, "{sv}", "IsScanning", + g_variant_new_boolean(scanner->thread != NULL)); + } + if (scanner->changed_props.write_locked) { + scanner->changed_props.write_locked = FALSE; + g_variant_builder_add( + builder, "{sv}", "WriteLocked", + g_variant_new_boolean(check_write_locked(scanner))); + } + if (scanner->changed_props.update_id) { + scanner->changed_props.update_id = FALSE; + g_variant_builder_add(builder, "{sv}", "UpdateID", + g_variant_new_uint64(scanner->update_id)); + } + if (scanner->changed_props.categories) { + scanner->changed_props.categories = FALSE; + g_variant_builder_add(builder, "{sv}", "Categories", + categories_get_variant()); + } + + g_dbus_connection_emit_signal(scanner->conn, + NULL, + BUS_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", + BUS_IFACE, builder, NULL), + &error); + g_variant_builder_unref(builder); + g_assert_no_error(error); + + scanner->changed_props.idler = 0; + return FALSE; +} + +static void +scanner_write_lock_changed(scanner_t *scanner) +{ + if (scanner->changed_props.idler == 0) + scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed, + scanner); + + scanner->changed_props.write_locked = TRUE; +} + +static void +scanner_acquire_write_lock(scanner_t *scanner, const char *sender) +{ + g_debug("acquired write lock for %s", sender); + scanner->write_lock = g_strdup(sender); + scanner->write_lock_name_watcher = g_bus_watch_name_on_connection( + scanner->conn, sender, G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, scanner_write_lock_vanished, scanner, NULL); + + scanner_write_lock_changed(scanner); +} + +static void +scanner_release_write_lock(scanner_t *scanner) +{ + g_debug("release write lock previously owned by %s", scanner->write_lock); + + g_free(scanner->write_lock); + scanner->write_lock = NULL; + + g_bus_unwatch_name(scanner->write_lock_name_watcher); + scanner->write_lock_name_watcher = 0; + + scanner_write_lock_changed(scanner); +} + +static void +scanner_pending_free(scanner_pending_t *pending) +{ + g_list_free_full(pending->paths, g_free); + g_free(pending->category); + g_free(pending); +} + +static scanner_pending_t * +scanner_pending_get_or_add(scanner_t *scanner, const char *category) +{ + scanner_pending_t *pending; + GList *n, *nlast = NULL; + + g_assert(scanner->thread == NULL); + + for (n = scanner->pending_scan; n != NULL; n = n->next) { + nlast = n; + pending = n->data; + if (strcmp(pending->category, category) == 0) + return pending; + } + + pending = g_new0(scanner_pending_t, 1); + pending->category = g_strdup(category); + pending->paths = NULL; + + /* I can't believe there is no g_list_insert_after() :-( */ + n = g_list_alloc(); + n->data = pending; + n->next = NULL; + n->prev = nlast; + + if (nlast) + nlast->next = n; + else + scanner->pending_scan = n; + + return pending; +} + +/* NOTE: assumes array was already validated for duplicates/restrictions */ +static void +scanner_pending_add_all(scanner_pending_t *pending, const GArray *arr) +{ + char **itr; + + + for (itr = (char **)arr->data; *itr != NULL; itr++) + pending->paths = g_list_prepend(pending->paths, g_strdup(*itr)); + + pending->paths = g_list_reverse(pending->paths); +} + +static void +scanner_pending_add(scanner_pending_t *pending, const GArray *restrictions, const char *path) +{ + GList *n, *nlast; + const char * const *itr; + gboolean allowed = FALSE; + + for (n = pending->paths; n != NULL; n = n->next) { + const char *other = n->data; + if (g_str_has_prefix(path, other)) { + g_debug("Path already in pending scan in category %s: %s (%s)", + pending->category, path, other); + return; + } + } + + for (itr = (const char *const*)restrictions->data; *itr != NULL; itr++) { + if (g_str_has_prefix(path, *itr)) { + allowed = TRUE; + break; + } + } + if (!allowed) { + g_warning("Path is outside of category %s directories: %s", + pending->category, path); + return; + } + + nlast = NULL; + for (n = pending->paths; n != NULL; ) { + char *other = n->data; + + nlast = n; + + if (!g_str_has_prefix(other, path)) + n = n->next; + else { + GList *tmp; + + g_debug("Path covers previous pending scan in category %s, " + "replace %s (%s)", + pending->category, other, path); + + tmp = n->next; + nlast = n->next ? n->next : n->prev; + + pending->paths = g_list_delete_link(pending->paths, n); + g_free(other); + + n = tmp; + } + } + + g_debug("New scan path for category %s: %s", pending->category, path); + + /* I can't believe there is no g_list_insert_after() :-( */ + n = g_list_alloc(); + n->data = g_strdup(path); + n->next = NULL; + n->prev = nlast; + + if (nlast) + nlast->next = n; + else + pending->paths = n; +} + +static void +scan_params_all(gpointer key, gpointer value, gpointer user_data) +{ + scanner_pending_t *pending; + scanner_category_t *sc = value; + scanner_t *scanner = user_data; + + pending = scanner_pending_get_or_add(scanner, sc->category); + scanner_pending_add_all(pending, sc->dirs); +} + +static void +dbus_scanner_scan_params_set(scanner_t *scanner, GVariant *params) +{ + GVariantIter *itr; + GVariant *el; + char *cat; + gboolean empty = TRUE; + + g_variant_get(params, "(a{sv})", &itr); + while (g_variant_iter_loop(itr, "{sv}", &cat, &el)) { + scanner_category_t *sc; + scanner_pending_t *pending; + GVariantIter *subitr; + char *path; + gboolean nodirs = TRUE; + + sc = g_hash_table_lookup(categories, cat); + if (!sc) { + g_warning("Unexpected scan category: %s, skipped.", cat); + continue; + } + + pending = scanner_pending_get_or_add(scanner, cat); + empty = FALSE; + + g_variant_get(el, "as", &subitr); + while (g_variant_iter_loop(subitr, "s", &path)) { + scanner_pending_add(pending, sc->dirs, path); + nodirs = FALSE; + } + + if (nodirs) + scanner_pending_add_all(pending, sc->dirs); + } + g_variant_iter_free(itr); + + if (empty) + g_hash_table_foreach(categories, scan_params_all, scanner); +} + +static void +scanner_is_scanning_changed(scanner_t *scanner) +{ + if (scanner->changed_props.idler == 0) + scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed, + scanner); + + scanner->changed_props.is_scanning = TRUE; +} + +static void +scan_progress_cb(lms_t *lms, const char *path, int pathlen, lms_progress_status_t status, void *data) +{ + const scanner_t *scanner = data; + if (scanner->pending_stop) + lms_stop_processing(lms); +} + +static lms_t * +setup_lms(const char *category, const scanner_t *scanner) +{ + scanner_category_t *sc; + char **itr; + lms_t *lms; + + sc = g_hash_table_lookup(categories, category); + if (!sc) { + g_error("Unknown category %s", category); + return NULL; + } + + if (sc->parsers->len == 0) { + g_warning("No parsers for category %s", category); + return NULL; + } + + lms = lms_new(db_path); + if (!lms) { + g_warning("Failed to create lms"); + return NULL; + } + + lms_set_commit_interval(lms, commit_interval); + lms_set_slave_timeout(lms, slave_timeout * 1000); + + if (charsets) { + for (itr = charsets; *itr != NULL; itr++) + if (lms_charset_add(lms, *itr) != 0) + g_warning("Couldn't add charset: %s", *itr); + } + + for (itr = (char **)sc->parsers->data; *itr != NULL; itr++) { + const char *parser = *itr; + lms_plugin_t *plugin; + + if (parser[0] == '/') + plugin = lms_parser_add(lms, parser); + else + plugin = lms_parser_find_and_add(lms, parser); + + if (!plugin) + g_warning("Couldn't add parser: %s", parser); + } + + lms_set_progress_callback(lms, scan_progress_cb, scanner, NULL); + + return lms; +} + +static gboolean +scanner_thread_cleanup(gpointer data) +{ + scanner_t *scanner = data; + gpointer ret; + + g_debug("cleanup scanner work thread"); + ret = g_thread_join(scanner->thread); + g_assert(ret == scanner); + + scanner->thread = NULL; + scanner->cleanup_thread_idler = 0; + + if (scanner->pending_stop) { + g_dbus_method_invocation_return_value(scanner->pending_stop, NULL); + g_object_unref(scanner->pending_stop); + scanner->pending_stop = NULL; + } + + g_list_free_full(scanner->pending_scan, + (GDestroyNotify)scanner_pending_free); + scanner->pending_scan = NULL; + + scanner_is_scanning_changed(scanner); + scanner_write_lock_changed(scanner); + + return FALSE; +} + +/* + * Note on thread usage and locks (or lack of locks): + * + * The main thread is responsible for launching the worker thread, + * setting 'scanner->thread' pointer, which is later checked *ONLY* by + * main thread. When the thread is done, it will notify the main + * thread with scanner_thread_cleanup() so it can unset the pointer + * and do whatever it needs, so 'scanner->thread' is exclusively + * managed by main thread. + * + * The other shared data 'scanner->pending_scan' is managed by the + * main thread only when 'scanner->thread' is unset. If there is a + * worker thread the main thread should never touch that list, thus + * there is *NO NEED FOR LOCKS*. + * + * The thread will stop its work by checking 'scanner->pending_stop', + * this is also done without a lock as there is no need for such thing + * given above. The stop is also voluntary and it can happen on a + * second iteration of work. + */ +static gpointer +scanner_thread_work(gpointer data) +{ + GList *lst; + scanner_t *scanner = data; + + g_debug("started scanner thread"); + + lst = scanner->pending_scan; + scanner->pending_scan = NULL; + while (lst) { + scanner_pending_t *pending; + lms_t *lms; + + if (scanner->pending_stop) + break; + + pending = lst->data; + lst = g_list_delete_link(lst, lst); + + g_debug("scan category: %s", pending->category); + lms = setup_lms(pending->category, scanner); + if (lms) { + while (pending->paths) { + char *path; + + if (scanner->pending_stop) + break; + + path = pending->paths->data; + pending->paths = g_list_delete_link(pending->paths, + pending->paths); + + g_debug("scan category %s, path %s", pending->category, path); + if (!scanner->pending_stop) + lms_check(lms, path); + if (!scanner->pending_stop && + g_file_test(path, G_FILE_TEST_EXISTS)) + lms_process(lms, path); + + g_free(path); + } + lms_free(lms); + } + + scanner_pending_free(pending); + } + + g_debug("finished scanner thread"); + + if (vacuum) { + GTimer *timer = g_timer_new(); + + g_debug("Starting SQL VACUUM..."); + g_timer_start(timer); + do_vacuum(); + g_timer_stop(timer); + g_debug("Finished VACUUM in %0.3f seconds.", + g_timer_elapsed(timer, NULL)); + g_timer_destroy(timer); + } + + scanner->cleanup_thread_idler = g_idle_add(scanner_thread_cleanup, scanner); + + return scanner; +} + +static void +do_scan(scanner_t *scanner) +{ + scanner->thread = g_thread_new("scanner", scanner_thread_work, scanner); + + scanner_is_scanning_changed(scanner); + scanner_write_lock_changed(scanner); +} + +static void +dbus_scanner_scan(GDBusMethodInvocation *inv, scanner_t *scanner, GVariant *params) +{ + if (scanner->thread) { + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.AlreadyScanning", + "Scanner was already scanning."); + return; + } + + if (scanner->write_lock) { + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.WriteLocked", + "Data Base has a write lock for another process."); + return; + } + + dbus_scanner_scan_params_set(scanner, params); + + do_scan(scanner); + + g_dbus_method_invocation_return_value(inv, NULL); +} + +static void +dbus_scanner_stop(GDBusMethodInvocation *inv, scanner_t *scanner) +{ + if (!scanner->thread) { + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.NotScanning", + "Scanner was already stopped."); + return; + } + if (scanner->pending_stop) { + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.AlreadyStopping", + "Scanner was already being stopped."); + return; + } + + scanner->pending_stop = g_object_ref(inv); +} + +static void +dbus_scanner_request_write_lock(GDBusMethodInvocation *inv, scanner_t *scanner, const char *sender) +{ + if (check_write_locked(scanner)) { + if (scanner->write_lock && strcmp(scanner->write_lock, sender) == 0) + g_dbus_method_invocation_return_value(inv, NULL); + else if (scanner->write_lock) + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.AlreadyLocked", + "Scanner is already locked"); + else + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.IsScanning", + "Scanner is scanning and can't grant a write lock"); + return; + } + + scanner_acquire_write_lock(scanner, sender); + g_dbus_method_invocation_return_value(inv, NULL); +} + +static void +dbus_scanner_release_write_lock(GDBusMethodInvocation *inv, scanner_t *scanner, const char *sender) +{ + if (!scanner->write_lock || strcmp(scanner->write_lock, sender) != 0) { + g_dbus_method_invocation_return_dbus_error( + inv, "org.lightmediascanner.NotLocked", + "Scanner was not locked by you."); + return; + } + + scanner_release_write_lock(scanner); + g_dbus_method_invocation_return_value(inv, NULL); +} + +static void +scanner_method_call(GDBusConnection *conn, const char *sender, const char *opath, const char *iface, const char *method, GVariant *params, GDBusMethodInvocation *inv, gpointer data) +{ + scanner_t *scanner = data; + + if (strcmp(method, "Scan") == 0) + dbus_scanner_scan(inv, scanner, params); + else if (strcmp(method, "Stop") == 0) + dbus_scanner_stop(inv, scanner); + else if (strcmp(method, "RequestWriteLock") == 0) + dbus_scanner_request_write_lock(inv, scanner, sender); + else if (strcmp(method, "ReleaseWriteLock") == 0) + dbus_scanner_release_write_lock(inv, scanner, sender); +} + +static void +scanner_update_id_changed(scanner_t *scanner) +{ + if (scanner->changed_props.idler == 0) + scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed, + scanner); + + scanner->changed_props.update_id = TRUE; +} + +static void +scanner_categories_changed(scanner_t *scanner) +{ + if (scanner->changed_props.idler == 0) + scanner->changed_props.idler = g_idle_add(scanner_dbus_props_changed, + scanner); + + scanner->changed_props.categories = TRUE; +} + +static GVariant * +scanner_get_prop(GDBusConnection *conn, const char *sender, const char *opath, const char *iface, const char *prop, GError **error, gpointer data) +{ + scanner_t *scanner = data; + GVariant *ret; + + if (strcmp(prop, "IsScanning") == 0) + ret = g_variant_new_boolean(scanner->thread != NULL); + else if (strcmp(prop, "WriteLocked") == 0) + ret = g_variant_new_boolean(check_write_locked(scanner)); + else if (strcmp(prop, "UpdateID") == 0) { + guint64 update_id = 0; + + if (!check_write_locked(scanner)) + update_id = get_update_id(); + if (update_id > 0 && update_id != scanner->update_id) { + scanner->update_id = update_id; + scanner_update_id_changed(scanner); + } + ret = g_variant_new_uint64(scanner->update_id); + } else if (strcmp(prop, "Categories") == 0) + ret = categories_get_variant(); + else + ret = NULL; + + return ret; +} + +static void +scanner_destroyed(gpointer data) +{ + scanner_t *scanner = data; + + g_free(scanner->write_lock); + + if (scanner->write_lock_name_watcher) + g_bus_unwatch_name(scanner->write_lock_name_watcher); + + if (scanner->thread) { + g_warning("Shutdown while scanning, wait..."); + g_thread_join(scanner->thread); + } + + if (scanner->cleanup_thread_idler) { + g_source_remove(scanner->cleanup_thread_idler); + scanner_thread_cleanup(scanner); + } + + if (scanner->changed_props.idler) { + g_source_remove(scanner->changed_props.idler); + scanner_dbus_props_changed(scanner); + } + + g_assert(scanner->thread == NULL); + g_assert(scanner->pending_scan == NULL); + g_assert(scanner->cleanup_thread_idler == 0); + g_assert(scanner->pending_stop == NULL); + g_assert(scanner->changed_props.idler == 0); + + g_free(scanner); +} + +static const GDBusInterfaceVTable scanner_vtable = { + scanner_method_call, + scanner_get_prop, + NULL +}; + +static void +on_name_acquired(GDBusConnection *conn, const gchar *name, gpointer data) +{ + GDBusInterfaceInfo *iface; + unsigned id; + scanner_t *scanner; + + scanner = g_new0(scanner_t, 1); + g_assert(scanner != NULL); + scanner->conn = conn; + scanner->pending_scan = NULL; + scanner->update_id = get_update_id(); + + iface = g_dbus_node_info_lookup_interface(introspection_data, BUS_IFACE); + + id = g_dbus_connection_register_object(conn, + BUS_PATH, + iface, + &scanner_vtable, + scanner, + scanner_destroyed, + NULL); + g_assert(id > 0); + + if (startup_scan) { + g_debug("Do startup scan"); + g_hash_table_foreach(categories, scan_params_all, scanner); + do_scan(scanner); + } + + scanner_update_id_changed(scanner); + scanner_write_lock_changed(scanner); + scanner_is_scanning_changed(scanner); + scanner_categories_changed(scanner); + + g_debug("Acquired name org.lightmediascanner and registered object"); +} + +static gboolean +str_array_find(const GArray *arr, const char *str) +{ + char **itr; + for (itr = (char **)arr->data; *itr; itr++) + if (strcmp(*itr, str) == 0) + return TRUE; + + return FALSE; +} + +static scanner_category_t * +scanner_category_get_or_add(const char *category) +{ + scanner_category_t *sc = g_hash_table_lookup(categories, category); + if (sc) + return sc; + + sc = scanner_category_new(category); + g_hash_table_insert(categories, sc->category, sc); + return sc; +} + +static void +scanner_category_add_parser(scanner_category_t *sc, const char *parser) +{ + char *p; + + if (str_array_find(sc->parsers, parser)) + return; + + p = g_strdup(parser); + g_array_append_val(sc->parsers, p); +} + +static void +scanner_category_add_dir(scanner_category_t *sc, const char *dir) +{ + char *p; + + if (str_array_find(sc->dirs, dir)) + return; + + p = g_strdup(dir); + g_array_append_val(sc->dirs, p); +} + +static int +populate_categories(void *data, const char *path) +{ + struct lms_parser_info *info; + const char * const *itr; + long do_parsers = (long)data; + + info = lms_parser_info(path); + if (!info) + return 1; + + if (strcmp(info->name, "dummy") == 0) + goto end; + + if (!info->categories) + goto end; + + + for (itr = info->categories; *itr != NULL; itr++) { + scanner_category_t *sc; + + if (strcmp(*itr, "all") == 0) + continue; + + sc = scanner_category_get_or_add(*itr); + + if (do_parsers) + scanner_category_add_parser(sc, path); + } + +end: + lms_parser_info_free(info); + + return 1; +} + +static int +populate_category_all_parsers(void *data, const char *path) +{ + struct lms_parser_info *info; + scanner_category_t *sc = data; + + info = lms_parser_info(path); + if (!info) + return 1; + + if (strcmp(info->name, "dummy") != 0) + scanner_category_add_parser(sc, path); + + lms_parser_info_free(info); + return 1; +} + +static int +populate_category_parsers(void *data, const char *path, const struct lms_parser_info *info) +{ + scanner_category_t *sc = data; + + if (strcmp(info->name, "dummy") != 0) + scanner_category_add_parser(sc, path); + + return 1; +} + +static void +_populate_parser_internal(const char *category, const char *parser) +{ + scanner_category_t *sc = scanner_category_get_or_add(category); + + if (strcmp(parser, "all") == 0) + lms_parsers_list(populate_category_all_parsers, sc); + else if (strcmp(parser, "all-category") == 0) + lms_parsers_list_by_category(sc->category, populate_category_parsers, + sc); + else + scanner_category_add_parser(sc, parser); +} + +static void +populate_parser_foreach(gpointer key, gpointer value, gpointer user_data) +{ + const char *category = key; + const char *parser = user_data; + _populate_parser_internal(category, parser); +} + +static void +populate_parser(const char *category, const char *parser) +{ + if (!category) + g_hash_table_foreach(categories, populate_parser_foreach, + (gpointer)parser); + else + _populate_parser_internal(category, parser); +} + +static void +_populate_dir_internal(const char *category, const char *dir) +{ + scanner_category_t *sc = scanner_category_get_or_add(category); + + if (strcmp(dir, "defaults") != 0) + scanner_category_add_dir(sc, dir); + else { + struct { + const char *cat; + const char *path; + } *itr, defaults[] = { + {"audio", g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)}, + {"video", g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS)}, + {"picture", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)}, + {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)}, + {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS)}, + {"multimedia", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)}, + {NULL, NULL} + }; + for (itr = defaults; itr->cat != NULL; itr++) { + if (strcmp(itr->cat, category) == 0) + scanner_category_add_dir(sc, itr->path); + } + } +} + +static void +populate_dir_foreach(gpointer key, gpointer value, gpointer user_data) +{ + const char *category = key; + const char *dir = user_data; + _populate_dir_internal(category, dir); +} + +static void +populate_dir(const char *category, const char *dir) +{ + if (!category) + g_hash_table_foreach(categories, populate_dir_foreach, + (gpointer)dir); + else + _populate_dir_internal(category, dir); +} + +static void +debug_categories(gpointer key, gpointer value, gpointer user_data) +{ + const scanner_category_t *sc = value; + const char * const *itr; + + g_debug("category: %s", sc->category); + + if (sc->parsers->len) { + for (itr = (const char * const *)sc->parsers->data; *itr != NULL; itr++) + g_debug(" parser: %s", *itr); + } else + g_debug(" parser: <none>"); + + if (sc->dirs->len) { + for (itr = (const char * const *)sc->dirs->data; *itr != NULL; itr++) + g_debug(" dir...: %s", *itr); + } else + g_debug(" dir...: <none>"); +} + +static gboolean +on_sig_term(gpointer data) +{ + GMainLoop *loop = data; + + g_debug("got SIGTERM, exit."); + g_main_loop_quit(loop); + return FALSE; +} + +static gboolean +on_sig_int(gpointer data) +{ + GMainLoop *loop = data; + + g_debug("got SIGINT, exit."); + g_main_loop_quit(loop); + return FALSE; +} + +int +main(int argc, char *argv[]) +{ + int ret = EXIT_SUCCESS; + unsigned id; + GMainLoop *loop; + GError *error = NULL; + GOptionContext *opt_ctx; + char **parsers = NULL; + char **dirs = NULL; + GOptionEntry opt_entries[] = { + {"db-path", 'p', 0, G_OPTION_ARG_FILENAME, &db_path, + "Path to LightMediaScanner SQLit3 data base, " + "defaults to \"~/.config/lightmediascannerd/db.sqlite3\".", + "PATH"}, + {"commit-interval", 'c', 0, G_OPTION_ARG_INT, &commit_interval, + "Execute SQL COMMIT after NUMBER files are processed, " + "defaults to 100.", + "NUMBER"}, + {"slave-timeout", 't', 0, G_OPTION_ARG_INT, &slave_timeout, + "Number of seconds to wait for slave to reply, otherwise kills it. " + "Defaults to 60.", + "SECONDS"}, + {"vacuum", 'V', 0, G_OPTION_ARG_NONE, &vacuum, + "Execute SQL VACUUM after every scan.", NULL}, + {"startup-scan", 'S', 0, G_OPTION_ARG_NONE, &startup_scan, + "Execute full scan on startup.", NULL}, + {"charset", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &charsets, + "Extra charset to use. (Multiple use)", "CHARSET"}, + {"parser", 'P', 0, G_OPTION_ARG_STRING_ARRAY, &parsers, + "Parsers to use, defaults to all. Format is 'category:parsername' or " + "'parsername' to apply parser to all categories. The special " + "parsername 'all' declares all known parsers, while 'all-category' " + "declares all parsers of that category. If one parser is provided, " + "then no defaults are used, you can pre-populate all categories " + "with their parsers by using --parser=all-category.", + "CATEGORY:PARSER"}, + {"directory", 'D', 0, G_OPTION_ARG_STRING_ARRAY, &dirs, + "Directories to use, defaults to FreeDesktop.Org standard. " + "Format is 'category:directory' or 'path' to " + "apply directory to all categories. The special directory " + "'defaults' declares all directories used by default for that " + "category. If one directory is provided, then no defaults are used, " + "you can pre-populate all categories with their directories by " + "using --directory=defaults.", + "CATEGORY:DIRECTORY"}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + opt_ctx = g_option_context_new( + "\nLightMediaScanner D-Bus daemon.\n\n" + "Usually there is no need to declare options, defaults should " + "be good. However one may want the defaults and in addition to scan " + "everything in an USB with:\n\n" + "\tlightmediascannerd --directory=defaults --parser=all-category " + "--directory=usb:/media/usb --parser=usb:all"); + g_option_context_add_main_entries(opt_ctx, opt_entries, + "lightmediascannerd"); + if (!g_option_context_parse(opt_ctx, &argc, &argv, &error)) { + g_option_context_free(opt_ctx); + g_error("Option parsing failed: %s\n", error->message); + g_error_free(error); + return EXIT_FAILURE; + } + + g_option_context_free(opt_ctx); + + categories = g_hash_table_new_full( + g_str_hash, g_str_equal, NULL, + (GDestroyNotify)scanner_category_destroy); + + if (!db_path) + db_path = g_strdup_printf("%s/lightmediascannerd/db.sqlite3", + g_get_user_config_dir()); + if (!g_file_test(db_path, G_FILE_TEST_EXISTS)) { + char *dname = g_path_get_dirname(db_path); + if (!g_file_test(dname, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) { + if (g_mkdir_with_parents(dname, 0755) != 0) { + g_error("Couldn't create directory %s", dname); + g_free(dname); + ret = EXIT_FAILURE; + goto end_options; + } + } + g_free(dname); + } + + if (!parsers) + lms_parsers_list(populate_categories, (void *)1L); + else { + char **itr; + + lms_parsers_list(populate_categories, (void *)0L); + + for (itr = parsers; *itr != NULL; itr++) { + char *sep = strchr(*itr, ':'); + const char *path; + if (!sep) + path = *itr; + else { + path = sep + 1; + *sep = '\0'; + } + + if (path[0] != '\0') + populate_parser(sep ? *itr : NULL, path); + + if (sep) + *sep = ':'; + } + } + + if (!dirs) + populate_dir(NULL, "defaults"); + else { + char **itr; + + for (itr = dirs; *itr != NULL; itr++) { + char *sep = strchr(*itr, ':'); + const char *path; + if (!sep) + path = *itr; + else { + path = sep + 1; + *sep = '\0'; + } + + if (path[0] != '\0') + populate_dir(sep ? *itr : NULL, path); + + if (sep) + *sep = ':'; + } + } + + g_debug("db-path: %s", db_path); + g_debug("commit-interval: %d files", commit_interval); + g_debug("slave-timeout: %d seconds", slave_timeout); + + if (charsets) { + char *tmp = g_strjoinv(", ", charsets); + g_debug("charsets: %s", tmp); + g_free(tmp); + } else + g_debug("charsets: <none>"); + + g_hash_table_foreach(categories, debug_categories, NULL); + + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); + g_assert(introspection_xml != NULL); + + id = g_bus_own_name(G_BUS_TYPE_SESSION, "org.lightmediascanner", + G_BUS_NAME_OWNER_FLAGS_NONE, + NULL, on_name_acquired, NULL, NULL, NULL); + + g_debug("starting main loop"); + + loop = g_main_loop_new(NULL, FALSE); + g_unix_signal_add(SIGTERM, on_sig_term, loop); + g_unix_signal_add(SIGINT, on_sig_int, loop); + g_main_loop_run(loop); + + g_debug("main loop is finished"); + + g_bus_unown_name(id); + g_main_loop_unref(loop); + + g_dbus_node_info_unref(introspection_data); + +end_options: + g_free(db_path); + g_strfreev(charsets); + g_strfreev(parsers); + g_strfreev(dirs); + + if (categories) + g_hash_table_destroy(categories); + + return ret; +} |