forked from ArduPilot/SupportProxy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconntdb_lib.py
More file actions
155 lines (130 loc) · 4.98 KB
/
Copy pathconntdb_lib.py
File metadata and controls
155 lines (130 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""
Reader for connections.tdb — the live per-connection state the
supportproxy children mirror out via the heartbeat fork-and-write idiom.
This module has no Flask dependency so the keydb.py CLI and the
webadmin Flask app can both use it. webadmin/connections.py is the
thin Flask wrapper that resolves the path from app config and
delegates here.
Schema mirrors `struct ConnEntry` in conntdb.h. Forward-compatible:
records of size >= CONNENTRY_MIN_SIZE are accepted; trailing bytes
from a newer C++ schema are ignored on read.
"""
import errno
import os
import socket
import struct
import time
import keydb_lib
CONN_FILE = 'connections.tdb'
CONN_MAGIC = 0x436f6e6e45424553 # "ConnEBES"
# Pre-flags layout was 64 bytes (matches sizeof(ConnEntry) at the time
# of this writing). Anything smaller is invalid; trailing bytes from a
# newer schema are ignored.
CONNENTRY_MIN_SIZE = 64
# struct ConnEntry layout (little-endian, natural alignment):
# QQQ magic, connected_at, last_update (24)
# ii port2, conn_index ( 8)
# III pid, rx_msgs, tx_msgs (12)
# I peer_ip_be ( 4)
# HBB peer_port_be, transport, is_user ( 4)
# I flags ( 4)
# I _pad ( 4)
# Raw: 60 bytes. C++ rounds sizeof() up to 64 to align the next
# instance at an 8-byte boundary (alignof(uint64_t)). Add 4 explicit
# pad bytes here so the on-disk size matches.
PACK_FORMAT = "<QQQiiIIIIHBBII4x"
CONNENTRY_CURRENT_SIZE = struct.calcsize(PACK_FORMAT)
assert CONNENTRY_CURRENT_SIZE == 64, CONNENTRY_CURRENT_SIZE
# struct ConnKey { int port2; int conn_index; }
KEY_FORMAT = "<ii"
TRANSPORT_NAMES = {
0: 'udp',
1: 'tcp',
2: 'ws',
3: 'wss',
}
class ConnEntry:
__slots__ = ('magic', 'connected_at', 'last_update',
'port2', 'conn_index', 'pid',
'rx_msgs', 'tx_msgs',
'peer_ip_be', 'peer_port_be',
'transport', 'is_user', 'flags')
def __init__(self):
for s in self.__slots__:
setattr(self, s, 0)
@classmethod
def unpack(cls, data):
if len(data) < CONNENTRY_MIN_SIZE:
raise ValueError("record too small: %d bytes" % len(data))
if len(data) < CONNENTRY_CURRENT_SIZE:
data = data + b'\x00' * (CONNENTRY_CURRENT_SIZE - len(data))
body = data[:CONNENTRY_CURRENT_SIZE]
ce = cls()
(ce.magic, ce.connected_at, ce.last_update,
ce.port2, ce.conn_index, ce.pid,
ce.rx_msgs, ce.tx_msgs,
ce.peer_ip_be, ce.peer_port_be,
ce.transport, ce.is_user,
ce.flags, _pad) = struct.unpack(PACK_FORMAT, body)
return ce
@property
def transport_name(self):
return TRANSPORT_NAMES.get(self.transport, str(self.transport))
@property
def peer_ip(self):
return socket.inet_ntoa(struct.pack("<I", self.peer_ip_be))
@property
def peer_port(self):
return socket.ntohs(self.peer_port_be)
@property
def peer(self):
return "%s:%d" % (self.peer_ip, self.peer_port)
def uptime_s(self, now=None):
if now is None:
now = time.time()
return max(0, int(now) - int(self.connected_at))
def age_s(self, now=None):
if now is None:
now = time.time()
return int(now) - int(self.last_update)
def conn_path_for(keydb_path):
"""connections.tdb sits in the same directory as keys.tdb."""
keydb_dir = os.path.dirname(os.path.abspath(keydb_path)) or '.'
return os.path.join(keydb_dir, CONN_FILE)
def iter_active(path, now=None, max_age_s=30):
"""Yield ConnEntry records currently in connections.tdb at ``path``.
Records older than ``max_age_s`` (last_update too far in the past)
are skipped — defence in depth against orphans the supportproxy parent
failed to clean up. Returns nothing if the file is missing.
"""
if not os.path.exists(path):
return
try:
db = keydb_lib.open_db(path)
except OSError as e:
if e.errno in (errno.ENOENT, errno.EACCES):
return
raise
try:
if now is None:
now = time.time()
k = db.firstkey()
while k is not None:
v = db.get(k)
if (v is not None and len(v) >= CONNENTRY_MIN_SIZE
and len(k) == struct.calcsize(KEY_FORMAT)):
try:
ce = ConnEntry.unpack(v)
except (ValueError, struct.error):
ce = None
if ce is not None and ce.magic == CONN_MAGIC:
if int(now) - int(ce.last_update) <= max_age_s:
yield ce
k = db.nextkey(k)
finally:
db.close()
def list_active(path, **kw):
"""Sorted list of active records (by port2, conn_index)."""
out = list(iter_active(path, **kw))
out.sort(key=lambda c: (c.port2, c.conn_index))
return out