mirror of
https://github.com/vincentbernat/i3wm-configuration.git
synced 2025-06-26 03:38:33 +02:00
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:
parent
fb0bd7fcca
commit
2012ba0c15
1 changed files with 140 additions and 137 deletions
277
bin/i3-companion
277
bin/i3-companion
|
@ -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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue