techdj/production.py

201 lines
5.5 KiB
Python

#!/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]()