217 lines
4.8 KiB
Go
217 lines
4.8 KiB
Go
package device
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/config"
|
|
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
|
|
)
|
|
|
|
type Device struct {
|
|
Name string
|
|
Client *keenetic.Client
|
|
SkipCollectors []string
|
|
|
|
UpdateInterval time.Duration
|
|
CacheTTL time.Duration
|
|
|
|
Cache map[string]any
|
|
CacheMutex sync.RWMutex
|
|
LastUpdate time.Time
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
type Manager struct {
|
|
devices []*Device
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
func NewManager(configs []config.DeviceConfig) *Manager {
|
|
m := &Manager{}
|
|
|
|
for _, cfg := range configs {
|
|
client, err := keenetic.NewClient(cfg.Address)
|
|
if err != nil {
|
|
log.Printf("Failed to create client for %s: %v", cfg.Name, err)
|
|
continue
|
|
}
|
|
|
|
timeout, err := time.ParseDuration(cfg.Timeout)
|
|
if err != nil {
|
|
timeout = 10 * time.Second
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
err = client.Init(ctx, cfg.Username, cfg.Password)
|
|
cancel()
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to initialize client for %s: %v", cfg.Name, err)
|
|
continue
|
|
}
|
|
|
|
updateInterval, _ := time.ParseDuration(cfg.UpdateInterval)
|
|
cacheTTL, _ := time.ParseDuration(cfg.CacheTTL)
|
|
|
|
deviceCtx, deviceCancel := context.WithCancel(context.Background())
|
|
device := &Device{
|
|
Name: cfg.Name,
|
|
Client: client,
|
|
SkipCollectors: cfg.SkipCollectors,
|
|
UpdateInterval: updateInterval,
|
|
CacheTTL: cacheTTL,
|
|
Cache: make(map[string]any),
|
|
ctx: deviceCtx,
|
|
cancel: deviceCancel,
|
|
}
|
|
|
|
m.devices = append(m.devices, device)
|
|
log.Printf("Successfully initialized device: %s (hostname: %s)", cfg.Name, client.Hostname)
|
|
|
|
// Запуск фонового обновления
|
|
device.wg.Add(1)
|
|
go device.startBackgroundUpdater()
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func (m *Manager) GetDevices() []*Device {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
result := make([]*Device, len(m.devices))
|
|
copy(result, m.devices)
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) GetDevice(name string) (*Device, error) {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
for i := range m.devices {
|
|
if m.devices[i].Name == name {
|
|
return m.devices[i], nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("device %s not found", name)
|
|
}
|
|
|
|
func (m *Manager) Shutdown(ctx context.Context) error {
|
|
m.mutex.RLock()
|
|
devices := m.devices
|
|
m.mutex.RUnlock()
|
|
|
|
log.Printf("shutting down %d device(s)...", len(devices))
|
|
|
|
// Cancel all device contexts
|
|
for _, dev := range devices {
|
|
if dev.cancel != nil {
|
|
dev.cancel()
|
|
}
|
|
}
|
|
|
|
// Wait for all background updaters to finish with timeout
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for _, dev := range devices {
|
|
dev.wg.Wait()
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
log.Println("all background updaters stopped successfully")
|
|
return nil
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("shutdown timeout exceeded")
|
|
}
|
|
}
|
|
|
|
func (d *Device) startBackgroundUpdater() {
|
|
defer d.wg.Done()
|
|
|
|
if d.UpdateInterval <= 0 {
|
|
return
|
|
}
|
|
|
|
log.Printf("starting background updater for device %s (interval: %s)", d.Name, d.UpdateInterval)
|
|
|
|
ticker := time.NewTicker(d.UpdateInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
d.updateCache()
|
|
|
|
case <-d.ctx.Done():
|
|
log.Printf("stopping background updater for device %s", d.Name)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Device) updateCache() {
|
|
collectors := map[string]func(context.Context) (any, error){
|
|
"system": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetSystemInfo(ctx)
|
|
},
|
|
"interface": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetInterfaceInfo(ctx)
|
|
},
|
|
"internet": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetInternetStatus(ctx)
|
|
},
|
|
"hotspot": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetHotspotClientInfo(ctx)
|
|
},
|
|
"interface_stats": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetConnectedInterfaceStats(ctx)
|
|
},
|
|
"process": func(ctx context.Context) (any, error) {
|
|
return d.Client.GetProcessInfo(ctx)
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
updatedCount := 0
|
|
for name, fetchFn := range collectors {
|
|
if d.shouldSkipCollector(name) {
|
|
continue
|
|
}
|
|
|
|
data, err := fetchFn(ctx)
|
|
if err != nil {
|
|
log.Printf("background update failed for device %s, collector %s: %v", d.Name, name, err)
|
|
continue
|
|
}
|
|
|
|
d.CacheMutex.Lock()
|
|
d.Cache[name] = data
|
|
d.LastUpdate = time.Now()
|
|
d.CacheMutex.Unlock()
|
|
|
|
updatedCount++
|
|
}
|
|
|
|
if updatedCount > 0 {
|
|
log.Printf("background update completed for device %s: %d collector(s) updated", d.Name, updatedCount)
|
|
}
|
|
}
|
|
|
|
func (d *Device) shouldSkipCollector(name string) bool {
|
|
return slices.Contains(d.SkipCollectors, name)
|
|
}
|