dwipe 3.0.1__tar.gz → 3.0.2__tar.gz

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.
Files changed (56) hide show
  1. dwipe-3.0.2/.github/workflows/tests.yml +47 -0
  2. {dwipe-3.0.1 → dwipe-3.0.2}/.gitignore +2 -1
  3. dwipe-3.0.1/README.md → dwipe-3.0.2/PKG-INFO +64 -10
  4. dwipe-3.0.1/PKG-INFO → dwipe-3.0.2/README.md +45 -26
  5. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/DeviceInfo.py +183 -18
  6. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/DeviceWorker.py +1 -1
  7. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/DiskWipe.py +37 -10
  8. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/FirmwareWipeTask.py +24 -51
  9. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/NvmeTool.py +4 -4
  10. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/PersistentState.py +25 -2
  11. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/SataTool.py +7 -5
  12. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/Tunables.py +5 -5
  13. dwipe-3.0.2/dwipe/VerifyTask.py +194 -0
  14. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/WipeJob.py +92 -686
  15. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/WriteTask.py +21 -26
  16. dwipe-3.0.2/dwipe/device_io.py +105 -0
  17. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/main.py +4 -0
  18. dwipe-3.0.2/dwipe/markers.py +155 -0
  19. dwipe-3.0.2/dwipe/verification.py +283 -0
  20. dwipe-3.0.2/images/dwipe-2026-02-05.07-58.gif +0 -0
  21. {dwipe-3.0.1 → dwipe-3.0.2}/pyproject.toml +21 -2
  22. dwipe-3.0.2/tests/conftest.py +50 -0
  23. dwipe-3.0.2/tests/device_double.py +135 -0
  24. dwipe-3.0.2/tests/test_cli_options.py +25 -0
  25. dwipe-3.0.2/tests/test_device_change_monitor.py +87 -0
  26. dwipe-3.0.2/tests/test_device_io.py +165 -0
  27. dwipe-3.0.2/tests/test_device_worker.py +228 -0
  28. dwipe-3.0.2/tests/test_deviceinfo_logic.py +314 -0
  29. dwipe-3.0.2/tests/test_diskwipe_terminal.py +41 -0
  30. dwipe-3.0.2/tests/test_drive_prechecker.py +157 -0
  31. dwipe-3.0.2/tests/test_exception_observability.py +92 -0
  32. dwipe-3.0.2/tests/test_firmware_wipe_task.py +126 -0
  33. dwipe-3.0.2/tests/test_nvme_tool.py +182 -0
  34. dwipe-3.0.2/tests/test_persistent_state.py +203 -0
  35. dwipe-3.0.2/tests/test_safe_write.py +90 -0
  36. dwipe-3.0.2/tests/test_satatool_parse.py +118 -0
  37. dwipe-3.0.2/tests/test_structured_logger.py +167 -0
  38. dwipe-3.0.2/tests/test_utils.py +172 -0
  39. dwipe-3.0.2/tests/test_verification.py +97 -0
  40. dwipe-3.0.2/tests/test_verify_analysis.py +80 -0
  41. dwipe-3.0.2/tests/test_wipejob_logic.py +208 -0
  42. dwipe-3.0.2/tests/test_wipejob_resume.py +214 -0
  43. dwipe-3.0.1/dwipe/VerifyTask.py +0 -412
  44. {dwipe-3.0.1 → dwipe-3.0.2}/LICENSE +0 -0
  45. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/DeviceChangeMonitor.py +0 -0
  46. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/DrivePreChecker.py +0 -0
  47. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/Prereqs.py +0 -0
  48. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/StructuredLogger.py +0 -0
  49. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/Utils.py +0 -0
  50. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/WipeTask.py +0 -0
  51. {dwipe-3.0.1 → dwipe-3.0.2}/dwipe/__init__.py +0 -0
  52. {dwipe-3.0.1 → dwipe-3.0.2}/images/dwipe-2025-12-31-09-37.gif +0 -0
  53. {dwipe-3.0.1 → dwipe-3.0.2}/linter +0 -0
  54. {dwipe-3.0.1 → dwipe-3.0.2}/resources/dwipe-help-screen.png +0 -0
  55. {dwipe-3.0.1 → dwipe-3.0.2}/resources/dwipe-main-screen.png +0 -0
  56. {dwipe-3.0.1 → dwipe-3.0.2}/runner +0 -0
@@ -0,0 +1,47 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+
22
+ - name: Install package with test extras
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install -e '.[test]'
26
+
27
+ - name: Run unit tests with coverage
28
+ run: pytest --cov=dwipe --cov-report=term-missing --cov-fail-under=40
29
+
30
+ lint:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.13"
39
+
40
+ - name: Install pylint
41
+ run: python -m pip install --upgrade pip pylint
42
+
43
+ - name: Static checks (undefined names / bad calls)
44
+ # Full advisory lint is available locally via ./linter. This job blocks
45
+ # only on the error classes that indicate real bugs (undefined name,
46
+ # bad call signature) and currently passes at 10.00/10.
47
+ run: pylint --disable=all --enable=E0601,E0602,E0603,E1120,E1121 dwipe
@@ -6,4 +6,5 @@ venv.zd/
6
6
  *.pyc
7
7
  *egg-info*
8
8
  dist/
9
- .github/
9
+ .coverage
10
+ htmlcov/
@@ -1,7 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: dwipe
3
+ Version: 3.0.2
4
+ Summary: A tool to wipe disks and partitions for Linux
5
+ Keywords: disk,partition,wipe,clean,scrub
6
+ Author-email: Joe Defen <joedef@google.com>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ License-File: LICENSE
13
+ Requires-Dist: console-window == 1.4.2
14
+ Requires-Dist: pytest >= 7.0 ; extra == "test"
15
+ Requires-Dist: pytest-cov >= 4.0 ; extra == "test"
16
+ Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
17
+ Project-URL: Homepage, https://github.com/joedefen/dwipe
18
+ Provides-Extra: test
19
+
1
20
  # dwipe
2
21
  `dwipe` is a tool to wipe disks and partitions for Linux to help secure your data. `dwipe` aims to reduce mistakes by providing ample information about your devices during selection.
3
22
 
4
- ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2025-12-31-09-37.gif)
23
+ ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2026-02-05.07-58.gif)
5
24
  ### Quick Comparison
6
25
 
7
26
  | Feature | dwipe | nwipe | shred | dd |
@@ -15,7 +34,7 @@
15
34
  | Device/partition blocking | ✓ | ✗ | ✗ | ✗ |
16
35
  | Resume interrupted software wipes | ✓ | ✗ | ✗ | ✗ |
17
36
  | Wipe operation logging | ✓ | ✗ | ✗ | ✗ |
18
- | Mount detection/prevention | ✓ | ✓ | ✗ | ✗ |
37
+ | Mount & LUKS/LVM/RAID/swap detection | ✓ | ✓ | ✗ | ✗ |
19
38
  | Fast Statistical sampling verification | ✓ | ✗ | ✗ | ✗ |
20
39
  | Certified destruction (DoD/Gutmann standards, full verification, certificates) | ✗ | ✓ | ✗ | ✗ |
21
40
 
@@ -30,7 +49,7 @@
30
49
  * **Multiple simultaneous wipes** - Start wipes on multiple devices at once with individual progress tracking and completion states
31
50
  * **Persistent state tracking** - Wipe status survives reboots; partially wiped and completed states persist on the device (can resume wipes across power cycles)
32
51
  * **Smart device identification** - Uses UUID/PARTUUID/serial numbers for stable device tracking across reconnections
33
- * **Safety protections** - Prevents wiping mounted devices, detects overlapping wipes, supports device blocking, inline confirmation prompts
52
+ * **Safety protections** - Prevents wiping mounted or in-use devices (including LUKS/LVM/RAID containers and swap), detects overlapping wipes, supports device blocking, inline confirmation prompts
34
53
  * **Intelligent verification with statistical sampling** - Fast verification that covers entire disk even at low percentages (1% verification samples from all 100 disk sections)
35
54
  * **Direct I/O to disk** - Fast and efficient wiping that avoids page cache pollution with automatic slowdown/stall detection and graceful abort
36
55
  * **Multiple color themes** - Terminal color themes designed for readability with context-sensitive visual feedback (mounted devices dimmed, active wipes bright)
@@ -75,7 +94,7 @@
75
94
  `dwipe` provides comprehensive disk wiping capabilities with safety features:
76
95
 
77
96
  * **Smart device display** - Shows disks and partitions with labels, sizes, types, and vendor/model information to help identify devices correctly
78
- * **Safety protections** - Prevents wiping mounted devices, detects overlapping wipes, supports manual disk blocking
97
+ * **Safety protections** - Prevents wiping mounted or in-use devices (including LUKS/LVM/RAID containers and swap), detects overlapping wipes, supports manual disk blocking
79
98
  * **Hot-swap detection** - Updates the device list when storage changes; newly added devices are marked with **^** to make them easy to spot
80
99
  * **Multiple simultaneous wipes** - Start wipes on multiple devices at once, with individual progress tracking and completion states
81
100
  * **Flexible wipe modes** - Choose between Rand, Zero, Rand+V (with auto-verify), or Zero+V (with auto-verify)
@@ -125,6 +144,10 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
125
144
  - `--firmware-wipes` or `-F` - Enable firmware wipes
126
145
  - Enables hardware-based secure erase operations (NVMe Sanitize/Format, SATA ATA Security Erase)
127
146
  - Without this flag, only software wipes (Zero/Rand) are available
147
+ - `--auto-block-containers {on,off}` - Auto-block encrypted/stacked containers on first sight (default: on)
148
+ - Blocks devices whose filesystem is a container (LUKS, LVM, RAID member, swap) the first time they are seen, so wiping requires an explicit unblock (**b**)
149
+ - The block is remembered per device; once you unblock, it stays unblocked
150
+ - Set to `off` when re-provisioning many encrypted disks to avoid unblocking each one
128
151
  - `--dump-lsblk` - Dump parsed device information and exit (for debugging)
129
152
  - `--help` - Show help message with all available options
130
153
 
@@ -132,7 +155,7 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
132
155
 
133
156
  `dwipe` uses color coding to provide instant visual feedback about device and operation status:
134
157
 
135
- - **Dimmed (gray)** - Mounted or blocked devices (cannot be wiped)
158
+ - **Dimmed (gray)** - Mounted, in-use (LUKS/LVM/RAID/swap), or blocked devices (cannot be wiped)
136
159
  - **Default (white)** - Ready to wipe, idle state, or previously wiped (before this session)
137
160
  - **Bright cyan/blue + bold** - Active wipe or verification in progress (0-100% write, v0-v100% verify)
138
161
  - **Bold yellow** - Stopped or partially completed wipe
@@ -174,15 +197,41 @@ The **STATE** column shows the current status of each device:
174
197
  |-------|---------|
175
198
  | **-** | Device is ready for wiping |
176
199
  | **^** | Device is ready for wiping AND was added after `dwipe` started (hot-swapped) |
177
- | **Mnt** | Partition is mounted or disk has mounted partitions - cannot be wiped |
200
+ | **Mnt** | Partition/disk is mounted, or in use via a swap/LUKS/LVM/RAID mapping - cannot be wiped |
178
201
  | **N%** | Wipe is in progress (shows percentage complete, 0-100%) |
179
202
  | **vN%** | Verification is in progress (shows percentage complete, v0-v100%) |
180
203
  | **STOP** | Wipe or verification is being stopped |
181
204
  | **s** | Wipe was stopped - device is partially wiped (can restart or verify) |
182
205
  | **W** | Wipe was completed successfully (can wipe again or verify) |
183
- | **Blk** | Disk is manually blocked - partitions are hidden and cannot be wiped |
206
+ | **Blk** | Device/disk is blocked (with **b**, or auto-blocked when it is a LUKS/LVM/RAID/swap container) - cannot be wiped |
184
207
  | **Unbl** | Disk was just unblocked (transitory state) |
185
208
 
209
+ ### Mounts / Status Column
210
+
211
+ The **MOUNTS/STATUS** column shows how a device is currently in use:
212
+
213
+ | Display | Meaning |
214
+ |---------|---------|
215
+ | **/**, **/data** | A real mount point (mounted directly, or through a LUKS/LVM/RAID mapping) |
216
+ | **\busy** | In use but not mounted — e.g. an *open* LUKS container, an active LVM volume group, a running RAID array, or active swap |
217
+
218
+ Both are treated as mounted: the device is dimmed and **w** (wipe) is not offered. `\busy` uses a backslash (not `/`) to make clear it is not a real mount point, yet it is treated like one for safety. To wipe such a device, first release the layer holding it (see [Wipe issues](#wipe-issues)).
219
+
220
+ > **Note:** A device held by a lower layer (dm-crypt/LUKS, LVM, md/RAID, swap) is protected even when it is not itself mounted. Wiping the backing device while a mapping is active would corrupt the container, so `dwipe` treats it as mounted. Mount points reached through a mapping are attributed back to the underlying device, so a LUKS partition shows the mount of its unlocked mapping.
221
+
222
+ ### Auto-Blocking Containers
223
+
224
+ `dwipe` auto-blocks devices that hold encrypted or stacked containers **the first time they are seen**, whether or not they are currently in use. This guards data that is otherwise easy to wipe by accident, since a *closed* container shows almost no hint beyond its type:
225
+
226
+ - `crypto_LUKS` (dm-crypt/LUKS)
227
+ - `LVM2_member` (LVM physical volume)
228
+ - `linux_raid_member` / `isw_raid_member` / `ddf_raid_member` (RAID members)
229
+ - `swap`
230
+
231
+ Such a device shows state **Blk** with an `auto-blocked` status, and its whole disk is protected too (inherited block). To wipe it, press **b** to unblock — the unblock is remembered, so it is not re-blocked on later scans.
232
+
233
+ Auto-blocking is on by default. Disable it for bulk re-provisioning with `--auto-block-containers off` (saved as a preference).
234
+
186
235
  ### Available Actions
187
236
 
188
237
  The top line shows available actions. Some are context-sensitive (only available for certain devices):
@@ -448,9 +497,14 @@ Press **ESC** from the main screen to clear the filter and return to showing all
448
497
  - **Colors don't work** - Ensure your terminal supports colors (most modern terminals do)
449
498
 
450
499
  ### Wipe issues
451
- - **Can't wipe a device** - Check the STATE column:
500
+ - **Can't wipe a device** - Check the STATE and MOUNTS/STATUS columns:
452
501
  - **Mnt** - Unmount the partition first: `sudo umount /dev/sdXN`
453
- - **Blk** - Press **b** to unblock
502
+ - **`\busy`** status, or **Mnt** caused by a lower layer - release the mapping/volume first, then rescan (**r**):
503
+ - LUKS: unmount, then `sudo cryptsetup close <name>`
504
+ - LVM: `sudo vgchange -an <vg>` (deactivate the volume group)
505
+ - RAID: `sudo mdadm --stop /dev/mdX`
506
+ - Swap: `sudo swapoff /dev/...`
507
+ - **Blk** - Press **b** to unblock (containers such as LUKS/LVM/RAID/swap are auto-blocked until you do)
454
508
  - **Busy** - Another partition on the disk is being wiped
455
509
  - **Wipe is very slow** - Normal for large drives; check write rate to verify progress
456
510
  - **Wipe seems stuck** - Most likely due to bad disks; Direct I/O makes progress almost constant on good disks.
@@ -546,4 +600,4 @@ Issues and pull requests welcome at [github.com/joedefen/dwipe](https://github.c
546
600
 
547
601
  ## License
548
602
 
549
- MIT License - see [LICENSE](LICENSE) file for details.
603
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -1,23 +1,7 @@
1
- Metadata-Version: 2.4
2
- Name: dwipe
3
- Version: 3.0.1
4
- Summary: A tool to wipe disks and partitions for Linux
5
- Keywords: disk,partition,wipe,clean,scrub
6
- Author-email: Joe Defen <joedef@google.com>
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: POSIX :: Linux
12
- License-File: LICENSE
13
- Requires-Dist: console-window == 1.4.2
14
- Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
15
- Project-URL: Homepage, https://github.com/joedefen/dwipe
16
-
17
1
  # dwipe
18
2
  `dwipe` is a tool to wipe disks and partitions for Linux to help secure your data. `dwipe` aims to reduce mistakes by providing ample information about your devices during selection.
19
3
 
20
- ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2025-12-31-09-37.gif)
4
+ ![Demo of dwipe](https://raw.githubusercontent.com/joedefen/dwipe/master/images/dwipe-2026-02-05.07-58.gif)
21
5
  ### Quick Comparison
22
6
 
23
7
  | Feature | dwipe | nwipe | shred | dd |
@@ -31,7 +15,7 @@ Project-URL: Homepage, https://github.com/joedefen/dwipe
31
15
  | Device/partition blocking | ✓ | ✗ | ✗ | ✗ |
32
16
  | Resume interrupted software wipes | ✓ | ✗ | ✗ | ✗ |
33
17
  | Wipe operation logging | ✓ | ✗ | ✗ | ✗ |
34
- | Mount detection/prevention | ✓ | ✓ | ✗ | ✗ |
18
+ | Mount & LUKS/LVM/RAID/swap detection | ✓ | ✓ | ✗ | ✗ |
35
19
  | Fast Statistical sampling verification | ✓ | ✗ | ✗ | ✗ |
36
20
  | Certified destruction (DoD/Gutmann standards, full verification, certificates) | ✗ | ✓ | ✗ | ✗ |
37
21
 
@@ -46,7 +30,7 @@ Project-URL: Homepage, https://github.com/joedefen/dwipe
46
30
  * **Multiple simultaneous wipes** - Start wipes on multiple devices at once with individual progress tracking and completion states
47
31
  * **Persistent state tracking** - Wipe status survives reboots; partially wiped and completed states persist on the device (can resume wipes across power cycles)
48
32
  * **Smart device identification** - Uses UUID/PARTUUID/serial numbers for stable device tracking across reconnections
49
- * **Safety protections** - Prevents wiping mounted devices, detects overlapping wipes, supports device blocking, inline confirmation prompts
33
+ * **Safety protections** - Prevents wiping mounted or in-use devices (including LUKS/LVM/RAID containers and swap), detects overlapping wipes, supports device blocking, inline confirmation prompts
50
34
  * **Intelligent verification with statistical sampling** - Fast verification that covers entire disk even at low percentages (1% verification samples from all 100 disk sections)
51
35
  * **Direct I/O to disk** - Fast and efficient wiping that avoids page cache pollution with automatic slowdown/stall detection and graceful abort
52
36
  * **Multiple color themes** - Terminal color themes designed for readability with context-sensitive visual feedback (mounted devices dimmed, active wipes bright)
@@ -91,7 +75,7 @@ Project-URL: Homepage, https://github.com/joedefen/dwipe
91
75
  `dwipe` provides comprehensive disk wiping capabilities with safety features:
92
76
 
93
77
  * **Smart device display** - Shows disks and partitions with labels, sizes, types, and vendor/model information to help identify devices correctly
94
- * **Safety protections** - Prevents wiping mounted devices, detects overlapping wipes, supports manual disk blocking
78
+ * **Safety protections** - Prevents wiping mounted or in-use devices (including LUKS/LVM/RAID containers and swap), detects overlapping wipes, supports manual disk blocking
95
79
  * **Hot-swap detection** - Updates the device list when storage changes; newly added devices are marked with **^** to make them easy to spot
96
80
  * **Multiple simultaneous wipes** - Start wipes on multiple devices at once, with individual progress tracking and completion states
97
81
  * **Flexible wipe modes** - Choose between Rand, Zero, Rand+V (with auto-verify), or Zero+V (with auto-verify)
@@ -141,6 +125,10 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
141
125
  - `--firmware-wipes` or `-F` - Enable firmware wipes
142
126
  - Enables hardware-based secure erase operations (NVMe Sanitize/Format, SATA ATA Security Erase)
143
127
  - Without this flag, only software wipes (Zero/Rand) are available
128
+ - `--auto-block-containers {on,off}` - Auto-block encrypted/stacked containers on first sight (default: on)
129
+ - Blocks devices whose filesystem is a container (LUKS, LVM, RAID member, swap) the first time they are seen, so wiping requires an explicit unblock (**b**)
130
+ - The block is remembered per device; once you unblock, it stays unblocked
131
+ - Set to `off` when re-provisioning many encrypted disks to avoid unblocking each one
144
132
  - `--dump-lsblk` - Dump parsed device information and exit (for debugging)
145
133
  - `--help` - Show help message with all available options
146
134
 
@@ -148,7 +136,7 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
148
136
 
149
137
  `dwipe` uses color coding to provide instant visual feedback about device and operation status:
150
138
 
151
- - **Dimmed (gray)** - Mounted or blocked devices (cannot be wiped)
139
+ - **Dimmed (gray)** - Mounted, in-use (LUKS/LVM/RAID/swap), or blocked devices (cannot be wiped)
152
140
  - **Default (white)** - Ready to wipe, idle state, or previously wiped (before this session)
153
141
  - **Bright cyan/blue + bold** - Active wipe or verification in progress (0-100% write, v0-v100% verify)
154
142
  - **Bold yellow** - Stopped or partially completed wipe
@@ -190,15 +178,41 @@ The **STATE** column shows the current status of each device:
190
178
  |-------|---------|
191
179
  | **-** | Device is ready for wiping |
192
180
  | **^** | Device is ready for wiping AND was added after `dwipe` started (hot-swapped) |
193
- | **Mnt** | Partition is mounted or disk has mounted partitions - cannot be wiped |
181
+ | **Mnt** | Partition/disk is mounted, or in use via a swap/LUKS/LVM/RAID mapping - cannot be wiped |
194
182
  | **N%** | Wipe is in progress (shows percentage complete, 0-100%) |
195
183
  | **vN%** | Verification is in progress (shows percentage complete, v0-v100%) |
196
184
  | **STOP** | Wipe or verification is being stopped |
197
185
  | **s** | Wipe was stopped - device is partially wiped (can restart or verify) |
198
186
  | **W** | Wipe was completed successfully (can wipe again or verify) |
199
- | **Blk** | Disk is manually blocked - partitions are hidden and cannot be wiped |
187
+ | **Blk** | Device/disk is blocked (with **b**, or auto-blocked when it is a LUKS/LVM/RAID/swap container) - cannot be wiped |
200
188
  | **Unbl** | Disk was just unblocked (transitory state) |
201
189
 
190
+ ### Mounts / Status Column
191
+
192
+ The **MOUNTS/STATUS** column shows how a device is currently in use:
193
+
194
+ | Display | Meaning |
195
+ |---------|---------|
196
+ | **/**, **/data** | A real mount point (mounted directly, or through a LUKS/LVM/RAID mapping) |
197
+ | **\busy** | In use but not mounted — e.g. an *open* LUKS container, an active LVM volume group, a running RAID array, or active swap |
198
+
199
+ Both are treated as mounted: the device is dimmed and **w** (wipe) is not offered. `\busy` uses a backslash (not `/`) to make clear it is not a real mount point, yet it is treated like one for safety. To wipe such a device, first release the layer holding it (see [Wipe issues](#wipe-issues)).
200
+
201
+ > **Note:** A device held by a lower layer (dm-crypt/LUKS, LVM, md/RAID, swap) is protected even when it is not itself mounted. Wiping the backing device while a mapping is active would corrupt the container, so `dwipe` treats it as mounted. Mount points reached through a mapping are attributed back to the underlying device, so a LUKS partition shows the mount of its unlocked mapping.
202
+
203
+ ### Auto-Blocking Containers
204
+
205
+ `dwipe` auto-blocks devices that hold encrypted or stacked containers **the first time they are seen**, whether or not they are currently in use. This guards data that is otherwise easy to wipe by accident, since a *closed* container shows almost no hint beyond its type:
206
+
207
+ - `crypto_LUKS` (dm-crypt/LUKS)
208
+ - `LVM2_member` (LVM physical volume)
209
+ - `linux_raid_member` / `isw_raid_member` / `ddf_raid_member` (RAID members)
210
+ - `swap`
211
+
212
+ Such a device shows state **Blk** with an `auto-blocked` status, and its whole disk is protected too (inherited block). To wipe it, press **b** to unblock — the unblock is remembered, so it is not re-blocked on later scans.
213
+
214
+ Auto-blocking is on by default. Disable it for bulk re-provisioning with `--auto-block-containers off` (saved as a preference).
215
+
202
216
  ### Available Actions
203
217
 
204
218
  The top line shows available actions. Some are context-sensitive (only available for certain devices):
@@ -464,9 +478,14 @@ Press **ESC** from the main screen to clear the filter and return to showing all
464
478
  - **Colors don't work** - Ensure your terminal supports colors (most modern terminals do)
465
479
 
466
480
  ### Wipe issues
467
- - **Can't wipe a device** - Check the STATE column:
481
+ - **Can't wipe a device** - Check the STATE and MOUNTS/STATUS columns:
468
482
  - **Mnt** - Unmount the partition first: `sudo umount /dev/sdXN`
469
- - **Blk** - Press **b** to unblock
483
+ - **`\busy`** status, or **Mnt** caused by a lower layer - release the mapping/volume first, then rescan (**r**):
484
+ - LUKS: unmount, then `sudo cryptsetup close <name>`
485
+ - LVM: `sudo vgchange -an <vg>` (deactivate the volume group)
486
+ - RAID: `sudo mdadm --stop /dev/mdX`
487
+ - Swap: `sudo swapoff /dev/...`
488
+ - **Blk** - Press **b** to unblock (containers such as LUKS/LVM/RAID/swap are auto-blocked until you do)
470
489
  - **Busy** - Another partition on the disk is being wiped
471
490
  - **Wipe is very slow** - Normal for large drives; check write rate to verify progress
472
491
  - **Wipe seems stuck** - Most likely due to bad disks; Direct I/O makes progress almost constant on good disks.
@@ -562,4 +581,4 @@ Issues and pull requests welcome at [github.com/joedefen/dwipe](https://github.c
562
581
 
563
582
  ## License
564
583
 
565
- MIT License - see [LICENSE](LICENSE) file for details.
584
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -27,6 +27,23 @@ class DeviceInfo:
27
27
  disk_majors = set() # major devices that are disks
28
28
  _discovery_cycle = 0 # Counter to prove discovery is running
29
29
 
30
+ # Filesystem types marking an encrypted/stacked container whose contents
31
+ # are hidden behind another layer. These are auto-blocked on first sight.
32
+ CONTAINER_FSTYPES = (
33
+ 'crypto_LUKS', 'LVM2_member', 'linux_raid_member',
34
+ 'isw_raid_member', 'ddf_raid_member', 'swap',
35
+ )
36
+ CONTAINER_FSTYPE_PREFIXES = ('crypto_', 'DM_')
37
+
38
+ @staticmethod
39
+ def is_container_fstype(fstype):
40
+ """True if fstype marks a container (LUKS/LVM/RAID/swap)."""
41
+ if not fstype:
42
+ return False
43
+ if fstype in DeviceInfo.CONTAINER_FSTYPES:
44
+ return True
45
+ return any(fstype.startswith(p) for p in DeviceInfo.CONTAINER_FSTYPE_PREFIXES)
46
+
30
47
  @staticmethod
31
48
  def clean_partition_label(label):
32
49
  """Clean up partition labels by decoding escape sequences and simplifying common labels.
@@ -99,6 +116,9 @@ class DeviceInfo:
99
116
  hw_caps_state=ProbeState.PENDING, # probe state for hw_caps
100
117
  is_usb=False, # True if device is on USB bus
101
118
  is_rotational=False, # True if HDD (spinning disk)
119
+ in_use=False, # mounted/swap or backing an active holder
120
+ is_container=False, # LUKS/LVM/RAID/swap container
121
+ auto_blocked=False, # blocked automatically (container)
102
122
  )
103
123
 
104
124
  def get_hw_capabilities(self, ns):
@@ -112,11 +132,7 @@ class DeviceInfo:
112
132
  if ns.hw_caps_state == ProbeState.READY:
113
133
  return ns.hw_caps, ns.hw_nopes
114
134
 
115
- # 2. Skip probing non-wipeable devices (mounted/blocked)
116
- if ns.state in ('Mnt', 'iMnt', 'Blk', 'iBlk', 'Busy'):
117
- return ns.hw_caps, ns.hw_nopes
118
-
119
- # 3. Skip probing if device has active job (would block on SATA wipe)
135
+ # 2. Skip probing if device has active job (would block on SATA wipe)
120
136
  if ns.job:
121
137
  return ns.hw_caps, ns.hw_nopes
122
138
 
@@ -260,12 +276,40 @@ class DeviceInfo:
260
276
  pass
261
277
  return devices
262
278
 
279
+ @staticmethod
280
+ def _block_name_from_devpath(devpath):
281
+ """Resolve a /dev path to its canonical kernel block name.
282
+
283
+ Handles /dev/sda1, /dev/md0, /dev/dm-0 and indirection through
284
+ /dev/mapper/<name> or /dev/disk/by-* (resolved via realpath).
285
+ Returns '' for anything that is not a block device path.
286
+ """
287
+ if not devpath.startswith('/dev/'):
288
+ return ''
289
+ try:
290
+ real = os.path.realpath(devpath)
291
+ except OSError:
292
+ real = devpath
293
+ return os.path.basename(real)
294
+
295
+ @staticmethod
296
+ def _read_sysfs_dir(path):
297
+ """List a sysfs directory, returning [] if it is missing or unreadable."""
298
+ try:
299
+ return os.listdir(path)
300
+ except OSError:
301
+ return []
302
+
263
303
  def _parse_proc_mounts(self):
264
304
  """Parse /proc/mounts for mount points (non-blocking).
265
305
 
266
306
  Format: device mountpoint fstype options dump pass
267
307
  /dev/sda1 /boot ext4 rw,relatime 0 0
268
308
 
309
+ Device paths are resolved to canonical block names so a mount on a
310
+ dm-crypt/LVM/RAID mapping (e.g. /dev/mapper/crypt -> dm-0) is
311
+ attributed to the underlying block device.
312
+
269
313
  Returns:
270
314
  dict: {device_name: {'mounts': [mountpoint1, ...], 'fstype': str}}
271
315
  """
@@ -275,7 +319,9 @@ class DeviceInfo:
275
319
  for line in f:
276
320
  parts = line.split()
277
321
  if len(parts) >= 3 and parts[0].startswith('/dev/'):
278
- device = parts[0][5:] # Strip '/dev/'
322
+ device = self._block_name_from_devpath(parts[0])
323
+ if not device:
324
+ continue
279
325
  mountpoint = parts[1]
280
326
  fstype = parts[2]
281
327
  if device not in mounts:
@@ -285,6 +331,94 @@ class DeviceInfo:
285
331
  pass
286
332
  return mounts
287
333
 
334
+ @staticmethod
335
+ def _parse_proc_swaps():
336
+ """Return canonical block names currently used as swap (non-blocking)."""
337
+ names = set()
338
+ try:
339
+ with open('/proc/swaps', 'r', encoding='utf-8') as f:
340
+ next(f, None) # skip header line
341
+ for line in f:
342
+ parts = line.split()
343
+ if parts:
344
+ name = DeviceInfo._block_name_from_devpath(parts[0])
345
+ if name:
346
+ names.add(name)
347
+ except (FileNotFoundError, PermissionError, Exception):
348
+ pass
349
+ return names
350
+
351
+ def _mark_block_usage(self, entries, swap_names):
352
+ """Flag devices consumed by a holder (LUKS, LVM, RAID, ...).
353
+
354
+ A device is *in use* when it is mounted, used as swap, or backs any
355
+ holder (an active dm/md mapping). Mountpoints of a mounted holder are
356
+ propagated down its slave chain so a LUKS container shows the mount of
357
+ its unlocked mapping; a holder that is open but unmounted marks the
358
+ backing device in-use (rendered as '\\busy'). All reads are sysfs-only
359
+ and non-blocking.
360
+ """
361
+ holders, slaves = {}, {}
362
+ for name in entries:
363
+ holders[name] = self._read_sysfs_dir(f'/sys/class/block/{name}/holders')
364
+ slaves[name] = self._read_sysfs_dir(f'/sys/class/block/{name}/slaves')
365
+
366
+ # Seed the walk with devices that are mounted or used as swap
367
+ queue = [n for n, ns in entries.items() if ns.mounts]
368
+ for name in swap_names:
369
+ if name in entries:
370
+ entries[name].in_use = True
371
+ if name not in queue:
372
+ queue.append(name)
373
+ visited = set(queue)
374
+
375
+ # Propagate mount visibility down to backing devices via slaves
376
+ while queue:
377
+ cur = queue.pop()
378
+ cur_mounts = entries[cur].mounts if cur in entries else []
379
+ for backing in slaves.get(cur, []):
380
+ if backing not in entries:
381
+ continue
382
+ bns = entries[backing]
383
+ for mount in cur_mounts:
384
+ if mount not in bns.mounts:
385
+ bns.mounts.append(mount)
386
+ bns.in_use = True
387
+ if backing not in visited:
388
+ visited.add(backing)
389
+ queue.append(backing)
390
+
391
+ # Any device with a holder is in use (mapping active) even if unmounted.
392
+ # Also classify containers (by fstype or by an existing holder).
393
+ for name, holder_list in holders.items():
394
+ if name not in entries:
395
+ continue
396
+ ns = entries[name]
397
+ if holder_list:
398
+ ns.in_use = True
399
+ if holder_list or self.is_container_fstype(ns.fstype):
400
+ ns.is_container = True
401
+
402
+ @staticmethod
403
+ def device_in_use(name):
404
+ """Best-effort, non-blocking check that a device is in use right now.
405
+
406
+ Guards a wipe at the moment it starts, closing the window between a
407
+ discovery refresh and the actual write. True if the device has any
408
+ holder (active dm/md mapping) or is currently mounted.
409
+ """
410
+ if DeviceInfo._read_sysfs_dir(f'/sys/class/block/{name}/holders'):
411
+ return True
412
+ try:
413
+ with open('/proc/mounts', 'r', encoding='utf-8') as f:
414
+ for line in f:
415
+ parts = line.split()
416
+ if parts and DeviceInfo._block_name_from_devpath(parts[0]) == name:
417
+ return True
418
+ except (FileNotFoundError, PermissionError, Exception):
419
+ pass
420
+ return False
421
+
288
422
  def _is_partition(self, name):
289
423
  """Check if device is a partition (non-blocking).
290
424
 
@@ -508,6 +642,8 @@ class DeviceInfo:
508
642
  entry.model = getattr(prev, 'model', '')
509
643
  entry.port = getattr(prev, 'port', '')
510
644
  entry.is_rotational = getattr(prev, 'is_rotational', False)
645
+ entry.in_use = getattr(prev, 'in_use', False)
646
+ entry.is_container = getattr(prev, 'is_container', False)
511
647
  else:
512
648
  # Non-dark device: probe for metadata
513
649
  if entry.type == 'disk':
@@ -582,13 +718,17 @@ class DeviceInfo:
582
718
 
583
719
  entries[name] = entry
584
720
 
721
+ # Phase 3c: Mark devices consumed by a holder (LUKS/LVM/RAID/swap)
722
+ swap_names = self._parse_proc_swaps()
723
+ self._mark_block_usage(entries, swap_names)
724
+
585
725
  # Phase 4: Build parent-child relationships (minors lists)
586
726
  for name, parent_name in parent_map.items():
587
727
  if parent_name in entries:
588
728
  entries[parent_name].minors.append(name)
589
729
  self.disk_majors.add(entries[name].major)
590
- # Propagate mount state to parent
591
- if entries[name].mounts:
730
+ # Propagate mount/in-use state to parent (holder-backed too)
731
+ if entries[name].mounts or getattr(entries[name], 'in_use', False):
592
732
  entries[parent_name].state = 'iMnt'
593
733
 
594
734
  # Phase 4b: Clear marker display and state for disks with actual partitions
@@ -621,6 +761,8 @@ class DeviceInfo:
621
761
  v_child.mounts = entry.mounts
622
762
  v_child.parent = name
623
763
  v_child.uuid = entry.uuid
764
+ v_child.in_use = getattr(entry, 'in_use', False)
765
+ v_child.is_container = getattr(entry, 'is_container', False)
624
766
 
625
767
  final_entries[v_key] = v_child
626
768
  entry.minors.append(v_key)
@@ -742,7 +884,7 @@ class DeviceInfo:
742
884
 
743
885
  # Exclude common virtual and optical device prefixes as a safety net
744
886
  # (most should already be filtered by ro check or missing sysfs)
745
- excluded_prefixes = ('zram', 'loop', 'dm-', 'ram', 'sr', 'scd')
887
+ excluded_prefixes = ('zram', 'loop', 'dm-', 'md', 'ram', 'sr', 'scd')
746
888
  if any(name.startswith(prefix) for prefix in excluded_prefixes):
747
889
  continue
748
890
 
@@ -757,10 +899,8 @@ class DeviceInfo:
757
899
  for ns in nss.values():
758
900
  wids.state = max(wids.state, len(ns.state))
759
901
  wids.name = max(wids.name, len(ns.name) + 2)
760
- if ns.label is None:
761
- pass
762
- wids.label = max(wids.label, len(ns.label))
763
- wids.fstype = max(wids.fstype, len(ns.fstype))
902
+ wids.label = max(wids.label, len(ns.label or ''))
903
+ wids.fstype = max(wids.fstype, len(ns.fstype or ''))
764
904
  self.head_str = self.get_head_str()
765
905
 
766
906
  def get_head_str(self):
@@ -829,15 +969,17 @@ class DeviceInfo:
829
969
  hw_state = getattr(ns, 'hw_caps_state', ProbeState.PENDING)
830
970
  is_usb = getattr(ns, 'is_usb', False)
831
971
 
972
+ # Non-wipeable devices always show '---' regardless of capabilities
832
973
  fw_label = ''
833
- if hw_caps:
974
+ if ns.state in ('Mnt', 'iMnt', 'Blk', 'iBlk', 'Busy'):
975
+ fw_label = '---'
976
+ elif hw_caps:
834
977
  fw_label = hw_summary
835
978
  elif hw_nopes and not is_usb:
836
979
  first_issue = hw_nopes.split(',')[0].strip()
837
980
  fw_label = f'✗{first_issue}'
838
- elif hw_state in (ProbeState.PENDING, ProbeState.PROBING):
839
- if ns.state not in ('Mnt', 'iMnt', 'Blk', 'iBlk', 'Busy') and not is_usb:
840
- fw_label = '...'
981
+ elif hw_state in (ProbeState.PENDING, ProbeState.PROBING) and not is_usb:
982
+ fw_label = '...'
841
983
 
842
984
  if not fw_label:
843
985
  fw_label = '---'
@@ -878,6 +1020,12 @@ class DeviceInfo:
878
1020
  elif ns.mounts:
879
1021
  # Disk has mounts - show them
880
1022
  emit += f'{sep}{",".join(ns.mounts)}'
1023
+ elif getattr(ns, 'in_use', False):
1024
+ # In use but not mounted (open LUKS/LVM/RAID mapping)
1025
+ emit += f'{sep}\\busy'
1026
+ elif getattr(ns, 'auto_blocked', False):
1027
+ # Auto-blocked container (LUKS/LVM/RAID/swap)
1028
+ emit += f'{sep}auto-blocked'
881
1029
  elif ns.marker and ns.marker.strip() and not ns.minors:
882
1030
  # Disk has wipe status - show it ONLY if no child partitions
883
1031
  # (partitions take precedence over marker)
@@ -890,6 +1038,12 @@ class DeviceInfo:
890
1038
  emit += sep + print_str_or_dash(ns.label, wids.label)
891
1039
  if ns.mounts:
892
1040
  emit += f'{sep}{",".join(ns.mounts)}'
1041
+ elif getattr(ns, 'in_use', False):
1042
+ # In use but not mounted (open LUKS/LVM/RAID mapping)
1043
+ emit += f'{sep}\\busy'
1044
+ elif getattr(ns, 'auto_blocked', False):
1045
+ # Auto-blocked container (LUKS/LVM/RAID/swap)
1046
+ emit += f'{sep}auto-blocked'
893
1047
  else:
894
1048
  emit += f'{sep}{ns.marker}'
895
1049
 
@@ -1045,19 +1199,30 @@ class DeviceInfo:
1045
1199
 
1046
1200
  # Apply persistent blocked states
1047
1201
  if self.persistent_state:
1202
+ auto_block = getattr(self.opts, 'auto_block_containers', True)
1048
1203
  for ns in nss.values():
1049
1204
  # Update last_seen timestamp
1050
1205
  self.persistent_state.update_device_seen(ns)
1206
+ # Auto-block containers (LUKS/LVM/RAID/swap) the first time seen.
1207
+ # Once the user unblocks, a record exists so this won't re-block.
1208
+ if auto_block and getattr(ns, 'is_container', False) \
1209
+ and not self.persistent_state.has_device_record(ns):
1210
+ self.persistent_state.set_device_locked(ns, True, auto=True)
1051
1211
  # Apply persistent block state
1052
1212
  if self.persistent_state.get_device_locked(ns):
1053
1213
  ns.state = 'Blk'
1214
+ if self.persistent_state.is_device_auto_blocked(ns):
1215
+ ns.auto_blocked = True
1054
1216
 
1055
1217
  # Clear inferred states so they can be re-computed based on current job status
1056
1218
  self.clear_inferred_states(nss)
1057
1219
 
1058
1220
  # Re-apply Mnt state for devices with mounts (cleared above, needed for set_all_states)
1221
+ # Devices that are in use (mounted/swap/backing a holder) are treated as
1222
+ # mounted so they cannot be wiped. But Blk trumps Mnt - blocked devices
1223
+ # stay blocked even if mounted.
1059
1224
  for ns in nss.values():
1060
- if ns.mounts:
1225
+ if (ns.mounts or getattr(ns, 'in_use', False)) and ns.state != 'Blk':
1061
1226
  ns.state = 'Mnt'
1062
1227
 
1063
1228
  self.set_all_states(nss) # set inferred states (propagates Mnt/Busy to parents)