xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/plugin_mixin.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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