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 <steven.braun.mz@gmail.com>
This commit is contained in:
shadeyg56 2024-04-30 01:35:53 -05:00 committed by GitHub
parent 8bb7478e38
commit 0815e7eb96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 206 additions and 72 deletions

View File

@ -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 <FILE>` 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

View File

@ -31,6 +31,7 @@ def battery_setup():
def battery_get_thresholds():
if lsmod("thinkpad_acpi"):
thinkpad_print_thresholds()

View File

@ -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(

View File

@ -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:

View File

@ -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(

View File

@ -14,6 +14,7 @@ from subprocess import call, run
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 @@ from auto_cpufreq.battery_scripts.battery import *
@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 @@ from auto_cpufreq.battery_scripts.battery import *
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 main(config, daemon, debug, update, install, remove, live, log, monitor, sta
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 main(config, daemon, debug, update, install, remove, live, log, monitor, sta
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 main(config, daemon, debug, update, install, remove, live, log, monitor, sta
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 main(config, daemon, debug, update, install, remove, live, log, monitor, sta
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 main(config, daemon, debug, update, install, remove, live, log, monitor, sta
except KeyboardInterrupt:
gnome_power_start_live()
print("")
sys.exit()
break
conf.notifier.stop()
elif stats:
not_running_daemon_check()
config_info_dialog()

View File

@ -12,7 +12,6 @@ import time
import click
import pickle
import warnings
import configparser
# import pkg_resources
import importlib.metadata
from math import isclose
@ -27,6 +26,7 @@ from datetime import datetime
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:

View File

View File

@ -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()

View File

@ -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)

View File

@ -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"];

17
poetry.lock generated
View File

@ -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"

View File

@ -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"