From 0815e7eb96035e68a3ec57b52e6b6cb4a608b7c7 Mon Sep 17 00:00:00 2001 From: shadeyg56 <31134255+shadeyg56@users.noreply.github.com> Date: Tue, 30 Apr 2024 01:35:53 -0500 Subject: [PATCH] Rework config and reload config on file change/creation/deletion (#663) * add config.py and config_event_handler.py also introduces the utils folder * update config imports and variables * add 'pyinotify' dependency * config: check for changes using threading * config: handle errors and new eventsx * config: set_path even if file doesn't exist and make new ConfigParser on every update * fix get_config call * config: check for changes on moved file * call notifier.start() manually to prevent hanging * config: update comments * battery: fix config imports * config: fix config deletion detection * Add load from user config in XDG_CONFIG_HOME if available (#672) * Add load from user config from in XDG_CONFIG_HOME if available This update introduces the flexibility to load the configuration file from multiple locations, prioritizing user preferences and system standards. Previously, the configuration was strictly read from a hardcoded system path (`/etc/auto-cpufreq.conf`). Now, the application first checks if the user has specified a configuration file path via command line arguments. If not, it looks for a configuration file in the user's config directory (`$XDG_CONFIG_HOME/auto-cpufreq/auto-cpufreq.conf`). If neither is found, it defaults to the original system-wide configuration file. This allows users to add their auto-cpufreq configuration to their dotfiles. * If --config is set but invalid, exit with error * Remove redundant empty string check on config file path * Remove duplicate isfile check for config path See also: https://github.com/AdnanHodzic/auto-cpufreq/pull/672#discussion_r1548003119 * Update configuration options in README See also: #672 * config: move find_config_file function and fix finding home directory * auto_cpufreq: fix hanging on --daemon, --live, and --monitor * swap pyinotify for patched version --------- Co-authored-by: Steven Braun --- README.md | 6 +- auto_cpufreq/battery_scripts/battery.py | 1 + auto_cpufreq/battery_scripts/ideapad_acpi.py | 14 ++-- .../battery_scripts/ideapad_laptop.py | 20 ++--- auto_cpufreq/battery_scripts/thinkpad.py | 14 ++-- auto_cpufreq/bin/auto_cpufreq.py | 58 +++++++------- auto_cpufreq/core.py | 17 +--- auto_cpufreq/utils/__init__.py | 0 auto_cpufreq/utils/config.py | 80 +++++++++++++++++++ auto_cpufreq/utils/config_event_handler.py | 29 +++++++ nix/default.nix | 21 +++-- poetry.lock | 17 +++- pyproject.toml | 1 + 13 files changed, 206 insertions(+), 72 deletions(-) create mode 100644 auto_cpufreq/utils/__init__.py create mode 100644 auto_cpufreq/utils/config.py create mode 100644 auto_cpufreq/utils/config_event_handler.py diff --git a/README.md b/README.md index 5d690c1e..abc1fa04 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,11 @@ See [`--force` flag](#overriding-governor) for more info. You can configure separate profiles for the battery and power supply. These profiles will let you pick which governor to use, as well as how and when turbo boost is enabled. The possible values for turbo boost behavior are `always`, `auto`, and `never`. The default behavior is `auto`, which only activates turbo during high load. -By default, auto-cpufreq does not use the config file! If you wish to use it, the location where it needs to be placed to be read automatically is: `/etc/auto-cpufreq.conf` +By default, auto-cpufreq does not use a config file. If you wish to configure auto-cpufreq statically, we look for a configuration file in the following order: + +1. Commandline argument: `--config ` if passed as commandline argument to `auto-cpufreq` +2. User-specific configuration: `$XDG_CONFIG_HOME/auto-cpufreq/auto-cpufreq.conf` +3. System-wide configuration: `/etc/auto-cpufreq.conf` #### Example config file contents ```python diff --git a/auto_cpufreq/battery_scripts/battery.py b/auto_cpufreq/battery_scripts/battery.py index 904aa630..8b15b7d3 100644 --- a/auto_cpufreq/battery_scripts/battery.py +++ b/auto_cpufreq/battery_scripts/battery.py @@ -31,6 +31,7 @@ def battery_setup(): def battery_get_thresholds(): + if lsmod("thinkpad_acpi"): thinkpad_print_thresholds() diff --git a/auto_cpufreq/battery_scripts/ideapad_acpi.py b/auto_cpufreq/battery_scripts/ideapad_acpi.py index ca5f9288..ee69d1ac 100644 --- a/auto_cpufreq/battery_scripts/ideapad_acpi.py +++ b/auto_cpufreq/battery_scripts/ideapad_acpi.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os import subprocess -from auto_cpufreq.core import get_config +from auto_cpufreq.utils.config import config def set_battery(value, mode, bat): @@ -14,9 +14,9 @@ def set_battery(value, mode, bat): def get_threshold_value(mode): - config = get_config() - if config.has_option("battery", f"{mode}_threshold"): - return config["battery"][f"{mode}_threshold"] + conf = config.get_config() + if conf.has_option("battery", f"{mode}_threshold"): + return conf["battery"][f"{mode}_threshold"] else: if mode == "start": @@ -26,11 +26,11 @@ def get_threshold_value(mode): def ideapad_acpi_setup(): - config = get_config() + conf = config.get_config() - if not config.has_option("battery", "enable_thresholds"): + if not conf.has_option("battery", "enable_thresholds"): return - if not config["battery"]["enable_thresholds"] == "true": + if not conf["battery"]["enable_thresholds"] == "true": return battery_count = len([name for name in os.listdir( diff --git a/auto_cpufreq/battery_scripts/ideapad_laptop.py b/auto_cpufreq/battery_scripts/ideapad_laptop.py index f424a832..77304be4 100644 --- a/auto_cpufreq/battery_scripts/ideapad_laptop.py +++ b/auto_cpufreq/battery_scripts/ideapad_laptop.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os import subprocess -from auto_cpufreq.core import get_config +from auto_cpufreq.utils.config import config def set_battery(value, mode, bat): @@ -14,9 +14,9 @@ def set_battery(value, mode, bat): def get_threshold_value(mode): - config = get_config() - if config.has_option("battery", f"{mode}_threshold"): - return config["battery"][f"{mode}_threshold"] + conf = config.get_config() + if conf.has_option("battery", f"{mode}_threshold"): + return conf["battery"][f"{mode}_threshold"] else: if mode == "start": return 0 @@ -52,21 +52,21 @@ def check_conservation_mode(): def ideapad_laptop_setup(): - config = get_config() + conf = config.get_config() - if not config.has_option("battery", "enable_thresholds"): + if not conf.has_option("battery", "enable_thresholds"): return - if not config["battery"]["enable_thresholds"] == "true": + if not conf["battery"]["enable_thresholds"] == "true": return battery_count = len([name for name in os.listdir( "/sys/class/power_supply/") if name.startswith('BAT')]) - if config.has_option("battery", "ideapad_laptop_conservation_mode"): - if config["battery"]["ideapad_laptop_conservation_mode"] == "true": + if conf.has_option("battery", "ideapad_laptop_conservation_mode"): + if conf["battery"]["ideapad_laptop_conservation_mode"] == "true": conservation_mode(1) return - if config["battery"]["ideapad_laptop_conservation_mode"] == "false": + if conf["battery"]["ideapad_laptop_conservation_mode"] == "false": conservation_mode(0) if check_conservation_mode() is False: diff --git a/auto_cpufreq/battery_scripts/thinkpad.py b/auto_cpufreq/battery_scripts/thinkpad.py index 214ed402..cab5133c 100644 --- a/auto_cpufreq/battery_scripts/thinkpad.py +++ b/auto_cpufreq/battery_scripts/thinkpad.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os import subprocess -from auto_cpufreq.core import get_config +from auto_cpufreq.utils.config import config def set_battery(value, mode, bat): @@ -14,9 +14,9 @@ def set_battery(value, mode, bat): def get_threshold_value(mode): - config = get_config() - if config.has_option("battery", f"{mode}_threshold"): - return config["battery"][f"{mode}_threshold"] + conf = config.get_config() + if conf.has_option("battery", f"{mode}_threshold"): + return conf["battery"][f"{mode}_threshold"] else: if mode == "start": @@ -26,11 +26,11 @@ def get_threshold_value(mode): def thinkpad_setup(): - config = get_config() + conf = config.get_config() - if not config.has_option("battery", "enable_thresholds"): + if not conf.has_option("battery", "enable_thresholds"): return - if not config["battery"]["enable_thresholds"] == "true": + if not conf["battery"]["enable_thresholds"] == "true": return battery_count = len([name for name in os.listdir( diff --git a/auto_cpufreq/bin/auto_cpufreq.py b/auto_cpufreq/bin/auto_cpufreq.py index 39fb0944..26cef3f3 100755 --- a/auto_cpufreq/bin/auto_cpufreq.py +++ b/auto_cpufreq/bin/auto_cpufreq.py @@ -14,6 +14,7 @@ from auto_cpufreq.core import * from auto_cpufreq.power_helper import * from auto_cpufreq.battery_scripts.battery import * +from auto_cpufreq.utils.config import config as conf, find_config_file # cli @click.command() @click.option("--monitor", is_flag=True, help="Monitor and see suggestions for CPU optimizations") @@ -28,7 +29,7 @@ @click.option( "--config", is_flag=False, - default="/etc/auto-cpufreq.conf", + required=False, help="Use config file at defined path", ) @click.option("--debug", is_flag=True, help="Show debug info (include when submitting bugs)") @@ -40,9 +41,11 @@ def main(config, daemon, debug, update, install, remove, live, log, monitor, stats, version, donate, force, get_state, completions): # display info if config file is used + config_path = find_config_file(config) + conf.set_path(config_path) def config_info_dialog(): - if get_config(config) and hasattr(get_config, "using_cfg_file"): - print("\nUsing settings defined in " + config + " file") + if conf.has_config(): + print("\nUsing settings defined in " + config_path + " file") # set governor override unless None or invalid if force is not None: @@ -67,20 +70,13 @@ def config_info_dialog(): if os.getenv("PKG_MARKER") == "SNAP" and dcheck == "enabled": gnome_power_detect_snap() tlp_service_detect_snap() - battery_setup() - while True: - footer() - gov_check() - cpufreqctl() - distro_info() - sysinfo() - set_autofreq() - countdown(2) elif os.getenv("PKG_MARKER") != "SNAP": gnome_power_detect() tlp_service_detect() - battery_setup() - while True: + battery_setup() + conf.notifier.start() + while True: + try: footer() gov_check() cpufreqctl() @@ -88,16 +84,16 @@ def config_info_dialog(): sysinfo() set_autofreq() countdown(2) - else: - pass - #"daemon_not_found" is not defined - #daemon_not_found() + except KeyboardInterrupt: + break; + conf.notifier.stop() elif monitor: config_info_dialog() root_check() print('\nNote: You can quit monitor mode by pressing "ctrl+c"') battery_setup() battery_get_thresholds() + conf.notifier.start() if os.getenv("PKG_MARKER") == "SNAP": gnome_power_detect_snap() tlp_service_detect_snap() @@ -105,15 +101,19 @@ def config_info_dialog(): gnome_power_detect() tlp_service_detect() while True: - time.sleep(1) - running_daemon_check() - footer() - gov_check() - cpufreqctl() - distro_info() - sysinfo() - mon_autofreq() - countdown(2) + try: + time.sleep(1) + running_daemon_check() + footer() + gov_check() + cpufreqctl() + distro_info() + sysinfo() + mon_autofreq() + countdown(2) + except KeyboardInterrupt: + break + conf.notifier.stop() elif live: root_check() config_info_dialog() @@ -121,6 +121,7 @@ def config_info_dialog(): time.sleep(1) battery_setup() battery_get_thresholds() + conf.notifier.start() if os.getenv("PKG_MARKER") == "SNAP": gnome_power_detect_snap() tlp_service_detect_snap() @@ -141,7 +142,8 @@ def config_info_dialog(): except KeyboardInterrupt: gnome_power_start_live() print("") - sys.exit() + break + conf.notifier.stop() elif stats: not_running_daemon_check() config_info_dialog() diff --git a/auto_cpufreq/core.py b/auto_cpufreq/core.py index 0f093566..e61fa333 100755 --- a/auto_cpufreq/core.py +++ b/auto_cpufreq/core.py @@ -12,7 +12,6 @@ import click import pickle import warnings -import configparser # import pkg_resources import importlib.metadata from math import isclose @@ -27,6 +26,7 @@ sys.path.append("../") from auto_cpufreq.power_helper import * +from auto_cpufreq.utils.config import config warnings.filterwarnings("ignore") @@ -84,15 +84,6 @@ def file_stats(): auto_cpufreq_stats_file = open(auto_cpufreq_stats_path, "w") sys.stdout = auto_cpufreq_stats_file -def get_config(config_file=""): - if not hasattr(get_config, "config"): - get_config.config = configparser.ConfigParser() - - if os.path.isfile(config_file): - get_config.config.read(config_file) - get_config.using_cfg_file = True - - return get_config.config def get_override(): if os.path.isfile(governor_override_state): @@ -645,7 +636,7 @@ def set_frequencies(): if not hasattr(set_frequencies, "min_limit"): set_frequencies.min_limit = int(getoutput(f"cpufreqctl.auto-cpufreq --frequency-min-limit")) - conf = get_config() + conf = config.get_config() for freq_type in frequency.keys(): value = None @@ -686,7 +677,7 @@ def set_frequencies(): # set powersave and enable turbo def set_powersave(): - conf = get_config() + conf = config.get_config() if conf.has_option("battery", "governor"): gov = conf["battery"]["governor"] else: @@ -909,7 +900,7 @@ def mon_powersave(): # set performance and enable turbo def set_performance(): - conf = get_config() + conf = config.get_config() if conf.has_option("charger", "governor"): gov = conf["charger"]["governor"] else: diff --git a/auto_cpufreq/utils/__init__.py b/auto_cpufreq/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/auto_cpufreq/utils/config.py b/auto_cpufreq/utils/config.py new file mode 100644 index 00000000..650049a8 --- /dev/null +++ b/auto_cpufreq/utils/config.py @@ -0,0 +1,80 @@ +from configparser import ConfigParser, ParsingError +from auto_cpufreq.utils.config_event_handler import ConfigEventHandler +import pyinotify +from subprocess import run, PIPE +import os +import sys + +def find_config_file(args_config_file: str | None) -> str: + """ + Find the config file to use. + + Look for a config file in the following priorization order: + 1. Command line argument + 2. User config file + 3. System config file + + :param args_config_file: Path to the config file provided as a command line argument + :return: The path to the config file to use + """ + + # Prepare paths + + # use $SUDO_USER or $USER to get home dir since sudo can't access + # user env vars + home = run(["getent passwd ${SUDO_USER:-$USER} | cut -d: -f6"], + shell=True, + stdout=PIPE, + universal_newlines=True).stdout.rstrip() + user_config_dir = os.getenv("XDG_CONFIG_HOME", default=os.path.join(home, ".config")) + user_config_file = os.path.join(user_config_dir, "auto-cpufreq/auto-cpufreq.conf") + system_config_file = "/etc/auto-cpufreq.conf" + + if args_config_file is not None: # (1) Command line argument was specified + # Check if the config file path points to a valid file + if os.path.isfile(args_config_file): + return args_config_file + else: + # Not a valid file + print(f"Config file specified with '--config {args_config_file}' not found.") + sys.exit(1) + elif os.path.isfile(user_config_file): # (2) User config file + return user_config_file + else: # (3) System config file (default if nothing else is found) + return system_config_file + +class _Config: + def __init__(self) -> None: + self.path: str = "" + self._config: ConfigParser = ConfigParser() + self.watch_manager: pyinotify.WatchManager = pyinotify.WatchManager() + self.config_handler = ConfigEventHandler(self) + + # check for file changes using threading + self.notifier: pyinotify.ThreadedNotifier = pyinotify.ThreadedNotifier( + self.watch_manager, self.config_handler) + + def set_path(self, path: str) -> None: + self.path = path; + mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY \ + | pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO + self.watch_manager.add_watch(os.path.dirname(path), mask=mask) + if os.path.isfile(path): + self.update_config() + + + def has_config(self) -> bool: + return os.path.isfile(self.path) + + def get_config(self) -> ConfigParser: + return self._config + + def update_config(self) -> None: + # create new ConfigParser to prevent old data from remaining + self._config = ConfigParser() + try: + self._config.read(self.path) + except ParsingError as e: + print(f"The following error occured while parsing the config file: \n{e}") + +config = _Config() \ No newline at end of file diff --git a/auto_cpufreq/utils/config_event_handler.py b/auto_cpufreq/utils/config_event_handler.py new file mode 100644 index 00000000..7dc79bc0 --- /dev/null +++ b/auto_cpufreq/utils/config_event_handler.py @@ -0,0 +1,29 @@ +import pyinotify + +class ConfigEventHandler(pyinotify.ProcessEvent): + def __init__(self, config) -> None: + self.config = config + + def _process_update(self, event: pyinotify.Event): + if event.pathname.rstrip("~") == self.config.path: + self.config.update_config() + + # activates when auto-cpufreq config file is modified + def process_IN_MODIFY(self, event: pyinotify.Event) -> None: + self._process_update(event) + + # activates when auto-cpufreq config file is deleted + def process_IN_DELETE(self, event: pyinotify.Event) -> None: + self._process_update(event) + + # activates when auto-cpufreq config file is created + def process_IN_CREATE(self, event: pyinotify.Event) -> None: + self._process_update(event) + + # activates when auto-cpufreq config file is moved from watched directory + def process_IN_MOVED_FROM(self, event: pyinotify.Event) -> None: + self._process_update(event) + + # activates when auto-cpufreq config file is moved into the watched directory + def process_IN_MOVED_TO(self, event: pyinotify.Event) -> None: + self._process_update(event) \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index 562d67a2..e0536b73 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,11 +1,12 @@ { lib, - python310Packages, + python3Packages, pkgs, fetchFromGitHub, }: let - psutilGit = python310Packages.psutil.overrideAttrs (oldAttrs: { + + psutil = python3Packages.psutil.overrideAttrs (oldAttrs: { src = fetchFromGitHub { owner = "giampaolo"; repo = "psutil"; @@ -13,8 +14,18 @@ let sha256 = "61JwXP/cZrXqdBnb2J0kdDJoKpltO62KcpM0sYX6g1A="; }; }); + + pyinotify = python3Packages.pyinotify.overrideAttrs (oldAttrs: { + src = fetchFromGitHub { + owner = "shadeyg56"; + repo = "pyinotify-3.12"; + rev = "923cebec3a2a84c7e38c9e68171eb93f5d07ce5d"; + hash = "sha256-714CximEK4YhIqDmvqJYOUGs39gvDkWGrkNrXwxT8iM="; + }; + }); + in -python310Packages.buildPythonPackage { +python3Packages.buildPythonPackage { # use pyproject.toml instead of setup.py format = "pyproject"; @@ -24,9 +35,9 @@ python310Packages.buildPythonPackage { nativeBuildInputs = with pkgs; [wrapGAppsHook gobject-introspection]; - buildInputs = with pkgs; [gtk3 python310Packages.poetry-core]; + buildInputs = with pkgs; [gtk3 python3Packages.poetry-core]; - propagatedBuildInputs = with python310Packages; [requests pygobject3 click distro psutilGit setuptools poetry-dynamic-versioning]; + propagatedBuildInputs = with python3Packages; [requests pygobject3 click distro psutil setuptools poetry-dynamic-versioning pyinotify]; doCheck = false; pythonImportsCheck = ["auto_cpufreq"]; diff --git a/poetry.lock b/poetry.lock index 29b57a2f..2a99ca66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -871,6 +871,21 @@ files = [ [package.dependencies] pycairo = ">=1.16,<2.0" +[[package]] +name = "pyinotify" +version = "0.9.6" +description = "Linux filesystem events monitoring" +optional = false +python-versions = "*" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/shadeyg56/pyinotify-3.12" +reference = "HEAD" +resolved_reference = "923cebec3a2a84c7e38c9e68171eb93f5d07ce5d" + [[package]] name = "pyproject-hooks" version = "1.0.0" @@ -1285,4 +1300,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e3f4ec63d598a563c21fca6d7c885183baf257663537ca010dec33674163b175" +content-hash = "ee73b2db6a43cac87120f38c93d0a8a297bec52f1346b55bc0ca2992aa464482" diff --git a/pyproject.toml b/pyproject.toml index e7ffc29f..47fcc502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ click = "^8.1.0" distro = "^1.8.0" requests = "^2.31.0" PyGObject = "^3.46.0" +pyinotify = {git = "https://github.com/shadeyg56/pyinotify-3.12"} [tool.poetry.group.dev.dependencies] poetry = "^1.6.1"