├── LICENSE ├── README.md └── simhdd.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergey Kazenniy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simhdd 2 | Программа предназначена для одновременного тестирования на ошибки нескольких жестких дисков. 3 | 4 | # ВНИМАНИЕ! 5 | Программа не предупреждает о деструктивности своих действий. Автор не несет ответственности за возможную потерю данных. Будте осторожны. 6 | 7 | # Использование 8 | 9 | ``` 10 | sudo simhdd.py 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /simhdd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from atapt import atapt 4 | import ctypes 5 | import sys 6 | import time 7 | import os 8 | import termios 9 | import fcntl 10 | from multiprocessing import Process, Manager 11 | 12 | SECTORS_AT_ONCE = 256 13 | SLOW_SECTOR_LATENCY = 50 14 | NEED_QUIT = True 15 | ERASE_WITH_PATTERN = False 16 | 17 | 18 | def printDisks(sel): 19 | print("{:^5}{:^30}{:^20}{:^6} {}{:^24}{}".format("#", "Model", 20 | "Serial", "Size", "Mode Loop", "Progress", " Speed Slow Error")) 21 | print(123 * "-") 22 | for key in select: 23 | if key == 0: 24 | continue 25 | print() 26 | if select.index(key) == sel: 27 | print('\033[30m' + '\033[47m', end="") 28 | else: 29 | print('\033[37m' + '\033[40m', end="") 30 | print("{:^5}{:<30}{:<20}{:>5} Gb {:^7}{:^5}{:<22}{:>2}{:^7}{:^7}{:^7}".format(select.index(key), disks[key].model, 31 | disks[key].serial, int(disks[key].size), 32 | mode[disks[key].serial], loop[disks[key].serial], 33 | "[" + progress[disks[key].serial] * "#" + (20 - progress[disks[key].serial]) * " " + "]", 34 | busy[disks[key].serial], speed[disks[key].serial], slow[disks[key].serial], error[disks[key].serial], end="")) 35 | print('\033[37m' + '\033[40m', end="") 36 | print() 37 | 38 | 39 | def showSmart(serial): 40 | disk = disks[serial] 41 | os.system('clear') 42 | # Disk identifycation 43 | print() 44 | print("Device: " + disk.dev) 45 | print("Model: " + disk.model) 46 | print("Firmware: " + disk.firmware) 47 | print("Serial: " + disk.serial) 48 | print("Sectors: %d" % disk.sectors) 49 | print("Size: %d Gb" % disk.size) 50 | if disk.ssd: 51 | print("Type: SSD") 52 | else: 53 | print("Type: HDD") 54 | if disk.rpm > 1: 55 | print("RPM: %d" % disk.rpm) 56 | print("log. sector size: %d bytes" % disk.logicalSectorSize) 57 | print("phys. sector size: %d bytes" % disk.physicalSectorSize) 58 | 59 | # Read SMART 60 | print() 61 | print("Read SMART") 62 | disk.readSmart() 63 | print() 64 | print("SMART VALUES:") 65 | print("ID# ATTRIBUTE NAME TYPE UPDATED VALUE WORST THRESH RAW") 66 | for id in sorted(disk.smart): 67 | if disk.smart[id][3] < disk.smart[id][5]: 68 | print("\033[91m", end="") 69 | # [pre_fail, online, current, worst, raw, treshold] 70 | print("{:>3} {:<24} {:10} {:7} {} {} {} {}".format(id, disk.getSmartStr(id), 71 | "Pre-fail" if disk.smart[id][0] else "Old_age", 72 | "Always" if disk.smart[id][1] else "Offline", 73 | " %03d" % disk.smart[id][2], 74 | " %03d" % disk.smart[id][3], 75 | " %03d" % disk.smart[id][5], disk.getSmartRawStr(id))) 76 | print("\033[0m", end="") 77 | if disk.readSmartStatus() == atapt.SMART_BAD_STATUS: 78 | print("\033[91mSMART STATUS BAD!\033[0m") 79 | 80 | # Read SMART Self-test log 81 | print() 82 | log = disk.getSelftestLog() 83 | print("SMART Self-test log structure revision number %d" % log[0]) 84 | print("Test Status Remaining LifeTime(hours) LBA of first error") 85 | for i in log[1]: 86 | print("{:<20}".format(i[0]), end="") 87 | print("{:<25}".format(i[1]), end="") 88 | print("{:^9}".format(str(i[2]) + "%"), end="") 89 | print("{:^15}".format(i[3]), end="") 90 | if i[1] == "in progress": 91 | print("{:^25}".format("-"), end="") 92 | else: 93 | print("{:^25}".format(i[4]), end="") 94 | print() 95 | 96 | while not sys.stdin.read(1): 97 | time.sleep(0.1) 98 | os.system('clear') 99 | 100 | 101 | def nextBusy(busy): 102 | if busy == "|": 103 | return("/") 104 | elif busy == "/": 105 | return("-") 106 | elif busy == "-": 107 | return("\\") 108 | elif busy == "\\": 109 | return("|") 110 | 111 | 112 | def diskLongTest(serial): 113 | disk = disks[serial] 114 | slow[serial] = 0 115 | error[serial] = 0 116 | progress[serial] = 0 117 | loop[serial] = 0 118 | speed[serial] = 0 119 | awgSpeed = 0 120 | mode[serial] = "Long" 121 | time.sleep(1) 122 | busy[serial] = "|" 123 | try: 124 | disk.runSmartSelftest(2) # Execute SMART Extended self-test routine immediately in off-line mode 125 | except atapt.senseError: 126 | error[serial] = 1 127 | disk.readSmart() 128 | while disk.selftestStatus >> 4 == 0x0F: 129 | time.sleep(0.5) 130 | disk.readSmart() 131 | progress[serial] = 20 - (disk.selftestStatus & 0x0F) * 2 132 | busy[serial] = nextBusy(busy[serial]) 133 | if mode[serial] != "Long": 134 | error[serial] = 0 135 | try: 136 | disk.runSmartSelftest(0x7F) # Abort off-line mode self-test routine 137 | except atapt.senseError: 138 | error[serial] = 1 139 | slow[serial] = 0 140 | progress[serial] = 0 141 | busy[serial] = " " 142 | return 143 | if disk.selftestStatus != 0: 144 | error[serial] = disk.selftestStatus 145 | else: 146 | progress[serial] = 20 147 | mode[serial] = "Idle" 148 | busy[serial] = " " 149 | speed[serial] = 0 150 | 151 | 152 | def diskShortTest(serial): 153 | disk = disks[serial] 154 | slow[serial] = 0 155 | error[serial] = 0 156 | progress[serial] = 0 157 | loop[serial] = 0 158 | speed[serial] = 0 159 | awgSpeed = 0 160 | mode[serial] = "Short" 161 | time.sleep(1) 162 | busy[serial] = "|" 163 | try: 164 | disk.runSmartSelftest(1) # Execute SMART Short self-test routine immediately in off-line mode 165 | except atapt.senseError: 166 | error[serial] = 1 167 | disk.readSmart() 168 | while disk.selftestStatus >> 4 == 0x0F: 169 | time.sleep(0.5) 170 | disk.readSmart() 171 | progress[serial] = 20 - (disk.selftestStatus & 0x0F) * 2 172 | busy[serial] = nextBusy(busy[serial]) 173 | if mode[serial] != "Short": 174 | error[serial] = 0 175 | try: 176 | disk.runSmartSelftest(0x7F) # Abort off-line mode self-test routine 177 | except atapt.senseError: 178 | error[serial] = 1 179 | slow[serial] = 0 180 | progress[serial] = 0 181 | busy[serial] = " " 182 | return 183 | if disk.selftestStatus != 0: 184 | error[serial] = disk.selftestStatus 185 | else: 186 | progress[serial] = 20 187 | mode[serial] = "Idle" 188 | busy[serial] = " " 189 | speed[serial] = 0 190 | 191 | 192 | def diskVerify(serial): 193 | disk = disks[serial] 194 | slow[serial] = 0 195 | error[serial] = 0 196 | progress[serial] = 0 197 | loop[serial] = 0 198 | speed[serial] = 0 199 | awgSpeed = 0 200 | mode[serial] = "Read" 201 | time.sleep(1) 202 | busy[serial] = "|" 203 | blockSize = disk.logicalSectorSize * SECTORS_AT_ONCE / 1024 / 1024 204 | tail = disk.sectors % SECTORS_AT_ONCE 205 | for i in range(0, disk.sectors - tail - 1, SECTORS_AT_ONCE): 206 | try: 207 | disk.verifySectors(SECTORS_AT_ONCE, i) 208 | except atapt.senseError: 209 | error[serial] = error[serial] + 1 210 | awgSpeed = awgSpeed + int(1 / (0.001 * disk.duration) * blockSize) 211 | if disk.ata_error != 0: 212 | error[serial] = error[serial] + 1 213 | elif disk.duration > SLOW_SECTOR_LATENCY: 214 | slow[serial] = slow[serial] + 1 215 | if (i % (SECTORS_AT_ONCE * 512)) == 0: 216 | speed[serial] = int(awgSpeed / 512) 217 | awgSpeed = 0 218 | progress[serial] = int(20 / disk.sectors * i) 219 | busy[serial] = nextBusy(busy[serial]) 220 | if mode[serial] != "Read": 221 | slow[serial] = 0 222 | error[serial] = 0 223 | progress[serial] = 0 224 | busy[serial] = " " 225 | return 226 | try: 227 | disk.verifySectors(tail, disk.sectors - tail) 228 | except atapt.senseError: 229 | pass 230 | if disk.ata_error != 0: 231 | error[serial] = error[serial] + 1 232 | elif disk.duration > 100: 233 | slow[serial] = slow[serial] + 1 234 | mode[serial] = "Idle" 235 | progress[serial] = 20 236 | busy[serial] = " " 237 | speed[serial] = 0 238 | 239 | 240 | def diskErase(serial): 241 | disk = disks[serial] 242 | loop[serial] = 0 243 | mode[serial] = "Write" 244 | time.sleep(1) 245 | busy[serial] = "|" 246 | blockSize = disk.logicalSectorSize * SECTORS_AT_ONCE / 1024 / 1024 247 | while 1: 248 | speed[serial] = 0 249 | awgSpeed = 0 250 | slow[serial] = 0 251 | error[serial] = 0 252 | progress[serial] = 0 253 | tail = disk.sectors % SECTORS_AT_ONCE 254 | buf = ctypes.c_buffer(disk.logicalSectorSize * SECTORS_AT_ONCE) 255 | for i in range(disk.logicalSectorSize * SECTORS_AT_ONCE): 256 | if ERASE_WITH_PATTERN: 257 | buf[i] = int(i % 128) 258 | else: 259 | buf[i] = 0 260 | for i in range(0, disk.sectors - tail - 1, SECTORS_AT_ONCE): 261 | try: 262 | disk.writeSectors(SECTORS_AT_ONCE, i, buf) 263 | except atapt.senseError: 264 | error[serial] = error[serial] + 1 265 | awgSpeed = awgSpeed + int(1 / (0.001 * disk.duration) * blockSize) 266 | if disk.ata_error != 0: 267 | error[serial] = error[serial] + 1 268 | elif disk.duration > SLOW_SECTOR_LATENCY: 269 | slow[serial] = slow[serial] + 1 270 | if (i % (SECTORS_AT_ONCE * 512)) == 0: 271 | speed[serial] = int(awgSpeed / 512) 272 | awgSpeed = 0 273 | progress[serial] = int(20 / disk.sectors * i) 274 | busy[serial] = nextBusy(busy[serial]) 275 | if mode[serial] != "Write": 276 | slow[serial] = 0 277 | error[serial] = 0 278 | progress[serial] = 0 279 | loop[serial] = 0 280 | busy[serial] = " " 281 | return 282 | buf = ctypes.c_buffer(disk.logicalSectorSize * tail) 283 | for i in range(disk.logicalSectorSize * tail): 284 | if ERASE_WITH_PATTERN: 285 | buf[i] = int(i % 128) 286 | else: 287 | buf[i] = 0 288 | try: 289 | disk.writeSectors(tail, disk.sectors - tail, buf) 290 | except atapt.senseError: 291 | pass 292 | if disk.ata_error != 0: 293 | error[serial] = error[serial] + 1 294 | elif disk.duration > 100: 295 | slow[serial] = slow[serial] + 1 296 | loop[serial] = loop[serial] + 1 297 | 298 | 299 | def checkDevs(): 300 | state = True 301 | devs = list(filter(lambda x: x.find('sd') != -1 and len(x) == 3, os.listdir("/dev"))) 302 | for key in list(disks.keys()): 303 | if disks[key].dev[-3:] not in devs: 304 | disks.pop(key) 305 | select.remove(key) 306 | progress.pop(key) 307 | mode.pop(key) 308 | loop.pop(key) 309 | slow.pop(key) 310 | error.pop(key) 311 | busy.pop(key) 312 | speed.pop(key) 313 | state = False 314 | for dev in devs: 315 | disk = atapt.atapt("/dev/" + dev) 316 | if disks.get(disk.serial) and disks[disk.serial].dev == disk.dev: 317 | continue 318 | else: 319 | if disks.get(disk.serial): 320 | disks.pop(disk.serial) 321 | select.remove(disk.serial) 322 | progress.pop(disk.serial) 323 | mode.pop(disk.serial) 324 | loop.pop(disk.serial) 325 | slow.pop(disk.serial) 326 | error.pop(disk.serial) 327 | busy.pop(disk.serial) 328 | speed.pop(disk.serial) 329 | state = False 330 | disks[disk.serial] = disk 331 | select.append(disks[disk.serial].serial) 332 | progress[disk.serial] = 0 333 | loop[disk.serial] = 0 334 | slow[disk.serial] = 0 335 | error[disk.serial] = 0 336 | speed[disk.serial] = 0 337 | busy[disk.serial] = " " 338 | mode[disk.serial] = "Idle" 339 | if not state: 340 | print("\a") 341 | os.system('clear') 342 | return(False) 343 | else: 344 | return(True) 345 | 346 | 347 | m = Manager() 348 | disks = {} 349 | select = [0] 350 | progress = m.dict() 351 | mode = m.dict() 352 | loop = m.dict() 353 | slow = m.dict() 354 | error = m.dict() 355 | busy = m.dict() 356 | speed = m.dict() 357 | 358 | checkDevs() 359 | 360 | sel = 0 361 | try: 362 | fd = sys.stdin.fileno() 363 | 364 | oldterm = termios.tcgetattr(fd) 365 | newattr = termios.tcgetattr(fd) 366 | newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO 367 | termios.tcsetattr(fd, termios.TCSANOW, newattr) 368 | 369 | oldflags = fcntl.fcntl(fd, fcntl.F_GETFL) 370 | fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) 371 | print('\033[9;0]' + '\033[14;0]', end="") 372 | os.system('clear') 373 | # os.system('dmesg -D') 374 | 375 | while 1: 376 | ch = sys.stdin.read(1) 377 | print('\033[0;0H', end="") 378 | printDisks(sel) 379 | if (ch >= "1") and (ch <= str(len(select) - 1)): 380 | sel = int(ch) 381 | ch = 0 382 | if sel > 0: 383 | print("Select action : I-Info V-Verify E-Erase R-Short L-Long S-Stop C-Rescan", end="") 384 | if NEED_QUIT: 385 | print(" Q-Quit") 386 | else: 387 | print(" ") 388 | if ch == "i" or ch == "I": 389 | if checkDevs(): 390 | showSmart(select[sel]) 391 | sel = 0 392 | elif ch == "v" or ch == "V": 393 | if checkDevs(): 394 | d = Process(target=diskVerify, args=(select[sel],)) 395 | d.start() 396 | sel = 0 397 | elif ch == "e" or ch == "E": 398 | if checkDevs(): 399 | d = Process(target=diskErase, args=(select[sel],)) 400 | d.start() 401 | sel = 0 402 | elif ch == "r" or ch == "R": 403 | if checkDevs(): 404 | d = Process(target=diskShortTest, args=(select[sel],)) 405 | d.start() 406 | sel = 0 407 | elif ch == "l" or ch == "L": 408 | if checkDevs(): 409 | d = Process(target=diskLongTest, args=(select[sel],)) 410 | d.start() 411 | sel = 0 412 | elif ch == "s" or ch == "S": 413 | mode[select[sel]] = "Idle" 414 | sel = 0 415 | elif ch == "c" or ch == "C": 416 | checkDevs() 417 | sel = 0 418 | elif NEED_QUIT and (ch == "q" or ch == "Q"): 419 | exit() 420 | else: 421 | print("Select disk : _" + 100 * " ") 422 | 423 | time.sleep(0.1) 424 | finally: 425 | termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm) 426 | fcntl.fcntl(fd, fcntl.F_SETFL, oldflags) 427 | 428 | --------------------------------------------------------------------------------