adding mac support mainly

This commit is contained in:
lwoodard
2026-04-13 08:11:19 -06:00
parent 9c681e482e
commit 0f5a136764
9 changed files with 657 additions and 190 deletions

View File

@@ -16,16 +16,31 @@ import (
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"gopkg.in/yaml.v3"
)
const (
socketPath = "/run/streamdeck-go/helper.sock"
whitelistPath = "/etc/streamdeck-go/privileged.yaml"
socketMode = 0660
)
const socketMode = 0660
// socketPath returns the Unix socket path for the helper daemon.
// /run is standard on Linux; /var/run is used on macOS.
func socketPath() string {
if runtime.GOOS == "darwin" {
return "/var/run/streamdeck-go/helper.sock"
}
return "/run/streamdeck-go/helper.sock"
}
// whitelistPath returns the path to the root-owned command whitelist.
// /etc is standard on Linux; /usr/local/etc is the convention on macOS.
func whitelistPath() string {
if runtime.GOOS == "darwin" {
return "/usr/local/etc/streamdeck-go/privileged.yaml"
}
return "/etc/streamdeck-go/privileged.yaml"
}
type whitelist struct {
Commands map[string]string `yaml:"commands"`
@@ -46,25 +61,28 @@ func main() {
log.Fatal("streamdeck-helper must run as root (install as a system service)")
}
wl, err := loadWhitelist(whitelistPath)
if err != nil {
log.Fatalf("load whitelist %q: %v", whitelistPath, err)
}
log.Printf("loaded %d whitelisted commands from %s", len(wl.Commands), whitelistPath)
sock := socketPath()
wlist := whitelistPath()
if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
wl, err := loadWhitelist(wlist)
if err != nil {
log.Fatalf("load whitelist %q: %v", wlist, err)
}
log.Printf("loaded %d whitelisted commands from %s", len(wl.Commands), wlist)
if err := os.MkdirAll(filepath.Dir(sock), 0755); err != nil {
log.Fatalf("create socket dir: %v", err)
}
// Remove stale socket from a previous run.
_ = os.Remove(socketPath)
_ = os.Remove(sock)
ln, err := net.Listen("unix", socketPath)
ln, err := net.Listen("unix", sock)
if err != nil {
log.Fatalf("listen on %s: %v", socketPath, err)
log.Fatalf("listen on %s: %v", sock, err)
}
defer ln.Close()
if err := os.Chmod(socketPath, socketMode); err != nil {
if err := os.Chmod(sock, socketMode); err != nil {
log.Fatalf("chmod socket: %v", err)
}
// Chown the socket to root:streamdeck so group members can connect.
@@ -73,11 +91,11 @@ func main() {
log.Fatalf("group 'streamdeck' not found — run 'make install-helper' first: %v", err)
} else if gid, err := strconv.Atoi(grp.Gid); err != nil {
log.Fatalf("invalid gid %q: %v", grp.Gid, err)
} else if err := os.Lchown(socketPath, 0, gid); err != nil {
} else if err := os.Lchown(sock, 0, gid); err != nil {
log.Fatalf("chown socket: %v", err)
}
log.Printf("listening on %s (group: streamdeck)", socketPath)
log.Printf("listening on %s (group: streamdeck)", sock)
for {
conn, err := ln.Accept()
@@ -111,7 +129,7 @@ func handle(conn net.Conn, wl *whitelist) {
shell, ok := wl.Commands[req.Command]
if !ok {
log.Printf("REJECTED unknown command %q", req.Command)
send(conn, response{Error: fmt.Sprintf("unknown command %q — add it to %s", req.Command, whitelistPath)})
send(conn, response{Error: fmt.Sprintf("unknown command %q — add it to %s", req.Command, whitelistPath())})
return
}

View File

@@ -7,7 +7,6 @@ import (
"flag"
"fmt"
"image"
"image/color"
"image/draw"
"image/gif"
_ "image/jpeg"
@@ -17,6 +16,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
@@ -26,9 +26,17 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
"gopkg.in/yaml.v3"
)
const helperSocket = "/run/streamdeck-go/helper.sock"
// helperSocketPath returns the Unix socket path for the privileged helper.
// /run is the standard location on Linux; /var/run is used on macOS.
func helperSocketPath() string {
if runtime.GOOS == "darwin" {
return "/var/run/streamdeck-go/helper.sock"
}
return "/run/streamdeck-go/helper.sock"
}
func main() {
cfgPath := flag.String("config", defaultConfigPath(), "path to config file")
@@ -467,10 +475,39 @@ func runCommand(cmd string) {
}
}
// runPrivileged sends a named command to the helper daemon over its Unix socket.
// The helper validates the name against its root-owned whitelist and runs it.
// runPrivileged dispatches a named privileged command.
// On macOS: reads the user's local whitelist and runs via osascript (admin auth dialog).
// On Linux: sends to the root helper daemon over its Unix socket.
func runPrivileged(name string) error {
conn, err := net.Dial("unix", helperSocket)
if runtime.GOOS == "darwin" {
return runPrivilegedDarwin(name)
}
return runPrivilegedHelper(name)
}
// runPrivilegedDarwin looks up name in ~/.config/streamdeck-go/privileged.yaml
// and executes it via osascript, which shows the standard macOS admin auth dialog.
func runPrivilegedDarwin(name string) error {
wlPath := darwinWhitelistPath()
commands, err := loadPrivilegedCommands(wlPath)
if err != nil {
return fmt.Errorf("load whitelist %q: %w", wlPath, err)
}
shell, ok := commands[name]
if !ok {
return fmt.Errorf("unknown command %q — add it to %s", name, wlPath)
}
script := fmt.Sprintf(`do shell script %q with administrator privileges`, shell)
out, err := exec.Command("osascript", "-e", script).CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, out)
}
return nil
}
// runPrivilegedHelper sends a named command to the root helper daemon over its Unix socket.
func runPrivilegedHelper(name string) error {
conn, err := net.Dial("unix", helperSocketPath())
if err != nil {
return fmt.Errorf("helper unavailable (is streamdeck-go-helper.service running?): %w", err)
}
@@ -499,6 +536,32 @@ func runPrivileged(name string) error {
return nil
}
func darwinWhitelistPath() string {
base := os.Getenv("XDG_CONFIG_HOME")
if base == "" {
home, _ := os.UserHomeDir()
base = filepath.Join(home, ".config")
}
return filepath.Join(base, "streamdeck-go", "privileged.yaml")
}
func loadPrivilegedCommands(path string) (map[string]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var wl struct {
Commands map[string]string `yaml:"commands"`
}
if err := yaml.Unmarshal(data, &wl); err != nil {
return nil, err
}
if wl.Commands == nil {
return map[string]string{}, nil
}
return wl.Commands, nil
}
func defaultConfigPath() string {
base := os.Getenv("XDG_CONFIG_HOME")
if base == "" {
@@ -548,8 +611,3 @@ keys: {}
`
}
func blank(w, h int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(img, img.Bounds(), &image.Uniform{color.Black}, image.Point{}, draw.Src)
return img
}