hd-idle/main.go
2025-08-06 15:51:53 +02:00

246 lines
5.6 KiB
Go

// 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 <http://www.gnu.org/licenses/>.
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 <disk>] [-s <symlink_policy>] [-a <name>] [-i <idle_time>] " +
"[-c <command_type>] [-p power_condition] [-l <logfile>] [-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
}