201 lines
5.5 KiB
Python
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]()
|