398 lines
14 KiB
Python
398 lines
14 KiB
Python
#!/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
|
|
- init(context, safeeyes_config, plugin_config)
|
|
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
|
|
- 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
|
|
- 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
|
|
|
|
from safeeyes import utility
|
|
from safeeyes.model import PluginDependency, RequiredPluginException
|
|
|
|
sys.path.append(os.path.abspath(utility.SYSTEM_PLUGINS_DIR))
|
|
sys.path.append(os.path.abspath(utility.USER_PLUGINS_DIR))
|
|
|
|
HORIZONTAL_LINE_LENGTH = 64
|
|
|
|
class PluginManager:
|
|
"""
|
|
Imports the Safe Eyes plugins and calls the methods defined in those plugins.
|
|
"""
|
|
|
|
def __init__(self):
|
|
logging.info('Load all the plugins')
|
|
self.__plugins = {}
|
|
self.last_break = None
|
|
self.horizontal_line = '─' * HORIZONTAL_LINE_LENGTH
|
|
|
|
def init(self, context, config):
|
|
"""
|
|
Initialize all the plugins with init(context, safe_eyes_config, plugin_config) function.
|
|
"""
|
|
# 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)
|
|
continue
|
|
# Initialize the plugins
|
|
for plugin in self.__plugins.values():
|
|
plugin.init_plugin(context, config)
|
|
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()
|
|
|
|
def start(self):
|
|
"""
|
|
Execute the on_start() function of plugins.
|
|
"""
|
|
for plugin in self.__plugins.values():
|
|
plugin.call_plugin_method("on_start")
|
|
return True
|
|
|
|
def stop(self):
|
|
"""
|
|
Execute the on_stop() function of plugins.
|
|
"""
|
|
for plugin in self.__plugins.values():
|
|
plugin.call_plugin_method("on_stop")
|
|
return True
|
|
|
|
def exit(self):
|
|
"""
|
|
Execute the on_exit() function of plugins.
|
|
"""
|
|
for plugin in self.__plugins.values():
|
|
plugin.call_plugin_method("on_exit")
|
|
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
|
|
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
|
|
|
|
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")
|
|
|
|
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)
|
|
|
|
def update_next_break(self, break_obj, break_time):
|
|
"""
|
|
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)
|
|
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 == '':
|
|
continue
|
|
widget += '<b>{}</b>\n{}\n{}\n\n\n'.format(title, self.horizontal_line, content)
|
|
except BaseException:
|
|
continue
|
|
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
|
|
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):
|
|
# Look for plugin.py
|
|
if os.path.isfile(os.path.join(utility.SYSTEM_PLUGINS_DIR, plugin_id, 'plugin.py')):
|
|
plugin_dir = utility.SYSTEM_PLUGINS_DIR
|
|
elif os.path.isfile(os.path.join(utility.USER_PLUGINS_DIR, plugin_id, 'plugin.py')):
|
|
plugin_dir = utility.USER_PLUGINS_DIR
|
|
else:
|
|
raise Exception('plugin.py not found for the plugin: %s', plugin_id)
|
|
# Look for config.json
|
|
plugin_path = os.path.join(plugin_dir, plugin_id)
|
|
plugin_config_path = os.path.join(plugin_path, 'config.json')
|
|
if not os.path.isfile(plugin_config_path):
|
|
raise Exception('config.json not found for the plugin: %s', plugin_id)
|
|
plugin_config = utility.load_json(plugin_config_path)
|
|
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:
|
|
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)
|
|
|
|
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
|