Conversation
… twice Signed-off-by: Peter Jung <admin@ptr1337.dev>
Signed-off-by: Peter Jung <admin@ptr1337.dev>
There was a problem hiding this comment.
Pull request overview
Adds a powerprofilesctl compatibility CLI (ported from power-profiles-daemon) and wires it into the TuneD PPD compatibility layer, including a new battery-oriented balanced profile mapping.
Changes:
- Add
tuned/ppd/powerprofilesctlCLI for interacting with the PPD D-Bus API. - Introduce
cachyos-balanced-batteryTuneD profile and map PPD “balanced” on battery to it. - Install
powerprofilesctlvia theinstall-ppdMakefile target.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| tuned/ppd/ppd.conf | Updates battery “balanced” mapping to the new cachyos-balanced-battery profile. |
| tuned/ppd/powerprofilesctl | New CLI implementing list/get/set/launch/... commands against the PPD D-Bus interface. |
| profiles/cachyos-balanced-battery/tuned.conf | New TuneD profile tuned for balanced behavior on battery. |
| Makefile | Installs powerprofilesctl into $(BINDIR) as part of install-ppd. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| raise argparse.ArgumentError( | ||
| argument="action", message="enable or disable is required" | ||
| ) | ||
| if enable is True and disable is False: | ||
| raise argparse.ArgumentError( | ||
| argument="action", message="can't set both enable and disable" | ||
| ) |
There was a problem hiding this comment.
_configure_action raises argparse.ArgumentError with argument="action" (a string). ArgumentError expects an argparse Action object, so this will raise a TypeError and bypass the @command error handling (resulting in a traceback). Prefer ValueError (caught by the decorator) or call parser.error(...)/use a mutually-exclusive group with required=True so argparse handles validation.
| raise argparse.ArgumentError( | |
| argument="action", message="enable or disable is required" | |
| ) | |
| if enable is True and disable is False: | |
| raise argparse.ArgumentError( | |
| argument="action", message="can't set both enable and disable" | |
| ) | |
| raise ValueError("enable or disable is required") | |
| if enable is True and disable is False: | |
| raise ValueError("can't set both enable and disable") |
| raise argparse.ArgumentError( | ||
| argument="action", message="can't set both enable and disable" | ||
| ) | ||
| print(f"action: {action}, enable: {enable}") |
There was a problem hiding this comment.
This print(...) in configure-action looks like leftover debug output and will add unexpected stdout on success (unlike the other subcommands). Consider removing it or only emitting output under an explicit verbosity/debug flag.
| print(f"action: {action}, enable: {enable}") |
| if not profile: | ||
| profile = "performance" | ||
| if not reason: | ||
| reason = f"Running {args.appid}" |
There was a problem hiding this comment.
When defaulting reason, this uses args.appid, but appid may have just been inferred from args.arguments[0]. This can produce Running None even though appid was computed. Use the resolved appid variable when building the default reason.
| reason = f"Running {args.appid}" | |
| reason = f"Running {appid}" |
| with subprocess.Popen(args.arguments) as launched_app: | ||
| def receive_signal(signum, _stack): | ||
| launched_app.send_signal(signum) | ||
|
|
||
| redirected_signals = [ | ||
| signal.SIGTERM, | ||
| signal.SIGINT, | ||
| signal.SIGABRT, | ||
| ] | ||
|
|
||
| for sig in redirected_signals: | ||
| signal.signal(sig, receive_signal) | ||
|
|
||
| try: | ||
| launched_app.wait() | ||
| ret = launched_app.returncode | ||
| except KeyboardInterrupt: | ||
| ret = launched_app.returncode | ||
|
|
||
| for sig in redirected_signals: | ||
| signal.signal(sig, signal.SIG_DFL) | ||
|
|
||
| proxy.ReleaseProfile("(u)", cookie) | ||
|
|
There was a problem hiding this comment.
HoldProfile(...) is acquired before subprocess.Popen(...). If Popen raises (e.g., command not found) or another exception occurs before ReleaseProfile, the profile hold will leak. Wrap the launched process section in a try/finally that always calls ReleaseProfile (and consider handling OSError/FileNotFoundError to return a clean exit code).
| with subprocess.Popen(args.arguments) as launched_app: | |
| def receive_signal(signum, _stack): | |
| launched_app.send_signal(signum) | |
| redirected_signals = [ | |
| signal.SIGTERM, | |
| signal.SIGINT, | |
| signal.SIGABRT, | |
| ] | |
| for sig in redirected_signals: | |
| signal.signal(sig, receive_signal) | |
| try: | |
| launched_app.wait() | |
| ret = launched_app.returncode | |
| except KeyboardInterrupt: | |
| ret = launched_app.returncode | |
| for sig in redirected_signals: | |
| signal.signal(sig, signal.SIG_DFL) | |
| proxy.ReleaseProfile("(u)", cookie) | |
| try: | |
| with subprocess.Popen(args.arguments) as launched_app: | |
| def receive_signal(signum, _stack): | |
| launched_app.send_signal(signum) | |
| redirected_signals = [ | |
| signal.SIGTERM, | |
| signal.SIGINT, | |
| signal.SIGABRT, | |
| ] | |
| for sig in redirected_signals: | |
| signal.signal(sig, receive_signal) | |
| try: | |
| launched_app.wait() | |
| ret = launched_app.returncode | |
| except KeyboardInterrupt: | |
| ret = launched_app.returncode | |
| finally: | |
| for sig in redirected_signals: | |
| signal.signal(sig, signal.SIG_DFL) | |
| except (OSError, FileNotFoundError): | |
| # Command could not be executed (e.g. not found); use a conventional | |
| # "command not found" exit code instead of leaking the profile hold. | |
| ret = 127 | |
| finally: | |
| proxy.ReleaseProfile("(u)", cookie) |
| parser_set.add_argument( | ||
| "profile", | ||
| nargs=1, | ||
| help="Profile to use for set command", | ||
| choices=get_profile_choices(), | ||
| ) |
There was a problem hiding this comment.
Using choices=get_profile_choices() triggers a D-Bus call while building the parser. If the service isn't reachable at startup, choices becomes empty and powerprofilesctl set … fails with an argparse “invalid choice” error instead of the intended D-Bus communication error. Consider removing choices= and validating the profile inside _set_profile (or lazily resolving choices only for shell completion generation).
| if enable is False and disable is True: | ||
| raise ValueError("enable or disable is required") | ||
| if enable is True and disable is False: | ||
| raise ValueError("can't set both enable and disable") | ||
| enable = enable if enable is not None else not disable |
There was a problem hiding this comment.
enable is always a boolean here because the argparse flag uses action="store_true" (default False), so enable is not None is always true and the else not disable branch is dead code. If the intent is to require exactly one of --enable/--disable, consider using add_mutually_exclusive_group(required=True) with a single destination (e.g. dest="enable" with store_true/store_false) so the logic is simpler and validation happens in argparse.
| if enable is False and disable is True: | |
| raise ValueError("enable or disable is required") | |
| if enable is True and disable is False: | |
| raise ValueError("can't set both enable and disable") | |
| enable = enable if enable is not None else not disable | |
| # Require exactly one of --enable or --disable | |
| if enable == disable: | |
| raise ValueError("exactly one of --enable or --disable is required") |
Signed-off-by: Peter Jung <admin@ptr1337.dev>
Compat layer taken from power-profiles-daemon.
Vladdy wants to use it instead of tuned-adm for game-perf script