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.
- dwipe/DeviceChangeMonitor.py +244 -0
- dwipe/DeviceInfo.py +592 -193
- dwipe/DeviceWorker.py +572 -0
- dwipe/DiskWipe.py +569 -136
- dwipe/DrivePreChecker.py +161 -48
- dwipe/FirmwareWipeTask.py +627 -132
- dwipe/NvmeTool.py +225 -0
- dwipe/PersistentState.py +20 -9
- dwipe/SataTool.py +499 -0
- dwipe/StructuredLogger.py +4 -3
- dwipe/Tunables.py +62 -0
- dwipe/Utils.py +192 -5
- dwipe/VerifyTask.py +4 -2
- dwipe/WipeJob.py +25 -13
- dwipe/WipeTask.py +4 -2
- dwipe/WriteTask.py +1 -1
- dwipe/main.py +28 -8
- {dwipe-2.0.2.dist-info → dwipe-3.0.1.dist-info}/METADATA +218 -99
- dwipe-3.0.1.dist-info/RECORD +24 -0
- dwipe/LsblkMonitor.py +0 -124
- dwipe/ToolManager.py +0 -618
- dwipe-2.0.2.dist-info/RECORD +0 -21
- {dwipe-2.0.2.dist-info → dwipe-3.0.1.dist-info}/WHEEL +0 -0
- {dwipe-2.0.2.dist-info → dwipe-3.0.1.dist-info}/entry_points.txt +0 -0
- {dwipe-2.0.2.dist-info → dwipe-3.0.1.dist-info}/licenses/LICENSE +0 -0
dwipe/Utils.py
CHANGED
|
@@ -3,6 +3,7 @@ Utils class - Utility functions for dwipe
|
|
|
3
3
|
"""
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
+
import pwd
|
|
6
7
|
import datetime
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from .StructuredLogger import StructuredLogger
|
|
@@ -81,7 +82,6 @@ class Utils:
|
|
|
81
82
|
real_user = os.environ.get('SUDO_USER')
|
|
82
83
|
if real_user:
|
|
83
84
|
# Running with sudo - get the real user's home directory
|
|
84
|
-
import pwd
|
|
85
85
|
real_home = pwd.getpwnam(real_user).pw_dir
|
|
86
86
|
config_dir = Path(real_home) / '.config' / 'dwipe'
|
|
87
87
|
else:
|
|
@@ -93,7 +93,6 @@ class Utils:
|
|
|
93
93
|
# Fix ownership if running with sudo
|
|
94
94
|
if real_user:
|
|
95
95
|
try:
|
|
96
|
-
import pwd
|
|
97
96
|
pw_record = pwd.getpwnam(real_user)
|
|
98
97
|
uid, gid = pw_record.pw_uid, pw_record.pw_gid
|
|
99
98
|
os.chown(config_dir, uid, gid)
|
|
@@ -112,7 +111,6 @@ class Utils:
|
|
|
112
111
|
real_user = os.environ.get('SUDO_USER')
|
|
113
112
|
if real_user:
|
|
114
113
|
try:
|
|
115
|
-
import pwd
|
|
116
114
|
pw_record = pwd.getpwnam(real_user)
|
|
117
115
|
uid, gid = pw_record.pw_uid, pw_record.pw_gid
|
|
118
116
|
os.chown(file_path, uid, gid)
|
|
@@ -207,8 +205,19 @@ class Utils:
|
|
|
207
205
|
result_str = summary['result']
|
|
208
206
|
size_str = device['size']
|
|
209
207
|
time_str = summary['total_elapsed']
|
|
210
|
-
# Get rate from
|
|
211
|
-
rate_str =
|
|
208
|
+
# Get rate from wipe step (skip precheck/verify steps for firmware wipes)
|
|
209
|
+
rate_str = 'N/A'
|
|
210
|
+
for step in summary['steps']:
|
|
211
|
+
step_name = step.get('step', '').lower()
|
|
212
|
+
# Look for actual wipe steps: firmware wipes start with "firmware",
|
|
213
|
+
# software wipes have "writing" in the step name
|
|
214
|
+
# Skip precheck, pre-verify, post-verify steps
|
|
215
|
+
if step_name.startswith('firmware') or 'writing' in step_name:
|
|
216
|
+
rate_str = step.get('rate', 'N/A')
|
|
217
|
+
break
|
|
218
|
+
# Fallback to first step if no wipe step found
|
|
219
|
+
if rate_str == 'N/A' and summary['steps']:
|
|
220
|
+
rate_str = summary['steps'][0].get('rate', 'N/A')
|
|
212
221
|
|
|
213
222
|
# Build base message
|
|
214
223
|
operation = plan['operation'].capitalize()
|
|
@@ -305,3 +314,181 @@ class Utils:
|
|
|
305
314
|
Utils.fix_file_ownership(log_path)
|
|
306
315
|
except Exception:
|
|
307
316
|
pass # Don't fail if logging fails
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def parse_nvme_sanitize_capabilities(id_ctrl_data: dict) -> dict:
|
|
320
|
+
"""Parse NVMe sanitize capabilities from id-ctrl JSON data.
|
|
321
|
+
|
|
322
|
+
Extracts the sanicap field and interprets the capability bits according
|
|
323
|
+
to the NVMe specification:
|
|
324
|
+
- Bit 0: Crypto Erase
|
|
325
|
+
- Bit 1: Block Erase
|
|
326
|
+
- Bit 2: Overwrite
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
id_ctrl_data: Dictionary from nvme id-ctrl command (with -o json)
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
dict: Maps capability names to method names, e.g.,
|
|
333
|
+
{'Crypto': 'sanitize_crypto', 'Block': 'sanitize_block'}
|
|
334
|
+
"""
|
|
335
|
+
modes = {}
|
|
336
|
+
sanicap = id_ctrl_data.get('sanicap', 0)
|
|
337
|
+
|
|
338
|
+
if sanicap > 0:
|
|
339
|
+
if sanicap & 0x01:
|
|
340
|
+
modes['Crypto'] = 'sanitize_crypto'
|
|
341
|
+
if sanicap & 0x02:
|
|
342
|
+
modes['Block'] = 'sanitize_block'
|
|
343
|
+
if sanicap & 0x04:
|
|
344
|
+
modes['Ovwr'] = 'sanitize_overwrite'
|
|
345
|
+
|
|
346
|
+
return modes
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def parse_nvme_sanitize_flags(id_ctrl_data: dict) -> tuple:
|
|
350
|
+
"""Parse NVMe sanitize capabilities into individual boolean flags.
|
|
351
|
+
|
|
352
|
+
Interprets sanicap bits according to NVMe specification:
|
|
353
|
+
- Bit 0: Crypto Erase
|
|
354
|
+
- Bit 1: Block Erase
|
|
355
|
+
- Bit 2: Overwrite
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
id_ctrl_data: Dictionary from nvme id-ctrl command (with -o json)
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
tuple: (has_sanitize, crypto_erase_supported, block_erase_supported,
|
|
362
|
+
overwrite_supported)
|
|
363
|
+
"""
|
|
364
|
+
sanicap = id_ctrl_data.get('sanicap', 0)
|
|
365
|
+
has_sanitize = sanicap > 0
|
|
366
|
+
crypto_erase_supported = bool(sanicap & 0x01)
|
|
367
|
+
block_erase_supported = bool(sanicap & 0x02)
|
|
368
|
+
overwrite_supported = bool(sanicap & 0x04)
|
|
369
|
+
|
|
370
|
+
return has_sanitize, crypto_erase_supported, block_erase_supported, overwrite_supported
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ClipboardHelper:
|
|
374
|
+
"""Helper for copying text to clipboard across different Linux environments.
|
|
375
|
+
|
|
376
|
+
Supports:
|
|
377
|
+
- OSC 52 escape sequence (works over SSH if terminal supports it)
|
|
378
|
+
- Wayland (wl-copy)
|
|
379
|
+
- X11 (xclip, xsel)
|
|
380
|
+
- Terminal fallback (print to screen when clipboard unavailable)
|
|
381
|
+
|
|
382
|
+
Detection priority:
|
|
383
|
+
1. If SSH session detected -> use OSC 52 (terminal clipboard escape sequence)
|
|
384
|
+
2. If WAYLAND_DISPLAY set -> try wl-copy
|
|
385
|
+
3. If DISPLAY set -> try xclip, then xsel
|
|
386
|
+
4. Fallback -> terminal mode (manual copy)
|
|
387
|
+
"""
|
|
388
|
+
import subprocess
|
|
389
|
+
import shutil
|
|
390
|
+
|
|
391
|
+
# Singleton instance for caching probe results
|
|
392
|
+
_instance = None
|
|
393
|
+
_probed = False
|
|
394
|
+
_clipboard_cmd = None # e.g., ['wl-copy'] or ['xclip', '-selection', 'clipboard']
|
|
395
|
+
_use_osc52 = False # Use OSC 52 terminal escape sequence
|
|
396
|
+
_is_ssh = False
|
|
397
|
+
_method_name = None # Human-readable method name
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def _probe(cls):
|
|
401
|
+
"""Probe available clipboard methods (called once, results cached)."""
|
|
402
|
+
if cls._probed:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
cls._probed = True
|
|
406
|
+
import shutil
|
|
407
|
+
|
|
408
|
+
# Check for SSH session - use OSC 52 escape sequence
|
|
409
|
+
cls._is_ssh = bool(os.environ.get('SSH_CLIENT') or os.environ.get('SSH_TTY')
|
|
410
|
+
or os.environ.get('SSH_CONNECTION'))
|
|
411
|
+
|
|
412
|
+
if cls._is_ssh:
|
|
413
|
+
# OSC 52 works through terminal emulators that support it
|
|
414
|
+
# (iTerm2, kitty, alacritty, Windows Terminal, tmux with set-clipboard)
|
|
415
|
+
cls._use_osc52 = True
|
|
416
|
+
cls._method_name = 'OSC 52 (terminal escape sequence)'
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
# Check Wayland first
|
|
420
|
+
if os.environ.get('WAYLAND_DISPLAY'):
|
|
421
|
+
if shutil.which('wl-copy'):
|
|
422
|
+
cls._clipboard_cmd = ['wl-copy']
|
|
423
|
+
cls._method_name = 'wl-copy (Wayland)'
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
# Check X11
|
|
427
|
+
if os.environ.get('DISPLAY'):
|
|
428
|
+
if shutil.which('xclip'):
|
|
429
|
+
cls._clipboard_cmd = ['xclip', '-selection', 'clipboard']
|
|
430
|
+
cls._method_name = 'xclip (X11)'
|
|
431
|
+
return
|
|
432
|
+
if shutil.which('xsel'):
|
|
433
|
+
cls._clipboard_cmd = ['xsel', '--clipboard', '--input']
|
|
434
|
+
cls._method_name = 'xsel (X11)'
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
cls._method_name = 'terminal (no clipboard tool found)'
|
|
438
|
+
|
|
439
|
+
@classmethod
|
|
440
|
+
def get_method_name(cls):
|
|
441
|
+
"""Return human-readable description of clipboard method."""
|
|
442
|
+
cls._probe()
|
|
443
|
+
return cls._method_name
|
|
444
|
+
|
|
445
|
+
@classmethod
|
|
446
|
+
def has_clipboard(cls):
|
|
447
|
+
"""Return True if a clipboard method is available (tool or OSC 52)."""
|
|
448
|
+
cls._probe()
|
|
449
|
+
return cls._clipboard_cmd is not None or cls._use_osc52
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def copy(cls, text):
|
|
453
|
+
"""Copy text to clipboard.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
text: String to copy
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
tuple: (success: bool, error_message: str or None)
|
|
460
|
+
"""
|
|
461
|
+
cls._probe()
|
|
462
|
+
import subprocess
|
|
463
|
+
import base64
|
|
464
|
+
|
|
465
|
+
# Try OSC 52 escape sequence (works over SSH with supporting terminals)
|
|
466
|
+
if cls._use_osc52:
|
|
467
|
+
try:
|
|
468
|
+
encoded = base64.b64encode(text.encode('utf-8')).decode('ascii')
|
|
469
|
+
# OSC 52: \033]52;c;<base64>\a
|
|
470
|
+
# 'c' = clipboard selection
|
|
471
|
+
osc52 = f'\033]52;c;{encoded}\a'
|
|
472
|
+
sys.stdout.write(osc52)
|
|
473
|
+
sys.stdout.flush()
|
|
474
|
+
return True, None
|
|
475
|
+
except Exception as e:
|
|
476
|
+
return False, f"OSC 52 failed: {e}"
|
|
477
|
+
|
|
478
|
+
if not cls._clipboard_cmd:
|
|
479
|
+
return False, "No clipboard available"
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
proc = subprocess.run(
|
|
483
|
+
cls._clipboard_cmd,
|
|
484
|
+
input=text.encode('utf-8'),
|
|
485
|
+
capture_output=True,
|
|
486
|
+
timeout=5
|
|
487
|
+
)
|
|
488
|
+
if proc.returncode == 0:
|
|
489
|
+
return True, None
|
|
490
|
+
return False, proc.stderr.decode('utf-8', errors='replace').strip()
|
|
491
|
+
except subprocess.TimeoutExpired:
|
|
492
|
+
return False, "Clipboard command timed out"
|
|
493
|
+
except Exception as e:
|
|
494
|
+
return False, str(e)
|
dwipe/VerifyTask.py
CHANGED
|
@@ -310,7 +310,9 @@ class VerifyTask(WipeTask):
|
|
|
310
310
|
def get_status(self):
|
|
311
311
|
"""Get current progress status (thread-safe, called from main thread)
|
|
312
312
|
|
|
313
|
-
Returns
|
|
313
|
+
Returns:
|
|
314
|
+
tuple: (elapsed_str, pct_str, rate_str, eta_str, more_state)
|
|
315
|
+
- pct_str has 'v' prefix (e.g., "v45%")
|
|
314
316
|
"""
|
|
315
317
|
mono = time.monotonic()
|
|
316
318
|
elapsed_time = mono - self.start_mono
|
|
@@ -347,7 +349,7 @@ class VerifyTask(WipeTask):
|
|
|
347
349
|
else:
|
|
348
350
|
when_str = '0'
|
|
349
351
|
|
|
350
|
-
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
352
|
+
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str, self.more_state
|
|
351
353
|
|
|
352
354
|
def get_summary_dict(self):
|
|
353
355
|
"""Get final summary for this verify task
|
dwipe/WipeJob.py
CHANGED
|
@@ -18,6 +18,7 @@ from .Utils import Utils
|
|
|
18
18
|
from .WipeTask import WipeTask
|
|
19
19
|
from .WriteTask import WriteTask, WriteZeroTask, WriteRandTask
|
|
20
20
|
from .VerifyTask import VerifyTask, VerifyZeroTask, VerifyRandTask
|
|
21
|
+
from .FirmwareWipeTask import FirmwareWipeTask
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class WipeJob:
|
|
@@ -166,7 +167,7 @@ class WipeJob:
|
|
|
166
167
|
|
|
167
168
|
## SLOWDOWN / STALL DETECTION/ABORT FEATURE (proxied from tasks)
|
|
168
169
|
##
|
|
169
|
-
self.slowdown_stop = getattr(opts, 'slowdown_stop',
|
|
170
|
+
self.slowdown_stop = getattr(opts, 'slowdown_stop', 64)
|
|
170
171
|
self.stall_timeout = getattr(opts, 'stall_timeout', 60)
|
|
171
172
|
self.max_slowdown_ratio = 0
|
|
172
173
|
self.max_stall_secs = 0
|
|
@@ -230,6 +231,9 @@ class WipeJob:
|
|
|
230
231
|
self.current_task = task
|
|
231
232
|
self.current_task_index = i
|
|
232
233
|
|
|
234
|
+
# Set job reference so tasks can access shared state (e.g., for firmware verify)
|
|
235
|
+
task.job = self
|
|
236
|
+
|
|
233
237
|
# Run the task
|
|
234
238
|
task.run_task()
|
|
235
239
|
|
|
@@ -264,17 +268,22 @@ class WipeJob:
|
|
|
264
268
|
summary = task.get_summary_dict()
|
|
265
269
|
self.verify_result = summary.get('result', None)
|
|
266
270
|
|
|
271
|
+
# Write marker with verify status after verification completes
|
|
272
|
+
if not task.exception and not self.do_abort and self.verify_result:
|
|
273
|
+
is_random = (self.expected_pattern == "random")
|
|
274
|
+
self._write_marker_with_verify_status(is_random)
|
|
275
|
+
|
|
267
276
|
# Check for task errors (AFTER proxying state)
|
|
268
277
|
if task.exception:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
278
|
+
# For write tasks and firmware wipe tasks, failure is critical - set exception and break
|
|
279
|
+
if isinstance(task, (WriteTask, FirmwareWipeTask)):
|
|
280
|
+
self.exception = task.exception
|
|
272
281
|
# Sync abort state before breaking
|
|
273
282
|
if task.do_abort:
|
|
274
283
|
self.do_abort = True
|
|
275
284
|
break
|
|
276
|
-
# For
|
|
277
|
-
# (wipe
|
|
285
|
+
# For other tasks (precheck, pre-verify, post-verify), continue without setting job exception
|
|
286
|
+
# (these are support tasks - only the actual wipe matters for job success/failure)
|
|
278
287
|
|
|
279
288
|
# Check if task was aborted (sync abort state)
|
|
280
289
|
if task.do_abort and not self.do_abort:
|
|
@@ -588,12 +597,13 @@ class WipeJob:
|
|
|
588
597
|
f"Completed: {percent_complete:.2f}%")
|
|
589
598
|
|
|
590
599
|
def get_status(self):
|
|
591
|
-
"""Get status tuple: (elapsed, percent, rate, eta)
|
|
600
|
+
"""Get status tuple: (elapsed, percent, rate, eta, more_state)
|
|
592
601
|
|
|
593
602
|
Returns stats for current phase only:
|
|
594
603
|
- Write phase (0-100%): elapsed/rate/eta for writing
|
|
595
604
|
- Flushing phase: 100% FLUSH while kernel syncs to device
|
|
596
605
|
- Verify phase (v0-v100%): elapsed/rate/eta for verification only
|
|
606
|
+
- more_state: optional extra status from derived task class
|
|
597
607
|
"""
|
|
598
608
|
# NEW: Proxy to current task if using task-based architecture
|
|
599
609
|
if self.current_task is not None:
|
|
@@ -646,7 +656,7 @@ class WipeJob:
|
|
|
646
656
|
else:
|
|
647
657
|
when_str = '0'
|
|
648
658
|
|
|
649
|
-
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
659
|
+
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str, ""
|
|
650
660
|
else:
|
|
651
661
|
# Write phase: 0-100% (across all passes)
|
|
652
662
|
written = self.total_written
|
|
@@ -683,7 +693,7 @@ class WipeJob:
|
|
|
683
693
|
else:
|
|
684
694
|
when_str = '0'
|
|
685
695
|
|
|
686
|
-
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
696
|
+
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str, ""
|
|
687
697
|
|
|
688
698
|
def get_plan_dict(self, mode=None):
|
|
689
699
|
"""Generate plan dictionary for structured logging
|
|
@@ -763,7 +773,7 @@ class WipeJob:
|
|
|
763
773
|
|
|
764
774
|
# Build top-level summary
|
|
765
775
|
summary = {
|
|
766
|
-
"result": "stopped" if self.do_abort else "completed",
|
|
776
|
+
"result": "stopped" if self.do_abort else ("failed" if self.exception else "completed"),
|
|
767
777
|
"total_elapsed": Utils.ago_str(int(total_elapsed)),
|
|
768
778
|
"total_errors": total_errors,
|
|
769
779
|
"pct_complete": round(pct_complete, 1),
|
|
@@ -850,7 +860,7 @@ class WipeJob:
|
|
|
850
860
|
|
|
851
861
|
# Build top-level summary
|
|
852
862
|
summary = {
|
|
853
|
-
"result": "stopped" if self.do_abort else "completed",
|
|
863
|
+
"result": "stopped" if self.do_abort else ("failed" if self.exception else "completed"),
|
|
854
864
|
"total_elapsed": Utils.ago_str(int(total_elapsed)),
|
|
855
865
|
"total_errors": self.total_errors,
|
|
856
866
|
"pct_complete": round(pct_complete, 1),
|
|
@@ -1223,8 +1233,10 @@ class WipeJob:
|
|
|
1223
1233
|
# Auto-start verification if enabled and write completed successfully
|
|
1224
1234
|
verify_pct = getattr(self.opts, 'verify_pct', 0)
|
|
1225
1235
|
auto_verify = getattr(self.opts, 'wipe_mode', "").endswith('+V')
|
|
1226
|
-
if auto_verify and
|
|
1227
|
-
|
|
1236
|
+
if auto_verify and not self.do_abort and not self.exception:
|
|
1237
|
+
# Default to 2% verification if +V mode enabled but verify_pct not set
|
|
1238
|
+
actual_verify_pct = verify_pct if verify_pct > 0 else 2
|
|
1239
|
+
self.verify_partition(actual_verify_pct)
|
|
1228
1240
|
# Write marker with verification status after verification completes
|
|
1229
1241
|
# Use desired_mode to determine if random or zero
|
|
1230
1242
|
is_random = (desired_mode == 'Rand')
|
dwipe/WipeTask.py
CHANGED
|
@@ -57,6 +57,7 @@ class WipeTask:
|
|
|
57
57
|
self.do_abort = False
|
|
58
58
|
self.done = False
|
|
59
59
|
self.exception = None
|
|
60
|
+
self.more_state = "" # Optional extra status info from derived classes
|
|
60
61
|
|
|
61
62
|
# Progress tracking
|
|
62
63
|
self.total_written = 0 # Bytes processed (write or verify)
|
|
@@ -80,11 +81,12 @@ class WipeTask:
|
|
|
80
81
|
"""Get current progress status (thread-safe, called from main thread)
|
|
81
82
|
|
|
82
83
|
Returns:
|
|
83
|
-
tuple: (elapsed_str, pct_str, rate_str, eta_str)
|
|
84
|
+
tuple: (elapsed_str, pct_str, rate_str, eta_str, more_state)
|
|
84
85
|
- elapsed_str: e.g., "5m23s"
|
|
85
86
|
- pct_str: e.g., "45%" or "v23%" (for verify)
|
|
86
87
|
- rate_str: e.g., "450MB/s"
|
|
87
88
|
- eta_str: e.g., "2m15s"
|
|
89
|
+
- more_state: optional extra status from derived class
|
|
88
90
|
"""
|
|
89
91
|
mono = time.monotonic()
|
|
90
92
|
elapsed_time = mono - self.start_mono
|
|
@@ -116,7 +118,7 @@ class WipeTask:
|
|
|
116
118
|
else:
|
|
117
119
|
when_str = '0'
|
|
118
120
|
|
|
119
|
-
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
121
|
+
return Utils.ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str, self.more_state
|
|
120
122
|
|
|
121
123
|
def get_summary_dict(self):
|
|
122
124
|
"""Get final summary after task completion
|
dwipe/WriteTask.py
CHANGED
|
@@ -54,7 +54,7 @@ class WriteTask(WipeTask):
|
|
|
54
54
|
self.marker_update_interval = 30 # Update every 30 seconds
|
|
55
55
|
|
|
56
56
|
# Performance monitoring
|
|
57
|
-
self.slowdown_stop = getattr(opts, 'slowdown_stop',
|
|
57
|
+
self.slowdown_stop = getattr(opts, 'slowdown_stop', 64)
|
|
58
58
|
self.stall_timeout = getattr(opts, 'stall_timeout', 60)
|
|
59
59
|
self.max_slowdown_ratio = 0
|
|
60
60
|
self.max_stall_secs = 0
|
dwipe/main.py
CHANGED
|
@@ -16,18 +16,41 @@ from .DiskWipe import DiskWipe
|
|
|
16
16
|
from .DeviceInfo import DeviceInfo
|
|
17
17
|
from .Utils import Utils
|
|
18
18
|
from .Prereqs import Prereqs
|
|
19
|
+
from .PersistentState import PersistentState
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def main():
|
|
22
23
|
"""Main entry point"""
|
|
23
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
|
+
|
|
24
32
|
parser = argparse.ArgumentParser()
|
|
25
33
|
parser.add_argument('--dump-lsblk', action='store_true',
|
|
26
34
|
help='dump parsed lsblk and exit for debugging')
|
|
27
|
-
parser.add_argument('
|
|
28
|
-
help='
|
|
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)')
|
|
29
49
|
opts = parser.parse_args()
|
|
30
50
|
|
|
51
|
+
# Convert string booleans to actual booleans
|
|
52
|
+
opts.dense = opts.dense == 'True'
|
|
53
|
+
|
|
31
54
|
dwipe = None # Initialize to None so exception handler can reference it
|
|
32
55
|
try:
|
|
33
56
|
if os.geteuid() != 0:
|
|
@@ -35,14 +58,11 @@ def main():
|
|
|
35
58
|
Utils.rerun_module_as_root('dwipe.main')
|
|
36
59
|
|
|
37
60
|
prereqs = Prereqs(verbose=True)
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
prereqs.check_all(['lsblk', 'hdparm', 'nvme'])
|
|
41
|
-
else:
|
|
42
|
-
prereqs.check_all(['lsblk'])
|
|
61
|
+
# blkid for filesystem detection; hdparm and nvme for firmware wipes
|
|
62
|
+
prereqs.check_all(['blkid', 'hdparm', 'nvme'])
|
|
43
63
|
prereqs.report_and_exit_if_failed()
|
|
44
64
|
|
|
45
|
-
dwipe = DiskWipe(opts=opts
|
|
65
|
+
dwipe = DiskWipe(opts=opts, persistent_state=persist)
|
|
46
66
|
|
|
47
67
|
dwipe.main_loop()
|
|
48
68
|
except Exception as exce:
|