steamcannon-thin 1.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. data/CHANGELOG +288 -0
  2. data/COPYING +18 -0
  3. data/README +69 -0
  4. data/Rakefile +44 -0
  5. data/benchmark/abc +51 -0
  6. data/benchmark/benchmarker.rb +80 -0
  7. data/benchmark/runner +82 -0
  8. data/bin/thin +6 -0
  9. data/example/adapter.rb +32 -0
  10. data/example/async_app.ru +126 -0
  11. data/example/async_chat.ru +247 -0
  12. data/example/async_tailer.ru +100 -0
  13. data/example/config.ru +22 -0
  14. data/example/monit_sockets +20 -0
  15. data/example/monit_unixsock +20 -0
  16. data/example/myapp.rb +1 -0
  17. data/example/ramaze.ru +12 -0
  18. data/example/thin.god +80 -0
  19. data/example/thin_solaris_smf.erb +36 -0
  20. data/example/thin_solaris_smf.readme.txt +150 -0
  21. data/example/vlad.rake +64 -0
  22. data/ext/thin_parser/common.rl +55 -0
  23. data/ext/thin_parser/ext_help.h +14 -0
  24. data/ext/thin_parser/extconf.rb +6 -0
  25. data/ext/thin_parser/parser.c +1249 -0
  26. data/ext/thin_parser/parser.h +49 -0
  27. data/ext/thin_parser/parser.rl +157 -0
  28. data/ext/thin_parser/thin.c +436 -0
  29. data/lib/rack/adapter/loader.rb +91 -0
  30. data/lib/rack/adapter/rails.rb +183 -0
  31. data/lib/thin.rb +56 -0
  32. data/lib/thin/backends/base.rb +149 -0
  33. data/lib/thin/backends/swiftiply_client.rb +56 -0
  34. data/lib/thin/backends/tcp_server.rb +29 -0
  35. data/lib/thin/backends/unix_server.rb +51 -0
  36. data/lib/thin/command.rb +53 -0
  37. data/lib/thin/connection.rb +224 -0
  38. data/lib/thin/controllers/cluster.rb +178 -0
  39. data/lib/thin/controllers/controller.rb +188 -0
  40. data/lib/thin/controllers/service.rb +75 -0
  41. data/lib/thin/controllers/service.sh.erb +39 -0
  42. data/lib/thin/daemonizing.rb +180 -0
  43. data/lib/thin/headers.rb +39 -0
  44. data/lib/thin/logging.rb +54 -0
  45. data/lib/thin/request.rb +156 -0
  46. data/lib/thin/response.rb +101 -0
  47. data/lib/thin/runner.rb +220 -0
  48. data/lib/thin/server.rb +253 -0
  49. data/lib/thin/stats.html.erb +216 -0
  50. data/lib/thin/stats.rb +52 -0
  51. data/lib/thin/statuses.rb +43 -0
  52. data/lib/thin/version.rb +32 -0
  53. data/lib/thin_parser.so +0 -0
  54. data/spec/backends/swiftiply_client_spec.rb +66 -0
  55. data/spec/backends/tcp_server_spec.rb +33 -0
  56. data/spec/backends/unix_server_spec.rb +37 -0
  57. data/spec/command_spec.rb +25 -0
  58. data/spec/configs/cluster.yml +9 -0
  59. data/spec/configs/single.yml +9 -0
  60. data/spec/connection_spec.rb +106 -0
  61. data/spec/controllers/cluster_spec.rb +267 -0
  62. data/spec/controllers/controller_spec.rb +129 -0
  63. data/spec/controllers/service_spec.rb +50 -0
  64. data/spec/daemonizing_spec.rb +196 -0
  65. data/spec/headers_spec.rb +40 -0
  66. data/spec/logging_spec.rb +46 -0
  67. data/spec/perf/request_perf_spec.rb +50 -0
  68. data/spec/perf/response_perf_spec.rb +19 -0
  69. data/spec/perf/server_perf_spec.rb +39 -0
  70. data/spec/rack/loader_spec.rb +42 -0
  71. data/spec/rack/rails_adapter_spec.rb +173 -0
  72. data/spec/rails_app/app/controllers/application.rb +10 -0
  73. data/spec/rails_app/app/controllers/simple_controller.rb +19 -0
  74. data/spec/rails_app/app/helpers/application_helper.rb +3 -0
  75. data/spec/rails_app/app/views/simple/index.html.erb +15 -0
  76. data/spec/rails_app/config/boot.rb +109 -0
  77. data/spec/rails_app/config/environment.rb +64 -0
  78. data/spec/rails_app/config/environments/development.rb +18 -0
  79. data/spec/rails_app/config/environments/production.rb +19 -0
  80. data/spec/rails_app/config/environments/test.rb +22 -0
  81. data/spec/rails_app/config/initializers/inflections.rb +10 -0
  82. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  83. data/spec/rails_app/config/routes.rb +35 -0
  84. data/spec/rails_app/public/404.html +30 -0
  85. data/spec/rails_app/public/422.html +30 -0
  86. data/spec/rails_app/public/500.html +30 -0
  87. data/spec/rails_app/public/dispatch.cgi +10 -0
  88. data/spec/rails_app/public/dispatch.fcgi +24 -0
  89. data/spec/rails_app/public/dispatch.rb +10 -0
  90. data/spec/rails_app/public/favicon.ico +0 -0
  91. data/spec/rails_app/public/images/rails.png +0 -0
  92. data/spec/rails_app/public/index.html +277 -0
  93. data/spec/rails_app/public/javascripts/application.js +2 -0
  94. data/spec/rails_app/public/javascripts/controls.js +963 -0
  95. data/spec/rails_app/public/javascripts/dragdrop.js +972 -0
  96. data/spec/rails_app/public/javascripts/effects.js +1120 -0
  97. data/spec/rails_app/public/javascripts/prototype.js +4225 -0
  98. data/spec/rails_app/public/robots.txt +5 -0
  99. data/spec/rails_app/script/about +3 -0
  100. data/spec/rails_app/script/console +3 -0
  101. data/spec/rails_app/script/destroy +3 -0
  102. data/spec/rails_app/script/generate +3 -0
  103. data/spec/rails_app/script/performance/benchmarker +3 -0
  104. data/spec/rails_app/script/performance/profiler +3 -0
  105. data/spec/rails_app/script/performance/request +3 -0
  106. data/spec/rails_app/script/plugin +3 -0
  107. data/spec/rails_app/script/process/inspector +3 -0
  108. data/spec/rails_app/script/process/reaper +3 -0
  109. data/spec/rails_app/script/process/spawner +3 -0
  110. data/spec/rails_app/script/runner +3 -0
  111. data/spec/rails_app/script/server +3 -0
  112. data/spec/request/mongrel_spec.rb +39 -0
  113. data/spec/request/parser_spec.rb +254 -0
  114. data/spec/request/persistent_spec.rb +35 -0
  115. data/spec/request/processing_spec.rb +50 -0
  116. data/spec/response_spec.rb +91 -0
  117. data/spec/runner_spec.rb +168 -0
  118. data/spec/server/builder_spec.rb +44 -0
  119. data/spec/server/pipelining_spec.rb +110 -0
  120. data/spec/server/robustness_spec.rb +34 -0
  121. data/spec/server/stopping_spec.rb +55 -0
  122. data/spec/server/swiftiply.yml +6 -0
  123. data/spec/server/swiftiply_spec.rb +32 -0
  124. data/spec/server/tcp_spec.rb +57 -0
  125. data/spec/server/threaded_spec.rb +27 -0
  126. data/spec/server/unix_socket_spec.rb +26 -0
  127. data/spec/server_spec.rb +100 -0
  128. data/spec/spec_helper.rb +220 -0
  129. data/tasks/announce.rake +22 -0
  130. data/tasks/deploy.rake +13 -0
  131. data/tasks/email.erb +30 -0
  132. data/tasks/gem.rake +66 -0
  133. data/tasks/rdoc.rake +25 -0
  134. data/tasks/site.rake +15 -0
  135. data/tasks/spec.rake +43 -0
  136. data/tasks/stats.rake +28 -0
  137. metadata +251 -0
@@ -0,0 +1,80 @@
1
+ require 'rack/lobster'
2
+
3
+ class Benchmarker
4
+ PORT = 7000
5
+ ADDRESS = '0.0.0.0'
6
+
7
+ attr_accessor :requests, :concurrencies, :servers, :keep_alive
8
+
9
+ def initialize
10
+ @servers = %w(Mongrel EMongrel Thin)
11
+ @requests = 1000
12
+ @concurrencies = [1, 10, 100]
13
+ end
14
+
15
+ def writer(&block)
16
+ @writer = block
17
+ end
18
+
19
+ def run!
20
+ @concurrencies.each do |concurrency|
21
+ @servers.each do |server|
22
+ req_sec, failed = run_one(server, concurrency)
23
+ @writer.call(server, @requests, concurrency, req_sec, failed)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+ def start_server(handler_name)
30
+ @server = fork do
31
+ [STDOUT, STDERR].each { |o| o.reopen "/dev/null" }
32
+
33
+ case handler_name
34
+ when 'EMongrel'
35
+ require 'swiftcore/evented_mongrel'
36
+ handler_name = 'Mongrel'
37
+ end
38
+
39
+ app = proc do |env|
40
+ [200, {'Content-Type' => 'text/html', 'Content-Length' => '11'}, ['hello world']]
41
+ end
42
+
43
+ handler = Rack::Handler.const_get(handler_name)
44
+ handler.run app, :Host => ADDRESS, :Port => PORT
45
+ end
46
+
47
+ sleep 2
48
+ end
49
+
50
+ def stop_server
51
+ Process.kill('SIGKILL', @server)
52
+ Process.wait
53
+ end
54
+
55
+ def run_ab(concurrency)
56
+ `nice -n20 ab -c #{concurrency} -n #{@requests} #{@keep_alive ? '-k' : ''} #{ADDRESS}:#{PORT}/ 2> /dev/null`
57
+ end
58
+
59
+ def run_one(handler_name, concurrency)
60
+ start_server(handler_name)
61
+
62
+ out = run_ab(concurrency)
63
+
64
+ stop_server
65
+
66
+ req_sec = if matches = out.match(/^Requests.+?(\d+\.\d+)/)
67
+ matches[1].to_i
68
+ else
69
+ 0
70
+ end
71
+
72
+ failed = if matches = out.match(/^Failed requests.+?(\d+)/)
73
+ matches[1].to_i
74
+ else
75
+ 0
76
+ end
77
+
78
+ [req_sec, failed]
79
+ end
80
+ end
data/benchmark/runner ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+ # Simple benchmark to compare Thin performance against
3
+ # other webservers supported by Rack.
4
+ #
5
+ # Run with:
6
+ #
7
+ # ruby simple.rb [num of request] [print|graph] [concurrency levels]
8
+ #
9
+
10
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
11
+ require 'thin'
12
+
13
+ require File.dirname(__FILE__) + '/benchmarker'
14
+ require 'optparse'
15
+
16
+ options = {
17
+ :requests => 1000,
18
+ :concurrencies => [1, 10, 100],
19
+ :keep_alive => false,
20
+ :output => :table
21
+ }
22
+
23
+ OptionParser.new do |opts|
24
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
25
+
26
+ opts.on("-n", "--requests NUM", "Number of requests") { |num| options[:requests] = num.to_i }
27
+ opts.on("-c", "--concurrencies EXP", "Concurrency levels") { |exp| options[:concurrencies] = eval(exp).to_a }
28
+ opts.on("-k", "--keep-alive", "Use persistent connections") { options[:keep_alive] = true }
29
+ opts.on("-t", "--table", "Output as text table") { options[:output] = :table }
30
+ opts.on("-g", "--graph", "Output as graph") { options[:output] = :graph }
31
+
32
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
33
+ end.parse!(ARGV)
34
+
35
+ # benchmark output_type, %w(WEBrick Mongrel EMongrel Thin), request, levels
36
+ b = Benchmarker.new
37
+ b.requests = options[:requests]
38
+ b.concurrencies = options[:concurrencies]
39
+ b.keep_alive = options[:keep_alive]
40
+
41
+ case options[:output]
42
+ when :table
43
+ puts 'server request concurrency req/s failures'
44
+ puts '=' * 52
45
+
46
+ b.writer do |server, requests, concurrency, req_sec, failed|
47
+ puts "#{server.ljust(8)} #{requests} #{concurrency.to_s.ljust(4)} #{req_sec.to_s.ljust(8)} #{failed}"
48
+ end
49
+
50
+ b.run!
51
+
52
+ when :graph
53
+ require '/usr/local/lib/ruby/gems/1.8/gems/gruff-0.2.9/lib/gruff'
54
+ g = Gruff::Area.new
55
+ g.title = "#{options[:requests]} requests"
56
+ g.title << ' w/ Keep-Alive' if options[:keep_alive]
57
+
58
+ g.x_axis_label = 'Concurrency'
59
+ g.y_axis_label = 'Requests / sec'
60
+ g.maximum_value = 0
61
+ g.minimum_value = 0
62
+ g.labels = {}
63
+ b.concurrencies.each_with_index { |c, i| g.labels[i] = c.to_s }
64
+
65
+ results = {}
66
+
67
+ b.writer do |server, requests, concurrency, req_sec, failed|
68
+ print '.'
69
+ results[server] ||= []
70
+ results[server] << req_sec
71
+ end
72
+
73
+ b.run!
74
+ puts
75
+
76
+ results.each do |server, concurrencies|
77
+ g.data(server, concurrencies)
78
+ end
79
+
80
+ g.write('bench.png')
81
+ `open bench.png`
82
+ end
data/bin/thin ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # Thin command line interface script.
3
+ # Run <tt>thin -h</tt> to get more usage.
4
+
5
+ require 'thin'
6
+ Thin::Runner.new(ARGV).run!
@@ -0,0 +1,32 @@
1
+ # Run with: ruby adapter.rb
2
+ # Then browse to http://localhost:3000/test
3
+ # and http://localhost:3000/files/adapter.rb
4
+ require 'thin'
5
+
6
+ class SimpleAdapter
7
+ def call(env)
8
+ body = ["hello!"]
9
+ [
10
+ 200,
11
+ { 'Content-Type' => 'text/plain' },
12
+ body
13
+ ]
14
+ end
15
+ end
16
+
17
+ Thin::Server.start('0.0.0.0', 3000) do
18
+ use Rack::CommonLogger
19
+ map '/test' do
20
+ run SimpleAdapter.new
21
+ end
22
+ map '/files' do
23
+ run Rack::File.new('.')
24
+ end
25
+ end
26
+
27
+ # You could also start the server like this:
28
+ #
29
+ # app = Rack::URLMap.new('/test' => SimpleAdapter.new,
30
+ # '/files' => Rack::File.new('.'))
31
+ # Thin::Server.start('0.0.0.0', 3000, app)
32
+ #
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env rackup -s thin
2
+ #
3
+ # async_app.ru
4
+ # raggi/thin
5
+ #
6
+ # A second demo app for async rack + thin app processing!
7
+ # Now using http status code 100 instead.
8
+ #
9
+ # Created by James Tucker on 2008-06-17.
10
+ # Copyright 2008 James Tucker <raggi@rubyforge.org>.
11
+ #
12
+ #--
13
+ # Benchmark Results:
14
+ #
15
+ # raggi@mbk:~$ ab -c 100 -n 500 http://127.0.0.1:3000/
16
+ # This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
17
+ # Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
18
+ # Copyright 2006 The Apache Software Foundation, http://www.apache.org/
19
+ #
20
+ # Benchmarking 127.0.0.1 (be patient)
21
+ # Completed 100 requests
22
+ # Completed 200 requests
23
+ # Completed 300 requests
24
+ # Completed 400 requests
25
+ # Finished 500 requests
26
+ #
27
+ #
28
+ # Server Software: thin
29
+ # Server Hostname: 127.0.0.1
30
+ # Server Port: 3000
31
+ #
32
+ # Document Path: /
33
+ # Document Length: 12 bytes
34
+ #
35
+ # Concurrency Level: 100
36
+ # Time taken for tests: 5.263089 seconds
37
+ # Complete requests: 500
38
+ # Failed requests: 0
39
+ # Write errors: 0
40
+ # Total transferred: 47000 bytes
41
+ # HTML transferred: 6000 bytes
42
+ # Requests per second: 95.00 [#/sec] (mean)
43
+ # Time per request: 1052.618 [ms] (mean)
44
+ # Time per request: 10.526 [ms] (mean, across all concurrent requests)
45
+ # Transfer rate: 8.55 [Kbytes/sec] received
46
+ #
47
+ # Connection Times (ms)
48
+ # min mean[+/-sd] median max
49
+ # Connect: 0 3 2.2 3 8
50
+ # Processing: 1042 1046 3.1 1046 1053
51
+ # Waiting: 1037 1042 3.6 1041 1050
52
+ # Total: 1045 1049 3.1 1049 1057
53
+ #
54
+ # Percentage of the requests served within a certain time (ms)
55
+ # 50% 1049
56
+ # 66% 1051
57
+ # 75% 1053
58
+ # 80% 1053
59
+ # 90% 1054
60
+ # 95% 1054
61
+ # 98% 1056
62
+ # 99% 1057
63
+ # 100% 1057 (longest request)
64
+
65
+ class DeferrableBody
66
+ include EventMachine::Deferrable
67
+
68
+ def call(body)
69
+ body.each do |chunk|
70
+ @body_callback.call(chunk)
71
+ end
72
+ end
73
+
74
+ def each &blk
75
+ @body_callback = blk
76
+ end
77
+
78
+ end
79
+
80
+ class AsyncApp
81
+
82
+ # This is a template async response. N.B. Can't use string for body on 1.9
83
+ AsyncResponse = [-1, {}, []].freeze
84
+
85
+ def call(env)
86
+
87
+ body = DeferrableBody.new
88
+
89
+ # Get the headers out there asap, let the client know we're alive...
90
+ EventMachine::next_tick { env['async.callback'].call [200, {'Content-Type' => 'text/plain'}, body] }
91
+
92
+ # Semi-emulate a long db request, instead of a timer, in reality we'd be
93
+ # waiting for the response data. Whilst this happens, other connections
94
+ # can be serviced.
95
+ # This could be any callback based thing though, a deferrable waiting on
96
+ # IO data, a db request, an http request, an smtp send, whatever.
97
+ EventMachine::add_timer(1) {
98
+ body.call ["Woah, async!\n"]
99
+
100
+ EventMachine::next_tick {
101
+ # This could actually happen any time, you could spawn off to new
102
+ # threads, pause as a good looking lady walks by, whatever.
103
+ # Just shows off how we can defer chunks of data in the body, you can
104
+ # even call this many times.
105
+ body.call ["Cheers then!"]
106
+ body.succeed
107
+ }
108
+ }
109
+
110
+ # throw :async # Still works for supporting non-async frameworks...
111
+
112
+ AsyncResponse # May end up in Rack :-)
113
+ end
114
+
115
+ end
116
+
117
+ # The additions to env for async.connection and async.callback absolutely
118
+ # destroy the speed of the request if Lint is doing it's checks on env.
119
+ # It is also important to note that an async response will not pass through
120
+ # any further middleware, as the async response notification has been passed
121
+ # right up to the webserver, and the callback goes directly there too.
122
+ # Middleware could possibly catch :async, and also provide a different
123
+ # async.connection and async.callback.
124
+
125
+ # use Rack::Lint
126
+ run AsyncApp.new
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env rackup -s thin
2
+ #
3
+ # async_chat.ru
4
+ # raggi/thin
5
+ #
6
+ # Created by James Tucker on 2008-06-19.
7
+ # Copyright 2008 James Tucker <raggi@rubyforge.org>.
8
+
9
+ # Uncomment if appropriate for you..
10
+ EM.epoll
11
+ # EM.kqueue # bug on OS X in 0.12?
12
+
13
+ class DeferrableBody
14
+ include EventMachine::Deferrable
15
+
16
+ def initialize
17
+ @queue = []
18
+ end
19
+
20
+ def schedule_dequeue
21
+ return unless @body_callback
22
+ EventMachine::next_tick do
23
+ next unless body = @queue.shift
24
+ body.each do |chunk|
25
+ @body_callback.call(chunk)
26
+ end
27
+ schedule_dequeue unless @queue.empty?
28
+ end
29
+ end
30
+
31
+ def call(body)
32
+ @queue << body
33
+ schedule_dequeue
34
+ end
35
+
36
+ def each &blk
37
+ @body_callback = blk
38
+ schedule_dequeue
39
+ end
40
+
41
+ end
42
+
43
+ class Chat
44
+
45
+ module UserBody
46
+ attr_accessor :username
47
+ end
48
+
49
+ def initialize
50
+ @users = {}
51
+ end
52
+
53
+ def render_page
54
+ [] << <<-EOPAGE
55
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
56
+ <html>
57
+ <head>
58
+ <style>
59
+ body {
60
+ font-family: sans-serif;
61
+ margin: 0;
62
+ padding: 0;
63
+ margin-top: 4em;
64
+ margin-bottom: 1em;
65
+ }
66
+ #header {
67
+ background: silver;
68
+ height: 4em;
69
+ width: 100%;
70
+ position: fixed;
71
+ top: 0px;
72
+ border-bottom: 1px solid black;
73
+ padding-left: 0.5em;
74
+ }
75
+ #messages {
76
+ width: 100%;
77
+ height: 100%;
78
+ }
79
+ .message {
80
+ margin-left: 1em;
81
+ }
82
+ #send_form {
83
+ position: fixed;
84
+ bottom: 0px;
85
+ height: 1em;
86
+ width: 100%;
87
+ }
88
+ #message_box {
89
+ background: silver;
90
+ width: 100%;
91
+ border: 0px;
92
+ border-top: 1px solid black;
93
+ }
94
+ .gray {
95
+ color: gray;
96
+ }
97
+ </style>
98
+ <script type="text/javascript" src="http://ra66i.org/tmp/jquery-1.2.6.min.js"></script>
99
+ <script type="text/javascript">
100
+ XHR = function() {
101
+ var request = false;
102
+ try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {
103
+ try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch(e1) {
104
+ try { request = new XMLHttpRequest(); } catch(e2) {
105
+ return false;
106
+ }
107
+ }
108
+ }
109
+ return request;
110
+ }
111
+ scroll = function() {
112
+ window.scrollBy(0,50);
113
+ setTimeout('scroll()',100);
114
+ }
115
+ focus = function() {
116
+ $('#message_box').focus();
117
+ }
118
+ send_message = function(message_box) {
119
+ xhr = XHR();
120
+ xhr.open("POST", "/", true);
121
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
122
+ xhr.setRequestHeader("X_REQUESTED_WITH", "XMLHttpRequest");
123
+ xhr.send("message="+escape(message_box.value));
124
+ scroll();
125
+ message_box.value = '';
126
+ focus();
127
+ return false;
128
+ }
129
+ new_message = function(username, message) {
130
+ // TODO html escape message
131
+ formatted_message = "<div class='message'>" + username + ": " + message + "</div>";
132
+ messages_div = $('#messages');
133
+ $(formatted_message).appendTo(messages_div);
134
+ scroll();
135
+ return true;
136
+ }
137
+ </script>
138
+ <title>Async Chat</title>
139
+ </head>
140
+ <body>
141
+ <div id="header">
142
+ <h1>Async Chat</h1>
143
+ </div>
144
+ <div id="messages" onclick="focus();">
145
+ <span class="gray">Your first message will become your nickname!</span>
146
+ <span>Users: #{@users.map{|k,u|u.username}.join(', ')}</span>
147
+ </div>
148
+ <form id="send_form" onSubmit="return send_message(this.message)">
149
+ <input type="text" id="message_box" name="message"></input>
150
+ </form>
151
+ <script type="text/javascript">focus();</script>
152
+ </body>
153
+ </html>
154
+ EOPAGE
155
+ end
156
+
157
+ def register_user(user_id, renderer)
158
+ body = create_user(user_id)
159
+ body.call render_page
160
+ body.errback { delete_user user_id }
161
+ body.callback { delete_user user_id }
162
+
163
+ EventMachine::next_tick do
164
+ renderer.call [200, {'Content-Type' => 'text/html'}, body]
165
+ end
166
+ end
167
+
168
+ def new_message(user_id, message)
169
+ return unless @users[user_id]
170
+ if @users[user_id].username == :anonymous
171
+ username = unique_username(message)
172
+ log "User: #{user_id} is #{username}"
173
+ @users[user_id].username = message
174
+ message = "<span class='gray'>-> #{username} signed on.</span>"
175
+ end
176
+ username ||= @users[user_id].username
177
+ log "User: #{username} sent: #{message}"
178
+ @users.each do |id, body|
179
+ EventMachine::next_tick { body.call [js_message(username, message)] }
180
+ end
181
+ end
182
+
183
+ private
184
+ def unique_username(name)
185
+ name.concat('_') while @users.any? { |id,u| name == u.username }
186
+ name
187
+ end
188
+
189
+ def log(str)
190
+ print str, "\n"
191
+ end
192
+
193
+ def add_user(id, body)
194
+ @users[id] = body
195
+ end
196
+
197
+ def delete_user(id)
198
+ message = "User: #{id} - #{@users[id].username if @users[id]} disconnected."
199
+ log message
200
+ new_message(id, message)
201
+ @users.delete id
202
+ end
203
+
204
+ def js_message(username, message)
205
+ %(<script type="text/javascript">new_message("#{username}","#{message}");</script>)
206
+ end
207
+
208
+ def create_user(id)
209
+ message = "User: #{id} connected."
210
+ log message
211
+ new_message(id, message)
212
+ body = DeferrableBody.new
213
+ body.extend UserBody
214
+ body.username = :anonymous
215
+ add_user(id, body)
216
+ body
217
+ end
218
+
219
+ end
220
+
221
+ class AsyncChat
222
+
223
+ AsyncResponse = [-1, {}, []].freeze
224
+ AjaxResponse = [200, {}, []].freeze
225
+
226
+ def initialize
227
+ @chat = Chat.new
228
+ end
229
+
230
+ def call(env)
231
+ request = Rack::Request.new(env)
232
+ # TODO - cookie me, baby
233
+ user_id = request.env['REMOTE_ADDR']
234
+ if request.xhr?
235
+ message = request['message']
236
+ @chat.new_message(user_id, Rack::Utils.escape_html(message))
237
+ AjaxResponse
238
+ else
239
+ renderer = request.env['async.callback']
240
+ @chat.register_user(user_id, renderer)
241
+ AsyncResponse
242
+ end
243
+ end
244
+
245
+ end
246
+
247
+ run AsyncChat.new