i3-companion: replace network module by custom IPC for Network Manager

This commit is contained in:
Vincent Bernat 2021-07-12 07:32:39 +02:00
parent b3c386b778
commit 7df5011824
3 changed files with 176 additions and 33 deletions

View file

@ -3,16 +3,20 @@
"""Personal i3 companion."""
import argparse
import asyncio
import collections
import errno
import fcntl
import functools
import glob
import html
import logging
import logging.handlers
import sys
import os
import re
import asyncio
import shlex
import subprocess
import html
import functools
import collections
import sys
from i3ipc.aio import Connection
from i3ipc import Event
@ -25,6 +29,15 @@ logger = logging.getLogger("i3-companion")
DBusSignal = collections.namedtuple(
"DBusSignal", ["path", "interface", "member", "signature"]
)
StartEvent = object()
NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
NM_DEVICE_TYPE_ETHERNET = 1
NM_DEVICE_TYPE_WIFI = 2
NM_DEVICE_TYPE_MODEM = 8
NM_DEVICE_STATE_UNMANAGED = 10
NM_DEVICE_STATE_ACTIVATED = 100
def on(*events):
@ -46,7 +59,9 @@ async def notify(i3, **kwargs):
"""Send a notification with notify-send."""
peer = i3.session_bus["org.freedesktop.Notifications"]
peer = peer["/org/freedesktop/Notifications"]
interface = await peer.get_async_interface("org.freedesktop.Notifications")
notifications = await peer.get_async_interface(
"org.freedesktop.Notifications"
)
parameters = dict(
app_name=logger.name,
replaces_id=0,
@ -57,7 +72,7 @@ async def notify(i3, **kwargs):
expire_timeout=5000,
)
parameters.update(kwargs)
return await interface.Notify(**parameters)
return await notifications.Notify(**parameters)
# See https://fontawesome.com/v5.15/icons
@ -355,8 +370,11 @@ async def output_update(i3, event):
running.cancel()
output_update.running = None
def output_update_now():
"""Execute actions to react to XRandR change."""
async def output_update_now():
try:
await asyncio.sleep(2)
except asyncio.CancelledError:
return
output_update.running = None
logger.info("XRandR change detected")
@ -370,9 +388,7 @@ async def output_update(i3, event):
logger.warning(f"{cmd} exited with {proc.returncode}")
logger.debug("schedule XRandR change")
output_update.running = asyncio.get_event_loop().call_later(
2, output_update_now
)
output_update.running = asyncio.create_task(output_update_now())
@on(
@ -384,22 +400,22 @@ async def output_update(i3, event):
)
)
async def network_manager_notifications(i3, event, path, state, reason):
logger.debug("from %s state: %d, reason: %d", path, state, reason)
NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
"""Display notifications related to Network Manager state."""
logger.debug(f"from {path} state: {state}, reason: {reason}")
if state not in {NM_ACTIVE_CONNECTION_STATE_ACTIVATED}:
# We cannot get proper state unless the connection is
# activated, unless we maintain state.
return
peer = i3.system_bus["org.freedesktop.NetworkManager"][path]
try:
interface = await peer.get_async_interface(
nmca = await peer.get_async_interface(
"org.freedesktop.NetworkManager.Connection.Active"
)
except dbussy.DBusError:
logger.info("interface %s has vanished", path)
logger.info(f"interface {path} has vanished")
return
kind = await interface.Type
id = await interface.Id
kind = await nmca.Type
id = await nmca.Id
if kind == "vpn":
await notify(
i3, app_icon="network-vpn", summary=f"{id}", body="VPN connected!"
@ -420,6 +436,135 @@ async def network_manager_notifications(i3, event, path, state, reason):
)
@on(
StartEvent,
DBusSignal(
path="/",
interface="org.freedesktop.NetworkManager.Connection.Active",
member="StateChanged",
signature="uu",
),
DBusSignal(
path="/",
interface="org.freedesktop.NetworkManager.AccessPoint",
member="PropertiesChanged",
signature="a{sv}",
),
)
async def network_manager_status(i3, event, *args):
"""Compute network manager status."""
if isinstance(event, DBusSignal) and event.interface == "org.freedesktop.NetworkManager.AccessPoint":
path, props = args
if getattr(network_manager_status, "active_ap", None) != path:
return
if not "Strength" in props:
return
running = getattr(network_manager_status, "running", None)
if running is not None:
running.cancel()
network_manager_status.running = None
async def network_manager_status_now(sleep=1):
try:
await asyncio.sleep(sleep)
except asyncio.CancelledError:
return
network_manager_status.running = None
try:
await network_manager_status__now()
except Exception as e:
logger.exception("while updating network status: %s", 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["org.freedesktop.NetworkManager"]
nm = await bus["/org/freedesktop/NetworkManager"].get_async_interface(
"org.freedesktop.NetworkManager"
)
devices = await nm.AllDevices
for device in devices:
nmd = await bus[device].get_async_interface(
"org.freedesktop.NetworkManager.Device"
)
kind = await nmd.DeviceType
state = await nmd.State
if state == NM_DEVICE_STATE_UNMANAGED:
continue
if kind == NM_DEVICE_TYPE_WIFI:
if state != NM_DEVICE_STATE_ACTIVATED:
status.append("")
continue
nmw = await bus[device].get_async_interface(
"org.freedesktop.NetworkManager.Device.Wireless"
)
ap = await nmw.ActiveAccessPoint
if not ap:
status.append("")
continue
network_manager_status.active_ap = ap
nmap = await bus[ap].get_async_interface(
"org.freedesktop.NetworkManager.AccessPoint"
)
name = await nmap.Ssid
strength = int(await nmap.Strength)
status.append(""[strength // 34])
status.append(bytes(name).decode("utf-8"))
elif (
kind == NM_DEVICE_TYPE_ETHERNET
and state == NM_DEVICE_STATE_ACTIVATED
):
status.append("")
# Build status for VPN connection
connections = await nm.ActiveConnections
for conn in connections:
nma = await bus[conn].get_async_interface(
"org.freedesktop.NetworkManager.Connection.Active"
)
vpn = await nma.Vpn
if vpn:
state = await nma.State
if state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
status.append("︁")
status.append(await nma.Id)
# Final status line
status = " ".join(status).replace("%", "%%")
last = getattr(network_manager_status, "last", None)
if status != last:
logger.info(f"network status: {status}")
network_manager_status.last = status
# Update cache file (for when polybar restarts)
with open(f"{os.getenv('XDG_RUNTIME_DIR')}/i3/network.txt", "w") as out:
out.write(status)
# Send it to module/network
for name in glob.glob("/tmp/polybar_mqueue.*"):
try:
with open(name, "w") as out:
fd = out.fileno()
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
cmd = f"action:#network.send.{status}"
out.write(cmd)
except OSError as e:
if e.errno == errno.ENXIO:
pass
network_manager_status.running = asyncio.create_task(
network_manager_status_now()
)
async def main(options):
i3 = await Connection().connect()
i3.session_bus = await ravel.session_bus_async()
@ -478,6 +623,12 @@ async def main(options):
func=wrapping(fn, event),
)
# Run events that should run on start
for fn, events in on.functions.items():
for event in events:
if event is StartEvent:
asyncio.create_task(fn(i3, event))
await i3.main()
@ -507,7 +658,7 @@ if __name__ == "__main__":
root.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER=logger.name))
try:
asyncio.get_event_loop().run_until_complete(main(options))
asyncio.run(main(options))
except Exception as e:
logger.exception("%s", e)
sys.exit(1)

View file

@ -1,7 +1,5 @@
#!/bin/sh
export LAN=$(nmcli -t device | awk -F: '($2 == "ethernet") {print $1; exit}')
export WLAN=$(nmcli -t device | awk -F: '($2 == "wifi") {print $1; exit}')
export DPI=$(xrdb -query | sed -nE 's/^Xft\.dpi:\s*//p')
export HEIGHT=$((20 * DPI / 96))

View file

@ -31,11 +31,11 @@ modules-center = date
[bar/alone]
inherit = bar/common
modules-right = cpu memory brightness battery ethernet wlan disk pulseaudio
modules-right = cpu memory brightness battery network disk pulseaudio
[bar/primary]
inherit = bar/common
modules-right = cpu memory brightness battery ethernet wlan disk pulseaudio
modules-right = cpu memory brightness battery network disk pulseaudio
[bar/secondary]
inherit = bar/common
@ -98,16 +98,10 @@ type = internal/memory
interval = 5
label =  %percentage_used: 2%%
[module/wlan]
type = internal/network
interface = ${env:WLAN:}
interval = 5
format-connected = <ramp-signal> <label-connected>
label-connected = %essid%
format-disconnected = 
ramp-signal-0 = 
ramp-signal-1 = 
ramp-signal-2 = 
[module/network]
type = custom/ipc
hook-0 = cat $XDG_RUNTIME_DIR/i3/network.txt
initial = 1
[module/ethernet]
type = internal/network