dwipe 2.0.2__py3-none-any.whl → 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ """
2
+ DeviceChangeMonitor - Background thread for monitoring block device changes
3
+ """
4
+ import os
5
+ import select
6
+ import threading
7
+ import traceback
8
+ import ctypes
9
+ from ctypes.util import find_library
10
+
11
+
12
+ # inotify constants (from Linux headers)
13
+ IN_CREATE = 0x00000100
14
+ IN_DELETE = 0x00000200
15
+ IN_MOVED_TO = 0x00000080
16
+ IN_MOVED_FROM = 0x00000040
17
+
18
+ # Flags for inotify_init1
19
+ O_NONBLOCK = 0o4000
20
+ O_CLOEXEC = 0o2000000
21
+
22
+ # Try to load libc for inotify syscalls
23
+ try:
24
+ libc_name = find_library('c')
25
+ if libc_name is None:
26
+ libc_name = 'libc.so.6' # Fallback for systems where find_library fails
27
+ libc = ctypes.CDLL(libc_name, use_errno=True)
28
+ _HAVE_LIBC = True
29
+ except (OSError, TypeError):
30
+ _HAVE_LIBC = False
31
+
32
+
33
+ class DeviceChangeMonitor:
34
+ """Background monitor that detects block device changes via inotify or polling.
35
+
36
+ This class monitors for device hot-plug events without using lsblk (which
37
+ can block on devices undergoing firmware wipe). Device discovery is now
38
+ done directly via DeviceInfo.discover_devices().
39
+
40
+ Uses inotify on /sys/class/block for efficient event-driven monitoring.
41
+ Falls back to polling if inotify is unavailable.
42
+ """
43
+
44
+ def __init__(self, check_interval=0.2):
45
+ """
46
+ Initialize the device change monitor.
47
+
48
+ Args:
49
+ check_interval: How often to check for changes in polling mode (seconds)
50
+ """
51
+ self.check_interval = check_interval
52
+ self._lock = threading.Lock()
53
+ self._thread = None
54
+ self._stop_event = threading.Event()
55
+ self._changes_detected = False
56
+ self.last_fingerprint = None
57
+ # inotify resources
58
+ self._inotify_fd = None
59
+ self._watch_fd = None
60
+ self._use_inotify = False
61
+
62
+ def start(self):
63
+ """Start the background monitoring thread"""
64
+ if self._thread is not None and self._thread.is_alive():
65
+ return # Already running
66
+
67
+ # Try to initialize inotify
68
+ self._try_init_inotify()
69
+
70
+ self._stop_event.clear()
71
+ self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
72
+ self._thread.start()
73
+
74
+ def _try_init_inotify(self):
75
+ """Try to initialize inotify for /sys/class/block.
76
+
77
+ On failure, falls back to polling mode.
78
+ """
79
+ if not _HAVE_LIBC:
80
+ with open('/tmp/dwipe_inotify_debug.log', 'a', encoding='utf-8') as f:
81
+ f.write("libc not available, falling back to polling\n")
82
+ self._use_inotify = False
83
+ return
84
+
85
+ try:
86
+ # Call inotify_init1(O_NONBLOCK | O_CLOEXEC) via ctypes
87
+ libc.inotify_init1.argtypes = [ctypes.c_int]
88
+ libc.inotify_init1.restype = ctypes.c_int
89
+ self._inotify_fd = libc.inotify_init1(O_NONBLOCK | O_CLOEXEC)
90
+
91
+ if self._inotify_fd < 0:
92
+ raise OSError("inotify_init1 returned error")
93
+
94
+ # Call inotify_add_watch for /sys/class/block
95
+ libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
96
+ libc.inotify_add_watch.restype = ctypes.c_int
97
+ mask = IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM
98
+ self._watch_fd = libc.inotify_add_watch(self._inotify_fd, b'/sys/class/block', mask)
99
+
100
+ if self._watch_fd < 0:
101
+ raise OSError("inotify_add_watch returned error")
102
+
103
+ self._use_inotify = True
104
+ except OSError as e:
105
+ # inotify not available or permission denied - fall back to polling
106
+ with open('/tmp/dwipe_inotify_debug.log', 'a', encoding='utf-8') as f:
107
+ f.write(f"inotify initialization failed: {e}\n")
108
+ self._use_inotify = False
109
+ # Clean up any partially initialized resources
110
+ if self._inotify_fd is not None and self._inotify_fd >= 0:
111
+ try:
112
+ os.close(self._inotify_fd)
113
+ except OSError:
114
+ pass
115
+ self._inotify_fd = None
116
+ except Exception as e: # pylint: disable=broad-exception-caught
117
+ # Unexpected error - log and fall back to polling
118
+ with open('/tmp/dwipe_inotify_debug.log', 'a', encoding='utf-8') as f:
119
+ f.write(f"Unexpected error in inotify init: {e}\n")
120
+ traceback.print_exc(file=f)
121
+ self._use_inotify = False
122
+
123
+ def stop(self):
124
+ """Stop the background monitoring thread and clean up resources"""
125
+ self._stop_event.set()
126
+ if self._thread is not None:
127
+ self._thread.join(timeout=1.0)
128
+
129
+ # Clean up inotify resources
130
+ if self._inotify_fd is not None and self._inotify_fd >= 0:
131
+ try:
132
+ if self._watch_fd is not None and self._watch_fd >= 0 and _HAVE_LIBC:
133
+ libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
134
+ libc.inotify_rm_watch.restype = ctypes.c_int
135
+ libc.inotify_rm_watch(self._inotify_fd, self._watch_fd)
136
+ os.close(self._inotify_fd)
137
+ except OSError:
138
+ pass
139
+ finally:
140
+ self._inotify_fd = None
141
+ self._watch_fd = None
142
+
143
+ def _check_for_changes(self):
144
+ """
145
+ Check if block devices or partitions have changed (non-blocking).
146
+
147
+ Returns:
148
+ True if changes detected, False otherwise
149
+ """
150
+ try:
151
+ # 1. Quickest check: Does the list of block devices match?
152
+ # This catches "Forget" (DEL) and "Scan" (!) events immediately.
153
+ current_devs = os.listdir('/sys/class/block')
154
+
155
+ # 2. Secondary check: Do the partition sizes/counts match?
156
+ with open('/proc/partitions', 'r', encoding='utf-8') as f:
157
+ current_parts = f.read()
158
+
159
+ # Create a combined "Fingerprint"
160
+ fingerprint = f"{len(current_devs)}|{current_parts}"
161
+
162
+ if fingerprint != self.last_fingerprint:
163
+ self.last_fingerprint = fingerprint
164
+ return True
165
+
166
+ except Exception: # pylint: disable=broad-exception-caught
167
+ # If we can't read /sys or /proc, default to True
168
+ # so we don't get stuck with a blank screen.
169
+ return True
170
+ return False
171
+
172
+ def _monitor_loop(self):
173
+ """Background thread loop that monitors for device changes.
174
+
175
+ Dispatches to inotify-based or polling-based monitoring.
176
+ """
177
+ if self._use_inotify:
178
+ self._inotify_loop()
179
+ else:
180
+ self._polling_loop()
181
+
182
+ def _inotify_loop(self):
183
+ """Monitor for device changes using inotify events.
184
+
185
+ Sleeps until kernel wakes us up on /sys/class/block changes.
186
+ """
187
+ while not self._stop_event.is_set():
188
+ try:
189
+ # Use select with timeout to make thread interruptible
190
+ # Timeout allows checking stop event periodically
191
+ readable, _, _ = select.select([self._inotify_fd], [], [], 1.0)
192
+
193
+ if readable:
194
+ # Read and discard inotify events
195
+ # We just need to know *something* changed, not the details
196
+ try:
197
+ os.read(self._inotify_fd, 4096)
198
+ with self._lock:
199
+ self._changes_detected = True
200
+ except (OSError, BlockingIOError):
201
+ pass
202
+
203
+ except (OSError, ValueError) as e: # pylint: disable=broad-exception-caught
204
+ # If inotify fails, log and fall back to polling
205
+ with open('/tmp/dwipe_inotify_debug.log', 'a', encoding='utf-8') as f:
206
+ f.write(f"inotify_loop error: {e}, falling back to polling\n")
207
+ self._use_inotify = False
208
+ self._polling_loop()
209
+ return
210
+
211
+ def _polling_loop(self):
212
+ """Fallback polling-based monitoring.
213
+
214
+ Periodically checks /proc/partitions and /sys/class/block for changes.
215
+ """
216
+ while not self._stop_event.is_set():
217
+ if self._check_for_changes():
218
+ with self._lock:
219
+ self._changes_detected = True
220
+
221
+ # Sleep for the check interval
222
+ self._stop_event.wait(self.check_interval)
223
+
224
+ def get_and_clear(self):
225
+ """
226
+ Check if changes were detected since last call.
227
+
228
+ Returns:
229
+ True if changes detected, False otherwise
230
+ """
231
+ with self._lock:
232
+ result = self._changes_detected
233
+ self._changes_detected = False
234
+ return result
235
+
236
+ def peek(self):
237
+ """
238
+ Check if changes were detected without clearing the flag.
239
+
240
+ Returns:
241
+ True if changes detected, False otherwise
242
+ """
243
+ with self._lock:
244
+ return self._changes_detected