Skip to content

Commit

Permalink
changed send_json and its dependent functions to be non-blocking. (#15)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
killown and killown authored Feb 18, 2025
1 parent 7aa42b3 commit 1f7889c
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 50 deletions.
20 changes: 0 additions & 20 deletions wayfire/extra/ipc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
95 changes: 65 additions & 30 deletions wayfire/ipc.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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:
socket_name = os.getenv("WAYFIRE_SOCKET")

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
Expand Down Expand Up @@ -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. \
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 1f7889c

Please sign in to comment.