#!/usr/bin/env python3 """ FastAPI server that wraps xm_launcher. On startup, the miner auto-starts in a background thread. The FastAPI server runs alongside it via uvicorn. """ import os import sys import threading from pathlib import Path from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.responses import JSONResponse # Database imports import psycopg2 from psycopg2.extras import DictCursor from dotenv import load_dotenv load_dotenv() # Import the miner launcher from xm_launcher import ( XMRigLauncher, _stop_event, ) BASE_DIR = Path(__file__).parent.absolute() def get_db_conn(): db_url = os.getenv("DATABASE_URL") if not db_url: return None try: return psycopg2.connect(db_url) except Exception as e: print(f"[api] DB Connect Error: {e}", flush=True) return None BASE_DIR = Path(__file__).parent.absolute() def run_miner_background(): """Run the miner in a background thread.""" try: db_url = os.getenv("DATABASE_URL") if not db_url: print("[server] ERROR: DATABASE_URL not set in environment!", flush=True) return launcher = XMRigLauncher() print("[server] Starting miner in background (DB Mode)...", flush=True) launcher.start_pool_mining(db_url) except Exception as e: print(f"[server] Miner error: {e}", flush=True) import traceback traceback.print_exc() @asynccontextmanager async def lifespan(app: FastAPI): """Start the miner on server startup.""" miner_thread = threading.Thread(target=run_miner_background, daemon=True) miner_thread.start() print("[server] Miner thread started", flush=True) yield # Shutdown print("[server] Shutting down miner...", flush=True) _stop_event.set() app = FastAPI(title="Mineo v4", lifespan=lifespan) @app.get("/") async def root(): return {"status": "running", "service": "mineo_v4"} @app.get("/health") async def health(): return {"status": "ok"} @app.get("/job") async def current_job(): """Return the current active pool job from the database.""" conn = get_db_conn() if not conn: return JSONResponse(status_code=500, content={"error": "Database connection failed"}) try: cur = conn.cursor(cursor_factory=DictCursor) cur.execute("SELECT * FROM mining_jobs ORDER BY id DESC LIMIT 1") row = cur.fetchone() if row: # Convert row to dict return {k: v for k, v in row.items()} return JSONResponse(status_code=404, content={"error": "No jobs in database"}) finally: conn.close() @app.get("/result") async def current_result(): """Return the latest mining result from the database.""" conn = get_db_conn() if not conn: return JSONResponse(status_code=500, content={"error": "Database connection failed"}) try: cur = conn.cursor(cursor_factory=DictCursor) cur.execute("SELECT * FROM mining_results ORDER BY id DESC LIMIT 1") row = cur.fetchone() if row: row_dict = dict(row) # Serialize datetimes to ISO format for JSON compatibility if row_dict.get('found_at'): row_dict['found_at'] = row_dict['found_at'].isoformat() if row_dict.get('submitted_at'): row_dict['submitted_at'] = row_dict['submitted_at'].isoformat() return row_dict return JSONResponse(status_code=404, content={"error": "No results yet"}) finally: conn.close()