Initial commit: monitor-lets-go — automatic monitor layout daemon
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user