diff --git a/modules/meson.build b/modules/meson.build index 2d4685d2..23484230 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -166,6 +166,17 @@ shared_library( dependencies : [wp_dep, pipewire_dep, mathlib], ) +shared_library( + 'wireplumber-module-file-monitor-api', + [ + 'module-file-monitor-api.c', + ], + c_args : [common_c_args, '-DG_LOG_DOMAIN="m-file-monitor-api"'], + install : true, + install_dir : wireplumber_module_dir, + dependencies : [wp_dep, pipewire_dep], +) + if wpipc_dep.found() shared_library( 'wireplumber-module-ipc', diff --git a/modules/module-file-monitor-api.c b/modules/module-file-monitor-api.c new file mode 100644 index 00000000..aa3283b1 --- /dev/null +++ b/modules/module-file-monitor-api.c @@ -0,0 +1,212 @@ +/* WirePlumber + * + * Copyright © 2021 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include + +struct _WpFileMonitorApi +{ + WpPlugin parent; + + GHashTable *monitors; +}; + +enum { + ACTION_ADD_WATCH, + ACTION_REMOVE_WATCH, + SIGNAL_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = {0}; + +G_DECLARE_FINAL_TYPE (WpFileMonitorApi, wp_file_monitor_api, WP, + FILE_MONITOR_API, WpPlugin) +G_DEFINE_TYPE (WpFileMonitorApi, wp_file_monitor_api, WP_TYPE_PLUGIN) + +static void +wp_file_monitor_api_init (WpFileMonitorApi * self) +{ + self->monitors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, + g_object_unref); +} + +static void +wp_file_monitor_api_finalize (GObject * object) +{ + WpFileMonitorApi * self = WP_FILE_MONITOR_API (object); + + g_clear_pointer (&self->monitors, g_hash_table_unref); + + G_OBJECT_CLASS (wp_file_monitor_api_parent_class)->finalize (object); +} + +static void +on_file_monitor_changed (GFileMonitor *monitor, GFile *file, GFile *other, + GFileMonitorEvent evtype, gpointer data) +{ + WpFileMonitorApi * self = WP_FILE_MONITOR_API (data); + + g_autofree char *fpath = g_file_get_path (file); + g_autofree char *opath = NULL; + const gchar *evtype_str = NULL; + + if (other) + opath = g_file_get_path (other); + + switch(evtype) { + case G_FILE_MONITOR_EVENT_CHANGED: + evtype_str = "changed"; + break; + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + evtype_str = "changes-done-hint"; + break; + case G_FILE_MONITOR_EVENT_DELETED: + evtype_str = "deleted"; + break; + case G_FILE_MONITOR_EVENT_CREATED: + evtype_str = "created"; + break; + case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: + evtype_str = "attribute-changed"; + break; + case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: + evtype_str = "pre-unmount"; + break; + case G_FILE_MONITOR_EVENT_UNMOUNTED: + evtype_str = "unmounted"; + break; + case G_FILE_MONITOR_EVENT_MOVED: + evtype_str = "moved"; + break; + case G_FILE_MONITOR_EVENT_RENAMED: + evtype_str = "renamed"; + break; + case G_FILE_MONITOR_EVENT_MOVED_IN: + evtype_str = "moved-in"; + break; + case G_FILE_MONITOR_EVENT_MOVED_OUT: + evtype_str = "moved-out"; + break; + default: + wp_warning_object (self, "Unknown event type %d", evtype); + break; + } + + g_signal_emit (self, signals[SIGNAL_CHANGED], 0, fpath, opath, evtype_str); +} + +static gboolean +wp_file_monitor_api_add_watch (WpFileMonitorApi * self, const gchar *path, + const gchar *flags_str) +{ + g_autoptr (GError) e = NULL; + g_autoptr (GFileMonitor) fm = NULL; + g_autoptr (GFile) f = NULL; + GFileMonitorFlags flags = G_FILE_MONITOR_NONE; + + /* don't do anything if the directory is already being watched */ + if (g_hash_table_contains (self->monitors, path)) + return TRUE; + + /* get directory */ + f = g_file_new_for_path (path); + if (!f) { + wp_warning_object (self, "Invalid directory '%s'", path); + return FALSE; + } + + /* parse flags */ + for (guint i = 0; flags_str && i < strlen (flags_str); i++) { + switch (flags_str[i]) { + case 'o': flags |= G_FILE_MONITOR_WATCH_MOUNTS; break; + case 's': flags |= G_FILE_MONITOR_SEND_MOVED; break; + case 'h': flags |= G_FILE_MONITOR_WATCH_HARD_LINKS; break; + case 'm': flags |= G_FILE_MONITOR_WATCH_MOVES; break; + default: + break; + } + } + + /* create the file monitor for that directory */ + fm = g_file_monitor_directory (f, flags, NULL, &e); + if (e) { + wp_warning_object (self, "Failed to add watch for directory '%s': %s", path, + e->message); + return FALSE; + } + + /* handle changed signal and add it to monitors table */ + g_signal_connect (fm, "changed", G_CALLBACK (on_file_monitor_changed), self); + g_hash_table_insert (self->monitors, g_strdup (path), g_steal_pointer (&fm)); + return TRUE; +} + +static void +wp_file_monitor_api_remove_watch (WpFileMonitorApi * self, const gchar *path) +{ + g_hash_table_remove (self->monitors, path); +} + +static void +wp_file_monitor_api_enable (WpPlugin * plugin, WpTransition * transition) +{ + WpFileMonitorApi * self = WP_FILE_MONITOR_API (plugin); + + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); +} + +static void +wp_file_monitor_api_disable (WpPlugin * plugin) +{ + WpFileMonitorApi * self = WP_FILE_MONITOR_API (plugin); + + g_hash_table_remove_all (self->monitors); +} + +static void +wp_file_monitor_api_class_init (WpFileMonitorApiClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpPluginClass *plugin_class = (WpPluginClass *) klass; + + object_class->finalize = wp_file_monitor_api_finalize; + + plugin_class->enable = wp_file_monitor_api_enable; + plugin_class->disable = wp_file_monitor_api_disable; + + signals[ACTION_ADD_WATCH] = g_signal_new_class_handler ( + "add-watch", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_file_monitor_api_add_watch, + NULL, NULL, NULL, + G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_STRING); + + signals[ACTION_REMOVE_WATCH] = g_signal_new_class_handler ( + "remove-watch", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_file_monitor_api_remove_watch, + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); + + signals[SIGNAL_CHANGED] = g_signal_new ( + "changed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); +} + +WP_PLUGIN_EXPORT gboolean +wireplumber__module_init (WpCore * core, GVariant * args, GError ** error) +{ + wp_plugin_register (g_object_new (wp_file_monitor_api_get_type (), + "name", "file-monitor-api", + "core", core, + NULL)); + return TRUE; +} diff --git a/src/config/main.lua.d/30-alsa-monitor.lua b/src/config/main.lua.d/30-alsa-monitor.lua index 80220fdd..5d2f7e49 100644 --- a/src/config/main.lua.d/30-alsa-monitor.lua +++ b/src/config/main.lua.d/30-alsa-monitor.lua @@ -8,6 +8,11 @@ function alsa_monitor.enable() load_module("reserve-device") end + -- The "file-monitor-api" module needs to be loaded for MIDI device monitoring + if alsa_monitor.properties["alsa.midi.monitoring"] then + load_module("file-monitor-api") + end + load_monitor("alsa", { properties = alsa_monitor.properties, rules = alsa_monitor.rules, diff --git a/src/config/main.lua.d/50-alsa-config.lua b/src/config/main.lua.d/50-alsa-config.lua index c42d3be5..85bb9186 100644 --- a/src/config/main.lua.d/50-alsa-config.lua +++ b/src/config/main.lua.d/50-alsa-config.lua @@ -10,6 +10,9 @@ alsa_monitor.properties = { ["alsa.reserve"] = true, --["alsa.reserve.priority"] = -20, --["alsa.reserve.application-name"] = "WirePlumber", + + -- Enables monitoring of alsa MIDI devices + ["alsa.midi.monitoring"] = true, } alsa_monitor.rules = { diff --git a/tests/meson.build b/tests/meson.build index 0e32f1ff..2a696bfb 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -35,6 +35,7 @@ common_test_env = environment({ 'PIPEWIRE_RUNTIME_DIR': '/tmp', 'XDG_CONFIG_HOME': meson.current_build_dir() / '.config', 'XDG_STATE_HOME': meson.current_build_dir() / '.local' / '.state', + 'FILE_MONITOR_DIR': meson.current_build_dir() / '.local' / 'file_monitor', 'WIREPLUMBER_CONFIG_DIR': '/invalid', 'WIREPLUMBER_DATA_DIR': '/invalid', 'WIREPLUMBER_MODULE_DIR': meson.current_build_dir() / '..' / 'modules', diff --git a/tests/modules/file-monitor.c b/tests/modules/file-monitor.c new file mode 100644 index 00000000..ca0b5d12 --- /dev/null +++ b/tests/modules/file-monitor.c @@ -0,0 +1,119 @@ +/* WirePlumber + * + * Copyright © 2021 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "../common/base-test-fixture.h" + +typedef struct { + WpBaseTestFixture base; + WpPlugin *plugin; + gchar *path; + gchar *file; + gchar *evtype; +} TestFixture; + +static void +test_file_monitor_setup (TestFixture * f, gconstpointer user_data) +{ + wp_base_test_fixture_setup (&f->base, WP_BASE_TEST_FLAG_DONT_CONNECT); + g_autoptr (GError) error = NULL; + + wp_core_load_component (f->base.core, + "libwireplumber-module-file-monitor-api", "module", NULL, &error); + g_assert_no_error (error); + + f->plugin = wp_plugin_find (f->base.core, "file-monitor-api"); + g_assert_nonnull (f->plugin); + + f->path = g_strdup (g_getenv ("FILE_MONITOR_DIR")); + g_mkdir_with_parents (f->path, 0700); +} + +static void +test_file_monitor_teardown (TestFixture * f, gconstpointer user_data) +{ + g_clear_pointer (&f->evtype, g_free); + g_clear_pointer (&f->file, g_free); + g_clear_pointer (&f->path, g_free); + g_clear_object (&f->plugin); + wp_base_test_fixture_teardown (&f->base); +} + +static void +on_plugin_activated (WpObject * plugin, GAsyncResult * res, TestFixture * f) +{ + g_autoptr (GError) error = NULL; + if (!wp_object_activate_finish (plugin, res, &error)) { + wp_critical_object (plugin, "%s", error->message); + g_main_loop_quit (f->base.loop); + } +} + +static void +on_changed (WpPlugin *plugin, const gchar *file, const gchar *old, + const char *evtype, TestFixture * f) +{ + g_assert_nonnull (file); + g_assert_nonnull (evtype); + f->file = g_strdup (file); + f->evtype = g_strdup (evtype); + g_main_loop_quit (f->base.loop); +} + +static void +test_file_monitor_basic (TestFixture * f, gconstpointer user_data) +{ + gboolean res = FALSE; + + /* activate plugin */ + g_assert_nonnull (f->plugin); + wp_object_activate (WP_OBJECT (f->plugin), WP_PLUGIN_FEATURE_ENABLED, + NULL, (GAsyncReadyCallback) on_plugin_activated, f); + + /* delete the 'foo' file if it exists in path */ + g_autofree gchar *filename = g_build_filename (f->path, "foo", NULL); + remove (filename); + + /* handle changed signal */ + f->file = NULL; + f->evtype = NULL; + g_signal_connect (f->plugin, "changed", G_CALLBACK (on_changed), f); + + /* add watch */ + g_signal_emit_by_name (f->plugin, "add-watch", f->path, "m", &res); + g_assert_true (res); + + /* create the foo file in path */ + int fd = open (filename, O_CREAT | O_EXCL, 0700); + g_assert_cmpint (fd, >=, 0); + + /* run */ + g_main_loop_run (f->base.loop); + g_assert_cmpstr (f->file, ==, filename); + g_assert_cmpstr (f->evtype, ==, "created"); + + /* removed watch */ + g_signal_emit_by_name (f->plugin, "remove-watch", f->path); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + wp_init (WP_INIT_ALL); + + g_test_add ("/modules/file-monitor/basic", + TestFixture, NULL, + test_file_monitor_setup, + test_file_monitor_basic, + test_file_monitor_teardown); + + return g_test_run (); +} diff --git a/tests/modules/meson.build b/tests/modules/meson.build index 4b4d1bd2..c925174d 100644 --- a/tests/modules/meson.build +++ b/tests/modules/meson.build @@ -24,6 +24,13 @@ test( env: common_env, ) +test( + 'test-file-monitor', + executable('test-file-monitor', 'file-monitor.c', + dependencies: common_deps, c_args: common_args), + env: common_env, +) + test( 'test-si-node', executable('test-si-node', 'si-node.c',