i3-companion: move dampening/retry logic inside a decorator

There is some nasty bug we can run into:

```
ERROR: Task was destroyed but it is pending!
task: <Task pending name='Task-44' coro=<dampen.<locals>.decorator.<locals>.fn_now() running at /home/bernat/.config/i3/bin/i3-companion:115> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f687353e2b0>()]>>
```

This is often followed by a segfault. It's a bit difficult to
understand where it comes from. Sleeping a bit even if we don't want
to dampen seems to workaround this issue. It seems we have to keep a
reference to a task until it is cancelled properly.
This commit is contained in:
Vincent Bernat 2021-07-12 22:15:57 +02:00
parent fb0bd7fcca
commit 2012ba0c15

View file

@ -98,6 +98,53 @@ def on(*events):
return decorator return decorator
def dampen(sleep, *, unless=None, retry=0):
"""Dampen a function call."""
def decorator(fn):
async def fn_now(retry, *args, **kwargs):
try:
if unless is not None and unless(*args, **kwargs):
# see https://github.com/ldo/dbussy/issues/15
await asyncio.sleep(0.1)
else:
await asyncio.sleep(sleep)
except asyncio.CancelledError:
return
fn.running = None
try:
return await fn(*args, **kwargs)
except Exception as e:
if not retry:
logger.exception(f"while executing {fn}: %s", e)
return
retry -= 1
logger.warning(
f"while executing {fn} (remaining tries: %d): %s",
retry,
str(e),
)
if fn.running is not None:
return
fn.running = asyncio.create_task(
fn_now(retry, *args, **kwargs)
)
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
if fn.running is not None:
logger.debug(f"cancel call to previous {fn}")
fn.running.cancel()
fn.running = None
logger.debug(f"dampening call to {fn}")
fn.running = asyncio.create_task(fn_now(retry, *args, **kwargs))
fn.running = None
return wrapper
return decorator
async def notify(i3, **kwargs): async def notify(i3, **kwargs):
"""Send a notification with notify-send.""" """Send a notification with notify-send."""
peer = i3.session_bus["org.freedesktop.Notifications"] peer = i3.session_bus["org.freedesktop.Notifications"]
@ -379,32 +426,18 @@ async def workspace_info(i3, event):
@on(I3Event.OUTPUT) @on(I3Event.OUTPUT)
@dampen(2)
async def output_update(i3, event): async def output_update(i3, event):
"""React to a XRandR change.""" """React to a XRandR change."""
running = getattr(output_update, "running", None) logger.info("XRandR change detected")
if running is not None: cmds = (
running.cancel() "systemctl --user reload --no-block xsettingsd.service",
output_update.running = None "systemctl --user start --no-block wallpaper.service",
)
async def output_update_now(): for cmd in cmds:
try: proc = subprocess.run(shlex.split(cmd))
await asyncio.sleep(2) if proc.returncode != 0:
except asyncio.CancelledError: logger.warning(f"{cmd} exited with {proc.returncode}")
return
output_update.running = None
logger.info("XRandR change detected")
cmds = (
"systemctl --user reload --no-block xsettingsd.service",
"systemctl --user start --no-block wallpaper.service",
)
for cmd in cmds:
proc = subprocess.run(shlex.split(cmd))
if proc.returncode != 0:
logger.warning(f"{cmd} exited with {proc.returncode}")
logger.debug("schedule XRandR change")
output_update.running = asyncio.create_task(output_update_now())
@on( @on(
@ -421,7 +454,7 @@ async def bluetooth_notifications(
"""Display notifications related to Bluetooth state.""" """Display notifications related to Bluetooth state."""
if interface != "org.bluez.Device1": if interface != "org.bluez.Device1":
return return
if not "Connected" in changed: if "Connected" not in changed:
return return
logger.info(changed["Connected"]) logger.info(changed["Connected"])
peer = i3.system_bus["org.bluez"][path] peer = i3.system_bus["org.bluez"][path]
@ -451,8 +484,8 @@ async def network_manager_notifications(i3, event, path, state, reason):
ofnm = "org.freedesktop.NetworkManager" ofnm = "org.freedesktop.NetworkManager"
logger.debug(f"from {path} state: {state}, reason: {reason}") logger.debug(f"from {path} state: {state}, reason: {reason}")
if state not in {NM_ACTIVE_CONNECTION_STATE_ACTIVATED}: if state not in {NM_ACTIVE_CONNECTION_STATE_ACTIVATED}:
# We cannot get proper state unless the connection is # Deactivated state does not contain enough information,
# activated, unless we maintain state. # unless we maintain state.
return return
peer = i3.system_bus[ofnm][path] peer = i3.system_bus[ofnm][path]
try: try:
@ -497,128 +530,98 @@ async def network_manager_notifications(i3, event, path, state, reason):
signature="a{sv}", signature="a{sv}",
), ),
) )
@dampen(
1,
unless=lambda i3, event, *args: (
isinstance(event, DBusSignal) and event.interface.endswith(".Active")
),
retry=2,
)
async def network_manager_status(i3, event, *args): async def network_manager_status(i3, event, *args):
"""Compute network manager status.""" """Compute network manager status."""
ofnm = "org.freedesktop.NetworkManager" ofnm = "org.freedesktop.NetworkManager"
status = []
# Dampen updates # Build status from devices
running = getattr(network_manager_status, "running", None) bus = i3.system_bus[ofnm]
if running is not None: nm = await bus["/org/freedesktop/NetworkManager"].get_async_interface(ofnm)
running.cancel() devices = await nm.AllDevices
network_manager_status.running = None for device in devices:
nmd = await bus[device].get_async_interface(f"{ofnm}.Device")
async def network_manager_status_now(sleep=1): kind = await nmd.DeviceType
try: state = await nmd.State
await asyncio.sleep(sleep) if state == NM_DEVICE_STATE_UNMANAGED:
except asyncio.CancelledError: continue
return if kind == NM_DEVICE_TYPE_WIFI:
network_manager_status.running = None if state != NM_DEVICE_STATE_ACTIVATED:
try: status.append(icons["nowifi"])
await _network_manager_status_now()
except Exception as e:
logger.warning("while updating network status: %s", str(e))
if network_manager_status.running is None:
network_manager_status.running = asyncio.create_task(
network_manager_status_now(5)
)
async def _network_manager_status_now():
status = []
# Build status from devices
bus = i3.system_bus[ofnm]
nm = await bus["/org/freedesktop/NetworkManager"].get_async_interface(
ofnm
)
devices = await nm.AllDevices
for device in devices:
nmd = await bus[device].get_async_interface(f"{ofnm}.Device")
kind = await nmd.DeviceType
state = await nmd.State
if state == NM_DEVICE_STATE_UNMANAGED:
continue continue
if kind == NM_DEVICE_TYPE_WIFI: nmw = await bus[device].get_async_interface(
if state != NM_DEVICE_STATE_ACTIVATED: f"{ofnm}.Device.Wireless"
status.append(icons["nowifi"])
continue
nmw = await bus[device].get_async_interface(
f"{ofnm}.Device.Wireless"
)
ap = await nmw.ActiveAccessPoint
if not ap:
status.append(icons["nowifi"])
continue
network_manager_status.active_ap = ap
nmap = await bus[ap].get_async_interface(f"{ofnm}.AccessPoint")
name = await nmap.Ssid
strength = int(await nmap.Strength)
status.append(
[
icons["wifi-low"],
icons["wifi-medium"],
icons["wifi-high"],
][strength // 34]
)
status.append(
bytes(name)
.decode("utf-8", errors="replace")
.replace("%", "%%")
)
elif (
kind == NM_DEVICE_TYPE_ETHERNET
and state == NM_DEVICE_STATE_ACTIVATED
):
status.append(icons["wired"])
# Build status for VPN connection
connections = await nm.ActiveConnections
for conn in connections:
nma = await bus[conn].get_async_interface(
f"{ofnm}.Connection.Active"
) )
vpn = await nma.Vpn ap = await nmw.ActiveAccessPoint
if vpn: if not ap:
state = await nma.State status.append(icons["nowifi"])
if state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED: continue
status.append(icons["vpn"]) network_manager_status.active_ap = ap
status.append(await nma.Id) nmap = await bus[ap].get_async_interface(f"{ofnm}.AccessPoint")
name = await nmap.Ssid
strength = int(await nmap.Strength)
status.append(
[
icons["wifi-low"],
icons["wifi-medium"],
icons["wifi-high"],
][strength // 34]
)
status.append(
bytes(name)
.decode("utf-8", errors="replace")
.replace("%", "%%")
)
elif (
kind == NM_DEVICE_TYPE_ETHERNET
and state == NM_DEVICE_STATE_ACTIVATED
):
status.append(icons["wired"])
# Final status line # Build status for VPN connection
status = " ".join(status) connections = await nm.ActiveConnections
last = getattr(network_manager_status, "last", None) for conn in connections:
nma = await bus[conn].get_async_interface(f"{ofnm}.Connection.Active")
vpn = await nma.Vpn
if vpn:
state = await nma.State
if state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
status.append(icons["vpn"])
status.append(await nma.Id)
if status != last: # Final status line
logger.info("updated network status") status = " ".join(status)
last = getattr(network_manager_status, "last", None)
# Update cache file (for when polybar restarts) if status != last:
with open( logger.info("updated network status")
f"{os.getenv('XDG_RUNTIME_DIR')}/i3/network.txt", "w"
) as out:
out.write(status)
# Send it to polybar's module/network # Update cache file (for when polybar restarts)
for name in glob.glob("/tmp/polybar_mqueue.*"): with open(
try: f"{os.getenv('XDG_RUNTIME_DIR')}/i3/network.txt", "w"
with open( ) as out:
os.open(name, os.O_WRONLY | os.O_NONBLOCK), "w" out.write(status)
) as out:
cmd = f"action:#network.send.{status}"
out.write(cmd)
except OSError as e:
if e.errno != errno.ENXIO:
raise
network_manager_status.last = status # Send it to polybar's module/network
for name in glob.glob("/tmp/polybar_mqueue.*"):
try:
with open(
os.open(name, os.O_WRONLY | os.O_NONBLOCK), "w"
) as out:
cmd = f"action:#network.send.{status}"
out.write(cmd)
except OSError as e:
if e.errno != errno.ENXIO:
raise
if ( network_manager_status.last = status
isinstance(event, DBusSignal)
and event.interface == f"{ofnm}.Connection.Active"
):
await network_manager_status_now(0)
else:
network_manager_status.running = asyncio.create_task(
network_manager_status_now()
)
async def main(options): async def main(options):