2015-10-21

Local web server in Python

I regularly work on two machines, my MacBook with OS X and a client computer with Windows. OS X provides the usual suite of free software tools including a web server. It is impractical for me to set up a web server on the Windows machine, but I installed Python 2, and it comes with a server sufficient for local development, CGIHTTPServer. It took me a couple of days to get it working right. Here are my notes for future reference.

Create a directory that will contain your HTML, Javascript, and CSS files. The files can be kept in subdirectories but there needs to be a root directory from which the Python server starts. In that root directory, create a batch file start-server.py. This file will cause Windows to open a CMD shell window and run the Python interpreter inside it. The Python program will create a program listening on the local host IP (127.0.0.1) and a given port, then wait for input. You can set up a command-response loop within the CMD shell, but it is mostly useful to display log messages and provide an easy way to stop the server.

Unix practice is to reserve the first 1024 port numbers to the root user, so other programs use higher numbers. Since the default port for HTTP is 80, most examples on the web use 8000 or 8080 for a local server. I use 8088 as an homage to the Intel processors that ignited the personal computer explosion.

My first attempts to launch the server seemed to fail after the first page load. After banging my head against the wall for a couple of days, a discussion on web2r.com gave me the solution. When the default HTTP handler writes a log message, it tries to do a reverse DNS lookup to find the requester's domain name. In this environment, there is no DNS server, but the handler keeps trying until it times out. On my machine, this takes about 25 seconds! The same post gives a solution: subclass the handler and override the address_string() method.

#! /usr/bin/env python

HOST = "127.0.0.1"
PORT = 8088

import BaseHTTPServer, SimpleHTTPServer, CGIHTTPServer
import urlparse
import cgitb; cgitb.enable()
import sys
import select
import socket
import thread
import time
import webbrowser
input = raw_input

class MyHandler(CGIHTTPServer.CGIHTTPRequestHandler):
	# Disable logging DNS lookups to avoid ridiculous delays.
	def address_string(self):
		return str(self.client_address[0])

handler = MyHandler

class StoppableHTTPServer(BaseHTTPServer.HTTPServer):

	def server_bind(self):
		BaseHTTPServer.HTTPServer.server_bind(self)
		self.socket.settimeout(1)
		self.run = True

	def get_request(self):
		while self.run:
			try:
				sock, addr = self.socket.accept()
				sock.settimeout(None)
				return (sock, addr)
			except socket.timeout:
				pass

	def stop(self):
		self.run = False
		sys.stderr.write('Server is stopping.\n')

	def serve(self):
		while self.run:
			self.handle_request()

def load_url(path):
	httpd = StoppableHTTPServer((HOST,PORT), handler)
	thread.start_new_thread(httpd.serve, ())
	time.sleep(0.10)
	webbrowser.open_new(path%(HOST,PORT))
	sys.stderr.write('Server running\n' )
	while httpd.run:
		cmd = input("Command?\n")
		if 'stop' == cmd or 'quit' == cmd or 'q' == cmd:
			httpd.stop()
		elif 'help' == cmd:
			sys.stderr.write('Type stop or quit or q to stop the server.\n' )
		else:
			sys.stderr.write('Unknown command ' + cmd + '\n' );

load_url("http://%s:%u/home.html")

The default implementation of CGIHTTPHandler supports executable scripts, but requires them all to be kept in the /cgi-bin subdirectory. A trivial example is shown below. Watch your line endings. If you create a Python file with DOS line endings (CR-LF) the Python interpreter will choke on it. Make sure to use Unix line endings (LF) only.

#! /usr/bin/env python

import sys

# Send headers.
sys.stdout.write("Content-type: text/plain\n\n" )

# Send body.
sys.stdout.write("I got this!\n" )
I adapted the StoppableHTTPServer code from Stack Overflow. I use only the Python 2 version but that post includes a Python 3 version as well. More links: