#!/usr/bin/env python3 """ Watch for code changes and restart a target container. This script watches one or more directories (set via the `WATCH_PATHS` environment variable, comma-separated) and restarts the container named by `TARGET_CONTAINER` (defaults to `ur_gia`) when a filesystem change is detected. Typical usage in this repo (examples are provided in `docker-compose.yml`): - To restart the runtime router (UR) when core code changes set: WATCH_PATHS=/code/core TARGET_CONTAINER=ur_gia - To restart the scheduling command when app code changes set: WATCH_PATHS=/code/app TARGET_CONTAINER=scheduling_gia If you need to force a restart manually (for example to refresh a running management command), you can "touch" any file under the watched path: docker compose exec sh -c 'touch /code/core/__restart__' The watcher ignores `__pycache__`, `.pyc` files and `.git` paths. """ import os import subprocess import time from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer class ChangeHandler(FileSystemEventHandler): def __init__(self): self.last_restart = 0 self.restart_debounce = 2 # seconds def on_modified(self, event): self._check_and_restart(event.src_path) def on_created(self, event): self._check_and_restart(event.src_path) def on_deleted(self, event): self._check_and_restart(event.src_path) def _check_and_restart(self, path): # Ignore pycache and compiled files if "__pycache__" in path or ".pyc" in path or ".git" in path: return now = time.time() if now - self.last_restart < self.restart_debounce: return self.last_restart = now print(f'[{time.strftime("%H:%M:%S")}] Change detected: {path}', flush=True) time.sleep(1) self._restart_ur() def _restart_ur(self): # Determine target container from environment (default `ur_gia`) target = os.environ.get("TARGET_CONTAINER", "ur_gia") print(f'[{time.strftime("%H:%M:%S")}] Restarting {target}...', flush=True) # Try podman first (preferred in this setup), then docker cmd = f"podman restart {target} 2>/dev/null || docker restart {target} 2>/dev/null" result = subprocess.run(cmd, shell=True, capture_output=True) if result.returncode == 0: print( f'[{time.strftime("%H:%M:%S")}] {target} restarted successfully', flush=True, ) else: print(f'[{time.strftime("%H:%M:%S")}] {target} restart failed', flush=True) time.sleep(1) def main(): handler = ChangeHandler() observer = Observer() # Allow overriding watched paths via environment variable `WATCH_PATHS`. # Default is `/code/core,/code/app` but you can set e.g. `WATCH_PATHS=/code/core` watch_paths_env = os.environ.get("WATCH_PATHS", "/code/core,/code/app") watch_paths = [p.strip() for p in watch_paths_env.split(",") if p.strip()] for path in watch_paths: if os.path.exists(path): observer.schedule(handler, path, recursive=True) print(f"Watching: {path}", flush=True) else: print(f"Not found (will not watch): {path}", flush=True) observer.start() print( f'[{time.strftime("%H:%M:%S")}] File watcher started. Monitoring for changes...', flush=True, ) try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()