⚠️ CRITICAL: Clean up stale PlatformIO lock files that block builds. PlatformIO creates .download.lock files but doesn't always clean them up when processes crash or are killed. This causes future builds to hang indefinitely waiting for locks that will never be released. This
(max_age_minutes: float = 30)
| 279 | |
| 280 | |
| 281 | def cleanup_stale_platformio_locks(max_age_minutes: float = 30) -> int: |
| 282 | """ |
| 283 | ⚠️ CRITICAL: Clean up stale PlatformIO lock files that block builds. |
| 284 | |
| 285 | PlatformIO creates .download.lock files but doesn't always clean them up |
| 286 | when processes crash or are killed. This causes future builds to hang |
| 287 | indefinitely waiting for locks that will never be released. |
| 288 | |
| 289 | This function removes lock files older than max_age_minutes from: |
| 290 | 1. PlatformIO's global cache directory (~/.platformio/global_cache) |
| 291 | 2. FastLED's global cache directory (~/.fastled/global_cache) |
| 292 | |
| 293 | Args: |
| 294 | max_age_minutes: Maximum age of locks to keep (default: 30 minutes) |
| 295 | |
| 296 | Returns: |
| 297 | Number of stale locks removed |
| 298 | """ |
| 299 | cache_dirs = [ |
| 300 | Path.home() / ".platformio" / "global_cache", |
| 301 | Path.home() / ".fastled" / "global_cache", |
| 302 | ] |
| 303 | |
| 304 | cleaned_count = 0 |
| 305 | max_age_seconds = max_age_minutes * 60 |
| 306 | current_time = time.time() |
| 307 | |
| 308 | for cache_dir in cache_dirs: |
| 309 | if not cache_dir.exists(): |
| 310 | continue |
| 311 | |
| 312 | try: |
| 313 | # Find all .lock files recursively |
| 314 | for lock_file in cache_dir.rglob("*.lock"): |
| 315 | try: |
| 316 | # Check file age |
| 317 | age_seconds = current_time - lock_file.stat().st_mtime |
| 318 | |
| 319 | if age_seconds > max_age_seconds: |
| 320 | lock_file.unlink() |
| 321 | age_minutes = age_seconds / 60 |
| 322 | logger.info( |
| 323 | f"Cleaned stale lock: {lock_file.name} " |
| 324 | f"from {cache_dir.name} (age: {age_minutes:.1f} minutes)" |
| 325 | ) |
| 326 | cleaned_count += 1 |
| 327 | |
| 328 | # Also clean up associated .pid metadata files |
| 329 | pid_file = lock_file.with_suffix(lock_file.suffix + ".pid") |
| 330 | if pid_file.exists(): |
| 331 | pid_file.unlink() |
| 332 | |
| 333 | except (OSError, PermissionError) as e: |
| 334 | logger.debug(f"Could not remove lock {lock_file}: {e}") |
| 335 | |
| 336 | except KeyboardInterrupt as ki: |
| 337 | handle_keyboard_interrupt(ki) |
| 338 | raise |