package iwd import ( "log" "sync" "github.com/godbus/dbus/v5" ) // SignalMonitor monitors D-Bus signals from iwd type SignalMonitor struct { conn *dbus.Conn station *Station // Signal channel signalChan chan *dbus.Signal // Callbacks onStateChange func(state StationState, ssid string) onScanComplete func() // Control stopChan chan struct{} stopOnce sync.Once mu sync.RWMutex running bool // State tracking lastScanning bool } // NewSignalMonitor creates a new signal monitor func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor { return &SignalMonitor{ conn: conn, station: station, signalChan: make(chan *dbus.Signal, 100), stopChan: make(chan struct{}), } } // OnStateChange registers a callback for state changes func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) { sm.mu.Lock() defer sm.mu.Unlock() sm.onStateChange = callback } // OnScanComplete registers a callback for scan completion func (sm *SignalMonitor) OnScanComplete(callback func()) { sm.mu.Lock() defer sm.mu.Unlock() sm.onScanComplete = callback } // Start begins monitoring D-Bus signals func (sm *SignalMonitor) Start() error { sm.mu.Lock() if sm.running { sm.mu.Unlock() return nil } sm.running = true sm.mu.Unlock() // Subscribe to PropertiesChanged signals for Station interface stationPath := sm.station.GetPath() // Add signal match for PropertiesChanged on Station interface matchOptions := []dbus.MatchOption{ dbus.WithMatchObjectPath(stationPath), dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), dbus.WithMatchMember("PropertiesChanged"), } if err := sm.conn.AddMatchSignal(matchOptions...); err != nil { sm.mu.Lock() sm.running = false sm.mu.Unlock() return err } // Register signal channel sm.conn.Signal(sm.signalChan) // Get initial scanning state scanning, err := sm.station.IsScanning() if err == nil { sm.lastScanning = scanning } // Start monitoring goroutine go sm.monitor() log.Printf("D-Bus signal monitoring started for station %s", stationPath) return nil } // Stop stops monitoring D-Bus signals. Idempotent: stopOnce guards the // channel close so concurrent Stop() callers do not panic on double-close. func (sm *SignalMonitor) Stop() { sm.mu.Lock() if !sm.running { sm.mu.Unlock() return } sm.running = false sm.mu.Unlock() sm.stopOnce.Do(func() { close(sm.stopChan) }) // Remove signal channel sm.conn.RemoveSignal(sm.signalChan) log.Printf("D-Bus signal monitoring stopped") } // monitor is the main signal processing loop func (sm *SignalMonitor) monitor() { for { select { case sig := <-sm.signalChan: sm.handleSignal(sig) case <-sm.stopChan: return } } } // handleSignal processes a D-Bus signal func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) { // Only process PropertiesChanged signals if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { return } // Verify signal is from Station interface if len(sig.Body) < 2 { return } interfaceName, ok := sig.Body[0].(string) if !ok || interfaceName != StationInterface { return } // Parse changed properties changedProps, ok := sig.Body[1].(map[string]dbus.Variant) if !ok { return } // Check for State property change if stateVariant, ok := changedProps["State"]; ok { if state, ok := stateVariant.Value().(string); ok { sm.handleStateChange(StationState(state)) } } // Check for Scanning property change if scanningVariant, ok := changedProps["Scanning"]; ok { if scanning, ok := scanningVariant.Value().(bool); ok { sm.handleScanningChange(scanning) } } // Check for ConnectedNetwork property change if _, ok := changedProps["ConnectedNetwork"]; ok { // Network connection changed, trigger state update sm.handleConnectionChange() } } // handleStateChange processes a state change func (sm *SignalMonitor) handleStateChange(state StationState) { sm.mu.RLock() callback := sm.onStateChange sm.mu.RUnlock() if callback == nil { return } // Get connected SSID if connected ssid := "" if state == StateConnected { network, err := sm.station.GetConnectedNetwork() if err == nil { props, err := network.GetProperties() if err == nil { ssid = props.Name } } } callback(state, ssid) } // handleScanningChange processes scanning state changes func (sm *SignalMonitor) handleScanningChange(scanning bool) { // Detect scan completion (transition from true to false) if sm.lastScanning && !scanning { sm.mu.RLock() callback := sm.onScanComplete sm.mu.RUnlock() if callback != nil { callback() } } sm.lastScanning = scanning } // handleConnectionChange processes connection changes func (sm *SignalMonitor) handleConnectionChange() { // Get current state and trigger state change callback state, err := sm.station.GetState() if err != nil { return } sm.handleStateChange(state) }