File changer never ran - changed.
This commit is contained in:
25
CLAUDE.md
25
CLAUDE.md
@@ -28,6 +28,7 @@ The ESPHome container remains immutable and handles all compilation/deployment o
|
|||||||
- Tracks last modification time per file in `ESPHomeFileHandler.last_modified`
|
- Tracks last modification time per file in `ESPHomeFileHandler.last_modified`
|
||||||
- Monitors all `.yaml`/`.yml` files directly in the config directory
|
- Monitors all `.yaml`/`.yml` files directly in the config directory
|
||||||
- Optionally triggers `git push` when `AUTO_PUSH=true`
|
- Optionally triggers `git push` when `AUTO_PUSH=true`
|
||||||
|
- In production (Gunicorn), started via `post_fork` hook in first worker only
|
||||||
|
|
||||||
3. **Git Operations** (app.py)
|
3. **Git Operations** (app.py)
|
||||||
- `git_clone()` - Initial clone of Gitea repository on startup
|
- `git_clone()` - Initial clone of Gitea repository on startup
|
||||||
@@ -59,6 +60,15 @@ The service expects a flat structure with each device having its own `.yaml` fil
|
|||||||
- Prevents concurrent operations on the same device
|
- Prevents concurrent operations on the same device
|
||||||
- Operations run in separate threads spawned from file watcher or webhook handlers
|
- Operations run in separate threads spawned from file watcher or webhook handlers
|
||||||
|
|
||||||
|
## Project Files
|
||||||
|
|
||||||
|
- `app.py` - Main Flask application with file watcher, git operations, and API endpoints
|
||||||
|
- `gunicorn_config.py` - Gunicorn configuration with hooks for file watcher initialization
|
||||||
|
- `requirements.txt` - Python dependencies
|
||||||
|
- `Dockerfile` - Container image definition
|
||||||
|
- `docker-compose.yml` - Multi-container orchestration (webhook + esphome services)
|
||||||
|
- `CLAUDE.md` - This documentation file
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Local Development (Without Docker)
|
### Local Development (Without Docker)
|
||||||
@@ -259,12 +269,25 @@ On startup, the service:
|
|||||||
3. If yes, configures git user and pulls latest changes
|
3. If yes, configures git user and pulls latest changes
|
||||||
4. All git operations have 60-second timeout
|
4. All git operations have 60-second timeout
|
||||||
|
|
||||||
### Gunicorn Configuration (Dockerfile)
|
### Gunicorn Configuration
|
||||||
|
|
||||||
|
**Configuration File:** `gunicorn_config.py`
|
||||||
|
|
||||||
- 2 workers
|
- 2 workers
|
||||||
- 600 second timeout
|
- 600 second timeout
|
||||||
- Binds to 0.0.0.0:5000
|
- Binds to 0.0.0.0:5000
|
||||||
|
|
||||||
|
**Hooks for File Watcher:**
|
||||||
|
- `on_starting` hook: Initializes git repository once before workers are forked
|
||||||
|
- `post_fork` hook: Starts file watcher thread in the first worker only (worker.age == 0)
|
||||||
|
- This ensures the file watcher runs correctly under Gunicorn without duplicates
|
||||||
|
|
||||||
|
**Why hooks are needed:**
|
||||||
|
When running under Gunicorn, the `if __name__ == '__main__':` block in app.py is never executed. The hooks ensure that:
|
||||||
|
1. Git initialization happens once at startup
|
||||||
|
2. File watcher starts in exactly one worker process
|
||||||
|
3. Multiple workers don't create duplicate file watchers
|
||||||
|
|
||||||
## Docker Compose Architecture
|
## Docker Compose Architecture
|
||||||
|
|
||||||
Two services work together:
|
Two services work together:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
COPY gunicorn_config.py .
|
||||||
|
|
||||||
# Create config directory
|
# Create config directory
|
||||||
RUN mkdir -p /config
|
RUN mkdir -p /config
|
||||||
@@ -27,9 +28,7 @@ EXPOSE 5000
|
|||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
ENV ESPHOME_CONFIG_DIR=/config
|
ENV ESPHOME_CONFIG_DIR=/config
|
||||||
ENV AUTO_COMPILE=true
|
|
||||||
ENV AUTO_UPLOAD=false
|
|
||||||
ENV DEBOUNCE_SECONDS=5
|
ENV DEBOUNCE_SECONDS=5
|
||||||
|
|
||||||
# Run the webhook service
|
# Run the webhook service with Gunicorn config
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "600", "app:app"]
|
CMD ["gunicorn", "-c", "gunicorn_config.py", "app:app"]
|
||||||
|
|||||||
58
gunicorn_config.py
Normal file
58
gunicorn_config.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Gunicorn configuration file for ESPHome Gitea Sync Service"""
|
||||||
|
import logging
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
# Gunicorn configuration
|
||||||
|
bind = "0.0.0.0:5000"
|
||||||
|
workers = 2
|
||||||
|
timeout = 600
|
||||||
|
loglevel = "info"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Track if file watcher has been started
|
||||||
|
_watcher_started = False
|
||||||
|
|
||||||
|
|
||||||
|
def on_starting(server):
|
||||||
|
"""
|
||||||
|
Called just before the master process is initialized.
|
||||||
|
Perfect for one-time setup like git initialization.
|
||||||
|
"""
|
||||||
|
logger.info("Gunicorn master process starting - initializing git repository")
|
||||||
|
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from app import initialize_git_repo
|
||||||
|
|
||||||
|
try:
|
||||||
|
initialize_git_repo()
|
||||||
|
logger.info("Git repository initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize git repository: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def post_fork(server, worker):
|
||||||
|
"""
|
||||||
|
Called after a worker has been forked.
|
||||||
|
Start the file watcher only in the first worker to avoid duplicates.
|
||||||
|
"""
|
||||||
|
global _watcher_started
|
||||||
|
|
||||||
|
# Only start the file watcher in the first worker (age/number would be 0)
|
||||||
|
# Check worker.age which is the worker's sequential number
|
||||||
|
if worker.age == 0 and not _watcher_started:
|
||||||
|
_watcher_started = True
|
||||||
|
logger.info(f"Starting file watcher in worker {worker.pid}")
|
||||||
|
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from app import start_file_watcher
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start file watcher in a daemon thread
|
||||||
|
watcher_thread = Thread(target=start_file_watcher, daemon=True)
|
||||||
|
watcher_thread.start()
|
||||||
|
logger.info("File watcher started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start file watcher: {e}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Worker {worker.pid} started (file watcher already running in another worker)")
|
||||||
Reference in New Issue
Block a user