/* * hd-idle.c - external disk idle daemon * * Copyright (c) 2007 Christian Mueller. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * hd-idle is a utility program for spinning-down external disks after a period * of idle time. Since most external IDE disk enclosures don't support setting * the IDE idle timer, a program like hd-idle is required to spin down idle * disks automatically. * * A word of caution: hard disks don't like spinning-up too often. Laptop disks * are more robust in this respect than desktop disks but if you set your disks * to spin down after a few seconds you may damage the disk over time due to the * stress the spin-up causes on the spindle motor and bearings. It seems that * manufacturers recommend a minimum idle time of 3-5 minutes, the default in * hd-idle is 10 minutes. * * Please note that hd-idle can spin down any disk accessible via the SCSI * layer (USB, IEEE1394, ...) but it will NOT work with real SCSI disks because * they don't spin up automatically. Thus it's not called scsi-idle and I don't * recommend using it on a real SCSI system unless you have a kernel patch that * automatically starts the SCSI disks after receiving a sense buffer indicating * the disk has been stopped. Without such a patch, real SCSI disks won't start * again and you can as well pull the plug. * * You have been warned... * * CVS Change Log: * --------------- * * $Log: hd-idle.c,v $ * Revision 1.2 2007/04/23 22:14:27 cjmueller * Bug fixes * - Comment changes; no functionality changes... * * Revision 1.1.1.1 2007/04/23 21:49:43 cjmueller * initial import into CVS * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #define STAT_FILE "/proc/diskstats" #define dprintf if (debug) printf #if 1 /* AMV/WK Patch 20090520: Skip removable devices */ #define SYSFS_PATH "/sys/" #endif /* typedefs and structures */ typedef struct DISKSTATS { struct DISKSTATS *next; char name[50]; time_t last_io; time_t spindown; time_t spinup; unsigned int spun_down : 1; unsigned int reads; unsigned int writes; } DISKSTATS; /* function prototypes */ static void daemonize (void); static DISKSTATS *get_diskstats (const char *name); static void spindown_disk (const char *name); static void log_spinup (DISKSTATS *ds); /* global/static variables */ DISKSTATS *ds_root; const char *logfile = "/dev/null"; int debug; /* main function */ int main(int argc, char *argv[]) { int have_logfile = 0; int idle_time = 600; int sleep_time; int opt; if(setpriority(PRIO_PROCESS, 0, 19) < 0 ) { fprintf(stderr, "%s: setpriority() failed\n", argv[0]); } /* process command line options */ while ((opt = getopt(argc, argv, "t:i:l:dh")) != -1) { switch (opt) { case 't': /* just spin-down the specified disk and exit */ spindown_disk(optarg); return(0); case 'i': idle_time = atoi(optarg); break; case 'l': logfile = optarg; have_logfile = 1; break; case 'd': debug = 1; break; case 'h': printf("usage: hd-idle [-t ] [-i ] [-l ] [-d] [-h]\n"); return(0); case ':': fprintf(stderr, "error: option -%c requires an argument\n", optopt); return(1); case '?': fprintf(stderr, "error: unknown option -%c\n", optopt); return(1); } } /* set sleep interval */ if ((sleep_time = idle_time / 10) == 0) { sleep_time = 1; } /* daemonize unless we're running in debug mode */ if (!debug) { daemonize(); } /* main loop: probe for idle disks and stop them */ for (;;) { DISKSTATS tmp; FILE *fp; char buf[200]; if ((fp = fopen(STAT_FILE, "r")) == NULL) { perror(STAT_FILE); return(2); } memset(&tmp, 0x00, sizeof(tmp)); while (fgets(buf, sizeof(buf), fp) != NULL) { /* 49 = sizeof(tmp.name) - 1 */ if (sscanf(buf, "%*d %*d %49s %*u %*u %u %*u %*u %*u %u %*u %*u %*u %*u", tmp.name, &tmp.reads, &tmp.writes) == 3) { DISKSTATS *ds; time_t now = time(NULL); /* make sure this is a SCSI disk (sd[a-z]) */ if (tmp.name[0] != 's' || tmp.name[1] != 'd' || !isalpha(tmp.name[2]) || tmp.name[3] != '\0') { continue; } /* get previous statistics for this disk */ ds = get_diskstats(tmp.name); if (ds == NULL) { /* new disk; just add it to the linked list */ ds = malloc(sizeof(*ds)); memcpy(ds, &tmp, sizeof(*ds)); ds->last_io = time(NULL); ds->spinup = ds->last_io; ds->next = ds_root; ds_root = ds; } else if (ds->reads == tmp.reads && ds->writes == tmp.writes) { if (!ds->spun_down) { /* no activity on this disk and still running */ if (now - ds->last_io > idle_time) { spindown_disk(ds->name); ds->spindown = time(NULL); ds->spun_down = 1; } } } else { /* disk had some activity */ if (ds->spun_down) { /* disk was spun down, thus it has just spun up */ if (have_logfile) { log_spinup(ds); } ds->spinup = time(NULL); } ds->reads = tmp.reads; ds->writes = tmp.writes; ds->last_io = time(NULL); ds->spun_down = 0; } } } fclose(fp); sleep(sleep_time); } return(0); } /* become a daemon */ static void daemonize(void) { int maxfd; int i; /* fork #1: exit parent process and continue in the background */ if ((i = fork()) < 0) { perror("couldn't fork"); exit(2); } else if (i > 0) { _exit(0); } /* fork #2: detach from terminal and fork again so we can never regain * access to the terminal */ setsid(); if ((i = fork()) < 0) { perror("couldn't fork #2"); exit(2); } else if (i > 0) { _exit(0); } /* change to root directory and close file descriptors */ chdir("/"); maxfd = getdtablesize(); for (i = 0; i < maxfd; i++) { /* close(i); */ } /* use /dev/null for stdin, stdout and stderr */ open("/dev/null", O_RDONLY); open("/dev/null", O_WRONLY); open("/dev/null", O_WRONLY); } /* get DISKSTATS entry by name of disk */ static DISKSTATS *get_diskstats(const char *name) { DISKSTATS *ds; for (ds = ds_root; ds != NULL; ds = ds->next) { if (!strcmp(ds->name, name)) { return(ds); } } return(NULL); } /* spin-down a disk */ static void spindown_disk(const char *name) { struct sg_io_hdr io_hdr; unsigned char sense_buf[255]; char dev_name[100]; int fd; #if 1 /* AVM/WK Patch 20090520: Skip removable devices */ { FILE *rfp; char sRemovableFile[200]; char cremovable = '1'; snprintf(sRemovableFile, sizeof(sRemovableFile), SYSFS_PATH "/block/%s/removable", name); if ((rfp = fopen(sRemovableFile, "r")) == NULL) { perror(sRemovableFile); fprintf(stderr, "removable??? : skipping disk %s in hd-idle spindown!\n", name); return; } cremovable = fgetc(rfp); fclose(rfp); if (cremovable != '0') { fprintf(stderr, "hd-idle: skipping removable disk %s\n", name); return; } } #endif #if 1 /* AVM/VGJ Patch 20171016: Skip suspended devices */ { FILE *rfp; char sSuspendedFile[200]; char csuspended = 0; snprintf(sSuspendedFile, sizeof(sSuspendedFile), SYSFS_PATH "/block/%s/device/../../../../avm_usb_suspend_status", name); rfp = fopen(sSuspendedFile, "r"); if (rfp == NULL) { snprintf(sSuspendedFile, sizeof(sSuspendedFile), SYSFS_PATH "/block/%s/device/../../../../power/runtime_status", name); rfp = fopen(sSuspendedFile, "r"); } if (rfp != NULL) { csuspended = fgetc(rfp); fclose(rfp); if (csuspended == 's') { fprintf(stderr, "hd-idle: skipping suspended disk %s\n", name); return; } } } #endif /* fabricate SCSI IO request */ memset(&io_hdr, 0x00, sizeof(io_hdr)); io_hdr.interface_id = 'S'; io_hdr.dxfer_direction = SG_DXFER_NONE; /* SCSI stop unit command */ io_hdr.cmdp = (unsigned char *) "\x1b\x00\x00\x00\x00\x00"; io_hdr.cmd_len = 6; io_hdr.sbp = sense_buf; io_hdr.mx_sb_len = (unsigned char) sizeof(sense_buf); /* open disk device (kernel 2.4 will probably need "sg" names here) */ snprintf(dev_name, sizeof(dev_name), "/dev/%s", name); if ((fd = open(dev_name, O_RDONLY)) < 0) { perror(dev_name); return; } /* execute SCSI request */ if (ioctl(fd, SG_IO, &io_hdr) < 0) { char buf[100]; snprintf(buf, sizeof(buf), "ioctl on %s:", name); perror(buf); } else if (io_hdr.masked_status != 0) { fprintf(stderr, "error: SCSI command failed with status 0x%02x\n", io_hdr.masked_status); } close(fd); } /* write a spin-up event message to the log file */ static void log_spinup(DISKSTATS *ds) { FILE *fp; if ((fp = fopen(logfile, "a")) != NULL) { /* Print statistics to logfile * * Note: This doesn't work too well if there are multiple disks * because the I/O we're dealing with might be on another * disk so we effectively wake up the disk the log file is * stored on as well. Then again the logfile is a debugging * option, so what... */ time_t now = time(NULL); char tstr[20]; char dstr[20]; strftime(dstr, sizeof(dstr), "%Y-%m-%d", localtime(&now)); strftime(tstr, sizeof(tstr), "%H:%M:%S", localtime(&now)); fprintf(fp, "date: %s, time: %s, disk: %s, running: %ld, stopped: %ld\n", dstr, tstr, ds->name, (long) ds->spindown - (long) ds->spinup, (long) time(NULL) - (long) ds->spindown); /* Sync to make sure writing to the logfile won't cause another * spinup in 30 seconds (or whatever bdflush uses as flush interval). * Since there's already some I/O which caused the spin-up and we don't * want to cause even more I/O, wait some time before doing this. */ fclose(fp); sleep(3); sync(); } }