/* scsimon.c -- a simple SCSI drive health monitor (version 1.2) */ #include #include #include #include #include #include #include #include /* Celsius -> Fahrenheit conversion */ #define C_TO_F(T) ((int)((9.0/5.0) * (T) + 32)) /* SCSI opcodes */ #define SCSI_INQUIRY 0x12 #define SCSI_SEND_DIAG 0x1d #define SCSI_LOG_SENSE 0x4d /* SCSI log pages */ #define SCSI_SUPPORTED_LOG_PAGES 0x00 /* list supported log pages */ #define SCSI_LOG_PAGE_TEMP 0x0d /* drive temperature */ #define SCSI_LOG_PAGE_SELF_TEST 0x10 /* self-test results */ #define SCSI_LOG_PAGE_IE 0x2f /* informational exceptions */ #define SCSI_LOG_PAGE_SEAGATE 0x3e /* Seagate factory info */ /* SCSI diagnostic modes */ #define SCSI_DIAG_BG_SHORT 0x01 /* short background self-test */ #define SCSI_DIAG_BG_EXT 0x02 /* extended background self-test */ #define SCSI_DIAG_ABORT 0x04 /* abort self-test */ int scsi_open(char *path) { return open(path, O_RDONLY | O_NONBLOCK); } int scsi_close(int dev) { return close(dev); } int scsi_send_command(int dev, char *cmd, int cmd_len, char *buf, int buf_len) { dsreq_t r; memset(&r, 0, sizeof(dsreq_t)); /* Assemble the request structure */ r.ds_cmdbuf = (caddr_t) cmd; r.ds_cmdlen = cmd_len; r.ds_databuf = (caddr_t) buf; r.ds_datalen = buf_len; r.ds_sensebuf = (caddr_t) buf; r.ds_senselen = buf_len; r.ds_time = 5 * 1000; /* 5 seconds should be enough */ r.ds_flags = DSRQ_READ; /* Issue the request */ if (ioctl(dev, DS_ENTER, &r)) return -errno; return 0; } int scsi_inquire(int dev, char **outbuf) { /* op lun page reserved alloc_len ctrl */ char cmd[6] = { SCSI_INQUIRY, 0, 0, 0, 36, 0 }; char buf[36]; *outbuf = NULL; memset(buf, 0, 36); if (scsi_send_command(dev, cmd, sizeof(cmd), buf, cmd[4]) != 0) { fprintf(stderr, "ERROR: Inquiry failed: %s\n", strerror(errno)); return -1; } /* Return vendor, model & revision */ *outbuf = (char *)calloc(29, sizeof(char)); if (*outbuf) { memcpy(*outbuf, (void *)(buf + 8), 28); } return 0; } int scsi_log_sense(int dev, int page, char *buf, int buf_len) { char cmd[10] = { SCSI_LOG_SENSE, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; cmd[2] = (char)(0x40 | (page & 0x3f)); /* set page code */ cmd[7] = (char)((buf_len >> 8) & 0xff); /* set max. response length */ cmd[8] = (char)(buf_len & 0xff); return scsi_send_command(dev, cmd, sizeof(cmd), buf, buf_len); } int scsi_send_diag(int dev, int mode) { /* op st|pf|off reserved plist_len plist_len ctrl */ char cmd[6] = { SCSI_SEND_DIAG, 0, 0, 0, 0, 0 }; cmd[1] = (char)((mode & 0x7) << 5); /* set diag. code */ return scsi_send_command(dev, cmd, sizeof(cmd), NULL, 0); } int scsi_has_log_page(int dev, int page) { char buf[1024]; int i, hasPage = 0; memset(buf, 0, sizeof(buf)); /* Get a list of all supported log pages */ if (scsi_log_sense(dev, SCSI_SUPPORTED_LOG_PAGES, buf, sizeof(buf)) != 0) { fprintf(stderr, "ERROR: Log sense failed: %s\n", strerror(errno)); return 0; } /* 4 == length of header */ for (i = 4; i < ((buf[2] << 8) | buf[3]) + 4; i++) { if (buf[i] == page) { hasPage = 1; break; } } return hasPage; } int scsi_get_temp(int dev, int *Tcur, int *Tmax) { char buf[1024], *bp; int num, type, len; *Tcur = *Tmax = -1; memset(buf, 0, sizeof(buf)); /* Check if the drive has a temperature log page */ if (!scsi_has_log_page(dev, SCSI_LOG_PAGE_TEMP)) { goto try_ie; } /* Get temperature data */ if (scsi_log_sense(dev, SCSI_LOG_PAGE_TEMP, buf, sizeof(buf)) != 0) { fprintf(stderr, "ERROR: Log sense failed: %s\n", strerror(errno)); return -1; } *Tcur = (int)buf[9]; *Tmax = (int)buf[15]; return 0; try_ie: /* Check if the drive has an Informational Exceptions log page */ if (!scsi_has_log_page(dev, SCSI_LOG_PAGE_IE)) { fprintf(stderr, "ERROR: Device does not support temperature readout.\n"); return -1; } memset(buf, 0, sizeof(buf)); /* Get IE data */ if (scsi_log_sense(dev, SCSI_LOG_PAGE_IE, buf, sizeof(buf)) != 0) { fprintf(stderr, "ERROR: Log sense failed: %s\n", strerror(errno)); return -1; } num = ((buf[2] << 8) | buf[3]) - 4; /* page length */ bp = &buf[0] + 4; /* skip header */ while (num >= 4) { type = (bp[0] << 8) | bp[1]; len = bp[3]; if (type == 0) { /* general param. data page */ *Tcur = (int)bp[6]; if (*Tcur == 0xff) *Tcur = -1; break; } bp += len + 4; num -= len + 4; } return 0; } const char *scsi_selftest_code2str[] = { "default", "bg short", "bg extended", "reserved", "bg aborted", "fg short", "fg extended", "reserved" }; const char *scsi_selftest_result2str[] = { "completed OK", "aborted (SEND DIAG.)", "aborted (non-SEND-DIAG.)", "unknown error", "failure in unknown segment", "failure in 1st segment", "failure in 2nd segment", "failure in some segment", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "in progress" }; int scsi_get_selftest(int dev, int **uptime, int **code, int **result) { char buf[1024], *p; int i, n; int *up, *cd, *rs; *uptime = *code = *result = NULL; memset(buf, 0, sizeof(buf)); /* Check if the drive supports reading self-test results */ if (!scsi_has_log_page(dev, SCSI_LOG_PAGE_SELF_TEST)) { fprintf(stderr, "ERROR: Device does not support self-test readout.\n"); return -1; } /* Get self-test data */ if (scsi_log_sense(dev, SCSI_LOG_PAGE_SELF_TEST, buf, sizeof(buf)) != 0) { fprintf(stderr, "ERROR: Log sense failed: %s\n", strerror(errno)); return -1; } /* Convert self-test data */ up = (int *)calloc(20, sizeof(int)); cd = (int *)calloc(20, sizeof(int)); rs = (int *)calloc(20, sizeof(int)); for (i = 0; i < 20; i++) up[i] = cd[i] = rs[i] = -1; for (i = 0, p = buf + 4; i < 20; i++, p += 20) { n = (p[6] << 8) | p[7]; if (n == 0 && p[4] == 0) /* Stop if we've reached the end */ break; up[i] = n; cd[i] = (p[4] >> 5) & 0x7; rs[i] = p[4] & 0xf; } *uptime = up; *code = cd; *result = rs; return 0; } static unsigned long long to_ull(char *b, int len) { char *bp = b; unsigned long long n = 0; int i, j = len; /* Just to be safe... */ if (j > sizeof(unsigned long long)) { bp += j - sizeof(unsigned long long); j = sizeof(unsigned long long); } /* Glue the bytes together */ for (i = 0; i < j; i++) { if (i > 0) n <<= 8; n |= bp[i]; } return n; } int scsi_get_seagate(int dev, unsigned long long *uptime, unsigned long long *next) { char buf[1024], *bp; int type, len, num = 0; *uptime = *next = 0; memset(buf, 0, sizeof(buf)); /* Check if the drive supports reading Seagate factory data */ if (!scsi_has_log_page(dev, SCSI_LOG_PAGE_SEAGATE)) return -1; /* Get Seagate data */ if (scsi_log_sense(dev, SCSI_LOG_PAGE_SEAGATE, buf, sizeof(buf)) != 0) { fprintf(stderr, "ERROR: Log sense failed: %s\n", strerror(errno)); return -1; } /* Get number of bytes in the page (minus the header) */ num = ((buf[2] << 8) | buf[3]) - 4; bp = &buf[0] + 4; /* skip header */ /* Scan the list for uptime & time of next internal test */ while (num >= 4) { type = (bp[0] << 8) | bp[1]; len = bp[3]; if (type == 0) /* uptime */ *uptime = to_ull(&bp[4], len); else if (type == 8) /* next internal test */ *next = to_ull(&bp[4], len); bp += len + 4; num -= len + 4; } return 0; } void do_drive(char *path, int doTest) { char *inq = NULL; int *stUptime = NULL, *stCode = NULL, *stResult = NULL; int dev, Tcur = 0, Tmax = 0, i; unsigned long long uptime = 0, next = 0; dev = scsi_open(path); if (dev < 0) { fprintf(stderr, "ERROR: Cannot open device: %s\n", strerror(errno)); exit(1); } if (doTest) { printf("Performing self-test ...\n"); if (scsi_send_diag(dev, doTest) != 0) { fprintf(stderr, "ERROR: Failed to issue self-test!\n"); exit(1); } } if (scsi_inquire(dev, &inq) == 0) { printf("Inquiry response:\t[%s]\n", inq); free(inq); } if (scsi_get_temp(dev, &Tcur, &Tmax) == 0) { if (Tcur != -1) printf("Current temperature:\t%dC/%dF\n", Tcur, C_TO_F(Tcur)); if (Tmax != -1) printf("Maximum temperature:\t%dC/%dF\n", Tmax, C_TO_F(Tmax)); } if (scsi_get_seagate(dev, &uptime, &next) == 0) { unsigned long long m = uptime, d = 0, h = 0; if (m >= 60 * 24) d = (unsigned long long)((double)m / 60.0 / 24.0); m %= 60 * 24; if (m >= 60) h = (unsigned long long)((double)m / 60.0); m %= 60; printf("Drive uptime (total):\t%lld minutes (= %lldd %lldh %lldm)\n", uptime, d, h, m); printf("Next internal test in:\t%lld minutes\n", next); } if (scsi_get_selftest(dev, &stUptime, &stCode, &stResult) == 0) { printf("Self-test data (newest first):\n"); for (i = 0; i < 20; i++) { int u = stUptime[i]; int c = stCode[i]; int r = stResult[i]; if (u == -1 && c == -1 && r == -1) continue; printf("\t%2d: at: %dh\ttype: %-12s\tresult: %s\n", i, u, c != -1 ? scsi_selftest_code2str[c] : "?", r != -1 ? scsi_selftest_result2str[r] : "?"); } free(stUptime); free(stCode); free(stResult); } scsi_close(dev); } void usage(void) { fprintf(stderr, "Usage: scsimon [-t {s|e|a}] [-v] device\n"); fprintf(stderr, "Example: scsimon /dev/scsi/sc0d1l0\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, "\t-t type : perform a self-test ('short', 'extended' or 'abort')\n"); fprintf(stderr, "\t-v : display version and exit\n"); } int main(int argc, char *argv[]) { int c, doTest = 0; while ((c = getopt(argc, argv, "t:v")) != -1) switch (c) { case 't': if (!strcasecmp(optarg, "short") || !strcasecmp(optarg, "s")) { doTest = SCSI_DIAG_BG_SHORT; } else if (!strcasecmp(optarg, "extended") || !strcasecmp(optarg, "e")) { doTest = SCSI_DIAG_BG_EXT; } else if (!strcasecmp(optarg, "abort") || !strcasecmp(optarg, "a")) { doTest = SCSI_DIAG_ABORT; } else { fprintf(stderr, "ERROR: Unknown test type (supported: 'short', 'extended' and 'abort')."); return 1; } break; case 'v': printf("scsimon: Version 1.2\n"); return 0; default: usage(); return 1; } argc -= optind; argv += optind; if (argc < 1) { fprintf(stderr, "ERROR: Missing device name.\n\n"); usage(); return 1; } else if (argc > 1) { fprintf(stderr, "WARNING: Options after '%s' ignored.\n", argv[0]); } do_drive(argv[0], doTest); return 0; }