summaryrefslogtreecommitdiff
path: root/code
diff options
context:
space:
mode:
Diffstat (limited to 'code')
-rw-r--r--code/snippets/abort.rb3
-rw-r--r--code/snippets/abort_message.rb4
-rw-r--r--code/snippets/abort_with_at_exit.rb4
-rw-r--r--code/snippets/at_exit.rb5
-rw-r--r--code/snippets/babysitting_processes.rb10
-rw-r--r--code/snippets/bg_process.rb15
-rw-r--r--code/snippets/closed_fileno.rb4
-rw-r--r--code/snippets/cow.rb12
-rw-r--r--code/snippets/cow_no_writes.rb10
-rw-r--r--code/snippets/custom_exit_bang.rb3
-rw-r--r--code/snippets/custom_exit_code.rb3
-rw-r--r--code/snippets/daemonize.rb14
-rw-r--r--code/snippets/daemons_grp_eq_pid.rb2
-rw-r--r--code/snippets/daemons_grp_inherited.rb7
-rw-r--r--code/snippets/decorate_default_signal_behaviour.rb6
-rw-r--r--code/snippets/env_aint_a_hash.rb3
-rw-r--r--code/snippets/env_launch.sh1
-rw-r--r--code/snippets/env_set.rb4
-rw-r--r--code/snippets/exceeding_soft_rlimits.rb7
-rw-r--r--code/snippets/exec_python.rb8
-rw-r--r--code/snippets/exit_0.rb3
-rw-r--r--code/snippets/exit_bang.rb3
-rw-r--r--code/snippets/exit_bang_skips_at_exit.rb4
-rw-r--r--code/snippets/fileno.rb3
-rw-r--r--code/snippets/getrlimit.rb2
-rw-r--r--code/snippets/if_fork.rb6
-rw-r--r--code/snippets/if_fork_pid.rb8
-rw-r--r--code/snippets/multiple_filenos.rb13
-rw-r--r--code/snippets/orphan_process.rb9
-rw-r--r--code/snippets/pipe.rb2
-rw-r--r--code/snippets/pipe_direction.rb2
-rw-r--r--code/snippets/pipe_io.rb5
-rw-r--r--code/snippets/pipe_sharing_with_fork.rb16
-rw-r--r--code/snippets/prefork.rb29
-rw-r--r--code/snippets/process_spawn.rb7
-rw-r--r--code/snippets/process_spawn_waitpid.rb11
-rw-r--r--code/snippets/process_wait_queue.rb18
-rw-r--r--code/snippets/program_name.rb7
-rw-r--r--code/snippets/raise_exit.rb2
-rw-r--r--code/snippets/rlimits.rb12
-rw-r--r--code/snippets/setrlimit.rb3
-rw-r--r--code/snippets/signals_chld_naive.rb30
-rw-r--r--code/snippets/signals_chld_nohang.rb42
-rw-r--r--code/snippets/socketpair.rb3
-rw-r--r--code/snippets/socketpair_communication.rb26
-rw-r--r--code/snippets/soft_to_hard_rlimit.rb1
-rw-r--r--code/snippets/spawn_open3_eg.rb14
-rw-r--r--code/snippets/spawn_popen_block.rb8
-rw-r--r--code/snippets/spawn_popen_no_block.rb4
-rw-r--r--code/snippets/standard_streams.rb3
-rw-r--r--code/snippets/trap_int_signal.rb8
-rw-r--r--code/snippets/wait2.rb26
-rw-r--r--code/snippets/wait_for_each_process.rb14
-rw-r--r--code/snippets/waitpid2.rb11
-rw-r--r--code/snippets/zombie_process.rb17
-rw-r--r--code/snippets/zombies_eg.rb7
-rw-r--r--code/spyglass/.gitignore4
-rw-r--r--code/spyglass/Gemfile13
-rw-r--r--code/spyglass/Gemfile.lock32
-rw-r--r--code/spyglass/LICENSE189
-rw-r--r--code/spyglass/README.md37
-rw-r--r--code/spyglass/Rakefile45
-rwxr-xr-xcode/spyglass/bin/spyglass83
-rw-r--r--code/spyglass/config.ru7
-rw-r--r--code/spyglass/docs/configurator.html137
-rw-r--r--code/spyglass/docs/docco.css500
-rw-r--r--code/spyglass/docs/logging.html105
-rw-r--r--code/spyglass/docs/lookout.html325
-rw-r--r--code/spyglass/docs/master.html268
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-bold.eotbin0 -> 29804 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-bold.ttfbin0 -> 66836 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-bold.woffbin0 -> 33244 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-light.eotbin0 -> 29509 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-light.ttfbin0 -> 68620 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/aller-light.woffbin0 -> 33124 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/novecento-bold.eotbin0 -> 18190 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/novecento-bold.ttfbin0 -> 48136 bytes
-rwxr-xr-xcode/spyglass/docs/public/fonts/novecento-bold.woffbin0 -> 20576 bytes
-rw-r--r--code/spyglass/docs/public/stylesheets/normalize.css375
-rw-r--r--code/spyglass/docs/server.html109
-rw-r--r--code/spyglass/docs/spyglass.html106
-rw-r--r--code/spyglass/docs/worker.html305
-rw-r--r--code/spyglass/ext/spyglass_parser/common.rl55
-rw-r--r--code/spyglass/ext/spyglass_parser/ext_help.h14
-rw-r--r--code/spyglass/ext/spyglass_parser/extconf.rb6
-rw-r--r--code/spyglass/ext/spyglass_parser/parser.c1249
-rw-r--r--code/spyglass/ext/spyglass_parser/parser.h49
-rw-r--r--code/spyglass/ext/spyglass_parser/parser.rl157
-rw-r--r--code/spyglass/ext/spyglass_parser/spyglass.c436
-rw-r--r--code/spyglass/lib/spyglass.rb53
-rw-r--r--code/spyglass/lib/spyglass/configurator.rb31
-rw-r--r--code/spyglass/lib/spyglass/logging.rb25
-rw-r--r--code/spyglass/lib/spyglass/lookout.rb83
-rw-r--r--code/spyglass/lib/spyglass/master.rb102
-rw-r--r--code/spyglass/lib/spyglass/server.rb17
-rw-r--r--code/spyglass/lib/spyglass/worker.rb93
-rw-r--r--code/spyglass/test/66613524f6c67942d7d19386f51c551cconfig.ru9
-rw-r--r--code/spyglass/test/basic_rack_test.rb15
-rw-r--r--code/spyglass/test/boots_test.rb13
-rw-r--r--code/spyglass/test/config.ru10
-rw-r--r--code/spyglass/test/helper.rb33
-rw-r--r--code/spyglass/test/log/.gitkeep0
-rw-r--r--code/spyglass/test/sinatra_test.rb25
-rw-r--r--code/spyglass/test/timeout_test.rb51
104 files changed, 5670 insertions, 0 deletions
diff --git a/code/snippets/abort.rb b/code/snippets/abort.rb
new file mode 100644
index 0000000..51ec639
--- /dev/null
+++ b/code/snippets/abort.rb
@@ -0,0 +1,3 @@
+# Will exit with exit code 1.
+abort
+
diff --git a/code/snippets/abort_message.rb b/code/snippets/abort_message.rb
new file mode 100644
index 0000000..131c055
--- /dev/null
+++ b/code/snippets/abort_message.rb
@@ -0,0 +1,4 @@
+# You can pass a message to Kernel#abort. This message will be printed
+# to STDERR before the process exits.
+abort "Something went horribly wrong."
+
diff --git a/code/snippets/abort_with_at_exit.rb b/code/snippets/abort_with_at_exit.rb
new file mode 100644
index 0000000..562ca88
--- /dev/null
+++ b/code/snippets/abort_with_at_exit.rb
@@ -0,0 +1,4 @@
+# Kernel#at_exit blocks are invoked when using Kernel#abort.
+at_exit { puts 'Last!' }
+abort "Something went horribly wrong."
+
diff --git a/code/snippets/at_exit.rb b/code/snippets/at_exit.rb
new file mode 100644
index 0000000..1705210
--- /dev/null
+++ b/code/snippets/at_exit.rb
@@ -0,0 +1,5 @@
+# When Kernel#exit is invoked, before exiting Ruby invokes any blocks
+# defined by Kernel#at_exit.
+at_exit { puts 'Last!' }
+exit
+
diff --git a/code/snippets/babysitting_processes.rb b/code/snippets/babysitting_processes.rb
new file mode 100644
index 0000000..4f039f5
--- /dev/null
+++ b/code/snippets/babysitting_processes.rb
@@ -0,0 +1,10 @@
+fork do
+ 5.times do
+ sleep 1
+ puts "I am an orphan!"
+ end
+end
+
+Process.wait
+abort "Parent process died..."
+
diff --git a/code/snippets/bg_process.rb b/code/snippets/bg_process.rb
new file mode 100644
index 0000000..b7323e8
--- /dev/null
+++ b/code/snippets/bg_process.rb
@@ -0,0 +1,15 @@
+message = 'Good Morning'
+recipient = 'tree@mybackyard.com'
+
+fork do
+ # In this contrived example the parent process forks a child to take
+ # care of sending data to the stats collector. Meanwhile the parent
+ # process has continued on with its work of sending the actual payload.
+
+ # The parent process doesn't want to be slowed down with this task, and
+ # it doesn't matter if this would fail for some reason.
+ StatsCollector.record message, recipient
+end
+
+# send message to recipient
+
diff --git a/code/snippets/closed_fileno.rb b/code/snippets/closed_fileno.rb
new file mode 100644
index 0000000..96fa439
--- /dev/null
+++ b/code/snippets/closed_fileno.rb
@@ -0,0 +1,4 @@
+passwd = File.open('/etc/passwd')
+puts passwd.fileno
+passwd.close
+puts passwd.fileno
diff --git a/code/snippets/cow.rb b/code/snippets/cow.rb
new file mode 100644
index 0000000..f7d85d4
--- /dev/null
+++ b/code/snippets/cow.rb
@@ -0,0 +1,12 @@
+arr = [1,2,3]
+
+fork do
+ # At this point the child process has been initialized.
+ # Because of CoW the arr variable hasn't been copied yet.
+ arr << 4
+ # The above line of code modifies the array, so a copy of
+ # the array will need to be made for this process before
+ # it can modify it. The array in the parent process remains
+ # unchanged.
+end
+
diff --git a/code/snippets/cow_no_writes.rb b/code/snippets/cow_no_writes.rb
new file mode 100644
index 0000000..6e93ff7
--- /dev/null
+++ b/code/snippets/cow_no_writes.rb
@@ -0,0 +1,10 @@
+arr = [1,2,3]
+
+fork do
+ # At this point the child process has been initialized.
+ # Using CoW this process doesn't need to copy the arr variable,
+ # since it hasn't modified any shared values it can continue reading
+ # from the same memory location as the parent process.
+ p arr
+end
+
diff --git a/code/snippets/custom_exit_bang.rb b/code/snippets/custom_exit_bang.rb
new file mode 100644
index 0000000..604b238
--- /dev/null
+++ b/code/snippets/custom_exit_bang.rb
@@ -0,0 +1,3 @@
+# You can still pass an exit code.
+exit! 33
+
diff --git a/code/snippets/custom_exit_code.rb b/code/snippets/custom_exit_code.rb
new file mode 100644
index 0000000..de83030
--- /dev/null
+++ b/code/snippets/custom_exit_code.rb
@@ -0,0 +1,3 @@
+# You can pass a custom exit code to this method
+exit 22
+
diff --git a/code/snippets/daemonize.rb b/code/snippets/daemonize.rb
new file mode 100644
index 0000000..473e018
--- /dev/null
+++ b/code/snippets/daemonize.rb
@@ -0,0 +1,14 @@
+def daemonize_app
+ if RUBY_VERSION < "1.9"
+ exit if fork
+ Process.setsid
+ exit if fork
+ Dir.chdir "/"
+ STDIN.reopen "/dev/null"
+ STDOUT.reopen "/dev/null", "a"
+ STDERR.reopen "/dev/null", "a"
+ else
+ Process.daemon
+ end
+end
+
diff --git a/code/snippets/daemons_grp_eq_pid.rb b/code/snippets/daemons_grp_eq_pid.rb
new file mode 100644
index 0000000..ad4e7b0
--- /dev/null
+++ b/code/snippets/daemons_grp_eq_pid.rb
@@ -0,0 +1,2 @@
+puts Process.getpgrp
+puts Process.pid
diff --git a/code/snippets/daemons_grp_inherited.rb b/code/snippets/daemons_grp_inherited.rb
new file mode 100644
index 0000000..1899024
--- /dev/null
+++ b/code/snippets/daemons_grp_inherited.rb
@@ -0,0 +1,7 @@
+puts Process.pid
+puts Process.getpgrp
+
+fork {
+ puts Process.pid
+ puts Process.getpgrp
+} \ No newline at end of file
diff --git a/code/snippets/decorate_default_signal_behaviour.rb b/code/snippets/decorate_default_signal_behaviour.rb
new file mode 100644
index 0000000..b27bb1c
--- /dev/null
+++ b/code/snippets/decorate_default_signal_behaviour.rb
@@ -0,0 +1,6 @@
+system_handler = trap(:INT) {
+ puts 'about to exit!'
+ system_handler.call
+}
+sleep 5 # so that we have time to send it a signal
+
diff --git a/code/snippets/env_aint_a_hash.rb b/code/snippets/env_aint_a_hash.rb
new file mode 100644
index 0000000..0616c50
--- /dev/null
+++ b/code/snippets/env_aint_a_hash.rb
@@ -0,0 +1,3 @@
+puts ENV['EDITOR']
+puts ENV.has_key?('PATH')
+puts ENV.is_a?(Hash)
diff --git a/code/snippets/env_launch.sh b/code/snippets/env_launch.sh
new file mode 100644
index 0000000..c085eec
--- /dev/null
+++ b/code/snippets/env_launch.sh
@@ -0,0 +1 @@
+$ MESSAGE='wing it' ruby -e "puts ENV['MESSAGE']"
diff --git a/code/snippets/env_set.rb b/code/snippets/env_set.rb
new file mode 100644
index 0000000..b950dab
--- /dev/null
+++ b/code/snippets/env_set.rb
@@ -0,0 +1,4 @@
+# The same thing, with places reversed!
+ENV['MESSAGE'] = 'wing it'
+system "echo $MESSAGE"
+
diff --git a/code/snippets/exceeding_soft_rlimits.rb b/code/snippets/exceeding_soft_rlimits.rb
new file mode 100644
index 0000000..4b6f2ed
--- /dev/null
+++ b/code/snippets/exceeding_soft_rlimits.rb
@@ -0,0 +1,7 @@
+# Set the maximum number of open files to 3. We know this
+# will be maxed out because the standard streams occupy
+# the first three file descriptors.
+Process.setrlimit(:NOFILE, 3)
+
+File.open('/dev/null')
+
diff --git a/code/snippets/exec_python.rb b/code/snippets/exec_python.rb
new file mode 100644
index 0000000..b60d0b1
--- /dev/null
+++ b/code/snippets/exec_python.rb
@@ -0,0 +1,8 @@
+hosts = File.open('/etc/hosts')
+
+python_code = %Q[import os; print os.fdopen(#{hosts.fileno}).read()]
+
+# The hash as the last arguments maps any file descriptors that should
+# stay open through the exec.
+exec 'python', '-c', python_code, {hosts.fileno => hosts}
+
diff --git a/code/snippets/exit_0.rb b/code/snippets/exit_0.rb
new file mode 100644
index 0000000..abc539b
--- /dev/null
+++ b/code/snippets/exit_0.rb
@@ -0,0 +1,3 @@
+# This will exit the program with the success status code (0).
+exit
+
diff --git a/code/snippets/exit_bang.rb b/code/snippets/exit_bang.rb
new file mode 100644
index 0000000..49d1f61
--- /dev/null
+++ b/code/snippets/exit_bang.rb
@@ -0,0 +1,3 @@
+# This will exit the program with a status code 1.
+exit!
+
diff --git a/code/snippets/exit_bang_skips_at_exit.rb b/code/snippets/exit_bang_skips_at_exit.rb
new file mode 100644
index 0000000..91e7fc7
--- /dev/null
+++ b/code/snippets/exit_bang_skips_at_exit.rb
@@ -0,0 +1,4 @@
+# This block will never be invoked.
+at_exit { puts 'Silence!' }
+exit!
+
diff --git a/code/snippets/fileno.rb b/code/snippets/fileno.rb
new file mode 100644
index 0000000..0aef6ba
--- /dev/null
+++ b/code/snippets/fileno.rb
@@ -0,0 +1,3 @@
+passwd = File.open('/etc/passwd')
+puts passwd.fileno
+
diff --git a/code/snippets/getrlimit.rb b/code/snippets/getrlimit.rb
new file mode 100644
index 0000000..bd21159
--- /dev/null
+++ b/code/snippets/getrlimit.rb
@@ -0,0 +1,2 @@
+p Process.getrlimit(:NOFILE)
+
diff --git a/code/snippets/if_fork.rb b/code/snippets/if_fork.rb
new file mode 100644
index 0000000..5d426a8
--- /dev/null
+++ b/code/snippets/if_fork.rb
@@ -0,0 +1,6 @@
+if fork
+ puts "entered the if block"
+else
+ puts "entered the else block"
+end
+
diff --git a/code/snippets/if_fork_pid.rb b/code/snippets/if_fork_pid.rb
new file mode 100644
index 0000000..2c3c262
--- /dev/null
+++ b/code/snippets/if_fork_pid.rb
@@ -0,0 +1,8 @@
+puts "parent process pid is #{Process.pid}"
+
+if fork
+ puts "entered the if block from #{Process.pid}"
+else
+ puts "entered the else block from #{Process.pid}"
+end
+
diff --git a/code/snippets/multiple_filenos.rb b/code/snippets/multiple_filenos.rb
new file mode 100644
index 0000000..aca4caf
--- /dev/null
+++ b/code/snippets/multiple_filenos.rb
@@ -0,0 +1,13 @@
+passwd = File.open('/etc/passwd')
+puts passwd.fileno
+
+hosts = File.open('/etc/hosts')
+puts hosts.fileno
+
+# Close the open passwd file. The frees up its file descriptor
+# number to be used by the next opened resource.
+passwd.close
+
+null = File.open('/dev/null')
+puts null.fileno
+
diff --git a/code/snippets/orphan_process.rb b/code/snippets/orphan_process.rb
new file mode 100644
index 0000000..3b25f92
--- /dev/null
+++ b/code/snippets/orphan_process.rb
@@ -0,0 +1,9 @@
+fork do
+ 5.times do
+ sleep 1
+ puts "I'm an orphan!"
+ end
+end
+
+abort "Parent process died..."
+
diff --git a/code/snippets/pipe.rb b/code/snippets/pipe.rb
new file mode 100644
index 0000000..f62a237
--- /dev/null
+++ b/code/snippets/pipe.rb
@@ -0,0 +1,2 @@
+reader, writer = IO.pipe #=> [#<IO:fd 5>, #<IO:fd 6>]
+
diff --git a/code/snippets/pipe_direction.rb b/code/snippets/pipe_direction.rb
new file mode 100644
index 0000000..2a81c11
--- /dev/null
+++ b/code/snippets/pipe_direction.rb
@@ -0,0 +1,2 @@
+reader, writer = IO.pipe
+reader.write("Trying to get the reader to write something")
diff --git a/code/snippets/pipe_io.rb b/code/snippets/pipe_io.rb
new file mode 100644
index 0000000..f2f817b
--- /dev/null
+++ b/code/snippets/pipe_io.rb
@@ -0,0 +1,5 @@
+reader, writer = IO.pipe
+writer.write("Into the pipe I go...")
+writer.close
+puts reader.read
+
diff --git a/code/snippets/pipe_sharing_with_fork.rb b/code/snippets/pipe_sharing_with_fork.rb
new file mode 100644
index 0000000..038981a
--- /dev/null
+++ b/code/snippets/pipe_sharing_with_fork.rb
@@ -0,0 +1,16 @@
+reader, writer = IO.pipe
+
+fork do
+ reader.close
+
+ 10.times do
+ # heavy lifting
+ writer.puts "Another one bites the dust"
+ end
+end
+
+writer.close
+while message = reader.gets
+ $stdout.puts message
+end
+
diff --git a/code/snippets/prefork.rb b/code/snippets/prefork.rb
new file mode 100644
index 0000000..65b285f
--- /dev/null
+++ b/code/snippets/prefork.rb
@@ -0,0 +1,29 @@
+require 'socket'
+
+# Open a socket.
+socket = TCPServer.open('0.0.0.0', 8080)
+
+# Preload app code.
+# require 'config/environment'
+
+# Forward any relevant signals to the child processes.
+[:INT, :QUIT].each do |signal|
+ Signal.trap(signal) {
+ wpids.each { |wpid| Process.kill(signal, wpid) }
+ }
+end
+
+# For keeping track of child process pids.
+wpids = []
+
+5.times {
+ wpids << fork do
+ loop {
+ connection = socket.accept
+ connection.puts 'Hello Readers!'
+ connection.close
+ }
+ end
+}
+
+Process.waitall \ No newline at end of file
diff --git a/code/snippets/process_spawn.rb b/code/snippets/process_spawn.rb
new file mode 100644
index 0000000..04f6b0f
--- /dev/null
+++ b/code/snippets/process_spawn.rb
@@ -0,0 +1,7 @@
+# This call will start up the 'rails server' process with the
+# RAILS_ENV environment variable set to 'test'.
+Process.spawn({'RAILS_ENV' => 'test'}, 'rails server')
+
+# This call will merge STDERR with STDOUT for the duration
+# of the 'ls --help' program.
+Process.spawn('ls', '--zz', STDERR => STDOUT)
diff --git a/code/snippets/process_spawn_waitpid.rb b/code/snippets/process_spawn_waitpid.rb
new file mode 100644
index 0000000..a8b28c4
--- /dev/null
+++ b/code/snippets/process_spawn_waitpid.rb
@@ -0,0 +1,11 @@
+# Do it the blocking way
+system 'sleep 5'
+
+# Do it the non-blocking way
+Process.spawn 'sleep 5'
+
+# Do it the blocking way with Process.spawn
+# Notice that it returns the pid of the child process
+pid = Process.spawn 'sleep 5'
+Process.waitpid(pid)
+
diff --git a/code/snippets/process_wait_queue.rb b/code/snippets/process_wait_queue.rb
new file mode 100644
index 0000000..872f8d8
--- /dev/null
+++ b/code/snippets/process_wait_queue.rb
@@ -0,0 +1,18 @@
+# We create two child processes.
+2.times do
+ fork do
+ # Both processes exit immediately.
+ abort "Finished!"
+ end
+end
+
+# The parent process waits for the first process, then sleeps for 5 seconds.
+# In the meantime the second child process has exited and is no
+# longer running.
+puts Process.wait
+sleep 5
+
+# The parent process asks to wait once again, and amazingly enough, the second
+# process' exit information has been queued up and is returned here.
+puts Process.wait
+
diff --git a/code/snippets/program_name.rb b/code/snippets/program_name.rb
new file mode 100644
index 0000000..989af3d
--- /dev/null
+++ b/code/snippets/program_name.rb
@@ -0,0 +1,7 @@
+puts $PROGRAM_NAME
+
+10.downto(1) do |num|
+ $PROGRAM_NAME = "Process: #{num}"
+ puts $PROGRAM_NAME
+end
+
diff --git a/code/snippets/raise_exit.rb b/code/snippets/raise_exit.rb
new file mode 100644
index 0000000..7344236
--- /dev/null
+++ b/code/snippets/raise_exit.rb
@@ -0,0 +1,2 @@
+# Similar to abort, an unhandled exception will set the exit code to 1.
+raise 'hell'
diff --git a/code/snippets/rlimits.rb b/code/snippets/rlimits.rb
new file mode 100644
index 0000000..a391c15
--- /dev/null
+++ b/code/snippets/rlimits.rb
@@ -0,0 +1,12 @@
+# The maximum number of simultaneous processes
+# allowed for the current user.
+Process.getrlimit(:NPROC)
+
+# The largest size file that may be created.
+Process.getrlimit(:FSIZE)
+
+# The maximum size of the stack segment of the
+# process.
+Process.getrlimit(:STACK)
+
+
diff --git a/code/snippets/setrlimit.rb b/code/snippets/setrlimit.rb
new file mode 100644
index 0000000..990ca5e
--- /dev/null
+++ b/code/snippets/setrlimit.rb
@@ -0,0 +1,3 @@
+Process.setrlimit(:NOFILE, 4096)
+p Process.getrlimit(:NOFILE)
+
diff --git a/code/snippets/signals_chld_naive.rb b/code/snippets/signals_chld_naive.rb
new file mode 100644
index 0000000..59929fb
--- /dev/null
+++ b/code/snippets/signals_chld_naive.rb
@@ -0,0 +1,30 @@
+child_processes = 3
+dead_processes = 0
+# We fork 3 child processes.
+child_processes.times do
+ fork do
+ # They sleep for 3 seconds.
+ sleep 3
+ end
+end
+
+# Our parent process will be busy doing some intense mathematics.
+# But still wants to know when one of its children exits.
+
+# By trapping the :CHLD signal our process will be notified by the kernel
+# when one of its children exits.
+trap(:CHLD) do
+ # Since Process.wait queues up any data that it has for us we can ask for it
+ # here, since we know that one of our child processes has exited.
+
+ puts Process.wait
+ dead_processes += 1
+ # We exit explicitly once all the child processes are accounted for.
+ exit if dead_processes == child_processes
+end
+
+# Work it.
+loop do
+ (Math.sqrt(rand(44)) ** 8).floor
+ sleep 1
+end
diff --git a/code/snippets/signals_chld_nohang.rb b/code/snippets/signals_chld_nohang.rb
new file mode 100644
index 0000000..621a70c
--- /dev/null
+++ b/code/snippets/signals_chld_nohang.rb
@@ -0,0 +1,42 @@
+child_processes = 3
+dead_processes = 0
+# We fork 3 child processes.
+child_processes.times do
+ fork do
+ # They sleep for 3 seconds.
+ sleep 3
+ end
+end
+
+# Sync $stdout so the call to #puts in the CHLD handler isn't
+# buffered. Can cause a ThreadError if a signal handler is
+# interrupted after calling #puts. Always a good idea to do
+# this if your handlers will be doing IO.
+$stdout.sync = true
+
+# Our parent process will be busy doing some intense mathematics.
+# But still wants to know when one of its children exits.
+
+# By trapping the :CHLD signal our process will be notified by the kernel
+# when one of its children exits.
+trap(:CHLD) do
+ # Since Process.wait queues up any data that it has for us we can ask for it
+ # here, since we know that one of our child processes has exited.
+
+ # We loop over a non-blocking Process.wait to ensure that any dead child
+ # processes are accounted for.
+ begin
+ while pid = Process.wait(-1, Process::WNOHANG)
+ puts pid
+ dead_processes += 1
+ end
+ rescue Errno::ECHILD
+ end
+end
+
+loop do
+ # We exit ourself once all the child processes are accounted for.
+ exit if dead_processes == child_processes
+
+ sleep 1
+end
diff --git a/code/snippets/socketpair.rb b/code/snippets/socketpair.rb
new file mode 100644
index 0000000..06ae044
--- /dev/null
+++ b/code/snippets/socketpair.rb
@@ -0,0 +1,3 @@
+require 'socket'
+Socket.pair(:UNIX, :DGRAM, 0) #=> [#<Socket:fd 15>, #<Socket:fd 16>]
+
diff --git a/code/snippets/socketpair_communication.rb b/code/snippets/socketpair_communication.rb
new file mode 100644
index 0000000..c8e0f09
--- /dev/null
+++ b/code/snippets/socketpair_communication.rb
@@ -0,0 +1,26 @@
+require 'socket'
+
+child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
+maxlen = 1000
+
+fork do
+ parent_socket.close
+
+ 4.times do
+ instruction = child_socket.recv(maxlen)
+ child_socket.send("#{instruction} accomplished!", 0)
+ end
+end
+child_socket.close
+
+2.times do
+ parent_socket.send("Heavy lifting", 0)
+end
+2.times do
+ parent_socket.send("Feather lifting", 0)
+end
+
+4.times do
+ $stdout.puts parent_socket.recv(maxlen)
+end
+
diff --git a/code/snippets/soft_to_hard_rlimit.rb b/code/snippets/soft_to_hard_rlimit.rb
new file mode 100644
index 0000000..e6a2e11
--- /dev/null
+++ b/code/snippets/soft_to_hard_rlimit.rb
@@ -0,0 +1 @@
+Process.setrlimit(:NOFILE, Process.getrlimit(:NOFILE)[1])
diff --git a/code/snippets/spawn_open3_eg.rb b/code/snippets/spawn_open3_eg.rb
new file mode 100644
index 0000000..0417aa0
--- /dev/null
+++ b/code/snippets/spawn_open3_eg.rb
@@ -0,0 +1,14 @@
+# This is available as part of the standard library.
+require 'open3'
+
+Open3.popen3('grep', 'data') { |stdin, stdout, stderr|
+ stdin.puts "some\ndata"
+ stdin.close
+ puts stdout.read
+}
+
+# Open3 will use Process.spawn when available. Options can be passed to
+# Process.spawn like so:
+Open3.popen3('ls', '-uhh', :err => :out) { |stdin, stdout, stderr|
+ puts stdout.read
+}
diff --git a/code/snippets/spawn_popen_block.rb b/code/snippets/spawn_popen_block.rb
new file mode 100644
index 0000000..193af6a
--- /dev/null
+++ b/code/snippets/spawn_popen_block.rb
@@ -0,0 +1,8 @@
+# An IO object is passed into the block. In this case we open the stream
+# for writing, so the stream is set to the STDIN of the spawned process.
+#
+# If we open the stream for reading (the default) then
+# the stream is set to the STDOUT of the spawned process.
+IO.popen('less', 'w') { |stream|
+ stream.puts "some\ndata"
+}
diff --git a/code/snippets/spawn_popen_no_block.rb b/code/snippets/spawn_popen_no_block.rb
new file mode 100644
index 0000000..9cf54a0
--- /dev/null
+++ b/code/snippets/spawn_popen_no_block.rb
@@ -0,0 +1,4 @@
+# This example will return a file descriptor (IO object). Reading from it
+# will return what was printed to STDOUT from the shell command.
+IO.popen('ls')
+
diff --git a/code/snippets/standard_streams.rb b/code/snippets/standard_streams.rb
new file mode 100644
index 0000000..9f24fd3
--- /dev/null
+++ b/code/snippets/standard_streams.rb
@@ -0,0 +1,3 @@
+puts STDIN.fileno
+puts STDOUT.fileno
+puts STDERR.fileno
diff --git a/code/snippets/trap_int_signal.rb b/code/snippets/trap_int_signal.rb
new file mode 100644
index 0000000..b1d9d5b
--- /dev/null
+++ b/code/snippets/trap_int_signal.rb
@@ -0,0 +1,8 @@
+trap(:INT) { puts 'This is the first signal handler' }
+
+old_handler = trap(:INT) {
+ old_handler.call
+ puts 'This is the second handler'
+ exit
+}
+sleep 5 # so that we have time to send it a signal
diff --git a/code/snippets/wait2.rb b/code/snippets/wait2.rb
new file mode 100644
index 0000000..329cbc7
--- /dev/null
+++ b/code/snippets/wait2.rb
@@ -0,0 +1,26 @@
+# We create 5 child processes.
+5.times do
+ fork do
+ # Each generates a random number. If even they exit
+ # with a 111 exit code, otherwise they use a 112 exit code.
+ if rand(5).even?
+ exit 111
+ else
+ exit 112
+ end
+ end
+end
+
+5.times do
+ # We wait for each of the child processes to exit.
+ pid, status = Process.wait2
+
+ # If the child process exited with the 111 exit code
+ # then we know they encountered an even number.
+ if status.exitstatus == 111
+ puts "#{pid} encountered an even number!"
+ else
+ puts "#{pid} encountered an odd number!"
+ end
+end
+
diff --git a/code/snippets/wait_for_each_process.rb b/code/snippets/wait_for_each_process.rb
new file mode 100644
index 0000000..ca14418
--- /dev/null
+++ b/code/snippets/wait_for_each_process.rb
@@ -0,0 +1,14 @@
+# We create 3 child processes.
+3.times do
+ fork do
+ # Each one sleeps for a random amount of number less than 5 seconds.
+ sleep rand(5)
+ end
+end
+
+3.times do
+ # We wait for each child process to exit and print the pid that
+ # gets returned.
+ puts Process.wait
+end
+
diff --git a/code/snippets/waitpid2.rb b/code/snippets/waitpid2.rb
new file mode 100644
index 0000000..3a5c2f2
--- /dev/null
+++ b/code/snippets/waitpid2.rb
@@ -0,0 +1,11 @@
+favourite = fork do
+ exit 77
+end
+
+middle_child = fork do
+ abort "I want to be waited on!"
+end
+
+pid, status = Process.waitpid2 favourite
+puts status.exitstatus
+
diff --git a/code/snippets/zombie_process.rb b/code/snippets/zombie_process.rb
new file mode 100644
index 0000000..bf2413e
--- /dev/null
+++ b/code/snippets/zombie_process.rb
@@ -0,0 +1,17 @@
+message = 'Good Morning'
+recipient = 'tree@mybackyard.com'
+
+pid = fork do
+ # In this contrived example the parent process forks a child to take
+ # care of sending data to the stats collector. Meanwhile the parent
+ # process has continued on with its work of sending the actual payload.
+
+ # The parent process doesn't want to be slowed down with this task, and
+ # it doesn't matter if this would fail for some reason.
+ StatsCollector.record message, recipient
+end
+
+# This line ensures that the process performing the stats collection
+# won't become a zombie.
+Process.detach(pid)
+
diff --git a/code/snippets/zombies_eg.rb b/code/snippets/zombies_eg.rb
new file mode 100644
index 0000000..475c11c
--- /dev/null
+++ b/code/snippets/zombies_eg.rb
@@ -0,0 +1,7 @@
+# Create a child process that exits after 1 second.
+pid = fork { sleep 1 }
+# Print its pid.
+puts pid
+# Put the parent process to sleep so we can inspect the
+# process status of the child
+sleep 5
diff --git a/code/spyglass/.gitignore b/code/spyglass/.gitignore
new file mode 100644
index 0000000..cdb8017
--- /dev/null
+++ b/code/spyglass/.gitignore
@@ -0,0 +1,4 @@
+tmp/
+doc/
+test/log/*.log
+lib/*.bundle
diff --git a/code/spyglass/Gemfile b/code/spyglass/Gemfile
new file mode 100644
index 0000000..d46b645
--- /dev/null
+++ b/code/spyglass/Gemfile
@@ -0,0 +1,13 @@
+source 'https://rubygems.org'
+
+gem 'rack', '~> 1.3.5'
+gem 'rake-compiler' # For the http parser
+gem 'launchy'
+
+group :test do
+ gem 'excon'
+ gem 'minitest'
+ gem 'posix-spawn'
+ gem 'sinatra'
+end
+
diff --git a/code/spyglass/Gemfile.lock b/code/spyglass/Gemfile.lock
new file mode 100644
index 0000000..f294659
--- /dev/null
+++ b/code/spyglass/Gemfile.lock
@@ -0,0 +1,32 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.3.4)
+ excon (0.25.3)
+ launchy (2.3.0)
+ addressable (~> 2.3)
+ minitest (5.0.6)
+ posix-spawn (0.3.6)
+ rack (1.3.10)
+ rack-protection (1.5.0)
+ rack
+ rake (10.1.0)
+ rake-compiler (0.8.3)
+ rake
+ sinatra (1.3.3)
+ rack (~> 1.3, >= 1.3.6)
+ rack-protection (~> 1.2)
+ tilt (~> 1.3, >= 1.3.3)
+ tilt (1.4.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ excon
+ launchy
+ minitest
+ posix-spawn
+ rack (~> 1.3.5)
+ rake-compiler
+ sinatra
diff --git a/code/spyglass/LICENSE b/code/spyglass/LICENSE
new file mode 100644
index 0000000..62b07fc
--- /dev/null
+++ b/code/spyglass/LICENSE
@@ -0,0 +1,189 @@
+SHORT VERSION:
+
+You CAN use the included code for educational and personal purposes
+only. You CANNOT redistribute the included code as is.
+
+LONG VERSION:
+
+By purchasing, installing, or otherwise using the enclosed
+product, you agree to be bound by the terms and conditions of this
+License Agreement.
+
+IMPORTANT – READ CAREFULLY: This license agreement (“LICENSE”) is
+a legal agreement between you (either an individual or a single
+entity, also referred to as "LICENSEE", "YOU") and Jesse Storimer,
+for the software containing this LICENSE which may also
+include the software’s source code written in a high-level
+computer language, associated media, printed materials, and
+"online" or electronic documentation (collectively referred to as
+“SOFTWARE”).
+
+Any earlier license we may have granted to you for the use of
+earlier versions of the SOFTWARE is replaced by this LICENSE.
+
+SOFTWARE PRODUCT LICENSE
+
+The SOFTWARE is protected by copyright laws and international
+copyright treaties, as well as other intellectual property laws
+and treaties and contains confidential information and trade
+secrets. Jesse Storimer retains all rights not expressly
+granted to you in this LICENSE.
+
+I. GRANT OF LICENSE
+
+Jesse Storimer hereby grants to you, and you accept, a
+non-exclusive, non-transferable license to install, copy, use and
+modify the SOFTWARE only as authorized below.
+
+This license grants you the right to use this SOFTWARE for personal
+use.
+
+All product licenses are perpetual and royalty-free.
+
+1. PERSONAL LICENSE
+
+You are granted a license to use the SOFTWARE for personal use.
+You may not redistribute the SOFTWARE in any form.
+
+SOURCE CODE
+
+Jesse Storimer DOES NOT provide technical support for
+modified source code. The SOFTWARE’s source code is provided as
+is. In the event you develop any troubleshooting-related
+modifications of the SOFTWARE, either independently or jointly
+with Jesse Storimer, such modifications and all rights
+associated therewith will be the exclusive property of Jesse Storimer.
+You are granted the right to use such modifications as
+set forth in this agreement. You acknowledge that the SOFTWARE’s
+source code contains valuable and proprietary trade secrets of
+Jesse Storimer. All individuals employed by or belonging to
+your entity agree to expend every effort to ensure its confidentiality.
+You agree to assume full responsibility for such employees’ or
+contractors’ use, or misuse, of such disclosed source code as if
+it was your use. These obligations shall not apply to any
+information generally available to the public, independently
+developed or obtained without reliance on Jesse Storimer's
+information, or approved in writing for release by Jesse Storimer
+without restriction.
+
+II. OTHER RIGHTS AND LIMITATIONS
+
+At no time may the SOFTWARE be used for development purposes by
+other individuals than the licensed developer(s). You are not allowed
+to resell, transfer, rent, lease, or sublicense the SOFTWARE and
+your associated rights. You are not allowed to use, copy, modify,
+or merge copies of the SOFTWARE and any accompanying documents
+except as permitted in this LICENSE.
+
+III. DELIVERY
+
+Jesse Storimer shall deliver to LICENSEE a master copy of
+the SOFTWARE licensed hereunder in electronic files only.
+Documentation shall also be provided in electronic format.
+
+IV. UPGRADES
+
+You are eligible for free upgrades (e.g. v1.5 to v1.8),
+patches, and bug-fixes for the SOFTWARE, including source code.
+SOFTWARE labeled as an upgrade replaces and/or supplements (and
+may disable) the product that formed the basis for your
+eligibility for the upgrade. You may use the resulting upgraded
+product only in accordance with the terms of this LICENSE.
+
+You are entitled to receive all version updates for the SOFTWARE
+period of 1 (one) year.
+
+V. TERMINATION
+
+This LICENSE shall last as long as you use the SOFTWARE in
+compliance with this LICENSE. Jesse Storimer may terminate
+this LICENSE if you fail to comply with any of the terms and
+conditions herein. In such event you agree to remove and destroy
+all copies of the SOFTWARE and any applicable source code.
+
+Jesse Storimer reserves the right to discontinue at any time
+any product, shall it be offered individually or as a part of a
+product SUITE. However, Jesse Storimer is obligated to
+provide the proper level of support for all discontinued products
+for a period of 1 (one) year after the date of discontinuance.
+
+VI. COPYRIGHT
+
+All title and copyrights in and to the SOFTWARE, the accompanying
+printed materials, and any copies of the SOFTWARE, and any
+trademarks or service marks of Jesse Storimer are owned by
+Jesse Storimer. All title and intellectual property rights
+in and to the content that may be accessed through use of the
+SOFTWARE is the property of the respective content owner and may
+be protected by applicable copyright or other intellectual
+property laws and treaties. This LICENSE grants you no rights to
+use such content.
+
+VII. LIMITED WARRANTY
+
+Jesse Storimer warrants solely that the SOFTWARE will
+perform substantially in accordance with the accompanying written
+materials for a period of ninety (90) days. Jesse Storimer
+does not warrant the use of the SOFTWARE will be uninterrupted or
+error free at all times and in all circumstances, nor that program
+errors will be corrected. This limited warranty shall not apply to
+any error or failure resulting from (i) machine error, (ii)
+LICENSEE's failure to follow operating instructions, (iii)
+negligence or accident, or (iv) modifications to the SOFTWARE by
+any person or entity other than Jesse Storimer. In the event
+of a breach of warranty, LICENSEE's sole and exclusive remedy, is
+repair of all or any portion of the SOFTWARE.
+If such remedy fails of its essential purpose, LICENSEE's sole
+remedy and Jesse Storimer's maximum liability shall be a
+refund of the paid purchase price for the defective SOFTWARE only.
+This limited warranty is only valid if Jesse Storimer
+receives written notice of breach of warranty within thirty days
+after the warranty period expires.
+
+VIII. LIMITATION OF LIABILITY
+
+TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT
+WILL Jesse Storimer BE LIABLE FOR ANY INDIRECT, SPECIAL,
+INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF OR
+INABILITY TO USE THE PRODUCT, INCLUDING, WITHOUT LIMITATION,
+DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR
+MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES,
+EVEN IF ADVISED OF THE POSSIBILITY THEREOF, AND REGARDLESS OF THE
+LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH
+THE CLAIM IS BASED. IN ANY CASE, Jesse Storimer's ENTIRE
+LIABILITY UNDER ANY PROVISION OF THIS AGREEMENT SHALL NOT EXCEED
+IN THE AGGREGATE THE SUM OF THE LICENSE FEES LICENSEE PAID TO
+Jesse Storimer FOR THE PRODUCT GIVING RISE TO SUCH DAMAGES,
+NOTWITHSTANDING ANY FAILURE OF ESSENTIAL PURPOSE OF ANY LIMITED
+REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS
+EXCLUSION AND LIMITATION MAY NOT BE APPLICABLE. Jesse
+Storimer IS NOT RESPONSIBLE FOR ANY LIABILITY ARISING OUT OF
+CONTENT PROVIDED BY LICENSEE OR A THIRD PARTY THAT IS ACCESSED
+THROUGH THE PRODUCT AND/OR ANY MATERIAL LINKED THROUGH SUCH
+CONTENT. ANY DATA INCLUDED IN A PRODUCT UPON SHIPMENT FROM
+Jesse Storimer IS FOR TESTING USE ONLY AND Jesse
+Storimer HEREBY DISCLAIMS ANY AND ALL LIABILITY ARISING
+THEREFROM.
+THE EXTENT OF Jesse Storimer's LIABILITY FOR THE LIMITED
+WARRANTY SECTION SHALL BE AS SET FORTH THEREIN.
+
+IX. MISCELLANEOUS
+
+If any provision of this LICENSE is to be held
+unenforceable, such holding will not affect the validity of the
+other provisions hereof. Failure of a party to enforce any
+provision of this LICENSE shall not constitute or be construed as
+a waiver of such provision or of the right to enforce such
+provision. This License represents the entire understanding
+between the parties with respect to its subject matter.
+
+YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, THAT YOU
+UNDERSTAND THIS AGREEMENT, AND UNDERSTAND THAT BY CONTINUING THE
+INSTALLATION OF THE SOFTWARE PRODUCT, BY LOADING OR RUNNING THE
+SOFTWARE PRODUCT, OR BY PLACING OR COPYING THE SOFTWARE ONTO YOUR
+COMPUTER HARD DRIVE, YOU AGREE TO BE BOUND BY THIS AGREEMENT'S
+TERMS AND CONDITIONS. YOU FURTHER AGREE THAT, EXCEPT FOR WRITTEN
+SEPARATE AGREEMENTS BETWEEN Jesse Storimer AND YOU, THIS
+AGREEMENT IS A COMPLETE AND EXCLUSIVE STATEMENT OF THE RIGHTS AND
+LIABILITIES OF THE PARTIES. \ No newline at end of file
diff --git a/code/spyglass/README.md b/code/spyglass/README.md
new file mode 100644
index 0000000..102194c
--- /dev/null
+++ b/code/spyglass/README.md
@@ -0,0 +1,37 @@
+Spyglass Web Server
+===================
+
+Spyglass is a Rack web server that rides on Unix systems.
+
+It was designed from the ground up to be an educational tool. It's primary purpose will always be to show techniques and reveal intent over features and speed.
+
+How to use it
+==============
+
+To get a good introduction to the source code and the architecture run `rake read`. Your other options:
+
+1. If you have to use it to understand it then have a look at the Usage heading below.
+2. If you want to peruse the raw source then go ahead, you've already got it! You can browse the [Rocco](https://github.com/rtomayko/rocco)-annotated source code using `rake read`.
+3. If you're a tinkerer then you're free to make modifications to the code. There's not a good way to share them at the moment, but if you do anything interesting or want help with something then send an email to jesse@jstorimer.com.
+
+Usage
+======
+
+Spyglass is not packaged as a Rubygem.
+
+It comes with a script called `spyglass` in the `bin/` directory. If you want to use the binary with Rack applications on your system the easiest approach is to drop a symlink to `bin/spyglass` somewhere in your `PATH`.
+
+ ln -s /path/to/current/dir/bin/spyglass /usr/local/bin/spyglass
+ spyglass -h
+
+Dependencies for development/testing can be installed with Bundler, using the `bundle` command.
+
+Spyglass' http parser is borrowed from Thin, and is a C extension. You'll need to compile it for your system using `rake compile` before running the server.
+
+License
+========
+
+See LICENSE
+
+(c) 2011 Jesse Storimer
+
diff --git a/code/spyglass/Rakefile b/code/spyglass/Rakefile
new file mode 100644
index 0000000..aeea3b6
--- /dev/null
+++ b/code/spyglass/Rakefile
@@ -0,0 +1,45 @@
+require 'bundler/setup'
+
+require 'rake'
+require 'rake/extensiontask'
+require 'rake/testtask'
+require 'rake/clean'
+require 'launchy'
+CLEAN.include 'docs/*'
+
+desc "Compile the Ragel state machines"
+task :ragel do
+ Dir.chdir 'ext/spyglass_parser' do
+ target = "parser.c"
+ File.unlink target if File.exist? target
+ sh "ragel parser.rl -G2 -o #{target}"
+ raise "Failed to compile Ragel state machine" unless File.exist? target
+ end
+end
+
+Rake::ExtensionTask.new('spyglass_parser')
+
+Rake::TestTask.new(:test => :compile) do |t|
+ t.libs << 'test'
+ t.ruby_opts << '-rubygems'
+ t.test_files = FileList['test/*_test.rb']
+end
+
+desc 'Build documentation'
+task :doc do
+ sh "docco lib/*.rb"
+ sh "docco lib/**/*.rb"
+end
+
+desc 'Open documentation in your browser for reading'
+task :read do
+ Launchy.open('docs/spyglass.html')
+end
+
+desc 'Show the README'
+task :readme do
+ exec 'less README.md'
+end
+
+task :default => :readme
+
diff --git a/code/spyglass/bin/spyglass b/code/spyglass/bin/spyglass
new file mode 100755
index 0000000..83f4ff7
--- /dev/null
+++ b/code/spyglass/bin/spyglass
@@ -0,0 +1,83 @@
+#!/usr/bin/env ruby
+
+THIS_FILE = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
+$LOAD_PATH << File.dirname(THIS_FILE) + '/../lib'
+require 'rubygems'
+require 'spyglass'
+require 'optparse'
+
+opts = OptionParser.new do |opts|
+ opts.banner = "Usage: spyglass [options]"
+
+ opts.separator ""
+ opts.separator "Ruby options:"
+
+ lineno = 1
+ opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
+ eval line, TOPLEVEL_BINDING, "-e", lineno
+ lineno += 1
+ }
+
+ opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
+ $DEBUG = true
+ }
+
+ opts.on("-w", "--warn", "turn warnings on for your script") {
+ $-w = true
+ }
+
+ opts.on("-I", "--include PATH",
+ "specify $LOAD_PATH (may be used more than once)") { |path|
+ $LOAD_PATH.unshift(path.split(":"))
+ }
+
+ opts.on("-r", "--require LIBRARY",
+ "require the library, before executing your script") { |library|
+ require library
+ }
+
+ opts.separator ""
+ opts.separator "Spyglass options:"
+
+ opts.on("-p", "--port PORT", "use PORT (default: 4222)") { |port|
+ Spyglass::Config.port port
+ }
+
+ opts.on("-o", "--host HOST", "list on HOST (default: 0.0.0.0)") { |host|
+ Spyglass::Config.host host
+ }
+
+ opts.on("-c", "--configru FILE", "Load the rackup file at FILE (default: config.ru in current directory)") { |path|
+ Spyglass::Config.config_ru_path path
+ }
+
+ opts.on("-w", "--workers COUNT", "Prefork COUNT workers when handling requests (default: 2)") { |count|
+ Spyglass::Config.workers count.to_i
+ }
+
+ opts.on("-t", "--timeout SEC", "Time out the master process after SEC seconds (default: 30)") { |sec|
+ Spyglass::Config.timeout sec.to_i
+ }
+
+ opts.on("-v", "--verbose", "Enable verbose output") { |verbose|
+ Spyglass::Config.verbose true
+ }
+
+ opts.on("--vverbose", "Enable very verbose output") { |vverbose|
+ Spyglass::Config.vverbose true
+ }
+
+ opts.on_tail("-h", "--help", "Show this message") do
+ puts opts
+ abort
+ end
+
+ # Another typical switch to print the version.
+ opts.on_tail("--version", "Show version") do
+ puts Spyglass::Version
+ exit
+ end
+end
+
+opts.parse!(ARGV)
+Spyglass::Server.instance.start
diff --git a/code/spyglass/config.ru b/code/spyglass/config.ru
new file mode 100644
index 0000000..6c9ddb7
--- /dev/null
+++ b/code/spyglass/config.ru
@@ -0,0 +1,7 @@
+app = lambda do |env|
+ body = "Hello, World!"
+ [200, {"Content-Type" => "text/plain", "Content-Length" => body.length.to_s}, [body]]
+end
+
+run app
+
diff --git a/code/spyglass/docs/configurator.html b/code/spyglass/docs/configurator.html
new file mode 100644
index 0000000..69c19a5
--- /dev/null
+++ b/code/spyglass/docs/configurator.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>configurator.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>configurator.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="class"><span class="keyword">class</span> <span class="title">Configurator</span></span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <p>A hash of key =&gt; default</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="constant">OPTIONS</span> = {
+ <span class="symbol">:port</span> =&gt; <span class="number">4222</span>,
+ <span class="symbol">:host</span> =&gt; <span class="string">'0.0.0.0'</span>,
+ <span class="symbol">:workers</span> =&gt; <span class="number">2</span>,
+ <span class="symbol">:timeout</span> =&gt; <span class="number">30</span>,
+ <span class="symbol">:config_ru_path</span> =&gt; <span class="string">'config.ru'</span>,
+ <span class="symbol">:verbose</span> =&gt; <span class="keyword">false</span>,
+ <span class="symbol">:vverbose</span> =&gt; <span class="keyword">false</span>
+ }
+
+ <span class="class"><span class="keyword">class</span> <span class="inheritance">&lt;</span><span class="inheritance">&lt; <span class="parent">self</span></span></span>
+ <span class="constant">OPTIONS</span>.each <span class="keyword">do</span> |key, default|</pre></div></div>
+
+ </li>
+
+
+ <li id="section-3">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-3">&#182;</a>
+ </div>
+ <p>attr_writer key</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre>
+ define_method(key) <span class="keyword">do</span> |*args|
+ arg = args.shift
+ <span class="keyword">if</span> arg
+ instance_variable_set(<span class="string">"@<span class="subst">#{key}</span>"</span>, arg)
+ <span class="keyword">else</span>
+ instance_variable_get(<span class="string">"@<span class="subst">#{key}</span>"</span>) || default
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="constant">Config</span> = <span class="constant">Configurator</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/docco.css b/code/spyglass/docs/docco.css
new file mode 100644
index 0000000..f690a07
--- /dev/null
+++ b/code/spyglass/docs/docco.css
@@ -0,0 +1,500 @@
+/*--------------------- Typography ----------------------------*/
+
+@font-face {
+ font-family: 'aller-light';
+ src: url('public/fonts/aller-light.eot');
+ src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'),
+ url('public/fonts/aller-light.woff') format('woff'),
+ url('public/fonts/aller-light.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'aller-bold';
+ src: url('public/fonts/aller-bold.eot');
+ src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'),
+ url('public/fonts/aller-bold.woff') format('woff'),
+ url('public/fonts/aller-bold.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'novecento-bold';
+ src: url('public/fonts/novecento-bold.eot');
+ src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'),
+ url('public/fonts/novecento-bold.woff') format('woff'),
+ url('public/fonts/novecento-bold.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+/*--------------------- Layout ----------------------------*/
+html { height: 100%; }
+body {
+ font-family: "aller-light";
+ font-size: 14px;
+ line-height: 18px;
+ color: #30404f;
+ margin: 0; padding: 0;
+ height:100%;
+}
+#container { min-height: 100%; }
+
+a {
+ color: #000;
+}
+
+b, strong {
+ font-weight: normal;
+ font-family: "aller-bold";
+}
+
+p, ul, ol {
+ margin: 15px 0 0px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #112233;
+ line-height: 1em;
+ font-weight: normal;
+ font-family: "novecento-bold";
+ text-transform: uppercase;
+ margin: 30px 0 15px 0;
+}
+
+h1 {
+ margin-top: 40px;
+}
+
+hr {
+ border: 0;
+ background: 1px solid #ddd;
+ height: 1px;
+ margin: 20px 0;
+}
+
+pre, tt, code {
+ font-size: 12px; line-height: 16px;
+ font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
+ margin: 0; padding: 0;
+}
+ .annotation pre {
+ display: block;
+ margin: 0;
+ padding: 7px 10px;
+ background: #fcfcfc;
+ -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
+ -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
+ overflow-x: auto;
+ }
+ .annotation pre code {
+ border: 0;
+ padding: 0;
+ background: transparent;
+ }
+
+
+blockquote {
+ border-left: 5px solid #ccc;
+ margin: 0;
+ padding: 1px 0 1px 1em;
+}
+ .sections blockquote p {
+ font-family: Menlo, Consolas, Monaco, monospace;
+ font-size: 12px; line-height: 16px;
+ color: #999;
+ margin: 10px 0 0;
+ white-space: pre-wrap;
+ }
+
+ul.sections {
+ list-style: none;
+ padding:0 0 5px 0;;
+ margin:0;
+}
+
+/*
+ Force border-box so that % widths fit the parent
+ container without overlap because of margin/padding.
+
+ More Info : http://www.quirksmode.org/css/box.html
+*/
+ul.sections > li > div {
+ -moz-box-sizing: border-box; /* firefox */
+ -ms-box-sizing: border-box; /* ie */
+ -webkit-box-sizing: border-box; /* webkit */
+ -khtml-box-sizing: border-box; /* konqueror */
+ box-sizing: border-box; /* css3 */
+}
+
+
+/*---------------------- Jump Page -----------------------------*/
+#jump_to, #jump_page {
+ margin: 0;
+ background: white;
+ -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
+ -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
+ font: 16px Arial;
+ cursor: pointer;
+ text-align: right;
+ list-style: none;
+}
+
+#jump_to a {
+ text-decoration: none;
+}
+
+#jump_to a.large {
+ display: none;
+}
+#jump_to a.small {
+ font-size: 22px;
+ font-weight: bold;
+ color: #676767;
+}
+
+#jump_to, #jump_wrapper {
+ position: fixed;
+ right: 0; top: 0;
+ padding: 10px 15px;
+ margin:0;
+}
+
+#jump_wrapper {
+ display: none;
+ padding:0;
+}
+
+#jump_to:hover #jump_wrapper {
+ display: block;
+}
+
+#jump_page {
+ padding: 5px 0 3px;
+ margin: 0 0 25px 25px;
+}
+
+#jump_page .source {
+ display: block;
+ padding: 15px;
+ text-decoration: none;
+ border-top: 1px solid #eee;
+}
+
+#jump_page .source:hover {
+ background: #f5f5ff;
+}
+
+#jump_page .source:first-child {
+}
+
+/*---------------------- Low resolutions (> 320px) ---------------------*/
+@media only screen and (min-width: 320px) {
+ .pilwrap { display: none; }
+
+ ul.sections > li > div {
+ display: block;
+ padding:5px 10px 0 10px;
+ }
+
+ ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
+ padding-left: 30px;
+ }
+
+ ul.sections > li > div.content {
+ background: #f5f5ff;
+ overflow-x:auto;
+ -webkit-box-shadow: inset 0 0 5px #e5e5ee;
+ box-shadow: inset 0 0 5px #e5e5ee;
+ border: 1px solid #dedede;
+ margin:5px 10px 5px 10px;
+ padding-bottom: 5px;
+ }
+
+ ul.sections > li > div.annotation pre {
+ margin: 7px 0 7px;
+ padding-left: 15px;
+ }
+
+ ul.sections > li > div.annotation p tt, .annotation code {
+ background: #f8f8ff;
+ border: 1px solid #dedede;
+ font-size: 12px;
+ padding: 0 0.2em;
+ }
+}
+
+/*---------------------- (> 481px) ---------------------*/
+@media only screen and (min-width: 481px) {
+ #container {
+ position: relative;
+ }
+ body {
+ background-color: #F5F5FF;
+ font-size: 15px;
+ line-height: 21px;
+ }
+ pre, tt, code {
+ line-height: 18px;
+ }
+ p, ul, ol {
+ margin: 0 0 15px;
+ }
+
+
+ #jump_to {
+ padding: 5px 10px;
+ }
+ #jump_wrapper {
+ padding: 0;
+ }
+ #jump_to, #jump_page {
+ font: 10px Arial;
+ text-transform: uppercase;
+ }
+ #jump_page .source {
+ padding: 5px 10px;
+ }
+ #jump_to a.large {
+ display: inline-block;
+ }
+ #jump_to a.small {
+ display: none;
+ }
+
+
+
+ #background {
+ position: absolute;
+ top: 0; bottom: 0;
+ width: 350px;
+ background: #fff;
+ border-right: 1px solid #e5e5ee;
+ z-index: -1;
+ }
+
+ ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
+ padding-left: 40px;
+ }
+
+ ul.sections > li {
+ white-space: nowrap;
+ }
+
+ ul.sections > li > div {
+ display: inline-block;
+ }
+
+ ul.sections > li > div.annotation {
+ max-width: 350px;
+ min-width: 350px;
+ min-height: 5px;
+ padding: 13px;
+ overflow-x: hidden;
+ white-space: normal;
+ vertical-align: top;
+ text-align: left;
+ }
+ ul.sections > li > div.annotation pre {
+ margin: 15px 0 15px;
+ padding-left: 15px;
+ }
+
+ ul.sections > li > div.content {
+ padding: 13px;
+ vertical-align: top;
+ background: #f5f5ff;
+ border: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ }
+
+ .pilwrap {
+ position: relative;
+ display: inline;
+ }
+
+ .pilcrow {
+ font: 12px Arial;
+ text-decoration: none;
+ color: #454545;
+ position: absolute;
+ top: 3px; left: -20px;
+ padding: 1px 2px;
+ opacity: 0;
+ -webkit-transition: opacity 0.2s linear;
+ }
+ .for-h1 .pilcrow {
+ top: 47px;
+ }
+ .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow {
+ top: 35px;
+ }
+
+ ul.sections > li > div.annotation:hover .pilcrow {
+ opacity: 1;
+ }
+}
+
+/*---------------------- (> 1025px) ---------------------*/
+@media only screen and (min-width: 1025px) {
+
+ body {
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ #background {
+ width: 525px;
+ }
+ ul.sections > li > div.annotation {
+ max-width: 525px;
+ min-width: 525px;
+ padding: 10px 25px 1px 50px;
+ }
+ ul.sections > li > div.content {
+ padding: 9px 15px 16px 25px;
+ }
+}
+
+/*---------------------- Syntax Highlighting -----------------------------*/
+
+td.linenos { background-color: #f0f0f0; padding-right: 10px; }
+span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
+/*
+
+github.com style (c) Vasily Polovnyov <vast@whiteants.net>
+
+*/
+
+pre code {
+ display: block; padding: 0.5em;
+ color: #000;
+ background: #f8f8ff
+}
+
+pre .comment,
+pre .template_comment,
+pre .diff .header,
+pre .javadoc {
+ color: #408080;
+ font-style: italic
+}
+
+pre .keyword,
+pre .assignment,
+pre .literal,
+pre .css .rule .keyword,
+pre .winutils,
+pre .javascript .title,
+pre .lisp .title,
+pre .subst {
+ color: #954121;
+ /*font-weight: bold*/
+}
+
+pre .number,
+pre .hexcolor {
+ color: #40a070
+}
+
+pre .string,
+pre .tag .value,
+pre .phpdoc,
+pre .tex .formula {
+ color: #219161;
+}
+
+pre .title,
+pre .id {
+ color: #19469D;
+}
+pre .params {
+ color: #00F;
+}
+
+pre .javascript .title,
+pre .lisp .title,
+pre .subst {
+ font-weight: normal
+}
+
+pre .class .title,
+pre .haskell .label,
+pre .tex .command {
+ color: #458;
+ font-weight: bold
+}
+
+pre .tag,
+pre .tag .title,
+pre .rules .property,
+pre .django .tag .keyword {
+ color: #000080;
+ font-weight: normal
+}
+
+pre .attribute,
+pre .variable,
+pre .instancevar,
+pre .lisp .body {
+ color: #008080
+}
+
+pre .regexp {
+ color: #B68
+}
+
+pre .class {
+ color: #458;
+ font-weight: bold
+}
+
+pre .symbol,
+pre .ruby .symbol .string,
+pre .ruby .symbol .keyword,
+pre .ruby .symbol .keymethods,
+pre .lisp .keyword,
+pre .tex .special,
+pre .input_number {
+ color: #990073
+}
+
+pre .builtin,
+pre .constructor,
+pre .built_in,
+pre .lisp .title {
+ color: #0086b3
+}
+
+pre .preprocessor,
+pre .pi,
+pre .doctype,
+pre .shebang,
+pre .cdata {
+ color: #999;
+ font-weight: bold
+}
+
+pre .deletion {
+ background: #fdd
+}
+
+pre .addition {
+ background: #dfd
+}
+
+pre .diff .change {
+ background: #0086b3
+}
+
+pre .chunk {
+ color: #aaa
+}
+
+pre .tex .formula {
+ opacity: 0.5;
+}
diff --git a/code/spyglass/docs/logging.html b/code/spyglass/docs/logging.html
new file mode 100644
index 0000000..5c4262a
--- /dev/null
+++ b/code/spyglass/docs/logging.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>logging.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>logging.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="class"><span class="keyword">module</span> <span class="title">Logging</span></span>
+ <span class="function"><span class="keyword">def</span> <span class="title">out</span><span class="params">(message)</span></span>
+ <span class="variable">$stdout</span>.puts preamble + message
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">err</span><span class="params">(message)</span></span>
+ <span class="variable">$stderr</span>.puts preamble + message
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">verbose</span><span class="params">(message)</span></span>
+ <span class="keyword">return</span> <span class="keyword">unless</span> <span class="constant">Config</span>.verbose
+ out(message)
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">vverbose</span><span class="params">(message)</span></span>
+ <span class="keyword">return</span> <span class="keyword">unless</span> <span class="constant">Config</span>.vverbose
+ out(message)
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">preamble</span></span>
+ <span class="string">"[<span class="subst">#{<span class="constant">Process</span>.pid}</span>] [<span class="subst">#{<span class="keyword">self</span>.<span class="keyword">class</span>.name}</span>] "</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/lookout.html b/code/spyglass/docs/lookout.html
new file mode 100644
index 0000000..a017f33
--- /dev/null
+++ b/code/spyglass/docs/lookout.html
@@ -0,0 +1,325 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>lookout.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>lookout.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="class"><span class="keyword">class</span> <span class="title">Lookout</span></span>
+ <span class="keyword">include</span> <span class="constant">Singleton</span>, <span class="constant">Logging</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <p>This method is the main entry point for the Lookout class. It takes
+a socket object.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="function"><span class="keyword">def</span> <span class="title">start</span><span class="params">(socket)</span></span>
+ trap_signals</pre></div></div>
+
+ </li>
+
+
+ <li id="section-3">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-3">&#182;</a>
+ </div>
+ <p>The Lookout doesn&#39;t know anything about the app itself, so there&#39;s
+no app related setup to do here.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> loop <span class="keyword">do</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-4">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-4">&#182;</a>
+ </div>
+ <p>Accepts a new connection on our socket. This class won&#39;t actually
+do anything interesting with this connection, it will pass it down
+to the <code>Master</code> class created below to do the actual request handling.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> conn = socket.accept
+ out <span class="string">"Received incoming connection"</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-5">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-5">&#182;</a>
+ </div>
+ <p>In this block the Lookout forks a new process and invokes a Master,
+passing along the socket it received and the connection it accepted
+above.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@master_pid</span> = fork <span class="keyword">do</span>
+ master = <span class="constant">Master</span>.new(conn, socket)
+ master.start
+ <span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-6">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-6">&#182;</a>
+ </div>
+ <p>The Lookout can now close its handle on the client socket. This doesn&#39;t
+translate to the socket being closed on the clients end because the
+forked Master process also has a handle on the same socket. Since this
+handle is now cleaned up it&#39;s up to the Master process to ensure that
+its handle gets cleaned up.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> conn.close</pre></div></div>
+
+ </li>
+
+
+ <li id="section-7">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-7">&#182;</a>
+ </div>
+ <p>Now this process blocks until the Master process exits. The Master process
+will only exit once traffic is slow enough that it has reached its timeout
+without receiving any new connections.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="constant">Process</span>.waitpid(<span class="variable">@master_pid</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-8">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-8">&#182;</a>
+ </div>
+ <p>The interaction of fork(2)/waitpid(2) above deserve some explanation.</p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-9">
+ <div class="annotation">
+
+ <div class="pilwrap for-h3">
+ <a class="pilcrow" href="#section-9">&#182;</a>
+ </div>
+ <h3>Why fork(2)? Why not just spin up the Master?</h3>
+<p>The whole point of the Lookout process is to be very lean. The only resource
+that it initializes is the listening socket for the server. It doesn&#39;t load
+any of your application into memory, so its resource footprint is very small.</p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-10">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-10">&#182;</a>
+ </div>
+ <p>The reason that it does a fork(2) before invoking the Master is because once
+the Master times out we want the Lookout process to remain lean when accepting
+the next connection. </p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-11">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-11">&#182;</a>
+ </div>
+ <p>If it were to load the application code without forking
+then there would be no (simple) way for it to later unload the application code.</p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-12">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-12">&#182;</a>
+ </div>
+ <p>By doing a fork(2), then waiting for the Master process to exit, that guarantees
+that all resources (notably memory usage) that were in use by the Master process
+will be reclaimed by the kernel. </p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-13">
+ <div class="annotation">
+
+ <div class="pilwrap for-h3">
+ <a class="pilcrow" href="#section-13">&#182;</a>
+ </div>
+ <h3>Who knows what your app will demand!</h3>
+<p>While handling requests your app may require lots of memory. Containing this in a
+child process, and exiting that process, is the easiest way to ensure that memory
+bloat isn&#39;t shared with our simple parent process.</p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-14">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-14">&#182;</a>
+ </div>
+ <p>This allows our Lookout process will to go back around
+the loop with nothing more than it started with, just a listening socket.</p>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-15">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-15">&#182;</a>
+ </div>
+ <p>The fork(2)/waitpid(2) approach requires little code to implement, and pushes
+responsibility down to the kernel to track resource usage and nicely clean up
+the Master process when it&#39;s finished.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">trap_signals</span></span>
+ [<span class="symbol">:INT</span>, <span class="symbol">:QUIT</span>].each <span class="keyword">do</span> |sig|
+ trap(sig) {
+ <span class="keyword">begin</span>
+ <span class="constant">Process</span>.kill(sig, <span class="variable">@master_pid</span>) <span class="keyword">if</span> <span class="variable">@master_pid</span>
+ <span class="keyword">rescue</span> <span class="constant">Errno::ESRCH</span>
+ <span class="keyword">end</span>
+ exit
+ }
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/master.html b/code/spyglass/docs/master.html
new file mode 100644
index 0000000..11f2abc
--- /dev/null
+++ b/code/spyglass/docs/master.html
@@ -0,0 +1,268 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>master.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>master.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="class"><span class="keyword">class</span> <span class="title">Master</span></span>
+ <span class="keyword">include</span> <span class="constant">Logging</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">initialize</span><span class="params">(connection, socket)</span></span>
+ <span class="variable">@connection</span>, <span class="variable">@socket</span> = connection, socket
+ <span class="variable">@worker_pids</span> = []</pre></div></div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <p>The Master shares this pipe with each of its worker processes. It
+passes the writable end down to each spawned worker while it listens
+on the readable end. Each worker will write to the pipe each time
+it accepts a new connection. If The Master doesn&#39;t get anything on
+the pipe before <code>Config.timeout</code> elapses then it kills its workers
+and exits. </p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@readable_pipe</span>, <span class="variable">@writable_pipe</span> = <span class="constant">IO</span>.pipe
+ <span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-3">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-3">&#182;</a>
+ </div>
+ <p>This method starts the Master. It enters an infinite loop where it creates
+processes to handle web requests and ensures that they stay active. It takes
+a connection as an argument from the Lookout instance. A Master will only
+be started when a connection is received by the Lookout.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="function"><span class="keyword">def</span> <span class="title">start</span></span>
+ trap_signals
+
+ load_app
+ out <span class="string">"Loaded the app"</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-4">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-4">&#182;</a>
+ </div>
+ <p>The first worker we spawn has to handle the connection that was already
+passed to us.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> spawn_worker(<span class="variable">@connection</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-5">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-5">&#182;</a>
+ </div>
+ <p>The Master can now close its handle on the client socket since the
+forked worker also got a handle on the same socket. Since this one
+is now closed it&#39;s up to the Worker process to close its handle when
+it&#39;s done. At that point the client connection will perceive that
+it&#39;s been closed on their end.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@connection</span>.close</pre></div></div>
+
+ </li>
+
+
+ <li id="section-6">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-6">&#182;</a>
+ </div>
+ <p>We spawn the rest of the workers.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> (<span class="constant">Config</span>.workers - <span class="number">1</span>).times { spawn_worker }
+ out <span class="string">"Spawned <span class="subst">#{<span class="constant">Config</span>.workers}</span> workers. Babysitting now..."</span>
+
+ loop <span class="keyword">do</span>
+ <span class="keyword">if</span> timed_out?(<span class="constant">IO</span>.select([<span class="variable">@readable_pipe</span>], <span class="keyword">nil</span>, <span class="keyword">nil</span>, <span class="constant">Config</span>.timeout))
+ out <span class="string">"Timed out after <span class="subst">#{<span class="constant">Config</span>.timeout}</span> s. Exiting."</span>
+
+ kill_workers(<span class="symbol">:QUIT</span>)
+ exit
+ <span class="keyword">else</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-7">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-7">&#182;</a>
+ </div>
+ <p>Clear the data on the pipe so it doesn&#39;t appear to be readable
+next time around the loop.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@readable_pipe</span>.read_nonblock <span class="number">1</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">timed_out?</span><span class="params">(select_result)</span></span>
+ !select_result
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">spawn_worker</span><span class="params">(connection = <span class="keyword">nil</span>)</span></span>
+ <span class="variable">@worker_pids</span> &lt;&lt; fork { <span class="constant">Worker</span>.new(<span class="variable">@socket</span>, <span class="variable">@app</span>, <span class="variable">@writable_pipe</span>, connection).start }
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">trap_signals</span></span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-8">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-8">&#182;</a>
+ </div>
+ <p>The QUIT signal triggers a graceful shutdown. The master shuts down
+immediately and lets each worker finish the request they are currently
+processing.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> trap(<span class="symbol">:QUIT</span>) <span class="keyword">do</span>
+ verbose <span class="string">"Received QUIT"</span>
+
+ kill_workers(<span class="symbol">:QUIT</span>)
+ exit
+ <span class="keyword">end</span>
+
+ trap(<span class="symbol">:CHLD</span>) <span class="keyword">do</span>
+ dead_worker = <span class="constant">Process</span>.wait
+ <span class="variable">@worker_pids</span>.delete(dead_worker)
+
+ <span class="variable">@worker_pids</span>.each <span class="keyword">do</span> |wpid|
+ <span class="keyword">begin</span>
+ dead_worker = <span class="constant">Process</span>.waitpid(wpid, <span class="constant">Process::WNOHANG</span>)
+ <span class="variable">@worker_pids</span>.delete(dead_worker)
+ <span class="keyword">rescue</span> <span class="constant">Errno::ECHILD</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ spawn_worker
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">kill_workers</span><span class="params">(sig)</span></span>
+ <span class="variable">@worker_pids</span>.each <span class="keyword">do</span> |wpid|
+ <span class="constant">Process</span>.kill(sig, wpid)
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">load_app</span></span>
+ <span class="variable">@app</span>, options = <span class="constant">Rack::Builder</span>.parse_file(<span class="constant">Config</span>.config_ru_path)
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/public/fonts/aller-bold.eot b/code/spyglass/docs/public/fonts/aller-bold.eot
new file mode 100755
index 0000000..1b32532
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-bold.eot
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/aller-bold.ttf b/code/spyglass/docs/public/fonts/aller-bold.ttf
new file mode 100755
index 0000000..dc4cc9c
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-bold.ttf
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/aller-bold.woff b/code/spyglass/docs/public/fonts/aller-bold.woff
new file mode 100755
index 0000000..fa16fd0
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-bold.woff
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/aller-light.eot b/code/spyglass/docs/public/fonts/aller-light.eot
new file mode 100755
index 0000000..40bd654
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-light.eot
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/aller-light.ttf b/code/spyglass/docs/public/fonts/aller-light.ttf
new file mode 100755
index 0000000..c2c7290
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-light.ttf
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/aller-light.woff b/code/spyglass/docs/public/fonts/aller-light.woff
new file mode 100755
index 0000000..81a09d1
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/aller-light.woff
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/novecento-bold.eot b/code/spyglass/docs/public/fonts/novecento-bold.eot
new file mode 100755
index 0000000..98a9a7f
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/novecento-bold.eot
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/novecento-bold.ttf b/code/spyglass/docs/public/fonts/novecento-bold.ttf
new file mode 100755
index 0000000..2af39b0
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/novecento-bold.ttf
Binary files differ
diff --git a/code/spyglass/docs/public/fonts/novecento-bold.woff b/code/spyglass/docs/public/fonts/novecento-bold.woff
new file mode 100755
index 0000000..de558b5
--- /dev/null
+++ b/code/spyglass/docs/public/fonts/novecento-bold.woff
Binary files differ
diff --git a/code/spyglass/docs/public/stylesheets/normalize.css b/code/spyglass/docs/public/stylesheets/normalize.css
new file mode 100644
index 0000000..73abb76
--- /dev/null
+++ b/code/spyglass/docs/public/stylesheets/normalize.css
@@ -0,0 +1,375 @@
+/*! normalize.css v2.0.1 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/*
+ * Corrects `block` display not defined in IE 8/9.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/*
+ * Corrects `inline-block` display not defined in IE 8/9.
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+/*
+ * Prevents modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/*
+ * Addresses styling for `hidden` attribute not present in IE 8/9.
+ */
+
+[hidden] {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/*
+ * 1. Sets default font family to sans-serif.
+ * 2. Prevents iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+ -ms-text-size-adjust: 100%; /* 2 */
+}
+
+/*
+ * Removes default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/*
+ * Addresses `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/*
+ * Improves readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/*
+ * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
+ * Safari 5, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+}
+
+/*
+ * Addresses styling not present in IE 8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/*
+ * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/*
+ * Addresses styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/*
+ * Addresses styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+
+/*
+ * Corrects font family set oddly in Safari 5 and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+ font-size: 1em;
+}
+
+/*
+ * Improves readability of pre-formatted text in all browsers.
+ */
+
+pre {
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/*
+ * Sets consistent quote types.
+ */
+
+q {
+ quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+/*
+ * Addresses inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/*
+ * Prevents `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/*
+ * Removes border when inside `a` element in IE 8/9.
+ */
+
+img {
+ border: 0;
+}
+
+/*
+ * Corrects overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/*
+ * Addresses margin not present in IE 8/9 and Safari 5.
+ */
+
+figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/*
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/*
+ * 1. Corrects color not being inherited in IE 8/9.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/*
+ * 1. Corrects font family not being inherited in all browsers.
+ * 2. Corrects font size not being inherited in all browsers.
+ * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
+ */
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/*
+ * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+ line-height: normal;
+}
+
+/*
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Corrects inability to style clickable `input` types in iOS.
+ * 3. Improves usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/*
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+input[disabled] {
+ cursor: default;
+}
+
+/*
+ * 1. Addresses box sizing set to `content-box` in IE 8/9.
+ * 2. Removes excess padding in IE 8/9.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/*
+ * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/*
+ * Removes inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/*
+ * Removes inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/*
+ * 1. Removes default vertical scrollbar in IE 8/9.
+ * 2. Improves readability and alignment in all browsers.
+ */
+
+textarea {
+ overflow: auto; /* 1 */
+ vertical-align: top; /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/*
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+} \ No newline at end of file
diff --git a/code/spyglass/docs/server.html b/code/spyglass/docs/server.html
new file mode 100644
index 0000000..1f5a12b
--- /dev/null
+++ b/code/spyglass/docs/server.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>server.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>server.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+
+ <span class="class"><span class="keyword">class</span> <span class="title">Server</span></span>
+ <span class="keyword">include</span> <span class="constant">Singleton</span>
+ <span class="keyword">include</span> <span class="constant">Logging</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">start</span></span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <p>Opens the main listening socket for the server. Now the server is responsive to
+incoming connections.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> sock = <span class="constant">TCPServer</span>.open(<span class="constant">Config</span>.host, <span class="constant">Config</span>.port)
+ out <span class="string">"Listening on port <span class="subst">#{<span class="constant">Config</span>.host}</span>:<span class="subst">#{<span class="constant">Config</span>.port}</span>"</span>
+
+ <span class="constant">Lookout</span>.instance.start(sock)
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/spyglass.html b/code/spyglass/docs/spyglass.html
new file mode 100644
index 0000000..3c317a4
--- /dev/null
+++ b/code/spyglass/docs/spyglass.html
@@ -0,0 +1,106 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>Spyglass</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul class="sections">
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap for-h1">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+ <h1>Spyglass</h1>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <p>This is Spyglass, a Rack web server that rides on Unix designed to be simple and teach
+others about Unix programming.</p>
+<p>It&#39;s namesake comes from the fact that when it boots up it&#39;s nothing more than a lone socket
+keeping a lookout for incoming connections. </p>
+<p>When a connection comes in it spins up a Master
+process which preforks some workers to actually handle http requests. If the Master process is
+left idle long enough it will shut itself (and it&#39;s workers) down and go back to just a lone
+listening socket, on the lookout for incoming connections.</p>
+<h1>Components</h1>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-3">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-3">&#182;</a>
+ </div>
+ <ul>
+<li><p><a href="server.html">Server</a> gets the ball rolling.
+The role of Server is pretty minimal. It opens the initial listening TCP socket,
+then passes that socket onto the Lookout. The Lookout will actually handle reading
+from the socket.</p>
+</li>
+<li><p><a href="lookout.html">Lookout</a> keeps a watch and notifies others when a connection
+comes in.
+The Lookout is a pretty &#39;dumb&#39; object. All that it does is listen for incoming
+connections on the socket it&#39;s given. Once it receives a connection it does a fork(2)
+and invokes a Master process. The Master process actually handles the connection.</p>
+</li>
+<li><p><a href="master.html">Master</a> loads the application and babysits worker processes
+that actually talk to clients.
+The role of the Master class is to create and babysit worker processes
+that will actually handle web requests. The Master itself doesn&#39;t know
+anything about http, etc. it just knows how to manage processes.</p>
+</li>
+<li><p><a href="worker.html">Worker</a> parses HTTP, calls the app, and writes back to the client.</p>
+</li>
+</ul>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="keyword">require</span> <span class="string">'singleton'</span>
+<span class="keyword">require</span> <span class="string">'socket'</span>
+<span class="keyword">require</span> <span class="string">'stringio'</span>
+
+<span class="keyword">require</span> <span class="string">'rack/server'</span>
+<span class="keyword">require</span> <span class="string">'rack/builder'</span>
+
+<span class="keyword">require</span> <span class="string">'spyglass_parser'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/configurator'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/logging'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/server'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/lookout'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/master'</span>
+<span class="keyword">require</span> <span class="string">'spyglass/worker'</span>
+
+<span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="constant">Version</span> = <span class="string">'0.1.1'</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/docs/worker.html b/code/spyglass/docs/worker.html
new file mode 100644
index 0000000..a511c99
--- /dev/null
+++ b/code/spyglass/docs/worker.html
@@ -0,0 +1,305 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>worker.rb</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <link rel="stylesheet" media="all" href="docco.css" />
+</head>
+<body>
+ <div id="container">
+ <div id="background"></div>
+
+ <ul id="jump_to">
+ <li>
+ <a class="large" href="javascript:void(0);">Jump To &hellip;</a>
+ <a class="small" href="javascript:void(0);">+</a>
+ <div id="jump_wrapper">
+ <div id="jump_page">
+
+
+ <a class="source" href="configurator.html">
+ configurator.rb
+ </a>
+
+
+ <a class="source" href="logging.html">
+ logging.rb
+ </a>
+
+
+ <a class="source" href="lookout.html">
+ lookout.rb
+ </a>
+
+
+ <a class="source" href="master.html">
+ master.rb
+ </a>
+
+
+ <a class="source" href="server.html">
+ server.rb
+ </a>
+
+
+ <a class="source" href="worker.html">
+ worker.rb
+ </a>
+
+ </div>
+ </li>
+ </ul>
+
+ <ul class="sections">
+
+ <li id="title">
+ <div class="annotation">
+ <h1>worker.rb</h1>
+ </div>
+ </li>
+
+
+
+ <li id="section-1">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-1">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="keyword">require</span> <span class="string">'time'</span>
+<span class="keyword">require</span> <span class="string">'rack/utils'</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-2">
+ <div class="annotation">
+
+ <div class="pilwrap for-h1">
+ <a class="pilcrow" href="#section-2">&#182;</a>
+ </div>
+ <h1>Worker</h1>
+
+ </div>
+
+ </li>
+
+
+ <li id="section-3">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-3">&#182;</a>
+ </div>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">module</span> <span class="title">Spyglass</span></span>
+ <span class="class"><span class="keyword">class</span> <span class="title">Worker</span></span>
+ <span class="keyword">include</span> <span class="constant">Logging</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">initialize</span><span class="params">(socket, app, writable_pipe, connection = <span class="keyword">nil</span>)</span></span>
+ <span class="variable">@socket</span>, <span class="variable">@app</span>, <span class="variable">@writable_pipe</span> = socket, app, writable_pipe
+ <span class="variable">@parser</span> = <span class="constant">Spyglass::HttpParser</span>.new
+
+ handle_connection(connection) <span class="keyword">if</span> connection
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">start</span></span>
+ trap_signals
+
+ loop <span class="keyword">do</span>
+ handle_connection <span class="variable">@socket</span>.accept
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">handle_connection</span><span class="params">(conn)</span></span>
+ verbose <span class="string">"Received connection"</span></pre></div></div>
+
+ </li>
+
+
+ <li id="section-4">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-4">&#182;</a>
+ </div>
+ <p>This notifies our Master that we have received a connection, expiring
+it&#39;s <code>IO.select</code> and preventing it from timing out.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@writable_pipe</span>.write_nonblock(<span class="string">'.'</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-5">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-5">&#182;</a>
+ </div>
+ <p>This clears any state that the http parser has lying around
+from the last connection that was handled.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@parser</span>.reset</pre></div></div>
+
+ </li>
+
+
+ <li id="section-6">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-6">&#182;</a>
+ </div>
+ <p>The Rack spec requires that &#39;rack.input&#39; be encoded as ASCII-8BIT.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> empty_body = <span class="string">''</span>
+ empty_body.encode!(<span class="constant">Encoding::ASCII_8BIT</span>) <span class="keyword">if</span> empty_body.respond_to?(<span class="symbol">:encode!</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-7">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-7">&#182;</a>
+ </div>
+ <p>The Rack spec requires that the env contain certain keys before being
+passed to the app. These are the keys that aren&#39;t provided by each
+incoming request, server-specific stuff.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> env = {
+ <span class="string">'rack.input'</span> =&gt; <span class="constant">StringIO</span>.new(empty_body),
+ <span class="string">'rack.multithread'</span> =&gt; <span class="keyword">false</span>,
+ <span class="string">'rack.multiprocess'</span> =&gt; <span class="keyword">true</span>,
+ <span class="string">'rack.run_once'</span> =&gt; <span class="keyword">false</span>,
+ <span class="string">'rack.errors'</span> =&gt; <span class="constant">STDERR</span>,
+ <span class="string">'rack.version'</span> =&gt; [<span class="number">1</span>, <span class="number">0</span>]
+ }</pre></div></div>
+
+ </li>
+
+
+ <li id="section-8">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-8">&#182;</a>
+ </div>
+ <p>This reads data in from the client connection. We&#39;ll read up to
+10000 bytes at the moment.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> data = conn.readpartial(<span class="number">10000</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-9">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-9">&#182;</a>
+ </div>
+ <p>Here we pass the data and the env into the http parser. It parses
+the raw http request data and updates the env with all of the data
+it can withdraw.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> <span class="variable">@parser</span>.execute(env, data, <span class="number">0</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-10">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-10">&#182;</a>
+ </div>
+ <p>Call the Rack app, goes all the way down the rabbit hole and back again.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> status, headers, body = <span class="variable">@app</span>.call(env)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-11">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-11">&#182;</a>
+ </div>
+ <p>These are the default headers we always include in a response. We
+only speak HTTP 1.1 and we always close the client connection. At
+the monment keepalive is not supported.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> head = <span class="string">"HTTP/1.1 <span class="subst">#{status}</span>\r\n"</span> \
+ <span class="string">"Date: <span class="subst">#{<span class="constant">Time</span>.now.httpdate}</span>\r\n"</span> \
+ <span class="string">"Status: <span class="subst">#{<span class="constant">Rack::Utils::HTTP_STATUS_CODES</span>[status]}</span>\r\n"</span> \
+ <span class="string">"Connection: close\r\n"</span>
+
+ headers.each <span class="keyword">do</span> |k,v|
+ head &lt;&lt; <span class="string">"<span class="subst">#{k}</span>: <span class="subst">#{v}</span>\r\n"</span>
+ <span class="keyword">end</span>
+ conn.write <span class="string">"<span class="subst">#{head}</span>\r\n"</span>
+
+ body.each { |chunk| conn.write chunk }
+ body.close <span class="keyword">if</span> body.respond_to?(<span class="symbol">:close</span>)</pre></div></div>
+
+ </li>
+
+
+ <li id="section-12">
+ <div class="annotation">
+
+ <div class="pilwrap ">
+ <a class="pilcrow" href="#section-12">&#182;</a>
+ </div>
+ <p>Since keepalive is not supported we can close the client connection
+immediately after writing the body.</p>
+
+ </div>
+
+ <div class="content"><div class='highlight'><pre> conn.close
+
+ verbose <span class="string">"Closed connection"</span>
+ <span class="keyword">end</span>
+
+ <span class="function"><span class="keyword">def</span> <span class="title">trap_signals</span></span>
+ trap(<span class="symbol">:QUIT</span>) <span class="keyword">do</span>
+ out <span class="string">"Received QUIT"</span>
+ exit
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+ <span class="keyword">end</span>
+<span class="keyword">end</span></pre></div></div>
+
+ </li>
+
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/code/spyglass/ext/spyglass_parser/common.rl b/code/spyglass/ext/spyglass_parser/common.rl
new file mode 100644
index 0000000..df7afc0
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/common.rl
@@ -0,0 +1,55 @@
+%%{
+
+ machine http_parser_common;
+
+#### HTTP PROTOCOL GRAMMAR
+# line endings
+ CRLF = "\r\n";
+
+# character types
+ CTL = (cntrl | 127);
+ safe = ("$" | "-" | "_" | ".");
+ extra = ("!" | "*" | "'" | "(" | ")" | ",");
+ reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+");
+ sorta_safe = ("\"" | "<" | ">");
+ unsafe = (CTL | " " | "#" | "%" | sorta_safe);
+ national = any -- (alpha | digit | reserved | extra | safe | unsafe);
+ unreserved = (alpha | digit | safe | extra | national);
+ escape = ("%" "u"? xdigit xdigit);
+ uchar = (unreserved | escape | sorta_safe);
+ pchar = (uchar | ":" | "@" | "&" | "=" | "+");
+ tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
+
+# elements
+ token = (ascii -- (CTL | tspecials));
+
+# URI schemes and absolute paths
+ scheme = ( alpha | digit | "+" | "-" | "." )* ;
+ absolute_uri = (scheme ":" (uchar | reserved )*);
+
+ path = ( pchar+ ( "/" pchar* )* ) ;
+ query = ( uchar | reserved )* %query_string ;
+ param = ( pchar | "/" )* ;
+ params = ( param ( ";" param )* ) ;
+ rel_path = ( path? (";" params)? %request_path) ("?" %start_query query)?;
+ absolute_path = ( "/"+ rel_path );
+
+ Request_URI = ( "*" | absolute_uri | absolute_path ) >mark %request_uri;
+ Fragment = ( uchar | reserved )* >mark %fragment;
+ Method = ( upper | digit | safe ){1,20} >mark %request_method;
+
+ http_number = ( digit+ "." digit+ ) ;
+ HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ;
+ Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;
+
+ field_name = ( token -- ":" )+ >start_field %write_field;
+
+ field_value = any* >start_value %write_value;
+
+ message_header = field_name ":" " "* field_value :> CRLF;
+
+ Request = Request_Line ( message_header )* ( CRLF @done );
+
+main := Request;
+
+}%%
diff --git a/code/spyglass/ext/spyglass_parser/ext_help.h b/code/spyglass/ext/spyglass_parser/ext_help.h
new file mode 100644
index 0000000..8b4d754
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/ext_help.h
@@ -0,0 +1,14 @@
+#ifndef ext_help_h
+#define ext_help_h
+
+#define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "NULL found for " # T " when shouldn't be.");
+#define DATA_GET(from,type,name) Data_Get_Struct(from,type,name); RAISE_NOT_NULL(name);
+#define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "Wrong argument type for " # V " required " # T);
+
+#ifdef DEBUG
+#define TRACE() fprintf(stderr, "> %s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__)
+#else
+#define TRACE()
+#endif
+
+#endif
diff --git a/code/spyglass/ext/spyglass_parser/extconf.rb b/code/spyglass/ext/spyglass_parser/extconf.rb
new file mode 100644
index 0000000..44bae83
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/extconf.rb
@@ -0,0 +1,6 @@
+require 'mkmf'
+
+dir_config("spyglass_parser")
+have_library("c", "main")
+
+create_makefile("spyglass_parser")
diff --git a/code/spyglass/ext/spyglass_parser/parser.c b/code/spyglass/ext/spyglass_parser/parser.c
new file mode 100644
index 0000000..53329b0
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/parser.c
@@ -0,0 +1,1249 @@
+
+#line 1 "parser.rl"
+/**
+ * Copyright (c) 2005 Zed A. Shaw
+ * You can redistribute it and/or modify it under the same terms as Ruby.
+ */
+#include "parser.h"
+#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+
+#define LEN(AT, FPC) (FPC - buffer - parser->AT)
+#define MARK(M,FPC) (parser->M = (FPC) - buffer)
+#define PTR_TO(F) (buffer + parser->F)
+
+/** Machine **/
+
+
+#line 81 "parser.rl"
+
+
+/** Data **/
+
+#line 27 "parser.c"
+static const int http_parser_start = 1;
+static const int http_parser_first_final = 58;
+static const int http_parser_error = 0;
+
+static const int http_parser_en_main = 1;
+
+
+#line 85 "parser.rl"
+
+int spyglass_http_parser_init(http_parser *parser) {
+ int cs = 0;
+
+#line 40 "parser.c"
+ {
+ cs = http_parser_start;
+ }
+
+#line 89 "parser.rl"
+ parser->cs = cs;
+ parser->body_start = 0;
+ parser->content_len = 0;
+ parser->mark = 0;
+ parser->nread = 0;
+ parser->field_len = 0;
+ parser->field_start = 0;
+
+ return(1);
+}
+
+
+/** exec **/
+size_t spyglass_http_parser_execute(http_parser *parser, const char *buffer, size_t len, size_t off) {
+ const char *p, *pe;
+ int cs = parser->cs;
+
+ assert(off <= len && "offset past end of buffer");
+
+ p = buffer+off;
+ pe = buffer+len;
+
+ assert(*pe == '\0' && "pointer does not end on NUL");
+ assert(pe - p == len - off && "pointers aren't same distance");
+
+
+
+#line 73 "parser.c"
+ {
+ if ( p == pe )
+ goto _test_eof;
+ switch ( cs )
+ {
+case 1:
+ switch( (*p) ) {
+ case 36: goto tr0;
+ case 95: goto tr0;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto tr0;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto tr0;
+ } else
+ goto tr0;
+ goto st0;
+st0:
+cs = 0;
+ goto _out;
+tr0:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st2;
+st2:
+ if ( ++p == pe )
+ goto _test_eof2;
+case 2:
+#line 104 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st39;
+ case 95: goto st39;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st39;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st39;
+ } else
+ goto st39;
+ goto st0;
+tr2:
+#line 36 "parser.rl"
+ {
+ if (parser->request_method != NULL) {
+ parser->request_method(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st3;
+st3:
+ if ( ++p == pe )
+ goto _test_eof3;
+case 3:
+#line 131 "parser.c"
+ switch( (*p) ) {
+ case 42: goto tr4;
+ case 43: goto tr5;
+ case 47: goto tr6;
+ case 58: goto tr7;
+ }
+ if ( (*p) < 65 ) {
+ if ( 45 <= (*p) && (*p) <= 57 )
+ goto tr5;
+ } else if ( (*p) > 90 ) {
+ if ( 97 <= (*p) && (*p) <= 122 )
+ goto tr5;
+ } else
+ goto tr5;
+ goto st0;
+tr4:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st4;
+st4:
+ if ( ++p == pe )
+ goto _test_eof4;
+case 4:
+#line 155 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr8;
+ case 35: goto tr9;
+ }
+ goto st0;
+tr8:
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+tr31:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+#line 46 "parser.rl"
+ {
+ if (parser->fragment != NULL) {
+ parser->fragment(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+tr34:
+#line 46 "parser.rl"
+ {
+ if (parser->fragment != NULL) {
+ parser->fragment(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+tr44:
+#line 65 "parser.rl"
+ {
+ if (parser->request_path != NULL) {
+ parser->request_path(parser->data, PTR_TO(mark), LEN(mark,p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+tr51:
+#line 52 "parser.rl"
+ {MARK(query_start, p); }
+#line 53 "parser.rl"
+ {
+ if (parser->query_string != NULL) {
+ parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+tr55:
+#line 53 "parser.rl"
+ {
+ if (parser->query_string != NULL) {
+ parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st5;
+st5:
+ if ( ++p == pe )
+ goto _test_eof5;
+case 5:
+#line 235 "parser.c"
+ if ( (*p) == 72 )
+ goto tr10;
+ goto st0;
+tr10:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st6;
+st6:
+ if ( ++p == pe )
+ goto _test_eof6;
+case 6:
+#line 247 "parser.c"
+ if ( (*p) == 84 )
+ goto st7;
+ goto st0;
+st7:
+ if ( ++p == pe )
+ goto _test_eof7;
+case 7:
+ if ( (*p) == 84 )
+ goto st8;
+ goto st0;
+st8:
+ if ( ++p == pe )
+ goto _test_eof8;
+case 8:
+ if ( (*p) == 80 )
+ goto st9;
+ goto st0;
+st9:
+ if ( ++p == pe )
+ goto _test_eof9;
+case 9:
+ if ( (*p) == 47 )
+ goto st10;
+ goto st0;
+st10:
+ if ( ++p == pe )
+ goto _test_eof10;
+case 10:
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st11;
+ goto st0;
+st11:
+ if ( ++p == pe )
+ goto _test_eof11;
+case 11:
+ if ( (*p) == 46 )
+ goto st12;
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st11;
+ goto st0;
+st12:
+ if ( ++p == pe )
+ goto _test_eof12;
+case 12:
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st13;
+ goto st0;
+st13:
+ if ( ++p == pe )
+ goto _test_eof13;
+case 13:
+ if ( (*p) == 13 )
+ goto tr18;
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st13;
+ goto st0;
+tr18:
+#line 59 "parser.rl"
+ {
+ if (parser->http_version != NULL) {
+ parser->http_version(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st14;
+tr26:
+#line 30 "parser.rl"
+ { MARK(mark, p); }
+#line 31 "parser.rl"
+ {
+ if (parser->http_field != NULL) {
+ parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st14;
+tr29:
+#line 31 "parser.rl"
+ {
+ if (parser->http_field != NULL) {
+ parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st14;
+st14:
+ if ( ++p == pe )
+ goto _test_eof14;
+case 14:
+#line 334 "parser.c"
+ if ( (*p) == 10 )
+ goto st15;
+ goto st0;
+st15:
+ if ( ++p == pe )
+ goto _test_eof15;
+case 15:
+ switch( (*p) ) {
+ case 13: goto st16;
+ case 33: goto tr21;
+ case 124: goto tr21;
+ case 126: goto tr21;
+ }
+ if ( (*p) < 45 ) {
+ if ( (*p) > 39 ) {
+ if ( 42 <= (*p) && (*p) <= 43 )
+ goto tr21;
+ } else if ( (*p) >= 35 )
+ goto tr21;
+ } else if ( (*p) > 46 ) {
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto tr21;
+ } else if ( (*p) > 90 ) {
+ if ( 94 <= (*p) && (*p) <= 122 )
+ goto tr21;
+ } else
+ goto tr21;
+ } else
+ goto tr21;
+ goto st0;
+st16:
+ if ( ++p == pe )
+ goto _test_eof16;
+case 16:
+ if ( (*p) == 10 )
+ goto tr22;
+ goto st0;
+tr22:
+#line 71 "parser.rl"
+ {
+ parser->body_start = p - buffer + 1;
+ if (parser->header_done != NULL) {
+ parser->header_done(parser->data, p + 1, pe - p - 1);
+ }
+ {p++; cs = 58; goto _out;}
+ }
+ goto st58;
+st58:
+ if ( ++p == pe )
+ goto _test_eof58;
+case 58:
+#line 387 "parser.c"
+ goto st0;
+tr21:
+#line 25 "parser.rl"
+ { MARK(field_start, p); }
+ goto st17;
+st17:
+ if ( ++p == pe )
+ goto _test_eof17;
+case 17:
+#line 397 "parser.c"
+ switch( (*p) ) {
+ case 33: goto st17;
+ case 58: goto tr24;
+ case 124: goto st17;
+ case 126: goto st17;
+ }
+ if ( (*p) < 45 ) {
+ if ( (*p) > 39 ) {
+ if ( 42 <= (*p) && (*p) <= 43 )
+ goto st17;
+ } else if ( (*p) >= 35 )
+ goto st17;
+ } else if ( (*p) > 46 ) {
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st17;
+ } else if ( (*p) > 90 ) {
+ if ( 94 <= (*p) && (*p) <= 122 )
+ goto st17;
+ } else
+ goto st17;
+ } else
+ goto st17;
+ goto st0;
+tr24:
+#line 26 "parser.rl"
+ {
+ parser->field_len = LEN(field_start, p);
+ }
+ goto st18;
+tr27:
+#line 30 "parser.rl"
+ { MARK(mark, p); }
+ goto st18;
+st18:
+ if ( ++p == pe )
+ goto _test_eof18;
+case 18:
+#line 436 "parser.c"
+ switch( (*p) ) {
+ case 13: goto tr26;
+ case 32: goto tr27;
+ }
+ goto tr25;
+tr25:
+#line 30 "parser.rl"
+ { MARK(mark, p); }
+ goto st19;
+st19:
+ if ( ++p == pe )
+ goto _test_eof19;
+case 19:
+#line 450 "parser.c"
+ if ( (*p) == 13 )
+ goto tr29;
+ goto st19;
+tr9:
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st20;
+tr45:
+#line 65 "parser.rl"
+ {
+ if (parser->request_path != NULL) {
+ parser->request_path(parser->data, PTR_TO(mark), LEN(mark,p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st20;
+tr52:
+#line 52 "parser.rl"
+ {MARK(query_start, p); }
+#line 53 "parser.rl"
+ {
+ if (parser->query_string != NULL) {
+ parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st20;
+tr56:
+#line 53 "parser.rl"
+ {
+ if (parser->query_string != NULL) {
+ parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, p));
+ }
+ }
+#line 41 "parser.rl"
+ {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, p));
+ }
+ }
+ goto st20;
+st20:
+ if ( ++p == pe )
+ goto _test_eof20;
+case 20:
+#line 510 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr31;
+ case 35: goto st0;
+ case 37: goto tr32;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto tr30;
+tr30:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st21;
+st21:
+ if ( ++p == pe )
+ goto _test_eof21;
+case 21:
+#line 528 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr34;
+ case 35: goto st0;
+ case 37: goto st22;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto st21;
+tr32:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st22;
+st22:
+ if ( ++p == pe )
+ goto _test_eof22;
+case 22:
+#line 546 "parser.c"
+ if ( (*p) == 117 )
+ goto st24;
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st23;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st23;
+ } else
+ goto st23;
+ goto st0;
+st23:
+ if ( ++p == pe )
+ goto _test_eof23;
+case 23:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st21;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st21;
+ } else
+ goto st21;
+ goto st0;
+st24:
+ if ( ++p == pe )
+ goto _test_eof24;
+case 24:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st23;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st23;
+ } else
+ goto st23;
+ goto st0;
+tr5:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st25;
+st25:
+ if ( ++p == pe )
+ goto _test_eof25;
+case 25:
+#line 592 "parser.c"
+ switch( (*p) ) {
+ case 43: goto st25;
+ case 58: goto st26;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st25;
+ } else if ( (*p) > 57 ) {
+ if ( (*p) > 90 ) {
+ if ( 97 <= (*p) && (*p) <= 122 )
+ goto st25;
+ } else if ( (*p) >= 65 )
+ goto st25;
+ } else
+ goto st25;
+ goto st0;
+tr7:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st26;
+st26:
+ if ( ++p == pe )
+ goto _test_eof26;
+case 26:
+#line 617 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr8;
+ case 35: goto tr9;
+ case 37: goto st27;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto st26;
+st27:
+ if ( ++p == pe )
+ goto _test_eof27;
+case 27:
+ if ( (*p) == 117 )
+ goto st29;
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st28;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st28;
+ } else
+ goto st28;
+ goto st0;
+st28:
+ if ( ++p == pe )
+ goto _test_eof28;
+case 28:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st26;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st26;
+ } else
+ goto st26;
+ goto st0;
+st29:
+ if ( ++p == pe )
+ goto _test_eof29;
+case 29:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st28;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st28;
+ } else
+ goto st28;
+ goto st0;
+tr6:
+#line 22 "parser.rl"
+ {MARK(mark, p); }
+ goto st30;
+st30:
+ if ( ++p == pe )
+ goto _test_eof30;
+case 30:
+#line 676 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr44;
+ case 35: goto tr45;
+ case 37: goto st31;
+ case 63: goto tr47;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto st30;
+st31:
+ if ( ++p == pe )
+ goto _test_eof31;
+case 31:
+ if ( (*p) == 117 )
+ goto st33;
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st32;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st32;
+ } else
+ goto st32;
+ goto st0;
+st32:
+ if ( ++p == pe )
+ goto _test_eof32;
+case 32:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st30;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st30;
+ } else
+ goto st30;
+ goto st0;
+st33:
+ if ( ++p == pe )
+ goto _test_eof33;
+case 33:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st32;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st32;
+ } else
+ goto st32;
+ goto st0;
+tr47:
+#line 65 "parser.rl"
+ {
+ if (parser->request_path != NULL) {
+ parser->request_path(parser->data, PTR_TO(mark), LEN(mark,p));
+ }
+ }
+ goto st34;
+st34:
+ if ( ++p == pe )
+ goto _test_eof34;
+case 34:
+#line 740 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr51;
+ case 35: goto tr52;
+ case 37: goto tr53;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto tr50;
+tr50:
+#line 52 "parser.rl"
+ {MARK(query_start, p); }
+ goto st35;
+st35:
+ if ( ++p == pe )
+ goto _test_eof35;
+case 35:
+#line 758 "parser.c"
+ switch( (*p) ) {
+ case 32: goto tr55;
+ case 35: goto tr56;
+ case 37: goto st36;
+ case 127: goto st0;
+ }
+ if ( 0 <= (*p) && (*p) <= 31 )
+ goto st0;
+ goto st35;
+tr53:
+#line 52 "parser.rl"
+ {MARK(query_start, p); }
+ goto st36;
+st36:
+ if ( ++p == pe )
+ goto _test_eof36;
+case 36:
+#line 776 "parser.c"
+ if ( (*p) == 117 )
+ goto st38;
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st37;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st37;
+ } else
+ goto st37;
+ goto st0;
+st37:
+ if ( ++p == pe )
+ goto _test_eof37;
+case 37:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st35;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st35;
+ } else
+ goto st35;
+ goto st0;
+st38:
+ if ( ++p == pe )
+ goto _test_eof38;
+case 38:
+ if ( (*p) < 65 ) {
+ if ( 48 <= (*p) && (*p) <= 57 )
+ goto st37;
+ } else if ( (*p) > 70 ) {
+ if ( 97 <= (*p) && (*p) <= 102 )
+ goto st37;
+ } else
+ goto st37;
+ goto st0;
+st39:
+ if ( ++p == pe )
+ goto _test_eof39;
+case 39:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st40;
+ case 95: goto st40;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st40;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st40;
+ } else
+ goto st40;
+ goto st0;
+st40:
+ if ( ++p == pe )
+ goto _test_eof40;
+case 40:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st41;
+ case 95: goto st41;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st41;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st41;
+ } else
+ goto st41;
+ goto st0;
+st41:
+ if ( ++p == pe )
+ goto _test_eof41;
+case 41:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st42;
+ case 95: goto st42;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st42;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st42;
+ } else
+ goto st42;
+ goto st0;
+st42:
+ if ( ++p == pe )
+ goto _test_eof42;
+case 42:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st43;
+ case 95: goto st43;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st43;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st43;
+ } else
+ goto st43;
+ goto st0;
+st43:
+ if ( ++p == pe )
+ goto _test_eof43;
+case 43:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st44;
+ case 95: goto st44;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st44;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st44;
+ } else
+ goto st44;
+ goto st0;
+st44:
+ if ( ++p == pe )
+ goto _test_eof44;
+case 44:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st45;
+ case 95: goto st45;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st45;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st45;
+ } else
+ goto st45;
+ goto st0;
+st45:
+ if ( ++p == pe )
+ goto _test_eof45;
+case 45:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st46;
+ case 95: goto st46;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st46;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st46;
+ } else
+ goto st46;
+ goto st0;
+st46:
+ if ( ++p == pe )
+ goto _test_eof46;
+case 46:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st47;
+ case 95: goto st47;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st47;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st47;
+ } else
+ goto st47;
+ goto st0;
+st47:
+ if ( ++p == pe )
+ goto _test_eof47;
+case 47:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st48;
+ case 95: goto st48;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st48;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st48;
+ } else
+ goto st48;
+ goto st0;
+st48:
+ if ( ++p == pe )
+ goto _test_eof48;
+case 48:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st49;
+ case 95: goto st49;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st49;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st49;
+ } else
+ goto st49;
+ goto st0;
+st49:
+ if ( ++p == pe )
+ goto _test_eof49;
+case 49:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st50;
+ case 95: goto st50;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st50;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st50;
+ } else
+ goto st50;
+ goto st0;
+st50:
+ if ( ++p == pe )
+ goto _test_eof50;
+case 50:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st51;
+ case 95: goto st51;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st51;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st51;
+ } else
+ goto st51;
+ goto st0;
+st51:
+ if ( ++p == pe )
+ goto _test_eof51;
+case 51:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st52;
+ case 95: goto st52;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st52;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st52;
+ } else
+ goto st52;
+ goto st0;
+st52:
+ if ( ++p == pe )
+ goto _test_eof52;
+case 52:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st53;
+ case 95: goto st53;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st53;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st53;
+ } else
+ goto st53;
+ goto st0;
+st53:
+ if ( ++p == pe )
+ goto _test_eof53;
+case 53:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st54;
+ case 95: goto st54;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st54;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st54;
+ } else
+ goto st54;
+ goto st0;
+st54:
+ if ( ++p == pe )
+ goto _test_eof54;
+case 54:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st55;
+ case 95: goto st55;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st55;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st55;
+ } else
+ goto st55;
+ goto st0;
+st55:
+ if ( ++p == pe )
+ goto _test_eof55;
+case 55:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st56;
+ case 95: goto st56;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st56;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st56;
+ } else
+ goto st56;
+ goto st0;
+st56:
+ if ( ++p == pe )
+ goto _test_eof56;
+case 56:
+ switch( (*p) ) {
+ case 32: goto tr2;
+ case 36: goto st57;
+ case 95: goto st57;
+ }
+ if ( (*p) < 48 ) {
+ if ( 45 <= (*p) && (*p) <= 46 )
+ goto st57;
+ } else if ( (*p) > 57 ) {
+ if ( 65 <= (*p) && (*p) <= 90 )
+ goto st57;
+ } else
+ goto st57;
+ goto st0;
+st57:
+ if ( ++p == pe )
+ goto _test_eof57;
+case 57:
+ if ( (*p) == 32 )
+ goto tr2;
+ goto st0;
+ }
+ _test_eof2: cs = 2; goto _test_eof;
+ _test_eof3: cs = 3; goto _test_eof;
+ _test_eof4: cs = 4; goto _test_eof;
+ _test_eof5: cs = 5; goto _test_eof;
+ _test_eof6: cs = 6; goto _test_eof;
+ _test_eof7: cs = 7; goto _test_eof;
+ _test_eof8: cs = 8; goto _test_eof;
+ _test_eof9: cs = 9; goto _test_eof;
+ _test_eof10: cs = 10; goto _test_eof;
+ _test_eof11: cs = 11; goto _test_eof;
+ _test_eof12: cs = 12; goto _test_eof;
+ _test_eof13: cs = 13; goto _test_eof;
+ _test_eof14: cs = 14; goto _test_eof;
+ _test_eof15: cs = 15; goto _test_eof;
+ _test_eof16: cs = 16; goto _test_eof;
+ _test_eof58: cs = 58; goto _test_eof;
+ _test_eof17: cs = 17; goto _test_eof;
+ _test_eof18: cs = 18; goto _test_eof;
+ _test_eof19: cs = 19; goto _test_eof;
+ _test_eof20: cs = 20; goto _test_eof;
+ _test_eof21: cs = 21; goto _test_eof;
+ _test_eof22: cs = 22; goto _test_eof;
+ _test_eof23: cs = 23; goto _test_eof;
+ _test_eof24: cs = 24; goto _test_eof;
+ _test_eof25: cs = 25; goto _test_eof;
+ _test_eof26: cs = 26; goto _test_eof;
+ _test_eof27: cs = 27; goto _test_eof;
+ _test_eof28: cs = 28; goto _test_eof;
+ _test_eof29: cs = 29; goto _test_eof;
+ _test_eof30: cs = 30; goto _test_eof;
+ _test_eof31: cs = 31; goto _test_eof;
+ _test_eof32: cs = 32; goto _test_eof;
+ _test_eof33: cs = 33; goto _test_eof;
+ _test_eof34: cs = 34; goto _test_eof;
+ _test_eof35: cs = 35; goto _test_eof;
+ _test_eof36: cs = 36; goto _test_eof;
+ _test_eof37: cs = 37; goto _test_eof;
+ _test_eof38: cs = 38; goto _test_eof;
+ _test_eof39: cs = 39; goto _test_eof;
+ _test_eof40: cs = 40; goto _test_eof;
+ _test_eof41: cs = 41; goto _test_eof;
+ _test_eof42: cs = 42; goto _test_eof;
+ _test_eof43: cs = 43; goto _test_eof;
+ _test_eof44: cs = 44; goto _test_eof;
+ _test_eof45: cs = 45; goto _test_eof;
+ _test_eof46: cs = 46; goto _test_eof;
+ _test_eof47: cs = 47; goto _test_eof;
+ _test_eof48: cs = 48; goto _test_eof;
+ _test_eof49: cs = 49; goto _test_eof;
+ _test_eof50: cs = 50; goto _test_eof;
+ _test_eof51: cs = 51; goto _test_eof;
+ _test_eof52: cs = 52; goto _test_eof;
+ _test_eof53: cs = 53; goto _test_eof;
+ _test_eof54: cs = 54; goto _test_eof;
+ _test_eof55: cs = 55; goto _test_eof;
+ _test_eof56: cs = 56; goto _test_eof;
+ _test_eof57: cs = 57; goto _test_eof;
+
+ _test_eof: {}
+ _out: {}
+ }
+
+#line 116 "parser.rl"
+
+ parser->cs = cs;
+ parser->nread += p - (buffer + off);
+
+ assert(p <= pe && "buffer overflow after parsing execute");
+ assert(parser->nread <= len && "nread longer than length");
+ assert(parser->body_start <= len && "body starts after buffer end");
+ assert(parser->mark < len && "mark is after buffer end");
+ assert(parser->field_len <= len && "field has length longer than whole buffer");
+ assert(parser->field_start < len && "field starts after buffer end");
+
+ if(parser->body_start) {
+ /* final \r\n combo encountered so stop right here */
+ parser->nread++;
+ }
+
+ return(parser->nread);
+}
+
+int spyglass_http_parser_finish(http_parser *parser)
+{
+ int cs = parser->cs;
+
+
+ parser->cs = cs;
+
+ if (spyglass_http_parser_has_error(parser) ) {
+ return -1;
+ } else if (spyglass_http_parser_is_finished(parser) ) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+int spyglass_http_parser_has_error(http_parser *parser) {
+ return parser->cs == http_parser_error;
+}
+
+int spyglass_http_parser_is_finished(http_parser *parser) {
+ return parser->cs == http_parser_first_final;
+}
diff --git a/code/spyglass/ext/spyglass_parser/parser.h b/code/spyglass/ext/spyglass_parser/parser.h
new file mode 100644
index 0000000..8d074ba
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/parser.h
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2005 Zed A. Shaw
+ * You can redistribute it and/or modify it under the same terms as Ruby.
+ */
+
+#ifndef http11_parser_h
+#define http11_parser_h
+
+#include <sys/types.h>
+
+#if defined(_WIN32)
+#include <stddef.h>
+#endif
+
+typedef void (*element_cb)(void *data, const char *at, size_t length);
+typedef void (*field_cb)(void *data, const char *field, size_t flen, const char *value, size_t vlen);
+
+typedef struct http_parser {
+ int cs;
+ size_t body_start;
+ int content_len;
+ size_t nread;
+ size_t mark;
+ size_t field_start;
+ size_t field_len;
+ size_t query_start;
+
+ void *data;
+
+ field_cb http_field;
+ element_cb request_method;
+ element_cb request_uri;
+ element_cb fragment;
+ element_cb request_path;
+ element_cb query_string;
+ element_cb http_version;
+ element_cb header_done;
+
+} http_parser;
+
+int http_parser_init(http_parser *parser);
+int http_parser_finish(http_parser *parser);
+size_t http_parser_execute(http_parser *parser, const char *data, size_t len, size_t off);
+int http_parser_has_error(http_parser *parser);
+int http_parser_is_finished(http_parser *parser);
+
+#define http_parser_nread(parser) (parser)->nread
+
+#endif
diff --git a/code/spyglass/ext/spyglass_parser/parser.rl b/code/spyglass/ext/spyglass_parser/parser.rl
new file mode 100644
index 0000000..684596a
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/parser.rl
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2005 Zed A. Shaw
+ * You can redistribute it and/or modify it under the same terms as Ruby.
+ */
+#include "parser.h"
+#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+
+#define LEN(AT, FPC) (FPC - buffer - parser->AT)
+#define MARK(M,FPC) (parser->M = (FPC) - buffer)
+#define PTR_TO(F) (buffer + parser->F)
+
+/** Machine **/
+
+%%{
+
+ machine http_parser;
+
+ action mark {MARK(mark, fpc); }
+
+
+ action start_field { MARK(field_start, fpc); }
+ action write_field {
+ parser->field_len = LEN(field_start, fpc);
+ }
+
+ action start_value { MARK(mark, fpc); }
+ action write_value {
+ if (parser->http_field != NULL) {
+ parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
+ }
+ }
+ action request_method {
+ if (parser->request_method != NULL) {
+ parser->request_method(parser->data, PTR_TO(mark), LEN(mark, fpc));
+ }
+ }
+ action request_uri {
+ if (parser->request_uri != NULL) {
+ parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, fpc));
+ }
+ }
+ action fragment {
+ if (parser->fragment != NULL) {
+ parser->fragment(parser->data, PTR_TO(mark), LEN(mark, fpc));
+ }
+ }
+
+ action start_query {MARK(query_start, fpc); }
+ action query_string {
+ if (parser->query_string != NULL) {
+ parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, fpc));
+ }
+ }
+
+ action http_version {
+ if (parser->http_version != NULL) {
+ parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
+ }
+ }
+
+ action request_path {
+ if (parser->request_path != NULL) {
+ parser->request_path(parser->data, PTR_TO(mark), LEN(mark,fpc));
+ }
+ }
+
+ action done {
+ parser->body_start = fpc - buffer + 1;
+ if (parser->header_done != NULL) {
+ parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
+ }
+ fbreak;
+ }
+
+ include http_parser_common "common.rl";
+
+}%%
+
+/** Data **/
+%% write data;
+
+int spyglass_http_parser_init(http_parser *parser) {
+ int cs = 0;
+ %% write init;
+ parser->cs = cs;
+ parser->body_start = 0;
+ parser->content_len = 0;
+ parser->mark = 0;
+ parser->nread = 0;
+ parser->field_len = 0;
+ parser->field_start = 0;
+
+ return(1);
+}
+
+
+/** exec **/
+size_t spyglass_http_parser_execute(http_parser *parser, const char *buffer, size_t len, size_t off) {
+ const char *p, *pe;
+ int cs = parser->cs;
+
+ assert(off <= len && "offset past end of buffer");
+
+ p = buffer+off;
+ pe = buffer+len;
+
+ assert(*pe == '\0' && "pointer does not end on NUL");
+ assert(pe - p == len - off && "pointers aren't same distance");
+
+
+ %% write exec;
+
+ parser->cs = cs;
+ parser->nread += p - (buffer + off);
+
+ assert(p <= pe && "buffer overflow after parsing execute");
+ assert(parser->nread <= len && "nread longer than length");
+ assert(parser->body_start <= len && "body starts after buffer end");
+ assert(parser->mark < len && "mark is after buffer end");
+ assert(parser->field_len <= len && "field has length longer than whole buffer");
+ assert(parser->field_start < len && "field starts after buffer end");
+
+ if(parser->body_start) {
+ /* final \r\n combo encountered so stop right here */
+ parser->nread++;
+ }
+
+ return(parser->nread);
+}
+
+int spyglass_http_parser_finish(http_parser *parser)
+{
+ int cs = parser->cs;
+
+
+ parser->cs = cs;
+
+ if (spyglass_http_parser_has_error(parser) ) {
+ return -1;
+ } else if (spyglass_http_parser_is_finished(parser) ) {
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+int spyglass_http_parser_has_error(http_parser *parser) {
+ return parser->cs == http_parser_error;
+}
+
+int spyglass_http_parser_is_finished(http_parser *parser) {
+ return parser->cs == http_parser_first_final;
+}
diff --git a/code/spyglass/ext/spyglass_parser/spyglass.c b/code/spyglass/ext/spyglass_parser/spyglass.c
new file mode 100644
index 0000000..aa91520
--- /dev/null
+++ b/code/spyglass/ext/spyglass_parser/spyglass.c
@@ -0,0 +1,436 @@
+/**
+ * Thin Parser adpated to Spyglass.
+ *
+ * Orignal version Copyright (c) 2005 Zed A. Shaw
+ * You can redistribute it and/or modify it under the same terms as Ruby.
+ */
+#include "ruby.h"
+#include "ext_help.h"
+#include <assert.h>
+#include <string.h>
+#include "parser.h"
+#include <ctype.h>
+
+static VALUE mSpyglass;
+static VALUE cHttpParser;
+static VALUE eHttpParserError;
+
+static VALUE global_empty;
+static VALUE global_http_prefix;
+static VALUE global_request_method;
+static VALUE global_request_uri;
+static VALUE global_fragment;
+static VALUE global_query_string;
+static VALUE global_http_version;
+static VALUE global_content_length;
+static VALUE global_http_content_length;
+static VALUE global_request_path;
+static VALUE global_content_type;
+static VALUE global_http_content_type;
+static VALUE global_gateway_interface;
+static VALUE global_gateway_interface_value;
+static VALUE global_server_name;
+static VALUE global_server_port;
+static VALUE global_server_protocol;
+static VALUE global_server_protocol_value;
+static VALUE global_http_host;
+static VALUE global_port_80;
+static VALUE global_http_body;
+static VALUE global_url_scheme;
+static VALUE global_url_scheme_value;
+static VALUE global_script_name;
+static VALUE global_path_info;
+
+#define TRIE_INCREASE 30
+
+/** Defines common length and error messages for input length validation. */
+#define DEF_MAX_LENGTH(N,length) const size_t MAX_##N##_LENGTH = length; const char *MAX_##N##_LENGTH_ERR = "HTTP element " # N " is longer than the " # length " allowed length."
+
+/** Validates the max length of given input and throws an HttpParserError exception if over. */
+#define VALIDATE_MAX_LENGTH(len, N) if(len > MAX_##N##_LENGTH) { rb_raise(eHttpParserError, MAX_##N##_LENGTH_ERR); }
+
+/** Defines global strings in the init method. */
+#define DEF_GLOBAL(N, val) global_##N = rb_obj_freeze(rb_str_new2(val)); rb_global_variable(&global_##N)
+
+/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_PTR */
+#ifndef RSTRING_PTR
+#define RSTRING_PTR(s) (RSTRING(s)->ptr)
+#endif
+
+/* for compatibility with Ruby 1.8.5, which doesn't declare RSTRING_LEN */
+#ifndef RSTRING_LEN
+#define RSTRING_LEN(s) (RSTRING(s)->len)
+#endif
+
+/* Defines the maximum allowed lengths for various input elements.*/
+DEF_MAX_LENGTH(FIELD_NAME, 256);
+DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
+DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
+DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
+DEF_MAX_LENGTH(REQUEST_PATH, 1024);
+DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
+DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
+
+
+static void http_field(void *data, const char *field, size_t flen, const char *value, size_t vlen)
+{
+ char *ch, *end;
+ VALUE req = (VALUE)data;
+ VALUE v = Qnil;
+ VALUE f = Qnil;
+
+ VALIDATE_MAX_LENGTH(flen, FIELD_NAME);
+ VALIDATE_MAX_LENGTH(vlen, FIELD_VALUE);
+
+ v = rb_str_new(value, vlen);
+ f = rb_str_dup(global_http_prefix);
+ f = rb_str_buf_cat(f, field, flen);
+
+ for(ch = RSTRING_PTR(f) + RSTRING_LEN(global_http_prefix), end = RSTRING_PTR(f) + RSTRING_LEN(f); ch < end; ch++) {
+ if(*ch == '-') {
+ *ch = '_';
+ } else {
+ *ch = toupper(*ch);
+ }
+ }
+
+ rb_hash_aset(req, f, v);
+}
+
+static void request_method(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = Qnil;
+
+ val = rb_str_new(at, length);
+ rb_hash_aset(req, global_request_method, val);
+}
+
+static void request_uri(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = Qnil;
+
+ VALIDATE_MAX_LENGTH(length, REQUEST_URI);
+
+ val = rb_str_new(at, length);
+ rb_hash_aset(req, global_request_uri, val);
+}
+
+static void fragment(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = Qnil;
+
+ VALIDATE_MAX_LENGTH(length, FRAGMENT);
+
+ val = rb_str_new(at, length);
+ rb_hash_aset(req, global_fragment, val);
+}
+
+static void request_path(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = Qnil;
+
+ VALIDATE_MAX_LENGTH(length, REQUEST_PATH);
+
+ val = rb_str_new(at, length);
+ rb_hash_aset(req, global_request_path, val);
+ rb_hash_aset(req, global_path_info, val);
+}
+
+static void query_string(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = Qnil;
+
+ VALIDATE_MAX_LENGTH(length, QUERY_STRING);
+
+ val = rb_str_new(at, length);
+ rb_hash_aset(req, global_query_string, val);
+}
+
+static void http_version(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE val = rb_str_new(at, length);
+ rb_hash_aset(req, global_http_version, val);
+}
+
+/** Finalizes the request header to have a bunch of stuff that's
+ needed. */
+
+static void header_done(void *data, const char *at, size_t length)
+{
+ VALUE req = (VALUE)data;
+ VALUE temp = Qnil;
+ VALUE ctype = Qnil;
+ VALUE clen = Qnil;
+ VALUE body = Qnil;
+ char *colon = NULL;
+
+ clen = rb_hash_aref(req, global_http_content_length);
+ if(clen != Qnil) {
+ rb_hash_aset(req, global_content_length, clen);
+ rb_hash_delete(req, global_http_content_length);
+ }
+
+ ctype = rb_hash_aref(req, global_http_content_type);
+ if(ctype != Qnil) {
+ rb_hash_aset(req, global_content_type, ctype);
+ rb_hash_delete(req, global_http_content_type);
+ }
+
+ rb_hash_aset(req, global_gateway_interface, global_gateway_interface_value);
+ if((temp = rb_hash_aref(req, global_http_host)) != Qnil) {
+ /* ruby better close strings off with a '\0' dammit */
+ colon = strchr(RSTRING_PTR(temp), ':');
+ if(colon != NULL) {
+ rb_hash_aset(req, global_server_name, rb_str_substr(temp, 0, colon - RSTRING_PTR(temp)));
+ rb_hash_aset(req, global_server_port,
+ rb_str_substr(temp, colon - RSTRING_PTR(temp)+1,
+ RSTRING_LEN(temp)));
+ } else {
+ rb_hash_aset(req, global_server_name, temp);
+ rb_hash_aset(req, global_server_port, global_port_80);
+ }
+ }
+
+ /* grab the initial body and stuff it into the hash */
+ if(length > 0) {
+ body = rb_hash_aref(req, global_http_body);
+ rb_io_write(body, rb_str_new(at, length));
+ }
+
+ /* according to Rack specs, query string must be empty string if none */
+ if (rb_hash_aref(req, global_query_string) == Qnil) {
+ rb_hash_aset(req, global_query_string, global_empty);
+ }
+ if (rb_hash_aref(req, global_path_info) == Qnil) {
+ rb_hash_aset(req, global_path_info, global_empty);
+ }
+
+ /* set some constants */
+ rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
+ rb_hash_aset(req, global_url_scheme, global_url_scheme_value);
+ rb_hash_aset(req, global_script_name, global_empty);
+}
+
+
+void Spyglass_HttpParser_free(void *data) {
+ TRACE();
+
+ if(data) {
+ free(data);
+ }
+}
+
+
+VALUE Spyglass_HttpParser_alloc(VALUE klass)
+{
+ VALUE obj;
+ http_parser *hp = ALLOC_N(http_parser, 1);
+ TRACE();
+ hp->http_field = http_field;
+ hp->request_method = request_method;
+ hp->request_uri = request_uri;
+ hp->fragment = fragment;
+ hp->request_path = request_path;
+ hp->query_string = query_string;
+ hp->http_version = http_version;
+ hp->header_done = header_done;
+ spyglass_http_parser_init(hp);
+
+ obj = Data_Wrap_Struct(klass, NULL, Spyglass_HttpParser_free, hp);
+
+ return obj;
+}
+
+
+/**
+ * call-seq:
+ * parser.new -> parser
+ *
+ * Creates a new parser.
+ */
+VALUE Spyglass_HttpParser_init(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+ spyglass_http_parser_init(http);
+
+ return self;
+}
+
+
+/**
+ * call-seq:
+ * parser.reset -> nil
+ *
+ * Resets the parser to it's initial state so that you can reuse it
+ * rather than making new ones.
+ */
+VALUE Spyglass_HttpParser_reset(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+ spyglass_http_parser_init(http);
+
+ return Qnil;
+}
+
+
+/**
+ * call-seq:
+ * parser.finish -> true/false
+ *
+ * Finishes a parser early which could put in a "good" or bad state.
+ * You should call reset after finish it or bad things will happen.
+ */
+VALUE Spyglass_HttpParser_finish(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+ spyglass_http_parser_finish(http);
+
+ return spyglass_http_parser_is_finished(http) ? Qtrue : Qfalse;
+}
+
+
+/**
+ * call-seq:
+ * parser.execute(req_hash, data, start) -> Integer
+ *
+ * Takes a Hash and a String of data, parses the String of data filling in the Hash
+ * returning an Integer to indicate how much of the data has been read. No matter
+ * what the return value, you should call HttpParser#finished? and HttpParser#error?
+ * to figure out if it's done parsing or there was an error.
+ *
+ * This function now throws an exception when there is a parsing error. This makes
+ * the logic for working with the parser much easier. You can still test for an
+ * error, but now you need to wrap the parser with an exception handling block.
+ *
+ * The third argument allows for parsing a partial request and then continuing
+ * the parsing from that position. It needs all of the original data as well
+ * so you have to append to the data buffer as you read.
+ */
+VALUE Spyglass_HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
+{
+ http_parser *http = NULL;
+ int from = 0;
+ char *dptr = NULL;
+ long dlen = 0;
+
+ DATA_GET(self, http_parser, http);
+
+ from = FIX2INT(start);
+ dptr = RSTRING_PTR(data);
+ dlen = RSTRING_LEN(data);
+
+ if(from >= dlen) {
+ rb_raise(eHttpParserError, "Requested start is after data buffer end.");
+ } else {
+ http->data = (void *)req_hash;
+ spyglass_http_parser_execute(http, dptr, dlen, from);
+
+ VALIDATE_MAX_LENGTH(http_parser_nread(http), HEADER);
+
+ if(spyglass_http_parser_has_error(http)) {
+ rb_raise(eHttpParserError, "Invalid HTTP format, parsing fails.");
+ } else {
+ return INT2FIX(http_parser_nread(http));
+ }
+ }
+}
+
+
+
+/**
+ * call-seq:
+ * parser.error? -> true/false
+ *
+ * Tells you whether the parser is in an error state.
+ */
+VALUE Spyglass_HttpParser_has_error(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+
+ return spyglass_http_parser_has_error(http) ? Qtrue : Qfalse;
+}
+
+
+/**
+ * call-seq:
+ * parser.finished? -> true/false
+ *
+ * Tells you whether the parser is finished or not and in a good state.
+ */
+VALUE Spyglass_HttpParser_is_finished(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+
+ return spyglass_http_parser_is_finished(http) ? Qtrue : Qfalse;
+}
+
+
+/**
+ * call-seq:
+ * parser.nread -> Integer
+ *
+ * Returns the amount of data processed so far during this processing cycle. It is
+ * set to 0 on initialize or reset calls and is incremented each time execute is called.
+ */
+VALUE Spyglass_HttpParser_nread(VALUE self)
+{
+ http_parser *http = NULL;
+ DATA_GET(self, http_parser, http);
+
+ return INT2FIX(http->nread);
+}
+
+void Init_spyglass_parser()
+{
+
+ mSpyglass = rb_define_module("Spyglass");
+
+ DEF_GLOBAL(empty, "");
+ DEF_GLOBAL(http_prefix, "HTTP_");
+ DEF_GLOBAL(request_method, "REQUEST_METHOD");
+ DEF_GLOBAL(request_uri, "REQUEST_URI");
+ DEF_GLOBAL(fragment, "FRAGMENT");
+ DEF_GLOBAL(query_string, "QUERY_STRING");
+ DEF_GLOBAL(http_version, "HTTP_VERSION");
+ DEF_GLOBAL(request_path, "REQUEST_PATH");
+ DEF_GLOBAL(content_length, "CONTENT_LENGTH");
+ DEF_GLOBAL(http_content_length, "HTTP_CONTENT_LENGTH");
+ DEF_GLOBAL(content_type, "CONTENT_TYPE");
+ DEF_GLOBAL(http_content_type, "HTTP_CONTENT_TYPE");
+ DEF_GLOBAL(gateway_interface, "GATEWAY_INTERFACE");
+ DEF_GLOBAL(gateway_interface_value, "CGI/1.2");
+ DEF_GLOBAL(server_name, "SERVER_NAME");
+ DEF_GLOBAL(server_port, "SERVER_PORT");
+ DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL");
+ DEF_GLOBAL(server_protocol_value, "HTTP/1.1");
+ DEF_GLOBAL(http_host, "HTTP_HOST");
+ DEF_GLOBAL(port_80, "80");
+ DEF_GLOBAL(http_body, "rack.input");
+ DEF_GLOBAL(url_scheme, "rack.url_scheme");
+ DEF_GLOBAL(url_scheme_value, "http");
+ DEF_GLOBAL(script_name, "SCRIPT_NAME");
+ DEF_GLOBAL(path_info, "PATH_INFO");
+
+ eHttpParserError = rb_define_class_under(mSpyglass, "InvalidRequest", rb_eIOError);
+
+ cHttpParser = rb_define_class_under(mSpyglass, "HttpParser", rb_cObject);
+ rb_define_alloc_func(cHttpParser, Spyglass_HttpParser_alloc);
+ rb_define_method(cHttpParser, "initialize", Spyglass_HttpParser_init,0);
+ rb_define_method(cHttpParser, "reset", Spyglass_HttpParser_reset,0);
+ rb_define_method(cHttpParser, "finish", Spyglass_HttpParser_finish,0);
+ rb_define_method(cHttpParser, "execute", Spyglass_HttpParser_execute,3);
+ rb_define_method(cHttpParser, "error?", Spyglass_HttpParser_has_error,0);
+ rb_define_method(cHttpParser, "finished?", Spyglass_HttpParser_is_finished,0);
+ rb_define_method(cHttpParser, "nread", Spyglass_HttpParser_nread,0);
+}
diff --git a/code/spyglass/lib/spyglass.rb b/code/spyglass/lib/spyglass.rb
new file mode 100644
index 0000000..0e3f9ba
--- /dev/null
+++ b/code/spyglass/lib/spyglass.rb
@@ -0,0 +1,53 @@
+# Spyglass
+# ========
+#
+# This is Spyglass, a Rack web server that rides on Unix designed to be simple and teach
+# others about Unix programming.
+#
+# It's namesake comes from the fact that when it boots up it's nothing more than a lone socket
+# keeping a lookout for incoming connections.
+#
+# When a connection comes in it spins up a Master
+# process which preforks some workers to actually handle http requests. If the Master process is
+# left idle long enough it will shut itself (and it's workers) down and go back to just a lone
+# listening socket, on the lookout for incoming connections.
+#
+# Components
+# ==========
+#
+# * [Server](server.html) gets the ball rolling.
+# The role of Server is pretty minimal. It opens the initial listening TCP socket,
+# then passes that socket onto the Lookout. The Lookout will actually handle reading
+# from the socket.
+#
+# * [Lookout](lookout.html) keeps a watch and notifies others when a connection
+# comes in.
+# The Lookout is a pretty 'dumb' object. All that it does is listen for incoming
+# connections on the socket it's given. Once it receives a connection it does a fork(2)
+# and invokes a Master process. The Master process actually handles the connection.
+#
+# * [Master](master.html) loads the application and babysits worker processes
+# that actually talk to clients.
+# The role of the Master class is to create and babysit worker processes
+# that will actually handle web requests. The Master itself doesn't know
+# anything about http, etc. it just knows how to manage processes.
+#
+# * [Worker](worker.html) parses HTTP, calls the app, and writes back to the client.
+require 'singleton'
+require 'socket'
+require 'stringio'
+
+require 'rack/server'
+require 'rack/builder'
+
+require 'spyglass_parser'
+require 'spyglass/configurator'
+require 'spyglass/logging'
+require 'spyglass/server'
+require 'spyglass/lookout'
+require 'spyglass/master'
+require 'spyglass/worker'
+
+module Spyglass
+ Version = '0.1.1'
+end
diff --git a/code/spyglass/lib/spyglass/configurator.rb b/code/spyglass/lib/spyglass/configurator.rb
new file mode 100644
index 0000000..6c1e0a7
--- /dev/null
+++ b/code/spyglass/lib/spyglass/configurator.rb
@@ -0,0 +1,31 @@
+module Spyglass
+ class Configurator
+ # A hash of key => default
+ OPTIONS = {
+ :port => 4222,
+ :host => '0.0.0.0',
+ :workers => 2,
+ :timeout => 30,
+ :config_ru_path => 'config.ru',
+ :verbose => false,
+ :vverbose => false
+ }
+
+ class << self
+ OPTIONS.each do |key, default|
+ # attr_writer key
+
+ define_method(key) do |*args|
+ arg = args.shift
+ if arg
+ instance_variable_set("@#{key}", arg)
+ else
+ instance_variable_get("@#{key}") || default
+ end
+ end
+ end
+ end
+ end
+
+ Config = Configurator
+end
diff --git a/code/spyglass/lib/spyglass/logging.rb b/code/spyglass/lib/spyglass/logging.rb
new file mode 100644
index 0000000..0819ea2
--- /dev/null
+++ b/code/spyglass/lib/spyglass/logging.rb
@@ -0,0 +1,25 @@
+module Spyglass
+ module Logging
+ def out(message)
+ $stdout.puts preamble + message
+ end
+
+ def err(message)
+ $stderr.puts preamble + message
+ end
+
+ def verbose(message)
+ return unless Config.verbose
+ out(message)
+ end
+
+ def vverbose(message)
+ return unless Config.vverbose
+ out(message)
+ end
+
+ def preamble
+ "[#{Process.pid}] [#{self.class.name}] "
+ end
+ end
+end
diff --git a/code/spyglass/lib/spyglass/lookout.rb b/code/spyglass/lib/spyglass/lookout.rb
new file mode 100644
index 0000000..a24e34a
--- /dev/null
+++ b/code/spyglass/lib/spyglass/lookout.rb
@@ -0,0 +1,83 @@
+module Spyglass
+ class Lookout
+ include Singleton, Logging
+
+ # This method is the main entry point for the Lookout class. It takes
+ # a socket object.
+ def start(socket)
+ trap_signals
+
+ # The Lookout doesn't know anything about the app itself, so there's
+ # no app related setup to do here.
+ loop do
+ # Accepts a new connection on our socket. This class won't actually
+ # do anything interesting with this connection, it will pass it down
+ # to the `Master` class created below to do the actual request handling.
+ conn = socket.accept
+ out "Received incoming connection"
+
+ # In this block the Lookout forks a new process and invokes a Master,
+ # passing along the socket it received and the connection it accepted
+ # above.
+ @master_pid = fork do
+ master = Master.new(conn, socket)
+ master.start
+ end
+
+ # The Lookout can now close its handle on the client socket. This doesn't
+ # translate to the socket being closed on the clients end because the
+ # forked Master process also has a handle on the same socket. Since this
+ # handle is now cleaned up it's up to the Master process to ensure that
+ # its handle gets cleaned up.
+ conn.close
+ # Now this process blocks until the Master process exits. The Master process
+ # will only exit once traffic is slow enough that it has reached its timeout
+ # without receiving any new connections.
+ Process.waitpid(@master_pid)
+
+ # The interaction of fork(2)/waitpid(2) above deserve some explanation.
+
+ # ### Why fork(2)? Why not just spin up the Master?
+ # The whole point of the Lookout process is to be very lean. The only resource
+ # that it initializes is the listening socket for the server. It doesn't load
+ # any of your application into memory, so its resource footprint is very small.
+
+ # The reason that it does a fork(2) before invoking the Master is because once
+ # the Master times out we want the Lookout process to remain lean when accepting
+ # the next connection.
+
+ # If it were to load the application code without forking
+ # then there would be no (simple) way for it to later unload the application code.
+
+ # By doing a fork(2), then waiting for the Master process to exit, that guarantees
+ # that all resources (notably memory usage) that were in use by the Master process
+ # will be reclaimed by the kernel.
+
+ # ### Who knows what your app will demand!
+ # While handling requests your app may require lots of memory. Containing this in a
+ # child process, and exiting that process, is the easiest way to ensure that memory
+ # bloat isn't shared with our simple parent process.
+
+ # This allows our Lookout process will to go back around
+ # the loop with nothing more than it started with, just a listening socket.
+
+ # The fork(2)/waitpid(2) approach requires little code to implement, and pushes
+ # responsibility down to the kernel to track resource usage and nicely clean up
+ # the Master process when it's finished.
+ end
+ end
+
+ def trap_signals
+ [:INT, :QUIT].each do |sig|
+ trap(sig) {
+ begin
+ Process.kill(sig, @master_pid) if @master_pid
+ rescue Errno::ESRCH
+ end
+ exit
+ }
+ end
+ end
+ end
+end
+
diff --git a/code/spyglass/lib/spyglass/master.rb b/code/spyglass/lib/spyglass/master.rb
new file mode 100644
index 0000000..84341f1
--- /dev/null
+++ b/code/spyglass/lib/spyglass/master.rb
@@ -0,0 +1,102 @@
+module Spyglass
+ class Master
+ include Logging
+
+ def initialize(connection, socket)
+ @connection, @socket = connection, socket
+ @worker_pids = []
+
+ # The Master shares this pipe with each of its worker processes. It
+ # passes the writable end down to each spawned worker while it listens
+ # on the readable end. Each worker will write to the pipe each time
+ # it accepts a new connection. If The Master doesn't get anything on
+ # the pipe before `Config.timeout` elapses then it kills its workers
+ # and exits.
+ @readable_pipe, @writable_pipe = IO.pipe
+ end
+
+ # This method starts the Master. It enters an infinite loop where it creates
+ # processes to handle web requests and ensures that they stay active. It takes
+ # a connection as an argument from the Lookout instance. A Master will only
+ # be started when a connection is received by the Lookout.
+ def start
+ trap_signals
+
+ load_app
+ out "Loaded the app"
+
+ # The first worker we spawn has to handle the connection that was already
+ # passed to us.
+ spawn_worker(@connection)
+ # The Master can now close its handle on the client socket since the
+ # forked worker also got a handle on the same socket. Since this one
+ # is now closed it's up to the Worker process to close its handle when
+ # it's done. At that point the client connection will perceive that
+ # it's been closed on their end.
+ @connection.close
+
+ # We spawn the rest of the workers.
+ (Config.workers - 1).times { spawn_worker }
+ out "Spawned #{Config.workers} workers. Babysitting now..."
+
+ loop do
+ if timed_out?(IO.select([@readable_pipe], nil, nil, Config.timeout))
+ out "Timed out after #{Config.timeout} s. Exiting."
+
+ kill_workers(:QUIT)
+ exit
+ else
+ # Clear the data on the pipe so it doesn't appear to be readable
+ # next time around the loop.
+ @readable_pipe.read_nonblock 1
+ end
+ end
+ end
+
+ def timed_out?(select_result)
+ !select_result
+ end
+
+ def spawn_worker(connection = nil)
+ @worker_pids << fork { Worker.new(@socket, @app, @writable_pipe, connection).start }
+ end
+
+ def trap_signals
+ # The QUIT signal triggers a graceful shutdown. The master shuts down
+ # immediately and lets each worker finish the request they are currently
+ # processing.
+ trap(:QUIT) do
+ verbose "Received QUIT"
+
+ kill_workers(:QUIT)
+ exit
+ end
+
+ trap(:CHLD) do
+ dead_worker = Process.wait
+ @worker_pids.delete(dead_worker)
+
+ @worker_pids.each do |wpid|
+ begin
+ dead_worker = Process.waitpid(wpid, Process::WNOHANG)
+ @worker_pids.delete(dead_worker)
+ rescue Errno::ECHILD
+ end
+ end
+
+ spawn_worker
+ end
+ end
+
+ def kill_workers(sig)
+ @worker_pids.each do |wpid|
+ Process.kill(sig, wpid)
+ end
+ end
+
+ def load_app
+ @app, options = Rack::Builder.parse_file(Config.config_ru_path)
+ end
+ end
+end
+
diff --git a/code/spyglass/lib/spyglass/server.rb b/code/spyglass/lib/spyglass/server.rb
new file mode 100644
index 0000000..cb97ff1
--- /dev/null
+++ b/code/spyglass/lib/spyglass/server.rb
@@ -0,0 +1,17 @@
+module Spyglass
+
+ class Server
+ include Singleton
+ include Logging
+
+ def start
+ # Opens the main listening socket for the server. Now the server is responsive to
+ # incoming connections.
+ sock = TCPServer.open(Config.host, Config.port)
+ out "Listening on port #{Config.host}:#{Config.port}"
+
+ Lookout.instance.start(sock)
+ end
+ end
+end
+
diff --git a/code/spyglass/lib/spyglass/worker.rb b/code/spyglass/lib/spyglass/worker.rb
new file mode 100644
index 0000000..7d4adda
--- /dev/null
+++ b/code/spyglass/lib/spyglass/worker.rb
@@ -0,0 +1,93 @@
+require 'time'
+require 'rack/utils'
+
+# Worker
+# ======
+#
+module Spyglass
+ class Worker
+ include Logging
+
+ def initialize(socket, app, writable_pipe, connection = nil)
+ @socket, @app, @writable_pipe = socket, app, writable_pipe
+ @parser = Spyglass::HttpParser.new
+
+ handle_connection(connection) if connection
+ end
+
+ def start
+ trap_signals
+
+ loop do
+ handle_connection @socket.accept
+ end
+ end
+
+ def handle_connection(conn)
+ verbose "Received connection"
+ # This notifies our Master that we have received a connection, expiring
+ # it's `IO.select` and preventing it from timing out.
+ @writable_pipe.write_nonblock('.')
+
+ # This clears any state that the http parser has lying around
+ # from the last connection that was handled.
+ @parser.reset
+
+ # The Rack spec requires that 'rack.input' be encoded as ASCII-8BIT.
+ empty_body = ''
+ empty_body.encode!(Encoding::ASCII_8BIT) if empty_body.respond_to?(:encode!)
+
+ # The Rack spec requires that the env contain certain keys before being
+ # passed to the app. These are the keys that aren't provided by each
+ # incoming request, server-specific stuff.
+ env = {
+ 'rack.input' => StringIO.new(empty_body),
+ 'rack.multithread' => false,
+ 'rack.multiprocess' => true,
+ 'rack.run_once' => false,
+ 'rack.errors' => STDERR,
+ 'rack.version' => [1, 0]
+ }
+
+ # This reads data in from the client connection. We'll read up to
+ # 10000 bytes at the moment.
+ data = conn.readpartial(10000)
+ # Here we pass the data and the env into the http parser. It parses
+ # the raw http request data and updates the env with all of the data
+ # it can withdraw.
+ @parser.execute(env, data, 0)
+
+ # Call the Rack app, goes all the way down the rabbit hole and back again.
+ status, headers, body = @app.call(env)
+
+ # These are the default headers we always include in a response. We
+ # only speak HTTP 1.1 and we always close the client connection. At
+ # the monment keepalive is not supported.
+ head = "HTTP/1.1 #{status}\r\n" \
+ "Date: #{Time.now.httpdate}\r\n" \
+ "Status: #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n" \
+ "Connection: close\r\n"
+
+ headers.each do |k,v|
+ head << "#{k}: #{v}\r\n"
+ end
+ conn.write "#{head}\r\n"
+
+ body.each { |chunk| conn.write chunk }
+ body.close if body.respond_to?(:close)
+ # Since keepalive is not supported we can close the client connection
+ # immediately after writing the body.
+ conn.close
+
+ verbose "Closed connection"
+ end
+
+ def trap_signals
+ trap(:QUIT) do
+ out "Received QUIT"
+ exit
+ end
+ end
+ end
+end
+
diff --git a/code/spyglass/test/66613524f6c67942d7d19386f51c551cconfig.ru b/code/spyglass/test/66613524f6c67942d7d19386f51c551cconfig.ru
new file mode 100644
index 0000000..2aa2c98
--- /dev/null
+++ b/code/spyglass/test/66613524f6c67942d7d19386f51c551cconfig.ru
@@ -0,0 +1,9 @@
+ require 'sinatra'
+
+ get '/zing' do
+ redirect 'http://example.com'
+ end
+
+ at_exit { File.unlink('/var/folders/0t/wmb8l4z555q1vy_n8wmg0jl00000gn/T/lifeline20130730-5833-ursook') rescue nil }
+
+ run Sinatra::Application
diff --git a/code/spyglass/test/basic_rack_test.rb b/code/spyglass/test/basic_rack_test.rb
new file mode 100644
index 0000000..bf4c4ed
--- /dev/null
+++ b/code/spyglass/test/basic_rack_test.rb
@@ -0,0 +1,15 @@
+require 'helper'
+require 'excon'
+
+class BasicRackTest < MiniTest::Unit::TestCase
+ def setup
+ spyglass
+ end
+
+ def test_it_responds
+ response = Excon.get("http://0.0.0.0:#{PORT}/fuzzy")
+
+ assert_equal 200, response.status, "Didn't get the right response code"
+ assert_match /Hello world/, response.body, "Didn't get the right response body"
+ end
+end
diff --git a/code/spyglass/test/boots_test.rb b/code/spyglass/test/boots_test.rb
new file mode 100644
index 0000000..e4b4945
--- /dev/null
+++ b/code/spyglass/test/boots_test.rb
@@ -0,0 +1,13 @@
+require 'helper'
+
+class BootTest < MiniTest::Unit::TestCase
+ def setup
+ spyglass
+ end
+
+ def test_it_boots
+ # If this returns `nil` then our process is still running, meaning
+ # it hasn't crashed!
+ refute Process.waitpid(@pid, Process::WNOHANG)
+ end
+end
diff --git a/code/spyglass/test/config.ru b/code/spyglass/test/config.ru
new file mode 100644
index 0000000..fbec204
--- /dev/null
+++ b/code/spyglass/test/config.ru
@@ -0,0 +1,10 @@
+require 'rack/lint'
+
+class HelloWorld
+ def call(env)
+ [200, {"Content-Type" => 'text/plain'}, ["Hello world!"]]
+ end
+end
+
+use Rack::Lint
+run HelloWorld.new
diff --git a/code/spyglass/test/helper.rb b/code/spyglass/test/helper.rb
new file mode 100644
index 0000000..64615cc
--- /dev/null
+++ b/code/spyglass/test/helper.rb
@@ -0,0 +1,33 @@
+require 'bundler/setup'
+require 'minitest/autorun'
+
+class MiniTest::Unit::TestCase
+ ROOT = File.dirname(__FILE__) + '/..'
+ TEST_CONFIG_RU = File.open("#{ROOT}/test/config.ru", 'r+')
+ PORT = 5656
+ LOG = File.join('test', 'log', 'spyglass.log')
+ warn "Spyglass output can be found in #{LOG}"
+
+ def spyglass(args = {})
+ string_args = args.map { |key, value| "--#{key}=#{value}" }
+ config_ru = @ad_hoc_config_ru || TEST_CONFIG_RU
+
+ cmd = "ruby -I\"#{ROOT}/lib\" -rubygems \"#{ROOT}/bin/spyglass\" --vverbose -c \"#{config_ru.path}\" -p#{PORT} #{string_args.join(' ')}"
+ @pid = Process.spawn(cmd, :err => :out, :out => LOG)
+ sleep 1.5
+ end
+
+ def config_ru(content)
+ # This ensures that we have a unique filename for each test run
+ filename = File.join(ROOT, 'test', Digest::MD5.hexdigest($PROGRAM_NAME) + 'config.ru')
+ @ad_hoc_config_ru = File.open(filename, 'w')
+ @ad_hoc_config_ru.write content
+ @ad_hoc_config_ru.flush
+ end
+
+ def teardown
+ @pid && Process.kill(:QUIT, @pid)
+ @ad_hoc_config_ru && File.unlink(@ad_hoc_config_ru.path)
+ rescue Errno::ESRCH
+ end
+end
diff --git a/code/spyglass/test/log/.gitkeep b/code/spyglass/test/log/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/code/spyglass/test/log/.gitkeep
diff --git a/code/spyglass/test/sinatra_test.rb b/code/spyglass/test/sinatra_test.rb
new file mode 100644
index 0000000..0179413
--- /dev/null
+++ b/code/spyglass/test/sinatra_test.rb
@@ -0,0 +1,25 @@
+require 'helper'
+require 'excon'
+
+class SinatraTest < MiniTest::Unit::TestCase
+ def setup
+ config_ru <<-RU
+ require 'sinatra'
+
+ get '/zing' do
+ redirect 'http://example.com'
+ end
+
+ run Sinatra::Application
+ RU
+
+ spyglass
+ end
+
+ def test_it_responds
+ response = Excon.get("http://0.0.0.0:#{PORT}/zing")
+
+ assert_equal 302, response.status, "Didn't get the right response code"
+ assert_match /example/, response.headers['Location'], "Didn't get the right location header"
+ end
+end
diff --git a/code/spyglass/test/timeout_test.rb b/code/spyglass/test/timeout_test.rb
new file mode 100644
index 0000000..9cbe134
--- /dev/null
+++ b/code/spyglass/test/timeout_test.rb
@@ -0,0 +1,51 @@
+require 'helper'
+require 'excon'
+require 'tempfile'
+
+class TimeoutTest < MiniTest::Unit::TestCase
+ def setup
+ @lifeline = Tempfile.open('lifeline')
+ @uri = "http://0.0.0.0:#{PORT}/zing"
+
+ config_ru <<-RU
+ require 'sinatra'
+
+ get '/zing' do
+ redirect 'http://example.com'
+ end
+
+ at_exit { File.unlink('#{@lifeline.path}') rescue nil }
+
+ run Sinatra::Application
+ RU
+ spyglass :timeout => 3
+ end
+
+ def test_times_out_after_timeout_has_expired
+ Excon.get(@uri)
+ sleep 3.5
+
+ # When the Master process loads the Sinatra it will set up the at_exit
+ # hook defined above. That process should exit after 3 seconds, removing
+ # our @lifeline file. If the file still exists at this point then the
+ # timeout didn't happen properly.
+ refute File.file?(@lifeline.path), "Spyglass didn't time out properly"
+ end
+
+ def test_doesnt_time_out_if_requests_keep_coming
+ Excon.get(@uri)
+ sleep 1
+ Excon.get(@uri)
+ sleep 1
+
+ Excon.get(@uri)
+ sleep 1.5
+ Excon.get(@uri)
+
+ # When the Master process loads the Sinatra it will set up the at_exit
+ # hook defined above. Since we're keeping the server saturated with
+ # requests, definitely not letting it sit idle for 3 seconds, the @lifeline
+ # file should still exist when we get here.
+ assert File.file?(@lifeline.path), "Spyglass didn't time out properly"
+ end
+end