diff --git a/cmd/acpi-wakeup-fixxer/main.go b/cmd/acpi-wakeup-fixxer/main.go new file mode 100644 index 0000000..331b646 --- /dev/null +++ b/cmd/acpi-wakeup-fixxer/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + "github.com/spf13/cobra" + + "code.tokarch.uk/mainnika/acpi-wakeup-fixxer/pkg/wakeup" +) + +var ( + devicesToDisableFlag []string +) + +var rootCmd = &cobra.Command{ + Use: "acpi-wakeup-fixxer", + Short: "A CLI tool to fix ACPI wakeup issues", + Args: cobra.NoArgs, + Long: "acpi-wakeup-fixxer is a command-line tool to fix ACPI wakeup issues on your system.", + Run: rootCmdRun, +} + +func init() { + rootCmd.PersistentFlags().StringArrayVar(&devicesToDisableFlag, "disable", []string{"LID0", "XHC1"}, "Devices to disable") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func rootCmdRun(cmd *cobra.Command, args []string) { + wakeUpController := wakeup.NewWakeupController() + + devices, err := wakeUpController.GetWakeupDevices(wakeup.StatusEnabled) + if err != nil { + slog.Error("failed to get wakeup devices", "error", err) + os.Exit(1) + } + + slog.Info("Found devices", "devices", devices) + slog.Info("Disabling devices", "devices", devicesToDisableFlag) + + var disabledDevices []string + for _, device := range devices { + needDisable := false + for _, deviceToDisable := range devicesToDisableFlag { + if device == deviceToDisable { + needDisable = true + break + } + } + + if needDisable { + slog.Info("Disabling device", "device", device) + if err := wakeUpController.ToggleWakeupDevice(device); err != nil { + slog.Error("failed to disable device", "device", device, "error", err) + continue + } + + disabledDevices = append(disabledDevices, device) + } + } + + if len(disabledDevices) > 0 { + slog.Info("Disabled devices", "devices", disabledDevices) + } else { + slog.Info("No devices were disabled") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5abddfe --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module code.tokarch.uk/mainnika/acpi-wakeup-fixxer + +go 1.22.2 + +require ( + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..50cc2e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/procfs/procfs.go b/pkg/procfs/procfs.go new file mode 100644 index 0000000..8ec8b5d --- /dev/null +++ b/pkg/procfs/procfs.go @@ -0,0 +1,36 @@ +package procfs + +import ( + "fmt" + "io" + "os" +) + +const ( + acpiWakeupDefaultPath = "/proc/acpi/wakeup" +) + +type Procfs interface { + ACPIWakeup() (io.ReadCloser, error) + ACPIWakeupWrite() (io.WriteCloser, error) +} + +type ProcfsDefaultPath struct{} + +func (p *ProcfsDefaultPath) ACPIWakeup() (io.ReadCloser, error) { + f, err := os.Open(acpiWakeupDefaultPath) + if err != nil { + return nil, fmt.Errorf("failed to open %s: %w", acpiWakeupDefaultPath, err) + } + + return f, nil +} + +func (p *ProcfsDefaultPath) ACPIWakeupWrite() (io.WriteCloser, error) { + f, err := os.OpenFile(acpiWakeupDefaultPath, os.O_WRONLY, 0) + if err != nil { + return nil, fmt.Errorf("failed to open for writing %s: %w", acpiWakeupDefaultPath, err) + } + + return f, nil +} diff --git a/pkg/wakeup/wakeup.go b/pkg/wakeup/wakeup.go new file mode 100644 index 0000000..c8f5e4b --- /dev/null +++ b/pkg/wakeup/wakeup.go @@ -0,0 +1,88 @@ +package wakeup + +import ( + "bufio" + "fmt" + "log/slog" + "strings" + + "code.tokarch.uk/mainnika/acpi-wakeup-fixxer/pkg/procfs" +) + +const ( + deviceHeader = "Device" + columnDeviceIndex = 0 + columnStatusIndex = 2 + columnIndexMax = 3 +) + +type Status string + +const StatusAll Status = "" +const StatusEnabled Status = "*enabled" +const StatusDisabled Status = "*disabled" + +type WakeupController struct { + ProcfsProvider procfs.Procfs +} + +func NewWakeupController() *WakeupController { + return &WakeupController{ProcfsProvider: &procfs.ProcfsDefaultPath{}} +} + +func (w *WakeupController) GetWakeupDevices(withStatus Status) ([]string, error) { + wakeupFile, err := w.ProcfsProvider.ACPIWakeup() + if err != nil { + return nil, fmt.Errorf("failed to get wakeup file: %w", err) + } + defer func() { _ = wakeupFile.Close() }() + + scanner := bufio.NewScanner(wakeupFile) + var devices []string + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < columnIndexMax { + slog.Warn("unexpected number of columns", "line", line) + continue + } + + device := fields[columnDeviceIndex] + if device == deviceHeader { + continue + } + + status := Status(fields[columnStatusIndex]) + if withStatus != StatusAll && status != withStatus { + continue + } + + devices = append(devices, device) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to scan wakeup file: %w", err) + } + + return devices, nil +} + +func (w *WakeupController) ToggleWakeupDevice(device string) error { + wakeupFile, err := w.ProcfsProvider.ACPIWakeupWrite() + if err != nil { + return fmt.Errorf("failed to open wakeup file for writing: %w", err) + } + + _, err = fmt.Fprintf(wakeupFile, "%s\n", device) + if err != nil { + _ = wakeupFile.Close() + return fmt.Errorf("failed to write to wakeup file: %w", err) + } + + err = wakeupFile.Close() + if err != nil { + return fmt.Errorf("failed to close wakeup file: %w", err) + } + + return nil +} diff --git a/pkg/wakeup/wakeup_test.go b/pkg/wakeup/wakeup_test.go new file mode 100644 index 0000000..035468c --- /dev/null +++ b/pkg/wakeup/wakeup_test.go @@ -0,0 +1,105 @@ +package wakeup + +import ( + "fmt" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type procfsMock struct{} + +func (p *procfsMock) ACPIWakeup() (io.ReadCloser, error) { + const testFileRaw = `Device S-state Status Sysfs node +PEG0 S3 *disabled +EC S4 *disabled platform:PNP0C09:00 +HDEF S3 *disabled pci:0000:00:1b.0 +RP01 S3 *enabled pci:0000:00:1c.0 +RP02 S3 *enabled pci:0000:00:1c.1 +RP03 S3 *enabled pci:0000:00:1c.2 +ARPT S4 *enabled pci:0000:03:00.0 +RP05 S3 *enabled pci:0000:00:1c.4 +RP06 S3 *enabled pci:0000:00:1c.5 +SPIT S3 *disabled spi:spi-APP000D:00 +XHC1 S3 *disabled pci:0000:00:14.0 +ADP1 S4 *disabled platform:ACPI0003:00 +LID0 S4 *disabled platform:PNP0C0D:00 +` + + return io.NopCloser(strings.NewReader(testFileRaw)), nil +} + +func (p *procfsMock) ACPIWakeupWrite() (io.WriteCloser, error) { + return nil, fmt.Errorf("not implemented") +} + +func TestSomething(t *testing.T) { + + tt := []struct { + name string + wantStatus Status + wantDevices []string + }{ + { + name: "Read and parse successfully", + wantStatus: StatusEnabled, + wantDevices: []string{ + "RP01", + "RP02", + "RP03", + "RP05", + "RP06", + "ARPT", + }, + }, + { + name: "Read and parse disabled successfully", + wantStatus: StatusDisabled, + wantDevices: []string{ + "PEG0", + "EC", + "HDEF", + "SPIT", + "XHC1", + "ADP1", + "LID0", + }, + }, + { + name: "Read and parse all successfully", + wantStatus: StatusAll, + wantDevices: []string{ + "RP01", + "RP02", + "RP03", + "RP05", + "RP06", + "ARPT", + "PEG0", + "EC", + "HDEF", + "SPIT", + "XHC1", + "ADP1", + "LID0", + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + c := NewWakeupController() + c.ProcfsProvider = &procfsMock{} + + devices, err := c.GetWakeupDevices(tc.wantStatus) + assert.NoError(t, err) + + assert.Len(t, devices, len(tc.wantDevices)) + for i, wantDevice := range tc.wantDevices { + assert.Contains(t, devices, wantDevice, "device %d not found", i) + } + }) + } +}