diff --git a/meson.build b/meson.build index 605b5d4..cdda443 100644 --- a/meson.build +++ b/meson.build @@ -30,7 +30,7 @@ cdata.set_quoted('PACKAGE_VERSION', meson.project_version()) cdata.set_quoted('VERSION', meson.project_version()) cdata.set_quoted('PACKAGE_SYSCONF_DIR', get_option('sysconfdir')) -glib_min_version = '2.66' +glib_min_version = '2.76' glib_version_def = 'GLIB_VERSION_@0@_@1@'.format( glib_min_version.split('.')[0], glib_min_version.split('.')[1]) diff --git a/src/linux/integration-test.py b/src/linux/integration-test.py index 749f867..c9cc834 100755 --- a/src/linux/integration-test.py +++ b/src/linux/integration-test.py @@ -5904,6 +5904,144 @@ class Tests(dbusmock.DBusTestCase): # client.get_devices_async(None, get_devices_cb) # ml.run() + def test_conf_d_support(self): + """Ensure support for conf.d style directories""" + + base_dir = tempfile.TemporaryDirectory(delete=False, prefix="UPower-") + conf_d_dir_path = os.path.join(base_dir.name, "UPower.conf.d") + + # Low, Critical and Action are all needed to avoid fallback to defaults + config = tempfile.NamedTemporaryFile( + delete=False, mode="w", dir=base_dir.name, suffix=".conf" + ) + config.write("[UPower]\n") + config.write("PercentageLow=20.0\n") + config.write("PercentageCritical=3.0\n") + config.write("PercentageAction=2.0\n") + config.write("AllowRiskyCriticalPowerAction=false\n") + config.write("CriticalPowerAction=HybridSleep\n") + config.close() + + # UPower.conf.d directory does not exist + self.start_daemon(cfgfile=config.name) + self.daemon_log.check_line( + "failed to find files in 'UPower.conf.d': Error opening directory", + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + # empty UPower.conf.d directory + conf_d_dir = os.mkdir(path=conf_d_dir_path) + self.start_daemon(cfgfile=config.name) + self.stop_daemon() + + # override config (in a UPower.conf.d/10-*.conf file) + config2 = tempfile.NamedTemporaryFile( + delete=False, mode="w", dir=conf_d_dir_path, prefix="10-", suffix=".conf" + ) + config2.write("[UPower]\n") + config2.write("AllowRiskyCriticalPowerAction=true\n") + config2.write("CriticalPowerAction=Suspend\n") + config2.close() + + # check warning message when CriticalPowerAction=Suspend and + # AllowRiskyCriticalPowerAction=true, meaning that support of + # UPower.conf.d dir is working + self.start_daemon(cfgfile=config.name, warns=True) + self.daemon_log.check_line( + 'The "Suspend" CriticalPowerAction setting is considered risky:', + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + # override config (in a UPower.conf.d/90-*.conf file) + config3 = tempfile.NamedTemporaryFile( + delete=False, mode="w", dir=conf_d_dir_path, prefix="90-", suffix=".conf" + ) + config3.write("[UPower]\n") + config3.write("CriticalPowerAction=Ignore\n") + config3.close() + + # check warning message when CriticalPowerAction=Ignore and + # AllowRiskyCriticalPowerAction=true, meaning that support of + # UPower.conf.d dir is working + self.start_daemon(cfgfile=config.name, warns=True) + self.daemon_log.check_line( + 'The "Ignore" CriticalPowerAction setting is considered risky:', + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + # config with invalid name 99-*.conf~ file (not actually overriding) + config_inv1 = tempfile.NamedTemporaryFile( + delete=False, mode="w", dir=conf_d_dir_path, prefix="99-", suffix=".conf~" + ) + config_inv1.write("[UPower]\n") + config_inv1.write("CriticalPowerAction=HybridSleep\n") + config_inv1.close() + + # check warning message when CriticalPowerAction=Ignore and + # AllowRiskyCriticalPowerAction=true, because the config file + # (config_inv1) setting it to the safe 'HybridSleep' is not activated + # due to wrong name + self.start_daemon(cfgfile=config.name, warns=True) + self.daemon_log.check_line( + 'The "Ignore" CriticalPowerAction setting is considered risky:', + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + # config with invalid name 999-*.conf file (not actually overriding) + config_inv2 = tempfile.NamedTemporaryFile( + delete=False, mode="w", dir=conf_d_dir_path, prefix="999-", suffix=".conf" + ) + config_inv2.write("[UPower]\n") + config_inv2.write("CriticalPowerAction=HybridSleep\n") + config_inv2.close() + + # check warning message when CriticalPowerAction=Ignore and + # AllowRiskyCriticalPowerAction=true, because the config files + # (config_inv1 and config_inv2) setting it to the safe 'HybridSleep' are + # not activated due to wrong names + self.start_daemon(cfgfile=config.name, warns=True) + self.daemon_log.check_line( + 'The "Ignore" CriticalPowerAction setting is considered risky:', + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + # config with invalid name 99-badname+*.conf file (not actually overriding) + config_inv3 = tempfile.NamedTemporaryFile( + delete=False, + mode="w", + dir=conf_d_dir_path, + prefix="99-badname+", + suffix=".conf", + ) + config_inv3.write("[UPower]\n") + config_inv3.write("CriticalPowerAction=HybridSleep\n") + config_inv3.close() + + # check warning message when CriticalPowerAction=Ignore and + # AllowRiskyCriticalPowerAction=true, because the config files + # (config_inv1, 2 and 3) setting it to the safe 'HybridSleep' are not + # activated due to wrong names + self.start_daemon(cfgfile=config.name, warns=True) + self.daemon_log.check_line( + 'The "Ignore" CriticalPowerAction setting is considered risky:', + timeout=UP_DAEMON_ACTION_DELAY + 0.5, + ) + self.stop_daemon() + + os.unlink(config_inv3.name) + os.unlink(config_inv2.name) + os.unlink(config_inv1.name) + os.unlink(config3.name) + os.unlink(config2.name) + os.unlink(config.name) + os.rmdir(conf_d_dir_path) + os.rmdir(base_dir.name) + # # Helper methods # diff --git a/src/up-config.c b/src/up-config.c index c80105f..836c993 100644 --- a/src/up-config.c +++ b/src/up-config.c @@ -104,6 +104,142 @@ up_config_class_init (UpConfigClass *klass) object_class->finalize = up_config_finalize; } +/** + * up_config_list_compare_files: + **/ +static gint +up_config_list_compare_files (gconstpointer a, gconstpointer b) +{ + return g_strcmp0 ((const gchar*)a, (const gchar*)b); +} + +/** + * up_config_list_confd_files: + * + * The format of the filename should be '^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$', + * that is, starting with "00-" to "99-", ending in ".conf", and with a mix of + * alphanumeric characters with dashes and underscores in between. For example: + * '01-upower-override.conf'. + * + * Files named differently, or containing invalid groups (currently only + * 'UPower' is valid), will not be considered. + * + * The candidate files within the given directory are sorted (with g_strcmp0(), + * so the ordering will be as with strcmp()). The configuration in the files + * being processed later will override previous config, in particular the main + * config, but also the one from previous files processed, if the Group and Key + * coincide. + * + * For example, consider 'UPower.conf' that contains the defaults: + * PercentageLow=20.0 + * PercentageCritical=5.0 + * PercentageAction=2.0 + * + * and there is a file 'UPower.conf.d/70-change-percentages.conf' + * containing settings for all 'Percentage*' keys: + * [UPower] + * PercentageLow=15.0 + * PercentageCritical=10.0 + * PercentageAction=5.0 + * + * and another 'UPower.conf.d/99-change-percentages-local.conf' + * containing settings only for 'PercentageAction': + * [UPower] + * PercentageAction=7.5 + * + * First the main 'UPower.conf' will be processed, then + * 'UPower.conf.d/70-change-percentages.conf' overriding the defaults + * of all percentages from the main config file with the given values, + * and finally 'UPower.conf.d/99-change-percentages-local.conf' + * overriding once again only 'PercentageAction'. The final, effective + * values are: + * PercentageLow=15.0 + * PercentageCritical=10.0 + * PercentageAction=7.5 + **/ +static GPtrArray* +up_config_list_confd_files (const gchar* conf_d_path, GError** error) +{ + g_autoptr (GPtrArray) ret_conf_d_files = NULL; + GDir *dir = NULL; + const gchar *filename = NULL; + const char *regex_pattern = "^([0-9][0-9])-([a-zA-Z0-9-_])*\\.conf$"; + g_autoptr (GRegex) regex = NULL; + + dir = g_dir_open (conf_d_path, 0, error); + if (dir == NULL) + return NULL; + + regex = g_regex_new (regex_pattern, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, NULL); + g_assert (regex != NULL); + + ret_conf_d_files = g_ptr_array_new_full (0, g_free); + + while ((filename = g_dir_read_name (dir)) != NULL) { + g_autofree gchar *file_path = NULL; + g_autoptr (GFile) file = NULL; + g_autoptr (GFileInfo) file_info = NULL; + + if (!g_regex_match (regex, filename, G_REGEX_MATCH_DEFAULT, NULL)) + continue; + + file_path = g_build_filename (conf_d_path, filename, NULL); + file = g_file_new_for_path (file_path); + file_info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, + NULL); + if (file_info != NULL) { + g_debug ("Will consider additional config file '%s'", file_path); + g_ptr_array_add (ret_conf_d_files, g_strdup (file_path)); + } + } + + g_dir_close (dir); + + g_ptr_array_sort_values (ret_conf_d_files, up_config_list_compare_files); + + return g_ptr_array_ref (ret_conf_d_files); +} + +/** + * up_config_override_from_confd: + **/ +static void +up_config_override_from_confd (GKeyFile *key_file, const gchar* new_config_path) +{ + g_autoptr (GKeyFile) new_keyfile = NULL; + gchar **keys = NULL; + gsize keys_size = 0; + + new_keyfile = g_key_file_new(); + if (!g_key_file_load_from_file (new_keyfile, new_config_path, G_KEY_FILE_NONE, NULL)) + return; + + if (!g_key_file_has_group (new_keyfile, "UPower")) + return; + + keys = g_key_file_get_keys (new_keyfile, "UPower", &keys_size, NULL); + if (keys == NULL) + return; + + for (gsize i = 0; i < keys_size; i++) { + g_autofree gchar *value = NULL; + g_autofree gchar *old_value = NULL; + + value = g_key_file_get_value (new_keyfile, "UPower", keys[i], NULL); + if (value == NULL) + continue; + + old_value = g_key_file_get_value (key_file, "UPower", keys[i], NULL); + + if (old_value != NULL) + g_key_file_set_value (key_file, "UPower", keys[i], value); + } + g_strfreev (keys); +} + /** * up_config_init: **/ @@ -112,16 +248,24 @@ up_config_init (UpConfig *config) { gboolean allow_risky_critical_action = FALSE; g_autofree gchar *critical_action = NULL; - GError *error = NULL; + g_autoptr (GError) error = NULL; g_autofree gchar *filename = NULL; gboolean ret; + g_autofree gchar *conf_dir = NULL; + g_autofree gchar *conf_d_path = NULL; + g_autoptr (GPtrArray) conf_d_files = NULL; config->priv = up_config_get_instance_private (config); config->priv->keyfile = g_key_file_new (); filename = g_strdup (g_getenv ("UPOWER_CONF_FILE_NAME")); - if (filename == NULL) + if (filename == NULL) { filename = g_build_filename (PACKAGE_SYSCONF_DIR,"UPower", "UPower.conf", NULL); + conf_d_path = g_build_filename (PACKAGE_SYSCONF_DIR, "UPower", "UPower.conf.d", NULL); + } else { + conf_dir = g_path_get_dirname (filename); + conf_d_path = g_build_filename (conf_dir, "UPower.conf.d", NULL); + } /* load */ ret = g_key_file_load_from_file (config->priv->keyfile, @@ -132,7 +276,18 @@ up_config_init (UpConfig *config) if (!ret) { g_warning ("failed to load config file '%s': %s", filename, error->message); - g_error_free (error); + g_clear_error (&error); + } + + conf_d_files = up_config_list_confd_files (conf_d_path, &error); + if (conf_d_files != NULL) { + for (guint i = 0; i < conf_d_files->len; i++) { + const gchar* conf_d_file = (const gchar*)(g_ptr_array_index (conf_d_files, i)); + up_config_override_from_confd (config->priv->keyfile, + conf_d_file); + } + } else { + g_debug ("failed to find files in 'UPower.conf.d': %s", error->message); } /* Warn for any dangerous configurations */