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"