dwipe 2.0.1__py3-none-any.whl → 3.0.0__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.
- dwipe/DeviceChangeMonitor.py +244 -0
- dwipe/DeviceInfo.py +703 -177
- dwipe/DeviceWorker.py +566 -0
- dwipe/DiskWipe.py +953 -214
- dwipe/DrivePreChecker.py +203 -0
- dwipe/FirmwareWipeTask.py +865 -0
- dwipe/NvmeTool.py +225 -0
- dwipe/PersistentState.py +45 -16
- dwipe/Prereqs.py +84 -0
- dwipe/SataTool.py +499 -0
- dwipe/StructuredLogger.py +644 -0
- dwipe/Tunables.py +62 -0
- dwipe/Utils.py +298 -3
- dwipe/VerifyTask.py +412 -0
- dwipe/WipeJob.py +631 -171
- dwipe/WipeTask.py +150 -0
- dwipe/WriteTask.py +402 -0
- dwipe/main.py +34 -9
- dwipe-3.0.0.dist-info/METADATA +566 -0
- dwipe-3.0.0.dist-info/RECORD +24 -0
- dwipe/ToolManager.py +0 -637
- dwipe/WipeJobFuture.py +0 -245
- dwipe-2.0.1.dist-info/METADATA +0 -410
- dwipe-2.0.1.dist-info/RECORD +0 -14
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/WHEEL +0 -0
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.1.dist-info → dwipe-3.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|