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/WipeTask.py ADDED
@@ -0,0 +1,150 @@
1
+ """
2
+ WipeTask - Abstract base class for wipe/verify operations
3
+
4
+ Defines the interface and shared state for all task types.
5
+ """
6
+ # pylint: disable=broad-exception-raised,broad-exception-caught
7
+ import time
8
+ from types import SimpleNamespace
9
+
10
+ from .Utils import Utils
11
+
12
+
13
+ class WipeTask:
14
+ """Abstract base class for wipe/verify operations
15
+
16
+ Defines the interface that all tasks must implement:
17
+ - run_task(): Execute the task (blocking, runs in thread)
18
+ - get_status(): Get current progress (thread-safe, called from main thread)
19
+ - get_summary_dict(): Get final summary after completion
20
+ - abort(): Signal task to stop
21
+
22
+ Shared state across all task types:
23
+ - Device info: device_path, total_size
24
+ - Control: opts, do_abort, done
25
+ - Progress: total_written, start_mono, wr_hists
26
+ - Errors: exception
27
+ """
28
+
29
+ # O_DIRECT requires aligned buffers and write sizes
30
+ BLOCK_SIZE = 4096 # Alignment requirement for O_DIRECT
31
+ WRITE_SIZE = 1 * 1024 * 1024 # 1MB (must be multiple of BLOCK_SIZE)
32
+ BUFFER_SIZE = WRITE_SIZE # Same size for O_DIRECT
33
+
34
+ # Marker constants (separate from O_DIRECT writes)
35
+ MARKER_SIZE = 16 * 1024 # 16KB for marker
36
+ STATE_OFFSET = 15 * 1024 # where json is written (for marker buffer)
37
+
38
+ # Aligned buffers allocated with mmap (initialized at module load)
39
+ buffer = None # Random data buffer (memoryview)
40
+ buffer_mem = None # Underlying mmap object
41
+ zero_buffer = None # Zero buffer (memoryview)
42
+ zero_buffer_mem = None # Underlying mmap object
43
+
44
+ def __init__(self, device_path, total_size, opts=None):
45
+ """Initialize base task with common attributes
46
+
47
+ Args:
48
+ device_path: Path to device (e.g., '/dev/sda1')
49
+ total_size: Total size in bytes
50
+ opts: Options namespace (wipe_mode, verify_pct, etc.)
51
+ """
52
+ self.device_path = device_path
53
+ self.total_size = total_size
54
+ self.opts = opts
55
+
56
+ # Control flags
57
+ self.do_abort = False
58
+ self.done = False
59
+ self.exception = None
60
+ self.more_state = "" # Optional extra status info from derived classes
61
+
62
+ # Progress tracking
63
+ self.total_written = 0 # Bytes processed (write or verify)
64
+ self.start_mono = time.monotonic()
65
+ self.wr_hists = [] # Progress history: list of SimpleNamespace(mono, written)
66
+ self.wr_hists.append(SimpleNamespace(mono=self.start_mono, written=0))
67
+
68
+ def run_task(self):
69
+ """Execute the task (blocking, runs in thread)
70
+
71
+ Must be implemented by subclasses. Should:
72
+ - Perform the actual work (write or verify)
73
+ - Update self.total_written as it progresses
74
+ - Check self.do_abort periodically and stop if True
75
+ - Set self.exception if errors occur
76
+ - Set self.done = True when complete (or use finally block)
77
+ """
78
+ raise NotImplementedError("Subclasses must implement run_task()")
79
+
80
+ def get_status(self):
81
+ """Get current progress status (thread-safe, called from main thread)
82
+
83
+ Returns:
84
+ tuple: (elapsed_str, pct_str, rate_str, eta_str, more_state)
85
+ - elapsed_str: e.g., "5m23s"
86
+ - pct_str: e.g., "45%" or "v23%" (for verify)
87
+ - rate_str: e.g., "450MB/s"
88
+ - eta_str: e.g., "2m15s"
89
+ - more_state: optional extra status from derived class
90
+ """
91
+ mono = time.monotonic()
92
+ elapsed_time = mono - self.start_mono
93
+
94
+ # Calculate percentage
95
+ pct = (self.total_written / self.total_size) * 100 if self.total_size > 0 else 0
96
+ pct = min(pct, 100)
97
+ pct_str = f'{int(round(pct))}%'
98
+
99
+ if self.do_abort:
100
+ pct_str = 'STOP'
101
+
102
+ # Track progress for rate calculation
103
+ self.wr_hists.append(SimpleNamespace(mono=mono, written=self.total_written))
104
+ floor = mono - 30 # 30 second window
105
+ while len(self.wr_hists) >= 3 and self.wr_hists[1].mono >= floor:
106
+ del self.wr_hists[0]
107
+
108
+ # Calculate rate from sliding window
109
+ delta_mono = mono - self.wr_hists[0].mono
110
+ rate = (self.total_written - self.wr_hists[0].written) / delta_mono if delta_mono > 1.0 else 0
111
+ rate_str = f'{Utils.human(int(round(rate, 0)))}/s'
112
+
113
+ # Calculate ETA
114
+ if rate > 0:
115
+ remaining = self.total_size - self.total_written
116
+ when = int(round(remaining / rate))
117
+ when_str = Utils.ago_str(when)
118
+ else:
119
+ when_str = '0'
120
+
121
+ return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str, self.more_state
122
+
123
+ def get_summary_dict(self):
124
+ """Get final summary after task completion
125
+
126
+ Returns:
127
+ dict: Summary with step name, elapsed, rate, bytes processed, etc.
128
+ """
129
+ mono = time.monotonic()
130
+ elapsed = mono - self.start_mono
131
+ rate_bps = self.total_written / elapsed if elapsed > 0 else 0
132
+
133
+ return {
134
+ "step": f"task {self.__class__.__name__}",
135
+ "elapsed": Utils.ago_str(int(elapsed)),
136
+ "rate": f"{Utils.human(int(rate_bps))}/s",
137
+ "bytes_processed": self.total_written,
138
+ }
139
+
140
+ def get_display_name(self):
141
+ """Get human-readable name for this task type (for progress display)
142
+
143
+ Returns:
144
+ str: Task name like "Zero", "Rand", "Crypto", "Verify", etc.
145
+ """
146
+ return "Task" # Default, should be overridden
147
+
148
+ def abort(self):
149
+ """Signal task to stop (thread-safe)"""
150
+ self.do_abort = True
dwipe/WriteTask.py ADDED
@@ -0,0 +1,402 @@
1
+ """
2
+ WriteTask - Abstract base class and implementations for write operations
3
+
4
+ Includes:
5
+ - WriteTask: Abstract base class with write loop, error handling, performance monitoring
6
+ - WriteZeroTask: Concrete class for writing zeros
7
+ - WriteRandTask: Concrete class for writing random data
8
+ """
9
+ # pylint: disable=broad-exception-raised,broad-exception-caught
10
+ import os
11
+ import json
12
+ import time
13
+ import subprocess
14
+ import traceback
15
+ from types import SimpleNamespace
16
+
17
+ from .WipeTask import WipeTask
18
+ from .Utils import Utils
19
+
20
+
21
+ class WriteTask(WipeTask):
22
+ """Abstract base class for write operations (WriteZeroTask, WriteRandTask)
23
+
24
+ Implements the main write loop with:
25
+ - O_DIRECT unbuffered I/O for maximum performance
26
+ - Error handling with safe_write() and reopen on error
27
+ - Performance monitoring (stall/slowdown detection)
28
+ - Periodic marker updates for crash recovery
29
+ - Multi-pass support
30
+
31
+ Subclasses must implement:
32
+ - get_buffer(chunk_size): Return buffer slice (zeros or random)
33
+ """
34
+
35
+ def __init__(self, device_path, total_size, opts=None, resume_from=0, pass_number=0):
36
+ """Initialize write task
37
+
38
+ Args:
39
+ device_path: Path to device (e.g., '/dev/sda1')
40
+ total_size: Total size in bytes (single pass)
41
+ opts: Options namespace
42
+ resume_from: Byte offset to resume from (0 for fresh start)
43
+ pass_number: Current pass number (0-indexed, for multi-pass)
44
+ """
45
+ super().__init__(device_path, total_size, opts)
46
+
47
+ # Resume support
48
+ self.resume_from = resume_from
49
+ self.total_written = resume_from # Start from resume offset
50
+ self.current_pass = pass_number
51
+
52
+ # Marker updates for crash recovery
53
+ self.last_marker_update_mono = time.monotonic() - 25 # Last marker write
54
+ self.marker_update_interval = 30 # Update every 30 seconds
55
+
56
+ # Performance monitoring
57
+ self.slowdown_stop = getattr(opts, 'slowdown_stop', 64)
58
+ self.stall_timeout = getattr(opts, 'stall_timeout', 60)
59
+ self.max_slowdown_ratio = 0
60
+ self.max_stall_secs = 0
61
+ self.baseline_speed = None # Bytes per second baseline
62
+ self.baseline_end_mono = None
63
+ self.last_progress_mono = time.monotonic()
64
+ self.last_progress_written = resume_from
65
+ self.last_slowdown_check = 0
66
+
67
+ # Error handling
68
+ self.max_consecutive_errors = 3
69
+ self.max_total_errors = 100
70
+ self.reopen_on_error = True
71
+ self.reopen_count = 0
72
+ self.total_errors = 0
73
+
74
+ # Initialize write history
75
+ self.wr_hists = [SimpleNamespace(mono=self.start_mono, written=resume_from)]
76
+
77
+ def get_buffer(self, chunk_size):
78
+ """Get buffer slice for writing (abstract method)
79
+
80
+ Args:
81
+ chunk_size: Number of bytes to return
82
+
83
+ Returns:
84
+ memoryview: Buffer slice of requested size
85
+
86
+ Must be implemented by subclasses:
87
+ - WriteZeroTask returns WipeTask.zero_buffer[:chunk_size]
88
+ - WriteRandTask returns WipeTask.buffer[:chunk_size]
89
+ """
90
+ raise NotImplementedError("Subclasses must implement get_buffer()")
91
+
92
+ def run_task(self):
93
+ """Execute write operation (blocking, runs in thread)"""
94
+ try:
95
+ # Set low I/O priority
96
+ self._setup_ionice()
97
+
98
+ # Open device with O_DIRECT for unbuffered I/O
99
+ fd = os.open(self.device_path, os.O_WRONLY | os.O_DIRECT)
100
+
101
+ try:
102
+ # Start from resume offset if resuming
103
+ offset_in_pass = self.resume_from
104
+
105
+ # SKIP MARKER AREA - don't overwrite it!
106
+ if offset_in_pass < WipeTask.MARKER_SIZE:
107
+ self.total_written += WipeTask.MARKER_SIZE - offset_in_pass
108
+ offset_in_pass = WipeTask.MARKER_SIZE
109
+
110
+ # Seek to current position (O_DIRECT requires block-aligned seeks)
111
+ os.lseek(fd, offset_in_pass, os.SEEK_SET)
112
+
113
+ # Write until end of pass
114
+ bytes_to_write = self.total_size - offset_in_pass
115
+ bytes_written_this_run = 0
116
+
117
+ while bytes_written_this_run < bytes_to_write and not self.do_abort:
118
+ current_mono = time.monotonic()
119
+
120
+ # Update baseline if needed (first 60 seconds)
121
+ self._update_baseline_if_needed(current_mono)
122
+
123
+ # Check for stall (frequently)
124
+ if self._check_for_stall(current_mono):
125
+ break
126
+
127
+ # Check for slowdown (every 10 seconds)
128
+ if self.baseline_speed is not None:
129
+ time_since_last_check = current_mono - self.last_slowdown_check
130
+ if time_since_last_check >= 10:
131
+ if self._check_for_slowdown(current_mono):
132
+ break
133
+ self.last_slowdown_check = current_mono
134
+
135
+ # Update progress tracking
136
+ if self.total_written > self.last_progress_written:
137
+ self.last_progress_mono = current_mono
138
+ self.last_progress_written = self.total_written
139
+
140
+ # Calculate chunk size (must be block-aligned for O_DIRECT)
141
+ remaining = bytes_to_write - bytes_written_this_run
142
+ chunk_size = min(WipeTask.WRITE_SIZE, remaining)
143
+ # Round down to block boundary
144
+ chunk_size = (chunk_size // WipeTask.BLOCK_SIZE) * WipeTask.BLOCK_SIZE
145
+ if chunk_size == 0:
146
+ break
147
+
148
+ # Get buffer from subclass (polymorphic)
149
+ chunk = self.get_buffer(chunk_size)
150
+
151
+ try:
152
+ # Write with O_DIRECT (bypasses page cache)
153
+ bytes_written, fd = self.safe_write(fd, chunk)
154
+ except Exception as e:
155
+ # Save exception for debugging
156
+ self.exception = str(e)
157
+ self.do_abort = True
158
+ bytes_written = 0
159
+
160
+ self.total_written += bytes_written
161
+ bytes_written_this_run += bytes_written
162
+
163
+ # Periodically update marker for crash recovery (every 30s)
164
+ if self.total_written > WipeTask.MARKER_SIZE:
165
+ self.maybe_update_marker()
166
+
167
+ # Check for errors or incomplete writes
168
+ if bytes_written < chunk_size:
169
+ break
170
+
171
+ finally:
172
+ # Close device file descriptor
173
+ if fd is not None:
174
+ os.close(fd)
175
+
176
+ self.done = True
177
+ except Exception:
178
+ self.exception = traceback.format_exc()
179
+ self.done = True
180
+
181
+ def safe_write(self, fd, chunk):
182
+ """Safe write with error recovery and reopen logic
183
+
184
+ Args:
185
+ fd: File descriptor
186
+ chunk: Data to write
187
+
188
+ Returns:
189
+ tuple: (bytes_written, fd) - fd might be new if reopened
190
+
191
+ Raises:
192
+ Exception: If should abort (too many consecutive/total errors)
193
+ """
194
+ consecutive_errors = 0
195
+ while True: # Keep trying until success, skip, or abort
196
+ try:
197
+ bytes_written = os.write(fd, chunk)
198
+ self.reopen_count = 0
199
+ return bytes_written, fd # success
200
+
201
+ except Exception as e:
202
+ consecutive_errors += 1
203
+ self.total_errors += 1
204
+
205
+ # Check if we should abort
206
+ if consecutive_errors >= self.max_consecutive_errors:
207
+ raise Exception(f"{consecutive_errors} consecutive write errors") from e
208
+
209
+ if self.total_errors >= self.max_total_errors:
210
+ raise Exception(f"{self.total_errors} total write errors") from e
211
+
212
+ # Not fatal yet - try reopening if enabled
213
+ if self.reopen_on_error:
214
+ try:
215
+ current_pos = self.total_written
216
+ # Open new fd first
217
+ new_fd = os.open(self.device_path, os.O_WRONLY | os.O_DIRECT)
218
+ try:
219
+ # Seek to correct position on new fd
220
+ os.lseek(new_fd, current_pos, os.SEEK_SET)
221
+ # Only close old fd after new one is ready
222
+ old_fd = fd
223
+ fd = new_fd
224
+ try:
225
+ os.close(old_fd)
226
+ except Exception:
227
+ pass # Old fd close failed, but new fd is good
228
+ self.reopen_count += 1
229
+ except Exception:
230
+ # New fd setup failed, close it and keep using old fd
231
+ os.close(new_fd)
232
+ raise
233
+ except Exception:
234
+ # Reopen failed - count as another error and retry with old fd
235
+ self.total_errors += 1
236
+
237
+ # Retry the write (continue loop)
238
+
239
+ def maybe_update_marker(self):
240
+ """Periodically update marker to enable crash recovery
241
+
242
+ Updates marker every marker_update_interval seconds (default 30s).
243
+ This allows resume to work even after crashes, power loss, or kill -9.
244
+ """
245
+ now_mono = time.monotonic()
246
+ if now_mono - self.last_marker_update_mono < self.marker_update_interval:
247
+ return # Not time yet
248
+
249
+ # Marker writes use separate file handle (buffered I/O, not O_DIRECT)
250
+ # because marker buffer is not aligned
251
+ try:
252
+ # Determine if this is a random or zero write
253
+ is_random = isinstance(self, WriteRandTask)
254
+ with open(self.device_path, 'r+b') as marker_file:
255
+ marker_file.seek(0)
256
+ marker_file.write(self._prep_marker_buffer(is_random))
257
+ self.last_marker_update_mono = now_mono
258
+ except Exception:
259
+ # If marker update fails, just continue - we'll try again in 30s
260
+ pass
261
+
262
+ def _prep_marker_buffer(self, is_random):
263
+ """Prepare marker buffer for this write task
264
+
265
+ Args:
266
+ is_random: bool, whether random data is being written
267
+
268
+ Returns:
269
+ bytearray: 16KB marker buffer with JSON status
270
+ """
271
+ data = {
272
+ "unixtime": int(time.time()),
273
+ "scrubbed_bytes": self.total_written,
274
+ "size_bytes": self.total_size,
275
+ "passes": 1, # Single pass per WriteTask
276
+ "mode": 'Rand' if is_random else 'Zero'
277
+ }
278
+ json_data = json.dumps(data).encode('utf-8')
279
+ buffer = bytearray(WipeTask.MARKER_SIZE) # Only 16KB, not 1MB
280
+ buffer[:WipeTask.STATE_OFFSET] = b'\x00' * WipeTask.STATE_OFFSET
281
+ buffer[WipeTask.STATE_OFFSET:WipeTask.STATE_OFFSET + len(json_data)] = json_data
282
+ remaining_size = WipeTask.MARKER_SIZE - (WipeTask.STATE_OFFSET + len(json_data))
283
+ buffer[WipeTask.STATE_OFFSET + len(json_data):] = b'\x00' * remaining_size
284
+ return buffer
285
+
286
+ def _check_for_stall(self, current_monotonic):
287
+ """Check for stall (no progress) - called frequently"""
288
+ if self.stall_timeout <= 0:
289
+ return False
290
+
291
+ time_since_progress = current_monotonic - self.last_progress_mono
292
+ self.max_stall_secs = max(time_since_progress, self.max_stall_secs)
293
+ if time_since_progress >= self.stall_timeout:
294
+ self.do_abort = True
295
+ self.exception = f"Stall detected: No progress for {time_since_progress:.1f} seconds"
296
+ return True
297
+
298
+ return False
299
+
300
+ def _check_for_slowdown(self, current_monotonic):
301
+ """Check for slowdown - called every 10 seconds"""
302
+ if self.slowdown_stop <= 0 or self.baseline_speed is None or self.baseline_speed <= 0:
303
+ return False
304
+
305
+ # Calculate current speed over last 30 seconds
306
+ floor = current_monotonic - 30
307
+ recent_history = [h for h in self.wr_hists if h.mono >= floor]
308
+
309
+ if len(recent_history) >= 2:
310
+ recent_start = recent_history[0]
311
+ recent_written = self.total_written - recent_start.written
312
+ recent_elapsed = current_monotonic - recent_start.mono
313
+
314
+ if recent_elapsed > 1.0:
315
+ current_speed = recent_written / recent_elapsed
316
+ self.baseline_speed = max(self.baseline_speed, current_speed)
317
+ slowdown_ratio = self.baseline_speed / max(current_speed, 1)
318
+ slowdown_ratio = int(round(slowdown_ratio, 0))
319
+ self.max_slowdown_ratio = max(self.max_slowdown_ratio, slowdown_ratio)
320
+
321
+ if slowdown_ratio > self.slowdown_stop:
322
+ self.do_abort = True
323
+ self.exception = (f"Slowdown abort: ({Utils.human(current_speed)}B/s)"
324
+ f" is 1/{slowdown_ratio} baseline")
325
+ return True
326
+
327
+ return False
328
+
329
+ def _update_baseline_if_needed(self, current_monotonic):
330
+ """Update baseline speed measurement if still in first 60 seconds"""
331
+ if self.baseline_speed is not None:
332
+ return # Baseline already established
333
+
334
+ if (current_monotonic - self.start_mono) >= 60:
335
+ total_written_60s = self.total_written - self.resume_from
336
+ elapsed_60s = current_monotonic - self.start_mono
337
+ if elapsed_60s > 0:
338
+ self.baseline_speed = total_written_60s / elapsed_60s
339
+ self.baseline_end_mono = current_monotonic
340
+ self.last_slowdown_check = current_monotonic # Start slowdown checking
341
+
342
+ def _setup_ionice(self):
343
+ """Setup I/O priority to best-effort class, lowest priority"""
344
+ try:
345
+ # Class 2 = best-effort, priority 7 = lowest (0 is highest, 7 is lowest)
346
+ subprocess.run(["ionice", "-c", "2", "-n", "7", "-p", str(os.getpid())],
347
+ capture_output=True, check=False)
348
+ except Exception:
349
+ pass
350
+
351
+ def get_summary_dict(self):
352
+ """Get final summary for this write task
353
+
354
+ Returns:
355
+ dict: Summary with step name, elapsed, rate, bytes written, errors, etc.
356
+ """
357
+ mono = time.monotonic()
358
+ elapsed = mono - self.start_mono
359
+ rate_bps = self.total_written / elapsed if elapsed > 0 else 0
360
+
361
+ # Determine mode from class name
362
+ mode = "Rand" if isinstance(self, WriteRandTask) else "Zero"
363
+
364
+ return {
365
+ "step": f"wipe {mode} {self.device_path}",
366
+ "elapsed": Utils.ago_str(int(elapsed)),
367
+ "rate": f"{Utils.human(int(rate_bps))}/s",
368
+ "bytes_written": self.total_written,
369
+ "bytes_total": self.total_size,
370
+ "passes_total": 1, # Single pass per WriteTask
371
+ "passes_completed": 1 if self.done and not self.exception else 0,
372
+ "current_pass": self.current_pass,
373
+ "peak_write_rate": f"{Utils.human(int(self.baseline_speed))}/s" if self.baseline_speed else None,
374
+ "worst_stall": Utils.ago_str(int(self.max_stall_secs)),
375
+ "worst_slowdown_ratio": round(self.max_slowdown_ratio, 1),
376
+ "errors": self.total_errors,
377
+ "reopen_count": self.reopen_count,
378
+ }
379
+
380
+
381
+ class WriteZeroTask(WriteTask):
382
+ """Write zeros to disk"""
383
+
384
+ def get_buffer(self, chunk_size):
385
+ """Return zero buffer slice"""
386
+ return WipeTask.zero_buffer[:chunk_size]
387
+
388
+ def get_display_name(self):
389
+ """Get display name for zeros write"""
390
+ return "Zero"
391
+
392
+
393
+ class WriteRandTask(WriteTask):
394
+ """Write random data to disk"""
395
+
396
+ def get_buffer(self, chunk_size):
397
+ """Return random buffer slice"""
398
+ return WipeTask.buffer[:chunk_size]
399
+
400
+ def get_display_name(self):
401
+ """Get display name for random write"""
402
+ return "Rand"
dwipe/main.py CHANGED
@@ -15,29 +15,54 @@ import traceback
15
15
  from .DiskWipe import DiskWipe
16
16
  from .DeviceInfo import DeviceInfo
17
17
  from .Utils import Utils
18
+ from .Prereqs import Prereqs
19
+ from .PersistentState import PersistentState
18
20
 
19
21
 
20
22
  def main():
21
23
  """Main entry point"""
22
24
  import argparse
25
+ from pathlib import Path
26
+
27
+ # Load persistent state first so we can use previous values as defaults
28
+ config_dir = Utils.get_config_dir()
29
+ config_path = config_dir / 'state.json'
30
+ persist = PersistentState(config_path=config_path)
31
+
23
32
  parser = argparse.ArgumentParser()
24
- parser.add_argument('-n', '--dry-run', action='store_true',
25
- help='just pretend to zap devices')
26
- parser.add_argument('-D', '--debug', action='count', default=0,
27
- help='debug mode (the more Ds, the higher the debug level)')
33
+ parser.add_argument('--dump-lsblk', action='store_true',
34
+ help='dump parsed lsblk and exit for debugging')
35
+ parser.add_argument('--mode', choices=['-V', '+V'], default=persist.state['wipe_mode'],
36
+ help='verification mode: -V (none) or +V (verify after wipe)')
37
+ parser.add_argument('--passes', type=int, choices=[1, 2, 4], default=persist.state['passes'],
38
+ help='number of passes for software wipes (1, 2, or 4)')
39
+ parser.add_argument('--verify-pct', type=int, choices=[1, 3, 10, 30, 100], default=persist.state['verify_pct'],
40
+ help='verification percentage after wipe (1, 3, 10, 30, or 100)')
41
+ parser.add_argument('--port-serial', choices=['Auto', 'On', 'Off'], default=persist.state['port_serial'],
42
+ help='display mode for disk port/serial info (Auto, On, or Off)')
43
+ parser.add_argument('--slowdown-stop', type=int, choices=[0, 4, 16, 64, 256], default=persist.state['slowdown_stop'],
44
+ help='stop wipe if disk slows down (0 = disabled, else check interval in ms)')
45
+ parser.add_argument('--stall-timeout', type=int, choices=[0, 60, 120, 300, 600], default=persist.state['stall_timeout'],
46
+ help='stall timeout in seconds (0 = disabled)')
47
+ parser.add_argument('--dense', choices=['True', 'False'], default='True' if persist.state['dense'] else 'False',
48
+ help='dense view: True (compact) or False (spaced lines)')
28
49
  opts = parser.parse_args()
29
50
 
51
+ # Convert string booleans to actual booleans
52
+ opts.dense = opts.dense == 'True'
53
+
30
54
  dwipe = None # Initialize to None so exception handler can reference it
31
55
  try:
32
56
  if os.geteuid() != 0:
33
57
  # Re-run the script with sudo needed and opted
34
58
  Utils.rerun_module_as_root('dwipe.main')
35
59
 
36
- dwipe = DiskWipe() # opts=opts)
37
- dwipe.dev_info = info = DeviceInfo(opts=opts)
38
- dwipe.partitions = info.assemble_partitions()
39
- if dwipe.DB:
40
- sys.exit(1)
60
+ prereqs = Prereqs(verbose=True)
61
+ # blkid for filesystem detection; hdparm and nvme for firmware wipes
62
+ prereqs.check_all(['blkid', 'hdparm', 'nvme'])
63
+ prereqs.report_and_exit_if_failed()
64
+
65
+ dwipe = DiskWipe(opts=opts, persistent_state=persist)
41
66
 
42
67
  dwipe.main_loop()
43
68
  except Exception as exce: