Skip to content

Commit 396b18c

Browse files
authored
Merge pull request slgobinath#720 from deltragon/smartpause-rework
smartpause: refactor into multiple classes
2 parents 45a1138 + c0cb1b2 commit 396b18c

File tree

8 files changed

+904
-285
lines changed

8 files changed

+904
-285
lines changed

safeeyes/plugins/smartpause/dependency_checker.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222

2323
def validate(plugin_config, plugin_settings):
2424
command = None
25-
if utility.DESKTOP_ENVIRONMENT == "gnome" and utility.IS_WAYLAND:
26-
command = "dbus-send"
27-
elif utility.DESKTOP_ENVIRONMENT == "sway":
25+
if utility.DESKTOP_ENVIRONMENT == "sway":
2826
command = "swayidle"
2927
elif utility.IS_WAYLAND:
3028
if not utility.module_exist("pywayland"):

safeeyes/plugins/smartpause/ext_idle_notify.py

Lines changed: 248 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,100 @@
1818

1919
# This file is heavily inspired by https://github.com/juienpro/easyland/blob/efc26a0b22d7bdbb0f8436183428f7036da4662a/src/easyland/idle.py
2020

21+
from dataclasses import dataclass
22+
import logging
2123
import threading
22-
import datetime
2324
import os
2425
import select
26+
import typing
2527

2628
from pywayland.client import Display
2729
from pywayland.protocol.wayland.wl_seat import WlSeat
28-
from pywayland.protocol.ext_idle_notify_v1 import ExtIdleNotifierV1
30+
from pywayland.protocol.ext_idle_notify_v1 import (
31+
ExtIdleNotifierV1,
32+
ExtIdleNotificationV1,
33+
)
2934

35+
from .interface import IdleMonitorInterface
36+
from safeeyes import utility
3037

31-
class ExtIdleNotify:
32-
_idle_notifier = None
33-
_seat = None
34-
_notification = None
35-
_notifier_set = False
36-
_running = True
37-
_thread = None
38-
_r_channel = None
39-
_w_channel = None
4038

41-
_idle_since = None
39+
@dataclass
40+
class IdleConfig:
41+
on_idle: typing.Callable[[], None]
42+
on_resumed: typing.Callable[[], None]
43+
idle_time: float
4244

43-
def __init__(self):
45+
46+
class IdleMonitorExtIdleNotify(IdleMonitorInterface):
47+
_ext_idle_notify_internal: typing.Optional["ExtIdleNotifyInternal"] = None
48+
_thread: typing.Optional[threading.Thread] = None
49+
50+
_r_channel_started: int
51+
_w_channel_started: int
52+
53+
_r_channel_stop: int
54+
_w_channel_stop: int
55+
56+
_r_channel_listen: int
57+
_w_channel_listen: int
58+
59+
_idle_config: typing.Optional[IdleConfig] = None
60+
61+
def init(self) -> None:
62+
# we spawn one wayland client once
63+
# when the monitor is not running, it should be quite idle
64+
self._r_channel_started, self._w_channel_started = os.pipe()
65+
self._r_channel_stop, self._w_channel_stop = os.pipe()
66+
self._r_channel_listen, self._w_channel_listen = os.pipe()
67+
os.set_blocking(self._r_channel_listen, False)
68+
69+
self._thread = threading.Thread(
70+
target=self._run, name="ExtIdleNotify", daemon=False
71+
)
72+
self._thread.start()
73+
74+
result = os.read(self._r_channel_started, 1)
75+
76+
if result == b"0":
77+
self._thread.join()
78+
self._thread = None
79+
raise Exception("ext-idle-notify-v1 not supported")
80+
81+
def start_monitor(
82+
self,
83+
on_idle: typing.Callable[[], None],
84+
on_resumed: typing.Callable[[], None],
85+
idle_time: float,
86+
) -> None:
87+
self._idle_config = IdleConfig(
88+
on_idle=on_idle,
89+
on_resumed=on_resumed,
90+
idle_time=idle_time,
91+
)
92+
93+
# 1 means start listening, or that the configuration changed
94+
os.write(self._w_channel_listen, b"1")
95+
96+
def configuration_changed(
97+
self,
98+
on_idle: typing.Callable[[], None],
99+
on_resumed: typing.Callable[[], None],
100+
idle_time: float,
101+
) -> None:
102+
self._idle_config = IdleConfig(
103+
on_idle=on_idle,
104+
on_resumed=on_resumed,
105+
idle_time=idle_time,
106+
)
107+
108+
# 1 means start listening, or that the configuration changed
109+
os.write(self._w_channel_listen, b"1")
110+
111+
def is_monitor_running(self) -> bool:
112+
return self._idle_config is not None
113+
114+
def _run(self) -> None:
44115
# Note that this creates a new connection to the wayland compositor.
45116
# This is not an issue per se, but does mean that the compositor sees this as
46117
# a new, separate client, that just happens to run in the same process as
@@ -54,80 +125,196 @@ def __init__(self):
54125
# https://lists.freedesktop.org/archives/wayland-devel/2019-March/040344.html
55126
# The best thing would be, of course, for gtk to gain native support for
56127
# ext-idle-notify-v1.
57-
self._display = Display()
58-
self._display.connect()
59-
self._r_channel, self._w_channel = os.pipe()
128+
with Display() as display:
129+
self._ext_idle_notify_internal = ExtIdleNotifyInternal(
130+
display,
131+
self._r_channel_stop,
132+
self._w_channel_started,
133+
self._r_channel_listen,
134+
self._on_idle,
135+
self._on_resumed,
136+
self._get_idle_time,
137+
)
138+
self._ext_idle_notify_internal.run()
139+
self._ext_idle_notify_internal = None
140+
141+
def _on_idle(self) -> None:
142+
if self._idle_config is not None:
143+
self._idle_config.on_idle()
144+
145+
def _on_resumed(self) -> None:
146+
if self._idle_config is not None:
147+
self._idle_config.on_resumed()
60148

61-
def stop(self):
62-
self._running = False
149+
def _get_idle_time(self) -> typing.Optional[float]:
150+
if self._idle_config is not None:
151+
return self._idle_config.idle_time
152+
else:
153+
return None
154+
155+
def stop_monitor(self) -> None:
156+
# 0 means to stop listening
157+
# It's not an issue to write to the channel if we're not listening anymore
158+
# already
159+
os.write(self._w_channel_listen, b"0")
160+
161+
self._idle_config = None
162+
163+
def stop(self) -> None:
63164
# write anything, just to wake up the channel
64-
os.write(self._w_channel, b"!")
65-
self._notification.destroy()
66-
self._notification = None
67-
self._seat = None
68-
self._thread.join()
69-
os.close(self._r_channel)
70-
os.close(self._w_channel)
165+
if self._thread is not None:
166+
os.write(self._w_channel_stop, b"!")
167+
self._thread.join()
168+
self._thread = None
169+
os.close(self._r_channel_stop)
170+
os.close(self._w_channel_stop)
71171

72-
def run(self):
73-
self._thread = threading.Thread(
74-
target=self._run, name="ExtIdleNotify", daemon=False
75-
)
76-
self._thread.start()
172+
os.close(self._r_channel_started)
173+
os.close(self._w_channel_started)
77174

78-
def _run(self):
175+
os.close(self._r_channel_listen)
176+
os.close(self._w_channel_listen)
177+
178+
179+
class ExtIdleNotifyInternal:
180+
"""This runs in the thread, and is only alive while the display exists.
181+
182+
Split out into a separate object to simplify lifetime handling.
183+
"""
184+
185+
_idle_notifier: typing.Optional[ExtIdleNotifierV1] = None
186+
_notification: typing.Optional[ExtIdleNotificationV1] = None
187+
_display: Display
188+
_r_channel_stop: int
189+
_w_channel_started: int
190+
_r_channel_listen: int
191+
_seat: typing.Optional[WlSeat] = None
192+
193+
_on_idle: typing.Callable[[], None]
194+
_on_resumed: typing.Callable[[], None]
195+
_get_idle_time: typing.Callable[[], typing.Optional[float]]
196+
197+
def __init__(
198+
self,
199+
display: Display,
200+
r_channel_stop: int,
201+
w_channel_started: int,
202+
r_channel_listen: int,
203+
on_idle: typing.Callable[[], None],
204+
on_resumed: typing.Callable[[], None],
205+
get_idle_time: typing.Callable[[], typing.Optional[float]],
206+
) -> None:
207+
self._display = display
208+
self._r_channel_stop = r_channel_stop
209+
self._w_channel_started = w_channel_started
210+
self._r_channel_listen = r_channel_listen
211+
self._on_idle = on_idle
212+
self._on_resumed = on_resumed
213+
self._get_idle_time = get_idle_time
214+
215+
def run(self) -> None:
216+
"""Run the wayland client.
217+
218+
This will block until it's stopped by the channel.
219+
When this stops, self should no longer be used.
220+
"""
79221
reg = self._display.get_registry()
80222
reg.dispatcher["global"] = self._global_handler
81223

224+
self._display.roundtrip()
225+
226+
while self._seat is None:
227+
self._display.dispatch(block=True)
228+
229+
if self._idle_notifier is None:
230+
self._seat = None
231+
232+
self._display.roundtrip()
233+
234+
# communicate to the outer thread that the compositor does not
235+
# implement the ext-idle-notify-v1 protocol
236+
os.write(self._w_channel_started, b"0")
237+
238+
return
239+
240+
os.write(self._w_channel_started, b"1")
241+
82242
display_fd = self._display.get_fd()
83243

84-
while self._running:
244+
while True:
85245
self._display.flush()
86246

87247
# this blocks until either there are new events in self._display
88248
# (retrieved using dispatch())
89-
# or until there are events in self._r_channel - which means that stop()
90-
# was called
249+
# or until there are events in self._r_channel_stop - which means that
250+
# stop() was called
91251
# unfortunately, this seems like the best way to make sure that dispatch
92252
# doesn't block potentially forever (up to multiple seconds in my usage)
93-
read, _w, _x = select.select((display_fd, self._r_channel), (), ())
253+
read, _w, _x = select.select(
254+
(display_fd, self._r_channel_stop, self._r_channel_listen), (), ()
255+
)
94256

95-
if self._r_channel in read:
257+
if self._r_channel_listen in read:
258+
# r_channel_listen is nonblocking
259+
# if there is nothing to read here, result should just be b""
260+
result = os.read(self._r_channel_listen, 1)
261+
if result == b"1":
262+
self._listen()
263+
elif result == b"0":
264+
if self._notification is not None:
265+
self._notification.destroy() # type: ignore[attr-defined]
266+
self._notification = None
267+
268+
if self._r_channel_stop in read:
96269
# the channel was written to, which means stop() was called
97-
# at this point, self._running should be false as well
98270
break
99271

100272
if display_fd in read:
101273
self._display.dispatch(block=True)
102274

103-
self._display.disconnect()
275+
self._display.roundtrip()
276+
277+
if self._notification is not None:
278+
self._notification.destroy() # type: ignore[attr-defined]
279+
self._notification = None
280+
281+
self._display.roundtrip()
282+
283+
self._seat = None
284+
self._idle_notifier = None
285+
286+
def _listen(self):
287+
"""Create a new idle notification listener.
104288
105-
def _global_handler(self, reg, id_num, iface_name, version):
289+
If one already exists, throw it away and recreate it with the new
290+
idle time.
291+
"""
292+
# note that the typing doesn't work correctly here - it always says that
293+
# get_idle_notification is not defined
294+
# so just don't check this method
295+
if self._notification is not None:
296+
self._notification.destroy()
297+
self._notification = None
298+
299+
timeout_sec = self._get_idle_time()
300+
if timeout_sec is None:
301+
logging.debug(
302+
"this should not happen. _listen() was called but idle time was not set"
303+
)
304+
self._notification = self._idle_notifier.get_idle_notification(
305+
int(timeout_sec * 1000), self._seat
306+
)
307+
self._notification.dispatcher["idled"] = self._idle_notifier_handler
308+
self._notification.dispatcher["resumed"] = self._idle_notifier_resume_handler
309+
310+
def _global_handler(self, reg, id_num, iface_name, version) -> None:
106311
if iface_name == "wl_seat":
107312
self._seat = reg.bind(id_num, WlSeat, version)
108313
if iface_name == "ext_idle_notifier_v1":
109314
self._idle_notifier = reg.bind(id_num, ExtIdleNotifierV1, version)
110315

111-
if self._idle_notifier and self._seat and not self._notifier_set:
112-
self._notifier_set = True
113-
timeout_sec = 1
114-
self._notification = self._idle_notifier.get_idle_notification(
115-
timeout_sec * 1000, self._seat
116-
)
117-
self._notification.dispatcher["idled"] = self._idle_notifier_handler
118-
self._notification.dispatcher["resumed"] = (
119-
self._idle_notifier_resume_handler
120-
)
121-
122-
def _idle_notifier_handler(self, notification):
123-
self._idle_since = datetime.datetime.now()
124-
125-
def _idle_notifier_resume_handler(self, notification):
126-
self._idle_since = None
127-
128-
def get_idle_time_seconds(self):
129-
if self._idle_since is None:
130-
return 0
316+
def _idle_notifier_handler(self, notification) -> None:
317+
utility.execute_main_thread(self._on_idle)
131318

132-
result = datetime.datetime.now() - self._idle_since
133-
return result.total_seconds()
319+
def _idle_notifier_resume_handler(self, notification) -> None:
320+
utility.execute_main_thread(self._on_resumed)

0 commit comments

Comments
 (0)