From 1f7889c997c802011a6f94dd8e7855a7e51a6ad2 Mon Sep 17 00:00:00 2001 From: killown Date: Tue, 18 Feb 2025 11:52:10 -0300 Subject: [PATCH] changed send_json and its dependent functions to be non-blocking. (#15) * changed send_json and its dependent functions to be non-blocking. * connect client after timeout * changed to the previous read_exact * read_exact using bytearray but returning bytes * don't reconnect client after response timeout * handle exceptions in read_next_event * raise exception instead of print * Add function to check if the client is connected * Remove unused var * Removed client connection after exception * removed orjson * replaced is_socket_active with is_connected * make sure there is no way to hang while sending data. * remove send with timeout, causing issues with create_wayland_output * add more seconds to create_wayland_output * allow the user decide the send_json timeout * removed timeout from create wayland output --------- Co-authored-by: killown --- wayfire/extra/ipc_utils.py | 20 -------- wayfire/ipc.py | 95 ++++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/wayfire/extra/ipc_utils.py b/wayfire/extra/ipc_utils.py index bf81f7b..983c830 100644 --- a/wayfire/extra/ipc_utils.py +++ b/wayfire/extra/ipc_utils.py @@ -64,26 +64,6 @@ def center_cursor_on_view(self, view_id: int) -> None: # Move the cursor to the calculated position self._stipc.move_cursor(cursor_x, cursor_y) - def is_socket_active(self, socket): - """ - Check if the given socket is still active. - - Args: - socket: The socket object to check. - - Example: - socket = WayfireSocket() - is_socket_active(socket) - - Returns: - bool: True if the socket is active, False otherwise. - """ - try: - socket.client.getpeername() - return True - except OSError: - return False - def move_view_to_empty_workspace(self, view_id: int) -> None: """ Moves a top-level view to an empty workspace. diff --git a/wayfire/ipc.py b/wayfire/ipc.py index 6d26064..363736e 100644 --- a/wayfire/ipc.py +++ b/wayfire/ipc.py @@ -1,10 +1,11 @@ import socket import json as js +import select +import time import os from typing import Any, List, Optional from wayfire.core.template import get_msg_template, geometry_to_json - class WayfireSocket: def __init__(self, socket_name: str | None=None, allow_manual_search=False): if socket_name is None: @@ -12,6 +13,7 @@ def __init__(self, socket_name: str | None=None, allow_manual_search=False): self.socket_name = None self.pending_events = [] + self.timeout = 3 if socket_name is None and allow_manual_search: # the last item is the most recent socket file @@ -42,24 +44,29 @@ def connect_client(self, socket_name): self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.client.connect(socket_name) - def close(self): - self.client.close() + def is_connected(self): + if self.client is None: + return False - def read_exact(self, n: int): - response = bytes() - while n > 0: - read_this_time = self.client.recv(n) - if not read_this_time: - raise Exception("Failed to read anything from the socket!") - n -= len(read_this_time) - response += read_this_time + try: + if self.client.fileno() < 0: + return False + return True + except (socket.error, ValueError): + return False - return response + def close(self): + self.client.close() def read_message(self): rlen = int.from_bytes(self.read_exact(4), byteorder="little") response_message = self.read_exact(rlen) - response = js.loads(response_message) + if not response_message: + raise Exception("Received empty response message") + try: + response = js.loads(response_message.decode("utf-8")) + except js.JSONDecodeError as e: + raise Exception(f"JSON decoding error: {e}") if "error" in response and response["error"] == "No such method found!": raise Exception(f"Method {response['method']} is not available. \ @@ -69,6 +76,51 @@ def read_message(self): raise Exception(response["error"]) return response + def send_json(self, msg): + if 'method' not in msg: + raise Exception("Malformed JSON request: missing method!") + + data = js.dumps(msg).encode("utf-8") + header = len(data).to_bytes(4, byteorder="little") + + if self.is_connected(): + self.client.send(header) + self.client.send(data) + else: + raise Exception("Unable to send data: The Wayfire socket instance is not connected.") + + end_time = time.time() + self.timeout + while True: + remaining_time = end_time - time.time() + if remaining_time <= 0: + raise Exception("Response timeout") + + readable, _, _ = select.select([self.client], [], [], remaining_time) + if readable: + try: + response = self.read_message() + except Exception as e: + raise Exception(f"Error reading message: {e}") + + if 'event' in response: + self.pending_events.append(response) + continue + + return response + else: + raise Exception("Response timeout") + + def read_exact(self, n: int): + response = bytearray() + while n > 0: + read_this_time = self.client.recv(n) + if not read_this_time: + raise Exception("Failed to read anything from the socket!") + n -= len(read_this_time) + response += read_this_time + + return bytes(response) + def read_next_event(self): if self.pending_events: return self.pending_events.pop(0) @@ -277,23 +329,6 @@ def _wayfire_plugin_from_method(method: str) -> str: return method.split("/")[0] - def send_json(self, msg): - if 'method' not in msg: - raise Exception("Malformed json request: missing method!") - - data = js.dumps(msg).encode("utf8") - header = len(data).to_bytes(4, byteorder="little") - self.client.send(header) - self.client.send(data) - - while True: - response = self.read_message() - if 'event' in response: - self.pending_events.append(response) - continue - - return response - def get_output(self, output_id: int): """ Retrieves information about a specific output.