dwipe 1.0.7__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dwipe/DeviceInfo.py +492 -0
- dwipe/DiskWipe.py +880 -0
- dwipe/PersistentState.py +195 -0
- dwipe/Utils.py +199 -0
- dwipe/WipeJob.py +1243 -0
- dwipe/WipeJobFuture.py +245 -0
- dwipe/main.py +19 -853
- dwipe-2.0.0.dist-info/METADATA +407 -0
- dwipe-2.0.0.dist-info/RECORD +13 -0
- dwipe-1.0.7.dist-info/METADATA +0 -72
- dwipe-1.0.7.dist-info/RECORD +0 -7
- {dwipe-1.0.7.dist-info → dwipe-2.0.0.dist-info}/WHEEL +0 -0
- {dwipe-1.0.7.dist-info → dwipe-2.0.0.dist-info}/entry_points.txt +0 -0
- {dwipe-1.0.7.dist-info → dwipe-2.0.0.dist-info}/licenses/LICENSE +0 -0
dwipe/main.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
dwipe:
|
|
3
|
+
dwipe: curses-based tool to wipe physical disks or partitions including
|
|
4
4
|
markers to know their state when wiped.
|
|
5
5
|
"""
|
|
6
6
|
# pylint: disable=too-many-branches,too-many-statements,import-outside-toplevel
|
|
@@ -9,872 +9,31 @@ dwipe: curse based tool to wipe physical disks or partitions including
|
|
|
9
9
|
# pylint: disable=too-many-return-statements,too-many-locals
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
-
from fnmatch import fnmatch
|
|
13
12
|
import sys
|
|
14
|
-
import re
|
|
15
|
-
import json
|
|
16
|
-
import subprocess
|
|
17
|
-
import time
|
|
18
|
-
import datetime
|
|
19
|
-
import threading
|
|
20
|
-
import random
|
|
21
|
-
import shutil
|
|
22
13
|
import traceback
|
|
23
|
-
import curses as cs
|
|
24
|
-
from types import SimpleNamespace
|
|
25
|
-
from console_window import ConsoleWindow, OptionSpinner
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
number = float(number)
|
|
31
|
-
while suffixes:
|
|
32
|
-
suffix = suffixes.pop(0)
|
|
33
|
-
number /= 1000 # decimal
|
|
34
|
-
if number < 999.95 or not suffixes:
|
|
35
|
-
return f'{number:.1f}{suffix}B' # decimal
|
|
36
|
-
return None
|
|
37
|
-
##############################################################################
|
|
38
|
-
def ago_str(delta_secs, signed=False):
|
|
39
|
-
""" Turn time differences in seconds to a compact representation;
|
|
40
|
-
e.g., '18h·39m'
|
|
41
|
-
"""
|
|
42
|
-
ago = int(max(0, round(delta_secs if delta_secs >= 0 else -delta_secs)))
|
|
43
|
-
divs = (60, 60, 24, 7, 52, 9999999)
|
|
44
|
-
units = ('s', 'm', 'h', 'd', 'w', 'y')
|
|
45
|
-
vals = (ago%60, int(ago/60)) # seed with secs, mins (step til 2nd fits)
|
|
46
|
-
uidx = 1 # best units
|
|
47
|
-
for div in divs[1:]:
|
|
48
|
-
# print('vals', vals, 'div', div)
|
|
49
|
-
if vals[1] < div:
|
|
50
|
-
break
|
|
51
|
-
vals = (vals[1]%div, int(vals[1]/div))
|
|
52
|
-
uidx += 1
|
|
53
|
-
rv = '-' if signed and delta_secs < 0 else ''
|
|
54
|
-
rv += f'{vals[1]}{units[uidx]}' if vals[1] else ''
|
|
55
|
-
rv += f'{vals[0]:d}{units[uidx-1]}'
|
|
56
|
-
return rv
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class WipeJob:
|
|
60
|
-
""" TBD """
|
|
61
|
-
|
|
62
|
-
# Generate a 1MB buffer of random data
|
|
63
|
-
BUFFER_SIZE = 1 * 1024 * 1024 # 1MB
|
|
64
|
-
WRITE_SIZE = 16 * 1024 # 16KB
|
|
65
|
-
STATE_OFFSET = 15 * 1024 # where json is written
|
|
66
|
-
buffer = bytearray(os.urandom(BUFFER_SIZE))
|
|
67
|
-
zero_buffer = bytes(WRITE_SIZE)
|
|
68
|
-
|
|
69
|
-
# Shared status string
|
|
70
|
-
|
|
71
|
-
def __init__(self, device_path, total_size, opts=None):
|
|
72
|
-
self.opts = opts if opts else SimpleNamespace(dry_run=False)
|
|
73
|
-
self.device_path = device_path
|
|
74
|
-
self.total_size = total_size
|
|
75
|
-
self.do_abort = False
|
|
76
|
-
self.thread = None
|
|
77
|
-
|
|
78
|
-
self.start_mono = time.monotonic() # Track the start time
|
|
79
|
-
self.total_written = 0
|
|
80
|
-
self.wr_hists = [] # list of (mono, written)
|
|
81
|
-
self.done = False
|
|
82
|
-
self.exception = None # in case of issues
|
|
83
|
-
|
|
84
|
-
@staticmethod
|
|
85
|
-
def start_job(device_path, total_size, opts):
|
|
86
|
-
""" TBD """
|
|
87
|
-
job = WipeJob(device_path=device_path, total_size=total_size, opts=opts)
|
|
88
|
-
job.thread = threading.Thread(target=job.write_partition)
|
|
89
|
-
job.wr_hists.append(SimpleNamespace(mono=time.monotonic(), written=0))
|
|
90
|
-
job.thread.start()
|
|
91
|
-
return job
|
|
92
|
-
|
|
93
|
-
def get_status_str(self):
|
|
94
|
-
""" TBD """
|
|
95
|
-
elapsed_time = time.monotonic() - self.start_mono
|
|
96
|
-
write_rate = self.total_written / elapsed_time if elapsed_time > 0 else 0
|
|
97
|
-
percent_complete = (self.total_written / self.total_size) * 100
|
|
98
|
-
return (f"Write rate: {write_rate / (1024 * 1024):.2f} MB/s, "
|
|
99
|
-
f"Completed: {percent_complete:.2f}%")
|
|
100
|
-
|
|
101
|
-
def get_status(self):
|
|
102
|
-
""" TBD """
|
|
103
|
-
pct_str, rate_str, when_str = '', '', ''
|
|
104
|
-
mono = time.monotonic()
|
|
105
|
-
written = self.total_written
|
|
106
|
-
elapsed_time = mono - self.start_mono
|
|
107
|
-
|
|
108
|
-
pct = (self.total_written / self.total_size) * 100
|
|
109
|
-
pct_str = f'{int(round(pct))}%'
|
|
110
|
-
if self.do_abort:
|
|
111
|
-
pct_str = 'STOP'
|
|
112
|
-
|
|
113
|
-
self.wr_hists.append(SimpleNamespace(mono=mono, written=written))
|
|
114
|
-
floor = mono - 30 # 30w moving average
|
|
115
|
-
while len(self.wr_hists) >= 3 and self.wr_hists[1].mono >= floor:
|
|
116
|
-
del self.wr_hists[0]
|
|
117
|
-
delta_mono = mono - self.wr_hists[0].mono
|
|
118
|
-
rate = (written - self.wr_hists[0].written) / delta_mono if delta_mono > 1.0 else 0
|
|
119
|
-
rate_str = f'{human(int(round(rate, 0)))}/s'
|
|
120
|
-
|
|
121
|
-
if rate > 0:
|
|
122
|
-
when = int(round((self.total_size - self.total_written)/rate))
|
|
123
|
-
when_str = ago_str(when)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
|
|
127
|
-
|
|
128
|
-
def prep_marker_buffer(self, is_random):
|
|
129
|
-
""" Get the 1st 16KB to write:
|
|
130
|
-
- 15K zeros
|
|
131
|
-
- JSON status + zero fill to 1KB
|
|
132
|
-
"""
|
|
133
|
-
data = { "unixtime": int(time.time()),
|
|
134
|
-
"scrubbed_bytes": self.total_written,
|
|
135
|
-
"size_bytes": self.total_size,
|
|
136
|
-
"mode": 'Rand' if is_random else 'Zero'
|
|
137
|
-
}
|
|
138
|
-
json_data = json.dumps(data).encode('utf-8')
|
|
139
|
-
buffer = bytearray(self.BUFFER_SIZE)
|
|
140
|
-
buffer[:self.STATE_OFFSET] = b'\x00' * self.STATE_OFFSET
|
|
141
|
-
buffer[self.STATE_OFFSET:self.STATE_OFFSET+len(json_data)] = json_data
|
|
142
|
-
remaining_size = self.BUFFER_SIZE - (self.STATE_OFFSET+len(json_data))
|
|
143
|
-
buffer[self.STATE_OFFSET+len(json_data):] = b'\x00' * remaining_size
|
|
144
|
-
return buffer
|
|
145
|
-
|
|
146
|
-
@staticmethod
|
|
147
|
-
def read_marker_buffer(device_name):
|
|
148
|
-
""" Open the device and read the first 16 KB """
|
|
149
|
-
try:
|
|
150
|
-
with open(f'/dev/{device_name}', 'rb') as device:
|
|
151
|
-
device.seek(0)
|
|
152
|
-
buffer = device.read(WipeJob.BUFFER_SIZE)
|
|
153
|
-
except Exception:
|
|
154
|
-
return None # cannot find info
|
|
155
|
-
|
|
156
|
-
if buffer[:WipeJob.STATE_OFFSET] != b'\x00' * (WipeJob.STATE_OFFSET):
|
|
157
|
-
return None # First 15 KB are not zeros
|
|
158
|
-
|
|
159
|
-
# Extract JSON data from the next 1 KB Strip trailing zeros
|
|
160
|
-
json_data_bytes = buffer[WipeJob.STATE_OFFSET:WipeJob.BUFFER_SIZE].rstrip(b'\x00')
|
|
161
|
-
|
|
162
|
-
if not json_data_bytes:
|
|
163
|
-
return None # No JSON data found
|
|
164
|
-
|
|
165
|
-
# Deserialize the JSON data
|
|
166
|
-
try:
|
|
167
|
-
data = json.loads(json_data_bytes.decode('utf-8'))
|
|
168
|
-
except (json.JSONDecodeError, Exception):
|
|
169
|
-
return None # Invalid JSON data!
|
|
170
|
-
|
|
171
|
-
rv = {}
|
|
172
|
-
for key, value in data.items():
|
|
173
|
-
if key in ('unixtime', 'scrubbed_bytes', 'size_bytes') and isinstance(value, int):
|
|
174
|
-
rv[key] = value
|
|
175
|
-
elif key in ('mode', ) and isinstance(value, str):
|
|
176
|
-
rv[key] = value
|
|
177
|
-
else:
|
|
178
|
-
return None # bogus data
|
|
179
|
-
if len(rv) != 4:
|
|
180
|
-
return None # bogus data
|
|
181
|
-
return SimpleNamespace(**rv)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def write_partition(self):
|
|
185
|
-
"""Writes random chunks to a device and updates the progress status."""
|
|
186
|
-
self.total_written = 0 # Track total bytes written
|
|
187
|
-
is_random = self.opts.random
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
-
with open(self.device_path, 'wb') as device:
|
|
191
|
-
# for loop in range(10000000000):
|
|
192
|
-
offset = 0
|
|
193
|
-
chunk = memoryview(WipeJob.zero_buffer)
|
|
194
|
-
while True:
|
|
195
|
-
# foo = 1/0 # to force exception only
|
|
196
|
-
if self.do_abort:
|
|
197
|
-
break
|
|
198
|
-
if is_random:
|
|
199
|
-
offset = random.randint(0, WipeJob.BUFFER_SIZE - WipeJob.WRITE_SIZE)
|
|
200
|
-
# Use memoryview to avoid copying the data
|
|
201
|
-
chunk = memoryview(WipeJob.buffer)[offset:offset + WipeJob.WRITE_SIZE]
|
|
202
|
-
|
|
203
|
-
if self.opts.dry_run:
|
|
204
|
-
bytes_written = self.total_size // 120
|
|
205
|
-
time.sleep(0.25)
|
|
206
|
-
else:
|
|
207
|
-
try:
|
|
208
|
-
bytes_written = device.write(chunk)
|
|
209
|
-
except Exception:
|
|
210
|
-
bytes_written = 0
|
|
211
|
-
self.total_written += bytes_written
|
|
212
|
-
# Optional: Check for errors or incomplete writes
|
|
213
|
-
if bytes_written < WipeJob.WRITE_SIZE:
|
|
214
|
-
break
|
|
215
|
-
if self.opts.dry_run and self.total_written >= self.total_size:
|
|
216
|
-
break
|
|
217
|
-
# clear the beginning of device whether aborted or not
|
|
218
|
-
# if we have started writing + status in JSON
|
|
219
|
-
if not self.opts.dry_run and self.total_written > 0:
|
|
220
|
-
device.seek(0)
|
|
221
|
-
# chunk = memoryview(WipeJob.zero_buffer)
|
|
222
|
-
bytes_written = device.write(self.prep_marker_buffer(is_random))
|
|
223
|
-
except Exception:
|
|
224
|
-
self.exception = traceback.format_exc()
|
|
225
|
-
|
|
226
|
-
self.done = True
|
|
227
|
-
|
|
228
|
-
class DeviceInfo:
|
|
229
|
-
""" Class to dig out the info we want from the system."""
|
|
230
|
-
disk_majors = set() # major devices that are disks
|
|
231
|
-
|
|
232
|
-
def __init__(self, opts):
|
|
233
|
-
self.opts = opts
|
|
234
|
-
self.DB = opts.debug
|
|
235
|
-
self.wids = None
|
|
236
|
-
self.head_str = None
|
|
237
|
-
self.partitions = None
|
|
238
|
-
|
|
239
|
-
@staticmethod
|
|
240
|
-
def _make_partition_namespace(major, name, size_bytes, dflt):
|
|
241
|
-
return SimpleNamespace(name=name, # /proc/partitions
|
|
242
|
-
major=major, # /proc/partitions
|
|
243
|
-
# minor=minor, # /proc/partitions
|
|
244
|
-
parent=None, # a partition
|
|
245
|
-
state=dflt, # run-time state
|
|
246
|
-
dflt=dflt, # default run-time state
|
|
247
|
-
label='', # blkid
|
|
248
|
-
fstype='', # blkid
|
|
249
|
-
model='', # /sys/class/block/{name}/device/vendor|model
|
|
250
|
-
# fsuse='-',
|
|
251
|
-
size_bytes=size_bytes, # /sys/block/{name}/...
|
|
252
|
-
marker='', # persistent status
|
|
253
|
-
mounts=[], # /proc/mounts
|
|
254
|
-
minors=[],
|
|
255
|
-
job=None, # if zap running
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
@staticmethod
|
|
260
|
-
def get_device_vendor_model(device_name):
|
|
261
|
-
""" Gets the vendor and model for a given device from the /sys/class/block directory.
|
|
262
|
-
- Args: - device_name: The device name, such as 'sda', 'sdb', etc.
|
|
263
|
-
- - Returns: A string containing the vendor and model information.
|
|
264
|
-
"""
|
|
265
|
-
def get_str(device_name, suffix):
|
|
266
|
-
try:
|
|
267
|
-
rv = ''
|
|
268
|
-
fullpath = f'/sys/class/block/{device_name}/device/{suffix}'
|
|
269
|
-
with open(fullpath, 'r', encoding='utf-8') as f: # Read information
|
|
270
|
-
rv = f.read().strip()
|
|
271
|
-
except (FileNotFoundError, Exception):
|
|
272
|
-
# print(f"Error reading {info} for {device_name} : {e}")
|
|
273
|
-
pass
|
|
274
|
-
return rv
|
|
275
|
-
|
|
276
|
-
# rv = f'{get_str(device_name, "vendor")}' #vendor seems useless/confusing
|
|
277
|
-
rv = f'{get_str(device_name, "model")}'
|
|
278
|
-
return rv.strip()
|
|
279
|
-
|
|
280
|
-
def parse_lsblk(self, dflt):
|
|
281
|
-
""" Parse ls_blk for all the goodies we need """
|
|
282
|
-
def eat_one(device):
|
|
283
|
-
entry = self._make_partition_namespace(0, '', '', dflt)
|
|
284
|
-
entry.name=device.get('name', '')
|
|
285
|
-
maj_min=device.get('maj:min', (-1,-1))
|
|
286
|
-
wds = maj_min.split(':', maxsplit=1)
|
|
287
|
-
entry.major = -1
|
|
288
|
-
if len(wds) > 0:
|
|
289
|
-
entry.major = int(wds[0])
|
|
290
|
-
entry.fstype = device.get('fstype', '')
|
|
291
|
-
if entry.fstype is None:
|
|
292
|
-
entry.fstype = ''
|
|
293
|
-
entry.type = device.get('type', '')
|
|
294
|
-
entry.label = device.get('label', '')
|
|
295
|
-
if not entry.label:
|
|
296
|
-
entry.label=device.get('partlabel', '')
|
|
297
|
-
if entry.label is None:
|
|
298
|
-
entry.label = ''
|
|
299
|
-
# entry.fsuse=device.get('fsuse%', '')
|
|
300
|
-
# if entry.fsuse is None:
|
|
301
|
-
# entry.fsuse = '-'
|
|
302
|
-
entry.size_bytes=int(device.get('size', 0))
|
|
303
|
-
mounts = device.get('mountpoints', [])
|
|
304
|
-
while len(mounts) >= 1 and mounts[0] is None:
|
|
305
|
-
del mounts[0]
|
|
306
|
-
entry.mounts = mounts
|
|
307
|
-
if not mounts:
|
|
308
|
-
marker = WipeJob.read_marker_buffer(entry.name)
|
|
309
|
-
now = int(round(time.time()))
|
|
310
|
-
if (marker and marker.size_bytes == entry.size_bytes
|
|
311
|
-
and 0 <= marker.scrubbed_bytes <= entry.size_bytes
|
|
312
|
-
and marker.unixtime < now):
|
|
313
|
-
pct = int(round((marker.scrubbed_bytes/marker.size_bytes)*100))
|
|
314
|
-
state = 'W' if pct >= 100 else 's'
|
|
315
|
-
dt = datetime.datetime.fromtimestamp(marker.unixtime)
|
|
316
|
-
entry.marker = f'{state} {pct}% {marker.mode} {dt.strftime('%Y/%m/%d %H:%M')}'
|
|
317
|
-
entry.state = state
|
|
318
|
-
|
|
319
|
-
return entry
|
|
320
|
-
|
|
321
|
-
# Run the `lsblk` command and get its output in JSON format with additional columns
|
|
322
|
-
result = subprocess.run(['lsblk', '-J', '--bytes', '-o',
|
|
323
|
-
'NAME,MAJ:MIN,FSTYPE,TYPE,LABEL,PARTLABEL,FSUSE%,SIZE,MOUNTPOINTS', ],
|
|
324
|
-
stdout=subprocess.PIPE, text=True, check=False)
|
|
325
|
-
parsed_data = json.loads(result.stdout)
|
|
326
|
-
entries = {}
|
|
327
|
-
|
|
328
|
-
# Parse each block device and its properties
|
|
329
|
-
for device in parsed_data['blockdevices']:
|
|
330
|
-
parent = eat_one(device)
|
|
331
|
-
parent.fstype = self.get_device_vendor_model(parent.name)
|
|
332
|
-
entries[parent.name] = parent
|
|
333
|
-
for child in device.get('children', []):
|
|
334
|
-
entry = eat_one(child)
|
|
335
|
-
entries[entry.name] = entry
|
|
336
|
-
entry.parent = parent.name
|
|
337
|
-
parent.minors.append(entry.name)
|
|
338
|
-
if not parent.fstype:
|
|
339
|
-
parent.fstype = 'DISK'
|
|
340
|
-
self.disk_majors.add(entry.major)
|
|
341
|
-
if entry.mounts:
|
|
342
|
-
entry.state = 'Mnt'
|
|
343
|
-
parent.state = 'Mnt'
|
|
344
|
-
|
|
345
|
-
if self.DB:
|
|
346
|
-
print('\n\nDB: --->>> after parse_lsblk:')
|
|
347
|
-
for entry in entries.values():
|
|
348
|
-
print(vars(entry))
|
|
349
|
-
|
|
350
|
-
return entries
|
|
351
|
-
|
|
352
|
-
@staticmethod
|
|
353
|
-
def set_one_state(nss, ns, to=None, test_to=None):
|
|
354
|
-
""" Optionally, update a state, and always set inferred states
|
|
355
|
-
"""
|
|
356
|
-
ready_states = ('s', 'W', '-', '^')
|
|
357
|
-
job_states = ('*%', 'STOP')
|
|
358
|
-
inferred_states = ('Busy', 'Mnt', )
|
|
359
|
-
# other_states = ('Lock', 'Unlk')
|
|
360
|
-
|
|
361
|
-
def state_in(to, states):
|
|
362
|
-
return to in states or fnmatch(to, states[0])
|
|
363
|
-
|
|
364
|
-
to = test_to if test_to else to
|
|
365
|
-
|
|
366
|
-
parent, minors = None, []
|
|
367
|
-
if ns.parent:
|
|
368
|
-
parent = nss.get(ns.parent)
|
|
369
|
-
for minor in ns.minors:
|
|
370
|
-
minor_ns = nss.get(minor, None)
|
|
371
|
-
if minor_ns:
|
|
372
|
-
minors.append(minor_ns)
|
|
373
|
-
|
|
374
|
-
if to == 'STOP' and not state_in(ns.state, job_states):
|
|
375
|
-
return False
|
|
376
|
-
if to == 'Lock' and (not state_in(ns.state, list(ready_states) + ['Mnt'])
|
|
377
|
-
or ns.parent):
|
|
378
|
-
return False
|
|
379
|
-
if to == 'Unlk' and ns.state != 'Lock':
|
|
380
|
-
return False
|
|
381
|
-
|
|
382
|
-
if to and fnmatch(to, '*%'):
|
|
383
|
-
if not state_in(ns.state, ready_states):
|
|
384
|
-
return False
|
|
385
|
-
for minor in minors:
|
|
386
|
-
if not state_in(minor.state, ready_states):
|
|
387
|
-
return False
|
|
388
|
-
elif to in ('s', 'W') and not state_in(ns.state, job_states):
|
|
389
|
-
return False
|
|
390
|
-
if test_to:
|
|
391
|
-
return True
|
|
392
|
-
|
|
393
|
-
if to is not None:
|
|
394
|
-
ns.state = to
|
|
395
|
-
|
|
396
|
-
# Here we set inferences that block starting jobs
|
|
397
|
-
# -- clearing these states will be done on the device
|
|
398
|
-
# refresh
|
|
399
|
-
if parent and state_in(ns.state, inferred_states):
|
|
400
|
-
if parent.state != 'Lock':
|
|
401
|
-
parent.state = ns.state
|
|
402
|
-
if state_in(ns.state, job_states):
|
|
403
|
-
if parent:
|
|
404
|
-
parent.state = 'Busy'
|
|
405
|
-
for minor in minors:
|
|
406
|
-
minor.state = 'Busy'
|
|
407
|
-
return True
|
|
408
|
-
|
|
409
|
-
@staticmethod
|
|
410
|
-
def set_all_states(nss):
|
|
411
|
-
""" Set every state per linkage inferences """
|
|
412
|
-
for ns in nss.values():
|
|
413
|
-
DeviceInfo.set_one_state(nss, ns)
|
|
414
|
-
|
|
415
|
-
def get_disk_partitions(self, nss):
|
|
416
|
-
""" Determine which partitions we want some are bogus like zram """
|
|
417
|
-
ok_nss = {}
|
|
418
|
-
for name, ns in nss.items():
|
|
419
|
-
if ns.type in ('disk', 'part'):
|
|
420
|
-
ok_nss[name] = ns
|
|
421
|
-
return ok_nss
|
|
422
|
-
|
|
423
|
-
def compute_field_widths(self, nss):
|
|
424
|
-
""" TBD """
|
|
425
|
-
|
|
426
|
-
wids = self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
|
|
427
|
-
for ns in nss.values():
|
|
428
|
-
wids.state = max(wids.state, len(ns.state))
|
|
429
|
-
# wids.fsuse = max(wids.fsuse, len(ns.fsuse))
|
|
430
|
-
wids.name = max(wids.name, len(ns.name)+2)
|
|
431
|
-
if ns.label is None:
|
|
432
|
-
pass
|
|
433
|
-
wids.label = max(wids.label, len(ns.label))
|
|
434
|
-
wids.fstype = max(wids.fstype, len(ns.fstype))
|
|
435
|
-
self.head_str = self.get_head_str()
|
|
436
|
-
if self.DB:
|
|
437
|
-
print('\n\nDB: --->>> after compute_field_widths():')
|
|
438
|
-
print(f'self.wids={vars(wids)}')
|
|
439
|
-
|
|
440
|
-
def get_head_str(self):
|
|
441
|
-
""" TBD """
|
|
442
|
-
sep = ' '
|
|
443
|
-
wids = self.wids
|
|
444
|
-
emit = f'{"STATE":_^{wids.state}}'
|
|
445
|
-
emit += f'{sep}{"NAME":_^{wids.name}}'
|
|
446
|
-
# emit += f'{sep}{"USE%":_^{wids.fsuse}}'
|
|
447
|
-
emit += f'{sep}{"SIZE":_^{wids.human}}'
|
|
448
|
-
emit += f'{sep}{"TYPE":_^{wids.fstype}}'
|
|
449
|
-
emit += f'{sep}{"LABEL":_^{wids.label}}'
|
|
450
|
-
emit += f'{sep}MOUNTS/STATUS'
|
|
451
|
-
return emit
|
|
452
|
-
|
|
453
|
-
def part_str(self, partition):
|
|
454
|
-
""" Convert partition to human value. """
|
|
455
|
-
# def print_str_or_dashes(name, width, chrs=' -'):
|
|
456
|
-
# if not name.strip(): # Create a string of '─' characters of the specified width
|
|
457
|
-
# result = f'{chrs}' * (width//2)
|
|
458
|
-
# result += ' ' * (width%2)
|
|
459
|
-
# else: # Format the name to be right-aligned within the specified width
|
|
460
|
-
# result = f'{name:>{width}}'
|
|
461
|
-
# return result
|
|
462
|
-
|
|
463
|
-
def print_str_or_dash(name, width, empty='-'):
|
|
464
|
-
if not name.strip(): # return
|
|
465
|
-
name = empty
|
|
466
|
-
return f'{name:^{width}}'
|
|
467
|
-
|
|
468
|
-
sep = ' '
|
|
469
|
-
ns = partition # shorthand
|
|
470
|
-
wids = self.wids
|
|
471
|
-
emit = f'{ns.state:^{wids.state}}'
|
|
472
|
-
# name_str = ('' if ns.parent is None else '⮞ ') + ns.name
|
|
473
|
-
#name_str = ('● ' if ns.parent is None else ' ') + ns.name
|
|
474
|
-
name_str = ('■ ' if ns.parent is None else ' ') + ns.name
|
|
475
|
-
# if ns.parent is None and 1 + len(name_str) <= wids.name:
|
|
476
|
-
# name_str += ' ' * (wids.name - len(name_str) - 1) + '■'
|
|
477
|
-
|
|
478
|
-
emit += f'{sep}{name_str:<{wids.name}}'
|
|
479
|
-
# emit += f'{sep}{ns.fsuse:^{wids.fsuse}}'
|
|
480
|
-
emit += f'{sep}{human(ns.size_bytes):>{wids.human}}'
|
|
481
|
-
emit += sep + print_str_or_dash(ns.fstype, wids.fstype)
|
|
482
|
-
# emit += f'{sep}{ns.fstype:>{wids.fstype}}'
|
|
483
|
-
if ns.parent is None:
|
|
484
|
-
emit += sep + '■' + '─'*(wids.label-2) + '■'
|
|
485
|
-
else:
|
|
486
|
-
emit += sep + print_str_or_dash(ns.label, wids.label)
|
|
487
|
-
# emit += f' {ns.label:>{wids.label}}'
|
|
488
|
-
if ns.mounts:
|
|
489
|
-
emit += f'{sep}{",".join(ns.mounts)}'
|
|
490
|
-
else:
|
|
491
|
-
emit += f'{sep}{ns.marker}'
|
|
492
|
-
return emit
|
|
493
|
-
|
|
494
|
-
def merge_dev_infos(self, nss, prev_nss=None):
|
|
495
|
-
""" Merge old DevInfos into new DevInfos """
|
|
496
|
-
if not prev_nss:
|
|
497
|
-
return nss
|
|
498
|
-
for name, prev_ns in prev_nss.items():
|
|
499
|
-
# merge old jobs forward
|
|
500
|
-
new_ns = nss.get(name, None)
|
|
501
|
-
if new_ns:
|
|
502
|
-
if prev_ns.job:
|
|
503
|
-
new_ns.job = prev_ns.job
|
|
504
|
-
new_ns.dflt = prev_ns.dflt
|
|
505
|
-
|
|
506
|
-
if prev_ns.state == 'Lock':
|
|
507
|
-
new_ns.state = 'Lock'
|
|
508
|
-
elif new_ns.state not in ('s', 'W'):
|
|
509
|
-
new_ns.state = new_ns.dflt
|
|
510
|
-
if prev_ns.state not in ('s', 'W', 'Busy', 'Unlk'):
|
|
511
|
-
new_ns.state = prev_ns.state # re-infer these
|
|
512
|
-
elif prev_ns.job:
|
|
513
|
-
# unplugged device with job..
|
|
514
|
-
nss[name] = prev_ns # carry forward
|
|
515
|
-
prev_ns.job.do_abort = True
|
|
516
|
-
for name, new_ns in nss.items():
|
|
517
|
-
if name not in prev_nss and new_ns.state not in ('s', 'W'):
|
|
518
|
-
new_ns.state = '^'
|
|
519
|
-
return nss
|
|
520
|
-
|
|
521
|
-
def assemble_partitions(self, prev_nss=None):
|
|
522
|
-
""" TBD """
|
|
523
|
-
nss = self.parse_lsblk(dflt='^' if prev_nss else '-')
|
|
524
|
-
|
|
525
|
-
nss = self.get_disk_partitions(nss)
|
|
526
|
-
|
|
527
|
-
nss = self.merge_dev_infos(nss, prev_nss)
|
|
528
|
-
|
|
529
|
-
self.set_all_states(nss) # set inferred states
|
|
530
|
-
|
|
531
|
-
self.compute_field_widths(nss)
|
|
532
|
-
|
|
533
|
-
if self.DB:
|
|
534
|
-
print('\n\nDB: --->>> after assemble_partitions():')
|
|
535
|
-
for name, ns in nss.items():
|
|
536
|
-
print(f'DB: {name}: {vars(ns)}')
|
|
537
|
-
self.partitions = nss
|
|
538
|
-
return nss
|
|
539
|
-
|
|
540
|
-
class DiskWipe:
|
|
541
|
-
"""" TBD """
|
|
542
|
-
singleton = None
|
|
543
|
-
def __init__(self, opts=None):
|
|
544
|
-
DiskWipe.singleton = self
|
|
545
|
-
self.opts = opts if opts else SimpleNamespace(debug=0, dry_run=False)
|
|
546
|
-
self.DB = bool(self.opts.debug)
|
|
547
|
-
self.mounts_lines = None
|
|
548
|
-
self.partitions = {} # a dict of namespaces keyed by name
|
|
549
|
-
self.visibles = [] # visible partitions given the filter
|
|
550
|
-
self.wids = None
|
|
551
|
-
self.job_cnt = 0
|
|
552
|
-
self.exit_when_no_jobs = False
|
|
553
|
-
|
|
554
|
-
self.prev_filter = '' # string
|
|
555
|
-
self.filter = None # compiled pattern
|
|
556
|
-
self.pick_is_running = False
|
|
557
|
-
self.pick_name = '' # device name of current pick line
|
|
558
|
-
self.pick_actions = {} # key, tag
|
|
559
|
-
self.dev_info = None
|
|
560
|
-
|
|
561
|
-
# EXPAND
|
|
562
|
-
self.win, self.spin = None, None
|
|
563
|
-
|
|
564
|
-
self.check_preqreqs()
|
|
565
|
-
|
|
566
|
-
@staticmethod
|
|
567
|
-
def check_preqreqs():
|
|
568
|
-
""" Check that needed programs are installed. """
|
|
569
|
-
ok = True
|
|
570
|
-
for prog in 'lsblk'.split():
|
|
571
|
-
if shutil.which(prog) is None:
|
|
572
|
-
ok = False
|
|
573
|
-
print(f'ERROR: cannot find {prog!r} on $PATH')
|
|
574
|
-
if not ok:
|
|
575
|
-
sys.exit(1)
|
|
576
|
-
|
|
577
|
-
def test_state(self, ns, to=None):
|
|
578
|
-
""" Test if OK to set state of partition """
|
|
579
|
-
return self.dev_info.set_one_state(self.partitions, ns, test_to=to)
|
|
580
|
-
|
|
581
|
-
def set_state(self, ns, to=None):
|
|
582
|
-
""" Set state of partition """
|
|
583
|
-
return self.dev_info.set_one_state(self.partitions, ns, to=to)
|
|
584
|
-
|
|
585
|
-
@staticmethod
|
|
586
|
-
def mod_pick(line):
|
|
587
|
-
""" Callback to modify the "pick line" being highlighted;
|
|
588
|
-
We use it to alter the state
|
|
589
|
-
"""
|
|
590
|
-
this = DiskWipe.singleton
|
|
591
|
-
this.pick_name, this.pick_actions = this.get_actions(line)
|
|
592
|
-
header = this.get_keys_line()
|
|
593
|
-
# ASSUME line ends in /....
|
|
594
|
-
parts = header.split('/', maxsplit=1)
|
|
595
|
-
wds = parts[0].split()
|
|
596
|
-
this.win.head.pad.move(0, 0)
|
|
597
|
-
for wd in wds:
|
|
598
|
-
if wd[0]in ('<', '|', '❚'):
|
|
599
|
-
this.win.add_header(wd + ' ', resume=True)
|
|
600
|
-
continue
|
|
601
|
-
if wd:
|
|
602
|
-
this.win.add_header(wd[0], attr=cs.A_BOLD|cs.A_UNDERLINE, resume=True)
|
|
603
|
-
if wd[1:]:
|
|
604
|
-
this.win.add_header(wd[1:] + ' ', resume=True)
|
|
605
|
-
|
|
606
|
-
this.win.add_header('/', attr=cs.A_BOLD+cs.A_UNDERLINE, resume=True)
|
|
607
|
-
if len(parts) > 1 and parts[1]:
|
|
608
|
-
this.win.add_header(f'{parts[1]}', resume=True)
|
|
609
|
-
_, col = this.win.head.pad.getyx()
|
|
610
|
-
pad = ' ' * (this.win.get_pad_width()-col)
|
|
611
|
-
this.win.add_header(pad, resume=True)
|
|
612
|
-
return line
|
|
613
|
-
def do_key(self, key):
|
|
614
|
-
""" TBD """
|
|
615
|
-
def stop_if_idle(part):
|
|
616
|
-
if part.state[-1] == '%':
|
|
617
|
-
if part.job and not part.job.done:
|
|
618
|
-
part.job.do_abort = True
|
|
619
|
-
return 1 if part.job else 0
|
|
620
|
-
|
|
621
|
-
def stop_all():
|
|
622
|
-
rv = 0
|
|
623
|
-
for part in self.partitions.values():
|
|
624
|
-
rv += stop_if_idle(part)
|
|
625
|
-
return rv # number jobs running
|
|
626
|
-
|
|
627
|
-
def exit_if_no_jobs():
|
|
628
|
-
if stop_all() == 0:
|
|
629
|
-
self.win.stop_curses()
|
|
630
|
-
os.system('clear; stty sane')
|
|
631
|
-
sys.exit(0)
|
|
632
|
-
return True # continue running
|
|
633
|
-
|
|
634
|
-
if self.exit_when_no_jobs:
|
|
635
|
-
return exit_if_no_jobs()
|
|
636
|
-
|
|
637
|
-
if not key:
|
|
638
|
-
return True
|
|
639
|
-
if key in (cs.KEY_ENTER, 10): # Handle ENTER
|
|
640
|
-
if self.opts.help_mode:
|
|
641
|
-
self.opts.help_mode = False
|
|
642
|
-
return None
|
|
643
|
-
|
|
644
|
-
if key in self.spin.keys:
|
|
645
|
-
value = self.spin.do_key(key, self.win)
|
|
646
|
-
return value
|
|
647
|
-
|
|
648
|
-
if key == 27: # ESCAPE
|
|
649
|
-
self.prev_filter = ''
|
|
650
|
-
self.filter = None
|
|
651
|
-
self.win.pick_pos = 0
|
|
652
|
-
return None
|
|
653
|
-
|
|
654
|
-
if key in (ord('q'), ord('x')):
|
|
655
|
-
self.exit_when_no_jobs = True
|
|
656
|
-
self.filter = re.compile('STOPPING', re.IGNORECASE)
|
|
657
|
-
self.prev_filter = 'STOPPING'
|
|
658
|
-
return exit_if_no_jobs()
|
|
659
|
-
|
|
660
|
-
if key == ord('w') and not self.pick_is_running:
|
|
661
|
-
part = self.partitions[self.pick_name]
|
|
662
|
-
if self.test_state(part, to='0%'):
|
|
663
|
-
ans = self.win.answer(f'Type "y" to wipe {repr(part.name)} : '
|
|
664
|
-
+ f' st={repr(part.state)} sz={human(part.size_bytes)}'
|
|
665
|
-
+ f' ty={part.fstype} label={part.label}'
|
|
666
|
-
)
|
|
667
|
-
if ans.strip().lower().startswith('y'):
|
|
668
|
-
part.job = WipeJob.start_job(f'/dev/{part.name}',
|
|
669
|
-
part.size_bytes, opts=self.opts)
|
|
670
|
-
self.job_cnt += 1
|
|
671
|
-
self.set_state(part, to='0%')
|
|
672
|
-
return None
|
|
673
|
-
|
|
674
|
-
if key == ord('s') and self.pick_is_running:
|
|
675
|
-
part = self.partitions[self.pick_name]
|
|
676
|
-
stop_if_idle(part)
|
|
677
|
-
return None
|
|
678
|
-
|
|
679
|
-
if key == ord('S'):
|
|
680
|
-
for part in self.partitions.values():
|
|
681
|
-
stop_if_idle(part)
|
|
682
|
-
return None
|
|
683
|
-
|
|
684
|
-
if key == ord('l'):
|
|
685
|
-
part = self.partitions[self.pick_name]
|
|
686
|
-
self.set_state(part, 'Unlk' if part.state == 'Lock' else 'Lock')
|
|
687
|
-
|
|
688
|
-
if key == ord('/'):
|
|
689
|
-
# pylint: disable=protected-access
|
|
690
|
-
start_filter = self.prev_filter
|
|
691
|
-
|
|
692
|
-
prefix = ''
|
|
693
|
-
while True:
|
|
694
|
-
pattern = self.win.answer(f'{prefix}Enter filter regex:', seed=self.prev_filter)
|
|
695
|
-
self.prev_filter = pattern
|
|
696
|
-
|
|
697
|
-
pattern.strip()
|
|
698
|
-
if not pattern:
|
|
699
|
-
self.filter = None
|
|
700
|
-
break
|
|
701
|
-
|
|
702
|
-
try:
|
|
703
|
-
self.filter = re.compile(pattern, re.IGNORECASE)
|
|
704
|
-
break
|
|
705
|
-
except Exception:
|
|
706
|
-
prefix = 'Bad regex: '
|
|
707
|
-
|
|
708
|
-
if start_filter != self.prev_filter:
|
|
709
|
-
# when filter changes, move to top
|
|
710
|
-
self.win.pick_pos = 0
|
|
711
|
-
|
|
712
|
-
return None
|
|
713
|
-
return None
|
|
714
|
-
|
|
715
|
-
def get_keys_line(self):
|
|
716
|
-
""" TBD """
|
|
717
|
-
# KEYS
|
|
718
|
-
line = ''
|
|
719
|
-
for key, verb in self.pick_actions.items():
|
|
720
|
-
if key[0] == verb[0]:
|
|
721
|
-
line += f' {verb}'
|
|
722
|
-
else:
|
|
723
|
-
line += f' {key}:{verb}'
|
|
724
|
-
line += ' ❚'
|
|
725
|
-
line += ' Stop' if self.job_cnt > 0 else ''
|
|
726
|
-
line += f' quit ?:help /{self.prev_filter} Mode='
|
|
727
|
-
line += f'{"Random" if self.opts.random else "Zeros"}'
|
|
728
|
-
if self.opts.dry_run:
|
|
729
|
-
line += ' DRY-RUN'
|
|
730
|
-
# for action in self.actions:
|
|
731
|
-
# line += f' {action[0]}:{action}'
|
|
732
|
-
return line[1:]
|
|
733
|
-
|
|
734
|
-
def get_actions(self, part):
|
|
735
|
-
""" Determine the type of the current line and available commands."""
|
|
736
|
-
# KEYS
|
|
737
|
-
name, actions = '', {}
|
|
738
|
-
lines = self.win.body.texts
|
|
739
|
-
if 0 <= self.win.pick_pos < len(lines):
|
|
740
|
-
# line = lines[self.win.pick_pos]
|
|
741
|
-
part = self.visibles[self.win.pick_pos]
|
|
742
|
-
name = part.name
|
|
743
|
-
self.pick_is_running = bool(part.job)
|
|
744
|
-
# EXPAND
|
|
745
|
-
if self.test_state(part, to='STOP'):
|
|
746
|
-
actions['s'] = 'stop'
|
|
747
|
-
elif self.test_state(part, to='0%'):
|
|
748
|
-
actions['w'] = 'wipe'
|
|
749
|
-
if self.test_state(part, to='Lock'):
|
|
750
|
-
actions['l'] = 'lock'
|
|
751
|
-
if self.test_state(part, to='Unlk'):
|
|
752
|
-
actions['l'] = 'unlk'
|
|
753
|
-
return name, actions
|
|
754
|
-
|
|
755
|
-
def main_loop(self):
|
|
756
|
-
""" TBD """
|
|
757
|
-
|
|
758
|
-
spin = self.spin = OptionSpinner()
|
|
759
|
-
spin.default_obj = self.opts
|
|
760
|
-
spin.add_key('help_mode', '? - toggle help screen', vals=[False, True])
|
|
761
|
-
spin.add_key('random', 'r - toggle whether random or zeros', vals=[True, False])
|
|
762
|
-
# KEYS
|
|
763
|
-
other = 'wsl/Sqx'
|
|
764
|
-
other_keys = set(ord(x) for x in other)
|
|
765
|
-
other_keys.add(cs.KEY_ENTER)
|
|
766
|
-
other_keys.add(10) # another form of ENTER
|
|
767
|
-
other_keys.add(27) # ESCAPE
|
|
768
|
-
|
|
769
|
-
self.win = ConsoleWindow(head_line=True, body_rows=200, head_rows=4,
|
|
770
|
-
keys=spin.keys ^ other_keys, mod_pick=self.mod_pick)
|
|
771
|
-
self.opts.name = "[hit 'n' to enter name]"
|
|
772
|
-
check_devices_mono = time.monotonic()
|
|
773
|
-
while True:
|
|
774
|
-
if self.opts.help_mode:
|
|
775
|
-
self.win.set_pick_mode(False)
|
|
776
|
-
self.spin.show_help_nav_keys(self.win)
|
|
777
|
-
self.spin.show_help_body(self.win)
|
|
778
|
-
# KEYS
|
|
779
|
-
lines = [
|
|
780
|
-
'CONTEXT SENSITIVE:',
|
|
781
|
-
' w - wipe device',
|
|
782
|
-
' s - stop wipe device',
|
|
783
|
-
' l - lock/unlock disk',
|
|
784
|
-
' S - stop ALL wipes in progress',
|
|
785
|
-
'GENERALLY AVAILABLE:',
|
|
786
|
-
' q or x - quit program (CTL-C disabled)',
|
|
787
|
-
' / - filter devices by (anchored) regex',
|
|
788
|
-
' ESC = clear filter and jump to top',
|
|
789
|
-
' ENTER = return from help',
|
|
790
|
-
|
|
791
|
-
]
|
|
792
|
-
for line in lines:
|
|
793
|
-
self.win.put_body(line)
|
|
794
|
-
else:
|
|
795
|
-
def wanted(name):
|
|
796
|
-
return not self.filter or self.filter.search(name)
|
|
797
|
-
# self.win.set_pick_mode(self.opts.pick_mode, self.opts.pick_size)
|
|
798
|
-
self.win.set_pick_mode(True)
|
|
799
|
-
|
|
800
|
-
self.visibles = []
|
|
801
|
-
for name, partition in self.partitions.items():
|
|
802
|
-
partition.line = None
|
|
803
|
-
if partition.job:
|
|
804
|
-
if partition.job.done:
|
|
805
|
-
partition.job.thread.join()
|
|
806
|
-
to='s' if partition.job.do_abort else 'W'
|
|
807
|
-
self.set_state(partition, to=to)
|
|
808
|
-
partition.dflt = to
|
|
809
|
-
partition.mounts = []
|
|
810
|
-
self.job_cnt -= 1
|
|
811
|
-
if partition.job.exception:
|
|
812
|
-
self.win.stop_curses()
|
|
813
|
-
print('\n\n\n========== ALERT =========\n')
|
|
814
|
-
print(f' FAILED: wipe {repr(partition.name)}')
|
|
815
|
-
print(partition.job.exception)
|
|
816
|
-
input('\n\n===== Press ENTER to continue ====> ')
|
|
817
|
-
self.win._start_curses()
|
|
818
|
-
|
|
819
|
-
partition.job = None
|
|
820
|
-
if partition.job:
|
|
821
|
-
elapsed, pct, rate, until = partition.job.get_status()
|
|
822
|
-
partition.state = pct
|
|
823
|
-
partition.mounts = [f'{elapsed} {rate} REM:{until}']
|
|
824
|
-
|
|
825
|
-
if partition.parent and partition.parent in self.partitions and (
|
|
826
|
-
self.partitions[partition.parent].state == 'Lock'):
|
|
827
|
-
continue
|
|
828
|
-
|
|
829
|
-
if wanted(name) or partition.job:
|
|
830
|
-
partition.line = self.dev_info.part_str(partition)
|
|
831
|
-
self.win.add_body(partition.line)
|
|
832
|
-
self.visibles.append(partition)
|
|
833
|
-
|
|
834
|
-
self.win.add_header(self.get_keys_line(), attr=cs.A_BOLD)
|
|
835
|
-
|
|
836
|
-
self.win.add_header(self.dev_info.head_str)
|
|
837
|
-
_, col = self.win.head.pad.getyx()
|
|
838
|
-
pad = ' ' * (self.win.get_pad_width()-col)
|
|
839
|
-
self.win.add_header(pad, resume=True)
|
|
840
|
-
self.win.render()
|
|
841
|
-
|
|
842
|
-
seconds = 3.0
|
|
843
|
-
_ = self.do_key(self.win.prompt(seconds=seconds))
|
|
844
|
-
|
|
845
|
-
if time.monotonic() - check_devices_mono > (seconds * 0.95):
|
|
846
|
-
info = DeviceInfo(opts=self.opts)
|
|
847
|
-
self.partitions = info.assemble_partitions(self.partitions)
|
|
848
|
-
self.dev_info = info
|
|
849
|
-
check_devices_mono = time.monotonic()
|
|
850
|
-
|
|
851
|
-
self.win.clear()
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
def rerun_module_as_root(module_name):
|
|
855
|
-
""" rerun using the module name """
|
|
856
|
-
if os.geteuid() != 0: # Re-run the script with sudo
|
|
857
|
-
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
858
|
-
vp = ['sudo', sys.executable, '-m', module_name] + sys.argv[1:]
|
|
859
|
-
os.execvp('sudo', vp)
|
|
15
|
+
from .DiskWipe import DiskWipe
|
|
16
|
+
from .DeviceInfo import DeviceInfo
|
|
17
|
+
from .Utils import Utils
|
|
860
18
|
|
|
861
19
|
|
|
862
20
|
def main():
|
|
863
|
-
"""Main
|
|
21
|
+
"""Main entry point"""
|
|
864
22
|
import argparse
|
|
865
23
|
parser = argparse.ArgumentParser()
|
|
866
24
|
parser.add_argument('-n', '--dry-run', action='store_true',
|
|
867
|
-
|
|
25
|
+
help='just pretend to zap devices')
|
|
868
26
|
parser.add_argument('-D', '--debug', action='count', default=0,
|
|
869
|
-
|
|
27
|
+
help='debug mode (the more Ds, the higher the debug level)')
|
|
870
28
|
opts = parser.parse_args()
|
|
871
29
|
|
|
30
|
+
dwipe = None # Initialize to None so exception handler can reference it
|
|
872
31
|
try:
|
|
873
32
|
if os.geteuid() != 0:
|
|
874
33
|
# Re-run the script with sudo needed and opted
|
|
875
|
-
rerun_module_as_root('dwipe.main')
|
|
34
|
+
Utils.rerun_module_as_root('dwipe.main')
|
|
876
35
|
|
|
877
|
-
dwipe = DiskWipe(opts=opts)
|
|
36
|
+
dwipe = DiskWipe() # opts=opts)
|
|
878
37
|
dwipe.dev_info = info = DeviceInfo(opts=opts)
|
|
879
38
|
dwipe.partitions = info.assemble_partitions()
|
|
880
39
|
if dwipe.DB:
|
|
@@ -882,11 +41,18 @@ def main():
|
|
|
882
41
|
|
|
883
42
|
dwipe.main_loop()
|
|
884
43
|
except Exception as exce:
|
|
885
|
-
if
|
|
886
|
-
|
|
44
|
+
# Try to clean up curses if it was initialized
|
|
45
|
+
try:
|
|
46
|
+
if dwipe and dwipe.win:
|
|
47
|
+
dwipe.win.stop_curses()
|
|
48
|
+
except Exception:
|
|
49
|
+
pass # Ignore errors during cleanup
|
|
50
|
+
|
|
51
|
+
# Always print the error to ensure it's visible
|
|
887
52
|
print("exception:", str(exce))
|
|
888
53
|
print(traceback.format_exc())
|
|
889
54
|
sys.exit(15)
|
|
890
55
|
|
|
56
|
+
|
|
891
57
|
if __name__ == "__main__":
|
|
892
58
|
main()
|