diff --git a/README.md b/README.md index 885b6ef..cdbd88b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ Configuration is done using environment variables. - `ram`: Sessions are stored in RAM. - `WOLBODGE_DEVICE_TYPE`: Device type to pick, defaults to `test`. - `test`: A dummy device, used for testing. - - `libgpiod`: Uses the libgpiod commands under the hood. The gpio chip is specified using `WOLBODGE_DEVICE_GPIOCHIP`, the power button pin using `WOLBODGE_DEVICE_POWER_BUTTON_PIN` and the power LED pin using `WOLBODGE_DEVICE_POWER_LED_PIN`. + - `command`: Runs a subprocess to aquire the info. See the example below for more info. + +### Example: Raspberry pi using the command device +- `WOLBODGE_DEVICE_TYPE=command` +- `WOLBODGE_DEVICE_SHELL=bash -c $COMMAND`: Configure which shell we should use. +- `WOLBODGE_DEVICE_STATUS_COMMAND=gpioget --bias pull-down gpiochip0 17 > $WOLBODGE_STATUS_FILE`: Which command should be used to aquire the device's status. It should echo out a "0" for off and a "1" for on into the file created at "$WOLBODGE_STATUS_FILE". +- `WOLBODGE_DEVICE_TOGGLE_STATUS_COMMAND=gpioset gpiochip0 27=1 && sleep 2 && gpioset gpiochip0 27=0`: Which command should be used to toggle the device's status. This usually involves pressing the power button virtually. ## Usage example Usage example for a backup script that backs up to a NAS that isn't online 24/7. @@ -39,4 +45,3 @@ fi ## TODOs - Alerts when a session has existed for a long time -- Command device that can use commands to turn on/off a device diff --git a/devices/command.go b/devices/command.go new file mode 100644 index 0000000..aab47ae --- /dev/null +++ b/devices/command.go @@ -0,0 +1,94 @@ +package devices + +import ( + "fmt" + "os" + "os/exec" + "strings" + "sync" +) + +// TODO: A command device +// You will be able to use shell commands to push the power button and get the status. +// +// Status will give an env variable: WOLBODGE_STATUS_FILE, you will be expected to write a "0" or a "1" there, depending on the state. + +func NewCommand(shell []string, statusCmd string, toggleStatusCmd string) Device { + device := &command{ + shell: shell, + statusCmd: statusCmd, + toggleStatusCmd: toggleStatusCmd, + lock: sync.Mutex{}, + } + return device +} + +type command struct { + shell []string + statusCmd string + toggleStatusCmd string + lock sync.Mutex +} + +func (d *command) Status() (bool, error) { + d.lock.Lock() + defer d.lock.Unlock() + + file, err := os.CreateTemp("", "wolbodge-status-*") + if err != nil { + return false, fmt.Errorf("failed to create temporary status file: %w", err) + } + defer os.Remove(file.Name()) + + cmd := d.makeCommand(d.statusCmd) + cmd.Env = append( + cmd.Environ(), + fmt.Sprintf("WOLBODGE_STATUS_FILE=%s", file.Name()), + ) + + err = cmd.Run() + if err != nil { + return false, fmt.Errorf("failed to run command: %w", err) + } + + data, err := os.ReadFile(file.Name()) + if err != nil { + return false, fmt.Errorf("failed to read status file: %w", err) + } + + content := string(data) + content = strings.TrimSpace(content) + + switch content { + case "0": + return false, nil + case "1": + return true, nil + default: + return false, fmt.Errorf("invalid status file content, expected \"0\" or \"1\", got \"%s\"", content) + } +} + +func (d *command) ToggleStatus() error { + d.lock.Lock() + defer d.lock.Unlock() + + cmd := d.makeCommand(d.toggleStatusCmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to run command: %w", err) + } + + return nil +} + +func (d *command) makeCommand(command string) *exec.Cmd { + args := make([]string, len(d.shell)) + copy(args, d.shell) + + for i := range args { + args[i] = strings.ReplaceAll(args[i], "$COMMAND", command) + } + + return exec.Command(args[0], args[1:]...) +} diff --git a/devices/devices.go b/devices/devices.go index c53aac0..1f2d495 100644 --- a/devices/devices.go +++ b/devices/devices.go @@ -6,8 +6,13 @@ import ( ) type Device interface { + // Retreive the status of the device. + // This is usually acheived by taking a reading from the power LED. Status() (bool, error) - PushPowerButton() error + + // Toggle the status of the device. + // This is usually achieved by pressing the power button. + ToggleStatus() error } func KeepDeviceInSync(logger *log.Logger, device Device, wantStatus func() (bool, error), exit <-chan bool) { @@ -36,15 +41,15 @@ func KeepDeviceInSync(logger *log.Logger, device Device, wantStatus func() (bool continue } - err = device.PushPowerButton() + err = device.ToggleStatus() if err != nil { - logger.Printf("Failed to push power button, waiting 2 minutes until next attempt: %s", err) + logger.Printf("Failed to toggle device status, waiting 2 minutes until next attempt: %s", err) // Cooldown to not overwhelm the device with start/stop requests time.Sleep(2 * time.Minute) return } - logger.Printf("Pushed power button") + logger.Printf("Toggled device status") time.Sleep(2 * time.Minute) } } diff --git a/devices/libgpiod.go b/devices/libgpiod.go deleted file mode 100644 index 1295b92..0000000 --- a/devices/libgpiod.go +++ /dev/null @@ -1,101 +0,0 @@ -package devices - -import ( - "fmt" - "os/exec" - "sync" - "time" -) - -// TODO: Do not use subprocesses here, they are unreliable, provide hidden dependencies and are a common target for vulnerabilities. -// TODO: Improve pulldown, pullup -func NewLibgpiod(gpiochip string, powerButtonPin int, powerLEDPin int) Device { - device := &libgpiod{ - gpiochip: gpiochip, - powerButtonPin: powerButtonPin, - powerLEDPin: powerLEDPin, - lock: sync.Mutex{}, - } - - return device -} - -type libgpiod struct { - gpiochip string - powerButtonPin int - powerLEDPin int - lock sync.Mutex -} - -func (d *libgpiod) Status() (bool, error) { - d.lock.Lock() - defer d.lock.Unlock() - - status, err := d.read(d.powerLEDPin) - if err != nil { - return false, err - } - - return status, nil -} - -func (d *libgpiod) PushPowerButton() error { - d.lock.Lock() - defer d.lock.Unlock() - - err := d.write(d.powerButtonPin, 1) - if err != nil { - return err - } - - time.Sleep(2 * time.Second) - - err = d.write(d.powerButtonPin, 0) - if err != nil { - return err - } - - return nil -} - -func (d *libgpiod) write(pin int, value int) error { - cmd := exec.Command( - "env", "sh", "-c", - fmt.Sprintf("gpioset %s %d=%d", d.gpiochip, pin, value), - ) - - outRaw, err := cmd.Output() - if err != nil { - return err - } - - out := string(outRaw) - if out == "" { - return nil - } - - return fmt.Errorf("unexpected command output: %s", err) -} - -func (d *libgpiod) read(pin int) (bool, error) { - cmd := exec.Command( - "env", "sh", "-c", - fmt.Sprintf("gpioget --bias pull-down %s %d", d.gpiochip, pin), - ) - - outRaw, err := cmd.Output() - if err != nil { - return false, err - } - - out := string(outRaw) - - switch out { - case "0\n": - return false, nil - case "1\n": - return true, nil - default: - return false, fmt.Errorf("unexpected command output: %s", out) - } -} diff --git a/devices/test.go b/devices/test.go index d714ab0..6841689 100644 --- a/devices/test.go +++ b/devices/test.go @@ -2,10 +2,10 @@ package devices import "sync" -func NewTest(initialStatus bool) Device { +func NewTest(status bool) Device { device := &test{ lock: sync.Mutex{}, - status: initialStatus, + status: status, } return device } @@ -22,7 +22,7 @@ func (d *test) Status() (bool, error) { return d.status, nil } -func (d *test) PushPowerButton() error { +func (d *test) ToggleStatus() error { d.lock.Lock() defer d.lock.Unlock() diff --git a/env.go b/env.go index 4b6ffc0..1df7ed1 100644 --- a/env.go +++ b/env.go @@ -5,6 +5,7 @@ import ( "log" "os" "strconv" + "strings" "git.1e99.eu/1e99/wolbodge/devices" "git.1e99.eu/1e99/wolbodge/sessions" @@ -31,15 +32,15 @@ func NewDevice() (devices.Device, error) { case "test": return devices.NewTest(false), nil - case "libgpiod": - var gpiochip string - var powerButtonPin int - var powerLEDPin int - Env("WOLBODGE_DEVICE_GPIOCHIP", &gpiochip, "gpiochip0") - Env("WOLBODGE_DEVICE_POWER_BUTTON_PIN", &powerButtonPin, "27") - Env("WOLBODGE_DEVICE_POWER_LED_PIN", &powerLEDPin, "17") + case "command": + var shell []string + var statusCmd string + var toggleStatusCmd string + Env("WOLBODGE_DEVICE_SHELL", &shell, "") + Env("WOLBODGE_DEVICE_STATUS_COMMAND", &statusCmd, "") + Env("WOLBODGE_DEVICE_TOGGLE_STATUS_COMMAND", &toggleStatusCmd, "") - return devices.NewLibgpiod(gpiochip, powerButtonPin, powerLEDPin), nil + return devices.NewCommand(shell, statusCmd, toggleStatusCmd), nil default: return nil, fmt.Errorf("unknown device type: %s", deviceType) @@ -73,5 +74,8 @@ func Env(name string, out any, def string) { case *string: *value = raw + + case *[]string: + *value = strings.Fields(raw) } }