79 lines
1.9 KiB
Go
79 lines
1.9 KiB
Go
// Package hook runs shell commands after monitor layout changes.
|
|
// Hooks execute asynchronously with a timeout; failures are logged
|
|
// but never interrupt the daemon's operation.
|
|
package hook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const defaultTimeout = 30 * time.Second
|
|
|
|
// Runner executes shell commands from the hooks configuration.
|
|
type Runner struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewRunner creates a hook runner with the given logger.
|
|
func NewRunner(logger *slog.Logger) *Runner {
|
|
return &Runner{logger: logger}
|
|
}
|
|
|
|
// Run executes a list of shell commands concurrently. Each command gets
|
|
// a separate sub-shell via "sh -c". If a command exceeds defaultTimeout,
|
|
// it is killed. Errors are logged but never returned.
|
|
//
|
|
// Commands undergo basic shell-like expansion for tildes and environment
|
|
// variables before execution.
|
|
func (r *Runner) Run(ctx context.Context, commands []string) {
|
|
for _, cmdStr := range commands {
|
|
cmdStr = strings.TrimSpace(cmdStr)
|
|
if cmdStr == "" {
|
|
continue
|
|
}
|
|
|
|
go func(cmdStr string) {
|
|
if err := r.runOne(ctx, cmdStr); err != nil {
|
|
r.logger.Error("hook failed", "command", cmdStr, "error", err)
|
|
}
|
|
}(cmdStr)
|
|
}
|
|
}
|
|
|
|
// runOne executes a single command with a timeout.
|
|
func (r *Runner) runOne(ctx context.Context, cmdStr string) error {
|
|
cmdStr = expandPath(cmdStr)
|
|
|
|
cmdCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
|
|
defer cancel()
|
|
|
|
r.logger.Debug("running hook", "command", cmdStr)
|
|
|
|
cmd := exec.CommandContext(cmdCtx, "sh", "-c", cmdStr)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("hook: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// expandPath performs basic tilde and $HOME expansion.
|
|
func expandPath(s string) string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return s
|
|
}
|
|
s = strings.ReplaceAll(s, "~", home)
|
|
s = strings.ReplaceAll(s, "$HOME", home)
|
|
s = strings.ReplaceAll(s, "${HOME}", home)
|
|
return s
|
|
}
|