Skip to content

Quickshell.Networking — Wi-Fi disconnect drives an unstoppable reconnect loop #802

@atsugvA

Description

@atsugvA

Quickshell.Networking — Wi-Fi disconnect drives an unstoppable reconnect loop

Quickshell: 0.3.0 (Arch quickshell 0.3.0-1.1)
NetworkManager: (host system)
Compositor: Niri
Reporter context: refactor of a shell from nmcli-driven Wi-Fi to native Quickshell.Networking bindings. Everything except disconnect ended up working cleanly; disconnect is the one path that we could not get right via any combination of native calls.


Symptom

After any user-initiated Wi-Fi disconnect issued via the native API (WifiDevice.disconnect(), WifiNetwork.disconnect(), or WifiDevice.autoconnect = false), NetworkManager enters a tight reconnect retry loop that:

  • Keeps the device "active" enough that the user has no internet (each attempt fails before completing, or attempts loop too quickly to settle).
  • Does not stop when QML stops setting any properties.
  • Does not stop when the device's scanner is disabled.
  • Continues until Quickshell is restarted entirely.
  • After QS restart, NM is quiescent and a manual connect via QS works.

The exact same Wi-Fi adapter, NM version, and user profile does not loop under our legacy nmcli-only implementation (single nmcli device disconnect <iface> call). Switching back to that path inside the QS shell instantly
restores correct behaviour.

So: NM itself is fine. The legacy nmcli call is fine. Something about keeping QS.Networking alive in the process while disconnect is issued causes the loop.


Reproduction (in our shell, but isolatable)

QML facade roughly:

property var _wifiDev: /* first WifiDevice from Networking.devices */

function disconnectCurrent() {
    if (!_wifiDev) return
    _wifiDev.autoconnect = false    // tried in isolation: loop
    _wifiDev.disconnect()           // tried in isolation: loop
    // Combination: also loops.
    // Adding `_wifiDev.scannerEnabled = false`: also loops.
}

Click any disconnect button on the currently-connected SSID → loop.
QML never re-issues connect() during the loop (verified by
console.log on every connect entry point — zero re-entries).

A minimal test PanelWindow that imports Quickshell.Networking, picks the first WifiDevice, and exposes a button calling dev.disconnect() should reproduce on any NM box.


What we tried (all looped)

In our repo these are around 10 commits (2 separate tries) but in concept:

Attempt Disconnect call Result
1 WifiNetwork.disconnect() on the active network loop
2 WifiDevice.disconnect() only loop
3 WifiDevice.autoconnect = false, then WifiNetwork.disconnect() loop
4 WifiDevice.autoconnect = false, then WifiDevice.disconnect() loop
5 nmcli connection modify <active-uuid> autoconnect no then WifiDevice.disconnect() loop
6 nmcli device disconnect <iface> (single call) with QS.Networking observers active loop
7 WifiDevice.autoconnect = false + WifiDevice.scannerEnabled = false + nmcli device disconnect <iface> loop

Same nmcli device disconnect <iface> call from the legacy shell (no import Quickshell.Networking, no Networking.devices observers): works.

Logged signals during the loop:

  • WifiDevice.autoconnectChanged fires exactly once (to false, the value we wrote). It is not flipped back to true by any external party.
  • Our QML never calls connect() / connectWithPsk / connectWithSettings again (zero log lines for those during the loop).
  • WifiDevice.connectedChanged fires repeatedly (state oscillates).
  • The device's scannerEnabled going false does not break the loop.

So the loop is NM-driven, but somehow only when QS.Networking is observing the device. We cannot reach the relevant NM property/method from the public QML API.


Hypothesis

Something in Quickshell.Networking's internal D-Bus observation either:

  • Calls Device.Reapply / re-activates the profile after the disconnect D-Bus call, or
  • Keeps the device in a state where NM's "available connection" evaluator re-arms autoconnect on every scan completion, or
  • Maps WifiDevice.disconnect() to a D-Bus method that lacks the "explicit-disconnect" semantics that nmcli device disconnect carries (org.freedesktop.NetworkManager.Device.Disconnect should — but maybe the bus call is missing a flag or being followed by an activation).

We cannot tell from QML which is which.


What would unblock us

Any of:

  1. A flag / method on WifiDevice that maps to NM's
    Device.Disconnect with the explicit-disconnect bit set and is not followed by anything else.
  2. A documented "disconnect and stay disconnected" pattern for Quickshell.Networking.
  3. Confirmation that the current disconnect() implementation does carry explicit-disconnect, plus a way to diagnose what's actually re-arming autoconnect.

Until then we keep nmcli device disconnect <iface> as a single subprocess shell-out in services/WifiScanner.qml and use native bindings for everything else (device enumeration, scan list, connect, forget, ethernet, wifiEnabled).


Files in this repo

  • services/WifiScanner.qml — reverted to legacy nmcli implementation.
  • services/Network.qml — fully native.
  • modules/widgets/network/EthernetRow.qml — native.
  • modules/controlcenter/CCTogglesSection.qml — native.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions