#!/usr/bin/env python3 """ TechDJ production process manager. Usage: python production.py start — start TechDJ in the background python production.py stop — stop TechDJ python production.py restart — restart TechDJ python production.py status — show whether TechDJ is running python production.py logs — tail the live log output """ import sys import os import signal import subprocess import time import json BASE_DIR = os.path.dirname(os.path.abspath(__file__)) PID_FILE = os.path.join(BASE_DIR, 'techdj.pid') LOG_FILE = os.path.join(BASE_DIR, 'techdj.log') CONF_FILE = os.path.join(BASE_DIR, 'gunicorn.conf.py') SERVER_MOD = 'server:dj_app' # Prefer the venv's gunicorn if it exists. _venv_gunicorn = os.path.join(BASE_DIR, '.venv', 'bin', 'gunicorn') GUNICORN = _venv_gunicorn if os.path.exists(_venv_gunicorn) else 'gunicorn' # ── Helpers ─────────────────────────────────────────────────────────────────── def _load_config(): try: with open(os.path.join(BASE_DIR, 'config.json')) as f: return json.load(f) except Exception: return {} def _read_pid(): try: with open(PID_FILE) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return None def _is_running(pid): if pid is None: return False try: os.kill(pid, 0) return True except (ProcessLookupError, PermissionError): return False def _check_gunicorn(): try: subprocess.check_call( [GUNICORN, '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except (FileNotFoundError, subprocess.CalledProcessError): print(f"ERROR: gunicorn not found at '{GUNICORN}'.") print(" Install it with: pip install gunicorn") sys.exit(1) # ── Commands ────────────────────────────────────────────────────────────────── def start(): pid = _read_pid() if _is_running(pid): print(f"TechDJ is already running (PID {pid})") return _check_gunicorn() cfg = _load_config() host = cfg.get('host', '0.0.0.0') port = cfg.get('dj_port', 5000) lport = cfg.get('listener_port', 5001) print(f"Starting TechDJ...") print(f" DJ panel : http://{host}:{port}") print(f" Listener : http://{host}:{lport}") print(f" Log file : {LOG_FILE}") cmd = [ GUNICORN, '--config', CONF_FILE, '--daemon', '--pid', PID_FILE, '--access-logfile', LOG_FILE, '--error-logfile', LOG_FILE, SERVER_MOD, ] try: subprocess.check_call(cmd, cwd=BASE_DIR) except subprocess.CalledProcessError as exc: print(f"ERROR: gunicorn exited with code {exc.returncode}. Check {LOG_FILE}") sys.exit(1) # Give gunicorn a moment to write the PID file. for _ in range(10): time.sleep(0.5) pid = _read_pid() if _is_running(pid): break else: print(f"ERROR: TechDJ did not start. Check logs:") _tail_log(20) sys.exit(1) print(f"TechDJ started (PID {pid})") def stop(): pid = _read_pid() if not _is_running(pid): print("TechDJ is not running.") if os.path.exists(PID_FILE): os.remove(PID_FILE) return print(f"Stopping TechDJ (PID {pid})...") os.kill(pid, signal.SIGTERM) for _ in range(20): time.sleep(0.5) if not _is_running(pid): break else: print(" Graceful shutdown timed out — sending SIGKILL.") try: os.kill(pid, signal.SIGKILL) except ProcessLookupError: pass if os.path.exists(PID_FILE): os.remove(PID_FILE) print("TechDJ stopped.") def restart(): stop() time.sleep(1) start() def status(): pid = _read_pid() if _is_running(pid): cfg = _load_config() host = cfg.get('host', '0.0.0.0') port = cfg.get('dj_port', 5000) lport = cfg.get('listener_port', 5001) print(f"TechDJ is running (PID {pid})") print(f" DJ panel : http://{host}:{port}") print(f" Listener : http://{host}:{lport}") print(f" Log file : {LOG_FILE}") else: print("TechDJ is NOT running.") def _tail_log(lines=50): if not os.path.exists(LOG_FILE): print(f"No log file at {LOG_FILE}") return subprocess.call(['tail', f'-{lines}', LOG_FILE]) def logs(): if not os.path.exists(LOG_FILE): print(f"No log file at {LOG_FILE}") sys.exit(1) # Replace this process with tail -f so Ctrl+C works cleanly. os.execvp('tail', ['tail', '-f', LOG_FILE]) # ── Entry point ─────────────────────────────────────────────────────────────── COMMANDS = { 'start': start, 'stop': stop, 'restart': restart, 'status': status, 'logs': logs, } if __name__ == '__main__': cmd = sys.argv[1] if len(sys.argv) > 1 else 'status' if cmd not in COMMANDS: print(f"Usage: python production.py [{'|'.join(COMMANDS)}]") sys.exit(1) COMMANDS[cmd]()