// hd-idle - spin down idle hard disks // Copyright (C) 2018 Andoni del Olmo // // 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 3 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, see . package main import ( "fmt" "github.com/adelolmo/hd-idle/io" "os" "strconv" "time" ) const ( defaultIdleTime = 600 * time.Second symlinkResolveOnce = 0 symlinkResolveRetry = 1 ) func main() { if os.Getenv("START_HD_IDLE") == "false" { fmt.Println("START_HD_IDLE=false exiting now.") os.Exit(0) } singleDiskMode := false var disk string defaultConf := DefaultConf{ Idle: defaultIdleTime, CommandType: SCSI, PowerCondition: 0, Debug: false, SymlinkPolicy: 0, } var config = &Config{ Devices: []DeviceConf{}, Defaults: defaultConf, NameMap: map[string]string{}, } var deviceConf *DeviceConf if len(os.Args) == 0 { usage() os.Exit(1) } for index, arg := range os.Args[1:] { switch arg { case "-t": var err error disk, err = argument(index) if err != nil { fmt.Println("Missing disk argument after -t. Must be a device (e.g. -t sda).") os.Exit(1) } singleDiskMode = true case "-s": s, err := argument(index) if err != nil { fmt.Println("Missing symlink_policy. Must be 0 or 1.") os.Exit(1) } switch s { case "0": config.Defaults.SymlinkPolicy = symlinkResolveOnce case "1": config.Defaults.SymlinkPolicy = symlinkResolveRetry default: fmt.Printf("Wrong symlink_policy -s %s. Must be 0 or 1.\n", s) os.Exit(1) } case "-a": if deviceConf != nil { config.Devices = append(config.Devices, *deviceConf) } name, err := argument(index) if err != nil { fmt.Println("Missing disk argument after -a. Must be a device (e.g. -a sda).") os.Exit(1) } deviceRealPath, err := io.RealPath(name) if err != nil { deviceRealPath = "" fmt.Printf("Unable to resolve symlink: %s\n", name) } deviceConf = &DeviceConf{ Name: deviceRealPath, GivenName: name, Idle: config.Defaults.Idle, CommandType: config.Defaults.CommandType, PowerCondition: config.Defaults.PowerCondition, } config.NameMap[deviceRealPath] = name case "-i": s, err := argument(index) if err != nil { fmt.Println("Missing idle_time after -i. Must be a number.") os.Exit(1) } idle, err := strconv.Atoi(s) if err != nil { fmt.Printf("Wrong idle_time -i %d. Must be a number.", idle) os.Exit(1) } if deviceConf == nil { config.Defaults.Idle = time.Duration(idle) * time.Second break } deviceConf.Idle = time.Duration(idle) * time.Second case "-c": command, err := argument(index) if err != nil { fmt.Println("Missing command_type after -c. Must be one of: scsi, ata.") os.Exit(1) } switch command { case SCSI, ATA: if deviceConf == nil { config.Defaults.CommandType = command break } deviceConf.CommandType = command default: fmt.Printf("Wrong command_type -c %s. Must be one of: scsi, ata.", command) os.Exit(1) } case "-p": s, err := argument(index) if err != nil { fmt.Println("Missing power condition after -p. Must be a number from 0-15.") os.Exit(1) } powerCondition, err := strconv.ParseUint(s, 0, 4) if err != nil { fmt.Printf("Invalid power condition %s: %s", s, err.Error()) os.Exit(1) } if deviceConf == nil { config.Defaults.PowerCondition = uint8(powerCondition) break } deviceConf.PowerCondition = uint8(powerCondition) case "-l": logfile, err := argument(index) if err != nil { fmt.Println("Missing logfile after -l.") os.Exit(1) } config.Defaults.LogFile = logfile case "-d": config.Defaults.Debug = true case "-h": usage() os.Exit(0) } } if singleDiskMode { if err := spindownDisk( disk, config.Defaults.CommandType, config.Defaults.PowerCondition, config.Defaults.Debug, ); err != nil { fmt.Println(err.Error()) os.Exit(1) } os.Exit(0) } if deviceConf != nil { config.Devices = append(config.Devices, *deviceConf) } fmt.Println(config.String()) interval := poolInterval(config.Devices) config.SkewTime = interval * 3 for { ObserveDiskActivity(config) time.Sleep(interval) } } func argument(index int) (string, error) { argIndex := index + 2 if argIndex >= len(os.Args) { return "", fmt.Errorf("option requires argument") } arg := os.Args[argIndex] if arg[:1] == "-" { return "", fmt.Errorf("option requires argument") } return arg, nil } func usage() { fmt.Println("usage: hd-idle [-t ] [-s ] [-a ] [-i ] " + "[-c ] [-p power_condition] [-l ] [-d] [-h]") } func poolInterval(deviceConfs []DeviceConf) time.Duration { if len(deviceConfs) == 0 { return defaultIdleTime / 10 } interval := defaultIdleTime for _, dev := range deviceConfs { if dev.Idle == 0 { continue } if dev.Idle < interval { interval = dev.Idle } } sleepTime := interval / 10 if sleepTime == 0 { return time.Second } return sleepTime }