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/DeviceInfo.py CHANGED
@@ -1,6 +1,11 @@
1
1
  """
2
2
  DeviceInfo class for device discovery and information management
3
3
  """
4
+ # pylint: disable=invalid-name,broad-exception-caught
5
+ # pylint: disable=line-too-long,too-many-locals,too-many-branches
6
+ # pylint: disable=too-many-return-statements,too-many-nested-blocks
7
+ # pylint: disable=too-many-statements
8
+
4
9
  import os
5
10
  import re
6
11
  import json
@@ -8,13 +13,15 @@ import subprocess
8
13
  import time
9
14
  import datetime
10
15
  import curses
16
+ import traceback
11
17
  from fnmatch import fnmatch
12
18
  from types import SimpleNamespace
19
+ from console_window import Theme
20
+ from dataclasses import asdict
13
21
 
14
22
  from .WipeJob import WipeJob
15
23
  from .Utils import Utils
16
- from console_window import Theme
17
- from .PersistentState import PersistentState
24
+ from .DrivePreChecker import DrivePreChecker
18
25
 
19
26
 
20
27
  class DeviceInfo:
@@ -23,8 +30,8 @@ class DeviceInfo:
23
30
 
24
31
  def __init__(self, opts, persistent_state=None):
25
32
  self.opts = opts
26
- self.DB = opts.debug
27
- self.wids = None
33
+ self.checker = DrivePreChecker()
34
+ self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
28
35
  self.head_str = None
29
36
  self.partitions = None
30
37
  self.persistent_state = persistent_state
@@ -49,8 +56,36 @@ class DeviceInfo:
49
56
  uuid='', # filesystem UUID or PARTUUID
50
57
  serial='', # disk serial number (for whole disks)
51
58
  port='', # port (for whole disks)
59
+ hw_caps={}, # hw_wipe capabilities (for whole disks)
60
+ hw_nopes={}, # hw reasons cannot do hw wipe
52
61
  )
53
62
 
63
+ def get_hw_capabilities(self, ns):
64
+ """
65
+ Populates and returns hardware wipe capabilities for a disk.
66
+ Returns cached data if already present.
67
+ """
68
+ # 1. Check if we already have cached results
69
+ if hasattr(ns, 'hw_caps') and (ns.hw_caps or ns.hw_nopes):
70
+ return ns.hw_caps, ns.hw_nopes
71
+
72
+ # Initialize defaults
73
+ ns.hw_caps, ns.hw_nopes = {}, {}
74
+
75
+ # Skip hardware checks if firmware wipes are disabled
76
+ if not getattr(self.opts, 'firmware_wipes', False):
77
+ return ns.hw_caps, ns.hw_nopes
78
+
79
+ # 4. Perform the actual Probe
80
+ dev_path = f"/dev/{ns.name}"
81
+ if ns.name.startswith('nv'):
82
+ result = self.checker.check_nvme_drive(dev_path)
83
+ elif ns.name.startswith('sd'):
84
+ result = self.checker.check_ata_drive(dev_path)
85
+ # 5. Store Results
86
+ ns.hw_caps, ns.hw_nopes = result.modes, result.issues
87
+ return ns.hw_caps, ns.hw_nopes
88
+
54
89
  def _get_port_from_sysfs(self, device_name):
55
90
  try:
56
91
  sysfs_path = f'/sys/class/block/{device_name}'
@@ -94,7 +129,6 @@ class DeviceInfo:
94
129
  except Exception as e:
95
130
  # Log exception to file for debugging
96
131
  with open('/tmp/dwipe_port_debug.log', 'a', encoding='utf-8') as f:
97
- import traceback
98
132
  f.write(f"Exception in _get_port_from_sysfs({device_name}): {e}\n")
99
133
  traceback.print_exc(file=f)
100
134
  return ''
@@ -118,8 +152,15 @@ class DeviceInfo:
118
152
  rv = f'{get_str(device_name, "model")}'
119
153
  return rv.strip()
120
154
 
121
- def parse_lsblk(self, dflt, prev_nss=None):
122
- """Parse ls_blk for all the goodies we need"""
155
+ def parse_lsblk(self, dflt, prev_nss=None, lsblk_output=None):
156
+ """Parse ls_blk for all the goodies we need
157
+
158
+ Args:
159
+ dflt: Default state for new devices
160
+ prev_nss: Previous device namespaces for merging
161
+ lsblk_output: Optional lsblk JSON output string. If provided, uses this
162
+ instead of running lsblk command. Useful for background monitoring.
163
+ """
123
164
  def eat_one(device):
124
165
  entry = self._make_partition_namespace(0, '', '', dflt)
125
166
  entry.name = device.get('name', '')
@@ -187,22 +228,39 @@ class DeviceInfo:
187
228
  verify_prefix = '✓ '
188
229
  elif verify_status == 'fail':
189
230
  verify_prefix = '✗ '
190
- entry.marker = f'{verify_prefix}{state} {pct}% {marker.mode} {dt.strftime("%Y/%m/%d %H:%M")}'
231
+
232
+ # Add error suffix if job failed abnormally
233
+ error_suffix = ''
234
+ abort_reason = getattr(marker, 'abort_reason', None)
235
+ if abort_reason:
236
+ error_suffix = f' Err[{abort_reason}]'
237
+
238
+ entry.marker = f'{verify_prefix}{state} {pct}% {marker.mode} {dt.strftime("%Y/%m/%d %H:%M")}{error_suffix}'
191
239
  entry.state = state
192
240
  entry.dflt = state # Set dflt so merge logic knows this partition has a marker
193
241
 
194
242
  return entry
195
243
 
196
- # Run the `lsblk` command and get its output in JSON format with additional columns
197
- # Use timeout to prevent UI freeze if lsblk hangs on problematic devices
198
- try:
199
- result = subprocess.run(['lsblk', '-J', '--bytes', '-o',
200
- 'NAME,MAJ:MIN,FSTYPE,TYPE,LABEL,PARTLABEL,FSUSE%,SIZE,MOUNTPOINTS,UUID,PARTUUID,SERIAL'],
201
- stdout=subprocess.PIPE, text=True, check=False, timeout=10.0)
202
- parsed_data = json.loads(result.stdout)
203
- except subprocess.TimeoutExpired:
204
- # lsblk hung - return empty dict to use previous device state
205
- return {}
244
+ # Get lsblk output - either from parameter or by running command
245
+ if lsblk_output: # Non-empty string from background monitor
246
+ # Use provided output string
247
+ try:
248
+ parsed_data = json.loads(lsblk_output)
249
+ except (json.JSONDecodeError, Exception):
250
+ # Invalid JSON - return empty dict
251
+ return {}
252
+ else:
253
+ # Run the `lsblk` command and get its output in JSON format with additional columns
254
+ # Use timeout to prevent UI freeze if lsblk hangs on problematic devices
255
+ try:
256
+ result = subprocess.run(['lsblk', '-J', '--bytes', '-o',
257
+ 'NAME,MAJ:MIN,FSTYPE,TYPE,LABEL,PARTLABEL,FSUSE%,SIZE,MOUNTPOINTS,UUID,PARTUUID,SERIAL'],
258
+ stdout=subprocess.PIPE, text=True, check=False, timeout=10.0)
259
+ parsed_data = json.loads(result.stdout)
260
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception): # pylint: disable=broad-exception-caught
261
+ # lsblk hung, returned bad JSON, or other error - return empty dict
262
+ # assemble_partitions will detect this and preserve previous state
263
+ return {}
206
264
  entries = {}
207
265
 
208
266
  # Parse each block device and its properties
@@ -224,13 +282,13 @@ class DeviceInfo:
224
282
  final_entries = {}
225
283
  for name, entry in entries.items():
226
284
  final_entries[name] = entry
227
-
285
+
228
286
  # Only process top-level physical disks
229
287
  if entry.parent is None:
230
288
  # Hardware Info Gathering
231
289
  entry.model = self._get_device_vendor_model(entry.name)
232
290
  entry.port = self._get_port_from_sysfs(entry.name)
233
-
291
+
234
292
  # The Split (Superfloppy Case)
235
293
  # If it has children, the children already hold the data.
236
294
  # If it has NO children but HAS data, we create the '----' child.
@@ -245,7 +303,7 @@ class DeviceInfo:
245
303
 
246
304
  final_entries[v_key] = v_child
247
305
  entry.minors.append(v_key)
248
-
306
+
249
307
  # Clean the hardware row of data-specific strings
250
308
  entry.fstype = entry.model if entry.model else 'DISK'
251
309
  entry.label = ''
@@ -253,11 +311,6 @@ class DeviceInfo:
253
311
 
254
312
  entries = final_entries
255
313
 
256
- if self.DB:
257
- print('\n\nDB: --->>> after parse_lsblk:')
258
- for entry in entries.values():
259
- print(vars(entry))
260
-
261
314
  return entries
262
315
 
263
316
  @staticmethod
@@ -282,9 +335,9 @@ class DeviceInfo:
282
335
 
283
336
  if to == 'STOP' and not state_in(ns.state, job_states):
284
337
  return False
285
- if to == 'Lock' and not state_in(ns.state, list(ready_states) + ['Mnt']):
338
+ if to == 'Blk' and not state_in(ns.state, list(ready_states) + ['Mnt']):
286
339
  return False
287
- if to == 'Unlk' and ns.state != 'Lock':
340
+ if to == 'Unbl' and ns.state != 'Blk':
288
341
  return False
289
342
 
290
343
  if to and fnmatch(to, '*%'):
@@ -304,7 +357,7 @@ class DeviceInfo:
304
357
  # Here we set inferences that block starting jobs
305
358
  # -- clearing these states will be done on the device refresh
306
359
  if parent and state_in(ns.state, inferred_states):
307
- if parent.state != 'Lock':
360
+ if parent.state != 'Blk':
308
361
  parent.state = ns.state
309
362
  if state_in(ns.state, job_states):
310
363
  if parent:
@@ -313,6 +366,14 @@ class DeviceInfo:
313
366
  minor.state = 'Busy'
314
367
  return True
315
368
 
369
+ @staticmethod
370
+ def clear_inferred_states(nss):
371
+ """Clear all inferred states (Busy, Mnt) so they can be re-inferred"""
372
+ inferred_states = ('Busy', 'Mnt')
373
+ for ns in nss.values():
374
+ if ns.state in inferred_states:
375
+ ns.state = ns.dflt
376
+
316
377
  @staticmethod
317
378
  def set_all_states(nss):
318
379
  """Set every state per linkage inferences"""
@@ -337,6 +398,8 @@ class DeviceInfo:
337
398
  # Must be disk or partition type
338
399
  if ns.type not in ('disk', 'part'):
339
400
  continue
401
+ if ns.size_bytes <= 0: # not relevant to wiping
402
+ continue
340
403
 
341
404
  # Must be writable (excludes CD-ROMs, eMMC boot partitions, etc.)
342
405
  ro_path = f'/sys/class/block/{name}/ro'
@@ -361,7 +424,7 @@ class DeviceInfo:
361
424
 
362
425
  def compute_field_widths(self, nss):
363
426
  """Compute field widths for display formatting"""
364
- wids = self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
427
+ wids = self.wids
365
428
  for ns in nss.values():
366
429
  wids.state = max(wids.state, len(ns.state))
367
430
  wids.name = max(wids.name, len(ns.name) + 2)
@@ -370,9 +433,6 @@ class DeviceInfo:
370
433
  wids.label = max(wids.label, len(ns.label))
371
434
  wids.fstype = max(wids.fstype, len(ns.fstype))
372
435
  self.head_str = self.get_head_str()
373
- if self.DB:
374
- print('\n\nDB: --->>> after compute_field_widths():')
375
- print(f'self.wids={vars(wids)}')
376
436
 
377
437
  def get_head_str(self):
378
438
  """Generate header string for device list"""
@@ -457,8 +517,8 @@ class DeviceInfo:
457
517
  # Check for newly inserted flag first (hot-swapped devices should always show orange)
458
518
  if getattr(ns, 'newly_inserted', False):
459
519
  # Newly inserted device - orange/bright
460
- if ns.state in ('Mnt', 'Lock'):
461
- # Dim the orange for mounted/locked devices
520
+ if ns.state in ('Mnt', 'Blk'):
521
+ # Dim the orange for mounted/blocked devices
462
522
  attr = curses.color_pair(Theme.HOTSWAP) | curses.A_DIM
463
523
  else:
464
524
  attr = curses.color_pair(Theme.HOTSWAP) | curses.A_BOLD
@@ -477,8 +537,8 @@ class DeviceInfo:
477
537
  elif ns.state == '^':
478
538
  # Newly inserted device (hot-swapped) - orange/bright
479
539
  attr = curses.color_pair(Theme.HOTSWAP) | curses.A_BOLD
480
- elif ns.state in ('Mnt', 'Lock'):
481
- # Dim mounted or locked devices
540
+ elif ns.state in ('Mnt', 'Blk'):
541
+ # Dim mounted or blocked devices
482
542
  attr = curses.A_DIM
483
543
 
484
544
  # Override with red/danger color if verify failed
@@ -544,12 +604,12 @@ class DeviceInfo:
544
604
  new_ns.verify_failed_msg = prev_ns.verify_failed_msg
545
605
  new_ns.mounts = [prev_ns.verify_failed_msg]
546
606
 
547
- if prev_ns.state == 'Lock':
548
- new_ns.state = 'Lock'
607
+ if prev_ns.state == 'Blk':
608
+ new_ns.state = 'Blk'
549
609
  elif new_ns.state not in ('s', 'W'):
550
610
  new_ns.state = new_ns.dflt
551
611
  # Don't copy forward percentage states (like "v96%") - only persistent states
552
- if prev_ns.state not in ('s', 'W', 'Busy', 'Unlk') and not prev_ns.state.endswith('%'):
612
+ if prev_ns.state not in ('s', 'W', 'Busy', 'Unbl') and not prev_ns.state.endswith('%'):
553
613
  new_ns.state = prev_ns.state # re-infer these
554
614
  elif prev_ns.job:
555
615
  # unplugged device with job..
@@ -564,30 +624,101 @@ class DeviceInfo:
564
624
  new_ns.newly_inserted = True # Mark for orange color even if locked/mounted
565
625
  return nss
566
626
 
567
- def assemble_partitions(self, prev_nss=None):
568
- """Assemble and filter partitions for display"""
569
- nss = self.parse_lsblk(dflt='^' if prev_nss else '-', prev_nss=prev_nss)
627
+ def assemble_partitions(self, prev_nss=None, lsblk_output=None):
628
+ """Assemble and filter partitions for display
629
+
630
+ Args:
631
+ prev_nss: Previous device namespaces for merging
632
+ lsblk_output: Optional lsblk JSON output string from LsblkMonitor
633
+ """
634
+ nss = self.parse_lsblk(dflt='^' if prev_nss else '-', prev_nss=prev_nss,
635
+ lsblk_output=lsblk_output)
636
+
637
+ # If parse_lsblk failed (returned empty) and we have previous data, keep previous state
638
+ if not nss and prev_nss:
639
+ # lsblk scan failed or returned no devices - preserve previous state
640
+ # This prevents losing devices when lsblk temporarily fails
641
+ # But clear temporary status messages from completed jobs
642
+ for ns in prev_nss.values():
643
+ if not ns.job and ns.mounts:
644
+ # Job finished - clear temporary status messages like "Verified: zeroed"
645
+ ns.mounts = [m for m in ns.mounts if not m.startswith(('Verified:', 'Stopped'))]
646
+ return prev_nss # Return early - don't reprocess
570
647
 
571
648
  nss = self.get_disk_partitions(nss)
572
649
 
573
650
  nss = self.merge_dev_infos(nss, prev_nss)
574
651
 
575
- # Apply persistent locked states
652
+ # Apply persistent blocked states
576
653
  if self.persistent_state:
577
654
  for ns in nss.values():
578
655
  # Update last_seen timestamp
579
656
  self.persistent_state.update_device_seen(ns)
580
- # Apply persistent lock state
657
+ # Apply persistent block state
581
658
  if self.persistent_state.get_device_locked(ns):
582
- ns.state = 'Lock'
659
+ ns.state = 'Blk'
583
660
 
661
+ # Clear inferred states so they can be re-computed based on current job status
662
+ self.clear_inferred_states(nss)
584
663
  self.set_all_states(nss) # set inferred states
585
664
 
586
665
  self.compute_field_widths(nss)
587
-
588
- if self.DB:
589
- print('\n\nDB: --->>> after assemble_partitions():')
590
- for name, ns in nss.items():
591
- print(f'DB: {name}: {vars(ns)}')
592
- self.partitions = nss
593
666
  return nss
667
+
668
+ @staticmethod
669
+ def dump(parts=None, title='after lsblk'):
670
+ """Print nicely formatted device information"""
671
+ if not parts:
672
+ return
673
+
674
+ print(f'\n{"=" * 80}')
675
+ print(f'{title}')
676
+ print(f'{"=" * 80}\n')
677
+
678
+ # Separate disks and partitions
679
+ disks = {name: part for name, part in parts.items() if part.type == 'disk'}
680
+ partitions = {name: part for name, part in parts.items() if part.type == 'part'}
681
+
682
+ # Print each disk with its partitions
683
+ for disk_name in sorted(disks.keys()):
684
+ disk = disks[disk_name]
685
+
686
+ # Disk header
687
+ print(f'┌─ {disk.name} ({disk.model or "Unknown Model"})')
688
+ print(f'│ Size: {Utils.human(disk.size_bytes)} Serial: {disk.serial or "N/A"} Port: {disk.port or "N/A"}')
689
+ print(f'│ State: {disk.state} Marker: {disk.marker or "(none)"}')
690
+
691
+ # Hardware capabilities
692
+ if disk.hw_caps:
693
+ caps = ', '.join(disk.hw_caps.keys())
694
+ print(f'│ Hardware: {caps}')
695
+
696
+ # Find and print partitions for this disk
697
+ disk_parts = [(name, part) for name, part in partitions.items()
698
+ if part.parent == disk.name]
699
+
700
+ if disk_parts:
701
+ for i, (part_name, part) in enumerate(sorted(disk_parts)):
702
+ is_last = (i == len(disk_parts) - 1)
703
+ branch = '└─' if is_last else '├─'
704
+
705
+ # Partition info
706
+ label_str = f'"{part.label}"' if part.label else '(no label)'
707
+ fstype_str = part.fstype or '(no filesystem)'
708
+
709
+ print(f'│ {branch} {part.name}: {Utils.human(part.size_bytes)}')
710
+ print(f'│ {" " if is_last else "│ "} Label: {label_str} Type: {fstype_str}')
711
+ print(f'│ {" " if is_last else "│ "} State: {part.state} UUID: {part.uuid or "N/A"}')
712
+
713
+ if part.marker:
714
+ print(f'│ {" " if is_last else "│ "} Marker: {part.marker}')
715
+
716
+ if part.mounts:
717
+ mounts_str = ', '.join(part.mounts)
718
+ print(f'│ {" " if is_last else "│ "} Mounted: {mounts_str}')
719
+ else:
720
+ print(f'│ └─ (no partitions)')
721
+
722
+ print('│')
723
+
724
+ print(f'{"=" * 80}\n')