mirror of
https://github.com/Xevion/HATray.git
synced 2025-12-06 01:15:11 -06:00
feat!: expand to internal project, add app layer, develop service layer
This commit is contained in:
@@ -36,7 +36,7 @@ tasks:
|
|||||||
|
|
||||||
dev:
|
dev:
|
||||||
desc: Development workflow - build, test, and run
|
desc: Development workflow - build, test, and run
|
||||||
deps: [deps, fmt, vet, test, windows:build]
|
deps: [deps, fmt, vet, test, build]
|
||||||
cmds:
|
cmds:
|
||||||
- echo "Development workflow complete"
|
- echo "Development workflow complete"
|
||||||
- echo "Run 'task windows:run' to start the application"
|
- echo "Run 'task windows:run' to start the application"
|
||||||
25
cmd/main.go
Normal file
25
cmd/main.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"ha-tray/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create new application instance
|
||||||
|
app := internal.NewApp()
|
||||||
|
|
||||||
|
// Setup the application (logging, panic handling, service initialization)
|
||||||
|
if err := app.Setup(); err != nil {
|
||||||
|
log.Fatalf("Failed to setup application: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Application error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
110
internal/app.go
Normal file
110
internal/app.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"ha-tray/internal/app"
|
||||||
|
"ha-tray/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App represents the main application
|
||||||
|
type App struct {
|
||||||
|
appLayer *app.App
|
||||||
|
service service.Service
|
||||||
|
logger *slog.Logger
|
||||||
|
logFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new application instance
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initializes the application with logging and panic handling
|
||||||
|
func (a *App) Setup() error {
|
||||||
|
// Setup panic recovery
|
||||||
|
defer a.recoverPanic()
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
if err := a.setupLogging(); err != nil {
|
||||||
|
return fmt.Errorf("failed to setup logging: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup app layer
|
||||||
|
a.appLayer = app.NewApp(a.logger)
|
||||||
|
|
||||||
|
// Setup service
|
||||||
|
if err := a.setupService(); err != nil {
|
||||||
|
return fmt.Errorf("failed to setup service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the application
|
||||||
|
func (a *App) Run() error {
|
||||||
|
defer a.cleanup()
|
||||||
|
|
||||||
|
a.logger.Info("Starting HATray application")
|
||||||
|
|
||||||
|
// Run the service
|
||||||
|
return a.service.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupLogging initializes structured logging
|
||||||
|
func (a *App) setupLogging() error {
|
||||||
|
// Get the directory where the executable is located
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %v", err)
|
||||||
|
}
|
||||||
|
exeDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
|
// Open log file in the same directory as the executable
|
||||||
|
logFile, err := os.OpenFile(filepath.Join(exeDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logFile = logFile
|
||||||
|
|
||||||
|
// Create multi-writer to log to both file and stdout
|
||||||
|
multiWriter := io.MultiWriter(logFile, os.Stdout)
|
||||||
|
|
||||||
|
// Create JSON handler for structured logging
|
||||||
|
handler := slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
})
|
||||||
|
a.logger = slog.New(handler)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupService initializes the platform-specific service
|
||||||
|
func (a *App) setupService() error {
|
||||||
|
// Platform-specific service initialization using build flags
|
||||||
|
a.service = service.NewService(a.logger, a.appLayer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recoverPanic handles panic recovery and logs the error
|
||||||
|
func (a *App) recoverPanic() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if a.logger != nil {
|
||||||
|
a.logger.Error("Panic recovered", "panic", r)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Panic recovered: %v\n", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup performs cleanup operations
|
||||||
|
func (a *App) cleanup() {
|
||||||
|
if a.logFile != nil {
|
||||||
|
a.logFile.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
148
internal/app/app.go
Normal file
148
internal/app/app.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App represents the main application layer that is generic and cross-platform
|
||||||
|
type App struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
mu sync.RWMutex
|
||||||
|
state AppState
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppState represents the current state of the application
|
||||||
|
type AppState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateRunning AppState = "running"
|
||||||
|
StatePaused AppState = "paused"
|
||||||
|
StateStopped AppState = "stopped"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewApp creates a new application instance
|
||||||
|
func NewApp(logger *slog.Logger) *App {
|
||||||
|
return &App{
|
||||||
|
logger: logger,
|
||||||
|
state: StateRunning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause disconnects from the server and ceases any background tasks
|
||||||
|
func (a *App) Pause() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
a.logger.Info("Pausing application",
|
||||||
|
"action", "pause",
|
||||||
|
"previous_state", a.state,
|
||||||
|
"new_state", StatePaused)
|
||||||
|
|
||||||
|
// TODO: Implement actual pause logic
|
||||||
|
// - Disconnect from Home Assistant WebSocket
|
||||||
|
// - Stop background tasks
|
||||||
|
// - Pause sensor monitoring
|
||||||
|
|
||||||
|
a.state = StatePaused
|
||||||
|
|
||||||
|
a.logger.Info("Application paused successfully",
|
||||||
|
"action", "pause",
|
||||||
|
"state", a.state)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume connects to the server and initiates background tasks
|
||||||
|
func (a *App) Resume() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
a.logger.Info("Resuming application",
|
||||||
|
"action", "resume",
|
||||||
|
"previous_state", a.state,
|
||||||
|
"new_state", StateRunning)
|
||||||
|
|
||||||
|
// TODO: Implement actual resume logic
|
||||||
|
// - Connect to Home Assistant WebSocket
|
||||||
|
// - Start background tasks
|
||||||
|
// - Resume sensor monitoring
|
||||||
|
|
||||||
|
a.state = StateRunning
|
||||||
|
|
||||||
|
a.logger.Info("Application resumed successfully",
|
||||||
|
"action", "resume",
|
||||||
|
"state", a.state)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload pauses the application, re-reads configuration files, then resumes
|
||||||
|
func (a *App) Reload() error {
|
||||||
|
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",
|
||||||
|
"action", "reload",
|
||||||
|
"error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement configuration reload logic
|
||||||
|
// - Re-read TOML configuration files
|
||||||
|
// - Validate configuration
|
||||||
|
// - Update internal state with new configuration
|
||||||
|
|
||||||
|
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",
|
||||||
|
"action", "reload",
|
||||||
|
"error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("Application reload completed successfully",
|
||||||
|
"action", "reload",
|
||||||
|
"final_state", a.state)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of the application
|
||||||
|
func (a *App) GetState() AppState {
|
||||||
|
a.mu.RLock()
|
||||||
|
defer a.mu.RUnlock()
|
||||||
|
return a.state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the application completely
|
||||||
|
func (a *App) Stop() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
a.logger.Info("Stopping application",
|
||||||
|
"action", "stop",
|
||||||
|
"previous_state", a.state,
|
||||||
|
"new_state", StateStopped)
|
||||||
|
|
||||||
|
// TODO: Implement actual stop logic
|
||||||
|
// - Disconnect from all services
|
||||||
|
// - Clean up resources
|
||||||
|
// - Stop all background tasks
|
||||||
|
|
||||||
|
a.state = StateStopped
|
||||||
|
|
||||||
|
a.logger.Info("Application stopped successfully",
|
||||||
|
"action", "stop",
|
||||||
|
"state", a.state)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
17
internal/service/service.go
Normal file
17
internal/service/service.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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.
|
||||||
|
// The service layer is completely responsible for the lifecycle of the application, implemented per-platform.
|
||||||
|
type Service interface {
|
||||||
|
Run() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new service instance for the current platform
|
||||||
|
func NewService(logger *slog.Logger, appLayer *app.App) Service {
|
||||||
|
return newService(logger, appLayer)
|
||||||
|
}
|
||||||
149
internal/service/windows.go
Normal file
149
internal/service/windows.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ha-tray/internal/app"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"golang.org/x/sys/windows/svc/debug"
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const serviceName = "HATray"
|
||||||
|
|
||||||
|
// WindowsService implements the Service interface for Windows
|
||||||
|
type WindowsService struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
elog debug.Log
|
||||||
|
isDebug bool
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
// newService creates a new Windows service instance
|
||||||
|
func newService(logger *slog.Logger, appLayer *app.App) Service {
|
||||||
|
return &WindowsService{
|
||||||
|
logger: logger,
|
||||||
|
app: appLayer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements the Service interface for Windows
|
||||||
|
func (w *WindowsService) Run() error {
|
||||||
|
// Determine if we're running as a Windows service
|
||||||
|
isService, err := svc.IsWindowsService()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to determine if running as Windows service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.isDebug = !isService
|
||||||
|
|
||||||
|
if isService {
|
||||||
|
return w.runAsService()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive mode
|
||||||
|
return w.runInteractive()
|
||||||
|
}
|
||||||
|
|
||||||
|
// runAsService runs the application as a Windows service
|
||||||
|
func (w *WindowsService) runAsService() error {
|
||||||
|
var err error
|
||||||
|
if w.isDebug {
|
||||||
|
w.elog = debug.New(serviceName)
|
||||||
|
} else {
|
||||||
|
w.elog, err = eventlog.Open(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open event log: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer w.elog.Close()
|
||||||
|
|
||||||
|
w.elog.Info(1, fmt.Sprintf("starting %s service", serviceName))
|
||||||
|
|
||||||
|
run := svc.Run
|
||||||
|
if w.isDebug {
|
||||||
|
run = debug.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
err = run(serviceName, &windowsServiceHandler{
|
||||||
|
service: w,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.elog.Error(1, fmt.Sprintf("%s service failed: %v", serviceName, err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.elog.Info(1, fmt.Sprintf("%s service stopped", serviceName))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runInteractive runs the application in interactive mode
|
||||||
|
func (w *WindowsService) runInteractive() error {
|
||||||
|
w.logger.Info("Application starting in interactive mode")
|
||||||
|
|
||||||
|
// Simple interactive loop
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
w.logger.Debug("Application heartbeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsServiceHandler implements the Windows service handler interface
|
||||||
|
type windowsServiceHandler struct {
|
||||||
|
service *WindowsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsServiceHandler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||||
|
changes <- svc.Status{State: svc.StartPending}
|
||||||
|
|
||||||
|
h.service.logger.Info("Service starting")
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
|
||||||
|
// Main service loop
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case c := <-r:
|
||||||
|
switch c.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
changes <- c.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
h.service.logger.Info("Service stopping")
|
||||||
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
if err := h.service.app.Stop(); err != nil {
|
||||||
|
h.service.logger.Error("Failed to stop app layer", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case svc.Pause:
|
||||||
|
h.service.logger.Info("Service pausing")
|
||||||
|
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
||||||
|
if err := h.service.app.Pause(); err != nil {
|
||||||
|
h.service.logger.Error("Failed to pause app layer", "error", err)
|
||||||
|
}
|
||||||
|
case svc.Continue:
|
||||||
|
h.service.logger.Info("Service continuing")
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
if err := h.service.app.Resume(); err != nil {
|
||||||
|
h.service.logger.Error("Failed to resume app layer", "error", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
h.service.elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c))
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
h.service.logger.Debug("Service heartbeat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
main.go
184
main.go
@@ -1,184 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows/svc"
|
|
||||||
"golang.org/x/sys/windows/svc/debug"
|
|
||||||
"golang.org/x/sys/windows/svc/eventlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var elog debug.Log
|
|
||||||
|
|
||||||
type myservice struct{}
|
|
||||||
|
|
||||||
func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
|
||||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
|
||||||
changes <- svc.Status{State: svc.StartPending}
|
|
||||||
|
|
||||||
// Get the directory where the executable is located
|
|
||||||
exePath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
elog.Error(1, fmt.Sprintf("Failed to get executable path: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
exeDir := filepath.Dir(exePath)
|
|
||||||
|
|
||||||
// Open log file in the same directory as the executable
|
|
||||||
logFile, err := os.OpenFile(filepath.Join(exeDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
elog.Error(1, fmt.Sprintf("Failed to open log file: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer logFile.Close()
|
|
||||||
|
|
||||||
// Create JSON logger
|
|
||||||
logger := log.New(logFile, "", 0)
|
|
||||||
|
|
||||||
// Log startup
|
|
||||||
startupLog := map[string]interface{}{
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"level": "debug",
|
|
||||||
"message": "Service starting",
|
|
||||||
"service": "hass-tray",
|
|
||||||
}
|
|
||||||
startupJSON, _ := json.Marshal(startupLog)
|
|
||||||
logger.Println(string(startupJSON))
|
|
||||||
|
|
||||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
|
||||||
|
|
||||||
// Main service loop
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case c := <-r:
|
|
||||||
switch c.Cmd {
|
|
||||||
case svc.Interrogate:
|
|
||||||
changes <- c.CurrentStatus
|
|
||||||
case svc.Stop, svc.Shutdown:
|
|
||||||
// Log shutdown
|
|
||||||
shutdownLog := map[string]interface{}{
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"level": "debug",
|
|
||||||
"message": "Service stopping",
|
|
||||||
"service": "hass-tray",
|
|
||||||
}
|
|
||||||
shutdownJSON, _ := json.Marshal(shutdownLog)
|
|
||||||
logger.Println(string(shutdownJSON))
|
|
||||||
|
|
||||||
changes <- svc.Status{State: svc.StopPending}
|
|
||||||
return
|
|
||||||
case svc.Pause:
|
|
||||||
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
|
||||||
case svc.Continue:
|
|
||||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
|
||||||
default:
|
|
||||||
elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c))
|
|
||||||
}
|
|
||||||
case <-ticker.C:
|
|
||||||
// Log heartbeat
|
|
||||||
heartbeatLog := map[string]interface{}{
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"level": "debug",
|
|
||||||
"message": "Service heartbeat",
|
|
||||||
"service": "hass-tray",
|
|
||||||
}
|
|
||||||
heartbeatJSON, _ := json.Marshal(heartbeatLog)
|
|
||||||
logger.Println(string(heartbeatJSON))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runService(name string, isDebug bool) {
|
|
||||||
var err error
|
|
||||||
if isDebug {
|
|
||||||
elog = debug.New(name)
|
|
||||||
} else {
|
|
||||||
elog, err = eventlog.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer elog.Close()
|
|
||||||
|
|
||||||
elog.Info(1, fmt.Sprintf("starting %s service", name))
|
|
||||||
run := svc.Run
|
|
||||||
if isDebug {
|
|
||||||
run = debug.Run
|
|
||||||
}
|
|
||||||
err = run(name, &myservice{})
|
|
||||||
if err != nil {
|
|
||||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
elog.Info(1, fmt.Sprintf("%s service stopped", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
isDebug, err := svc.IsAnInteractiveSession()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDebug {
|
|
||||||
runService("hass-tray", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interactive mode - just run the service logic directly
|
|
||||||
fmt.Println("Running in interactive mode...")
|
|
||||||
|
|
||||||
// Get the current directory for log file
|
|
||||||
currentDir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get current directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open log file
|
|
||||||
logFile, err := os.OpenFile(filepath.Join(currentDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to open log file: %v", err)
|
|
||||||
}
|
|
||||||
defer logFile.Close()
|
|
||||||
|
|
||||||
// Create JSON logger
|
|
||||||
logger := log.New(logFile, "", 0)
|
|
||||||
|
|
||||||
// Log startup
|
|
||||||
startupLog := map[string]interface{}{
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"level": "debug",
|
|
||||||
"message": "Application starting in interactive mode",
|
|
||||||
"service": "hass-tray",
|
|
||||||
}
|
|
||||||
startupJSON, _ := json.Marshal(startupLog)
|
|
||||||
logger.Println(string(startupJSON))
|
|
||||||
|
|
||||||
fmt.Println("Press Ctrl+C to stop...")
|
|
||||||
|
|
||||||
// Simple interactive loop
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
// Log heartbeat
|
|
||||||
heartbeatLog := map[string]interface{}{
|
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
|
||||||
"level": "debug",
|
|
||||||
"message": "Application heartbeat",
|
|
||||||
"service": "hass-tray",
|
|
||||||
}
|
|
||||||
heartbeatJSON, _ := json.Marshal(heartbeatLog)
|
|
||||||
logger.Println(string(heartbeatJSON))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ vars:
|
|||||||
tasks:
|
tasks:
|
||||||
build:
|
build:
|
||||||
cmds:
|
cmds:
|
||||||
- go build -o ./dist/{{.EXE_NAME}} .
|
- go build -o ./dist/{{.EXE_NAME}} ./cmd/main.go
|
||||||
|
|
||||||
package:
|
package:
|
||||||
deps: [build]
|
deps: [build]
|
||||||
|
|||||||
Reference in New Issue
Block a user