]>
Commit | Line | Data |
---|---|---|
4b84d874 RF |
1 | """ |
2 | QEMU Console Socket Module: | |
3 | ||
4 | This python module implements a ConsoleSocket object, | |
5 | which can drain a socket and optionally dump the bytes to file. | |
6 | """ | |
0fc8f660 RF |
7 | # Copyright 2020 Linaro |
8 | # | |
9 | # Authors: | |
10 | # Robert Foley <robert.foley@linaro.org> | |
11 | # | |
12 | # This code is licensed under the GPL version 2 or later. See | |
13 | # the COPYING file in the top-level directory. | |
14 | # | |
4b84d874 | 15 | |
932ca4bb | 16 | from collections import deque |
0fc8f660 RF |
17 | import socket |
18 | import threading | |
0fc8f660 | 19 | import time |
e35c1382 | 20 | from typing import Deque, Optional |
4b84d874 | 21 | |
0fc8f660 | 22 | |
80ded8e9 | 23 | class ConsoleSocket(socket.socket): |
4b84d874 RF |
24 | """ |
25 | ConsoleSocket represents a socket attached to a char device. | |
0fc8f660 | 26 | |
5f263cb1 JS |
27 | :param address: An AF_UNIX path or address. |
28 | :param sock_fd: Optionally, an existing socket file descriptor. | |
29 | One of address or sock_fd must be specified. | |
30 | :param file: Optionally, a filename to log to. | |
31 | :param drain: Optionally, drains the socket and places the bytes | |
32 | into an in memory buffer for later processing. | |
4b84d874 | 33 | """ |
5f263cb1 JS |
34 | def __init__(self, |
35 | address: Optional[str] = None, | |
36 | sock_fd: Optional[int] = None, | |
37 | file: Optional[str] = None, | |
e35c1382 | 38 | drain: bool = False): |
5f263cb1 JS |
39 | if address is None and sock_fd is None: |
40 | raise ValueError("one of 'address' or 'sock_fd' must be specified") | |
41 | if address is not None and sock_fd is not None: | |
42 | raise ValueError("can't specify both 'address' and 'sock_fd'") | |
43 | ||
6cf4cce7 | 44 | self._recv_timeout_sec = 300.0 |
4b84d874 | 45 | self._sleep_time = 0.5 |
af0db882 | 46 | self._buffer: Deque[int] = deque() |
5f263cb1 JS |
47 | if address is not None: |
48 | socket.socket.__init__(self, socket.AF_UNIX, socket.SOCK_STREAM) | |
49 | self.connect(address) | |
50 | else: | |
51 | assert sock_fd is not None | |
52 | socket.socket.__init__(self, fileno=sock_fd) | |
0fc8f660 RF |
53 | self._logfile = None |
54 | if file: | |
8825fed8 | 55 | # pylint: disable=consider-using-with |
af0db882 | 56 | self._logfile = open(file, "bw") |
0fc8f660 | 57 | self._open = True |
714ac05a | 58 | self._drain_thread = None |
80ded8e9 RF |
59 | if drain: |
60 | self._drain_thread = self._thread_start() | |
0fc8f660 | 61 | |
afded359 | 62 | def __repr__(self) -> str: |
ee1a2723 JS |
63 | tmp = super().__repr__() |
64 | tmp = tmp.rstrip(">") | |
65 | tmp = "%s, logfile=%s, drain_thread=%s>" % (tmp, self._logfile, | |
66 | self._drain_thread) | |
67 | return tmp | |
afded359 | 68 | |
e35c1382 | 69 | def _drain_fn(self) -> None: |
80ded8e9 RF |
70 | """Drains the socket and runs while the socket is open.""" |
71 | while self._open: | |
72 | try: | |
73 | self._drain_socket() | |
74 | except socket.timeout: | |
75 | # The socket is expected to timeout since we set a | |
76 | # short timeout to allow the thread to exit when | |
77 | # self._open is set to False. | |
78 | time.sleep(self._sleep_time) | |
0fc8f660 | 79 | |
e35c1382 | 80 | def _thread_start(self) -> threading.Thread: |
80ded8e9 RF |
81 | """Kick off a thread to drain the socket.""" |
82 | # Configure socket to not block and timeout. | |
83 | # This allows our drain thread to not block | |
af76484e | 84 | # on receive and exit smoothly. |
80ded8e9 RF |
85 | socket.socket.setblocking(self, False) |
86 | socket.socket.settimeout(self, 1) | |
87 | drain_thread = threading.Thread(target=self._drain_fn) | |
88 | drain_thread.daemon = True | |
89 | drain_thread.start() | |
90 | return drain_thread | |
0fc8f660 | 91 | |
e35c1382 | 92 | def close(self) -> None: |
0fc8f660 RF |
93 | """Close the base object and wait for the thread to terminate""" |
94 | if self._open: | |
95 | self._open = False | |
80ded8e9 RF |
96 | if self._drain_thread is not None: |
97 | thread, self._drain_thread = self._drain_thread, None | |
0fc8f660 | 98 | thread.join() |
80ded8e9 | 99 | socket.socket.close(self) |
0fc8f660 RF |
100 | if self._logfile: |
101 | self._logfile.close() | |
102 | self._logfile = None | |
103 | ||
e35c1382 | 104 | def _drain_socket(self) -> None: |
0fc8f660 | 105 | """process arriving characters into in memory _buffer""" |
80ded8e9 | 106 | data = socket.socket.recv(self, 1) |
0fc8f660 | 107 | if self._logfile: |
af0db882 | 108 | self._logfile.write(data) |
0fc8f660 | 109 | self._logfile.flush() |
af0db882 | 110 | self._buffer.extend(data) |
0fc8f660 | 111 | |
ff3513e6 | 112 | def recv(self, bufsize: int = 1, flags: int = 0) -> bytes: |
4b84d874 RF |
113 | """Return chars from in memory buffer. |
114 | Maintains the same API as socket.socket.recv. | |
115 | """ | |
80ded8e9 RF |
116 | if self._drain_thread is None: |
117 | # Not buffering the socket, pass thru to socket. | |
ff3513e6 JS |
118 | return socket.socket.recv(self, bufsize, flags) |
119 | assert not flags, "Cannot pass flags to recv() in drained mode" | |
0fc8f660 | 120 | start_time = time.time() |
80ded8e9 | 121 | while len(self._buffer) < bufsize: |
4b84d874 | 122 | time.sleep(self._sleep_time) |
0fc8f660 RF |
123 | elapsed_sec = time.time() - start_time |
124 | if elapsed_sec > self._recv_timeout_sec: | |
125 | raise socket.timeout | |
af0db882 | 126 | return bytes((self._buffer.popleft() for i in range(bufsize))) |
0fc8f660 | 127 | |
e35c1382 | 128 | def setblocking(self, value: bool) -> None: |
80ded8e9 RF |
129 | """When not draining we pass thru to the socket, |
130 | since when draining we control socket blocking. | |
131 | """ | |
132 | if self._drain_thread is None: | |
133 | socket.socket.setblocking(self, value) | |
0fc8f660 | 134 | |
6cf4cce7 | 135 | def settimeout(self, value: Optional[float]) -> None: |
80ded8e9 RF |
136 | """When not draining we pass thru to the socket, |
137 | since when draining we control the timeout. | |
138 | """ | |
6cf4cce7 JS |
139 | if value is not None: |
140 | self._recv_timeout_sec = value | |
80ded8e9 | 141 | if self._drain_thread is None: |
6cf4cce7 | 142 | socket.socket.settimeout(self, value) |