diff --git a/build/unix/HATray.service b/build/unix/HATray.service new file mode 100644 index 0000000..214631e --- /dev/null +++ b/build/unix/HATray.service @@ -0,0 +1,44 @@ +[Unit] +Description=HATray - Home Assistant Tray Utility +Documentation=https://github.com/Xevion/HATray +After=network-online.target +Wants=network-online.target + +[Service] +Type=notify +NotifyAccess=main +ExecStart=$BINARY_PATH +ExecReload=/bin/kill -HUP $MAINPID + +Restart=on-failure +RestartSec=5 + +# CPUAccounting=yes +# MemoryAccounting=yes +# StandardOutput=journal +# StandardError=journal +# SyslogIdentifier=HATray + +# Environment variables +# Environment=HOME=/home/%i + +# Security settings +# NoNewPrivileges=true +# PrivateTmp=true +# ProtectSystem=strict +# ProtectHome=true +# ReadWritePaths=/home/%i/.config/HATray + +# Resource limits (cgroups v2) +# MemoryMax=128M +# MemoryHigh=96M +# MemorySwapMax=0 +# CPUQuota=10% +# CPUWeight=100 +# IOWeight=100 +# TasksMax=100 +# LimitNOFILE=1024 +# LimitCORE=0 + +[Install] +WantedBy=default.target diff --git a/cmd/main.go b/cmd/main.go index dcfed11..38cf7dc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,25 +14,25 @@ import ( func main() { logger, logFile, err := setupLogging() if err != nil { - log.Fatalf("Failed to setup logging: %v", err) + log.Fatalf("failed to setup logging: %v", err) } defer logFile.Sync() defer logFile.Close() defer func() { if r := recover(); r != nil { - logger.Error("Uncaught panic recovered", "panic", r) + logger.Error("uncaught panic recovered", "panic", r) } }() // Create service layer svc := service.NewService(logger) - logger.Info("HATray initialized, running service") + logger.Info("service initialized") // Main loop if err := svc.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Application error: %v\n", err) + fmt.Fprintf(os.Stderr, "application error: %v\n", err) os.Exit(1) } } diff --git a/go.mod b/go.mod index 1e57aa5..79cb131 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module ha-tray go 1.21 require golang.org/x/sys v0.15.0 + +require github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect diff --git a/go.sum b/go.sum index 063d2d3..8e427ac 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/app/app.go b/internal/app/app.go index 9dbd4b9..6df751d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -34,7 +34,7 @@ func (app *App) Pause() error { app.mu.Lock() defer app.mu.Unlock() - app.logger.Info("Pausing application", + app.logger.Info("pausing application", "action", "pause", "previous_state", app.state, "new_state", StatePaused) @@ -46,7 +46,7 @@ func (app *App) Pause() error { app.state = StatePaused - app.logger.Info("Application paused successfully", + app.logger.Info("paused successfully", "action", "pause", "state", app.state) @@ -58,7 +58,7 @@ func (app *App) Resume() error { app.mu.Lock() defer app.mu.Unlock() - app.logger.Info("Resuming application", + app.logger.Info("resuming application", "action", "resume", "previous_state", app.state, "new_state", StateRunning) @@ -70,7 +70,7 @@ func (app *App) Resume() error { app.state = StateRunning - app.logger.Info("Application resumed successfully", + app.logger.Info("resumed successfully", "action", "resume", "state", app.state) @@ -79,14 +79,14 @@ func (app *App) Resume() error { // Reload pauses the application, re-reads configuration files, then resumes func (a *App) Reload() error { - a.logger.Info("Starting application reload", + a.logger.Info("starting application reload", "action", "reload", "current_state", a.state) // Pause if not already paused if a.state != StatePaused { if err := a.Pause(); err != nil { - a.logger.Error("Failed to pause during reload", + a.logger.Error("failed to pause during reload", "action", "reload", "error", err) return err @@ -98,18 +98,18 @@ func (a *App) Reload() error { // - Validate configuration // - Update internal state with new configuration - a.logger.Info("Configuration reloaded successfully", + a.logger.Info("configuration reloaded successfully", "action", "reload") // Resume the application if err := a.Resume(); err != nil { - a.logger.Error("Failed to resume after reload", + a.logger.Error("failed to resume after reload", "action", "reload", "error", err) return err } - a.logger.Info("Application reload completed successfully", + a.logger.Info("application reload completed successfully", "action", "reload", "final_state", a.state) @@ -128,7 +128,7 @@ func (app *App) Stop() error { app.mu.Lock() defer app.mu.Unlock() - app.logger.Info("Stopping application", + app.logger.Info("stopping application", "action", "stop", "previous_state", app.state, "new_state", StateStopped) @@ -140,7 +140,7 @@ func (app *App) Stop() error { app.state = StateStopped - app.logger.Info("Application stopped successfully", + app.logger.Info("application stopped successfully", "action", "stop", "state", app.state) diff --git a/internal/service/linux.go b/internal/service/linux.go new file mode 100644 index 0000000..c0994dd --- /dev/null +++ b/internal/service/linux.go @@ -0,0 +1,90 @@ +//go:build linux + +package service + +import ( + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "ha-tray/internal/app" + + "github.com/coreos/go-systemd/daemon" +) + +const serviceName = "HATray" + +// linuxService implements the Service interface for Linux +// It integrates with systemd and controls the app layer +// according to systemd signals (start, stop, reload) +type linuxService struct { + logger *slog.Logger + app *app.App +} + +// NewService creates a new Linux service instance +func NewService(logger *slog.Logger) Service { + return &linuxService{ + logger: logger, + app: app.NewApp(logger), + } +} + +// Run implements the Service interface for Linux +func (s *linuxService) Run() error { + s.logger.Info("starting service") + + // Notify systemd that we are starting + daemon.SdNotify(false, "STATUS=Starting HATray...\n") + + // Setup signal handling for systemd + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2) + done := make(chan struct{}) + + // Notify systemd that we are ready + daemon.SdNotify(false, "READY=1") + daemon.SdNotify(false, "STATUS=HATray running\n") + + go func() { + for { + sig := <-sigs + s.logger.Info("signal received", "signal", sig) + switch sig { + case syscall.SIGINT, syscall.SIGTERM: + daemon.SdNotify(false, "STOPPING=1") + s.logger.Info("stopping service") + s.app.Stop() + close(done) + return + case syscall.SIGHUP: + s.logger.Info("reloading service") + daemon.SdNotify(false, "RELOADING=1") + s.app.Reload() + daemon.SdNotify(false, "READY=1") + case syscall.SIGUSR1: + s.logger.Info("pausing service") + s.app.Pause() + case syscall.SIGUSR2: + s.logger.Info("resuming service") + s.app.Resume() + } + } + }() + + // Main loop: heartbeat to systemd + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-done: + s.logger.Info("service stopped") + return nil + case <-ticker.C: + daemon.SdNotify(false, "WATCHDOG=1") + s.logger.Debug("heartbeat") + } + } +} diff --git a/tasks/Taskfile_linux.yml b/tasks/Taskfile_linux.yml new file mode 100644 index 0000000..96b4108 --- /dev/null +++ b/tasks/Taskfile_linux.yml @@ -0,0 +1,28 @@ +version: '3' + +vars: + BINARY_NAME: '{{.APP_NAME}}' + +tasks: + build: + cmds: + - go build -o ./bin/{{.BINARY_NAME}} ./cmd/main.go + + service: + desc: "Install the service" + deps: [build] + cmds: + - mkdir -p $HOME/.config/systemd/user + - cmd: systemctl stop HATray --user + ignore_error: true + - cp ./bin/{{.BINARY_NAME}} $HOME/.local/bin/{{.BINARY_NAME}} + # super hacky way of fixing MAINPID being replaced + - BINARY_PATH="$HOME/.local/bin/{{.BINARY_NAME}}" MAINPID='$MAINPID' envsubst < ./build/unix/HATray.service > $HOME/.config/systemd/user/HATray.service + - systemctl daemon-reload --user + # - systemctl enable HATray --user + - systemctl start HATray --user + + tail: + desc: "Tail the log file" + cmds: + - sudo journalctl -u HATray -f \ No newline at end of file