Delight

Aug 2009, Thomas Leonard

Examples

This page shows a longer example of a Delight (version 0.2) program.

The example is a mini web-server called "quickshare", which shares the current directory on the port of your choosing. It is used like this:

$ quickshare 5000
2008-10-12 11:15:57,505 Info  quickshare.main.Main - Server starting on padzu
2008-10-12 11:15:57,506 Info  quickshare.server.Server - Waiting for connections on 0.0.0.0:5000
2008-10-12 11:35:23,842 Info  quickshare.handler.DirRequestHandler - Handling client request: GET / HTTP/1.1
2008-10-12 11:35:27,826 Info  quickshare.handler.DirRequestHandler - Handling client request: GET /Makefile HTTP/1.1

Then anyone can point their browser at http://padzu:5000/ and download any files in the directory the server was run from.

quickshare.main

The Main class is usually the best place to start examining a new program. Because there is no global state, the main class acts as a configuration file than wires the rest of the program together and connects it to the outside world.

module quickshare.main

import dlt.net
import dlt.io
import dlt.log

import std.getopt: getopt

import quickshare.server: Server
import quickshare.handler: DirRequestHandler

class Main:
	in Network network
	in FileSystem fs
	in LogManager logManager

	void main(string[] args):
		const options = new Options(args)

		logManager.level = Level.Trace if options.verbose else Level.Info

		log_info("Server starting on {}", network.hostname)

		auto handler = new DirRequestHandler(fs.cwd)
		auto serverSocket = network.server.create(options.address, 32, true)
		auto server = new Server(serverSocket, handler)

		server.runServer()

From the in fields in Main and the arguments to Main.main, we can see that this program:

  • makes use of the network,
  • makes use of the file-system.
  • makes use of the command-line arguments,
  • reconfigures the logging system.

Looking inside the main method, we see that it creates four objects:

options
stores the parsed command-line arguments
handler
handles an individual request from a web browser by providing access to a directory (here, the current working directory)
serverSocket
a listening socket (which can queue up to 32 requests and which allows the address to be reused as soon as the program finishes)
server
A generic server that listens for incoming connections on serverSocket and passes each request to handler

We can also see which of these objects have access to each other. For example, we can see that options cannot read any configuration file (since we didn't pass fs to its constructor).

After the Main class, we have the simple Options class:

class Options:
	bool verbose
	Address address

	this(string[] args):
		getopt(args,
			"verbose|v", &verbose)

		if args.length != 2:
			throw new SystemExit("Usage: {} port".format(args[0]).idup)

		address = new Address(":" ~ args[1])

This accepts an option --verbose (or -v) flag and a required port number, from which it builds a local Address.

(note that args[0] is the name of the program)

quickshare.server

The server module defines the RequestHandler interface and a generic single-threaded server:

module quickshare.server

import dlt.net

interface RequestHandler:
	void handle(ClientSocket client)

# A generic single-threaded server
class Server:
	in ServerSocket ss
	in RequestHandler handler

	void runServer():
		log_info("Waiting for connections on {}", ss)
		while true:
			ClientSocket sock = ss.accept()
			log_trace("Got connection from {}", sock.remoteAddress)
			try:
				handler.handle(sock)
			catch (Exception ex):
				log_error("Error processing request: {}", ex)
				sock.send("Error: " ~ str(ex))
			finally:
				log_trace("Closing client socket")
				sock.close()

A Server takes a listening socket and a handler. Each time it gets a new connection on the socket it passes it to the request handler. When the handler returns (or throws an exception) the socket is closed.

This also shows a rare example of an appropriate use of log_error, since the process should not exit on error and this is the top-level event loop.

quickshare.handler

This provides a very primitive implementation of the HTTP protocol. It reads the request from the browser until it gets an empty line. If the request was for "/" then it sends a directory listing. Otherwise, it sends the requested file:

module quickshare.handler

import dlt.net
import dlt.io

import std.algorithm: sort

import quickshare.server: RequestHandler

# Replace HTML special characters with entities
string escape(string raw):
	return raw.replace("&", "&amp;").replace("<", "&lt;")

class DirRequestHandler implements RequestHandler:
	in FilePath dir

	void handle(ClientSocket client):
		char[] request
		while true:
			char[256] buf
			int got = client.receive(buf)
			assert got >= 0
			if not got:
				throw new Exception("Client closed connection!")
			request ~= buf[0 .. got]
			if request.endswith("\r\n\r\n"):
				break

		string[] requestLines = request.idup.splitlines()
		log_info("Handling client request: {}", requestLines[0])

		string[] parts = requestLines[0].split()
		assert parts[0] == "GET"
		assert parts[1].startswith("/")
		string resource = parts[1][1 .. $]

		# TODO: should unescape special characters

		if resource:
			sendResource(client, dir.child(resource))
		else:
			sendListing(client)

	void sendResource(ClientSocket client, FilePath item):
		client.send("HTTP/1.1 200 OK\r\n")
		client.send("Context-Type: text/plain\r\n\r\n")

		ubyte[4096] buf
		auto src = item.read()
		while true:
			int got = src.read(buf)
			if not got:
				break
			log_trace("Sending {} bytes of {}", got, item)
			client.send(buf[0 .. got])

	void sendListing(ClientSocket client):
		string[] items
		for item in dir.children:
			string name = item.name
			if not name.startswith("."):
				items ~= [name]
		items.sort()

		client.send("HTTP/1.1 200 OK\r\n")
		client.send("Context-Type: text/html\r\n\r\n")

		client.send("<html><body><ul>\n")
		for item in items:
			client.send(format("<li><a href='{0}'>{0}</a></li>\n", escape(item)))
		client.send("</ul></body></html>\n")

Future

Thoughts on things that could be improved:

  • Including the source file's name in the module line is clumsy. Should only have to specify the package, as in Java.
  • Support for named arguments would make parts of it clearer (e.g. the 32 passed when creating the server socket).
  • Shouldn't need the idup after format, which should return a string.
  • Use Python catch syntax? The parenthesis look out of place here.
  • socket.receive currently returns the number of bytes read. It would be more friendly (though less efficient) to return a new buffer.