feat: Add server management and database migrations
- Add start/stop/restart/status commands to Makefile - Add health endpoint with uptime tracking - Add CLI module (esp32-web command) - Add initial database migration - Listen on all interfaces (0.0.0.0:5500) Bump version to 0.1.1
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
"""ESP32-Web Flask Application."""
|
||||
from datetime import datetime, UTC
|
||||
from flask import Flask
|
||||
|
||||
from .config import Config
|
||||
from .extensions import db, migrate
|
||||
|
||||
# Track app start time
|
||||
_start_time = None
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
"""Application factory."""
|
||||
global _start_time
|
||||
_start_time = datetime.now(UTC)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
@@ -18,10 +25,20 @@ def create_app(config_class=Config):
|
||||
from .api import bp as api_bp
|
||||
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
||||
|
||||
# Health check
|
||||
# Health check with uptime
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return {'status': 'ok'}
|
||||
uptime_seconds = int((datetime.now(UTC) - _start_time).total_seconds())
|
||||
days, remainder = divmod(uptime_seconds, 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
if days > 0:
|
||||
uptime_str = f'{days}d{hours}h{minutes}m'
|
||||
elif hours > 0:
|
||||
uptime_str = f'{hours}h{minutes}m{seconds}s'
|
||||
else:
|
||||
uptime_str = f'{minutes}m{seconds}s'
|
||||
return {'status': 'ok', 'uptime': uptime_str, 'uptime_seconds': uptime_seconds}
|
||||
|
||||
# Start UDP collector in non-testing mode
|
||||
if not app.config.get('TESTING'):
|
||||
|
||||
139
src/esp32_web/cli.py
Normal file
139
src/esp32_web/cli.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""CLI commands for managing the application."""
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
PIDFILE = Path('/tmp/esp32-web.pid')
|
||||
LOGFILE = Path('/tmp/esp32-web.log')
|
||||
|
||||
|
||||
def get_pid() -> int | None:
|
||||
"""Get PID from pidfile if running."""
|
||||
if not PIDFILE.exists():
|
||||
return None
|
||||
try:
|
||||
pid = int(PIDFILE.read_text().strip())
|
||||
# Check if process exists
|
||||
os.kill(pid, 0)
|
||||
return pid
|
||||
except (ValueError, ProcessLookupError, PermissionError):
|
||||
PIDFILE.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
|
||||
def is_running() -> bool:
|
||||
"""Check if server is running."""
|
||||
return get_pid() is not None
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
"""ESP32-Web server management."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--port', default=5500, help='Port to listen on')
|
||||
@click.option('--host', default='0.0.0.0', help='Host to bind to')
|
||||
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
||||
def start(port: int, host: str, debug: bool):
|
||||
"""Start the server."""
|
||||
if is_running():
|
||||
click.echo(f'Server already running (PID {get_pid()})')
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(f'Starting server on {host}:{port}...')
|
||||
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# Parent - write pidfile and exit
|
||||
PIDFILE.write_text(str(pid))
|
||||
click.echo(f'Server started (PID {pid})')
|
||||
click.echo(f'Logs: {LOGFILE}')
|
||||
sys.exit(0)
|
||||
|
||||
# Child - become daemon
|
||||
os.setsid()
|
||||
|
||||
# Redirect stdout/stderr to logfile
|
||||
log_fd = os.open(str(LOGFILE), os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
|
||||
os.dup2(log_fd, sys.stdout.fileno())
|
||||
os.dup2(log_fd, sys.stderr.fileno())
|
||||
|
||||
# Import and run app
|
||||
from esp32_web import create_app
|
||||
app = create_app()
|
||||
app.run(host=host, port=port, debug=debug, use_reloader=False)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def stop():
|
||||
"""Stop the server."""
|
||||
pid = get_pid()
|
||||
if not pid:
|
||||
click.echo('Server not running')
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(f'Stopping server (PID {pid})...')
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
PIDFILE.unlink(missing_ok=True)
|
||||
click.echo('Server stopped')
|
||||
except ProcessLookupError:
|
||||
PIDFILE.unlink(missing_ok=True)
|
||||
click.echo('Server was not running')
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--port', default=5500, help='Port to listen on')
|
||||
@click.option('--host', default='0.0.0.0', help='Host to bind to')
|
||||
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
||||
@click.pass_context
|
||||
def restart(ctx, port: int, host: str, debug: bool):
|
||||
"""Restart the server."""
|
||||
if is_running():
|
||||
ctx.invoke(stop)
|
||||
import time
|
||||
time.sleep(1)
|
||||
ctx.invoke(start, port=port, host=host, debug=debug)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def status():
|
||||
"""Show server status."""
|
||||
pid = get_pid()
|
||||
if pid:
|
||||
click.echo(f'Server running (PID {pid})')
|
||||
click.echo(f'Logs: {LOGFILE}')
|
||||
# Try to get health status
|
||||
try:
|
||||
import urllib.request
|
||||
with urllib.request.urlopen('http://localhost:5500/health', timeout=2) as resp:
|
||||
click.echo(f'Health: {resp.read().decode()}')
|
||||
except Exception:
|
||||
click.echo('Health: unreachable')
|
||||
else:
|
||||
click.echo('Server not running')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('-n', '--lines', default=50, help='Number of lines to show')
|
||||
@click.option('-f', '--follow', is_flag=True, help='Follow log output')
|
||||
def logs(lines: int, follow: bool):
|
||||
"""Show server logs."""
|
||||
if not LOGFILE.exists():
|
||||
click.echo('No logs found')
|
||||
sys.exit(1)
|
||||
|
||||
if follow:
|
||||
os.execvp('tail', ['tail', '-f', str(LOGFILE)])
|
||||
else:
|
||||
os.execvp('tail', ['tail', '-n', str(lines), str(LOGFILE)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
Reference in New Issue
Block a user