refactor: create App internally, promote newService(), adjust variable names

This commit is contained in:
2025-06-22 18:58:01 -05:00
parent 2a23ddc9a5
commit 0aba8b4bfa
4 changed files with 86 additions and 91 deletions

View File

@@ -8,7 +8,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"ha-tray/internal/app"
"ha-tray/internal/service" "ha-tray/internal/service"
) )
@@ -26,9 +25,8 @@ func main() {
} }
}() }()
// Create app layer and service layer // Create service layer
appLayer := app.NewApp(logger) svc := service.NewService(logger)
svc := service.NewService(logger, appLayer)
logger.Info("HATray initialized, running service") logger.Info("HATray initialized, running service")

View File

@@ -30,13 +30,13 @@ func NewApp(logger *slog.Logger) *App {
} }
// Pause disconnects from the server and ceases any background tasks // Pause disconnects from the server and ceases any background tasks
func (a *App) Pause() error { func (app *App) Pause() error {
a.mu.Lock() app.mu.Lock()
defer a.mu.Unlock() defer app.mu.Unlock()
a.logger.Info("Pausing application", app.logger.Info("Pausing application",
"action", "pause", "action", "pause",
"previous_state", a.state, "previous_state", app.state,
"new_state", StatePaused) "new_state", StatePaused)
// TODO: Implement actual pause logic // TODO: Implement actual pause logic
@@ -44,23 +44,23 @@ func (a *App) Pause() error {
// - Stop background tasks // - Stop background tasks
// - Pause sensor monitoring // - Pause sensor monitoring
a.state = StatePaused app.state = StatePaused
a.logger.Info("Application paused successfully", app.logger.Info("Application paused successfully",
"action", "pause", "action", "pause",
"state", a.state) "state", app.state)
return nil return nil
} }
// Resume connects to the server and initiates background tasks // Resume connects to the server and initiates background tasks
func (a *App) Resume() error { func (app *App) Resume() error {
a.mu.Lock() app.mu.Lock()
defer a.mu.Unlock() defer app.mu.Unlock()
a.logger.Info("Resuming application", app.logger.Info("Resuming application",
"action", "resume", "action", "resume",
"previous_state", a.state, "previous_state", app.state,
"new_state", StateRunning) "new_state", StateRunning)
// TODO: Implement actual resume logic // TODO: Implement actual resume logic
@@ -68,11 +68,11 @@ func (a *App) Resume() error {
// - Start background tasks // - Start background tasks
// - Resume sensor monitoring // - Resume sensor monitoring
a.state = StateRunning app.state = StateRunning
a.logger.Info("Application resumed successfully", app.logger.Info("Application resumed successfully",
"action", "resume", "action", "resume",
"state", a.state) "state", app.state)
return nil return nil
} }
@@ -124,13 +124,13 @@ func (a *App) GetState() AppState {
} }
// Stop stops the application completely // Stop stops the application completely
func (a *App) Stop() error { func (app *App) Stop() error {
a.mu.Lock() app.mu.Lock()
defer a.mu.Unlock() defer app.mu.Unlock()
a.logger.Info("Stopping application", app.logger.Info("Stopping application",
"action", "stop", "action", "stop",
"previous_state", a.state, "previous_state", app.state,
"new_state", StateStopped) "new_state", StateStopped)
// TODO: Implement actual stop logic // TODO: Implement actual stop logic
@@ -138,11 +138,11 @@ func (a *App) Stop() error {
// - Clean up resources // - Clean up resources
// - Stop all background tasks // - Stop all background tasks
a.state = StateStopped app.state = StateStopped
a.logger.Info("Application stopped successfully", app.logger.Info("Application stopped successfully",
"action", "stop", "action", "stop",
"state", a.state) "state", app.state)
return nil return nil
} }

View File

@@ -1,17 +1,9 @@
package service package service
import (
"ha-tray/internal/app"
"log/slog"
)
// This is an intentionally very-simple interface as the main program entrypoint needs to know very little about the service layer. // This is an intentionally very-simple interface as the main program entrypoint needs to know very little about the service layer.
// The service layer is completely responsible for the lifecycle of the application, implemented per-platform. // The service layer is completely responsible for the lifecycle of the application, implemented per-platform.
type Service interface { type Service interface {
Run() error Run() error
} }
// NewService creates a new service instance for the current platform // You create a service using the NewService() function, implemented per-platform. If you don't have a NewService() function, you can't create a service on your platform.
func NewService(logger *slog.Logger, appLayer *app.App) Service {
return newService(logger, appLayer)
}

View File

@@ -9,9 +9,9 @@ import (
"ha-tray/internal/app" "ha-tray/internal/app"
"golang.org/x/sys/windows/svc" winsvc "golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/debug" winsvcDebug "golang.org/x/sys/windows/svc/debug"
"golang.org/x/sys/windows/svc/eventlog" winsvcEventlog "golang.org/x/sys/windows/svc/eventlog"
) )
const serviceName = "HATray" const serviceName = "HATray"
@@ -19,131 +19,136 @@ const serviceName = "HATray"
// WindowsService implements the Service interface for Windows // WindowsService implements the Service interface for Windows
type WindowsService struct { type WindowsService struct {
logger *slog.Logger logger *slog.Logger
elog debug.Log elog winsvcDebug.Log
isDebug bool isDebug bool
app *app.App app *app.App
} }
// newService creates a new Windows service instance // newService creates a new Windows service instance
func newService(logger *slog.Logger, appLayer *app.App) Service { func NewService(logger *slog.Logger) Service {
return &WindowsService{ return &WindowsService{
logger: logger, logger: logger,
app: appLayer, app: app.NewApp(logger),
} }
} }
// Run implements the Service interface for Windows // Run implements the Service interface for Windows
func (w *WindowsService) Run() error { func (svc *WindowsService) Run() error {
// Determine if we're running as a Windows service // Determine if we're running as a Windows service
isService, err := svc.IsWindowsService() isService, err := winsvc.IsWindowsService()
if err != nil { if err != nil {
return fmt.Errorf("failed to determine if running as Windows service: %v", err) return fmt.Errorf("failed to determine if running as Windows service: %v", err)
} }
w.isDebug = !isService svc.isDebug = !isService
if isService { if isService {
return w.runAsService() return svc.runAsService()
} }
// Interactive mode // Interactive mode
return w.runInteractive() return svc.runInteractive()
} }
// runAsService runs the application as a Windows service // runAsService runs the application as a Windows service
func (w *WindowsService) runAsService() error { func (svc *WindowsService) runAsService() error {
var err error var err error
if w.isDebug { if svc.isDebug {
w.elog = debug.New(serviceName) svc.elog = winsvcDebug.New(serviceName)
} else { } else {
w.elog, err = eventlog.Open(serviceName) svc.elog, err = winsvcEventlog.Open(serviceName)
if err != nil { if err != nil {
return fmt.Errorf("failed to open event log: %v", err) return fmt.Errorf("failed to open event log: %v", err)
} }
} }
defer w.elog.Close() defer svc.elog.Close()
w.elog.Info(1, fmt.Sprintf("starting %s service", serviceName)) svc.elog.Info(1, fmt.Sprintf("starting %s service", serviceName))
run := svc.Run run := winsvc.Run
if w.isDebug { if svc.isDebug {
run = debug.Run run = winsvcDebug.Run
} }
err = run(serviceName, &windowsServiceHandler{ err = run(serviceName, &serviceHandler{
service: w, service: svc,
}) })
if err != nil { if err != nil {
w.elog.Error(1, fmt.Sprintf("%s service failed: %v", serviceName, err)) svc.elog.Error(1, fmt.Sprintf("%s service failed: %v", serviceName, err))
return err return err
} }
w.elog.Info(1, fmt.Sprintf("%s service stopped", serviceName)) svc.elog.Info(1, fmt.Sprintf("%s service stopped", serviceName))
return nil return nil
} }
// runInteractive runs the application in interactive mode // runInteractive runs the application in interactive mode
func (w *WindowsService) runInteractive() error { func (svc *WindowsService) runInteractive() error {
w.logger.Info("Application starting in interactive mode") svc.logger.Info("Application starting in interactive mode")
// Simple interactive loop // Simple interactive loop
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
w.logger.Debug("Application heartbeat") svc.logger.Debug("Application heartbeat")
} }
return nil return nil
} }
// windowsServiceHandler implements the Windows service handler interface // serviceHandler implements the Windows service handler interface
type windowsServiceHandler struct { type serviceHandler struct {
service *WindowsService service *WindowsService
} }
func (h *windowsServiceHandler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { func (handler *serviceHandler) Execute(args []string, r <-chan winsvc.ChangeRequest, changes chan<- winsvc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue const cmdsAccepted = winsvc.AcceptStop | winsvc.AcceptShutdown | winsvc.AcceptPauseAndContinue
changes <- svc.Status{State: svc.StartPending} changes <- winsvc.Status{State: winsvc.StartPending}
h.service.logger.Info("Service starting") handler.service.logger.Info("Service starting")
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} changes <- winsvc.Status{State: winsvc.Running, Accepts: cmdsAccepted}
// Main service loop // Service heartbeat
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case c := <-r: case c := <-r:
switch c.Cmd { switch c.Cmd {
case svc.Interrogate: case winsvc.Interrogate:
changes <- c.CurrentStatus changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown: case winsvc.Stop, winsvc.Shutdown:
h.service.logger.Info("Service stopping") changes <- winsvc.Status{State: winsvc.StopPending}
changes <- svc.Status{State: svc.StopPending}
if err := h.service.app.Stop(); err != nil { handler.service.logger.Info("Service stopping")
h.service.logger.Error("Failed to stop app layer", "error", err) if err := handler.service.app.Stop(); err != nil {
handler.service.logger.Error("Failed to stop app layer", "error", err)
} }
return return
case svc.Pause: case winsvc.Pause:
h.service.logger.Info("Service pausing") changes <- winsvc.Status{State: winsvc.Paused, Accepts: cmdsAccepted}
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
if err := h.service.app.Pause(); err != nil { handler.service.logger.Info("Service pausing")
h.service.logger.Error("Failed to pause app layer", "error", err) if err := handler.service.app.Pause(); err != nil {
handler.service.logger.Error("Failed to pause app layer", "error", err)
} }
case svc.Continue: case winsvc.Continue:
h.service.logger.Info("Service continuing") changes <- winsvc.Status{State: winsvc.Running, Accepts: cmdsAccepted}
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
if err := h.service.app.Resume(); err != nil { handler.service.logger.Info("Service continuing")
h.service.logger.Error("Failed to resume app layer", "error", err) if err := handler.service.app.Resume(); err != nil {
handler.service.logger.Error("Failed to resume app layer", "error", err)
} }
default: default:
h.service.elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c)) // Log the error to the event log & service logger
handler.service.logger.Error("unexpected control request", "request", c)
handler.service.elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c))
} }
case <-ticker.C: case <-ticker.C:
h.service.logger.Debug("Service heartbeat") handler.service.logger.Debug("heartbeat")
} }
} }
} }