관리-도구
편집 파일: controller.py
from tuned import exports, logs from tuned.utils.commands import commands from tuned.consts import PPD_CONFIG_FILE, PPD_BASE_PROFILE_FILE, PPD_API_COMPATIBILITY from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_BALANCED, PPD_POWER_SAVER from enum import Enum from random import Random import pyinotify import threading import dbus import os import time 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" UNKNOWN_PROFILE = "unknown" UPOWER_DBUS_NAME = "org.freedesktop.UPower" UPOWER_DBUS_PATH = "/org/freedesktop/UPower" UPOWER_DBUS_INTERFACE = "org.freedesktop.UPower" PLATFORM_PROFILE_PATH = "/sys/firmware/acpi/platform_profile" PLATFORM_PROFILE_MAPPING = { "low-power": PPD_POWER_SAVER, "balanced": PPD_BALANCED, "performance": PPD_PERFORMANCE } class PerformanceDegraded(Enum): """ Possible reasons for performance degradation. """ NONE = "" LAP_DETECTED = "lap-detected" HIGH_OPERATING_TEMPERATURE = "high-operating-temperature" class PerformanceDegradedEventHandler(pyinotify.ProcessEvent): """ Event handler for checking performance degradation. """ def __init__(self, controller, path): super(PerformanceDegradedEventHandler, self).__init__() self._controller = controller self._path = path def process_IN_MODIFY(self, event): if event.pathname != self._path: return self._controller.check_performance_degraded() class PlatformProfileEventHandler(pyinotify.ProcessEvent): """ Event handler for switching PPD profiles based on the ACPI platform profile This handler should only invoke a PPD profile change if the change of the file at PLATFORM_PROFILE_PATH comes from within the kernel (e.g., when the user presses Fn-L on a Thinkpad laptop). This is currently detected as the file being modified without being opened before. """ CLOSE_MODIFY_BUFFER = 0.1 def __init__(self, controller): super(PlatformProfileEventHandler, self).__init__() self._controller = controller self._file_open = False self._last_close = 0 def process_IN_OPEN(self, event): if event.pathname != PLATFORM_PROFILE_PATH: return self._file_open = True self._last_close = 0 def process_IN_CLOSE_WRITE(self, event): if event.pathname != PLATFORM_PROFILE_PATH: return self._file_open = False self._last_close = time.time() def process_IN_CLOSE_NOWRITE(self, event): if event.pathname != PLATFORM_PROFILE_PATH: return self._file_open = False def process_IN_MODIFY(self, event): if event.pathname != PLATFORM_PROFILE_PATH or self._file_open or self._last_close + self.CLOSE_MODIFY_BUFFER > time.time(): # Do not invoke a profile change if a modify event comes: # 1. when the file is open, # 2. directly after the file is closed (the events may sometimes come in the wrong order). return self._controller.check_platform_profile() class ProfileHold(object): """ Class holding information about a single profile hold, i.e., a temporary profile switch requested by a process. """ def __init__(self, profile, reason, app_id, caller, watch): self.profile = profile self.reason = reason self.app_id = app_id self.caller = caller self.watch = watch def as_dict(self): """ Returns the hold information as a Python dictionary. """ return { "Profile": self.profile, "Reason": self.reason, "ApplicationId": self.app_id, } class ProfileHoldManager(object): """ Manager of profile holds responsible for their creation/deletion and for choosing the effective one. Holds are identified using integer cookies which are distributed to the hold-requesting processes. """ def __init__(self, controller): self._holds = {} self._cookie_generator = Random() self._controller = controller def _removal_callback(self, cookie, app_id): """ Returns the callback to invoke when the process with the given ID (which requested a hold with the given cookie) disappears. """ 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): """ Returns the hold to use from the set of all active ones. """ if any(hold.profile == PPD_POWER_SAVER for hold in self._holds.values()): return PPD_POWER_SAVER return PPD_PERFORMANCE def _cancel(self, cookie): """ Cancels the hold saved under the provided 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): """ Returns the information about current holds as a DBus-compatible array. """ return dbus.Array([hold.as_dict() for hold in self._holds.values()], signature="a{sv}") def add(self, profile, reason, app_id, caller): """ Adds a new profile hold. """ cookie = 0 while cookie == 0 or cookie in self._holds: cookie = self._cookie_generator.randint(0, 2**32-1) watch = self._controller.bus.watch_name_owner(caller, self._removal_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, caller, watch) exports.property_changed("ActiveProfileHolds", self.as_dbus_array()) self._controller.switch_profile(self._effective_hold_profile()) return cookie def has(self, cookie): """ Returns True if there is a hold under the given cookie. """ return cookie in self._holds def remove(self, cookie): """ Releases the hold saved under the provided cookie and sets the next profile. """ 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): """ Releases all profile holds. """ for cookie in list(self._holds.keys()): self._cancel(cookie) def check_caller(self, cookie, caller): return cookie in self._holds and self._holds[cookie].caller == caller class Controller(exports.interfaces.ExportableInterface): """ The main tuned-ppd controller, exporting its DBus interface. """ def __init__(self, bus, tuned_interface): super(Controller, self).__init__() self._bus = bus self._tuned_interface = tuned_interface self._cmd = commands() self._terminate = threading.Event() self._battery_handler = None self._on_battery = False self._watch_manager = pyinotify.WatchManager() self._notifier = pyinotify.ThreadedNotifier(self._watch_manager) self._inotify_watches = {} self._platform_profile_supported = os.path.isfile(PLATFORM_PROFILE_PATH) self._no_turbo_supported = os.path.isfile(NO_TURBO_PATH) self._lap_mode_supported = os.path.isfile(LAP_MODE_PATH) self._tuned_interface.connect_to_signal("profile_changed", self._tuned_profile_changed) self.initialize() def _upower_changed(self, interface, changed, invalidated): """ The callback to invoke when the power supply changes. """ self._on_battery = bool(self._upower_properties.Get(UPOWER_DBUS_INTERFACE, "OnBattery")) log.info("Battery status changed: " + ("DC (battery)" if self._on_battery else "AC (charging)")) self.switch_profile(self._active_profile) def _tuned_profile_changed(self, tuned_profile, result, errstr): """ The callback to invoke when TuneD signals a profile change. """ if not result: return if tuned_profile != self._tuned_interface.active_profile(): log.debug("Received a profile change signal from TuneD, but it is not relevant anymore.") return try: ppd_profile = self._config.tuned_to_ppd.get(tuned_profile, self._on_battery) except KeyError: ppd_profile = UNKNOWN_PROFILE log.warning("TuneD profile changed to an unknown profile '%s'" % tuned_profile) if self._active_profile != ppd_profile: log.info("Profile changed to '%s'" % ppd_profile) self._profile_holds.clear() self._active_profile = ppd_profile exports.property_changed("ActiveProfile", self._active_profile) if ppd_profile != UNKNOWN_PROFILE: self._base_profile = ppd_profile self._save_base_profile(ppd_profile) def _setup_battery_signaling(self): """ Sets up handling of power supply changes. """ self._on_battery = False if not self._config.battery_detection: if self._battery_handler is not None: self._battery_handler.remove() self._battery_handler = None return try: if self._battery_handler is None: upower_proxy = self._bus.get_object(UPOWER_DBUS_NAME, UPOWER_DBUS_PATH) self._upower_properties = dbus.Interface(upower_proxy, dbus.PROPERTIES_IFACE) self._battery_handler = upower_proxy.connect_to_signal("PropertiesChanged", self._upower_changed) self._on_battery = bool(self._upower_properties.Get(UPOWER_DBUS_INTERFACE, "OnBattery")) except dbus.exceptions.DBusException as error: log.debug(error) def _setup_inotify(self): """ Sets up inotify file watches. """ self._watch_manager.rm_watch(list(self._inotify_watches.values())) if self._no_turbo_supported: self._inotify_watches |= self._watch_manager.add_watch(path=os.path.dirname(NO_TURBO_PATH), mask=pyinotify.IN_MODIFY, proc_fun=PerformanceDegradedEventHandler(NO_TURBO_PATH, self)) if self._lap_mode_supported: self._inotify_watches |= self._watch_manager.add_watch(path=os.path.dirname(LAP_MODE_PATH), mask=pyinotify.IN_MODIFY, proc_fun=PerformanceDegradedEventHandler(LAP_MODE_PATH, self)) if self._platform_profile_supported and self._config.thinkpad_function_keys: self._inotify_watches |= self._watch_manager.add_watch(path=os.path.dirname(PLATFORM_PROFILE_PATH), mask=pyinotify.IN_OPEN | pyinotify.IN_MODIFY | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CLOSE_NOWRITE, proc_fun=PlatformProfileEventHandler(self)) def check_performance_degraded(self): """ Checks the current performance degradation status and sends a signal if it changed. """ performance_degraded = PerformanceDegraded.NONE if os.path.exists(NO_TURBO_PATH) and self._cmd.read_file(NO_TURBO_PATH).strip() == "1": performance_degraded = PerformanceDegraded.HIGH_OPERATING_TEMPERATURE if os.path.exists(LAP_MODE_PATH) and self._cmd.read_file(LAP_MODE_PATH).strip() == "1": performance_degraded = PerformanceDegraded.LAP_DETECTED if performance_degraded != self._performance_degraded: log.info("Performance degraded: %s" % performance_degraded.value) self._performance_degraded = performance_degraded exports.property_changed("PerformanceDegraded", performance_degraded.value) def check_platform_profile(self): """ Sets the active PPD profile based on the content of the ACPI platform profile. """ platform_profile = self._cmd.read_file(PLATFORM_PROFILE_PATH).strip() if platform_profile not in PLATFORM_PROFILE_MAPPING: return log.debug("Platform profile changed: %s" % platform_profile) new_profile = PLATFORM_PROFILE_MAPPING[platform_profile] self._profile_holds.clear() self.switch_profile(new_profile) self._base_profile = new_profile self._save_base_profile(new_profile) def _load_base_profile(self): """ Loads and returns the saved PPD base profile. """ return self._cmd.read_file(PPD_BASE_PROFILE_FILE, no_error=True).strip() or None def _save_base_profile(self, profile): """ Saves the given PPD profile into the base profile file. """ self._cmd.write_to_file(PPD_BASE_PROFILE_FILE, profile + "\n") def _set_tuned_profile(self, tuned_profile): """ Sets the TuneD profile to the given one if not already set. """ active_tuned_profile = self._tuned_interface.active_profile() if active_tuned_profile == tuned_profile: return True log.info("Setting TuneD profile to '%s'" % tuned_profile) ok, error_msg = self._tuned_interface.switch_profile(tuned_profile) if not ok: log.error(str(error_msg)) return bool(ok) def initialize(self): """ Initializes the controller. """ self._active_profile = None self._profile_holds = ProfileHoldManager(self) self._performance_degraded = PerformanceDegraded.NONE self.check_performance_degraded() self._config = PPDConfig(PPD_CONFIG_FILE, self._tuned_interface) self._setup_battery_signaling() self._base_profile = self._load_base_profile() or self._config.default_profile self.switch_profile(self._base_profile) self._save_base_profile(self._base_profile) self._setup_inotify() def run(self): """ Exports the DBus interface and runs the main daemon loop. """ exports.start() self._notifier.start() while not self._cmd.wait(self._terminate, 1): pass self._watch_manager.rm_watch(list(self._inotify_watches.values())) self._notifier.stop() exports.stop() @property def bus(self): """ DBus interface for communication with other services. """ return self._bus @property def base_profile(self): """ The base PPD profile. This is the profile to restore when all profile holds are released or when tuned-ppd is restarted. It may not be equal to the currently active profile. """ return self._base_profile def terminate(self): """ Stops the main loop of the daemon. """ self._terminate.set() def switch_profile(self, profile): """ Sets the currently active profile to the given one, if not already set. Does not change the base profile. """ if not self._set_tuned_profile(self._config.ppd_to_tuned.get(profile, self._on_battery)): return False if self._active_profile != profile: exports.property_changed("ActiveProfile", profile) self._active_profile = profile return True @exports.export("sss", "u", "hold-profile") def HoldProfile(self, profile, reason, app_id, caller): """ Initiates a profile hold and returns a cookie for referring to it. """ 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", "", "release-profile") def ReleaseProfile(self, cookie, caller): """ Releases a held profile with the given cookie. """ if not self._profile_holds.has(cookie): raise dbus.exceptions.DBusException("No active hold for cookie '%s'" % cookie) if not self._profile_holds.check_caller(cookie, caller): raise dbus.exceptions.DBusException("Cannot release a profile hold inititated by another process.") self._profile_holds.remove(cookie) @exports.signal("u") def ProfileReleased(self, cookie): """ The DBus signal sent when a held profile is released. """ pass @exports.property_setter("ActiveProfile", "switch-profile") def set_active_profile(self, profile, caller): """ Sets the base profile to the given one and also makes it active. If there are any active profile holds, these are cancelled. """ if profile not in self._config.ppd_to_tuned.keys(self._on_battery): raise dbus.exceptions.DBusException("Invalid profile '%s'" % profile) log.debug("Setting base profile to %s" % profile) self._profile_holds.clear() if not self.switch_profile(profile): raise dbus.exceptions.DBusException("Error setting profile %s'" % profile) self._base_profile = profile self._save_base_profile(profile) @exports.property_getter("ActiveProfile") def get_active_profile(self, caller): """ Returns the currently active PPD profile. """ return self._active_profile @exports.property_getter("Profiles") def get_profiles(self, caller): """ Returns a DBus array of all available PPD profiles. """ return dbus.Array( [{"Profile": profile, "Driver": DRIVER} for profile in self._config.ppd_to_tuned.keys(self._on_battery)], signature="a{sv}", ) @exports.property_getter("Actions") def get_actions(self, caller): """ Returns a DBus array of all available actions (currently there are none). """ return dbus.Array([], signature="s") @exports.property_getter("PerformanceDegraded") def get_performance_degraded(self, caller): """ Returns the current performance degradation status. """ return self._performance_degraded.value @exports.property_getter("ActiveProfileHolds") def get_active_profile_holds(self, caller): """ Returns a DBus array of active profile holds. """ return self._profile_holds.as_dbus_array() @exports.property_getter("Version") def version(self, caller): return PPD_API_COMPATIBILITY