SafeEyes/safeeyes/plugin_manager.py

398 lines
14 KiB
Python
Raw Normal View History

2017-10-07 15:10:31 +02:00
#!/usr/bin/env python
# Safe Eyes is a utility to remind you to take break frequently
# to protect your eyes from eye strain.
# Copyright (C) 2017 Gobinath
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
PluginManager loads all enabled plugins and call their lifecycle methods.
A plugin must have the following directory structure:
<plugin-id>
|- config.json
|- plugin.py
|- icon.png (Optional)
The plugin.py can have following methods but all are optional:
- description()
If a custom description has to be displayed, use this function
2023-02-04 12:59:19 +01:00
- init(context, safeeyes_config, plugin_config)
2017-10-07 15:10:31 +02:00
Initialize the plugin. Will be called after loading and after every changes in configuration
- on_start()
Executes when Safe Eyes is enabled
- on_stop()
Executes when Safe Eyes is disabled
2023-02-04 12:59:19 +01:00
- on_exit()
Executes before Safe Eyes exits
- on_pre_break(break_obj)
Executes at the start of the prepare time for a break
- on_start_break(break_obj)
Executes when a break starts
- on_stop_break()
Executes when a break stops
- on_countdown(countdown, seconds)
Executes every second throughout a break
- update_next_break(break_obj, break_time)
Executes when the next break changes
2017-10-07 15:10:31 +02:00
- enable()
Executes once the plugin.py is loaded as a module
- disable()
Executes if the plugin is disabled at the runtime by the user
"""
import importlib
import logging
import os
import sys
2020-03-18 13:33:11 +01:00
from safeeyes import utility
from safeeyes.model import PluginDependency, RequiredPluginException
2017-10-07 15:10:31 +02:00
2020-03-18 13:33:11 +01:00
sys.path.append(os.path.abspath(utility.SYSTEM_PLUGINS_DIR))
sys.path.append(os.path.abspath(utility.USER_PLUGINS_DIR))
2017-10-07 15:10:31 +02:00
HORIZONTAL_LINE_LENGTH = 64
2020-03-18 13:33:11 +01:00
class PluginManager:
2017-10-07 15:10:31 +02:00
"""
Imports the Safe Eyes plugins and calls the methods defined in those plugins.
"""
2020-04-15 23:52:14 +02:00
def __init__(self):
2017-10-07 15:10:31 +02:00
logging.info('Load all the plugins')
self.__plugins = {}
self.last_break = None
self.horizontal_line = '' * HORIZONTAL_LINE_LENGTH
def init(self, context, config):
"""
2020-04-15 23:52:14 +02:00
Initialize all the plugins with init(context, safe_eyes_config, plugin_config) function.
2017-10-07 15:10:31 +02:00
"""
# Load the plugins
for plugin in config.get('plugins'):
try:
loaded_plugin = LoadedPlugin(plugin)
self.__plugins[loaded_plugin.id] = loaded_plugin
except RequiredPluginException as e:
raise e
except BaseException as e:
traceback_wanted = logging.getLogger().getEffectiveLevel() == logging.DEBUG
if traceback_wanted:
import traceback
traceback.print_exc()
logging.error('Error in loading the plugin %s: %s', plugin['id'], e)
2017-10-07 15:10:31 +02:00
continue
# Initialize the plugins
for plugin in self.__plugins.values():
plugin.init_plugin(context, config)
2017-10-07 15:10:31 +02:00
return True
def needs_retry(self):
return self.get_retryable_error() is not None
def get_retryable_error(self):
for plugin in self.__plugins.values():
if plugin.required_plugin and plugin.errored and plugin.enabled:
if isinstance(plugin.last_error, PluginDependency) and plugin.last_error.retryable:
return RequiredPluginException(
plugin.id,
plugin.get_name(),
plugin.last_error
)
return None
def retry_errored_plugins(self):
for plugin in self.__plugins.values():
if plugin.required_plugin and plugin.errored and plugin.enabled:
if isinstance(plugin.last_error, PluginDependency) and plugin.last_error.retryable:
plugin.reload_errored()
2017-10-07 15:10:31 +02:00
def start(self):
"""
Execute the on_start() function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method("on_start")
2017-10-07 15:10:31 +02:00
return True
def stop(self):
"""
Execute the on_stop() function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method("on_stop")
2017-10-07 15:10:31 +02:00
return True
def exit(self):
"""
Execute the on_exit() function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method("on_exit")
2017-10-07 15:10:31 +02:00
return True
def pre_break(self, break_obj):
"""
Execute the on_pre_break(break_obj) function of plugins.
"""
for plugin in self.__plugins.values():
if plugin.call_plugin_method_break_obj("on_pre_break", 1, break_obj):
return False
2017-10-07 15:10:31 +02:00
return True
def start_break(self, break_obj):
"""
Execute the start_break(break_obj) function of plugins.
"""
self.last_break = break_obj
for plugin in self.__plugins.values():
if plugin.call_plugin_method_break_obj("on_start_break", 1, break_obj):
return False
2017-10-07 15:10:31 +02:00
return True
def stop_break(self):
"""
Execute the stop_break() function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method("on_stop_break")
2017-10-07 15:10:31 +02:00
def countdown(self, countdown, seconds):
"""
Execute the on_countdown(countdown, seconds) function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method("on_countdown", 2, countdown, seconds)
2017-10-07 15:10:31 +02:00
def update_next_break(self, break_obj, break_time):
2017-10-07 15:10:31 +02:00
"""
Execute the update_next_break(break_time) function of plugins.
"""
for plugin in self.__plugins.values():
plugin.call_plugin_method_break_obj("update_next_break", 2, break_obj, break_time)
2017-10-07 15:10:31 +02:00
return True
def get_break_screen_widgets(self, break_obj):
"""
Return the HTML widget generated by the plugins.
The widget is generated by calling the get_widget_title and get_widget_content functions of plugins.
"""
widget = ''
for plugin in self.__plugins.values():
try:
title = plugin.call_plugin_method_break_obj("get_widget_title", 1, break_obj)
if title is None or not isinstance(title, str) or title == '':
continue
content = plugin.call_plugin_method_break_obj("get_widget_content", 1, break_obj)
if content is None or not isinstance(content, str) or content == '':
continue
title = title.upper().strip()
if title == '':
2017-10-07 15:10:31 +02:00
continue
widget += '<b>{}</b>\n{}\n{}\n\n\n'.format(title, self.horizontal_line, content)
except BaseException:
continue
2017-10-07 15:10:31 +02:00
return widget.strip()
def get_break_screen_tray_actions(self, break_obj):
"""
Return Tray Actions.
"""
actions = []
for plugin in self.__plugins.values():
action = plugin.call_plugin_method_break_obj("get_tray_action", 1, break_obj)
if action:
actions.append(action)
return actions
class LoadedPlugin:
# state of the plugin
enabled: bool = False
break_override_allowed: bool = False
errored: bool = False
required_plugin: bool = False
# misc data
# FIXME: rename to plugin_config to plugin_json? plugin_config and config are easy to confuse
config = None
plugin_config = None
plugin_dir = None
module = None
last_error = None
id = None
def __init__(self, plugin):
(plugin_config, plugin_dir) = self._load_config_json(plugin['id'])
self.id = plugin['id']
self.plugin_config = plugin_config
self.plugin_dir = plugin_dir
self.enabled = plugin["enabled"]
self.break_override_allowed = plugin_config.get('break_override_allowed', False)
self.required_plugin = plugin_config.get("required_plugin", False)
self.config = dict(plugin.get('settings', {}))
self.config['path'] = os.path.join(plugin_dir, plugin['id'])
if self.enabled or self.break_override_allowed:
plugin_path = os.path.join(plugin_dir, self.id)
message = utility.check_plugin_dependencies(plugin['id'], plugin_config, plugin.get('settings', {}), plugin_path)
if message:
self.errored = True
self.last_error = message
if self.required_plugin and not (isinstance(message, PluginDependency) and message.retryable):
raise RequiredPluginException(
plugin['id'],
plugin_config['meta']['name'],
message
)
return
self._import_plugin()
def reload_config(self, plugin):
if self.enabled and not plugin["enabled"]:
self.enabled = False
if not self.errored and utility.has_method(self.module, 'disable'):
self.module.disable()
if not self.enabled and plugin["enabled"]:
self.enabled = True
# Update the config
self.config = dict(plugin.get('settings', {}))
self.config['path'] = os.path.join(self.plugin_dir, plugin['id'])
if self.enabled or self.break_override_allowed:
plugin_path = os.path.join(self.plugin_dir, self.id)
message = utility.check_plugin_dependencies(
self.id,
self.plugin_config,
self.config,
plugin_path
)
if message:
self.errored = True
self.last_error = message
elif self.errored:
self.errored = False
self.last_error = None
if not self.errored and self.module is None:
# No longer errored, import the module now
self._import_plugin()
def reload_errored(self):
if not self.errored:
return
if self.enabled or self.break_override_allowed:
plugin_path = os.path.join(self.plugin_dir, self.id)
message = utility.check_plugin_dependencies(
self.id,
self.plugin_config,
self.config,
plugin_path
)
if message:
self.errored = True
self.last_error = message
elif self.errored:
self.errored = False
self.last_error = None
if not self.errored and self.module is None:
# No longer errored, import the module now
self._import_plugin()
def get_name(self):
return self.plugin_config['meta']['name']
def _import_plugin(self):
if self.errored:
# do not try to import errored plugin
2017-10-07 15:10:31 +02:00
return
self.module = importlib.import_module((self.id + '.plugin'))
logging.info("Successfully loaded %s", str(self.module))
if utility.has_method(self.module, 'enable'):
self.module.enable()
def _load_config_json(self, plugin_id):
2017-10-07 15:10:31 +02:00
# Look for plugin.py
if os.path.isfile(os.path.join(utility.SYSTEM_PLUGINS_DIR, plugin_id, 'plugin.py')):
2020-03-18 13:33:11 +01:00
plugin_dir = utility.SYSTEM_PLUGINS_DIR
elif os.path.isfile(os.path.join(utility.USER_PLUGINS_DIR, plugin_id, 'plugin.py')):
2020-03-18 13:33:11 +01:00
plugin_dir = utility.USER_PLUGINS_DIR
2017-10-07 15:10:31 +02:00
else:
raise Exception('plugin.py not found for the plugin: %s', plugin_id)
2017-10-07 15:10:31 +02:00
# Look for config.json
plugin_path = os.path.join(plugin_dir, plugin_id)
plugin_config_path = os.path.join(plugin_path, 'config.json')
2017-10-07 15:10:31 +02:00
if not os.path.isfile(plugin_config_path):
raise Exception('config.json not found for the plugin: %s', plugin_id)
2020-03-18 13:33:11 +01:00
plugin_config = utility.load_json(plugin_config_path)
2017-10-07 15:10:31 +02:00
if plugin_config is None:
raise Exception('config.json empty/invalid for the plugin: %s', plugin_id)
return (plugin_config, plugin_dir)
def init_plugin(self, context, safeeyes_config):
if self.errored:
2017-10-07 15:10:31 +02:00
return
if self.break_override_allowed or self.enabled:
if utility.has_method(self.module, 'init', 3):
self.module.init(context, safeeyes_config, self.config)
2017-10-07 15:10:31 +02:00
def call_plugin_method_break_obj(self, method_name: str, num_args, break_obj, *args, **kwargs):
if self.errored:
return None
enabled = False
if self.break_override_allowed:
enabled = break_obj.plugin_enabled(self.id, self.enabled)
else:
enabled = self.enabled
if enabled:
return self._call_plugin_method_internal(method_name, num_args, break_obj, *args, **kwargs)
return None
def call_plugin_method(self, method_name: str, num_args=0, *args, **kwargs):
if self.errored:
return None
if self.enabled:
return self._call_plugin_method_internal(method_name, num_args, *args, **kwargs)
return None
def _call_plugin_method_internal(self, method_name: str, num_args=0, *args, **kwargs):
# FIXME: cache if method exists
if utility.has_method(self.module, method_name, num_args):
return getattr(self.module, method_name)(*args, **kwargs)
return None