-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.py
More file actions
131 lines (113 loc) Β· 5.06 KB
/
Copy pathrun.py
File metadata and controls
131 lines (113 loc) Β· 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# -*- coding: utf-8 -*-
# ============================================================================ #
# DockerDiscordControl (DDC) #
# https://ddc.bot #
# Copyright (c) 2025 MAX #
# Licensed under the MIT License #
# ============================================================================ #
"""
Single Entry Point for DockerDiscordControl (DDC).
Starts both the Web UI (via Waitress) and the Discord Bot in a single process.
"""
import errno
import threading
import logging
import sys
import os
import time
from waitress import serve
from app.bootstrap.runtime import get_web_port
from app.web_ui import create_app
from bot import main as run_bot, install_sigterm_handler
from utils.logging_utils import get_module_logger
# Setup logger
logger = get_module_logger("ddc.main")
# A port that is still in use (host network, previous instance shutting down) gets
# a few more bind attempts before the process gives up.
WEB_BIND_ATTEMPTS = 5
WEB_BIND_RETRY_DELAY = 3
def _terminate_process(exit_code: int) -> None:
"""Exit the whole process immediately, whichever thread calls this."""
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except (OSError, ValueError):
pass
os._exit(exit_code)
def start_web_server():
"""Starts the Flask Web UI using Waitress in a separate thread."""
try:
port = get_web_port()
logger.info(f"π Starting Web UI via Waitress on port {port}...")
# Create Flask app
app = create_app()
# Scale waitress thread pool to host CPUs (clamped 4..8) so concurrent
# requests don't queue behind a fixed 4-thread pool. Override possible
# via DDC_WAITRESS_THREADS for tuning on small/large hosts.
cpu_count = os.cpu_count() or 4
try:
threads = int(os.environ.get("DDC_WAITRESS_THREADS", "0")) or max(4, min(8, cpu_count))
except (TypeError, ValueError):
threads = max(4, min(8, cpu_count))
threads = max(2, min(16, threads))
logger.info(f"Waitress thread pool size: {threads} (cpu_count={cpu_count})")
for attempt in range(1, WEB_BIND_ATTEMPTS + 1):
try:
serve(
app,
host="0.0.0.0",
port=port,
threads=threads,
ident="DDC-Web",
_quiet=True # Reduce waitress startup logs
)
break
except OSError as e:
if e.errno != errno.EADDRINUSE:
raise
if attempt == WEB_BIND_ATTEMPTS:
logger.critical(
f"π₯ Port {port} is already in use. With host networking set DDC_WEB_PORT "
f"to a free port; with bridge networking change the host port mapping."
)
raise
logger.warning(
f"Port {port} is already in use (attempt {attempt}/{WEB_BIND_ATTEMPTS}) - "
f"retrying in {WEB_BIND_RETRY_DELAY} s..."
)
time.sleep(WEB_BIND_RETRY_DELAY)
logger.critical("π₯ Web Server stopped unexpectedly")
except Exception as e:
logger.critical(f"π₯ Web Server failed to start: {e}", exc_info=True)
# sys.exit() here would only end this daemon thread and leave the container
# "Up" without a web UI (the only place to fix the configuration). Terminate
# the whole process instead so Docker's restart policy restarts the container.
logger.critical("π Web UI is not running - terminating DDC so the container gets restarted")
_terminate_process(1)
def main():
"""Main execution flow."""
# run.py is PID 1 in the container: without a handler SIGTERM is ignored and
# `docker stop` waits 10 s before killing. py-cord installs its own handler
# once bot.run() starts.
install_sigterm_handler(logger)
logger.info("==================================================")
logger.info(" DockerDiscordControl (DDC) - Startup Sequence ")
logger.info("==================================================")
# 1. Start Web Server (Daemon Thread)
# Daemon means it will be killed automatically when the main thread (Bot) exits
web_thread = threading.Thread(target=start_web_server, daemon=True, name="Web-UI")
web_thread.start()
# Give web server a moment to initialize logging/resources
time.sleep(1)
# 2. Start Discord Bot (Main Thread)
# py-cord handles signals (SIGINT/SIGTERM) well in the main thread
logger.info("π€ Starting Discord Bot...")
try:
run_bot()
except KeyboardInterrupt:
logger.info("π Received KeyboardInterrupt, shutting down...")
except Exception as e:
logger.critical(f"π Bot crashed: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()