refactor: Replace multiprocessing with threading for API and markdown servers to avoid conflicts

This commit is contained in:
Slipstream 2025-05-21 16:59:15 -06:00
parent 5ea3ef1e45
commit 3dc3a2d9ae
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
3 changed files with 234 additions and 27 deletions

View File

@ -24,7 +24,7 @@ else:
if __name__ == "__main__":
import multiprocessing
import threading
def run_uvicorn(bind_host):
print(f"Starting API server on {bind_host}:{port}")
@ -35,11 +35,18 @@ if __name__ == "__main__":
)
print(f"Data directory: {data_dir}")
# Start both IPv4 and IPv6 servers
processes = []
for bind_host in ["0.0.0.0", "::"]:
p = multiprocessing.Process(target=run_uvicorn, args=(bind_host,))
p.start()
processes.append(p)
for p in processes:
p.join()
# Start only IPv4 server to avoid conflicts
threads = []
for bind_host in ["0.0.0.0"]: # Removed "::" to simplify
t = threading.Thread(target=run_uvicorn, args=(bind_host,))
t.daemon = True
t.start()
threads.append(t)
# Keep the main thread running
try:
while True:
import time
time.sleep(1)
except KeyboardInterrupt:
print("Shutting down API server...")

View File

@ -237,21 +237,217 @@ async def root(request: Request):
# Function to start the server in a thread
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}...")
def run_server():
try:
uvicorn.run(app, host=host, port=port)
except Exception as e:
log.exception(f"Error running markdown server: {e}")
# Create a custom server that doesn't use Uvicorn directly
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.responses import HTMLResponse
import socket
import threading
# Start the server in a daemon thread
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
# Create a simple HTTP server to handle requests
class MarkdownServer(threading.Thread):
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")
return server_thread
return server
def start_server():
"""Start the markdown server as a background process (legacy method)."""

View File

@ -23,7 +23,7 @@ api_port = int(os.getenv("API_PORT", "8001"))
def run_unified_api():
"""Run the unified API service (dual-stack IPv4+IPv6)"""
import multiprocessing
import threading
def run_uvicorn(bind_host):
print(f"Starting unified API service on {bind_host}:{api_port} (HTTP only)")
@ -35,13 +35,17 @@ def run_unified_api():
)
try:
processes = []
for bind_host in ["127.0.0.1", "::1"]:
p = multiprocessing.Process(target=run_uvicorn, args=(bind_host,))
p.start()
processes.append(p)
for p in processes:
p.join()
# Use threading instead of multiprocessing to avoid pickling issues
threads = []
# Only run on IPv4 for now to avoid conflicts
for bind_host in ["127.0.0.1"]: # Removed "::1" to simplify
t = threading.Thread(target=run_uvicorn, args=(bind_host,))
t.daemon = True
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:
print(f"Error starting unified API service: {e}")