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