]> git.proxmox.com Git - mirror_qemu.git/blame - python/qemu/machine/console_socket.py
python/console_socket: accept existing FD in initializer
[mirror_qemu.git] / python / qemu / machine / console_socket.py
CommitLineData
4b84d874
RF
1"""
2QEMU Console Socket Module:
3
4This python module implements a ConsoleSocket object,
5which 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 16from collections import deque
0fc8f660
RF
17import socket
18import threading
0fc8f660 19import time
e35c1382 20from typing import Deque, Optional
4b84d874 21
0fc8f660 22
80ded8e9 23class 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)