Fix Unix remote watchdog socket leak

This commit is contained in:
Storm Dragon
2026-07-29 14:13:53 -04:00
parent 76b8f2f2cf
commit 122a6a38b3
2 changed files with 78 additions and 36 deletions
@@ -329,48 +329,54 @@ class driver(remoteDriver):
def watch_dog(self, active, event_queue): def watch_dog(self, active, event_queue):
# echo "command say this is a test" | socat - # echo "command say this is a test" | socat -
# UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock # UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
for socket_file, optional in self._get_socket_candidates(): try:
fenrir_sock = self._bind_socket(socket_file, optional) for socket_file, optional in self._get_socket_candidates():
if fenrir_sock is None: fenrir_sock = self._bind_socket(socket_file, optional)
continue if fenrir_sock is None:
self.fenrirSocks.append(fenrir_sock) continue
self.bound_sockets.append((fenrir_sock, socket_file)) self.fenrirSocks.append(fenrir_sock)
self.bound_sockets.append((fenrir_sock, socket_file))
if not self.fenrirSocks: if not self.fenrirSocks:
return return
self._try_register_instance() self._try_register_instance()
last_register = time.time() last_register = time.time()
while active.value: while active.value:
if time.time() - last_register > 10.0: if time.time() - last_register > 10.0:
self._try_register_instance() self._try_register_instance()
last_register = time.time() last_register = time.time()
# Check if the client is still connected and if data is available: # Check if the client is still connected and data is available.
try:
r, _, _ = select.select(self.fenrirSocks, [], [], 0.8)
except select.error:
break
if r == []:
continue
for fenrir_sock in r:
client_sock, client_addr = fenrir_sock.accept()
socket_file = self._socket_file_for_socket(fenrir_sock)
# Ensure client socket is always closed to prevent resource
# leaks
try: try:
self._handle_client(client_sock, event_queue, socket_file) r, _, _ = select.select(self.fenrirSocks, [], [], 0.8)
finally: except select.error:
# Always close client socket, even if data processing fails break
if r == []:
continue
for fenrir_sock in r:
client_sock, _client_addr = fenrir_sock.accept()
socket_file = self._socket_file_for_socket(fenrir_sock)
try: try:
client_sock.close() self._handle_client(
except Exception as e: client_sock, event_queue, socket_file
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error closing client socket: "
+ str(e),
debug.DebugLevel.ERROR,
) )
self._cleanup() finally:
try:
client_sock.close()
except Exception as e:
self.env["runtime"][
"DebugManager"
].write_debug_out(
"unixDriver watch_dog: Error closing client "
"socket: "
+ str(e),
debug.DebugLevel.ERROR,
)
finally:
# ProcessManager restarts a failed watchdog. Always release the
# old listeners first so each restart cannot leak another socket.
self._cleanup()
def shutdown(self): def shutdown(self):
self._cleanup() self._cleanup()
+36
View File
@@ -1,5 +1,7 @@
from unittest.mock import Mock, mock_open, patch from unittest.mock import Mock, mock_open, patch
import pytest
from fenrirscreenreader.remoteDriver import unixDriver from fenrirscreenreader.remoteDriver import unixDriver
@@ -27,6 +29,40 @@ def test_watchdog_keeps_serving_when_instance_registration_fails(
select_call.assert_called_once() select_call.assert_called_once()
def test_watchdog_closes_listeners_when_unexpected_error_escapes(
mock_environment,
):
driver = unixDriver.driver()
driver.env = mock_environment
listener = Mock()
active = Mock(value=True)
with patch.object(
driver,
"_get_socket_candidates",
return_value=[("/tmp/fenrir-test.sock", False)],
), patch.object(
driver, "_bind_socket", return_value=listener
), patch.object(
driver, "_try_register_instance"
), patch(
"fenrirscreenreader.remoteDriver.unixDriver.select.select",
side_effect=RuntimeError("unexpected failure"),
), patch(
"fenrirscreenreader.remoteDriver.unixDriver.os.path.exists",
return_value=False,
), patch(
"fenrirscreenreader.remoteDriver.unixDriver.remoteInstanceRegistry.remove_instance"
) as remove_instance:
with pytest.raises(RuntimeError, match="unexpected failure"):
driver.watch_dog(active, Mock())
listener.close.assert_called_once_with()
assert driver.fenrirSocks == []
assert driver.bound_sockets == []
remove_instance.assert_called_once_with()
class FakeClientSocket: class FakeClientSocket:
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data