| |
| """ |
| 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 |
|
|
| |
| import psycopg2 |
| from psycopg2.extras import DictCursor |
| from dotenv import load_dotenv |
|
|
| load_dotenv() |
|
|
| |
| 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 |
| |
| 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: |
| |
| 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) |
| |
| 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() |
|
|