Skip to content

Commit d065e48

Browse files
committed
feat: dbus stats tool
1 parent d0956fe commit d065e48

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

tools/dev-dbus-stats.py

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/bin/bash
2+
"true" '''\'
3+
set -eu
4+
set -o pipefail
5+
6+
FILE="$(readlink -f "$0")"
7+
ROOT_DIR="$(dirname "$FILE")"
8+
9+
if [ "$EUID" -eq 0 ]; then
10+
echo "NB: Running as root, skipping tool setup check."
11+
else
12+
"$ROOT_DIR/setup-tool-env.bash"
13+
fi
14+
15+
"$ROOT_DIR/.venv/bin/python" "$FILE" "$@"
16+
17+
exit 0 # required to stop shell execution here
18+
'''
19+
20+
# Basically ripped off some other script from the internet.
21+
# Can't remember where, but claim no credit.
22+
23+
import asyncio
24+
from dataclasses import dataclass
25+
from typing import Dict, List, Optional, Set
26+
27+
from dbus_fast import BusType
28+
import typed_argparse as tap
29+
from dbus_fast.aio.message_bus import MessageBus
30+
from dbus_fast.signature import Variant
31+
32+
33+
def get_cmdline(pid: int):
34+
if pid <= 0:
35+
return "<bad PID>"
36+
37+
try:
38+
procpath = '/proc/' + str(pid) + '/cmdline'
39+
with open(procpath, 'r') as f:
40+
return " ".join(f.readline().split('\0'))
41+
except:
42+
return "<err>"
43+
44+
45+
class Args(tap.TypedArgs):
46+
show_all: bool = tap.arg(
47+
help="show all connections instead of hiding those with flushed buffers"
48+
)
49+
system: bool = tap.arg(
50+
help="connect to system bus instead of session bus", default=False
51+
)
52+
53+
54+
@dataclass
55+
class ProcRecord:
56+
wkn: List[str]
57+
pid: int
58+
cmd: str
59+
stats: Optional[Dict[str, Variant]]
60+
61+
62+
# Parsing parameters
63+
async def _main(args: Args):
64+
bus = MessageBus(bus_type=BusType.SYSTEM if args.system else BusType.SESSION)
65+
await bus.connect()
66+
67+
introspect = await bus.introspect("org.freedesktop.DBus", "/org/freedesktop/DBus")
68+
remote_object = bus.get_proxy_object(
69+
"org.freedesktop.DBus", "/org/freedesktop/DBus", introspect
70+
)
71+
bus_iface = remote_object.get_interface("org.freedesktop.DBus")
72+
stats_iface = remote_object.get_interface("org.freedesktop.DBus.Debug.Stats")
73+
74+
async def get_stats(conn: str) -> Optional[Dict[str, Variant]]:
75+
try:
76+
return await stats_iface.call_get_connection_stats(conn)
77+
except:
78+
# failed: did you enable the Stats interface? (compilation option: --enable-stats)
79+
# https://bugs.freedesktop.org/show_bug.cgi?id=80759 would be nice too
80+
return None
81+
82+
names: Set[str] = set(await bus_iface.call_list_names())
83+
unique_names = {a for a in names if a.startswith(":")}
84+
well_known_names = names - unique_names
85+
owners = {
86+
name: await bus_iface.call_get_name_owner(name) for name in well_known_names
87+
}
88+
89+
async def fetch_info(conn: str):
90+
pid: int = await bus_iface.call_get_connection_unix_process_id(conn)
91+
return ProcRecord(
92+
wkn=sorted(k for k, v in owners.items() if v == conn),
93+
pid=pid,
94+
cmd=get_cmdline(pid),
95+
stats=await get_stats(conn),
96+
)
97+
98+
def is_boring(x: ProcRecord):
99+
if "klipper" in x.cmd:
100+
return False
101+
102+
if x.stats is None:
103+
return True
104+
105+
return (
106+
x.stats['IncomingBytes'].value == 0 and x.stats['OutgoingBytes'].value == 0
107+
)
108+
109+
stats = [(name, await fetch_info(name)) for name in unique_names]
110+
stats.sort(key=lambda kv: (kv[1].pid, kv[0]))
111+
112+
for k, v in stats:
113+
# hide boring connections
114+
if not args.show_all and is_boring(v):
115+
continue
116+
117+
print(f"{k} with pid {v.pid} '{v.cmd}':")
118+
print(f" Names: {' '.join(v.wkn)}")
119+
120+
if v.stats is not None:
121+
print(f" IncomingBytes: {v.stats['IncomingBytes'].value:>10}")
122+
print(f" PeakIncomingBytes: {v.stats['PeakIncomingBytes'].value:>10}")
123+
print(f" OutgoingBytes: {v.stats['OutgoingBytes'].value:>10}")
124+
print(f" PeakOutgoingBytes: {v.stats['PeakOutgoingBytes'].value:>10}")
125+
else:
126+
print(" No stats available.")
127+
128+
print("")
129+
130+
131+
def main():
132+
def go(args: Args):
133+
asyncio.run(_main(args))
134+
135+
tap.Parser(Args).bind(go).run()
136+
137+
138+
if __name__ == "__main__":
139+
main()

0 commit comments

Comments
 (0)