def app_serve(app_name: str) -> Dict[str, Any]:
"""
Serve an existing web application on a local HTTP server.
The server will automatically find an available port.
Can only serve one app at a time
Args:
app_name: Name of the application to serve
Returns:
A dictionary containing the result of the operation
"""
global http_server, server_port, app_response, response_ready
if http_server:
return "There is already a server running"
# Reset response state
app_response = None
response_ready = False
try:
# Find the app directory
app_path = os.path.join(APP_DIR, app_name)
if not os.path.exists(app_path):
return {
"success": False,
"error": f"App '{app_name}' not found at {app_path}"
}
# Stop any existing server
if http_server:
logger.info("Stopping existing HTTP server")
http_server.shutdown()
http_server.server_close()
http_server = None
# Find a free port
import socket
def find_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
# Try the default port first, if busy find a free one
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', server_port))
except OSError:
logger.info(f"Default port {server_port} is busy, finding a free port")
server_port = find_free_port()
logger.info(f"Found free port: {server_port}")
# Create a custom handler that serves from the app directory
# and replaces environment variables in JavaScript files
class EnvAwareHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=app_path, **kwargs)
def end_headers(self):
# Add cache control headers to ALL responses
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
def do_GET(self):
# Check if this is a wait_for_response request
if self.path.startswith('/wait_for_response'):
global app_response, response_lock, response_ready
# Reset response state for a new request
if self.path.startswith('/wait_for_response/reset'):
with response_lock:
app_response = None
response_ready = False
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response_data = json.dumps({"success": True, "message": "Response state reset"})
self.wfile.write(response_data.encode('utf-8'))
return
# Check if response already exists
if app_response is not None and response_ready:
# Return the response immediately
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response_data = json.dumps({"success": True, "data": app_response})
self.wfile.write(response_data.encode('utf-8'))
# Reset the response state after sending it
with response_lock:
app_response = None
response_ready = False
return
# Wait for the response with timeout
with response_lock:
# Wait for up to 180 seconds for the response to be ready
start_time = time.time()
while not response_ready and time.time() - start_time < 180:
response_lock.wait(180 - (time.time() - start_time))
# Check if the response is now available
if response_ready and app_response is not None:
break
# Check if we got the response or timed out
if response_ready and app_response is not None:
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
response_data = json.dumps({"success": True, "data": app_response})
self.wfile.write(response_data.encode('utf-8'))
else:
# Timeout occurred
self.send_response(408) # Request Timeout
self.send_header('Content-type', 'application/json')
self.end_headers()
response_data = json.dumps({"success": False, "error": "Timeout waiting for response"})
self.wfile.write(response_data.encode('utf-8'))
return
# Get the file path
path = self.translate_path(self.path)
# Check if the file exists
if os.path.isfile(path):
# Check if it's a JavaScript file that might need variable replacement
if path.endswith('.js'):
try:
with open(path, 'r') as f:
content = f.read()
# Check if the file contains environment variables that need to be replaced
if '$GOOSE_PORT' in content or '$GOOSE_SERVER__SECRET_KEY' in content:
# Replace environment variables
goose_port = os.environ.get('GOOSE_PORT', '0')
secret_key = os.environ.get('GOOSE_SERVER__SECRET_KEY', '')
# Replace variables
content = content.replace('$GOOSE_PORT', goose_port)
content = content.replace('$GOOSE_SERVER__SECRET_KEY', secret_key)
# Send the modified content
self.send_response(200)
self.send_header('Content-type', 'application/javascript')
self.send_header('Content-Length', str(len(content)))
self.end_headers()
self.wfile.write(content.encode('utf-8'))
return
except Exception as e:
logger.error(f"Error processing JavaScript file: {e}")
# If we didn't handle it specially, use the default handler
return super().do_GET()
# Start the server in a separate thread
import threading
# Use a thread-safe event to signal when the server is ready
server_ready = threading.Event()
server_error = [None] # Use a list to store error from thread
def run_server():
global http_server
try:
with socketserver.TCPServer(("", server_port), EnvAwareHandler) as server:
http_server = server
# Signal that server is ready
server_ready.set()
logger.info(f"Serving app '{app_name}' at http://localhost:{server_port}")
logger.info(f"Using GOOSE_PORT={os.environ.get('GOOSE_PORT', '3000')}")
logger.info(f"Using GOOSE_SERVER__SECRET_KEY={os.environ.get('GOOSE_SERVER__SECRET_KEY', '')[:5]}...")
server.serve_forever()
except Exception as e:
server_error[0] = str(e)
server_ready.set() # Signal even on error
logger.error(f"Server error: {e}")
server_thread = threading.Thread(target=run_server)
server_thread.daemon = True
server_thread.start()
# Wait for the server to start or fail, with timeout
if not server_ready.wait(timeout=2.0):
return {
"success": False,
"error": "Server failed to start within timeout period"
}
# Check if there was an error
if server_error[0]:
return {
"success": False,
"error": f"Failed to serve app: {server_error[0]}"
}
return {
"success": True,
"app_name": app_name,
"port": server_port,
"url": f"http://localhost:{server_port}",
"message": f"App '{app_name}' is now being served at http://localhost:{server_port}"
}
except Exception as e:
logger.error(f"Error serving app: {e}")
return {"success": False, "error": f"Failed to serve app: {str(e)}"}