Files
GIA/docker/watch_and_restart.py

112 lines
3.6 KiB
Python

#!/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 <service> sh -c 'touch /code/core/__restart__'
The watcher ignores `__pycache__`, `.pyc` files and `.git` paths.
"""
import os
import subprocess
import sys
import time
from pathlib import Path
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()