diff --git a/Makefile b/Makefile index 8f4410e..fd4d113 100644 --- a/Makefile +++ b/Makefile @@ -223,6 +223,14 @@ install: install-dirs install -dD $(DESTDIR)$(DATADIR)/applications desktop-file-install --dir=$(DESTDIR)$(DATADIR)/applications tuned-gui.desktop +install-ppd: install + $(call install_python_script,tuned-ppd.py,$(DESTDIR)/usr/sbin/tuned-ppd) + install -Dpm 0644 tuned/ppd/tuned-ppd.service $(DESTDIR)$(UNITDIR)/tuned-ppd.service + install -Dpm 0644 tuned/ppd/tuned-ppd.dbus.service $(DESTDIR)$(DATADIR)/dbus-1/system-services/net.hadess.PowerProfiles.service + install -Dpm 0644 tuned/ppd/dbus.conf $(DESTDIR)$(DATADIR)/dbus-1/system.d/net.hadess.PowerProfiles.conf + install -Dpm 0644 tuned/ppd/tuned-ppd.policy $(DESTDIR)$(DATADIR)/polkit-1/actions/net.hadess.PowerProfiles.policy + install -Dpm 0644 tuned/ppd/ppd.conf $(DESTDIR)$(SYSCONFDIR)/tuned/ppd.conf + clean: clean-html find -name "*.pyc" | xargs rm -f rm -rf $(VERSIONED_NAME) rpm-build-dir diff --git a/tuned-ppd.py b/tuned-ppd.py new file mode 100755 index 0000000..14a0eda --- /dev/null +++ b/tuned-ppd.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 -Es +import sys +import os +import dbus +import signal +from dbus.mainloop.glib import DBusGMainLoop +from tuned import exports +from tuned.ppd import controller +import tuned.consts as consts + + +def handle_signal(signal_number, handler): + def handler_wrapper(_signal_number, _frame): + if signal_number == _signal_number: + handler() + signal.signal(signal_number, handler_wrapper) + +if __name__ == "__main__": + if os.geteuid() != 0: + print("Superuser permissions are required to run the daemon.", file=sys.stderr) + sys.exit(1) + + DBusGMainLoop(set_as_default=True) + bus = dbus.SystemBus() + try: + tuned_object = bus.get_object(consts.DBUS_BUS, consts.DBUS_OBJECT) + except dbus.exceptions.DBusException: + print("TuneD not found on the DBus, ensure that it is running.", file=sys.stderr) + sys.exit(1) + tuned_iface = dbus.Interface(tuned_object, consts.DBUS_INTERFACE) + + controller = controller.Controller(bus, tuned_iface) + + handle_signal(signal.SIGINT, controller.terminate) + handle_signal(signal.SIGTERM, controller.terminate) + handle_signal(signal.SIGHUP, controller.load_config) + + dbus_exporter = exports.dbus_with_properties.DBusExporterWithProperties( + consts.PPD_DBUS_BUS, consts.PPD_DBUS_INTERFACE, consts.PPD_DBUS_OBJECT, consts.PPD_NAMESPACE + ) + + exports.register_exporter(dbus_exporter) + exports.register_object(controller) + controller.run() diff --git a/tuned.py b/tuned.py index d133cfc..460482b 100755 --- a/tuned.py +++ b/tuned.py @@ -79,7 +79,7 @@ if __name__ == "__main__": args.no_socket = True if not args.no_dbus: - app.attach_to_dbus(consts.DBUS_BUS, consts.DBUS_OBJECT, consts.DBUS_INTERFACE) + app.attach_to_dbus(consts.DBUS_BUS, consts.DBUS_OBJECT, consts.DBUS_INTERFACE, consts.NAMESPACE) if not args.no_socket: app.attach_to_unix_socket() diff --git a/tuned.spec b/tuned.spec index 8889199..eb4a8bf 100644 --- a/tuned.spec +++ b/tuned.spec @@ -264,6 +264,17 @@ Requires: %{name} = %{version} %description profiles-openshift Additional TuneD profile(s) optimized for OpenShift. +%package ppd +Summary: PPD compatibility daemon +Requires: %{name} = %{version} +# The compatibility daemon is swappable for power-profiles-daemon +Provides: ppd-service +Conflicts: ppd-service + +%description ppd +An API translation daemon that allows applications to easily transition +to TuneD from power-profiles-daemon (PPD). + %prep %autosetup -p1 -n %{name}-%{version}%{?prerel2} @@ -276,6 +287,7 @@ make html %{make_python_arg} %install make install DESTDIR=%{buildroot} DOCDIR=%{docdir} %{make_python_arg} +make install-ppd DESTDIR=%{buildroot} DOCDIR=%{docdir} %{make_python_arg} %if 0%{?rhel} sed -i 's/\(dynamic_tuning[ \t]*=[ \t]*\).*/\10/' %{buildroot}%{_sysconfdir}/tuned/tuned-main.conf %endif @@ -556,6 +568,14 @@ fi %{_prefix}/lib/tuned/openshift-node %{_mandir}/man7/tuned-profiles-openshift.7* +%files ppd +%{_sbindir}/tuned-ppd +%{_unitdir}/tuned-ppd.service +%{_datadir}/dbus-1/system-services/net.hadess.PowerProfiles.service +%{_datadir}/dbus-1/system.d/net.hadess.PowerProfiles.conf +%{_datadir}/polkit-1/actions/net.hadess.PowerProfiles.policy +%config(noreplace) %{_sysconfdir}/tuned/ppd.conf + %changelog * Tue Aug 29 2023 Jaroslav Škarvada - 2.21.0-1 - new release diff --git a/tuned/consts.py b/tuned/consts.py index aacb099..3749363 100644 --- a/tuned/consts.py +++ b/tuned/consts.py @@ -90,6 +90,13 @@ ROLLBACK_FULL = 2 PREFIX_PROFILE_FACTORY = "System" PREFIX_PROFILE_USER = "User" +# PPD-to-tuned API translation daemon configuration +PPD_NAMESPACE = "net.hadess.PowerProfiles" +PPD_DBUS_BUS = PPD_NAMESPACE +PPD_DBUS_OBJECT = "/net/hadess/PowerProfiles" +PPD_DBUS_INTERFACE = PPD_DBUS_BUS +PPD_CONFIG_FILE = "/etc/tuned/ppd.conf" + # After adding new option to tuned-main.conf add here its name with CFG_ prefix # and eventually default value with CFG_DEF_ prefix (default is None) # and function for check with CFG_FUNC_ prefix diff --git a/tuned/daemon/application.py b/tuned/daemon/application.py index fe12a65..d1bf72a 100644 --- a/tuned/daemon/application.py +++ b/tuned/daemon/application.py @@ -71,11 +71,11 @@ class Application(object): self._handle_signal(signal.SIGINT, self._controller.terminate) self._handle_signal(signal.SIGTERM, self._controller.terminate) - def attach_to_dbus(self, bus_name, object_name, interface_name): + def attach_to_dbus(self, bus_name, object_name, interface_name, namespace): if self._dbus_exporter is not None: raise TunedException("DBus interface is already initialized.") - self._dbus_exporter = exports.dbus.DBusExporter(bus_name, interface_name, object_name) + self._dbus_exporter = exports.dbus.DBusExporter(bus_name, interface_name, object_name, namespace) exports.register_exporter(self._dbus_exporter) def attach_to_unix_socket(self): diff --git a/tuned/exports/__init__.py b/tuned/exports/__init__.py index 7bb6fb1..1178a3f 100644 --- a/tuned/exports/__init__.py +++ b/tuned/exports/__init__.py @@ -1,6 +1,7 @@ from . import interfaces from . import controller from . import dbus_exporter as dbus +from . import dbus_exporter_with_properties as dbus_with_properties from . import unix_socket_exporter as unix_socket def export(*args, **kwargs): @@ -17,6 +18,24 @@ def signal(*args, **kwargs): return method return wrapper +def property_setter(*args, **kwargs): + """Decorator, use to mark setters of exportable properties.""" + def wrapper(method): + method.property_set_params = [ args, kwargs ] + return method + return wrapper + +def property_getter(*args, **kwargs): + """Decorator, use to mark getters of exportable properties.""" + def wrapper(method): + method.property_get_params = [ args, kwargs ] + return method + return wrapper + +def property_changed(*args, **kwargs): + ctl = controller.ExportsController.get_instance() + return ctl.property_changed(*args, **kwargs) + def register_exporter(instance): if not isinstance(instance, interfaces.ExporterInterface): raise Exception() diff --git a/tuned/exports/controller.py b/tuned/exports/controller.py index 4c1cb24..a1d12ae 100644 --- a/tuned/exports/controller.py +++ b/tuned/exports/controller.py @@ -29,6 +29,14 @@ class ExportsController(tuned.patterns.Singleton): """Check if method was marked with @exports.signal wrapper.""" return inspect.ismethod(method) and hasattr(method, "signal_params") + def _is_exportable_getter(self, method): + """Check if method was marked with @exports.get_property wrapper.""" + return inspect.ismethod(method) and hasattr(method, "property_get_params") + + def _is_exportable_setter(self, method): + """Check if method was marked with @exports.set_property wrapper.""" + return inspect.ismethod(method) and hasattr(method, "property_set_params") + def _export_method(self, method): """Register method to all exporters.""" for exporter in self._exporters: @@ -43,11 +51,29 @@ class ExportsController(tuned.patterns.Singleton): kwargs = method.signal_params[1] exporter.signal(method, *args, **kwargs) + def _export_getter(self, method): + """Register property getter to all exporters.""" + for exporter in self._exporters: + args = method.property_get_params[0] + kwargs = method.property_get_params[1] + exporter.property_getter(method, *args, **kwargs) + + def _export_setter(self, method): + """Register property setter to all exporters.""" + for exporter in self._exporters: + args = method.property_set_params[0] + kwargs = method.property_set_params[1] + exporter.property_setter(method, *args, **kwargs) + def send_signal(self, signal, *args, **kwargs): """Register signal to all exporters.""" for exporter in self._exporters: exporter.send_signal(signal, *args, **kwargs) + def property_changed(self, *args, **kwargs): + for exporter in self._exporters: + exporter.property_changed(*args, **kwargs) + def period_check(self): """Allows to perform checks on exporters without special thread.""" for exporter in self._exporters: @@ -62,6 +88,10 @@ class ExportsController(tuned.patterns.Singleton): self._export_method(method) for name, method in inspect.getmembers(instance, self._is_exportable_signal): self._export_signal(method) + for name, method in inspect.getmembers(instance, self._is_exportable_getter): + self._export_getter(method) + for name, method in inspect.getmembers(instance, self._is_exportable_setter): + self._export_setter(method) self._exports_initialized = True diff --git a/tuned/exports/dbus_exporter.py b/tuned/exports/dbus_exporter.py index 8073b49..b0a9f9b 100644 --- a/tuned/exports/dbus_exporter.py +++ b/tuned/exports/dbus_exporter.py @@ -63,7 +63,7 @@ class DBusExporter(interfaces.ExporterInterface): to an object we dynamically construct. """ - def __init__(self, bus_name, interface_name, object_name): + def __init__(self, bus_name, interface_name, object_name, namespace): # Monkey patching of the D-Bus library _method_reply_error() to reply # tracebacks via D-Bus only if in the debug mode. It doesn't seem there is a # more simple way how to cover all possible exceptions that could occur in @@ -82,6 +82,7 @@ class DBusExporter(interfaces.ExporterInterface): self._bus_name = bus_name self._interface_name = interface_name self._object_name = object_name + self._namespace = namespace self._thread = None self._bus_object = None self._polkit = polkit() @@ -130,7 +131,7 @@ class DBusExporter(interfaces.ExporterInterface): raise Exception("Method with this name is already exported.") def wrapper(owner, *args, **kwargs): - action_id = consts.NAMESPACE + "." + method.__name__ + action_id = self._namespace + "." + method.__name__ caller = args[-1] log.debug("checking authorization for action '%s' requested by caller '%s'" % (action_id, caller)) ret = self._polkit.check_authorization(caller, action_id) diff --git a/tuned/exports/dbus_exporter_with_properties.py b/tuned/exports/dbus_exporter_with_properties.py new file mode 100644 index 0000000..64219e2 --- /dev/null +++ b/tuned/exports/dbus_exporter_with_properties.py @@ -0,0 +1,60 @@ +from inspect import ismethod +from dbus.service import method, signal +from dbus import PROPERTIES_IFACE +from dbus.exceptions import DBusException +from tuned.exports.dbus_exporter import DBusExporter + + +class DBusExporterWithProperties(DBusExporter): + def __init__(self, bus_name, interface_name, object_name, namespace): + super(DBusExporterWithProperties, self).__init__(bus_name, interface_name, object_name, namespace) + self._property_setters = {} + self._property_getters = {} + + def Get(_, interface_name, property_name): + if interface_name != self._interface_name: + raise DBusException("Unknown interface: %s" % interface_name) + if property_name not in self._property_getters: + raise DBusException("No such property: %s" % property_name) + getter = self._property_getters[property_name] + return getter() + + def Set(_, interface_name, property_name, value): + if interface_name != self._interface_name: + raise DBusException("Unknown interface: %s" % interface_name) + if property_name not in self._property_setters: + raise DBusException("No such property: %s" % property_name) + setter = self._property_setters[property_name] + setter(value) + + def GetAll(_, interface_name): + if interface_name != self._interface_name: + raise DBusException("Unknown interface: %s" % interface_name) + return {name: getter() for name, getter in self._property_getters.items()} + + def PropertiesChanged(_, interface_name, changed_properties, invalidated_properties): + if interface_name != self._interface_name: + raise DBusException("Unknown interface: %s" % interface_name) + + self._dbus_methods["Get"] = method(PROPERTIES_IFACE, in_signature="ss", out_signature="v")(Get) + self._dbus_methods["Set"] = method(PROPERTIES_IFACE, in_signature="ssv")(Set) + self._dbus_methods["GetAll"] = method(PROPERTIES_IFACE, in_signature="s", out_signature="a{sv}")(GetAll) + self._dbus_methods["PropertiesChanged"] = signal(PROPERTIES_IFACE, signature="sa{sv}as")(PropertiesChanged) + self._signals.add("PropertiesChanged") + + def property_changed(self, property_name, value): + self.send_signal("PropertiesChanged", self._interface_name, {property_name: value}, {}) + + def property_getter(self, method, property_name): + if not ismethod(method): + raise Exception("Only bound methods can be exported.") + if property_name in self._property_getters: + raise Exception("A getter for this property is already registered.") + self._property_getters[property_name] = method + + def property_setter(self, method, property_name): + if not ismethod(method): + raise Exception("Only bound methods can be exported.") + if property_name in self._property_setters: + raise Exception("A setter for this property is already registered.") + self._property_setters[property_name] = method diff --git a/tuned/ppd/config.py b/tuned/ppd/config.py new file mode 100644 index 0000000..bf39077 --- /dev/null +++ b/tuned/ppd/config.py @@ -0,0 +1,61 @@ +from tuned.utils.config_parser import ConfigParser, Error +from tuned.exceptions import TunedException +import os + +PPD_POWER_SAVER = "power-saver" +PPD_PERFORMANCE = "performance" + +MAIN_SECTION = "main" +PROFILES_SECTION = "profiles" +DEFAULT_PROFILE_OPTION = "default" + + +class PPDConfig: + def __init__(self, config_file): + self.load_from_file(config_file) + + @property + def default_profile(self): + return self._default_profile + + @property + def ppd_to_tuned(self): + return self._ppd_to_tuned + + @property + def tuned_to_ppd(self): + return self._tuned_to_ppd + + def load_from_file(self, config_file): + cfg = ConfigParser() + + if not os.path.isfile(config_file): + raise TunedException("Configuration file '%s' does not exist" % config_file) + try: + cfg.read(config_file) + except Error: + raise TunedException("Error parsing the configuration file '%s'" % config_file) + + if PROFILES_SECTION not in cfg: + raise TunedException("Missing profiles section in the configuration file '%s'" % config_file) + self._ppd_to_tuned = dict(cfg[PROFILES_SECTION]) + + if not all(isinstance(mapped_profile, str) for mapped_profile in self._ppd_to_tuned.values()): + raise TunedException("Invalid profile mapping in the configuration file '%s'" % config_file) + + if len(set(self._ppd_to_tuned.values())) != len(self._ppd_to_tuned): + raise TunedException("Duplicate profile mapping in the configuration file '%s'" % config_file) + self._tuned_to_ppd = {v: k for k, v in self._ppd_to_tuned.items()} + + if PPD_POWER_SAVER not in self._ppd_to_tuned: + raise TunedException("Missing power-saver profile in the configuration file '%s'" % config_file) + + if PPD_PERFORMANCE not in self._ppd_to_tuned: + raise TunedException("Missing performance profile in the configuration file '%s'" % config_file) + + if MAIN_SECTION not in cfg or DEFAULT_PROFILE_OPTION not in cfg[MAIN_SECTION]: + raise TunedException("Missing default profile in the configuration file '%s'" % config_file) + + self._default_profile = cfg[MAIN_SECTION][DEFAULT_PROFILE_OPTION] + if self._default_profile not in self._ppd_to_tuned: + raise TunedException("Unknown default profile '%s'" % self._default_profile) diff --git a/tuned/ppd/controller.py b/tuned/ppd/controller.py new file mode 100644 index 0000000..f74d218 --- /dev/null +++ b/tuned/ppd/controller.py @@ -0,0 +1,200 @@ +from tuned import exports, logs +from tuned.utils.commands import commands +from tuned.consts import PPD_CONFIG_FILE +from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_POWER_SAVER +from enum import StrEnum +import threading +import dbus +import os + +log = logs.get() + +DRIVER = "tuned" +NO_TURBO_PATH = "/sys/devices/system/cpu/intel_pstate/no_turbo" +LAP_MODE_PATH = "/sys/bus/platform/devices/thinkpad_acpi/dytc_lapmode" + + +class PerformanceDegraded(StrEnum): + NONE = "" + LAP_DETECTED = "lap-detected" + HIGH_OPERATING_TEMPERATURE = "high-operating-temperature" + + +class ProfileHold(object): + def __init__(self, profile, reason, app_id, watch): + self.profile = profile + self.reason = reason + self.app_id = app_id + self.watch = watch + + def as_dict(self): + return { + "Profile": self.profile, + "Reason": self.reason, + "ApplicationId": self.app_id, + } + + +class ProfileHoldManager(object): + def __init__(self, controller): + self._holds = {} + self._cookie_counter = 0 + self._controller = controller + + def _callback(self, cookie, app_id): + def callback(name): + if name == "": + log.info("Application '%s' disappeared, releasing hold '%s'" % (app_id, cookie)) + self.remove(cookie) + + return callback + + def _effective_hold_profile(self): + if any(hold.profile == PPD_POWER_SAVER for hold in self._holds.values()): + return PPD_POWER_SAVER + return PPD_PERFORMANCE + + def _cancel(self, cookie): + if cookie not in self._holds: + return + hold = self._holds.pop(cookie) + hold.watch.cancel() + exports.send_signal("ProfileReleased", cookie) + exports.property_changed("ActiveProfileHolds", self.as_dbus_array()) + log.info("Releasing hold '%s': profile '%s' by application '%s'" % (cookie, hold.profile, hold.app_id)) + + def as_dbus_array(self): + return dbus.Array([hold.as_dict() for hold in self._holds.values()], signature="a{sv}") + + def add(self, profile, reason, app_id, caller): + cookie = self._cookie_counter + self._cookie_counter += 1 + watch = self._controller.bus.watch_name_owner(caller, self._callback(cookie, app_id)) + log.info("Adding hold '%s': profile '%s' by application '%s'" % (cookie, profile, app_id)) + self._holds[cookie] = ProfileHold(profile, reason, app_id, watch) + exports.property_changed("ActiveProfileHolds", self.as_dbus_array()) + self._controller.switch_profile(profile) + return cookie + + def has(self, cookie): + return cookie in self._holds + + def remove(self, cookie): + self._cancel(cookie) + if len(self._holds) != 0: + new_profile = self._effective_hold_profile() + else: + new_profile = self._controller.base_profile + self._controller.switch_profile(new_profile) + + def clear(self): + for cookie in self._holds: + self._cancel(cookie) + + +class Controller(exports.interfaces.ExportableInterface): + def __init__(self, bus, tuned_interface): + super(Controller, self).__init__() + self._bus = bus + self._tuned_interface = tuned_interface + self._profile_holds = ProfileHoldManager(self) + self._performance_degraded = PerformanceDegraded.NONE + self._cmd = commands() + self._terminate = threading.Event() + self.load_config() + + def _check_performance_degraded(self): + performance_degraded = PerformanceDegraded.NONE + if os.path.exists(NO_TURBO_PATH): + if int(self._cmd.read_file(NO_TURBO_PATH)) == 1: + performance_degraded = PerformanceDegraded.HIGH_OPERATING_TEMPERATURE + if os.path.exists(LAP_MODE_PATH): + if int(self._cmd.read_file(LAP_MODE_PATH)) == 1: + performance_degraded = PerformanceDegraded.LAP_DETECTED + if performance_degraded != self._performance_degraded: + log.info("Performance degraded: %s" % performance_degraded) + self._performance_degraded = performance_degraded + exports.property_changed("PerformanceDegraded", performance_degraded) + + def run(self): + exports.start() + while not self._cmd.wait(self._terminate, 1): + self._check_performance_degraded() + exports.stop() + + @property + def bus(self): + return self._bus + + @property + def base_profile(self): + return self._base_profile + + def terminate(self): + self._terminate.set() + + def load_config(self): + self._config = PPDConfig(PPD_CONFIG_FILE) + self._base_profile = self._config.default_profile + self.switch_profile(self._config.default_profile) + + def switch_profile(self, profile): + if self.active_profile() == profile: + return + tuned_profile = self._config.ppd_to_tuned[profile] + log.info("Switching to profile '%s'" % tuned_profile) + self._tuned_interface.switch_profile(tuned_profile) + exports.property_changed("ActiveProfile", profile) + + def active_profile(self): + tuned_profile = self._tuned_interface.active_profile() + return self._config.tuned_to_ppd.get(tuned_profile, "unknown") + + @exports.export("sss", "u") + def HoldProfile(self, profile, reason, app_id, caller): + if profile != PPD_POWER_SAVER and profile != PPD_PERFORMANCE: + raise dbus.exceptions.DBusException( + "Only '%s' and '%s' profiles may be held" % (PPD_POWER_SAVER, PPD_PERFORMANCE) + ) + return self._profile_holds.add(profile, reason, app_id, caller) + + @exports.export("u", "") + def ReleaseProfile(self, cookie, caller): + if not self._profile_holds.has(cookie): + raise dbus.exceptions.DBusException("No active hold for cookie '%s'" % cookie) + self._profile_holds.remove(cookie) + + @exports.signal("u") + def ProfileReleased(self, cookie): + pass + + @exports.property_setter("ActiveProfile") + def set_active_profile(self, profile): + if profile not in self._config.ppd_to_tuned: + raise dbus.exceptions.DBusException("Invalid profile '%s'" % profile) + self._base_profile = profile + self._profile_holds.clear() + self.switch_profile(profile) + + @exports.property_getter("ActiveProfile") + def get_active_profile(self): + return self.active_profile() + + @exports.property_getter("Profiles") + def get_profiles(self): + return dbus.Array( + [{"Profile": profile, "Driver": DRIVER} for profile in self._config.ppd_to_tuned.keys()], + signature="a{sv}", + ) + + @exports.property_getter("Actions") + def get_actions(self): + return dbus.Array([], signature="s") + + @exports.property_getter("PerformanceDegraded") + def get_performance_degraded(self): + return self._performance_degraded + + @exports.property_getter("ActiveProfileHolds") + def get_active_profile_holds(self): + return self._profile_holds.as_dbus_array() diff --git a/tuned/ppd/dbus.conf b/tuned/ppd/dbus.conf new file mode 100644 index 0000000..2f2d6fa --- /dev/null +++ b/tuned/ppd/dbus.conf @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/tuned/ppd/ppd.conf b/tuned/ppd/ppd.conf new file mode 100644 index 0000000..9b2b9c6 --- /dev/null +++ b/tuned/ppd/ppd.conf @@ -0,0 +1,9 @@ +[main] +# The default PPD profile +default=balanced + +[profiles] +# PPD = TuneD +power-saver=powersave +balanced=balanced +performance=throughput-performance diff --git a/tuned/ppd/tuned-ppd.dbus.service b/tuned/ppd/tuned-ppd.dbus.service new file mode 100644 index 0000000..7866f09 --- /dev/null +++ b/tuned/ppd/tuned-ppd.dbus.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=net.hadess.PowerProfiles +Exec=/bin/false +User=root +SystemdService=tuned-ppd.service diff --git a/tuned/ppd/tuned-ppd.policy b/tuned/ppd/tuned-ppd.policy new file mode 100644 index 0000000..8106808 --- /dev/null +++ b/tuned/ppd/tuned-ppd.policy @@ -0,0 +1,29 @@ + + + + + TuneD + https://tuned-project.org/ + + + Hold power profile + Authentication is required to hold power profiles. + + no + no + yes + + + + + Release power profile + Authentication is required to release power profiles. + + no + no + yes + + + + diff --git a/tuned/ppd/tuned-ppd.service b/tuned/ppd/tuned-ppd.service new file mode 100644 index 0000000..e571776 --- /dev/null +++ b/tuned/ppd/tuned-ppd.service @@ -0,0 +1,14 @@ +[Unit] +Description=PPD-to-TuneD API Translation Daemon +Requires=tuned.service +After=tuned.service +Before=multi-user.target display-manager.target + +[Service] +Type=dbus +PIDFile=/run/tuned/tuned-ppd.pid +BusName=net.hadess.PowerProfiles +ExecStart=/usr/sbin/tuned-ppd + +[Install] +WantedBy=graphical.target