1/* 2 * hd-idle.c - external disk idle daemon 3 * 4 * Copyright (c) 2007 Christian Mueller. 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 21/* 22 * hd-idle is a utility program for spinning-down external disks after a period 23 * of idle time. Since most external IDE disk enclosures don't support setting 24 * the IDE idle timer, a program like hd-idle is required to spin down idle 25 * disks automatically. 26 * 27 * A word of caution: hard disks don't like spinning-up too often. Laptop disks 28 * are more robust in this respect than desktop disks but if you set your disks 29 * to spin down after a few seconds you may damage the disk over time due to the 30 * stress the spin-up causes on the spindle motor and bearings. It seems that 31 * manufacturers recommend a minimum idle time of 3-5 minutes, the default in 32 * hd-idle is 10 minutes. 33 * 34 * Please note that hd-idle can spin down any disk accessible via the SCSI 35 * layer (USB, IEEE1394, ...) but it will NOT work with real SCSI disks because 36 * they don't spin up automatically. Thus it's not called scsi-idle and I don't 37 * recommend using it on a real SCSI system unless you have a kernel patch that 38 * automatically starts the SCSI disks after receiving a sense buffer indicating 39 * the disk has been stopped. Without such a patch, real SCSI disks won't start 40 * again and you can as well pull the plug. 41 * 42 * You have been warned... 43 * 44 * CVS Change Log: 45 * --------------- 46 * 47 * $Log: hd-idle.c,v $ 48 * Revision 1.6 2010/12/05 19:25:51 cjmueller 49 * Version 1.03 50 * ------------ 51 * 52 * Bugs 53 * - Use %u in dprintf() when reporting number of reads and writes (the 54 * corresponding variable is an unsigned int). 55 * - Fix example in README where the parameter "-a" was written as "-n". 56 * 57 * Revision 1.5 2010/11/06 15:30:04 cjmueller 58 * Version 1.02 59 * ------------ 60 * 61 * Features 62 * - In case the SCSI stop unit command fails with "check condition", print a 63 * hex dump of the sense buffer to stderr. This is supposed to help 64 * debugging. 65 * 66 * Revision 1.4 2010/02/26 14:03:44 cjmueller 67 * Version 1.01 68 * ------------ 69 * 70 * Features 71 * - The parameter "-a" now also supports symlinks for disk names. Thus, disks 72 * can be specified using something like /dev/disk/by-uuid/... Use "-d" to 73 * verify that the resulting disk name is what you want. 74 * 75 * Please note that disk names are resolved to device nodes at startup. Also, 76 * since many entries in /dev/disk/by-xxx are actually partitions, partition 77 * numbers are automatically removed from the resulting device node. 78 * 79 * Bugs 80 * - Not really a bug, but the disk name comparison used strstr which is a bit 81 * useless because only disks starting with "sd" and a single letter after 82 * that are currently considered. Replaced the comparison with strcmp() 83 * 84 * Revision 1.3 2009/11/18 20:53:17 cjmueller 85 * Features 86 * - New parameter "-a" to allow selecting idle timeouts for individual disks; 87 * compatibility to previous releases is maintained by having an implicit 88 * default which matches all SCSI disks 89 * 90 * Bugs 91 * - Changed comparison operator for idle periods from '>' to '>=' to prevent 92 * adding one polling interval to idle time 93 * - Changed sleep time before calling sync after updating the log file to 1s 94 * (from 3s) to accumulate fewer dirty blocks before synching. It's still 95 * a compromize but the log file is for debugging purposes, anyway. A test 96 * with fsync() was unsuccessful because the next bdflush-initiated sync 97 * still caused spin-ups. 98 * 99 * Revision 1.2 2007/04/23 22:14:27 cjmueller 100 * Bug fixes 101 * - Comment changes; no functionality changes... 102 * 103 * Revision 1.1.1.1 2007/04/23 21:49:43 cjmueller 104 * initial import into CVS 105 * 106 */ 107 108#include <stdlib.h> 109#include <stdio.h> 110#include <string.h> 111#include <time.h> 112#include <ctype.h> 113#include <errno.h> 114#include <unistd.h> 115#include <stdarg.h> 116 117#include <fcntl.h> 118#include <sys/types.h> 119#include <sys/stat.h> 120#include <sys/ioctl.h> 121#include <scsi/sg.h> 122#include <scsi/scsi.h> 123 124#define STAT_FILE "/proc/diskstats" 125#define DEFAULT_IDLE_TIME 600 126 127#define dprintf if (debug) printf 128 129/* typedefs and structures */ 130typedef struct IDLE_TIME { 131 struct IDLE_TIME *next; 132 char *name; 133 int idle_time; 134} IDLE_TIME; 135 136typedef struct DISKSTATS { 137 struct DISKSTATS *next; 138 char name[50]; 139 int idle_time; 140 time_t last_io; 141 time_t spindown; 142 time_t spinup; 143 unsigned int spun_down : 1; 144 unsigned int reads; 145 unsigned int writes; 146} DISKSTATS; 147 148/* function prototypes */ 149static void daemonize (void); 150static DISKSTATS *get_diskstats (const char *name); 151static void spindown_disk (const char *name); 152static void log_spinup (DISKSTATS *ds); 153static char *disk_name (char *name); 154static void phex (const void *p, int len, 155 const char *fmt, ...); 156 157/* global/static variables */ 158IDLE_TIME *it_root; 159DISKSTATS *ds_root; 160char *logfile = "/dev/null"; 161int debug; 162 163/* main function */ 164int main(int argc, char *argv[]) 165{ 166 IDLE_TIME *it; 167 int have_logfile = 0; 168 int min_idle_time; 169 int sleep_time; 170 int opt; 171 char command[128]; /* Foxconn added pling 04/17/2014 */ 172 173 /* create default idle-time parameter entry */ 174 if ((it = malloc(sizeof(*it))) == NULL) { 175 fprintf(stderr, "out of memory\n"); 176 exit(1); 177 } 178 it->next = NULL; 179 it->name = NULL; 180 it->idle_time = DEFAULT_IDLE_TIME; 181 it_root = it; 182 183 /* process command line options */ 184 while ((opt = getopt(argc, argv, "t:a:i:l:dh")) != -1) { 185 switch (opt) { 186 187 case 't': 188 /* just spin-down the specified disk and exit */ 189 spindown_disk(optarg); 190 return(0); 191 192 case 'a': 193 /* add a new set of idle-time parameters for this particular disk */ 194 if ((it = malloc(sizeof(*it))) == NULL) { 195 fprintf(stderr, "out of memory\n"); 196 return(2); 197 } 198 it->name = disk_name(optarg); 199 it->idle_time = DEFAULT_IDLE_TIME; 200 it->next = it_root; 201 it_root = it; 202 break; 203 204 case 'i': 205 /* set idle-time parameters for current (or default) disk */ 206 it->idle_time = atoi(optarg); 207 break; 208 209 case 'l': 210 logfile = optarg; 211 have_logfile = 1; 212 break; 213 214 case 'd': 215 debug = 1; 216 break; 217 218 case 'h': 219 printf("usage: hd-idle [-t <disk>] [-a <name>] [-i <idle_time>] [-l <logfile>] [-d] [-h]\n"); 220 return(0); 221 222 case ':': 223 fprintf(stderr, "error: option -%c requires an argument\n", optopt); 224 return(1); 225 226 case '?': 227 fprintf(stderr, "error: unknown option -%c\n", optopt); 228 return(1); 229 } 230 } 231 232 /* set sleep time to 1/10th of the shortest idle time */ 233 min_idle_time = 1 << 30; 234 for (it = it_root; it != NULL; it = it->next) { 235 if (it->idle_time != 0 && it->idle_time < min_idle_time) { 236 min_idle_time = it->idle_time; 237 } 238 } 239 if ((sleep_time = min_idle_time / 10) == 0) { 240 sleep_time = 1; 241 } 242 243 /* daemonize unless we're running in debug mode */ 244 if (!debug) { 245 daemonize(); 246 } 247 248 /* main loop: probe for idle disks and stop them */ 249 for (;;) { 250 DISKSTATS tmp; 251 FILE *fp; 252 char buf[200]; 253 254 if ((fp = fopen(STAT_FILE, "r")) == NULL) { 255 perror(STAT_FILE); 256 return(2); 257 } 258 259 memset(&tmp, 0x00, sizeof(tmp)); 260 261 while (fgets(buf, sizeof(buf), fp) != NULL) { 262 if (sscanf(buf, "%*d %*d %s %*u %*u %u %*u %*u %*u %u %*u %*u %*u %*u", 263 tmp.name, &tmp.reads, &tmp.writes) == 3) { 264 DISKSTATS *ds; 265 time_t now = time(NULL); 266 267 /* make sure this is a SCSI disk (sd[a-z]) */ 268 if (tmp.name[0] != 's' || 269 tmp.name[1] != 'd' || 270 !isalpha(tmp.name[2]) || 271 tmp.name[3] != '\0') { 272 continue; 273 } 274 275 dprintf("probing %s: reads: %u, writes: %u\n", tmp.name, tmp.reads, tmp.writes); 276 277 /* get previous statistics for this disk */ 278 ds = get_diskstats(tmp.name); 279 280 if (ds == NULL) { 281 /* new disk; just add it to the linked list */ 282 if ((ds = malloc(sizeof(*ds))) == NULL) { 283 fprintf(stderr, "out of memory\n"); 284 return(2); 285 } 286 memcpy(ds, &tmp, sizeof(*ds)); 287 ds->last_io = now; 288 ds->spinup = ds->last_io; 289 ds->next = ds_root; 290 ds_root = ds; 291 292 /* find idle time for this disk (falling-back to default; default means 293 * 'it->name == NULL' and this entry will always be the last due to the 294 * way this single-linked list is built when parsing command line 295 * arguments) 296 */ 297 for (it = it_root; it != NULL; it = it->next) { 298 if (it->name == NULL || !strcmp(ds->name, it->name)) { 299 ds->idle_time = it->idle_time; 300 break; 301 } 302 } 303 304 } else if (ds->reads == tmp.reads && ds->writes == tmp.writes) { 305 if (!ds->spun_down) { 306 /* no activity on this disk and still running */ 307 if (ds->idle_time != 0 && now - ds->last_io >= ds->idle_time) { 308 spindown_disk(ds->name); 309 ds->spindown = now; 310 ds->spun_down = 1; 311 /* Foxconn added start pling 04/17/2014 */ 312 /* Add hdparm command to better hdd compatibility */ 313 sprintf(command, "hdparm -y /dev/%s", ds->name); 314 system(command); 315 /* Foxconn added end pling 04/17/2014 */ 316 } 317 } 318 319 } else { 320 /* disk had some activity */ 321 if (ds->spun_down) { 322 /* disk was spun down, thus it has just spun up */ 323 if (have_logfile) { 324 log_spinup(ds); 325 } 326 ds->spinup = now; 327 } 328 ds->reads = tmp.reads; 329 ds->writes = tmp.writes; 330 ds->last_io = now; 331 ds->spun_down = 0; 332 } 333 } 334 } 335 336 fclose(fp); 337 sleep(sleep_time); 338 } 339 340 return(0); 341} 342 343/* become a daemon */ 344static void daemonize(void) 345{ 346 int maxfd; 347 int i; 348 349 /* fork #1: exit parent process and continue in the background */ 350 if ((i = fork()) < 0) { 351 perror("couldn't fork"); 352 exit(2); 353 } else if (i > 0) { 354 _exit(0); 355 } 356 357 /* fork #2: detach from terminal and fork again so we can never regain 358 * access to the terminal */ 359 setsid(); 360 if ((i = fork()) < 0) { 361 perror("couldn't fork #2"); 362 exit(2); 363 } else if (i > 0) { 364 _exit(0); 365 } 366 367 /* change to root directory and close file descriptors */ 368 chdir("/"); 369 maxfd = getdtablesize(); 370 for (i = 0; i < maxfd; i++) { 371 close(i); 372 } 373 374 /* use /dev/null for stdin, stdout and stderr */ 375 open("/dev/null", O_RDONLY); 376 open("/dev/null", O_WRONLY); 377 open("/dev/null", O_WRONLY); 378} 379 380/* get DISKSTATS entry by name of disk */ 381static DISKSTATS *get_diskstats(const char *name) 382{ 383 DISKSTATS *ds; 384 385 for (ds = ds_root; ds != NULL; ds = ds->next) { 386 if (!strcmp(ds->name, name)) { 387 return(ds); 388 } 389 } 390 391 return(NULL); 392} 393 394/* spin-down a disk */ 395static void spindown_disk(const char *name) 396{ 397 struct sg_io_hdr io_hdr; 398 unsigned char sense_buf[255]; 399 char dev_name[100]; 400 int fd; 401 402 dprintf("spindown: %s\n", name); 403 404 /* fabricate SCSI IO request */ 405 memset(&io_hdr, 0x00, sizeof(io_hdr)); 406 io_hdr.interface_id = 'S'; 407 io_hdr.dxfer_direction = SG_DXFER_NONE; 408 409 /* SCSI stop unit command */ 410 io_hdr.cmdp = (unsigned char *) "\x1b\x00\x00\x00\x00\x00"; 411 412 io_hdr.cmd_len = 6; 413 io_hdr.sbp = sense_buf; 414 io_hdr.mx_sb_len = (unsigned char) sizeof(sense_buf); 415 416 /* open disk device (kernel 2.4 will probably need "sg" names here) */ 417 snprintf(dev_name, sizeof(dev_name), "/dev/%s", name); 418 if ((fd = open(dev_name, O_RDONLY)) < 0) { 419 perror(dev_name); 420 return; 421 } 422 423 /* execute SCSI request */ 424 if (ioctl(fd, SG_IO, &io_hdr) < 0) { 425 char buf[100]; 426 snprintf(buf, sizeof(buf), "ioctl on %s:", name); 427 perror(buf); 428 429 } else if (io_hdr.masked_status != 0) { 430 fprintf(stderr, "error: SCSI command failed with status 0x%02x\n", 431 io_hdr.masked_status); 432 if (io_hdr.masked_status == CHECK_CONDITION) { 433 phex(sense_buf, io_hdr.sb_len_wr, "sense buffer:\n"); 434 } 435 } 436 437 close(fd); 438} 439 440/* write a spin-up event message to the log file */ 441static void log_spinup(DISKSTATS *ds) 442{ 443 FILE *fp; 444 445 if ((fp = fopen(logfile, "a")) != NULL) { 446 /* Print statistics to logfile 447 * 448 * Note: This doesn't work too well if there are multiple disks 449 * because the I/O we're dealing with might be on another 450 * disk so we effectively wake up the disk the log file is 451 * stored on as well. Then again the logfile is a debugging 452 * option, so what... 453 */ 454 time_t now = time(NULL); 455 char tstr[20]; 456 char dstr[20]; 457 458 strftime(dstr, sizeof(dstr), "%Y-%m-%d", localtime(&now)); 459 strftime(tstr, sizeof(tstr), "%H:%M:%S", localtime(&now)); 460 fprintf(fp, 461 "date: %s, time: %s, disk: %s, running: %ld, stopped: %ld\n", 462 dstr, tstr, ds->name, 463 (long) ds->spindown - (long) ds->spinup, 464 (long) time(NULL) - (long) ds->spindown); 465 466 /* Sync to make sure writing to the logfile won't cause another 467 * spinup in 30 seconds (or whatever bdflush uses as flush interval). 468 */ 469 fclose(fp); 470 sleep(1); 471 sync(); 472 } 473} 474 475/* Resolve disk names specified as "/dev/disk/by-xxx" or some other symlink. 476 * Please note that this function is only called during command line parsing 477 * and hd-idle per se does not support dynamic disk additions or removals at 478 * runtime. 479 * 480 * This might change in the future but would require some fiddling to avoid 481 * needless overhead -- after all, this was designed to run on tiny embedded 482 * devices, too. 483 */ 484static char *disk_name(char *path) 485{ 486 ssize_t len; 487 char buf[256]; 488 char *s; 489 490 if (*path != '/') { 491 /* just a disk name without /dev prefix */ 492 return(path); 493 } 494 495 if ((len = readlink(path, buf, sizeof(buf) - 1)) <= 0) { 496 if (errno != EINVAL) { 497 /* couldn't resolve disk name */ 498 return(path); 499 } 500 501 /* 'path' is not a symlink */ 502 strncpy(buf, path, sizeof(buf) - 1); 503 buf[sizeof(buf)-1] = '\0'; 504 len = strlen(buf); 505 } 506 buf[len] = '\0'; 507 508 /* remove partition numbers, if any */ 509 for (s = buf + strlen(buf) - 1; s >= buf && isdigit(*s); s--) { 510 *s = '\0'; 511 } 512 513 /* Extract basename of the disk in /dev. Note that this assumes that the 514 * final target of the symlink (if any) resolves to /dev/sd* 515 */ 516 if ((s = strrchr(buf, '/')) != NULL) { 517 s++; 518 } else { 519 s = buf; 520 } 521 522 if ((s = strdup(s)) == NULL) { 523 fprintf(stderr, "out of memory"); 524 exit(2); 525 } 526 527 if (debug) { 528 printf("using %s for %s\n", s, path); 529 } 530 return(s); 531} 532 533/* print hex dump to stderr (e.g. sense buffers) */ 534static void phex(const void *p, int len, const char *fmt, ...) 535{ 536 va_list va; 537 const unsigned char *buf = p; 538 int pos = 0; 539 int i; 540 541 /* print header */ 542 va_start(va, fmt); 543 vfprintf(stderr, fmt, va); 544 545 /* print hex block */ 546 while (len > 0) { 547 fprintf(stderr, "%08x ", pos); 548 549 /* print hex block */ 550 for (i = 0; i < 16; i++) { 551 if (i < len) { 552 fprintf(stderr, "%c%02x", ((i == 8) ? '-' : ' '), buf[i]); 553 } else { 554 fprintf(stderr, " "); 555 } 556 } 557 558 /* print ASCII block */ 559 fprintf(stderr, " "); 560 for (i = 0; i < ((len > 16) ? 16 : len); i++) { 561 fprintf(stderr, "%c", (buf[i] >= 32 && buf[i] < 128) ? buf[i] : '.'); 562 } 563 fprintf(stderr, "\n"); 564 565 pos += 16; 566 buf += 16; 567 len -= 16; 568 } 569} 570 571