1
0
Fork 0

Merge pull request #579 from zacikpa/ppd-daemon

PPD-to-TuneD API translation daemon
This commit is contained in:
Jaroslav Škarvada 2024-02-08 00:25:11 +01:00 committed by GitHub
commit 11c6c57087
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 528 additions and 5 deletions

View file

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

44
tuned-ppd.py Executable file
View file

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

View file

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

View file

@ -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 <jskarvad@redhat.com> - 2.21.0-1
- new release

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

61
tuned/ppd/config.py Normal file
View file

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

200
tuned/ppd/controller.py Normal file
View file

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

16
tuned/ppd/dbus.conf Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="net.hadess.PowerProfiles"/>
</policy>
<policy context="default">
<allow send_destination="net.hadess.PowerProfiles" send_interface="net.hadess.PowerProfiles"/>
<allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Peer"/>
</policy>
</busconfig>

9
tuned/ppd/ppd.conf Normal file
View file

@ -0,0 +1,9 @@
[main]
# The default PPD profile
default=balanced
[profiles]
# PPD = TuneD
power-saver=powersave
balanced=balanced
performance=throughput-performance

View file

@ -0,0 +1,5 @@
[D-BUS Service]
Name=net.hadess.PowerProfiles
Exec=/bin/false
User=root
SystemdService=tuned-ppd.service

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>TuneD</vendor>
<vendor_url>https://tuned-project.org/</vendor_url>
<action id="net.hadess.PowerProfiles.HoldProfile">
<description>Hold power profile</description>
<message>Authentication is required to hold power profiles.</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
<action id="net.hadess.PowerProfiles.ReleaseProfile">
<description>Release power profile</description>
<message>Authentication is required to release power profiles.</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
</policyconfig>

View file

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