Skip to content

Commit 80ded8e

Browse files
Robert Foleystsquad
authored andcommitted
python/qemu: Change ConsoleSocket to optionally drain socket.
The primary purpose of this change is to clean up machine.py's console_socket property to return a single type, a ConsoleSocket. ConsoleSocket now derives from a socket, which means that in the default case (of not draining), machine.py will see the same behavior as it did prior to ConsoleSocket. Signed-off-by: Robert Foley <[email protected]> Signed-off-by: Alex Bennée <[email protected]> Message-Id: <[email protected]> Message-Id: <[email protected]>
1 parent 4b84d87 commit 80ded8e

File tree

2 files changed

+59
-46
lines changed

2 files changed

+59
-46
lines changed

python/qemu/console_socket.py

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,68 +13,75 @@
1313
# the COPYING file in the top-level directory.
1414
#
1515

16-
import asyncore
1716
import socket
1817
import threading
1918
from collections import deque
2019
import time
2120

2221

23-
class ConsoleSocket(asyncore.dispatcher):
22+
class ConsoleSocket(socket.socket):
2423
"""
2524
ConsoleSocket represents a socket attached to a char device.
2625
27-
Drains the socket and places the bytes into an in memory buffer
28-
for later processing.
26+
Optionally (if drain==True), drains the socket and places the bytes
27+
into an in memory buffer for later processing.
2928
3029
Optionally a file path can be passed in and we will also
3130
dump the characters to this file for debugging purposes.
3231
"""
33-
def __init__(self, address, file=None):
32+
def __init__(self, address, file=None, drain=False):
3433
self._recv_timeout_sec = 300
3534
self._sleep_time = 0.5
3635
self._buffer = deque()
37-
self._asyncore_thread = None
38-
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
39-
self._sock.connect(address)
36+
socket.socket.__init__(self, socket.AF_UNIX, socket.SOCK_STREAM)
37+
self.connect(address)
4038
self._logfile = None
4139
if file:
4240
self._logfile = open(file, "w")
43-
asyncore.dispatcher.__init__(self, sock=self._sock)
4441
self._open = True
45-
self._thread_start()
42+
if drain:
43+
self._drain_thread = self._thread_start()
44+
else:
45+
self._drain_thread = None
4646

47-
def _thread_start(self):
48-
"""Kick off a thread to wait on the asyncore.loop"""
49-
if self._asyncore_thread is not None:
50-
return
51-
self._asyncore_thread = threading.Thread(target=asyncore.loop,
52-
kwargs={'timeout':1})
53-
self._asyncore_thread.daemon = True
54-
self._asyncore_thread.start()
47+
def _drain_fn(self):
48+
"""Drains the socket and runs while the socket is open."""
49+
while self._open:
50+
try:
51+
self._drain_socket()
52+
except socket.timeout:
53+
# The socket is expected to timeout since we set a
54+
# short timeout to allow the thread to exit when
55+
# self._open is set to False.
56+
time.sleep(self._sleep_time)
5557

56-
def handle_close(self):
57-
"""redirect close to base class"""
58-
# Call the base class close, but not self.close() since
59-
# handle_close() occurs in the context of the thread which
60-
# self.close() attempts to join.
61-
asyncore.dispatcher.close(self)
58+
def _thread_start(self):
59+
"""Kick off a thread to drain the socket."""
60+
# Configure socket to not block and timeout.
61+
# This allows our drain thread to not block
62+
# on recieve and exit smoothly.
63+
socket.socket.setblocking(self, False)
64+
socket.socket.settimeout(self, 1)
65+
drain_thread = threading.Thread(target=self._drain_fn)
66+
drain_thread.daemon = True
67+
drain_thread.start()
68+
return drain_thread
6269

6370
def close(self):
6471
"""Close the base object and wait for the thread to terminate"""
6572
if self._open:
6673
self._open = False
67-
asyncore.dispatcher.close(self)
68-
if self._asyncore_thread is not None:
69-
thread, self._asyncore_thread = self._asyncore_thread, None
74+
if self._drain_thread is not None:
75+
thread, self._drain_thread = self._drain_thread, None
7076
thread.join()
77+
socket.socket.close(self)
7178
if self._logfile:
7279
self._logfile.close()
7380
self._logfile = None
7481

75-
def handle_read(self):
82+
def _drain_socket(self):
7683
"""process arriving characters into in memory _buffer"""
77-
data = asyncore.dispatcher.recv(self, 1)
84+
data = socket.socket.recv(self, 1)
7885
# latin1 is needed since there are some chars
7986
# we are receiving that cannot be encoded to utf-8
8087
# such as 0xe2, 0x80, 0xA6.
@@ -85,27 +92,38 @@ def handle_read(self):
8592
for c in string:
8693
self._buffer.extend(c)
8794

88-
def recv(self, buffer_size=1):
95+
def recv(self, bufsize=1):
8996
"""Return chars from in memory buffer.
9097
Maintains the same API as socket.socket.recv.
9198
"""
99+
if self._drain_thread is None:
100+
# Not buffering the socket, pass thru to socket.
101+
return socket.socket.recv(self, bufsize)
92102
start_time = time.time()
93-
while len(self._buffer) < buffer_size:
103+
while len(self._buffer) < bufsize:
94104
time.sleep(self._sleep_time)
95105
elapsed_sec = time.time() - start_time
96106
if elapsed_sec > self._recv_timeout_sec:
97107
raise socket.timeout
98-
chars = ''.join([self._buffer.popleft() for i in range(buffer_size)])
108+
chars = ''.join([self._buffer.popleft() for i in range(bufsize)])
99109
# We choose to use latin1 to remain consistent with
100110
# handle_read() and give back the same data as the user would
101111
# receive if they were reading directly from the
102112
# socket w/o our intervention.
103113
return chars.encode("latin1")
104114

105-
def set_blocking(self):
106-
"""Maintain compatibility with socket API"""
107-
pass
115+
def setblocking(self, value):
116+
"""When not draining we pass thru to the socket,
117+
since when draining we control socket blocking.
118+
"""
119+
if self._drain_thread is None:
120+
socket.socket.setblocking(self, value)
108121

109122
def settimeout(self, seconds):
110-
"""Set current timeout on recv"""
111-
self._recv_timeout_sec = seconds
123+
"""When not draining we pass thru to the socket,
124+
since when draining we control the timeout.
125+
"""
126+
if seconds is not None:
127+
self._recv_timeout_sec = seconds
128+
if self._drain_thread is None:
129+
socket.socket.settimeout(self, seconds)

python/qemu/machine.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import subprocess
2424
import shutil
2525
import signal
26-
import socket
2726
import tempfile
2827
from typing import Optional, Type
2928
from types import TracebackType
@@ -673,12 +672,8 @@ def console_socket(self):
673672
Returns a socket connected to the console
674673
"""
675674
if self._console_socket is None:
676-
if self._drain_console:
677-
self._console_socket = console_socket.ConsoleSocket(
678-
self._console_address,
679-
file=self._console_log_path)
680-
else:
681-
self._console_socket = socket.socket(socket.AF_UNIX,
682-
socket.SOCK_STREAM)
683-
self._console_socket.connect(self._console_address)
675+
self._console_socket = console_socket.ConsoleSocket(
676+
self._console_address,
677+
file=self._console_log_path,
678+
drain=self._drain_console)
684679
return self._console_socket

0 commit comments

Comments
 (0)