diff options
Diffstat (limited to 'code/spyglass')
48 files changed, 5166 insertions, 0 deletions
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 …</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">¶</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">¶</a> + </div> + <p>A hash of key => default</p> + + </div> + + <div class="content"><div class='highlight'><pre> <span class="constant">OPTIONS</span> = { + <span class="symbol">:port</span> => <span class="number">4222</span>, + <span class="symbol">:host</span> => <span class="string">'0.0.0.0'</span>, + <span class="symbol">:workers</span> => <span class="number">2</span>, + <span class="symbol">:timeout</span> => <span class="number">30</span>, + <span class="symbol">:config_ru_path</span> => <span class="string">'config.ru'</span>, + <span class="symbol">:verbose</span> => <span class="keyword">false</span>, + <span class="symbol">:vverbose</span> => <span class="keyword">false</span> + } + + <span class="class"><span class="keyword">class</span> <span class="inheritance"><</span><span class="inheritance">< <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">¶</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 …</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">¶</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 …</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">¶</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">¶</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">¶</a> + </div> + <p>The Lookout doesn't know anything about the app itself, so there'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">¶</a> + </div> + <p>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 <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">¶</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">¶</a> + </div> + <p>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.</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">¶</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">¶</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">¶</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'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">¶</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">¶</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">¶</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">¶</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'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">¶</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">¶</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'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 …</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">¶</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">¶</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'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">¶</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">¶</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">¶</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'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.</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">¶</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">¶</a> + </div> + <p>Clear the data on the pipe so it doesn'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> << 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">¶</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 Binary files differnew file mode 100755 index 0000000..1b32532 --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-bold.eot diff --git a/code/spyglass/docs/public/fonts/aller-bold.ttf b/code/spyglass/docs/public/fonts/aller-bold.ttf Binary files differnew file mode 100755 index 0000000..dc4cc9c --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-bold.ttf diff --git a/code/spyglass/docs/public/fonts/aller-bold.woff b/code/spyglass/docs/public/fonts/aller-bold.woff Binary files differnew file mode 100755 index 0000000..fa16fd0 --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-bold.woff diff --git a/code/spyglass/docs/public/fonts/aller-light.eot b/code/spyglass/docs/public/fonts/aller-light.eot Binary files differnew file mode 100755 index 0000000..40bd654 --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-light.eot diff --git a/code/spyglass/docs/public/fonts/aller-light.ttf b/code/spyglass/docs/public/fonts/aller-light.ttf Binary files differnew file mode 100755 index 0000000..c2c7290 --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-light.ttf diff --git a/code/spyglass/docs/public/fonts/aller-light.woff b/code/spyglass/docs/public/fonts/aller-light.woff Binary files differnew file mode 100755 index 0000000..81a09d1 --- /dev/null +++ b/code/spyglass/docs/public/fonts/aller-light.woff diff --git a/code/spyglass/docs/public/fonts/novecento-bold.eot b/code/spyglass/docs/public/fonts/novecento-bold.eot Binary files differnew file mode 100755 index 0000000..98a9a7f --- /dev/null +++ b/code/spyglass/docs/public/fonts/novecento-bold.eot diff --git a/code/spyglass/docs/public/fonts/novecento-bold.ttf b/code/spyglass/docs/public/fonts/novecento-bold.ttf Binary files differnew file mode 100755 index 0000000..2af39b0 --- /dev/null +++ b/code/spyglass/docs/public/fonts/novecento-bold.ttf diff --git a/code/spyglass/docs/public/fonts/novecento-bold.woff b/code/spyglass/docs/public/fonts/novecento-bold.woff Binary files differnew file mode 100755 index 0000000..de558b5 --- /dev/null +++ b/code/spyglass/docs/public/fonts/novecento-bold.woff 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 …</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">¶</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">¶</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">¶</a> + </div> + <h1>Spyglass</h1> + + </div> + + </li> + + + <li id="section-2"> + <div class="annotation"> + + <div class="pilwrap "> + <a class="pilcrow" href="#section-2">¶</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'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. </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'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">¶</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 '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.</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'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 …</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">¶</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">¶</a> + </div> + <h1>Worker</h1> + + </div> + + </li> + + + <li id="section-3"> + <div class="annotation"> + + <div class="pilwrap "> + <a class="pilcrow" href="#section-3">¶</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">¶</a> + </div> + <p>This notifies our Master that we have received a connection, expiring +it'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">¶</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">¶</a> + </div> + <p>The Rack spec requires that 'rack.input' 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">¶</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'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> => <span class="constant">StringIO</span>.new(empty_body), + <span class="string">'rack.multithread'</span> => <span class="keyword">false</span>, + <span class="string">'rack.multiprocess'</span> => <span class="keyword">true</span>, + <span class="string">'rack.run_once'</span> => <span class="keyword">false</span>, + <span class="string">'rack.errors'</span> => <span class="constant">STDERR</span>, + <span class="string">'rack.version'</span> => [<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">¶</a> + </div> + <p>This reads data in from the client connection. We'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">¶</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">¶</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">¶</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 << <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">¶</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 |
