diff --git a/hypy_utils/badblocks.html b/hypy_utils/badblocks.html new file mode 100644 index 0000000..8814123 --- /dev/null +++ b/hypy_utils/badblocks.html @@ -0,0 +1,74 @@ + + + + + + + BadBlocks Scan Result for /dev/sda + + + + + +
+

BadBlocks Scan Result for /dev/sda

+

Scan started on {{ first.timestamp }} and ended on {{ last?.timestamp }}.

+

+ Total blocks: {{ last.end_block }} blocks | + Block size: {{ d.block_size }} | + Total size: {{ (last.end_block * d.block_size / 1_000_000_000_000).toFixed(2) }} TB + = {{ (last.end_block * d.block_size / 1024 / 1024 / 1024 / 1024).toFixed(2) }} TiB

+

Red blocks indicate bad blocks or blocks that take too long (8x normal time) to scan. Hover over a block to see more information.

+

Made with ♥ by Azalea | GitHub @ hykilpikonna/HyPyUtils

+
+ +
+
+
+ + +
+

Start: {{ hover?.l?.start_block?.toString(16) }}

+

End: {{ hover?.l?.end_block?.toString(16) }}

+

Duration: {{ hover?.l?.duration?.toFixed(2) }}

+
+ + + + diff --git a/hypy_utils/badblocks.py b/hypy_utils/badblocks.py index 3ea957b..2e5f02c 100644 --- a/hypy_utils/badblocks.py +++ b/hypy_utils/badblocks.py @@ -1,6 +1,9 @@ import argparse import datetime import json +import os +import platform +from shutil import which import signal import subprocess import time @@ -29,15 +32,14 @@ def to_gb(block: int): return block * BLOCK_SIZE / (1024 * 1024 * 1024) -def disk_info() -> (int, int): - +def disk_info() -> tuple[int, int]: # 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") # 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) - pss = int(subprocess.run(f"sudo blockdev --getpbsz {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"blockdev --getpbsz {DISK}", capture_output=True, text=True, shell=True).stdout) log.info(f"Logical sector size: {lss} bytes, physical sector size: {pss} bytes") return disk_size, lss @@ -47,7 +49,7 @@ def run_badblocks(start_block: int, end_block: int): # 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)") - 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() result = subprocess.run(command, capture_output=True, text=True, shell=True, start_new_session=True) duration = time.time() - duration @@ -98,21 +100,35 @@ def run_badblocks(start_block: int, end_block: int): if __name__ == "__main__": # Take in disk and block size as optional arguments 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("--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("--end", "-e", type=int, help="End block") + parser.add_argument("--rescan", action="store_true", help="Rescan the whole disk") args = parser.parse_args() DISK = args.disk BLOCK_SIZE = args.block_size START = args.start 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" if not LOG_FILE.exists(): LOG_FILE.write_text(json.dumps({"logs": [], "block_size": BLOCK_SIZE}, indent=2)) - else: + elif not args.rescan: # Check if the block size matches block_size = json.loads(LOG_FILE.read_text())["block_size"] if block_size != BLOCK_SIZE: @@ -128,8 +144,19 @@ if __name__ == "__main__": gb_approx = 1024 * 1024 * 1024 // BLOCK_SIZE disk_size, lss = disk_info() - for start in range(START or 0, END or disk_size, gb_approx): - end = min(start + gb_approx, disk_size) - run_badblocks(start, end) - if pending_stop: - break + if args.command == "scan": + for start in range(START or 0, END or disk_size, gb_approx): + end = min(start + gb_approx, disk_size) + run_badblocks(start, end) + 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.")