From f06fdada87b5d53a654c18e904eefce5a5b6dda4 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Mon, 5 Jan 2026 17:00:22 -0600 Subject: [PATCH 1/6] feat: add multi-client verification tests for safety monitor --- verify_multi_client.py => tests/verify_multi_client.py | 0 verify_config.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename verify_multi_client.py => tests/verify_multi_client.py (100%) delete mode 100644 verify_config.py diff --git a/verify_multi_client.py b/tests/verify_multi_client.py similarity index 100% rename from verify_multi_client.py rename to tests/verify_multi_client.py diff --git a/verify_config.py b/verify_config.py deleted file mode 100644 index e69de29..0000000 From fc8c123488fd20381e4385d40b4d32250584fecb Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Tue, 6 Jan 2026 09:29:38 -0600 Subject: [PATCH 2/6] bug: fix client connection duration timer resetting every second --- alpaca/device.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/alpaca/device.py b/alpaca/device.py index bd2cee4..6f1c910 100644 --- a/alpaca/device.py +++ b/alpaca/device.py @@ -32,7 +32,8 @@ def __init__(self, alpaca_config: AlpacaConfig, detect_config: DetectConfig): self.transaction_lock = threading.Lock() # Device state - self.connected_clients: Dict[Tuple[str, int], datetime] = {} # (IP, ClientID) -> ConnectionTime + self.connected_clients: Dict[Tuple[str, int], datetime] = {} # (IP, ClientID) -> Connection Start Time + self.client_last_seen: Dict[Tuple[str, int], datetime] = {} # (IP, ClientID) -> Last Heartbeat Time self.disconnected_clients: Dict[Tuple[str, int], Tuple[datetime, datetime]] = {} # (IP, ClientID) -> (ConnectionTime, DisconnectionTime) self.connection_lock = threading.Lock() self.connecting = False @@ -167,24 +168,36 @@ def _prune_stale_clients(self): cutoff_time = now - timedelta(seconds=CLIENT_TIMEOUT_SECONDS) stale_clients = [] - for key, last_seen in list(self.connected_clients.items()): + # Check last_seen for staleness, not initial connection time + for key, last_seen in list(self.client_last_seen.items()): if last_seen < cutoff_time: stale_clients.append(key) for key in stale_clients: client_ip, client_id = key - conn_time = self.connected_clients[key] + + # Retrieve original connection time for the record + conn_time = self.connected_clients.get(key, now) + + # Move to disconnected list self.disconnected_clients[key] = (conn_time, now) - del self.connected_clients[key] + + # Remove from active tracking + if key in self.connected_clients: + del self.connected_clients[key] + if key in self.client_last_seen: + del self.client_last_seen[key] + logger.warning(f"Watchdog: Pruned stale client {client_ip} (ID: {client_id}) - " - f"inactive for {(now - conn_time).total_seconds():.0f}s") + f"inactive for {(now - last_seen).total_seconds():.0f}s") def register_heartbeat(self, client_ip: str, client_id: int): """Update the last seen timestamp for a connected client""" with self.connection_lock: key = (client_ip, client_id) if key in self.connected_clients: - self.connected_clients[key] = get_current_time(self.alpaca_config.timezone) + # Only update last_seen, preserve connected_clients (start time) + self.client_last_seen[key] = get_current_time(self.alpaca_config.timezone) def _setup_mqtt(self): """Setup and return MQTT client based on detect_config""" @@ -334,7 +347,9 @@ def connect(self, client_ip: str, client_id: int): """Connect a client to the device""" with self.connection_lock: key = (client_ip, client_id) - self.connected_clients[key] = get_current_time(self.alpaca_config.timezone) + current_time = get_current_time(self.alpaca_config.timezone) + self.connected_clients[key] = current_time + self.client_last_seen[key] = current_time # Remove from disconnected clients if reconnecting if key in self.disconnected_clients: @@ -358,6 +373,7 @@ def disconnect(self, client_ip: str = None, client_id: int = None): disc_time = get_current_time(self.alpaca_config.timezone) self.disconnected_clients[key] = (conn_time, disc_time) self.connected_clients.clear() + self.client_last_seen.clear() self.disconnected_at = disc_time if self.connected_at: duration = (self.disconnected_at - self.connected_at).total_seconds() @@ -372,6 +388,9 @@ def disconnect(self, client_ip: str = None, client_id: int = None): self.disconnected_clients[key] = (conn_time, disc_time) del self.connected_clients[key] + if key in self.client_last_seen: + del self.client_last_seen[key] + logger.info(f"Client disconnected: {client_ip} (ID: {client_id}). Total clients: {len(self.connected_clients)}") if len(self.connected_clients) == 0: From f018710c367cc0f8c7924af7107de6eb4a4f9857 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Tue, 6 Jan 2026 09:34:54 -0600 Subject: [PATCH 3/6] fix: resolve client duration reset bug and workflow issues - Separate connection start time from last heartbeat tracking - Add client_last_seen dictionary for heartbeat timestamps - Fix register_heartbeat to only update last_seen, preserving connection start time - Update _prune_stale_clients to use last_seen for timeout detection - Fix workflow cache move step to check directory existence - Add workspace cleanup step to fix permission issues --- .github/workflows/snd.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/snd.yml b/.github/workflows/snd.yml index e8384db..229253f 100644 --- a/.github/workflows/snd.yml +++ b/.github/workflows/snd.yml @@ -23,6 +23,12 @@ jobs: platform: linux/arm64 platform_tag: arm64 steps: + - name: Clean workspace + if: always() + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: @@ -70,7 +76,9 @@ jobs: if: always() run: | rm -rf ${{ env.CACHE_PATH }} - mv ${{ env.CACHE_PATH_NEW }} ${{ env.CACHE_PATH }} + if [ -d "${{ env.CACHE_PATH_NEW }}" ]; then + mv ${{ env.CACHE_PATH_NEW }} ${{ env.CACHE_PATH }} + fi merge: name: Merge Multi-Arch Image @@ -102,5 +110,3 @@ jobs: docker pull ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:snd IMAGE_SIZE=$(docker images --format "{{.Size}}" ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:snd | head -n1) echo "Docker image size: $IMAGE_SIZE" - - \ No newline at end of file From 1b384b63af2eb939483428a1bc8418e8b66157c7 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Tue, 6 Jan 2026 10:06:13 -0600 Subject: [PATCH 4/6] fix: apply workflow fixes to build-and-release.yml - Add workspace cleanup step to fix permission issues - Add directory existence check in cache move step - Same fixes applied to snd.yml earlier --- .github/workflows/build-and-release.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 64c0cb6..308b627 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -35,6 +35,12 @@ jobs: release_type: ${{ steps.version.outputs.release_type }} docker_tag: ${{ steps.docker_tags.outputs.tag }} steps: + - name: Clean workspace + if: always() + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: @@ -145,6 +151,12 @@ jobs: platform: linux/arm64 platform_tag: arm64 steps: + - name: Clean workspace + if: always() + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: @@ -192,7 +204,9 @@ jobs: if: always() run: | rm -rf ${{ env.CACHE_PATH }} - mv ${{ env.CACHE_PATH_NEW }} ${{ env.CACHE_PATH }} + if [ -d "${{ env.CACHE_PATH_NEW }}" ]; then + mv ${{ env.CACHE_PATH_NEW }} ${{ env.CACHE_PATH }} + fi merge: name: Merge Multi-Arch Image @@ -585,4 +599,4 @@ jobs: body: newBody }); - console.log(`✅ Updated PR #${context.issue.number}`); \ No newline at end of file + console.log(`✅ Updated PR #${context.issue.number}`); From 249fc8ba24bc6d14d6cc152fce9568cfe9d5aca5 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Tue, 6 Jan 2026 10:14:30 -0600 Subject: [PATCH 5/6] fix: prevent workspace cleanup during checkout in build workflows --- .github/workflows/build-and-release.yml | 6 ++++++ .github/workflows/snd.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 308b627..b3cac5f 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -45,6 +45,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Determine Docker tags and cache id: docker_tags @@ -161,6 +162,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -243,6 +245,8 @@ jobs: - name: Checkout repository if: github.ref == 'refs/heads/main' uses: actions/checkout@v4 + with: + clean: false - name: Docker Hub Description if: github.ref == 'refs/heads/main' @@ -266,6 +270,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Generate commit history id: changelog @@ -364,6 +369,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Collect commit information id: collect diff --git a/.github/workflows/snd.yml b/.github/workflows/snd.yml index 229253f..d4cc18a 100644 --- a/.github/workflows/snd.yml +++ b/.github/workflows/snd.yml @@ -33,6 +33,7 @@ jobs: uses: actions/checkout@v4 with: ref: snd + clean: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 2913852ec4fb78d9ecf9f8f0ab163af2ff8a4aa5 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Tue, 6 Jan 2026 10:20:24 -0600 Subject: [PATCH 6/6] fix: use sudo for workspace cleanup in build workflows --- .github/workflows/build-and-release.yml | 4 ++-- .github/workflows/snd.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index b3cac5f..555bcb5 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -39,7 +39,7 @@ jobs: if: always() run: | sudo chown -R $USER:$USER ${{ github.workspace }} || true - rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true - name: Checkout repository uses: actions/checkout@v4 @@ -156,7 +156,7 @@ jobs: if: always() run: | sudo chown -R $USER:$USER ${{ github.workspace }} || true - rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/snd.yml b/.github/workflows/snd.yml index d4cc18a..fdb1174 100644 --- a/.github/workflows/snd.yml +++ b/.github/workflows/snd.yml @@ -27,7 +27,7 @@ jobs: if: always() run: | sudo chown -R $USER:$USER ${{ github.workspace }} || true - rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true - name: Checkout repository uses: actions/checkout@v4