New upstream version 1.21

This commit is contained in:
root
2025-08-06 15:37:55 +02:00
parent 0343893f09
commit 0ef82355c6
40 changed files with 3389 additions and 1136 deletions

132
sgio/ata.go Normal file
View File

@@ -0,0 +1,132 @@
// 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 sgio
import (
"fmt"
"github.com/benmcclelland/sgio"
"os"
)
const (
sgAta16 = 0x85 // ATA PASS-THROUGH(16)
sgAta12 = 0xa1 // ATA PASS-THROUGH (12)
sgAtaProtoNonData = 3 << 1
ataUsingLba = 1 << 6
ataOpStandbyNow1 = 0xe0 // https://wiki.osdev.org/ATA/ATAPI_Power_Management
ataOpStandbyNow2 = 0x94 // Retired in ATA4. Did not coexist with ATAPI.
)
func StopAtaDevice(device string, debug bool) error {
f, err := openDevice(device)
if err != nil {
return err
}
switch NewAtaDevice(device, debug).deviceType() {
case Jmicron:
if err = sendSgio(f, jmicronGetRegisters(), debug); err != nil {
return err
}
if debug {
fmt.Println(" issuing standby command")
}
if err = sendSgio(f, jmicronStandby(), debug); err != nil {
return err
}
return nil
default:
if debug {
fmt.Println(" issuing standby command")
}
if err = sendAtaCommand(f, ataOpStandbyNow1, debug); err != nil {
if err = sendAtaCommand(f, ataOpStandbyNow2, debug); err != nil {
return err
}
}
}
if err := f.Close(); err != nil {
return fmt.Errorf("cannot close file %s. Error: %s", device, err)
}
return nil
}
func jmicronGetRegisters() []uint8 {
cbd := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // len 12
cbd[0] = 0xdf
cbd[1] = 0x10 // read
cbd[4] = 0x01
cbd[6] = 0x72
cbd[7] = 0x0f
cbd[11] = 0xfd
return cbd
}
func jmicronStandby() []uint8 {
cbd := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // len 12
cbd[0] = 0xdf
cbd[1] = 0x10
cbd[10] = 0xa0 // device port. either 0xa0 or 0xb0
cbd[11] = ataOpStandbyNow1
return cbd
}
func sendAtaCommand(f *os.File, command uint8, debug bool) error {
cbd := []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} // len 16
cbd[0] = sgAta16
cbd[1] = sgAtaProtoNonData
cbd[13] = ataUsingLba
cbd[14] = command
return sendSgio(f, cbd, debug)
}
func sendSgio(f *os.File, inqCmdBlk []uint8, debug bool) error {
senseBuf := make([]byte, sgio.SENSE_BUF_LEN)
ioHdr := &sgio.SgIoHdr{
InterfaceID: 'S', // 0 4
DxferDirection: SgDxferNone, // 4 4
CmdLen: uint8(len(inqCmdBlk)), // 8 1
MxSbLen: sgio.SENSE_BUF_LEN, // 9 1
Cmdp: &inqCmdBlk[0], // 24 8
Sbp: &senseBuf[0], // 32 8
Timeout: 0, // 40 4
}
if debug {
dumpBytes(inqCmdBlk)
}
if err := sgio.SgioSyscall(f, ioHdr); err != nil {
return err
}
if err := sgio.CheckSense(ioHdr, &senseBuf); err != nil {
return err
}
return nil
}
func dumpBytes(p []uint8) {
fmt.Print("outgoing cdb: ")
for i := range p {
fmt.Printf("%02x ", p[i])
}
fmt.Print("\n")
}

47
sgio/common.go Normal file
View File

@@ -0,0 +1,47 @@
// 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 sgio
import (
"fmt"
"github.com/benmcclelland/sgio"
"os"
"syscall"
"unsafe"
)
const SgDxferNone = -1
func openDevice(fname string) (*os.File, error) {
f, err := os.OpenFile(fname, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
var version uint32
if (ioctl(f.Fd(), sgio.SG_GET_VERSION_NUM, uintptr(unsafe.Pointer(&version))) != nil) || (version < 30000) {
return nil, fmt.Errorf("device does not appear to be an sg device")
}
return f, nil
}
func ioctl(fd, cmd, ptr uintptr) error {
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if err != 0 {
return err
}
return nil
}

63
sgio/scsi.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 sgio
import (
"fmt"
"github.com/benmcclelland/sgio"
)
// https://en.wikipedia.org/wiki/SCSI_command
const startStopUnit = 0x1b
func StartStopScsiDevice(device string, powerCondition uint8) error {
f, err := openDevice(device)
if err != nil {
return err
}
senseBuf := make([]byte, sgio.SENSE_BUF_LEN)
//See https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf - 3.49 START STOP UNIT command
inqCmdBlk := []uint8{
startStopUnit,
0, //Reserved (7 bit) + IMMED
0, //Reserved (8 bit)
0, //Reserved (4 bit) + POWER CONDITION MODIFER
powerCondition << 4, //POWER CONDITION + Reserved (1 bit) + NO_ FLUSH + LOEJ + LOEJ
0} //CONTROL
ioHdr := &sgio.SgIoHdr{
InterfaceID: 'S',
DxferDirection: SgDxferNone,
Cmdp: &inqCmdBlk[0],
CmdLen: uint8(len(inqCmdBlk)),
Sbp: &senseBuf[0],
MxSbLen: sgio.SENSE_BUF_LEN,
}
if err := sgio.SgioSyscall(f, ioHdr); err != nil {
return err
}
if err := sgio.CheckSense(ioHdr, &senseBuf); err != nil {
return err
}
if err := f.Close(); err != nil {
return fmt.Errorf("cannot close file %s. Error: %s", device, err)
}
return nil
}

127
sgio/type.go Normal file
View File

@@ -0,0 +1,127 @@
// hd-idle - spin down idle hard disks
// Copyright (C) 2023 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 sgio
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const (
Jmicron = iota
Unknown = iota
sysblock = "/sys/block"
jmicron = "152d"
)
type AtaDevice struct {
device string
debug bool
fsRoot string
}
func NewAtaDevice(device string, debug bool) AtaDevice {
return AtaDevice{
device: device,
debug: debug,
fsRoot: sysblock,
}
}
type apt struct {
idVendor, idProduct, bcdDevice string
}
func (a apt) isJmicron() bool {
if a.idVendor != jmicron {
return false
}
switch a.idProduct {
case "2329", "2336", "2338", "2339":
return true
}
return false
}
func (ad AtaDevice) deviceType() int {
a, err := ad.identifyDevice(ad.device)
if err != nil {
if ad.debug {
fmt.Println("APT: Unsupported device")
}
return Unknown
}
if a.isJmicron() {
if ad.debug {
fmt.Println("APT: Found supported device jmicron")
}
return Jmicron
}
if ad.debug {
fmt.Println("APT: Unsupported device")
}
return Unknown
}
func (ad AtaDevice) identifyDevice(device string) (apt, error) {
diskname := strings.Split(device, "/")[2]
sysblockdisk := filepath.Join(ad.fsRoot, diskname)
idVendor, err := findSystemFile(sysblockdisk, "idVendor")
if err != nil {
return apt{}, err
}
idProduct, err := findSystemFile(sysblockdisk, "idProduct")
if err != nil {
return apt{}, err
}
bcdDevice, err := findSystemFile(sysblockdisk, "bcdDevice")
if err != nil {
return apt{}, err
}
if ad.debug {
fmt.Printf("APT: USB ID = 0x%s:0x%s (0x%3s)\n", idVendor, idProduct, bcdDevice)
}
return apt{
idVendor: idVendor,
idProduct: idProduct,
bcdDevice: bcdDevice,
},
nil
}
func findSystemFile(systemRoot, filename string) (string, error) {
_, err := os.ReadFile(filepath.Join(systemRoot, filename))
relativeDir := ""
var content []byte
depth := 0
for depth < 20 {
if err == nil {
return strings.TrimSpace(string(content)), nil
}
relativeDir += "/.."
content, err = os.ReadFile(systemRoot + relativeDir + "/" + filename)
depth++
}
return "", fmt.Errorf("device not found")
}

80
sgio/type_test.go Normal file
View File

@@ -0,0 +1,80 @@
package sgio
import (
"log"
"os"
"path/filepath"
"strings"
"testing"
)
const (
tmpDir = "/tmp/hd-idle/ata"
)
func TestAtaDevice_deviceType(t *testing.T) {
type fields struct {
device string
debug bool
fsRoot string
idVendor, idProduct, bcdDevice string
}
tests := []struct {
name string
fields fields
want int
}{
{
name: "find jmicron controller",
fields: fields{
device: "/dev/sde",
debug: true,
fsRoot: filepath.Join(tmpDir, "sys", "block"),
idVendor: "152d",
idProduct: "2339",
bcdDevice: "100",
},
want: Jmicron,
},
{
name: "unknown device",
fields: fields{
device: "/dev/sde",
debug: true,
fsRoot: filepath.Join(tmpDir, "sys", "block"),
idVendor: "1058",
idProduct: "25a3",
bcdDevice: "1021",
},
want: Unknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ad := AtaDevice{
device: tt.fields.device,
debug: tt.fields.debug,
fsRoot: tt.fields.fsRoot,
}
err := os.RemoveAll(tmpDir)
if err != nil {
log.Fatal(err)
}
infoDir := filepath.Join(tmpDir, "/sys/devices/pci0000:00/0000:00:15.0/usb2/2-2/2-2.3/2-2.3.2")
diskname := strings.Split(tt.fields.device, "/")[2]
deviceRoot := infoDir + "/2-2.3.2:1.0/host5/target5:0:0/5:0:0:0/block/" + diskname
_ = os.MkdirAll(deviceRoot, 0755)
_ = os.WriteFile(filepath.Join(infoDir, "idVendor"), []byte(tt.fields.idVendor), 0666)
_ = os.WriteFile(filepath.Join(infoDir, "idProduct"), []byte(tt.fields.idProduct), 0666)
_ = os.WriteFile(filepath.Join(infoDir, "bcdDevice"), []byte(tt.fields.bcdDevice), 0666)
_ = os.MkdirAll(filepath.Join(tmpDir, "/sys/block"), 0755)
if err = os.Symlink(deviceRoot, filepath.Join(tmpDir, "/sys/block", diskname)); err != nil {
log.Fatal(err)
}
if got := ad.deviceType(); got != tt.want {
t.Errorf("deviceType() = %v, want %v", got, tt.want)
}
})
}
}