From d38c3fb4ccf18c24cb7454109961b98325a8ff26 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 7 Jul 2021 16:09:34 +1000 Subject: [PATCH] lib: add functions to search in configuration/data directories The previous approach to loading config files was to ask WP for the directory and then search those for the config files. This patch changes the approach - a caller now asks WP to search for a specific config file or iterate over a config file directory. This allows us to implement a directory lookup order, i.e. "wireplumber.conf" may be in XDG_CONFIG_DIR, /etc/, /usr/share and the first one found is used. For configuration directories, the new method iterates over all matching entries (files + directories) and invokes a callback for each entry. This enables distributions to ship default files in /usr/share/wireplumber but have admins and users override them on a local basis. For lua scripts in particular, overriding a distribution-provided file with an empty file effectively disables it, adding a file adds it in the right sort order. --- docs/rst/daemon-configuration.rst | 26 ++-- lib/wp/wp.c | 202 ++++++++++++++++++++++++++ lib/wp/wp.h | 14 ++ modules/module-lua-scripting.c | 34 +---- modules/module-lua-scripting/config.c | 54 +++---- src/main.c | 26 ++-- 6 files changed, 261 insertions(+), 95 deletions(-) diff --git a/docs/rst/daemon-configuration.rst b/docs/rst/daemon-configuration.rst index abf0096f..fdb76af1 100644 --- a/docs/rst/daemon-configuration.rst +++ b/docs/rst/daemon-configuration.rst @@ -120,18 +120,24 @@ and ignored. Possible commands are Location of configuration files ------------------------------- -WirePlumber's default location of its configuration files is determined at -compile time by the build system. Typically, it ends up being `/etc/wireplumber`. +WirePlumber's default locations of its configuration files are determined at +compile time by the build system. Typically, those end up being +`XDG_CONFIG_DIR/wireplumber`, `/etc/wireplumber`, and +`/usr/share/wireplumber`, in that order of priority. -In more detail, this is controlled by the `--sysconfdir` meson option. When -this is set to an absolute path, such as `/etc`, the location of the -configuration files is set to be `$sysconfdir/wireplumber`. When this is set -to a relative path, such as `etc`, then the installation prefix (`--prefix`) +In more detail, the latter two are controlled by the `--sysconfdir` and `--datadir` +meson options. When those are set to an absolute path, such as `/etc`, the +location of the configuration files is set to be `$sysconfdir/wireplumber`. +When set to a relative path, such as `etc`, then the installation prefix (`--prefix`) is prepended to the path: `$prefix/$sysconfdir/wireplumber` -WirePlumber expects its `wireplumber.conf` to reside in that directory. -It is possible to override that at runtime by setting the -`WIREPLUMBER_CONFIG_FILE` environment variable:: +The three locations are intended for custom user configuration, +host-specific configuration and distribution-provided configuration, +respectively. At runtime, WirePlumber will search the directories +for the highest-priority directory to contain the `wireplumber.conf` +configuration file. This allows a user or system administrator to easily +override the distribution provided configuration files by placing an equally +named file in the respective directory. It is possible to override the lookup path at runtime by passing the `--config-file` or `-c` option:: @@ -144,6 +150,8 @@ by setting the `WIREPLUMBER_CONFIG_DIR` environment variable:: WIREPLUMBER_CONFIG_DIR=src/config wireplumber +If `WIREPLUMBER_CONFIG_DIR` is set, the default locations are ignored. + Location of modules ------------------- diff --git a/lib/wp/wp.c b/lib/wp/wp.c index 28c77358..a913a616 100644 --- a/lib/wp/wp.c +++ b/lib/wp/wp.c @@ -153,4 +153,206 @@ wp_get_data_dir (void) return data_dir; } +static gchar * +check_path (const gchar *basedir, const gchar *subdir, const gchar *filename) +{ + g_autofree gchar *path = g_build_filename (basedir, + subdir ? subdir : filename, + subdir ? filename : NULL, + NULL); + g_autofree gchar *abspath = g_canonicalize_filename (path, NULL); + wp_trace ("checking %s", abspath); + if (g_file_test (abspath, G_FILE_TEST_IS_REGULAR)) + return g_steal_pointer (&abspath); + return NULL; +} + +/*! + * \brief Flags to specify lookup directories + * Use one of the `WP_CDIR_SET` values instead of the values directly. + */ +typedef enum { + /* config group */ + WP_CDIR_ENV_CONFIG = (1 << 0), /**< $WIREPLUMBER_CONFIG_DIR */ + WP_CDIR_XDG_HOME = (1 << 1), /**< XDG_CONFIG_HOME */ + WP_CDIR_ETC = (1 << 2), /**< /etc/wireplumber */ + + /* data group */ + WP_CDIR_ENV_DATA = (1 << 10), /**< $WIREPLUMBER_DATA_DIR */ + WP_CDIR_USR = (1 << 11), /**< /usr/share/wireplumber */ + + /** The set for system-only files */ + WP_CDIR_SET_SYSTEM = WP_CDIR_ETC | WP_CDIR_USR | WP_CDIR_ENV_DATA, + + /** The set for user + system files */ + WP_CDIR_SET_USER = WP_CDIR_ENV_CONFIG | WP_CDIR_XDG_HOME | WP_CDIR_SET_SYSTEM, +} WpConfigDir; + +static GPtrArray * +lookup_dirs (guint flags) +{ + g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func (g_free); + const gchar *dir; + + /* Compile the list of lookup directories in priority order: + * - environment variables + * - XDG directories + * - etc + * - /usr/share/.... + * + * Note that environment variables *replace* the equivalent config directory + * group. + */ + if ((flags & WP_CDIR_ENV_CONFIG) && + (dir = g_getenv ("WIREPLUMBER_CONFIG_DIR"))) { + g_ptr_array_add (dirs, g_canonicalize_filename (dir, NULL)); + } else { + if ((flags & WP_CDIR_XDG_HOME) && (dir = wp_get_xdg_config_dir ())) + g_ptr_array_add (dirs, (gpointer)g_strdup (dir)); + if (flags & WP_CDIR_ETC) + g_ptr_array_add (dirs, (gpointer)g_strdup (WIREPLUMBER_DEFAULT_CONFIG_DIR)); + } + + /* data dirs */ + if ((flags & WP_CDIR_ENV_DATA) && (dir = g_getenv ("WIREPLUMBER_DATA_DIR"))) { + g_ptr_array_add (dirs, g_canonicalize_filename (dir, NULL)); + } else if (flags & WP_CDIR_USR) { + g_ptr_array_add (dirs, + (gpointer)g_canonicalize_filename(WIREPLUMBER_DEFAULT_DATA_DIR, NULL)); + } + + return g_steal_pointer (&dirs); +} + +/*! + * \brief Returns the full path of \a filename as found in + * the hierarchy of configuration and data directories. + * \returns An allocated string with the configuration file path or NULL if + * the file was not found. + */ +gchar * +wp_find_config_file (const gchar *filename, const char *subdir) +{ + g_autoptr(GPtrArray) dirs = lookup_dirs (WP_CDIR_SET_USER); + + if (g_path_is_absolute (filename)) + return g_strdup (filename); + + for (guint i = 0; i < dirs->len; i++) { + gchar *path = check_path (g_ptr_array_index (dirs, i), + subdir, filename); + if (path) + return path; + } + + return NULL; +} + +/*! + * \brief Returns the full path of \a filename as found in + * the hierarchy of system-only configuration and data directories. + * \returns An allocated string with the configuration file path or NULL if + * the file was not found. + */ +gchar * +wp_find_sysconfig_file (const gchar *filename, const char *subdir) +{ + g_autoptr(GPtrArray) dirs = lookup_dirs (WP_CDIR_SET_SYSTEM); + + if (g_path_is_absolute (filename)) + return g_strdup (filename); + + for (guint i = 0; i < dirs->len; i++) { + gchar *path = check_path (g_ptr_array_index (dirs, i), + subdir, filename); + if (path) + return path; + } + + return NULL; +} + +/** + * \brief Iterates over configuration files in the \a subdir and calls the + * \a func for each file. + * + * Files are sorted across the hierarchy of configuration and data + * directories with files in higher-priority directories shadowing files in + * lower-priority directories. Files are only checked for existence, a + * caller must be able to handle read errors. + * + * If the \a func returns a negative errno the iteration stops and that + * errno is returned to the caller. The \a func should set \a error on + * failure. + * + * Note that \a func is called for directories too, it is the responsibility + * of the caller to ignore or recurse into those. + * + * \param subdir The name of the subdirectory to search for in the + * configuration directories + * \param suffix The filename suffix, NULL matches all entries + * \param func The callback to invoke for each file. + * \param user_data Passed through to \a func + * \param error Passed through to \a func + * + * \return the number of files on success or a negative errno on failure + */ +int +wp_iter_config_files (const gchar *subdir, const gchar *suffix, + wp_file_iter_func func, gpointer user_data, + GError **error) +{ + g_autoptr(GHashTable) ht = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + g_autoptr(GPtrArray) dirs = NULL; + gint count = 0; + + if (subdir == NULL) + subdir = "."; + + /* Note: this list is highest-priority first */ + dirs = lookup_dirs (WP_CDIR_SET_USER); + + /* Store all filenames with their full path in the hashtable, overriding + * previous values. We need to run backwards through the list for that */ + for (guint i = dirs->len; i > 0; i--) { + g_autofree gchar *dirpath = g_build_filename (g_ptr_array_index (dirs, i - 1), + subdir, NULL); + g_autoptr(GDir) dir = g_dir_open (dirpath, 0, NULL); + + wp_trace ("searching config dir: %s", dirpath); + + if (dir) { + const gchar *filename; + while ((filename = g_dir_read_name (dir))) { + if (suffix && !g_str_has_suffix (filename, suffix)) + continue; + + g_hash_table_replace (ht, g_strdup (filename), + g_build_filename (dirpath, filename, NULL)); + } + } + } + + if (g_hash_table_size (ht) == 0) + return 0; + + /* Sort by filename */ + g_autoptr(GList) keys = g_hash_table_get_keys (ht); + keys = g_list_sort (keys, (GCompareFunc)g_strcmp0); + + /* Now we have our filenames in a sorted order so we can call the callback */ + for (GList *elem = g_list_first (keys); elem; elem = g_list_next (elem)) { + const gchar *path = g_hash_table_lookup (ht, elem->data); + gint rc = func (path, user_data, error); + + if (rc < 0) + return rc; + + count++; + } + + return count; +} + /*! \} */ diff --git a/lib/wp/wp.h b/lib/wp/wp.h index fc6abce9..d3d78c39 100644 --- a/lib/wp/wp.h +++ b/lib/wp/wp.h @@ -77,6 +77,20 @@ const gchar * wp_get_config_dir (void); WP_API const gchar * wp_get_data_dir (void); +WP_API +gchar * wp_find_config_file (const gchar *filename, const char *subdir); + +WP_API +gchar * wp_find_sysconfig_file (const gchar *filename, const char *subdir); + +typedef gint (*wp_file_iter_func)(const gchar *filename, gpointer user_data, + GError **error); + +WP_API +gint wp_iter_config_files (const gchar *subdir, const gchar *suffix, + wp_file_iter_func func, gpointer user_data, + GError **error); + G_END_DECLS #endif diff --git a/modules/module-lua-scripting.c b/modules/module-lua-scripting.c index 4e55c5d2..c2729baf 100644 --- a/modules/module-lua-scripting.c +++ b/modules/module-lua-scripting.c @@ -131,39 +131,7 @@ find_script (const gchar * script, gboolean daemon) g_file_test (script, G_FILE_TEST_IS_REGULAR)) return g_strdup (script); - /* /etc/wireplumber/scripts */ - { - g_autofree gchar * file = g_build_filename ( - wp_get_config_dir (), "scripts", script, NULL); - - wp_trace ("trying %s", file); - - if (g_file_test (file, G_FILE_TEST_IS_REGULAR)) - return g_steal_pointer (&file); - } - - { - g_autofree gchar * file = g_build_filename ( - wp_get_data_dir (), "scripts", script, NULL); - - wp_trace ("trying %s", file); - - if (g_file_test (file, G_FILE_TEST_IS_REGULAR)) - return g_steal_pointer (&file); - } - - /* {XDG_DATA_DIRS,/usr/local/share,/usr/share}/wireplumber/scripts */ - const gchar * const * data_dirs = g_get_system_data_dirs (); - for (; *data_dirs; data_dirs++) { - g_autofree gchar * file = g_build_filename ( - *data_dirs, "wireplumber", "scripts", script, NULL); - - wp_trace ("trying %s", file); - - if (g_file_test (file, G_FILE_TEST_IS_REGULAR)) - return g_steal_pointer (&file); - } - return NULL; + return wp_find_sysconfig_file (script, "scripts"); } static gboolean diff --git a/modules/module-lua-scripting/config.c b/modules/module-lua-scripting/config.c index 0aef7108..0449384e 100644 --- a/modules/module-lua-scripting/config.c +++ b/modules/module-lua-scripting/config.c @@ -8,6 +8,7 @@ #include #include +#include static gboolean load_components (lua_State *L, WpCore * core, GError ** error) @@ -78,9 +79,17 @@ done: } static gint -sort_filelist (gconstpointer a, gconstpointer b) +load_file (const gchar *path, gpointer data, GError **error) { - return g_strcmp0 (*(const gchar **) a, *(const gchar **) b); + lua_State *L = data; + + if (g_file_test (path, G_FILE_TEST_IS_DIR)) + return 0; + + wp_info ("loading config file: %s", path); + if (!wplua_load_path (L, path, 0, 0, error)) + return -EINVAL; + return 0; } gboolean @@ -89,51 +98,24 @@ wp_lua_scripting_load_configuration (const gchar * conf_file, { g_autofree gchar * path = NULL; g_autoptr (lua_State) L = wplua_new (); - gboolean found = FALSE; + gint nfiles = 0; wplua_enable_sandbox (L, WP_LUA_SANDBOX_MINIMAL_STD); /* load conf_file itself */ - path = g_build_filename (wp_get_config_dir (), conf_file, NULL); - if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) { + path = wp_find_config_file (conf_file, NULL); + if (path) { wp_info ("loading config file: %s", path); if (!wplua_load_path (L, path, 0, 0, error)) return FALSE; - found = TRUE; + nfiles = 1; } g_clear_pointer (&path, g_free); - /* aggregate split files from the ${conf_file}.d subdirectory */ - path = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s.d", - wp_get_config_dir (), conf_file); - if (g_file_test (path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) { - g_autoptr (GDir) conf_dir = g_dir_open (path, 0, error); - if (!conf_dir) - return FALSE; + path = g_strdup_printf ("%s.d", conf_file); + nfiles += wp_iter_config_files (path, ".lua", load_file, L, error); - /* sort files before loading them */ - g_autoptr (GPtrArray) filenames = g_ptr_array_new (); - const gchar *filename = NULL; - while ((filename = g_dir_read_name (conf_dir))) { - /* Only parse files that have the proper extension */ - if (g_str_has_suffix (filename, ".lua")) { - g_ptr_array_add (filenames, (gpointer) filename); - } - } - g_ptr_array_sort (filenames, sort_filelist); - - /* load */ - for (guint i = 0; i < filenames->len; i++) { - g_autofree gchar * file = g_build_filename (path, - g_ptr_array_index (filenames, i), NULL); - wp_info ("loading config file: %s", file); - if (!wplua_load_path (L, file, 0, 0, error)) - return FALSE; - found = TRUE; - } - } - - if (!found) { + if (nfiles == 0) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Could not locate configuration file '%s'", conf_file); return FALSE; diff --git a/src/main.c b/src/main.c index 9e866c2d..209d218d 100644 --- a/src/main.c +++ b/src/main.c @@ -327,22 +327,6 @@ init_done (WpCore * core, GAsyncResult * res, WpDaemon * d) } } -static gchar * -find_config (const gchar *config_file_name) -{ - g_autofree gchar *path = NULL; - - if (!config_file_name) - config_file_name = "wireplumber.conf"; - - if (g_path_is_absolute (config_file_name)) - path = g_strdup (config_file_name); - else - path = g_build_filename (wp_get_config_dir (), config_file_name, NULL); - - return g_canonicalize_filename (path, NULL); -} - gint main (gint argc, gchar **argv) { @@ -361,7 +345,15 @@ main (gint argc, gchar **argv) return WP_EXIT_USAGE; } - config_file_path = find_config (config_file); + if (!config_file) + config_file = "wireplumber.conf"; + + config_file_path = wp_find_config_file (config_file, NULL); + if (config_file_path == NULL) { + fprintf (stderr, "Unable to find the required configuration file %s\n", + config_file); + return WP_EXIT_CONFIG; + } properties = wp_properties_new ( PW_KEY_CONFIG_NAME, config_file_path,