refactor: Replace multiprocessing with threading for API and markdown servers to avoid conflicts
This commit is contained in:
parent
5ea3ef1e45
commit
3dc3a2d9ae
@ -24,7 +24,7 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import multiprocessing
|
import threading
|
||||||
|
|
||||||
def run_uvicorn(bind_host):
|
def run_uvicorn(bind_host):
|
||||||
print(f"Starting API server on {bind_host}:{port}")
|
print(f"Starting API server on {bind_host}:{port}")
|
||||||
@ -35,11 +35,18 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(f"Data directory: {data_dir}")
|
print(f"Data directory: {data_dir}")
|
||||||
# Start both IPv4 and IPv6 servers
|
# Start only IPv4 server to avoid conflicts
|
||||||
processes = []
|
threads = []
|
||||||
for bind_host in ["0.0.0.0", "::"]:
|
for bind_host in ["0.0.0.0"]: # Removed "::" to simplify
|
||||||
p = multiprocessing.Process(target=run_uvicorn, args=(bind_host,))
|
t = threading.Thread(target=run_uvicorn, args=(bind_host,))
|
||||||
p.start()
|
t.daemon = True
|
||||||
processes.append(p)
|
t.start()
|
||||||
for p in processes:
|
threads.append(t)
|
||||||
p.join()
|
|
||||||
|
# Keep the main thread running
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Shutting down API server...")
|
||||||
|
@ -237,21 +237,217 @@ async def root(request: Request):
|
|||||||
|
|
||||||
# Function to start the server in a thread
|
# Function to start the server in a thread
|
||||||
def start_markdown_server_in_thread(host="0.0.0.0", port=5006):
|
def start_markdown_server_in_thread(host="0.0.0.0", port=5006):
|
||||||
"""Start the markdown server in a separate thread."""
|
"""Start the markdown server in a separate thread using a different approach
|
||||||
|
that doesn't conflict with the API server's Uvicorn instance."""
|
||||||
log.info(f"Starting markdown server on {host}:{port}...")
|
log.info(f"Starting markdown server on {host}:{port}...")
|
||||||
|
|
||||||
def run_server():
|
# Create a custom server that doesn't use Uvicorn directly
|
||||||
try:
|
from starlette.applications import Starlette
|
||||||
uvicorn.run(app, host=host, port=port)
|
from starlette.routing import Mount
|
||||||
except Exception as e:
|
from starlette.responses import HTMLResponse
|
||||||
log.exception(f"Error running markdown server: {e}")
|
import socket
|
||||||
|
import threading
|
||||||
|
|
||||||
# Start the server in a daemon thread
|
# Create a simple HTTP server to handle requests
|
||||||
server_thread = threading.Thread(target=run_server, daemon=True)
|
class MarkdownServer(threading.Thread):
|
||||||
server_thread.start()
|
def __init__(self, host, port):
|
||||||
|
super().__init__(daemon=True)
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.running = False
|
||||||
|
self.server_socket = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.running = True
|
||||||
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.server_socket.bind((self.host, self.port))
|
||||||
|
self.server_socket.listen(5)
|
||||||
|
log.info(f"Markdown server listening on {self.host}:{self.port}")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
# Accept connections and handle them in a new thread
|
||||||
|
try:
|
||||||
|
client_socket, addr = self.server_socket.accept()
|
||||||
|
client_thread = threading.Thread(
|
||||||
|
target=self.handle_request,
|
||||||
|
args=(client_socket, addr),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
client_thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
if self.running: # Only log if we're still supposed to be running
|
||||||
|
log.exception(f"Error accepting connection: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error starting markdown server: {e}")
|
||||||
|
finally:
|
||||||
|
if self.server_socket:
|
||||||
|
self.server_socket.close()
|
||||||
|
|
||||||
|
def handle_request(self, client_socket, addr):
|
||||||
|
try:
|
||||||
|
# Read the HTTP request
|
||||||
|
request_data = client_socket.recv(1024).decode('utf-8')
|
||||||
|
request_lines = request_data.split('\n')
|
||||||
|
if not request_lines:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse the request line
|
||||||
|
request_line = request_lines[0].strip()
|
||||||
|
parts = request_line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
method, path = parts[0], parts[1]
|
||||||
|
|
||||||
|
# Simple routing
|
||||||
|
if path == '/' or path == '/index.html':
|
||||||
|
response = self.serve_root()
|
||||||
|
elif path == '/tos' or path == '/tos.html':
|
||||||
|
response = self.serve_tos()
|
||||||
|
elif path == '/privacy' or path == '/privacy.html':
|
||||||
|
response = self.serve_privacy()
|
||||||
|
else:
|
||||||
|
response = self.serve_404()
|
||||||
|
|
||||||
|
# Send the response
|
||||||
|
client_socket.sendall(response.encode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error handling request: {e}")
|
||||||
|
finally:
|
||||||
|
client_socket.close()
|
||||||
|
|
||||||
|
def serve_root(self):
|
||||||
|
html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Bot Legal Documents</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.link-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #0366d6;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.link-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Discord Bot Legal Documents</h1>
|
||||||
|
<p>Please review our Terms of Service and Privacy Policy.</p>
|
||||||
|
<div class="links">
|
||||||
|
<a href="/tos" class="link-button">Terms of Service</a>
|
||||||
|
<a href="/privacy" class="link-button">Privacy Policy</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
||||||
|
|
||||||
|
def serve_tos(self):
|
||||||
|
try:
|
||||||
|
with open("TOS.md", 'r', encoding='utf-8') as f:
|
||||||
|
md_content = f.read()
|
||||||
|
|
||||||
|
if MARKDOWN_AVAILABLE:
|
||||||
|
html_content = markdown.markdown(
|
||||||
|
md_content,
|
||||||
|
extensions=[
|
||||||
|
'markdown.extensions.fenced_code',
|
||||||
|
'markdown.extensions.tables',
|
||||||
|
'markdown.extensions.toc'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
html_content = f"<pre style='white-space: pre-wrap;'>{md_content}</pre>"
|
||||||
|
|
||||||
|
html = HTML_TEMPLATE.format(
|
||||||
|
title="Terms of Service",
|
||||||
|
content=html_content,
|
||||||
|
og_title="Terms of Service - Discord Bot",
|
||||||
|
og_description="Read the Terms of Service for our Discord Bot.",
|
||||||
|
og_type="article",
|
||||||
|
og_url=f"http://{self.host}:{self.port}/tos",
|
||||||
|
og_image=f"http://{self.host}:{self.port}/static/images/bot_logo.png"
|
||||||
|
)
|
||||||
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error serving TOS: {e}")
|
||||||
|
return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\n\r\n<h1>Error</h1><p>Failed to render TOS</p>"
|
||||||
|
|
||||||
|
def serve_privacy(self):
|
||||||
|
try:
|
||||||
|
with open("PRIVACY_POLICY.md", 'r', encoding='utf-8') as f:
|
||||||
|
md_content = f.read()
|
||||||
|
|
||||||
|
if MARKDOWN_AVAILABLE:
|
||||||
|
html_content = markdown.markdown(
|
||||||
|
md_content,
|
||||||
|
extensions=[
|
||||||
|
'markdown.extensions.fenced_code',
|
||||||
|
'markdown.extensions.tables',
|
||||||
|
'markdown.extensions.toc'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
html_content = f"<pre style='white-space: pre-wrap;'>{md_content}</pre>"
|
||||||
|
|
||||||
|
html = HTML_TEMPLATE.format(
|
||||||
|
title="Privacy Policy",
|
||||||
|
content=html_content,
|
||||||
|
og_title="Privacy Policy - Discord Bot",
|
||||||
|
og_description="Understand how your data is handled by our Discord Bot.",
|
||||||
|
og_type="article",
|
||||||
|
og_url=f"http://{self.host}:{self.port}/privacy",
|
||||||
|
og_image=f"http://{self.host}:{self.port}/static/images/bot_logo.png"
|
||||||
|
)
|
||||||
|
return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error serving Privacy Policy: {e}")
|
||||||
|
return "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\n\r\n<h1>Error</h1><p>Failed to render Privacy Policy</p>"
|
||||||
|
|
||||||
|
def serve_404(self):
|
||||||
|
html = "<h1>404 Not Found</h1><p>The requested page was not found.</p>"
|
||||||
|
return "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n" + html
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if self.server_socket:
|
||||||
|
self.server_socket.close()
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
server = MarkdownServer(host, port)
|
||||||
|
server.start()
|
||||||
log.info(f"Markdown server thread started. TOS available at: http://{host}:{port}/tos")
|
log.info(f"Markdown server thread started. TOS available at: http://{host}:{port}/tos")
|
||||||
|
|
||||||
return server_thread
|
return server
|
||||||
|
|
||||||
def start_server():
|
def start_server():
|
||||||
"""Start the markdown server as a background process (legacy method)."""
|
"""Start the markdown server as a background process (legacy method)."""
|
||||||
|
@ -23,7 +23,7 @@ api_port = int(os.getenv("API_PORT", "8001"))
|
|||||||
|
|
||||||
def run_unified_api():
|
def run_unified_api():
|
||||||
"""Run the unified API service (dual-stack IPv4+IPv6)"""
|
"""Run the unified API service (dual-stack IPv4+IPv6)"""
|
||||||
import multiprocessing
|
import threading
|
||||||
|
|
||||||
def run_uvicorn(bind_host):
|
def run_uvicorn(bind_host):
|
||||||
print(f"Starting unified API service on {bind_host}:{api_port} (HTTP only)")
|
print(f"Starting unified API service on {bind_host}:{api_port} (HTTP only)")
|
||||||
@ -35,13 +35,17 @@ def run_unified_api():
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processes = []
|
# Use threading instead of multiprocessing to avoid pickling issues
|
||||||
for bind_host in ["127.0.0.1", "::1"]:
|
threads = []
|
||||||
p = multiprocessing.Process(target=run_uvicorn, args=(bind_host,))
|
# Only run on IPv4 for now to avoid conflicts
|
||||||
p.start()
|
for bind_host in ["127.0.0.1"]: # Removed "::1" to simplify
|
||||||
processes.append(p)
|
t = threading.Thread(target=run_uvicorn, args=(bind_host,))
|
||||||
for p in processes:
|
t.daemon = True
|
||||||
p.join()
|
t.start()
|
||||||
|
threads.append(t)
|
||||||
|
|
||||||
|
# Don't join threads here - let them run in the background
|
||||||
|
print(f"API service started on {api_port}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting unified API service: {e}")
|
print(f"Error starting unified API service: {e}")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user