Skip to content

Commit 7978bce

Browse files
authored
Merge pull request slgobinath#807 from deltragon/fix-disable-trayicon-plugin
trayicon: fix removing trayicon when plugin is disabled
2 parents b1d878b + a63dbc3 commit 7978bce

File tree

3 files changed

+128
-46
lines changed

3 files changed

+128
-46
lines changed

safeeyes/plugin_manager.py

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,17 @@
6868
import logging
6969
import os
7070
import sys
71+
import typing
7172

7273
from safeeyes import utility
73-
from safeeyes.model import Break, PluginDependency, RequiredPluginException, TrayAction
74+
from safeeyes.context import Context
75+
from safeeyes.model import (
76+
Config,
77+
Break,
78+
PluginDependency,
79+
RequiredPluginException,
80+
TrayAction,
81+
)
7482

7583
sys.path.append(os.path.abspath(utility.SYSTEM_PLUGINS_DIR))
7684
sys.path.append(os.path.abspath(utility.USER_PLUGINS_DIR))
@@ -81,13 +89,16 @@
8189
class PluginManager:
8290
"""Imports the Safe Eyes plugins and calls the methods defined in those plugins."""
8391

84-
def __init__(self):
92+
__plugins: dict[str, "LoadedPlugin"]
93+
last_break: typing.Optional[Break]
94+
95+
def __init__(self) -> None:
8596
logging.info("Load all the plugins")
8697
self.__plugins = {}
8798
self.last_break = None
8899
self.horizontal_line = "─" * HORIZONTAL_LINE_LENGTH
89100

90-
def init(self, context, config):
101+
def init(self, context: Context, config: Config) -> None:
91102
"""Initialize all the plugins with init(context, safe_eyes_config,
92103
plugin_config) function.
93104
"""
@@ -111,12 +122,43 @@ def init(self, context, config):
111122
# Initialize the plugins
112123
for plugin in self.__plugins.values():
113124
plugin.init_plugin(context, config)
114-
return True
115125

116-
def needs_retry(self):
126+
def reload(self, context: Context, config: Config) -> None:
127+
"""Reinitialize all the plugins with updated config."""
128+
plugin_ids: set[str] = set()
129+
# Load the plugins
130+
for plugin in config.get("plugins"):
131+
plugin_id = plugin["id"]
132+
plugin_ids.add(plugin_id)
133+
if plugin_id in self.__plugins:
134+
self.__plugins[plugin_id].reload_config(plugin)
135+
else:
136+
try:
137+
loaded_plugin = LoadedPlugin(plugin)
138+
self.__plugins[loaded_plugin.id] = loaded_plugin
139+
except BaseException as e:
140+
traceback_wanted = (
141+
logging.getLogger().getEffectiveLevel() == logging.DEBUG
142+
)
143+
if traceback_wanted:
144+
import traceback
145+
146+
traceback.print_exc()
147+
logging.error("Error in loading the plugin %s: %s", plugin["id"], e)
148+
continue
149+
150+
removed_plugins = set(self.__plugins.keys()).difference(plugin_ids)
151+
for plugin_id in removed_plugins:
152+
self.__plugins[plugin_id].disable()
153+
154+
# Initialize the plugins
155+
for plugin in self.__plugins.values():
156+
plugin.init_plugin(context, config)
157+
158+
def needs_retry(self) -> bool:
117159
return self.get_retryable_error() is not None
118160

119-
def get_retryable_error(self):
161+
def get_retryable_error(self) -> typing.Optional[RequiredPluginException]:
120162
for plugin in self.__plugins.values():
121163
if plugin.required_plugin and plugin.errored and plugin.enabled:
122164
if (
@@ -129,7 +171,7 @@ def get_retryable_error(self):
129171

130172
return None
131173

132-
def retry_errored_plugins(self):
174+
def retry_errored_plugins(self) -> None:
133175
for plugin in self.__plugins.values():
134176
if plugin.required_plugin and plugin.errored and plugin.enabled:
135177
if (
@@ -138,32 +180,29 @@ def retry_errored_plugins(self):
138180
):
139181
plugin.reload_errored()
140182

141-
def start(self):
183+
def start(self) -> None:
142184
"""Execute the on_start() function of plugins."""
143185
for plugin in self.__plugins.values():
144186
plugin.call_plugin_method("on_start")
145-
return True
146187

147-
def stop(self):
188+
def stop(self) -> None:
148189
"""Execute the on_stop() function of plugins."""
149190
for plugin in self.__plugins.values():
150191
plugin.call_plugin_method("on_stop")
151-
return True
152192

153-
def exit(self):
193+
def exit(self) -> None:
154194
"""Execute the on_exit() function of plugins."""
155195
for plugin in self.__plugins.values():
156196
plugin.call_plugin_method("on_exit")
157-
return True
158197

159-
def pre_break(self, break_obj):
198+
def pre_break(self, break_obj) -> bool:
160199
"""Execute the on_pre_break(break_obj) function of plugins."""
161200
for plugin in self.__plugins.values():
162201
if plugin.call_plugin_method_break_obj("on_pre_break", 1, break_obj):
163202
return False
164203
return True
165204

166-
def start_break(self, break_obj):
205+
def start_break(self, break_obj) -> bool:
167206
"""Execute the start_break(break_obj) function of plugins."""
168207
self.last_break = break_obj
169208
for plugin in self.__plugins.values():
@@ -172,25 +211,24 @@ def start_break(self, break_obj):
172211

173212
return True
174213

175-
def stop_break(self):
214+
def stop_break(self) -> None:
176215
"""Execute the stop_break() function of plugins."""
177216
for plugin in self.__plugins.values():
178217
plugin.call_plugin_method("on_stop_break")
179218

180-
def countdown(self, countdown, seconds):
219+
def countdown(self, countdown, seconds) -> None:
181220
"""Execute the on_countdown(countdown, seconds) function of plugins."""
182221
for plugin in self.__plugins.values():
183222
plugin.call_plugin_method("on_countdown", 2, countdown, seconds)
184223

185-
def update_next_break(self, break_obj, break_time):
224+
def update_next_break(self, break_obj, break_time) -> None:
186225
"""Execute the update_next_break(break_time) function of plugins."""
187226
for plugin in self.__plugins.values():
188227
plugin.call_plugin_method_break_obj(
189228
"update_next_break", 2, break_obj, break_time
190229
)
191-
return True
192230

193-
def get_break_screen_widgets(self, break_obj):
231+
def get_break_screen_widgets(self, break_obj) -> str:
194232
"""Return the HTML widget generated by the plugins.
195233
196234
The widget is generated by calling the get_widget_title and
@@ -246,14 +284,14 @@ class LoadedPlugin:
246284
# misc data
247285
# FIXME: rename to plugin_config to plugin_json? plugin_config and config are easy
248286
# to confuse
249-
config = None
250-
plugin_config = None
251-
plugin_dir = None
252-
module = None
253-
last_error = None
254-
id = None
255-
256-
def __init__(self, plugin):
287+
config: dict
288+
plugin_config: dict
289+
plugin_dir: str
290+
module: typing.Optional[typing.Any] = None
291+
last_error: typing.Optional[typing.Union[str, PluginDependency]] = None
292+
id: str
293+
294+
def __init__(self, plugin: dict) -> None:
257295
(plugin_config, plugin_dir) = self._load_config_json(plugin["id"])
258296

259297
self.id = plugin["id"]
@@ -285,11 +323,10 @@ def __init__(self, plugin):
285323

286324
self._import_plugin()
287325

288-
def reload_config(self, plugin):
289-
if self.enabled and not plugin["enabled"]:
290-
self.enabled = False
291-
if not self.errored and utility.has_method(self.module, "disable"):
292-
self.module.disable()
326+
def reload_config(self, plugin: dict) -> None:
327+
if not plugin["enabled"]:
328+
self.disable()
329+
return
293330

294331
if not self.enabled and plugin["enabled"]:
295332
self.enabled = True
@@ -315,7 +352,18 @@ def reload_config(self, plugin):
315352
# No longer errored, import the module now
316353
self._import_plugin()
317354

318-
def reload_errored(self):
355+
def disable(self) -> None:
356+
if self.enabled:
357+
self.enabled = False
358+
if (
359+
not self.errored
360+
and self.module is not None
361+
and utility.has_method(self.module, "disable")
362+
):
363+
self.module.disable()
364+
logging.info("Successfully unloaded the plugin '%s'", self.id)
365+
366+
def reload_errored(self) -> None:
319367
if not self.errored:
320368
return
321369

@@ -336,10 +384,10 @@ def reload_errored(self):
336384
# No longer errored, import the module now
337385
self._import_plugin()
338386

339-
def get_name(self):
387+
def get_name(self) -> str:
340388
return self.plugin_config["meta"]["name"]
341389

342-
def _import_plugin(self):
390+
def _import_plugin(self) -> None:
343391
if self.errored:
344392
# do not try to import errored plugin
345393
return
@@ -350,7 +398,7 @@ def _import_plugin(self):
350398
if utility.has_method(self.module, "enable"):
351399
self.module.enable()
352400

353-
def _load_config_json(self, plugin_id):
401+
def _load_config_json(self, plugin_id: str) -> typing.Tuple[dict, str]:
354402
# Look for plugin.py
355403
if os.path.isfile(
356404
os.path.join(utility.SYSTEM_PLUGINS_DIR, plugin_id, "plugin.py")
@@ -373,16 +421,16 @@ def _load_config_json(self, plugin_id):
373421

374422
return (plugin_config, plugin_dir)
375423

376-
def init_plugin(self, context, safeeyes_config):
424+
def init_plugin(self, context: Context, safeeyes_config: Config) -> None:
377425
if self.errored:
378426
return
379427
if self.break_override_allowed or self.enabled:
380-
if utility.has_method(self.module, "init", 3):
428+
if self.module is not None and utility.has_method(self.module, "init", 3):
381429
self.module.init(context, safeeyes_config, self.config)
382430

383431
def call_plugin_method_break_obj(
384432
self, method_name: str, num_args, break_obj, *args, **kwargs
385-
):
433+
) -> typing.Any:
386434
if self.errored:
387435
return None
388436

@@ -399,7 +447,9 @@ def call_plugin_method_break_obj(
399447

400448
return None
401449

402-
def call_plugin_method(self, method_name: str, num_args=0, *args, **kwargs):
450+
def call_plugin_method(
451+
self, method_name: str, num_args=0, *args, **kwargs
452+
) -> typing.Any:
403453
if self.errored:
404454
return None
405455

@@ -412,7 +462,7 @@ def call_plugin_method(self, method_name: str, num_args=0, *args, **kwargs):
412462

413463
def _call_plugin_method_internal(
414464
self, method_name: str, num_args=0, *args, **kwargs
415-
):
465+
) -> typing.Any:
416466
# FIXME: cache if method exists
417467
if utility.has_method(self.module, method_name, num_args):
418468
return getattr(self.module, method_name)(*args, **kwargs)

safeeyes/plugins/trayicon/plugin.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
Safe Eyes tray icon plugin
3333
"""
3434

35-
tray_icon = None
35+
tray_icon: typing.Optional["TrayIcon"] = None
3636
safeeyes_config = None
3737

3838
SNI_NODE_INFO = Gio.DBusNodeInfo.new_for_xml(
@@ -405,6 +405,14 @@ def register(self):
405405
cancellable=None,
406406
)
407407

408+
# Note that according to the (freedesktop) spec, we should own the name
409+
# org.freedesktop.StatusNotifierItem-PID-ID and pass that to the watcher
410+
# instead
411+
# with the path being hardcoded at /StatusNotifierItem
412+
# The spec behaviour is worse for flatpak, however, as it requires owning a
413+
# pretty generic name.
414+
# Note that libappindicator/ayatana also used this non-standard behaviour -
415+
# this must be pretty well supported then.
408416
watcher.RegisterStatusNotifierItem("(s)", self.DBUS_SERVICE_PATH)
409417

410418
def unregister(self):
@@ -441,6 +449,8 @@ class TrayIcon:
441449

442450
_resume_timeout_id: typing.Optional[int] = None
443451

452+
_session_bus: Gio.DBusConnection
453+
444454
def __init__(self, context: Context, plugin_config):
445455
self.context = context
446456
self.on_show_settings = context.api.show_settings
@@ -458,10 +468,19 @@ def __init__(self, context: Context, plugin_config):
458468
self.allow_disabling = plugin_config["allow_disabling"]
459469
self.menu_locked = False
460470

461-
session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
471+
# This is using a separate dbus connection on purpose
472+
# StatusNotifierWatcher does not have an unregister method - the spec instead
473+
# says that the watcher should detect the item "going away from the bus"
474+
# in practice, this means that the connection closing is detected by the watcher
475+
# which can only happen if we use our own connection, and close it manually
476+
self._session_bus = Gio.DBusConnection.new_for_address_sync(
477+
Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION),
478+
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT
479+
| Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
480+
)
462481

463482
self.sni_service = StatusNotifierItemService(
464-
session_bus, menu_items=self.get_items()
483+
self._session_bus, menu_items=self.get_items()
465484
)
466485
self.sni_service.register()
467486

@@ -475,6 +494,10 @@ def initialize(self, plugin_config):
475494
self.update_menu()
476495
self.update_tooltip()
477496

497+
def unregister(self) -> None:
498+
self.sni_service.unregister()
499+
self._session_bus.close_sync()
500+
478501
def get_items(self):
479502
breaks_found = self.has_breaks()
480503

@@ -864,3 +887,12 @@ def on_start():
864887
def on_stop():
865888
"""Disable the tray icon."""
866889
tray_icon.disable_ui()
890+
891+
892+
def disable() -> None:
893+
"""Disable the tray icon plugin."""
894+
global tray_icon
895+
896+
if tray_icon:
897+
tray_icon.unregister()
898+
tray_icon = None

safeeyes/safeeyes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ def restart(self, config, set_active=False):
471471
self.break_screen.initialize(config)
472472

473473
try:
474-
self.plugins_manager.init(self.context, self.config)
474+
self.plugins_manager.reload(self.context, self.config)
475475
except RequiredPluginException as e:
476476
self.show_required_plugin_dialog(e)
477477
return

0 commit comments

Comments
 (0)