diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 64c0cb6..555bcb5 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -35,10 +35,17 @@ 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 + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Determine Docker tags and cache id: docker_tags @@ -145,10 +152,17 @@ jobs: platform: linux/arm64 platform_tag: arm64 steps: + - name: Clean workspace + if: always() + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -192,7 +206,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 @@ -229,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' @@ -252,6 +270,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Generate commit history id: changelog @@ -350,6 +369,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + clean: false - name: Collect commit information id: collect @@ -585,4 +605,4 @@ jobs: body: newBody }); - console.log(`✅ Updated PR #${context.issue.number}`); \ No newline at end of file + console.log(`✅ Updated PR #${context.issue.number}`); diff --git a/.github/workflows/snd.yml b/.github/workflows/snd.yml index e8384db..fdb1174 100644 --- a/.github/workflows/snd.yml +++ b/.github/workflows/snd.yml @@ -23,10 +23,17 @@ jobs: platform: linux/arm64 platform_tag: arm64 steps: + - name: Clean workspace + if: always() + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} || true + sudo rm -rf ${{ github.workspace }}/* ${{ github.workspace }}/.* 2>/dev/null || true + - name: Checkout repository uses: actions/checkout@v4 with: ref: snd + clean: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -70,7 +77,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 +111,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 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: 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