dwipe 2.0.1__py3-none-any.whl → 2.0.2__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/ToolManager.py CHANGED
@@ -18,77 +18,6 @@ from dataclasses import dataclass
18
18
  # Part 1: Tool Manager (Dependency Management)
19
19
  # ============================================================================
20
20
 
21
- class ToolManager:
22
- """Manages tool dependencies (hdparm, nvme-cli)"""
23
-
24
- TOOL_PACKAGES = {
25
- 'hdparm': {
26
- 'apt': ['hdparm'],
27
- 'dnf': ['hdparm'],
28
- 'yum': ['hdparm'],
29
- 'pacman': ['hdparm'],
30
- 'zypper': ['hdparm'],
31
- 'apk': ['hdparm'],
32
- 'brew': ['hdparm'],
33
- },
34
- 'nvme': {
35
- 'apt': ['nvme-cli'],
36
- 'dnf': ['nvme-cli'],
37
- 'yum': ['nvme-cli'],
38
- 'pacman': ['nvme-cli'],
39
- 'zypper': ['nvme-cli'],
40
- 'apk': ['nvme-cli'],
41
- 'brew': ['nvme-cli'],
42
- }
43
- }
44
-
45
- def __init__(self, auto_install: bool = False, verbose: bool = False):
46
- self.auto_install = auto_install
47
- self.verbose = verbose
48
- self.package_manager = self._detect_package_manager()
49
-
50
- def _detect_package_manager(self) -> Optional[str]:
51
- package_managers = {
52
- 'apt': ['apt-get', 'apt'],
53
- 'dnf': ['dnf'],
54
- 'yum': ['yum'],
55
- 'pacman': ['pacman'],
56
- 'zypper': ['zypper'],
57
- 'apk': ['apk'],
58
- 'brew': ['brew'],
59
- }
60
-
61
- for pm, binaries in package_managers.items():
62
- for binary in binaries:
63
- if shutil.which(binary):
64
- return pm
65
- return None
66
-
67
- def tool_available(self, tool_name: str) -> bool:
68
- return shutil.which(tool_name) is not None
69
-
70
- def ensure_tool(self, tool_name: str, critical: bool = True) -> bool:
71
- if self.tool_available(tool_name):
72
- return True
73
-
74
- if self.auto_install and self._install_tool(tool_name):
75
- return True
76
-
77
- if critical:
78
- print(f"ERROR: Required tool '{tool_name}' not found")
79
- packages = self.TOOL_PACKAGES.get(tool_name, {}).get(self.package_manager, [])
80
- if packages:
81
- print(f"Install with: sudo {self.package_manager} install {packages[0]}")
82
- return False
83
-
84
- def _install_tool(self, tool_name: str) -> bool:
85
- """Install tool using package manager"""
86
- # Simplified - use the installation logic from earlier if needed
87
- return False # Placeholder
88
-
89
- def get_tool_path(self, tool_name: str) -> Optional[str]:
90
- return shutil.which(tool_name)
91
-
92
21
  # ============================================================================
93
22
  # Part 2: Drive Pre-Checks
94
23
  # ============================================================================
@@ -102,7 +31,7 @@ class EraseStatus(Enum):
102
31
  UNKNOWN = "unknown"
103
32
 
104
33
  @dataclass
105
- class PreCheckResult:
34
+ class OLD-PreCheckResult:
106
35
  compatible: bool = False
107
36
  tool: Optional[str] = None
108
37
  frozen: bool = False
@@ -110,21 +39,32 @@ class PreCheckResult:
110
39
  enhanced_supported: bool = False
111
40
  issues: List[str] = None
112
41
  recommendation: Optional[str] = None
113
-
42
+
43
+ @dataclass
44
+ class PreCheckResult:
45
+ # compatible: bool = False
46
+ # tool: Optional[str] = None
47
+ # frozen: bool = False
48
+ # locked: bool = False
49
+ # enhanced_supported: bool = False
50
+ issues: List[str] = None # list of "why not" ... any set, no wipe
51
+ # recommendation: Optional[str] = None
52
+ modes = {} # dict of descr/how 'Cropto': '--wipe=crypto'
53
+
114
54
  def __post_init__(self):
115
55
  if self.issues is None:
116
56
  self.issues = []
117
57
 
118
58
  class DrivePreChecker:
119
59
  """Pre-check drive before attempting secure erase"""
120
-
60
+
121
61
  def __init__(self, timeout: int = 10):
122
62
  self.timeout = timeout
123
-
63
+
124
64
  def is_usb_attached(self, device: str) -> bool:
125
65
  """Check if device is USB-attached"""
126
66
  dev_name = os.path.basename(device)
127
-
67
+
128
68
  # Check via sysfs
129
69
  sys_path = f'/sys/block/{dev_name}'
130
70
  if os.path.exists(sys_path):
@@ -133,7 +73,7 @@ class DrivePreChecker:
133
73
  real_path = os.path.realpath(sys_path)
134
74
  if 'usb' in real_path.lower():
135
75
  return True
136
-
76
+
137
77
  # Check via udev
138
78
  udev_info = subprocess.run(
139
79
  ['udevadm', 'info', '-q', 'property', '-n', device],
@@ -145,144 +85,125 @@ class DrivePreChecker:
145
85
  return True
146
86
  except:
147
87
  pass
148
-
88
+
149
89
  return False
150
-
90
+
151
91
  def check_nvme_drive(self, device: str) -> PreCheckResult:
152
- """Check if NVMe secure erase will likely work"""
153
- result = PreCheckResult(tool='nvme')
154
-
92
+ """Probes NVMe and returns specific command flags for available wipe modes"""
93
+ result = PreCheckResult()
94
+ result.modes = {}
95
+
155
96
  try:
156
- # Check if device exists
157
- if not os.path.exists(device):
158
- result.issues.append(f"Device {device} does not exist")
159
- return result
160
-
161
- # Check USB attachment
162
- if self.is_usb_attached(device):
163
- result.issues.append("NVMe is USB-attached - hardware erase unreliable")
164
- result.recommendation = "Use software wipe"
165
- return result
166
-
167
- # Check if NVMe device responds
97
+ # Get controller capabilities in JSON for easy parsing
168
98
  id_ctrl = subprocess.run(
169
- ['nvme', 'id-ctrl', device],
170
- capture_output=True,
171
- text=True,
172
- timeout=self.timeout
99
+ ['nvme', 'id-ctrl', device, '-o', 'json'],
100
+ capture_output=True, text=True, timeout=self.timeout
173
101
  )
174
-
102
+
175
103
  if id_ctrl.returncode != 0:
176
- result.issues.append(f"Not an NVMe device: {id_ctrl.stderr}")
104
+ result.issues.append("NVMe controller unresponsive")
177
105
  return result
178
-
179
- # Check format support
180
- if 'Format NVM' not in id_ctrl.stdout:
181
- result.issues.append("Drive doesn't support Format NVM command")
182
-
183
- # Check for write protection
184
- id_ns = subprocess.run(
185
- ['nvme', 'id-ns', device],
186
- capture_output=True,
187
- text=True,
188
- timeout=self.timeout
189
- )
190
-
191
- if id_ns.returncode == 0 and 'Write Protected' in id_ns.stdout:
192
- result.issues.append("Namespace is write protected")
193
-
194
- result.compatible = len(result.issues) == 0
195
- result.recommendation = "Proceed with hardware erase" if result.compatible else "Use software wipe"
196
-
197
- except subprocess.TimeoutExpired:
198
- result.issues.append(f"Command timed out after {self.timeout}s")
106
+
107
+ import json
108
+ data = json.loads(id_ctrl.stdout)
109
+
110
+ # 1. Check for Sanitize Capabilities (The most modern/safe method)
111
+ # Bit 1: Block, Bit 2: Crypto, Bit 3: Overwrite
112
+ sanicap = data.get('sanicap', 0)
113
+ if sanicap > 0:
114
+ # We use OrderedDict or similar to put the 'best' options first
115
+ if sanicap & 0x04: # Crypto Erase
116
+ result.modes['Sanitize-Crypto'] = 'sanitize --action=0x04'
117
+ if sanicap & 0x02: # Block Erase (Physical)
118
+ result.modes['Sanitize-Block'] = 'sanitize --action=0x02'
119
+ if sanicap & 0x08: # Overwrite
120
+ result.modes['Sanitize-Overwrite'] = 'sanitize --action=0x03'
121
+
122
+ # 2. Check for Legacy Format Capabilities
123
+ # Bit 1: Crypto, Bit 2: User Data Erase
124
+ fna = data.get('fna', 0)
125
+ if 'Format NVM' in id_ctrl.stdout:
126
+ # Check if Crypto Erase is supported via Format
127
+ if (fna >> 2) & 0x1:
128
+ result.modes['Format-Crypto'] = 'format --ses=2'
129
+ # Standard User Data Erase
130
+ result.modes['Format-Erase'] = 'format --ses=1'
131
+
132
+ # Final Validation
133
+ if not result.modes:
134
+ result.issues.append("No HW wipe modes (Sanitize/Format) supported")
135
+
199
136
  except Exception as e:
200
- result.issues.append(f"Unexpected error: {e}")
201
-
137
+ result.issues.append(f"Probe Error: {str(e)}")
138
+
202
139
  return result
203
-
140
+
204
141
  def check_ata_drive(self, device: str) -> PreCheckResult:
205
- """Check if ATA secure erase will likely work"""
206
- result = PreCheckResult(tool='hdparm')
207
-
142
+ """Probes SATA/ATA and returns hdparm flags or specific blocking reasons
143
+ + Why the "NULL" password? In the modes dictionary above, we use NULL.
144
+ - To perform an ATA Secure Erase, you have to set a temporary password first,
145
+ then immediately issue the erase command with that same password.
146
+ - Most tools (and hdparm itself) use NULL or a simple string like p
147
+ as a throwaway.
148
+ - Note: If the dwipe app crashes after setting the password but before the
149
+ erase finishes, the drive will stay locked. On the next run, your enabled
150
+ check (Step 3) will catch this.
151
+ + Handling "Frozen" in the UI
152
+ -the "Frozen" issue is the one that will frustrate users most.
153
+ -The "Short Crisp Reason": Drive is FROZEN.
154
+ - The Fix: To unfreeze, try suspending (sleeping) and waking the computer,
155
+ or re-plugging the drive's power cable."
156
+
157
+ + Now, dwipe builds that list:
158
+ It calls can_use_hardware_erase().
159
+ It looks at result.issues. If empty, the [f]:irmW key is active.
160
+ """
161
+ result = PreCheckResult()
162
+ result.modes = {}
163
+
208
164
  try:
209
- if not os.path.exists(device):
210
- result.issues.append(f"Device {device} does not exist")
211
- return result
212
-
213
- # Check USB attachment
214
- if self.is_usb_attached(device):
215
- result.issues.append("Drive is USB-attached - hardware erase unreliable")
216
- result.recommendation = "Use software wipe"
217
- return result
218
-
219
- # Get drive info
165
+ # Get drive info via hdparm
220
166
  info = subprocess.run(
221
167
  ['hdparm', '-I', device],
222
- capture_output=True,
223
- text=True,
224
- timeout=self.timeout
168
+ capture_output=True, text=True, timeout=self.timeout
225
169
  )
226
-
170
+
227
171
  if info.returncode != 0:
228
- result.issues.append(f"Drive not responsive: {info.stderr}")
172
+ result.issues.append("Drive not responsive to hdparm")
229
173
  return result
230
-
231
- output = info.stdout
232
-
233
- # Check if frozen
234
- if 'frozen' in output.lower():
235
- result.frozen = True
236
- result.issues.append("Drive is FROZEN - will hang on erase")
237
-
238
- # Check if locked/enabled
239
- if 'enabled' in output and 'not' not in output:
240
- result.locked = True
241
- result.issues.append("Security is ENABLED - needs password")
242
-
243
- # Check enhanced erase support
244
- if 'supported: enhanced erase' in output:
245
- result.enhanced_supported = True
246
-
247
- # Check ATA device and erase support
248
- if 'ATA' not in output and 'SATA' not in output:
249
- result.issues.append("Not an ATA/SATA device")
250
-
251
- if 'SECURITY ERASE UNIT' not in output:
252
- result.issues.append("Drive doesn't support SECURITY ERASE UNIT")
253
-
254
- result.compatible = len(result.issues) == 0
255
-
256
- if result.compatible:
257
- result.recommendation = "Proceed with hardware erase"
258
- elif result.frozen:
259
- result.recommendation = "Thaw drive first or use software wipe"
260
- elif result.locked:
261
- result.recommendation = "Disable security first or use software wipe"
262
- else:
263
- result.recommendation = "Use software wipe"
264
-
265
- except subprocess.TimeoutExpired:
266
- result.issues.append(f"Command timed out after {self.timeout}s")
174
+
175
+ out = info.stdout.lower()
176
+
177
+ # 1. Check if the drive even supports Security Erase
178
+ if "security erase unit" not in out:
179
+ result.issues.append("Drive does not support ATA Security Erase")
180
+ return result
181
+
182
+ # 2. Check for "Frozen" state (The most common blocker)
183
+ # A frozen drive rejects security commands until a power cycle.
184
+ if "frozen" in out and "not frozen" not in out:
185
+ result.issues.append("Drive is FROZEN (BIOS/OS lock)")
186
+ # You might want to keep this in issues so user can't select it,
187
+ # or move it to a 'warning' if you want to allow them to try anyway.
188
+
189
+ # 3. Check if security is already "Enabled" (Drive is locked)
190
+ if "enabled" in out and "not enabled" not in out:
191
+ # If it's already locked, we can't wipe without the existing password.
192
+ result.issues.append("Security is ENABLED (Drive is password locked)")
193
+
194
+ # 4. Populate Modes if no fatal issues
195
+ if not result.issues:
196
+ # Enhanced Erase: Usually writes a pattern or destroys encryption keys
197
+ if "enhanced erase" in out:
198
+ result.modes['ATA-Enhanced'] = '--user-master u --security-erase-enhanced NULL'
199
+
200
+ # Normal Erase: Usually writes zeros to the whole platter
201
+ result.modes['ATA-Normal'] = '--user-master u --security-erase NULL'
202
+
267
203
  except Exception as e:
268
- result.issues.append(f"Unexpected error: {e}")
269
-
204
+ result.issues.append(f"ATA Probe Error: {str(e)}")
270
205
  return result
271
-
272
- def can_use_hardware_erase(self, device: str) -> PreCheckResult:
273
- """
274
- Determine if hardware erase will work.
275
- Returns comprehensive pre-check result.
276
- """
277
- if not os.path.exists(device):
278
- return PreCheckResult(issues=[f"Device {device} does not exist"])
279
-
280
- if 'nvme' in device:
281
- return self.check_nvme_drive(device)
282
- elif device.startswith('/dev/sd'):
283
- return self.check_ata_drive(device)
284
- else:
285
- return PreCheckResult(issues=[f"Unsupported device type: {device}"])
206
+
286
207
 
287
208
  # ============================================================================
288
209
  # Part 3: Drive Eraser with Monitoring
@@ -290,14 +211,56 @@ class DrivePreChecker:
290
211
 
291
212
  class DriveEraser:
292
213
  """Execute and monitor hardware secure erase"""
293
-
214
+
294
215
  def __init__(self, progress_callback: Optional[Callable] = None):
295
216
  self.status = EraseStatus.NOT_STARTED
296
217
  self.start_time = None
297
218
  self.progress_callback = progress_callback
298
219
  self.monitor_thread = None
299
220
  self.current_process = None
300
-
221
+
222
+ def run_firmware_wipe(self):
223
+ """The thread target for firmware wipes"""
224
+ self.start_mono = time.monotonic()
225
+
226
+ # 1. Start the process (non-blocking)
227
+ # self.opts.hw_cmd might be: "nvme sanitize --action=0x02"
228
+ full_cmd = f"{self.opts.tool} {self.opts.hw_cmd} {self.device_path}"
229
+ self.process = subprocess.Popen(full_cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
230
+
231
+ # 2. Polling Loop
232
+ while not self._abort_requested:
233
+ if self.process.poll() is not None:
234
+ break
235
+
236
+ # Optional: For NVMe, you can poll 'nvme sanitize-log' here
237
+ # to get actual 0-100% progress and update self.total_written
238
+
239
+ time.sleep(1)
240
+
241
+ # 3. Finalize
242
+ self.finish_mono = time.monotonic()
243
+ if self.process.returncode == 0:
244
+ self.total_written = self.total_size # Mark as done for the UI
245
+
246
+ def abort(self):
247
+ self._abort_requested = True
248
+ if self.process and self.process.poll() is None:
249
+ self.process.terminate() # Try nice first
250
+ time.sleep(0.5)
251
+ self.process.kill() # Then hammer it
252
+
253
+ """
254
+ # Inside get_summary_dict...
255
+ is_hw = getattr(self.opts, 'is_hardware', False)
256
+
257
+ if is_hw:
258
+ # Rate and written bytes don't follow standard rules
259
+ wipe_step["rate"] = "Hardware"
260
+ wipe_step["status"] = "Sanitizing..." if not self.done else "Complete"
261
+
262
+ """
263
+
301
264
  def start_nvme_erase(self, device: str) -> bool:
302
265
  """Start NVMe secure erase (non-blocking)"""
303
266
  try:
@@ -307,17 +270,17 @@ class DriveEraser:
307
270
  stderr=subprocess.PIPE,
308
271
  text=True
309
272
  )
310
-
273
+
311
274
  self.status = EraseStatus.STARTING
312
275
  self.start_time = time.time()
313
276
  self._start_monitoring(device, 'nvme')
314
277
  return True
315
-
278
+
316
279
  except Exception as e:
317
280
  print(f"Failed to start NVMe erase: {e}")
318
281
  self.status = EraseStatus.FAILED
319
282
  return False
320
-
283
+
321
284
  def start_ata_erase(self, device: str, enhanced: bool = True) -> bool:
322
285
  """Start ATA secure erase (non-blocking)"""
323
286
  try:
@@ -328,33 +291,33 @@ class DriveEraser:
328
291
  else:
329
292
  cmd.extend(['--security-erase', 'NULL'])
330
293
  cmd.append(device)
331
-
294
+
332
295
  self.current_process = subprocess.Popen(
333
296
  cmd,
334
297
  stdout=subprocess.PIPE,
335
298
  stderr=subprocess.PIPE,
336
299
  text=True
337
300
  )
338
-
301
+
339
302
  self.status = EraseStatus.STARTING
340
303
  self.start_time = time.time()
341
304
  self._start_monitoring(device, 'ata')
342
305
  return True
343
-
306
+
344
307
  except Exception as e:
345
308
  print(f"Failed to start ATA erase: {e}")
346
309
  self.status = EraseStatus.FAILED
347
310
  return False
348
-
311
+
349
312
  def _start_monitoring(self, device: str, drive_type: str):
350
313
  """Start background monitoring thread"""
351
314
  def monitor():
352
315
  time.sleep(3) # Let command start
353
316
  self.status = EraseStatus.IN_PROGRESS
354
-
317
+
355
318
  check_interval = 5
356
319
  max_checks = 7200 # 10 hours max
357
-
320
+
358
321
  for _ in range(max_checks):
359
322
  # Check if process completed
360
323
  if self.current_process and self.current_process.poll() is not None:
@@ -363,20 +326,20 @@ class DriveEraser:
363
326
  else:
364
327
  self.status = EraseStatus.FAILED
365
328
  break
366
-
329
+
367
330
  # Update progress callback
368
331
  if self.progress_callback:
369
332
  elapsed = time.time() - self.start_time
370
333
  progress = self._estimate_progress(elapsed, drive_type)
371
334
  self.progress_callback(progress, elapsed, self.status)
372
-
335
+
373
336
  time.sleep(check_interval)
374
337
  else:
375
338
  self.status = EraseStatus.FAILED
376
-
339
+
377
340
  self.monitor_thread = threading.Thread(target=monitor, daemon=True)
378
341
  self.monitor_thread.start()
379
-
342
+
380
343
  def _estimate_progress(self, elapsed_seconds: float, drive_type: str) -> float:
381
344
  """Estimate fake progress based on typical times"""
382
345
  if drive_type == 'nvme':
@@ -386,25 +349,25 @@ class DriveEraser:
386
349
  progress = min(1.0, elapsed_seconds / 3600)
387
350
  else:
388
351
  progress = 0.0
389
-
352
+
390
353
  return progress * 100
391
-
354
+
392
355
  def get_status(self) -> Dict:
393
356
  """Get current status info"""
394
357
  elapsed = time.time() - self.start_time if self.start_time else 0
395
-
358
+
396
359
  return {
397
360
  'status': self.status.value,
398
361
  'elapsed_seconds': elapsed,
399
362
  'monitor_alive': self.monitor_thread and self.monitor_thread.is_alive(),
400
363
  'process_active': self.current_process and self.current_process.poll() is None
401
364
  }
402
-
365
+
403
366
  def wait_for_completion(self, timeout: Optional[float] = None) -> bool:
404
367
  """Wait for erase to complete"""
405
368
  if not self.current_process:
406
369
  return False
407
-
370
+
408
371
  try:
409
372
  return_code = self.current_process.wait(timeout=timeout)
410
373
  return return_code == 0
@@ -420,17 +383,17 @@ class HardwareWipeController:
420
383
  Main controller for hardware wiping.
421
384
  This is what you'd integrate into dwipe.
422
385
  """
423
-
386
+
424
387
  def __init__(self, auto_install_tools: bool = False, verbose: bool = False):
425
388
  self.tool_mgr = ToolManager(auto_install=auto_install_tools, verbose=verbose)
426
389
  self.pre_checker = DrivePreChecker(timeout=15)
427
390
  self.eraser = None
428
391
  self.verbose = verbose
429
-
392
+
430
393
  def _log(self, message: str):
431
394
  if self.verbose:
432
395
  print(f"[HardwareWipe] {message}")
433
-
396
+
434
397
  def prepare(self) -> bool:
435
398
  """Ensure required tools are available"""
436
399
  if not self.tool_mgr.ensure_tool('hdparm', critical=True):
@@ -438,12 +401,12 @@ class HardwareWipeController:
438
401
  if not self.tool_mgr.ensure_tool('nvme', critical=True):
439
402
  return False
440
403
  return True
441
-
404
+
442
405
  def pre_check(self, device: str) -> PreCheckResult:
443
406
  """Perform comprehensive pre-check"""
444
407
  self._log(f"Pre-checking {device}...")
445
408
  result = self.pre_checker.can_use_hardware_erase(device)
446
-
409
+
447
410
  if self.verbose:
448
411
  print(f"Pre-check for {device}:")
449
412
  print(f" Compatible: {result.compatible}")
@@ -452,112 +415,112 @@ class HardwareWipeController:
452
415
  print(f" Issues: {', '.join(result.issues)}")
453
416
  if result.recommendation:
454
417
  print(f" Recommendation: {result.recommendation}")
455
-
418
+
456
419
  return result
457
-
420
+
458
421
  def wipe(self, device: str, fallback_callback: Optional[Callable] = None) -> bool:
459
422
  """
460
423
  Execute hardware wipe with automatic fallback.
461
-
424
+
462
425
  Args:
463
426
  device: Device path (/dev/sda, /dev/nvme0n1, etc.)
464
427
  fallback_callback: Function to call if hardware wipe fails
465
428
  Should accept device path and return bool
466
-
429
+
467
430
  Returns:
468
431
  True if wipe succeeded (hardware or software), False otherwise
469
432
  """
470
433
  if not self.prepare():
471
434
  print("Required tools not available")
472
435
  return False
473
-
436
+
474
437
  # Step 1: Pre-check
475
438
  pre_check = self.pre_check(device)
476
-
439
+
477
440
  if not pre_check.compatible:
478
441
  print(f"Hardware erase not compatible for {device}:")
479
442
  for issue in pre_check.issues:
480
443
  print(f" - {issue}")
481
-
444
+
482
445
  if fallback_callback:
483
446
  self._log("Falling back to software wipe...")
484
447
  return fallback_callback(device)
485
448
  return False
486
-
449
+
487
450
  # Step 2: Show user what to expect
488
451
  tool_name = pre_check.tool
489
452
  print(f"Using {tool_name} for hardware secure erase...")
490
453
  print("Note: Drive erases in firmware - tool will exit immediately.")
491
-
454
+
492
455
  if tool_name == 'nvme':
493
456
  print("Expected time: 2-10 seconds")
494
457
  elif tool_name == 'hdparm' and pre_check.enhanced_supported:
495
458
  print("Expected time: 10-60 seconds (enhanced erase)")
496
459
  elif tool_name == 'hdparm':
497
460
  print("Expected time: 1-3 hours per TB (normal erase)")
498
-
461
+
499
462
  # Step 3: Start erase
500
463
  self.eraser = DriveEraser(progress_callback=self._progress_update)
501
-
464
+
502
465
  try:
503
466
  if tool_name == 'nvme':
504
467
  success = self.eraser.start_nvme_erase(device)
505
468
  else: # hdparm
506
469
  enhanced = pre_check.enhanced_supported
507
470
  success = self.eraser.start_ata_erase(device, enhanced)
508
-
471
+
509
472
  if not success:
510
473
  raise RuntimeError("Failed to start erase")
511
-
474
+
512
475
  # Step 4: Monitor with timeout
513
476
  timeout = self._get_timeout(tool_name, device)
514
477
  print(f"Waiting up to {timeout//60} minutes for completion...")
515
-
478
+
516
479
  # Simple spinner while waiting
517
480
  spinner = ['|', '/', '-', '\\']
518
481
  i = 0
519
-
482
+
520
483
  while True:
521
484
  status = self.eraser.get_status()
522
-
485
+
523
486
  if status['status'] == EraseStatus.COMPLETE.value:
524
487
  print(f"\nHardware secure erase completed successfully!")
525
488
  return True
526
-
489
+
527
490
  elif status['status'] == EraseStatus.FAILED.value:
528
491
  print(f"\nHardware secure erase failed")
529
492
  break
530
-
493
+
531
494
  # Show spinner and elapsed time
532
495
  elapsed = status['elapsed_seconds']
533
496
  print(f"\r{spinner[i % 4]} Erasing... {int(elapsed)}s elapsed", end='')
534
497
  i += 1
535
-
498
+
536
499
  # Check timeout
537
500
  if elapsed > timeout:
538
501
  print(f"\nTimeout after {timeout} seconds")
539
502
  break
540
-
503
+
541
504
  time.sleep(0.5)
542
-
505
+
543
506
  # If we get here, hardware failed
544
507
  if fallback_callback:
545
508
  print("Falling back to software wipe...")
546
509
  return fallback_callback(device)
547
-
510
+
548
511
  return False
549
-
512
+
550
513
  except Exception as e:
551
514
  print(f"Error during hardware erase: {e}")
552
515
  if fallback_callback:
553
516
  return fallback_callback(device)
554
517
  return False
555
-
518
+
556
519
  def _progress_update(self, progress: float, elapsed: float, status: EraseStatus):
557
520
  """Callback for progress updates"""
558
521
  if self.verbose:
559
522
  print(f"[Progress] {progress:.1f}% - {elapsed:.0f}s - {status.value}")
560
-
523
+
561
524
  def _get_timeout(self, tool: str, device: str) -> int:
562
525
  """Get appropriate timeout based on drive type"""
563
526
  if tool == 'nvme':
@@ -572,7 +535,7 @@ class HardwareWipeController:
572
535
  except:
573
536
  return 7200 # 2 hours default
574
537
  return 3600 # 1 hour default
575
-
538
+
576
539
  def _get_drive_size_gb(self, device: str) -> float:
577
540
  """Get drive size in GB"""
578
541
  try:
@@ -590,6 +553,24 @@ class HardwareWipeController:
590
553
  pass
591
554
  return 500 # Default guess
592
555
 
556
+ def apply_marker(self):
557
+ # 1. Force the OS to realize the partitions are gone
558
+ subprocess.run(['blockdev', '--rereadpt', self.device_path])
559
+ time.sleep(1) # Give the kernel a breath
560
+
561
+ try:
562
+ with open(self.device_path, 'wb') as f:
563
+ # Clear first 16K
564
+ f.write(b'\x00' * 16384)
565
+ # Seek to 15K
566
+ f.seek(15360)
567
+ f.write(self.generate_json_marker())
568
+ f.flush()
569
+ os.fsync(f.fileno())
570
+ except OSError as e:
571
+ # If this happens, the drive is likely still 'settling' its FTL
572
+ return "RETRY_NEEDED"
573
+
593
574
  # ============================================================================
594
575
  # Part 5: Example Usage & Integration Helper
595
576
  # ============================================================================
@@ -603,29 +584,29 @@ def example_software_wipe(device: str) -> bool:
603
584
  def main():
604
585
  """Example standalone usage"""
605
586
  import argparse
606
-
587
+
607
588
  parser = argparse.ArgumentParser(description='Hardware Secure Erase Test')
608
589
  parser.add_argument('device', help='Device to wipe (e.g., /dev/sda)')
609
- parser.add_argument('--auto-install', action='store_true',
590
+ parser.add_argument('--auto-install', action='store_true',
610
591
  help='Automatically install missing tools')
611
592
  parser.add_argument('--verbose', '-v', action='store_true',
612
593
  help='Verbose output')
613
594
  parser.add_argument('--no-fallback', action='store_true',
614
595
  help='Don\'t fall back to software wipe')
615
596
  args = parser.parse_args()
616
-
597
+
617
598
  # Create controller
618
599
  controller = HardwareWipeController(
619
600
  auto_install_tools=args.auto_install,
620
601
  verbose=args.verbose
621
602
  )
622
-
603
+
623
604
  # Define fallback
624
605
  fallback = None if args.no_fallback else example_software_wipe
625
-
606
+
626
607
  # Execute wipe
627
608
  success = controller.wipe(args.device, fallback_callback=fallback)
628
-
609
+
629
610
  if success:
630
611
  print(f"\n✓ Wipe completed successfully")
631
612
  return 0