tty-egpf-monitor 0.5.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ """
2
+ TTY eBPF Monitor Python Client Library
3
+
4
+ A Python client library for interacting with the tty-egpf-monitord daemon.
5
+ Provides a clean, Pythonic interface to the HTTP+JSON API over Unix domain sockets.
6
+ """
7
+
8
+ from .client import TTYMonitorClient, TTYMonitorError
9
+ from .models import Port, LogEntry
10
+
11
+ __version__ = "0.5.26"
12
+ __author__ = "TTY eBPF Monitor Team"
13
+ __license__ = "GPL-3.0"
14
+
15
+ __all__ = [
16
+ "TTYMonitorClient",
17
+ "TTYMonitorError",
18
+ "Port",
19
+ "LogEntry",
20
+ ]
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TTY eBPF Monitor CLI
4
+
5
+ Command-line interface for the tty-egpf-monitor daemon using the Python client library.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ import json
11
+ from typing import Optional
12
+ from .client import TTYMonitorClient, TTYMonitorError
13
+
14
+
15
+ def cmd_add(client: TTYMonitorClient, args: argparse.Namespace) -> int:
16
+ """Add a port to monitor."""
17
+ try:
18
+ idx = client.add_port(args.device, args.baudrate, args.logfile)
19
+ print(json.dumps({"idx": idx}))
20
+ return 0
21
+ except TTYMonitorError as e:
22
+ print(f"Error: {e}", file=sys.stderr)
23
+ return 1
24
+
25
+
26
+ def cmd_list(client: TTYMonitorClient, args: argparse.Namespace) -> int:
27
+ """List configured ports."""
28
+ try:
29
+ ports = client.list_ports()
30
+ ports_data = [{"idx": p.idx, "dev": p.device} for p in ports]
31
+ print(json.dumps(ports_data))
32
+ return 0
33
+ except TTYMonitorError as e:
34
+ print(f"Error: {e}", file=sys.stderr)
35
+ return 1
36
+
37
+
38
+ def cmd_logs(client: TTYMonitorClient, args: argparse.Namespace) -> int:
39
+ """Download full log for a port."""
40
+ try:
41
+ # Parse port identifier (int or string)
42
+ port_id = args.port
43
+ try:
44
+ port_id = int(port_id)
45
+ except ValueError:
46
+ pass # Keep as string (device path)
47
+
48
+ logs = client.get_logs(port_id)
49
+ print(logs, end='') # Don't add extra newline, logs already have them
50
+ return 0
51
+ except TTYMonitorError as e:
52
+ print(f"Error: {e}", file=sys.stderr)
53
+ return 1
54
+
55
+
56
+ def cmd_stream(client: TTYMonitorClient, args: argparse.Namespace) -> int:
57
+ """Live stream logs for a port."""
58
+ try:
59
+ # Parse port identifier (int or string)
60
+ port_id = args.port
61
+ try:
62
+ port_id = int(port_id)
63
+ except ValueError:
64
+ pass # Keep as string (device path)
65
+
66
+ for line in client.stream_logs(port_id):
67
+ print(line)
68
+ sys.stdout.flush()
69
+
70
+ return 0
71
+ except KeyboardInterrupt:
72
+ return 0
73
+ except TTYMonitorError as e:
74
+ print(f"Error: {e}", file=sys.stderr)
75
+ return 1
76
+
77
+
78
+ def cmd_remove(client: TTYMonitorClient, args: argparse.Namespace) -> int:
79
+ """Remove a port from monitoring."""
80
+ try:
81
+ # Parse port identifier (int or string)
82
+ port_id = args.port
83
+ try:
84
+ port_id = int(port_id)
85
+ except ValueError:
86
+ pass # Keep as string (device path)
87
+
88
+ success = client.remove_port(port_id)
89
+ if success:
90
+ print(json.dumps({"ok": True}))
91
+ return 0
92
+ else:
93
+ print("Error: Remove failed", file=sys.stderr)
94
+ return 1
95
+ except TTYMonitorError as e:
96
+ print(f"Error: {e}", file=sys.stderr)
97
+ return 1
98
+
99
+
100
+ def main() -> int:
101
+ """Main CLI entry point."""
102
+ parser = argparse.ArgumentParser(
103
+ description="TTY eBPF Monitor Python CLI",
104
+ formatter_class=argparse.RawDescriptionHelpFormatter,
105
+ epilog="""
106
+ Examples:
107
+ tty-egpf-monitor-py add /dev/ttyUSB0 115200
108
+ tty-egpf-monitor-py list
109
+ tty-egpf-monitor-py stream /dev/ttyUSB0
110
+ tty-egpf-monitor-py logs 0
111
+ tty-egpf-monitor-py remove /dev/ttyUSB0
112
+
113
+ For more information, see: https://github.com/seelso-net/tty-egpf-monitor
114
+ """
115
+ )
116
+
117
+ parser.add_argument(
118
+ "--socket",
119
+ default="/run/tty-egpf-monitord.sock",
120
+ help="Path to daemon socket (default: %(default)s)"
121
+ )
122
+
123
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
124
+
125
+ # Add command
126
+ add_parser = subparsers.add_parser("add", help="Add a monitored port")
127
+ add_parser.add_argument("device", help="Device path (e.g., /dev/ttyUSB0)")
128
+ add_parser.add_argument("baudrate", nargs="?", type=int, default=115200, help="Baud rate (default: 115200)")
129
+ add_parser.add_argument("logfile", nargs="?", help="Custom log file path (optional)")
130
+
131
+ # List command
132
+ subparsers.add_parser("list", help="List configured ports")
133
+
134
+ # Logs command
135
+ logs_parser = subparsers.add_parser("logs", help="Download full log for a port")
136
+ logs_parser.add_argument("port", help="Port index or device path")
137
+
138
+ # Stream command
139
+ stream_parser = subparsers.add_parser("stream", help="Live stream logs for a port")
140
+ stream_parser.add_argument("port", help="Port index or device path")
141
+
142
+ # Remove command
143
+ remove_parser = subparsers.add_parser("remove", help="Remove a port from monitoring")
144
+ remove_parser.add_argument("port", help="Port index or device path")
145
+
146
+ args = parser.parse_args()
147
+
148
+ if not args.command:
149
+ parser.print_help()
150
+ return 2
151
+
152
+ # Create client
153
+ client = TTYMonitorClient(args.socket)
154
+
155
+ # Execute command
156
+ if args.command == "add":
157
+ return cmd_add(client, args)
158
+ elif args.command == "list":
159
+ return cmd_list(client, args)
160
+ elif args.command == "logs":
161
+ return cmd_logs(client, args)
162
+ elif args.command == "stream":
163
+ return cmd_stream(client, args)
164
+ elif args.command == "remove":
165
+ return cmd_remove(client, args)
166
+ else:
167
+ parser.print_help()
168
+ return 2
169
+
170
+
171
+ if __name__ == "__main__":
172
+ sys.exit(main())
@@ -0,0 +1,324 @@
1
+ """
2
+ TTY eBPF Monitor Python Client
3
+
4
+ Provides a Python interface to the tty-egpf-monitord daemon via Unix domain socket.
5
+ """
6
+
7
+ import json
8
+ import socket
9
+ import time
10
+ from typing import List, Optional, Iterator, Dict, Any, Union
11
+ from datetime import datetime
12
+ from .models import Port, LogEntry
13
+
14
+
15
+ class TTYMonitorError(Exception):
16
+ """Exception raised for TTY Monitor API errors."""
17
+ pass
18
+
19
+
20
+ class TTYMonitorClient:
21
+ """Client for interacting with tty-egpf-monitord daemon."""
22
+
23
+ def __init__(self, socket_path: str = "/run/tty-egpf-monitord.sock"):
24
+ """
25
+ Initialize the client.
26
+
27
+ Args:
28
+ socket_path: Path to the daemon's Unix domain socket
29
+ """
30
+ self.socket_path = socket_path
31
+
32
+ def _send_http_request(self, method: str, path: str, body: Optional[str] = None) -> str:
33
+ """Send HTTP request over Unix domain socket and return response body."""
34
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
35
+ try:
36
+ sock.connect(self.socket_path)
37
+
38
+ # Build HTTP request
39
+ if body:
40
+ request = (
41
+ f"{method} {path} HTTP/1.1\r\n"
42
+ f"Host: localhost\r\n"
43
+ f"Content-Type: application/json\r\n"
44
+ f"Content-Length: {len(body)}\r\n"
45
+ f"Connection: close\r\n"
46
+ f"\r\n"
47
+ f"{body}"
48
+ )
49
+ else:
50
+ request = (
51
+ f"{method} {path} HTTP/1.1\r\n"
52
+ f"Host: localhost\r\n"
53
+ f"Connection: close\r\n"
54
+ f"\r\n"
55
+ )
56
+
57
+ sock.sendall(request.encode())
58
+
59
+ # Read response
60
+ response = b""
61
+ while True:
62
+ chunk = sock.recv(4096)
63
+ if not chunk:
64
+ break
65
+ response += chunk
66
+
67
+ response_str = response.decode('utf-8')
68
+
69
+ # Parse HTTP response
70
+ if '\r\n\r\n' not in response_str:
71
+ raise TTYMonitorError("Invalid HTTP response")
72
+
73
+ headers, body = response_str.split('\r\n\r\n', 1)
74
+
75
+ # Check status code
76
+ status_line = headers.split('\r\n')[0]
77
+ if not status_line.startswith('HTTP/1.1 2'):
78
+ # Extract error message from body if available
79
+ error_msg = body.strip() if body else "Request failed"
80
+ raise TTYMonitorError(f"HTTP error: {status_line} - {error_msg}")
81
+
82
+ return body
83
+
84
+ except socket.error as e:
85
+ raise TTYMonitorError(f"Socket error: {e}")
86
+ finally:
87
+ sock.close()
88
+
89
+ def list_ports(self) -> List[Port]:
90
+ """
91
+ List all configured ports.
92
+
93
+ Returns:
94
+ List of Port objects
95
+ """
96
+ body = self._send_http_request("GET", "/ports")
97
+ try:
98
+ ports_data = json.loads(body)
99
+ return [Port.from_dict(port_data) for port_data in ports_data]
100
+ except json.JSONDecodeError as e:
101
+ raise TTYMonitorError(f"Invalid JSON response: {e}")
102
+
103
+ def add_port(self, device: str, baudrate: int = 115200, log_path: Optional[str] = None) -> int:
104
+ """
105
+ Add a port to monitor.
106
+
107
+ Args:
108
+ device: Device path (e.g., "/dev/ttyUSB0")
109
+ baudrate: Baud rate (default: 115200)
110
+ log_path: Custom log path (optional)
111
+
112
+ Returns:
113
+ Port index
114
+ """
115
+ request_data = {
116
+ "dev": device,
117
+ "log": log_path or "",
118
+ "baudrate": baudrate
119
+ }
120
+
121
+ body = self._send_http_request("POST", "/ports", json.dumps(request_data))
122
+ try:
123
+ response = json.loads(body)
124
+ idx = response["idx"]
125
+ if not isinstance(idx, int):
126
+ raise TTYMonitorError(f"Invalid idx type: {type(idx)}")
127
+ return idx
128
+ except (json.JSONDecodeError, KeyError) as e:
129
+ raise TTYMonitorError(f"Invalid response: {e}")
130
+
131
+ def remove_port(self, port_identifier: Union[str, int]) -> bool:
132
+ """
133
+ Remove a port from monitoring.
134
+
135
+ Args:
136
+ port_identifier: Port index (int) or device path (str)
137
+
138
+ Returns:
139
+ True if successful
140
+ """
141
+ if isinstance(port_identifier, int):
142
+ # Remove by index
143
+ body = self._send_http_request("DELETE", f"/ports/{port_identifier}")
144
+ else:
145
+ # Remove by device path
146
+ request_data = {"dev": str(port_identifier)}
147
+ body = self._send_http_request("DELETE", "/ports", json.dumps(request_data))
148
+
149
+ try:
150
+ response = json.loads(body)
151
+ ok_value = response.get("ok", False)
152
+ return bool(ok_value)
153
+ except json.JSONDecodeError:
154
+ return False
155
+
156
+ def get_logs(self, port_identifier: Union[str, int]) -> str:
157
+ """
158
+ Download full log for a port.
159
+
160
+ Args:
161
+ port_identifier: Port index (int) or device path (str)
162
+
163
+ Returns:
164
+ Raw log content
165
+ """
166
+ if isinstance(port_identifier, str):
167
+ # Resolve device path to index
168
+ ports = self.list_ports()
169
+ idx = None
170
+ for port in ports:
171
+ if port.device == port_identifier:
172
+ idx = port.idx
173
+ break
174
+ if idx is None:
175
+ raise TTYMonitorError(f"Device not found: {port_identifier}")
176
+ else:
177
+ idx = port_identifier
178
+
179
+ return self._send_http_request("GET", f"/logs/{idx}")
180
+
181
+ def parse_logs(self, log_content: str) -> List[LogEntry]:
182
+ """
183
+ Parse log content into LogEntry objects.
184
+
185
+ Args:
186
+ log_content: Raw log content from get_logs()
187
+
188
+ Returns:
189
+ List of parsed LogEntry objects
190
+ """
191
+ entries = []
192
+ for line in log_content.strip().split('\n'):
193
+ if line.strip():
194
+ try:
195
+ entry = LogEntry.parse_simple_log(line)
196
+ entries.append(entry)
197
+ except Exception:
198
+ # If parsing fails, create a basic entry with raw line
199
+ entries.append(LogEntry(
200
+ timestamp=datetime.now(),
201
+ event_type="UNPARSED",
202
+ process="",
203
+ raw_line=line.strip()
204
+ ))
205
+ return entries
206
+
207
+ def stream_logs(self, port_identifier: Union[str, int]) -> Iterator[str]:
208
+ """
209
+ Stream live logs for a port.
210
+
211
+ Args:
212
+ port_identifier: Port index (int) or device path (str)
213
+
214
+ Yields:
215
+ Log lines as they arrive
216
+ """
217
+ if isinstance(port_identifier, str):
218
+ # Resolve device path to index
219
+ ports = self.list_ports()
220
+ idx = None
221
+ for port in ports:
222
+ if port.device == port_identifier:
223
+ idx = port.idx
224
+ break
225
+ if idx is None:
226
+ raise TTYMonitorError(f"Device not found: {port_identifier}")
227
+ else:
228
+ idx = port_identifier
229
+
230
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
231
+ try:
232
+ sock.connect(self.socket_path)
233
+
234
+ # Send streaming request
235
+ request = (
236
+ f"GET /stream/{idx} HTTP/1.1\r\n"
237
+ f"Host: localhost\r\n"
238
+ f"Connection: close\r\n"
239
+ f"\r\n"
240
+ )
241
+ sock.sendall(request.encode())
242
+
243
+ # Read response headers
244
+ headers = b""
245
+ while b'\r\n\r\n' not in headers:
246
+ chunk = sock.recv(1)
247
+ if not chunk:
248
+ raise TTYMonitorError("Connection closed while reading headers")
249
+ headers += chunk
250
+
251
+ # Check status
252
+ headers_str = headers.decode('utf-8')
253
+ if not headers_str.startswith('HTTP/1.1 200'):
254
+ raise TTYMonitorError(f"Stream failed: {headers_str.split()[1]}")
255
+
256
+ # Read chunked response
257
+ buffer = b""
258
+ while True:
259
+ chunk = sock.recv(4096)
260
+ if not chunk:
261
+ break
262
+
263
+ buffer += chunk
264
+
265
+ # Process complete lines
266
+ while b'\n' in buffer:
267
+ line, buffer = buffer.split(b'\n', 1)
268
+ line_str = line.decode('utf-8', errors='replace').strip()
269
+
270
+ # Skip HTTP chunked encoding artifacts
271
+ if line_str and not line_str.isdigit() and line_str != '0':
272
+ # Skip chunked encoding hex numbers and empty lines
273
+ if not all(c in '0123456789abcdefABCDEF' for c in line_str):
274
+ yield line_str
275
+
276
+ except socket.error as e:
277
+ raise TTYMonitorError(f"Socket error: {e}")
278
+ finally:
279
+ sock.close()
280
+
281
+ def stream_parsed_logs(self, port_identifier: Union[str, int]) -> Iterator[LogEntry]:
282
+ """
283
+ Stream live logs for a port, parsed into LogEntry objects.
284
+
285
+ Args:
286
+ port_identifier: Port index (int) or device path (str)
287
+
288
+ Yields:
289
+ Parsed LogEntry objects as they arrive
290
+ """
291
+ for line in self.stream_logs(port_identifier):
292
+ try:
293
+ yield LogEntry.parse_simple_log(line)
294
+ except Exception:
295
+ # If parsing fails, yield raw entry
296
+ yield LogEntry(
297
+ timestamp=datetime.now(),
298
+ event_type="UNPARSED",
299
+ process="",
300
+ raw_line=line
301
+ )
302
+
303
+ def wait_for_event(self, port_identifier: Union[str, int], event_type: str, timeout: float = 30.0) -> Optional[LogEntry]:
304
+ """
305
+ Wait for a specific event type on a port.
306
+
307
+ Args:
308
+ port_identifier: Port index (int) or device path (str)
309
+ event_type: Event type to wait for (e.g., "OPEN", "WRITE", "READ")
310
+ timeout: Maximum time to wait in seconds
311
+
312
+ Returns:
313
+ LogEntry if event found, None if timeout
314
+ """
315
+ start_time = time.time()
316
+
317
+ for entry in self.stream_parsed_logs(port_identifier):
318
+ if entry.event_type == event_type:
319
+ return entry
320
+
321
+ if time.time() - start_time > timeout:
322
+ break
323
+
324
+ return None
@@ -0,0 +1,141 @@
1
+ """
2
+ Data models for TTY eBPF Monitor client.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Dict, Any
7
+ from datetime import datetime
8
+
9
+
10
+ @dataclass
11
+ class Port:
12
+ """Represents a monitored TTY port."""
13
+ idx: int
14
+ device: str
15
+ baudrate: Optional[int] = None
16
+ log_path: Optional[str] = None
17
+
18
+ @classmethod
19
+ def from_dict(cls, data: Dict[str, Any]) -> 'Port':
20
+ """Create Port from daemon response dictionary."""
21
+ return cls(
22
+ idx=data['idx'],
23
+ device=data['dev'],
24
+ baudrate=data.get('baudrate'),
25
+ log_path=data.get('log_path')
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class LogEntry:
31
+ """Represents a single log entry from the TTY monitor."""
32
+ timestamp: datetime
33
+ event_type: str
34
+ process: str
35
+ direction: Optional[str] = None
36
+ data: Optional[bytes] = None
37
+ baudrate: Optional[int] = None
38
+ ioctl_cmd: Optional[int] = None
39
+ raw_line: str = ""
40
+
41
+ @classmethod
42
+ def parse_simple_log(cls, line: str) -> 'LogEntry':
43
+ """Parse a simple format log line."""
44
+ import re
45
+
46
+ # Parse format: [04.09.25 14:52:46.874] WRITE: picocom APP->DEV "data"
47
+ pattern = r'\[(\d{2}\.\d{2}\.\d{2}) (\d{2}:\d{2}:\d{2}\.\d{3})\] (\w+): (\w+)(?: (APP->DEV|DEV->APP))?(?: (.*))?'
48
+ match = re.match(pattern, line.strip())
49
+
50
+ if not match:
51
+ # Fallback for other formats like MODE_CHANGE
52
+ pattern2 = r'\[(\d{2}\.\d{2}\.\d{2}) (\d{2}:\d{2}:\d{2}\.\d{3})\] (\w+): (.*)'
53
+ match2 = re.match(pattern2, line.strip())
54
+ if match2:
55
+ date_str, time_str, event_type, details = match2.groups()
56
+ timestamp = cls._parse_datetime(date_str, time_str)
57
+ return cls(
58
+ timestamp=timestamp,
59
+ event_type=event_type,
60
+ process=details.split()[0] if details else "",
61
+ raw_line=line.strip()
62
+ )
63
+
64
+ # If no pattern matches, return basic entry
65
+ return cls(
66
+ timestamp=datetime.now(),
67
+ event_type="UNKNOWN",
68
+ process="",
69
+ raw_line=line.strip()
70
+ )
71
+
72
+ date_str, time_str, event_type, process, direction, data_part = match.groups()
73
+ timestamp = cls._parse_datetime(date_str, time_str)
74
+
75
+ # Parse data if present (quoted string with escapes)
76
+ data = None
77
+ if data_part and data_part.startswith('"') and data_part.endswith('"'):
78
+ data_str = data_part[1:-1] # Remove quotes
79
+ # Decode escape sequences
80
+ data = cls._decode_escaped_data(data_str)
81
+
82
+ return cls(
83
+ timestamp=timestamp,
84
+ event_type=event_type,
85
+ process=process,
86
+ direction=direction,
87
+ data=data,
88
+ raw_line=line.strip()
89
+ )
90
+
91
+ @staticmethod
92
+ def _parse_datetime(date_str: str, time_str: str) -> datetime:
93
+ """Parse dd.mm.yy HH:MM:SS.mmm format."""
94
+ from datetime import datetime
95
+
96
+ # Parse date: dd.mm.yy
97
+ day_str, month_str, year_str = date_str.split('.')
98
+ year_int = int('20' + year_str) # Convert yy to 20yy
99
+
100
+ # Parse time: HH:MM:SS.mmm
101
+ time_part, ms_part = time_str.split('.')
102
+ hour_str, minute_str, second_str = time_part.split(':')
103
+
104
+ return datetime(
105
+ year=year_int,
106
+ month=int(month_str),
107
+ day=int(day_str),
108
+ hour=int(hour_str),
109
+ minute=int(minute_str),
110
+ second=int(second_str),
111
+ microsecond=int(ms_part) * 1000 # Convert ms to microseconds
112
+ )
113
+
114
+ @staticmethod
115
+ def _decode_escaped_data(data_str: str) -> bytes:
116
+ """Decode escaped data string to bytes."""
117
+ result = bytearray()
118
+ i = 0
119
+ while i < len(data_str):
120
+ if data_str[i] == '\\' and i + 1 < len(data_str):
121
+ if data_str[i + 1] == 'x' and i + 3 < len(data_str):
122
+ # Hex escape: \xNN
123
+ try:
124
+ hex_val = int(data_str[i+2:i+4], 16)
125
+ result.append(hex_val)
126
+ i += 4
127
+ except ValueError:
128
+ result.append(ord(data_str[i]))
129
+ i += 1
130
+ elif data_str[i + 1] in ['\\', '"']:
131
+ # Escaped backslash or quote
132
+ result.append(ord(data_str[i + 1]))
133
+ i += 2
134
+ else:
135
+ result.append(ord(data_str[i]))
136
+ i += 1
137
+ else:
138
+ result.append(ord(data_str[i]))
139
+ i += 1
140
+
141
+ return bytes(result)
@@ -0,0 +1,2 @@
1
+ # Marker file for PEP 561
2
+ # This package supports type hints
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: tty-egpf-monitor
3
+ Version: 0.5.26
4
+ Summary: Python client library for TTY eBPF Monitor daemon
5
+ Home-page: https://github.com/seelso-net/tty-egpf-monitor
6
+ Author: TTY eBPF Monitor Team
7
+ Author-email: TTY eBPF Monitor Team <contact@seelso.net>
8
+ Maintainer-email: TTY eBPF Monitor Team <contact@seelso.net>
9
+ License: GPL-3.0
10
+ Project-URL: Homepage, https://github.com/seelso-net/tty-egpf-monitor
11
+ Project-URL: Documentation, https://github.com/seelso-net/tty-egpf-monitor/blob/main/README.md
12
+ Project-URL: Repository, https://github.com/seelso-net/tty-egpf-monitor
13
+ Project-URL: Bug Tracker, https://github.com/seelso-net/tty-egpf-monitor/issues
14
+ Project-URL: APT Repository, https://seelso-net.github.io/tty-egpf-monitor
15
+ Keywords: serial,tty,monitoring,ebpf,uart,debugging,reverse-engineering,protocol-analysis,hardware
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: System Administrators
20
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.8
24
+ Classifier: Programming Language :: Python :: 3.9
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
30
+ Classifier: Topic :: System :: Monitoring
31
+ Classifier: Topic :: System :: Networking :: Monitoring
32
+ Classifier: Topic :: Terminals :: Serial
33
+ Requires-Python: >=3.8
34
+ Description-Content-Type: text/markdown
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=6.0; extra == "dev"
37
+ Requires-Dist: pytest-cov; extra == "dev"
38
+ Requires-Dist: black; extra == "dev"
39
+ Requires-Dist: flake8; extra == "dev"
40
+ Requires-Dist: mypy; extra == "dev"
41
+ Requires-Dist: build; extra == "dev"
42
+ Requires-Dist: twine; extra == "dev"
43
+ Dynamic: author
44
+ Dynamic: home-page
45
+ Dynamic: requires-python
46
+
47
+ # TTY eBPF Monitor Python Client
48
+
49
+ [![PyPI version](https://badge.fury.io/py/tty-egpf-monitor.svg)](https://badge.fury.io/py/tty-egpf-monitor)
50
+ [![Python](https://img.shields.io/pypi/pyversions/tty-egpf-monitor.svg)](https://pypi.org/project/tty-egpf-monitor/)
51
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
52
+
53
+ A Python client library for [TTY eBPF Monitor](https://github.com/seelso-net/tty-egpf-monitor), providing a clean, Pythonic interface to monitor serial port activity using eBPF technology.
54
+
55
+ ## Features
56
+
57
+ - 🐍 **Pure Python** - No C dependencies, works with any Python 3.8+
58
+ - 🔌 **Unix Socket API** - Communicates with daemon via Unix domain socket
59
+ - 📊 **Parsed Log Entries** - Automatic parsing of log format with timestamps
60
+ - 🔄 **Live Streaming** - Real-time event streaming with iterator interface
61
+ - 🛠️ **CLI Wrapper** - Drop-in replacement for the C CLI tool
62
+ - 📚 **Rich Examples** - Comprehensive examples for common use cases
63
+ - 🔍 **Data Analysis** - Built-in support for protocol analysis and debugging
64
+
65
+ ## Installation
66
+
67
+ ### Install from PyPI
68
+
69
+ ```bash
70
+ pip install tty-egpf-monitor
71
+ ```
72
+
73
+ ### Install the Daemon
74
+
75
+ The Python client requires the `tty-egpf-monitord` daemon to be running:
76
+
77
+ ```bash
78
+ # Install daemon via APT repository
79
+ curl -fsSL https://raw.githubusercontent.com/seelso-net/tty-egpf-monitor/main/install.sh | bash
80
+
81
+ # Or install manually
82
+ CODENAME=$(lsb_release -cs)
83
+ REPO_URL=https://seelso-net.github.io/tty-egpf-monitor
84
+ curl -fsSL ${REPO_URL}/public-apt-key.asc | sudo gpg --dearmor -o /usr/share/keyrings/tty-egpf-monitor.gpg
85
+ echo "deb [signed-by=/usr/share/keyrings/tty-egpf-monitor.gpg] ${REPO_URL} ${CODENAME} main" | sudo tee /etc/apt/sources.list.d/tty-egpf-monitor.list
86
+ sudo apt-get update && sudo apt-get install -y tty-egpf-monitord
87
+ sudo systemctl enable --now tty-egpf-monitord
88
+ ```
89
+
90
+ ## Quick Start
91
+
92
+ ### Library Usage
93
+
94
+ ```python
95
+ from tty_egpf_monitor import TTYMonitorClient
96
+
97
+ # Create client
98
+ client = TTYMonitorClient()
99
+
100
+ # Add a port to monitor
101
+ idx = client.add_port("/dev/ttyUSB0", baudrate=115200)
102
+ print(f"Monitoring port {idx}")
103
+
104
+ # List all ports
105
+ ports = client.list_ports()
106
+ for port in ports:
107
+ print(f"Port {port.idx}: {port.device}")
108
+
109
+ # Stream live events
110
+ for entry in client.stream_parsed_logs("/dev/ttyUSB0"):
111
+ print(f"[{entry.timestamp}] {entry.event_type}: {entry.process}")
112
+ if entry.data:
113
+ print(f" Data: {entry.data}")
114
+
115
+ # Remove port when done
116
+ client.remove_port("/dev/ttyUSB0")
117
+ ```
118
+
119
+ ### CLI Usage
120
+
121
+ The package includes a CLI tool compatible with the C version:
122
+
123
+ ```bash
124
+ # Add a port
125
+ tty-egpf-monitor-py add /dev/ttyUSB0 115200
126
+
127
+ # List ports
128
+ tty-egpf-monitor-py list
129
+
130
+ # Stream logs (by index or device path)
131
+ tty-egpf-monitor-py stream 0
132
+ tty-egpf-monitor-py stream /dev/ttyUSB0
133
+
134
+ # Download logs
135
+ tty-egpf-monitor-py logs /dev/ttyUSB0 > captured.jsonl
136
+
137
+ # Remove port
138
+ tty-egpf-monitor-py remove /dev/ttyUSB0
139
+ ```
140
+
141
+ ## API Reference
142
+
143
+ ### TTYMonitorClient
144
+
145
+ #### Methods
146
+
147
+ - **`list_ports()`** → `List[Port]`
148
+
149
+ List all configured ports.
150
+
151
+ - **`add_port(device, baudrate=115200, log_path=None)`** → `int`
152
+
153
+ Add a port to monitor. Returns the port index.
154
+
155
+ - **`remove_port(port_identifier)`** → `bool`
156
+
157
+ Remove a port by index (int) or device path (str).
158
+
159
+ - **`get_logs(port_identifier)`** → `str`
160
+
161
+ Download full log content for a port.
162
+
163
+ - **`stream_logs(port_identifier)`** → `Iterator[str]`
164
+
165
+ Stream raw log lines as they arrive.
166
+
167
+ - **`stream_parsed_logs(port_identifier)`** → `Iterator[LogEntry]`
168
+
169
+ Stream parsed log entries as they arrive.
170
+
171
+ - **`wait_for_event(port_identifier, event_type, timeout=30.0)`** → `Optional[LogEntry]`
172
+
173
+ Wait for a specific event type with timeout.
174
+
175
+ ### LogEntry
176
+
177
+ Represents a parsed log entry:
178
+
179
+ ```python
180
+ @dataclass
181
+ class LogEntry:
182
+ timestamp: datetime # When the event occurred
183
+ event_type: str # OPEN, CLOSE, READ, WRITE, IOCTL, MODE_CHANGE
184
+ process: str # Process name that triggered the event
185
+ direction: Optional[str] # APP->DEV or DEV->APP (for READ/write)
186
+ data: Optional[bytes] # Raw data (for read/write events)
187
+ raw_line: str # Original log line
188
+ ```
189
+
190
+ ### Port
191
+
192
+ Represents a monitored port:
193
+
194
+ ```python
195
+ @dataclass
196
+ class Port:
197
+ idx: int # Port index
198
+ device: str # Device path
199
+ baudrate: Optional[int] # Configured baud rate
200
+ log_path: Optional[str] # Log file path
201
+ ```
202
+
203
+ ## Examples
204
+
205
+ See the [`examples/`](examples/) directory for comprehensive usage examples:
206
+
207
+ - **`basic_usage.py`** - Core functionality demonstration
208
+ - **`monitor_serial_data.py`** - Real-time monitoring with processing
209
+ - **`automation_script.py`** - Automated testing and analysis
210
+
211
+ ## Error Handling
212
+
213
+ ```python
214
+ from tty_egpf_monitor import TTYMonitorError
215
+
216
+ try:
217
+ client = TTYMonitorClient()
218
+ client.add_port("/dev/ttyUSB0")
219
+ except TTYMonitorError as e:
220
+ print(f"Error: {e}")
221
+ ```
222
+
223
+ ## Requirements
224
+
225
+ - **Python**: 3.8 or later
226
+ - **Operating System**: Linux (Ubuntu 22.04+ recommended)
227
+ - **Daemon**: `tty-egpf-monitord` must be installed and running
228
+ - **Permissions**: Access to the daemon's Unix socket (usually `/run/tty-egpf-monitord.sock`)
229
+
230
+ ## Related Projects
231
+
232
+ - **[TTY eBPF Monitor](https://github.com/seelso-net/tty-egpf-monitor)** - Main project with C daemon and CLI
233
+ - **[APT Repository](https://seelso-net.github.io/tty-egpf-monitor)** - Binary packages for Ubuntu
234
+
235
+ ## License
236
+
237
+ This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](https://github.com/seelso-net/tty-egpf-monitor/blob/main/LICENSE) file for details.
@@ -0,0 +1,10 @@
1
+ tty_egpf_monitor/__init__.py,sha256=fDYuwC4Fuu_EHgK8s08RiQmS_SaJdahFUKgyRGntI2M,473
2
+ tty_egpf_monitor/cli.py,sha256=1QF1YS-uCowrUEFsYdmdwAqxOdmz94hvddlpljBYgng,5255
3
+ tty_egpf_monitor/client.py,sha256=CZWZhdsYgMeUZwX-0yg53A-4qEx8wrfzJ6r5nVTUp18,10999
4
+ tty_egpf_monitor/models.py,sha256=luhXtyXfQg1h8QYMiSTTaIG7BOfXX-QtNo6t0Yx1M4Y,4826
5
+ tty_egpf_monitor/py.typed,sha256=UlkhTBVJqiS1Epx-KILVobCtCGlhR6YKM79R8vO1HdU,61
6
+ tty_egpf_monitor-0.5.26.dist-info/METADATA,sha256=2z6OmNdCCMJBCbghGZnrT1JKEnzyxNHOcDLHXu_tW9k,7776
7
+ tty_egpf_monitor-0.5.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ tty_egpf_monitor-0.5.26.dist-info/entry_points.txt,sha256=l1X-FXgjKnh6pmOe16MjBN-9sww3FKFuknTy16IFW2s,66
9
+ tty_egpf_monitor-0.5.26.dist-info/top_level.txt,sha256=uXb0ct7ljNig6oqHa1nVH-TkoURcJ0_uLf2q8SUJaQs,17
10
+ tty_egpf_monitor-0.5.26.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tty-egpf-monitor-py = tty_egpf_monitor.cli:main
@@ -0,0 +1 @@
1
+ tty_egpf_monitor