repeater/internal/app/app.go

287 lines
7.5 KiB
Go

package app
import (
"embed"
"log"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/nemunaire/repeater/internal/api"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/hotspot"
"github.com/nemunaire/repeater/internal/logging"
"github.com/nemunaire/repeater/internal/models"
"github.com/nemunaire/repeater/internal/station"
"github.com/nemunaire/repeater/internal/station/backend"
"github.com/nemunaire/repeater/internal/syslog"
"github.com/nemunaire/repeater/internal/wifi"
)
// App represents the application
type App struct {
Status models.SystemStatus
StatusMutex sync.RWMutex
StartTime time.Time
Assets embed.FS
Config *config.Config
SyslogTailer *syslog.SyslogTailer
}
// New creates a new application instance
func New(assets embed.FS) *App {
return &App{
Status: models.SystemStatus{
Connected: false,
ConnectionState: "disconnected",
ConnectedSSID: "",
HotspotStatus: nil,
ConnectedCount: 0,
DataUsage: 0.0,
Uptime: 0,
},
StartTime: time.Now(),
Assets: assets,
}
}
// Initialize initializes the application
func (a *App) Initialize(cfg *config.Config) error {
// Store config reference
a.Config = cfg
// Initialize WiFi backend
if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil {
return err
}
// Start WiFi event monitoring
if err := wifi.StartEventMonitoring(); err != nil {
log.Printf("Warning: WiFi event monitoring failed: %v", err)
// Don't fail - polling fallback still works
}
// Initialize station backend
stationConfig := backend.BackendConfig{
InterfaceName: cfg.HotspotInterface,
ARPTablePath: cfg.ARPTablePath,
DHCPLeasesPath: cfg.DHCPLeasesPath,
HostapdInterface: cfg.HotspotInterface,
}
if err := station.Initialize(cfg.StationBackend, stationConfig); err != nil {
log.Printf("Warning: Station backend initialization failed: %v", err)
// Don't fail - will continue without station discovery
} else {
// Start event monitoring for station events
if err := station.StartEventMonitoring(backend.EventCallbacks{
OnStationConnected: a.handleStationConnected,
OnStationDisconnected: a.handleStationDisconnected,
OnStationUpdated: a.handleStationUpdated,
}); err != nil {
log.Printf("Warning: Station event monitoring failed: %v", err)
// Don't fail - polling fallback still works
}
}
// Start syslog tailing if enabled
if cfg.SyslogEnabled {
a.SyslogTailer = syslog.NewSyslogTailer(
cfg.SyslogPath,
cfg.SyslogFilter,
cfg.SyslogSource,
)
if err := a.SyslogTailer.Start(); err != nil {
log.Printf("Warning: Failed to start syslog tailing: %v", err)
// Don't fail - app continues without syslog
}
}
// Start periodic tasks
go a.periodicStatusUpdate()
go a.periodicDeviceUpdate()
logging.AddLog("Système", "Application initialisée")
return nil
}
// Run starts the HTTP server
func (a *App) Run(addr string) error {
router := api.SetupRouter(&a.Status, a.Config, a.Assets)
logging.AddLog("Système", "Serveur API démarré sur "+addr)
return router.Run(addr)
}
// Shutdown gracefully shuts down the application
func (a *App) Shutdown() {
// Stop syslog tailing if running
if a.SyslogTailer != nil {
a.SyslogTailer.Stop()
}
// Stop station monitoring and close backend
station.StopEventMonitoring()
station.Close()
wifi.StopEventMonitoring()
wifi.Close()
logging.AddLog("Système", "Application arrêtée")
}
// getSystemUptime reads system uptime from /proc/uptime
func getSystemUptime() int64 {
data, err := os.ReadFile("/proc/uptime")
if err != nil {
log.Printf("Error reading /proc/uptime: %v", err)
return 0
}
fields := strings.Fields(string(data))
if len(fields) == 0 {
return 0
}
uptime, err := strconv.ParseFloat(fields[0], 64)
if err != nil {
log.Printf("Error parsing uptime: %v", err)
return 0
}
return int64(uptime)
}
// getInterfaceBytes reads rx and tx bytes for a network interface
func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) {
rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes"
txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes"
// Read RX bytes
rxData, err := os.ReadFile(rxPath)
if err != nil {
log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err)
} else {
rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64)
}
// Read TX bytes
txData, err := os.ReadFile(txPath)
if err != nil {
log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err)
} else {
txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64)
}
return rxBytes, txBytes
}
// periodicStatusUpdate updates WiFi connection status periodically
func (a *App) periodicStatusUpdate() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
a.StatusMutex.Lock()
a.Status.Connected = wifi.IsConnected()
a.Status.ConnectionState = wifi.GetConnectionState()
a.Status.ConnectedSSID = wifi.GetConnectedSSID()
a.Status.Uptime = getSystemUptime()
// Get detailed hotspot status
a.Status.HotspotStatus = hotspot.GetDetailedStatus()
// Get network data usage for WiFi interface
if a.Config != nil {
rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)
// Convert to MB and sum rx + tx
a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024)
}
a.StatusMutex.Unlock()
}
}
// periodicDeviceUpdate updates connected devices list periodically
func (a *App) periodicDeviceUpdate() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
devices, err := station.GetStations()
if err != nil {
log.Printf("Error getting connected devices: %v", err)
}
a.StatusMutex.Lock()
a.Status.ConnectedDevices = devices
a.Status.ConnectedCount = len(devices)
a.StatusMutex.Unlock()
}
}
// handleStationConnected handles station connection events
func (a *App) handleStationConnected(st backend.Station) {
a.StatusMutex.Lock()
defer a.StatusMutex.Unlock()
// Convert backend.Station to models.ConnectedDevice
device := models.ConnectedDevice{
Name: st.Hostname,
Type: st.Type,
MAC: st.MAC,
IP: st.IP,
}
// Check if device already exists
found := false
for i, d := range a.Status.ConnectedDevices {
if d.MAC == device.MAC {
a.Status.ConnectedDevices[i] = device
found = true
break
}
}
// Add new device if not found
if !found {
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices, device)
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
logging.AddLog("Stations", "Device connected: "+device.MAC+" ("+device.IP+")")
}
}
// handleStationDisconnected handles station disconnection events
func (a *App) handleStationDisconnected(mac string) {
a.StatusMutex.Lock()
defer a.StatusMutex.Unlock()
// Remove device from list
for i, d := range a.Status.ConnectedDevices {
if d.MAC == mac {
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices[:i], a.Status.ConnectedDevices[i+1:]...)
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
logging.AddLog("Stations", "Device disconnected: "+mac)
break
}
}
}
// handleStationUpdated handles station update events
func (a *App) handleStationUpdated(st backend.Station) {
a.StatusMutex.Lock()
defer a.StatusMutex.Unlock()
// Update existing device
for i, d := range a.Status.ConnectedDevices {
if d.MAC == st.MAC {
a.Status.ConnectedDevices[i] = models.ConnectedDevice{
Name: st.Hostname,
Type: st.Type,
MAC: st.MAC,
IP: st.IP,
}
break
}
}
}