1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Window pane base class.""" 15 16import asyncio 17import logging 18from threading import Thread 19import time 20from typing import Callable 21 22from pw_console.get_pw_console_app import get_pw_console_app 23 24 25class PluginMixin: 26 """Handles background task management in a Pigweed Console plugin. 27 28 Pigweed Console plugins can inherit from this class if they require running 29 tasks in the background. This is important as any plugin code not in its 30 own dedicated thread can potentially block the user interface 31 32 Example usage: :: 33 34 import logging 35 from pw_console.plugin_mixin import PluginMixin 36 from pw_console.widgets import WindowPaneToolbar 37 38 class AwesomeToolbar(WindowPaneToolbar, PluginMixin): 39 TOOLBAR_HEIGHT = 1 40 41 def __init__(self, *args, **kwargs): 42 # Call parent class WindowPaneToolbar.__init__ 43 super().__init__(*args, **kwargs) 44 45 # Set PluginMixin to execute 46 # self._awesome_background_task every 10 seconds. 47 self.plugin_init( 48 plugin_callback=self._awesome_background_task, 49 plugin_callback_frequency=10.0, 50 plugin_logger_name='awesome_toolbar_plugin') 51 52 # This function will be run in a separate thread every 10 seconds. 53 def _awesome_background_task(self) -> bool: 54 time.sleep(1) # Do real work here. 55 56 if self.new_data_processed: 57 # If new data was processed, and the user interface 58 # should be updated return True. 59 60 # Log using self.plugin_logger for debugging. 61 self.plugin_logger.debug('New data processed') 62 63 # Return True to signal a UI redraw. 64 return True 65 66 # Returning False means no updates needed. 67 return False 68 69 Attributes: 70 plugin_callback: Callable that is run in a background thread. 71 plugin_callback_frequency: Number of seconds to wait between 72 executing plugin_callback. 73 plugin_logger: logging instance for this plugin. Useful for debugging 74 code running in a separate thread. 75 plugin_callback_future: `Future`_ object for the plugin background task. 76 plugin_event_loop: asyncio event loop running in the background thread. 77 plugin_enable_background_task: If True, keep periodically running 78 plugin_callback at the desired frequency. If False the background 79 task will stop. 80 81 .. _Future: https://docs.python.org/3/library/asyncio-future.html 82 """ 83 84 def plugin_init( 85 self, 86 plugin_callback: Callable[..., bool] | None = None, 87 plugin_callback_frequency: float = 30.0, 88 plugin_logger_name: str | None = 'pw_console_plugins', 89 ) -> None: 90 """Call this on __init__() to set plugin background task variables. 91 92 Args: 93 plugin_callback: Callable to run in a separate thread from the 94 Pigweed Console UI. This function should return True if the UI 95 should be redrawn after execution. 96 plugin_callback_frequency: Number of seconds to wait between 97 executing plugin_callback. 98 plugin_logger_name: Unique name for this plugin's Python 99 logger. Useful for debugging code running in a separate thread. 100 """ 101 self.plugin_callback = plugin_callback 102 self.plugin_callback_frequency = plugin_callback_frequency 103 self.plugin_logger = logging.getLogger(plugin_logger_name) 104 105 self.plugin_callback_future = None 106 107 # Event loop for executing plugin code. 108 self.plugin_event_loop = asyncio.new_event_loop() 109 self.plugin_enable_background_task = True 110 111 def plugin_start(self): 112 """Function used to start this plugin's background thead and task.""" 113 114 # Create an entry point for the plugin thread. 115 def _plugin_thread_entry(): 116 # Disable log propagation 117 self.plugin_logger.propagate = False 118 asyncio.set_event_loop(self.plugin_event_loop) 119 self.plugin_event_loop.run_forever() 120 121 # Create a thread for running user code so the UI isn't blocked. 122 thread = Thread(target=_plugin_thread_entry, args=(), daemon=True) 123 thread.start() 124 125 self.plugin_logger.debug('Starting plugin: %s', self) 126 if self.plugin_callback is None: 127 return 128 129 self.plugin_enable_background_task = True 130 self.plugin_callback_future = asyncio.run_coroutine_threadsafe( 131 # This function will be executed in a separate thread. 132 self._plugin_periodically_run_callback(), 133 # Using this asyncio event loop. 134 self.plugin_event_loop, 135 ) # type: ignore 136 137 def plugin_stop(self): 138 self.plugin_enable_background_task = False 139 140 async def _plugin_periodically_run_callback(self) -> None: 141 while self.plugin_enable_background_task: 142 start_time = time.time() 143 # Run the callback and redraw the UI if return value is True 144 if self.plugin_callback and self.plugin_callback(): 145 get_pw_console_app().redraw_ui() 146 run_time = time.time() - start_time 147 await asyncio.sleep(self.plugin_callback_frequency - run_time) 148