Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions ClipCascade_Desktop/src/core/application.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from logging.handlers import RotatingFileHandler
import sys


Expand Down Expand Up @@ -68,12 +69,10 @@ def __init__(

def setup_logging(self):
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
logging.basicConfig(
level=LOG_LEVEL,
format=LOG_FORMAT,
filename=self.log_file_path,
filemode="w",
handler = RotatingFileHandler(
self.log_file_path, maxBytes=1_000_000, backupCount=3
)
logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT, handlers=[handler])

def ensure_single_instance(self):
if PLATFORM == WINDOWS:
Expand Down
3 changes: 3 additions & 0 deletions ClipCascade_Desktop/src/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ def _strip_linux_cli_overrides(argv):

# core constants
RECONNECT_WS_TIMER = 10 # seconds
RECONNECT_WS_MAX_TIMER = 60 # seconds; reconnect backoff cap
WEBSOCKET_TIMEOUT = 3000 # milliseconds
WEBSOCKET_PING_INTERVAL = 30 # seconds; WS-level keepalive to detect dead sockets
WEBSOCKET_PING_TIMEOUT = 10 # seconds; must be < WEBSOCKET_PING_INTERVAL

# P2P signaling WebSocket keepalive (RFC 6455 ping/pong).
P2P_WS_PING_INTERVAL_SEC = 25
Expand Down
14 changes: 9 additions & 5 deletions ClipCascade_Desktop/src/stomp_ws/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ def __init__(self, url, headers={}, on_close_callback=None, sslopt=None):
self.errorCallback = None

def _connect(self, timeout=0):
run_kwargs = {
"ping_interval": WEBSOCKET_PING_INTERVAL,
"ping_timeout": WEBSOCKET_PING_TIMEOUT,
}
if self._ws_sslopt:
thread = Thread(
target=lambda: self.ws.run_forever(sslopt=self._ws_sslopt)
)
else:
thread = Thread(target=self.ws.run_forever)
run_kwargs["sslopt"] = self._ws_sslopt
thread = Thread(target=lambda: self.ws.run_forever(**run_kwargs))
thread.daemon = True
thread.start()

Expand All @@ -48,6 +49,9 @@ def _connect(self, timeout=0):
time.sleep(0.25)
total_ms += 250
if 0 < timeout < total_ms:
# detach only; ws.close() here nulls self.sock under the live run_forever
# thread -> spurious "'NoneType' object has no attribute 'sock'"
self.ws.on_close = None
raise TimeoutError(f"Connection to {self.url} timed out")

def _on_open(self, ws_app, *args):
Expand Down
58 changes: 46 additions & 12 deletions ClipCascade_Desktop/src/stomp_ws/stomp_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import logging
import threading
import time
import websocket


from interfaces.ws_interface import WSInterface
Expand Down Expand Up @@ -32,6 +34,8 @@ def __init__(self, config: Config, is_login_phase=True):
self.is_connected = False
self.disconnected = False
self.is_auto_reconnecting = False
self._reconnect_lock = threading.Lock()
self._reconnect_wakeup = threading.Event()

def set_tray_ref(self, sys_tray: TaskbarPanel):
"""
Expand All @@ -50,6 +54,8 @@ def get_stats(self):

def connect(self) -> tuple[bool, str]:
try:
if self.disconnected:
return False, "Websocket disconnected"
if self.is_connected:
return True, ""
self.client = Client(
Expand All @@ -73,7 +79,7 @@ def connect(self) -> tuple[bool, str]:
self.disconnect()
return False, "Websocket disconnected"

# logging.info("Websocket connected")
logging.info("STOMP client connected")
self.is_connected = True
self.is_auto_reconnecting = False
if not self.first_conn_lost:
Expand All @@ -93,17 +99,42 @@ def connect(self) -> tuple[bool, str]:

def _on_close(self):
self.is_connected = False
# Auto Reconnect
if not self.is_login_phase and not self.disconnected:
self.is_auto_reconnecting = True
if self.first_conn_lost:
self.notification_manager.notify(
title=f"{APP_NAME}: WebSocket Connection Lost ⛓️‍💥",
message="Check your internet connection. Retrying...",
)
self.first_conn_lost = False
time.sleep(RECONNECT_WS_TIMER) # seconds
self.connect()
if self.is_login_phase or self.disconnected:
return
if not self._reconnect_lock.acquire(blocking=False):
return # a reconnect loop is already running on another socket's thread
try:
self._reconnect_loop()
finally:
self._reconnect_lock.release()

def _reconnect_loop(self):
self.is_auto_reconnecting = True
if self.first_conn_lost:
self.notification_manager.notify(
title=f"{APP_NAME}: WebSocket Connection Lost ⛓️‍💥",
message="Check your internet connection. Retrying...",
)
self.first_conn_lost = False
backoff = RECONNECT_WS_TIMER
self._reconnect_wakeup.clear()
while not self.disconnected and not self.is_connected:
logging.info(f"Websocket connection lost, reconnecting in {backoff}s")
if self._reconnect_wakeup.wait(backoff):
break # disconnect() requested
connected, msg = self.connect()
if connected:
return
logging.info(f"Reconnect attempt failed: {msg}")
backoff = min(backoff * 2, RECONNECT_WS_MAX_TIMER)
self.is_auto_reconnecting = False

def _drop_connection(self):
"""Close the socket so the on_close path drives a reconnect."""
try:
self.client.ws.close()
except Exception:
pass

def send(self, payload: str, payload_type: str = "text"):
try:
Expand All @@ -117,6 +148,8 @@ def send(self, payload: str, payload_type: str = "text"):
self.client.send(destination=SEND_DESTINATION, body=body)
except Exception as e:
logging.error(f"Failed to send data: {e}")
if isinstance(e, (websocket.WebSocketException, OSError)):
self._drop_connection()

def _receive(self, frame: any) -> str:
try:
Expand Down Expand Up @@ -150,6 +183,7 @@ def disconnect(self):
self.clipboard_manager.previous_clipboard_hash = 0
self.disconnected = True
self.first_conn_lost = True
self._reconnect_wakeup.set()
try:
self.client.disconnect()
self.is_connected = False
Expand Down