From fa2c2f0b03ddfac1a00cb3e6bc66dd1196e26e24 Mon Sep 17 00:00:00 2001 From: Jason Lingohr Date: Thu, 10 Jul 2025 13:00:40 +1000 Subject: [PATCH 1/4] Add a ping_interval argument --- truenas_api_client/__init__.py | 17 +++++++++++++---- truenas_api_client/legacy.py | 8 ++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/truenas_api_client/__init__.py b/truenas_api_client/__init__.py index b84923c..2566d87 100644 --- a/truenas_api_client/__init__.py +++ b/truenas_api_client/__init__.py @@ -71,7 +71,8 @@ class Client: """Implicit wrapper of either a `JSONRPCClient` or a `LegacyClient`.""" def __init__(self, uri: str | None = None, reserved_ports=False, private_methods=False, py_exceptions=False, - log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True): + log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, + ping_interval: float | int = 0): """Initialize either a `JSONRPCClient` or a `LegacyClient`. Use `JSONRPCClient` unless `uri` ends with '/websocket'. @@ -86,6 +87,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods call_timeout: Number of seconds to allow an API call before timing out. Can be overridden on a per-call basis. Defaults to `CALL_TIMEOUT`. verify_ssl: `True` if SSL certificate should be verified before connecting. + ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). Raises: ClientException: `WSClient` timed out or some other connection error occurred. @@ -99,7 +101,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods self.uri_check(uri, py_exceptions) self.__client = client_class(uri, reserved_ports, private_methods, py_exceptions, log_py_exceptions, - call_timeout, verify_ssl) + call_timeout, verify_ssl, ping_interval) def uri_check(self, uri: str | None, py_exceptions: bool): # We pickle_load when handling py_exceptions, reduce risk of MITM on client causing a pickle.load @@ -123,7 +125,8 @@ class WSClient: The object used by `JSONRPCClient` to send and receive data. """ - def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = False, verify_ssl: bool = True): + def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = False, verify_ssl: bool = True, + ping_interval: float | int = 0): """Initialize a `WSClient`. Args: @@ -131,12 +134,14 @@ def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = client: Reference to the `JSONRPCClient` instance that uses this object. reserved_ports: `True` if the `socket` should bind to a reserved port, i.e. 600-1024. verify_ssl: `True` if SSL certificate should be verified before connecting. + ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). """ self.url = url self.client = client self.reserved_ports = reserved_ports self.verify_ssl = verify_ssl + self.ping_interval = ping_interval self.socket: socket.socket self.app: WebSocketApp @@ -176,6 +181,7 @@ def connect(self): on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, + ping_interval=self.ping_interval, ) Thread(daemon=True, target=self.app.run_forever).start() @@ -398,7 +404,8 @@ class JSONRPCClient: """ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods=False, py_exceptions=False, - log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True): + log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, + ping_interval: float | int = 0): """Initialize a `JSONRPCClient`. Args: @@ -411,6 +418,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods call_timeout: Number of seconds to allow an API call before timing out. Can be overridden on a per-call basis. Defaults to `CALL_TIMEOUT`. verify_ssl: `True` if SSL certificate should be verified before connecting. + ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). Raises: ClientException: `WSClient` timed out or some other connection error occurred. @@ -442,6 +450,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods client=self, reserved_ports=reserved_ports, verify_ssl=verify_ssl, + ping_interval=ping_interval, ) self._ws.connect() self._connected.wait(10) diff --git a/truenas_api_client/legacy.py b/truenas_api_client/legacy.py index 8657c40..b456d6a 100644 --- a/truenas_api_client/legacy.py +++ b/truenas_api_client/legacy.py @@ -28,11 +28,12 @@ class WSClient: - def __init__(self, url, *, client, reserved_ports=False, verify_ssl=True): + def __init__(self, url, *, client, reserved_ports=False, verify_ssl=True, ping_interval: float | int = 0): self.url = url self.client = client self.reserved_ports = reserved_ports self.verify_ssl = verify_ssl + self.ping_interval = ping_interval self.socket = None self.app = None @@ -67,6 +68,7 @@ def connect(self): on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, + ping_interval=self.ping_interval, ) Thread(daemon=True, target=self.app.run_forever).start() @@ -179,7 +181,8 @@ def result(self): class LegacyClient: def __init__(self, uri=None, reserved_ports=False, private_methods=False, py_exceptions=False, - log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True): + log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, + ping_interval: float | int = 0): """ Arguments: :reserved_ports(bool): should the local socket used a reserved port @@ -207,6 +210,7 @@ def __init__(self, uri=None, reserved_ports=False, private_methods=False, py_exc client=self, reserved_ports=reserved_ports, verify_ssl=verify_ssl, + ping_interval=ping_interval, ) self._ws.connect() self._connected.wait(10) From 19c4b607357a294900865db880aff56de0330d71 Mon Sep 17 00:00:00 2001 From: Jason Lingohr Date: Thu, 10 Jul 2025 13:42:20 +1000 Subject: [PATCH 2/4] Bugfix. --- truenas_api_client/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/truenas_api_client/__init__.py b/truenas_api_client/__init__.py index 2566d87..e9cbfc8 100644 --- a/truenas_api_client/__init__.py +++ b/truenas_api_client/__init__.py @@ -181,9 +181,8 @@ def connect(self): on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, - ping_interval=self.ping_interval, ) - Thread(daemon=True, target=self.app.run_forever).start() + Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval}).start() def send(self, data: bytes | str): """Send data to the server by calling `WebSocketApp.send()`. From 06cba793967fd0f992c33587c792b1fb184bc447 Mon Sep 17 00:00:00 2001 From: Jason Lingohr Date: Thu, 10 Jul 2025 13:45:41 +1000 Subject: [PATCH 3/4] Bugfix. --- truenas_api_client/legacy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/truenas_api_client/legacy.py b/truenas_api_client/legacy.py index b456d6a..a831584 100644 --- a/truenas_api_client/legacy.py +++ b/truenas_api_client/legacy.py @@ -68,9 +68,8 @@ def connect(self): on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, - ping_interval=self.ping_interval, ) - Thread(daemon=True, target=self.app.run_forever).start() + Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval}).start() def send(self, data): return self.app.send(data) From 9d6a0295585af8bd2b100879ed24f93830cbba6e Mon Sep 17 00:00:00 2001 From: Jason Lingohr Date: Mon, 14 Jul 2025 11:13:27 +1000 Subject: [PATCH 4/4] Also add a reconnect argument. --- truenas_api_client/__init__.py | 15 ++++++++++----- truenas_api_client/legacy.py | 8 +++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/truenas_api_client/__init__.py b/truenas_api_client/__init__.py index e9cbfc8..c7cc3a1 100644 --- a/truenas_api_client/__init__.py +++ b/truenas_api_client/__init__.py @@ -72,7 +72,7 @@ class Client: def __init__(self, uri: str | None = None, reserved_ports=False, private_methods=False, py_exceptions=False, log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, - ping_interval: float | int = 0): + ping_interval: float | int = 0, reconnect: int = None): """Initialize either a `JSONRPCClient` or a `LegacyClient`. Use `JSONRPCClient` unless `uri` ends with '/websocket'. @@ -88,6 +88,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods basis. Defaults to `CALL_TIMEOUT`. verify_ssl: `True` if SSL certificate should be verified before connecting. ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). + reconnect: Attempt to reconnect every n seconds. Defaults to None (no reconnects). Raises: ClientException: `WSClient` timed out or some other connection error occurred. @@ -101,7 +102,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods self.uri_check(uri, py_exceptions) self.__client = client_class(uri, reserved_ports, private_methods, py_exceptions, log_py_exceptions, - call_timeout, verify_ssl, ping_interval) + call_timeout, verify_ssl, ping_interval, reconnect) def uri_check(self, uri: str | None, py_exceptions: bool): # We pickle_load when handling py_exceptions, reduce risk of MITM on client causing a pickle.load @@ -126,7 +127,7 @@ class WSClient: """ def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = False, verify_ssl: bool = True, - ping_interval: float | int = 0): + ping_interval: float | int = 0, reconnect: int = None): """Initialize a `WSClient`. Args: @@ -135,6 +136,7 @@ def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = reserved_ports: `True` if the `socket` should bind to a reserved port, i.e. 600-1024. verify_ssl: `True` if SSL certificate should be verified before connecting. ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). + reconnect: Attempt to reconnect every n seconds. Defaults to None (no reconnects). """ self.url = url @@ -142,6 +144,7 @@ def __init__(self, url: str, *, client: 'JSONRPCClient', reserved_ports: bool = self.reserved_ports = reserved_ports self.verify_ssl = verify_ssl self.ping_interval = ping_interval + self.reconnect = reconnect self.socket: socket.socket self.app: WebSocketApp @@ -182,7 +185,7 @@ def connect(self): on_error=self._on_error, on_close=self._on_close, ) - Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval}).start() + Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval, "reconnect": self.reconnect}).start() def send(self, data: bytes | str): """Send data to the server by calling `WebSocketApp.send()`. @@ -404,7 +407,7 @@ class JSONRPCClient: """ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods=False, py_exceptions=False, log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, - ping_interval: float | int = 0): + ping_interval: float | int = 0, reconnect: int = None): """Initialize a `JSONRPCClient`. Args: @@ -418,6 +421,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods basis. Defaults to `CALL_TIMEOUT`. verify_ssl: `True` if SSL certificate should be verified before connecting. ping_interval: Number of seconds between WebSocket ping frames. Defaults to 0 (disabled). + reconnect: Attempt to reconnect every n seconds. Defaults to None (no reconnects). Raises: ClientException: `WSClient` timed out or some other connection error occurred. @@ -450,6 +454,7 @@ def __init__(self, uri: str | None = None, reserved_ports=False, private_methods reserved_ports=reserved_ports, verify_ssl=verify_ssl, ping_interval=ping_interval, + reconnect=reconnect, ) self._ws.connect() self._connected.wait(10) diff --git a/truenas_api_client/legacy.py b/truenas_api_client/legacy.py index a831584..ff3c4fd 100644 --- a/truenas_api_client/legacy.py +++ b/truenas_api_client/legacy.py @@ -28,12 +28,13 @@ class WSClient: - def __init__(self, url, *, client, reserved_ports=False, verify_ssl=True, ping_interval: float | int = 0): + def __init__(self, url, *, client, reserved_ports=False, verify_ssl=True, ping_interval: float | int = 0, reconnect: int = None): self.url = url self.client = client self.reserved_ports = reserved_ports self.verify_ssl = verify_ssl self.ping_interval = ping_interval + self.reconnect = reconnect self.socket = None self.app = None @@ -69,7 +70,7 @@ def connect(self): on_error=self._on_error, on_close=self._on_close, ) - Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval}).start() + Thread(daemon=True, target=self.app.run_forever, kwargs={"ping_interval": self.ping_interval, "reconnect": self.reconnect}).start() def send(self, data): return self.app.send(data) @@ -181,7 +182,7 @@ def result(self): class LegacyClient: def __init__(self, uri=None, reserved_ports=False, private_methods=False, py_exceptions=False, log_py_exceptions=False, call_timeout: float | UndefinedType = undefined, verify_ssl=True, - ping_interval: float | int = 0): + ping_interval: float | int = 0, reconnect: int = None): """ Arguments: :reserved_ports(bool): should the local socket used a reserved port @@ -210,6 +211,7 @@ def __init__(self, uri=None, reserved_ports=False, private_methods=False, py_exc reserved_ports=reserved_ports, verify_ssl=verify_ssl, ping_interval=ping_interval, + reconnect=reconnect, ) self._ws.connect() self._connected.wait(10)