diff --git a/internal/application/application.go b/internal/application/application.go index cee6e72..f985138 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -97,6 +97,7 @@ func Run() { os.Remove(testFile) logger.AddFile(filepath.Join(dataDir, "log.txt")) + logger.SetLevel = logger.LevelDebug DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite")) if err != nil { diff --git a/internal/network/network.go b/internal/network/network.go index 0a1ad4a..5a8589c 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -8,6 +8,7 @@ import ( "net" "sort" "strings" + "sync" "time" "github.com/godbus/dbus/v5" @@ -91,6 +92,7 @@ type Manager struct { conn *dbus.Conn nmObject dbus.BusObject Devices []Device + mu sync.RWMutex // protects Devices } // AccessPoint holds essential information about a discovered Wi-Fi network. @@ -142,7 +144,7 @@ func (m *Manager) FindDevices() error { var devicePaths []dbus.ObjectPath call := m.nmObject.Call(nmInterface+".GetDevices", 0) if call.Err != nil { - logger.Error("GetDevices D-Bus call failed: %w", call.Err) + logger.Error("GetDevices D-Bus call failed: %v", call.Err) return fmt.Errorf("GetDevices D-Bus call failed: %w", call.Err) } if err := call.Store(&devicePaths); err != nil { @@ -205,8 +207,33 @@ func (m *Manager) FindDevices() error { // CheckInternetConnection performs a check for an active internet connection. func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) { logger.Debug("[NET-040] Checking internet connectivity") - if err := m.FindDevices(); err != nil { - return &ConnectionStatus{StatusError, "Failed to scan devices"}, err + + // Previously called FindDevices() here, which rebuilt m.Devices every 2s, + // causing [NET-030] log spam and invalidating any pointers held by callers. + // Device enumeration is done once in Init(); we only need fresh State values. + // If this is a fresh Manager with no devices yet (e.g. called from + // checkState which creates a new Manager each tick), do a full device + // discovery. If devices are already known (e.g. the WiFiSetupPage's + // long-lived Manager), only refresh State to avoid the [NET-030] spam + // and avoid rebuilding the slice which would invalidate held pointers. + if len(m.Devices) == 0 { + if err := m.FindDevices(); err != nil { + return &ConnectionStatus{StatusError, "Failed to scan devices"}, err + } + } else { + m.mu.Lock() + for i := range m.Devices { + deviceObj := m.conn.Object(nmService, m.Devices[i].Path) + stateVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".State") + if err != nil { + logger.Warning("[NET-041] Could not refresh state for device %s: %v", m.Devices[i].Path, err) + continue + } + if state, ok := stateVariant.Value().(uint32); ok { + m.Devices[i].State = state + } + } + m.mu.Unlock() } // Check Ethernet first (Priority) @@ -235,14 +262,22 @@ func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) { // StartScan performs a Wi-Fi scan asynchronously. The optional stopChan can be used to abort. func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- error, stopChan <-chan struct{}) { + logger.Info("[NET-050] Starting Wi‑Fi scan") + + m.mu.RLock() + var wifiDevice *Device + for i := range m.Devices { if m.Devices[i].Type == TypeWifi { wifiDevice = &m.Devices[i] break } } + + m.mu.RUnlock() + if wifiDevice == nil { errorChan <- fmt.Errorf("no Wi-Fi device available for scanning") return @@ -250,27 +285,35 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e // Ensure Wi-Fi is enabled nmObj := m.conn.Object(nmService, nmPath) + prop, err := nmObj.GetProperty(nmInterface + ".WirelessEnabled") + if err != nil { errorChan <- fmt.Errorf("could not read WirelessEnabled property: %w", err) return } + if !prop.Value().(bool) { + call := nmObj.Call("org.freedesktop.DBus.Properties.Set", 0, nmInterface, "WirelessEnabled", dbus.MakeVariant(true), ) + if call.Err != nil { errorChan <- fmt.Errorf("could not enable Wi-Fi: %w", call.Err) return } + time.Sleep(2 * time.Second) // Give NetworkManager time to bring up the device } // Request a scan wifiDeviceWireless := m.conn.Object(nmService, wifiDevice.Path) + call := wifiDeviceWireless.Call(nmWirelessDeviceInterface+".RequestScan", 0, make(map[string]dbus.Variant)) + if call.Err != nil { errorChan <- fmt.Errorf("scan request failed: %w", call.Err) return @@ -284,11 +327,14 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e } var apPaths []dbus.ObjectPath + call = wifiDeviceWireless.Call(nmWirelessDeviceInterface+".GetAllAccessPoints", 0) + if call.Err != nil { errorChan <- fmt.Errorf("could not retrieve network list: %w", call.Err) return } + if err := call.Store(&apPaths); err != nil { errorChan <- fmt.Errorf("could not parse network list: %w", err) return @@ -299,6 +345,7 @@ func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- e errorChan <- err return } + resultsChan <- processedAPs } @@ -386,11 +433,40 @@ func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string) if wifiDevice.Type != TypeWifi { return fmt.Errorf("cannot connect: provided device is not a Wi-Fi device") } + m.mu.RLock() + currentState := wifiDevice.State + for _, d := range m.Devices { + if d.Path == wifiDevice.Path { + currentState = d.State + break + } + } + m.mu.RUnlock() - if wifiDevice.State == nmDeviceStateFailed || - wifiDevice.State == nmDeviceStateUnmanaged || - wifiDevice.State == nmDeviceStateUnavailable { - return fmt.Errorf("cannot connect: device is in an error state (state=%d)", wifiDevice.State) + if currentState == nmDeviceStateFailed || + currentState == nmDeviceStateUnmanaged || + currentState == nmDeviceStateUnavailable { + return fmt.Errorf("cannot connect: device is in an error state (state=%d)", currentState) + } + + // Delete any existing NM connection profile for this SSID to avoid + // accumulating duplicates across connection attempts. + settingsObj := m.conn.Object(nmService, "/org/freedesktop/NetworkManager/Settings") + var existingPaths []dbus.ObjectPath + if call := settingsObj.Call("org.freedesktop.NetworkManager.Settings.ListConnections", 0); call.Err == nil { + _ = call.Store(&existingPaths) + for _, cp := range existingPaths { + cObj := m.conn.Object(nmService, cp) + var csettings map[string]map[string]dbus.Variant + if c2 := cObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0); c2.Err == nil { + if err := c2.Store(&csettings); err == nil { + if id, ok := csettings["connection"]["id"].Value().(string); ok && id == ap.SSID { + logger.Info("[NET-073] Deleting existing profile for %q before reconnect", ap.SSID) + _ = cObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Delete", 0) + } + } + } + } } settings := map[string]map[string]dbus.Variant{ @@ -435,10 +511,11 @@ func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string) logger.Info("[NET-072] Connection created successfully. Connection=%s Active=%s", connectionPath, activeConnectionPath) - // Wait for the connection to be fully established - if _, err := m.WaitForInternet(30*time.Second, 5*time.Second); err != nil { - return fmt.Errorf("failed to establish internet connection: %w", err) - } + // Removed WaitForInternet() call here. It blocked the caller's thread + // for up to 35s and returned an error even on successful NM initiation + // if internet wasn't stable in time. The page's checkConnectionStatusLoop + // and handleConnectionStatusUpdate handle post-connection state tracking. + logger.Info("[NET-072] Connection initiated. Connection=%s Active=%s", connectionPath, activeConnectionPath) return nil } diff --git a/internal/page/wifiSetupPage.go b/internal/page/wifiSetupPage.go index 3da1405..ce5f13a 100644 --- a/internal/page/wifiSetupPage.go +++ b/internal/page/wifiSetupPage.go @@ -97,8 +97,23 @@ func (p *WiFiSetupPage) Init(ctx *PageCtx) error { return fmt.Errorf("failed to find network devices: %w. Try rebooting the computer. If this is your 3rd reboot, call for help", err) } + // FindDevices() rebuilds m.Devices each call, so store a VALUE copy, + // not a pointer into the slice — it would be invalidated on next call. + for i := range p.networkManager.Devices { + if p.networkManager.Devices[i].Type == network.TypeWifi { + device := p.networkManager.Devices[i] + p.wifiDevice = &device + break + } + } + if p.wifiDevice == nil { + p.networkManager.Close() + p.networkManager = nil + return fmt.Errorf("no Wi-Fi adapter found. Try rebooting the computer. If this is your 3rd reboot, call for help") + } + // Create UI elements - white := sdl.Color{R: 255, G: 255, B: 255, A: 255} + white := colors.White p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, white) if err != nil { return fmt.Errorf("failed to create title item: %w", err) @@ -117,7 +132,6 @@ func (p *WiFiSetupPage) Init(ctx *PageCtx) error { // Start scanning and status monitoring p.startScan() - go p.checkConnectionStatusLoop() return nil } @@ -143,6 +157,11 @@ func (p *WiFiSetupPage) checkConnectionStatusLoop() { // startScan triggers a new asynchronous network scan. func (p *WiFiSetupPage) startScan() { + if p.stopScan != nil { + close(p.stopScan) + } + p.stopScan = make(chan struct{}) + p.isScanning = true p.selectedIndex = 0 p.scrollOffset = 0 @@ -150,6 +169,7 @@ func (p *WiFiSetupPage) startScan() { if p.wifiDevice != nil { go p.networkManager.StartScan(p.resultsChan, p.errorChan, p.stopScan) + go p.checkConnectionStatusLoop() } else { p.isScanning = false } diff --git a/notes.org b/notes.org new file mode 100644 index 0000000..7df70a0 --- /dev/null +++ b/notes.org @@ -0,0 +1,3 @@ +* Networking Issues +- The networking package needs a LOT more logging... Nothing gets output when things crash. +- \ No newline at end of file