[+] Badblocks UI

This commit is contained in:
2024-12-10 06:31:16 -05:00
parent 52fcbfc205
commit 6952b160f1
2 changed files with 113 additions and 12 deletions
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="html5">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BadBlocks Scan Result for /dev/sda</title>
<script src="https://unpkg.com/petite-vue"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body v-scope @vue:mounted="mounted" class="p-4 relative flex flex-col gap-3">
<div>
<h1 class="text-2xl font-bold mb-4">BadBlocks Scan Result for /dev/sda</h1>
<p>Scan started on {{ first.timestamp }} and ended on {{ last?.timestamp }}.</p>
<p>
Total blocks: {{ last.end_block }} blocks |
Block size: {{ d.block_size }} |
Total size: <span class="text-red-500">{{ (last.end_block * d.block_size / 1_000_000_000_000).toFixed(2) }} TB</span>
<span class="text-gray-400">= {{ (last.end_block * d.block_size / 1024 / 1024 / 1024 / 1024).toFixed(2) }} TiB</span></p>
<p><span class="text-red-500">Red</span> blocks indicate bad blocks or blocks that take too long (8x normal time) to scan. Hover over a block to see more information.</p>
<p>Made with ♥ by <a href="https://github.com/hykilpikonna" class="text-red-500 underline">Azalea</a> | GitHub @ <a href="https://github.com/hykilpikonna/HyPyUtils" class="text-red-500 underline">hykilpikonna/HyPyUtils</a></p>
</div>
<div class="flex flex-wrap gap-0.5">
<div v-for="(log, index) in d.logs" :key="index"
class="inline-block w-2 h-2"
:style="{backgroundColor: getBlockColor(log)}"
@mouseenter="showHoverInfo($event, log, index)" @mouseleave="hideHoverInfo"></div>
</div>
<!-- Tooltip for showing hover information -->
<div v-if="hover"
:style="{top: hover?.y + 'px', left: hover?.x + 'px'}"
class="absolute bg-gray-800 text-white text-sm rounded px-2 py-1 shadow-md pointer-events-none transition-opacity duration-150">
<p>Start: {{ hover?.l?.start_block?.toString(16) }}</p>
<p>End: {{ hover?.l?.end_block?.toString(16) }}</p>
<p>Duration: {{ hover?.l?.duration?.toFixed(2) }}</p>
</div>
</body>
<script>
PetiteVue.createApp({
d: { logs: [] }, // timestamp, duration, start_block, end_block, bad_blocks
max_dur: 0, min_dur: 0, hover: null, firs: null, last: null,
onInit() {
this.max_dur = Math.max(...this.d.logs.map(l => l.duration))
this.min_dur = Math.min(...this.d.logs.map(l => l.duration))
this.max_dur = Math.min(this.max_dur, this.min_dur * 8)
console.log(`Min duration: ${this.min_dur}, Max duration: ${this.max_dur}`)
this.first = this.d.logs[0]
this.last = this.d.logs[this.d.logs.length - 1]
},
mounted() {
if (this.d.logs.length) return this.onInit() // For injecting data from server-side
fetch('http://localhost:8080/badblocks_log__dev_sda.json').then(resp => resp.json())
.then(data => { this.d = data; this.onInit() })
},
getBlockColor(log) {
if (log.bad_blocks.length) return 'red'
const ratio = 1 - ((log.duration - this.min_dur) / (this.max_dur - this.min_dur))
return `rgb(${Math.round(255 * (1 - ratio))}, ${Math.round(255 * ratio)}, 0)`
},
showHoverInfo(event, log, index) {
const rect = event.target.getBoundingClientRect();
this.hover = { l: log,
x: rect.left + window.scrollX + 10,
y: rect.top + window.scrollY - 30
}
},
hideHoverInfo() { this.hover = null }
}).mount()
</script>
</html>
+39 -12
View File
@@ -1,6 +1,9 @@
import argparse import argparse
import datetime import datetime
import json import json
import os
import platform
from shutil import which
import signal import signal
import subprocess import subprocess
import time import time
@@ -29,15 +32,14 @@ def to_gb(block: int):
return block * BLOCK_SIZE / (1024 * 1024 * 1024) return block * BLOCK_SIZE / (1024 * 1024 * 1024)
def disk_info() -> (int, int): def disk_info() -> tuple[int, int]:
# Get the disk size in blocks # Get the disk size in blocks
disk_size = int(subprocess.run(f"sudo blockdev --getsize64 {DISK}", capture_output=True, text=True, shell=True).stdout) // BLOCK_SIZE disk_size = int(subprocess.run(f"blockdev --getsize64 {DISK}", capture_output=True, text=True, shell=True).stdout) // BLOCK_SIZE
log.info(f"Disk size: {to_gb(disk_size):,.0f} GB, {disk_size:#x} blocks") log.info(f"Disk size: {to_gb(disk_size):,.0f} GB, {disk_size:#x} blocks")
# Get the size of a logical sector (LDA) # Get the size of a logical sector (LDA)
lss = int(subprocess.run(f"sudo blockdev --getss {DISK}", capture_output=True, text=True, shell=True).stdout) lss = int(subprocess.run(f"blockdev --getss {DISK}", capture_output=True, text=True, shell=True).stdout)
pss = int(subprocess.run(f"sudo blockdev --getpbsz {DISK}", capture_output=True, text=True, shell=True).stdout) pss = int(subprocess.run(f"blockdev --getpbsz {DISK}", capture_output=True, text=True, shell=True).stdout)
log.info(f"Logical sector size: {lss} bytes, physical sector size: {pss} bytes") log.info(f"Logical sector size: {lss} bytes, physical sector size: {pss} bytes")
return disk_size, lss return disk_size, lss
@@ -47,7 +49,7 @@ def run_badblocks(start_block: int, end_block: int):
# Print block address in hex # Print block address in hex
log.debug(f"Scanning from {start_block:#x} ({to_gb(start_block):,.0f} GB) to {end_block:#x} ({to_gb(end_block):,.0f} GB)") log.debug(f"Scanning from {start_block:#x} ({to_gb(start_block):,.0f} GB) to {end_block:#x} ({to_gb(end_block):,.0f} GB)")
command = f"sudo badblocks -b 4096 -v {DISK} {end_block} {start_block}" command = f"badblocks -b 4096 -v {DISK} {end_block} {start_block}"
duration = time.time() duration = time.time()
result = subprocess.run(command, capture_output=True, text=True, shell=True, start_new_session=True) result = subprocess.run(command, capture_output=True, text=True, shell=True, start_new_session=True)
duration = time.time() - duration duration = time.time() - duration
@@ -98,21 +100,35 @@ def run_badblocks(start_block: int, end_block: int):
if __name__ == "__main__": if __name__ == "__main__":
# Take in disk and block size as optional arguments # Take in disk and block size as optional arguments
parser = argparse.ArgumentParser("Bad block detection utility") parser = argparse.ArgumentParser("Bad block detection utility")
parser.add_argument("command", type=str, help="Command to run", choices=["scan", "plot"])
parser.add_argument("--disk", "-d", type=str, help="Disk to scan") parser.add_argument("--disk", "-d", type=str, help="Disk to scan")
parser.add_argument("--block-size", "-b", type=int, default=4096, help="Block size in bytes") parser.add_argument("--block-size", "-b", type=int, default=4096, help="Block size in bytes")
parser.add_argument("--start", "-s", type=int, help="Start block") parser.add_argument("--start", "-s", type=int, help="Start block")
parser.add_argument("--end", "-e", type=int, help="End block") parser.add_argument("--end", "-e", type=int, help="End block")
parser.add_argument("--rescan", action="store_true", help="Rescan the whole disk")
args = parser.parse_args() args = parser.parse_args()
DISK = args.disk DISK = args.disk
BLOCK_SIZE = args.block_size BLOCK_SIZE = args.block_size
START = args.start START = args.start
END = args.end END = args.end
try:
assert platform.system() != "Windows", "Windows is not supported, go use DiskGenius or something"
assert which("badblocks"), "badblocks command not found, please install e2fsprogs"
assert which("blockdev"), "blockdev command not found, please install util-linux"
assert DISK and Path(DISK).exists(), f"Disk {DISK} does not exist"
assert BLOCK_SIZE % 512 == 0, "Block size must be a multiple of 512"
assert os.geteuid() == 0, "You need to run as root to access the disk"
except AssertionError as e:
log.error(e.args[0])
exit(1)
LOG_FILE = Path(__file__).parent / f"badblocks_log_{DISK.replace('/', '_')}.json" LOG_FILE = Path(__file__).parent / f"badblocks_log_{DISK.replace('/', '_')}.json"
if not LOG_FILE.exists(): if not LOG_FILE.exists():
LOG_FILE.write_text(json.dumps({"logs": [], "block_size": BLOCK_SIZE}, indent=2)) LOG_FILE.write_text(json.dumps({"logs": [], "block_size": BLOCK_SIZE}, indent=2))
else: elif not args.rescan:
# Check if the block size matches # Check if the block size matches
block_size = json.loads(LOG_FILE.read_text())["block_size"] block_size = json.loads(LOG_FILE.read_text())["block_size"]
if block_size != BLOCK_SIZE: if block_size != BLOCK_SIZE:
@@ -128,8 +144,19 @@ if __name__ == "__main__":
gb_approx = 1024 * 1024 * 1024 // BLOCK_SIZE gb_approx = 1024 * 1024 * 1024 // BLOCK_SIZE
disk_size, lss = disk_info() disk_size, lss = disk_info()
for start in range(START or 0, END or disk_size, gb_approx): if args.command == "scan":
end = min(start + gb_approx, disk_size) for start in range(START or 0, END or disk_size, gb_approx):
run_badblocks(start, end) end = min(start + gb_approx, disk_size)
if pending_stop: run_badblocks(start, end)
break if pending_stop:
break
# Plot
ouf = Path(f"badblocks{DISK.replace('/', '_')}.html")
html = ((Path(__file__).parent / 'badblocks.html').read_text()
.replace("d: { logs: [] }", f"d: {LOG_FILE.read_text()}")
.replace("/dev/sda", DISK)
)
ouf.write_text(html)
log.info(f"Results saved to {ouf}.")
log.warning(f"You can open the html {ouf.absolute().as_uri()} in your browser. I can't open it for you because this script is running in sudo.")