scgi 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +21 -0
- data/README +331 -0
- data/bin/scgi_ctrl +132 -0
- data/lib/RailsSCGIProcessor.rb +28 -0
- data/lib/scgi.rb +272 -0
- metadata +49 -0
data/COPYING
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2007 Jeremy Evans
|
2
|
+
Copyright (c) 2004 Zed A. Shaw
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
= ruby-scgi
|
2
|
+
|
3
|
+
ruby-scgi is a small Ruby script for running Ruby on Rails (and possibly other
|
4
|
+
web applications) for high-speed deployment of your applications in production.
|
5
|
+
It is intended as a replacement for the ancient FastCGI code base and bring
|
6
|
+
some big advantages to Rails deployment for production operations.
|
7
|
+
|
8
|
+
SCGI (Simple Common Gateway Interface) is a project to replace CGI and FastCGI
|
9
|
+
with a simpler protocol to both implement and manage. It was written by Neil
|
10
|
+
Schemenauer and adopted by many Python developers as a hosting option.
|
11
|
+
|
12
|
+
ruby-scgi is distributed as a gem, and can be installed with:
|
13
|
+
|
14
|
+
sudo gem install scgi
|
15
|
+
|
16
|
+
Feedback/Bugs/Support Requests should be handled through RubyForge at
|
17
|
+
http://rubyforge.org/projects/scgi/.
|
18
|
+
|
19
|
+
The RDoc is available at http://code.jeremyevans.net/doc/ruby-scgi/.
|
20
|
+
Subversion access is available at svn://code.jeremyevans.net/ruby-scgi/.
|
21
|
+
|
22
|
+
== Advantages
|
23
|
+
|
24
|
+
* Simultaneous support for Apache1, Apache2, and lighttpd on OSX and most
|
25
|
+
Linux/BSD systems.
|
26
|
+
* Same performance as FastCGI and better performance than other methods.
|
27
|
+
* Simple to install, run, and configure.
|
28
|
+
* Supports both single-port and multi-port clustering on most systems for that
|
29
|
+
extra boost in concurrency.
|
30
|
+
* Supports both command line and config file configuration.
|
31
|
+
* Gives out limited status information to help manage your Rails application's
|
32
|
+
resources.
|
33
|
+
* Makes it easy to manage your production deployment, and you can even run your
|
34
|
+
application in development mode exactly the same way as with script/server
|
35
|
+
for extra testing efficiency.
|
36
|
+
* You can set a maximum concurrent connections limit, which causes any
|
37
|
+
connections over the limit to get redirected to a /busy.html file. This can
|
38
|
+
help keep your site alive under heavy load.
|
39
|
+
* Simple to configure with your web server. Even if you use clustering you'll
|
40
|
+
be able to manage your webserver and Rails application independently.
|
41
|
+
* Reasonable defaults for almost everything based on user feedback.
|
42
|
+
* Completely free code licensed under Rails's own license.
|
43
|
+
* No external dependencies other than Ruby
|
44
|
+
* The core implementation and the command line tools are easily extensible for
|
45
|
+
other Ruby web frameworks.
|
46
|
+
|
47
|
+
== Comparison With FastCGI
|
48
|
+
|
49
|
+
SCGI and FastCGI have similar goals: To keep Ruby running between requests and
|
50
|
+
process the requests as fast as possible. The difference is that SCGI is much
|
51
|
+
simpler and easier to implement so there's less chance to get it wrong.
|
52
|
+
|
53
|
+
Specifically, ruby-scgi is written in pure Ruby so it doesn't leak memory,
|
54
|
+
runs everywhere, and is easy to install (no compilers needed).
|
55
|
+
|
56
|
+
One thing that SCGI doesn't support is using UNIX Domain sockets in addition to
|
57
|
+
TCP/IP sockets. This isn't really needed, but it is handy in a shared hosting
|
58
|
+
situation where you don't want others connecting to your processes or if you
|
59
|
+
have to request open ports. Sorry, no UNIX Domain sockets in SCGI.
|
60
|
+
|
61
|
+
== Comparison With WEBrick
|
62
|
+
|
63
|
+
In theory WEBrick should be able to run just as fast as SRR. They are both
|
64
|
+
written in pure Ruby. They both do similar processing (although WEBrick's are
|
65
|
+
a little more complicated). They both return about the same amount of data.
|
66
|
+
|
67
|
+
In practice WEBrick in production mode runs much slower than SRR in production
|
68
|
+
mode. The (dis)advantage (depending on your point of view) is that you have to
|
69
|
+
manage your webserver differently than you manage your application.
|
70
|
+
|
71
|
+
== Comparison With CGI
|
72
|
+
|
73
|
+
CGI is where every time a request comes in for rails the whole Ruby on Rails
|
74
|
+
framework is loaded. This is very slow, but it's easy to install.
|
75
|
+
|
76
|
+
An alternative is to use the cgi2scgi program distributed with the SCGI source
|
77
|
+
available from http://www.mems-exchange.org/software/scgi/ along with the
|
78
|
+
Apache modules. This program basically is a small little C program that runs
|
79
|
+
quickly as a CGI, but passes it's requests to your SRR backend. It's not all
|
80
|
+
that fast, but if you're stuck with cgi-bin only access then this might be just
|
81
|
+
the way to go. Since SCGI runs over TCP/IP you can even host your SRR on a
|
82
|
+
totally different machine with this.
|
83
|
+
|
84
|
+
== Running and Configuration
|
85
|
+
|
86
|
+
If you want to start ruby-scgi with the default configuration, just run:
|
87
|
+
|
88
|
+
scgi_ctrl -d /path/to/rails/app start
|
89
|
+
|
90
|
+
To stop the application:
|
91
|
+
|
92
|
+
scgi_ctrl -d /path/to/rails/app stop
|
93
|
+
|
94
|
+
To restart the application:
|
95
|
+
|
96
|
+
scgi_ctrl -d /path/to/rails/app start # start actually restarts
|
97
|
+
|
98
|
+
Note that restarting/stopping is controlled by a pid file (the location is
|
99
|
+
configurable). If the pidfile exists, it is read and the pids in it are
|
100
|
+
killed. If restarting, new processes are forked after the existing processes
|
101
|
+
are killed.
|
102
|
+
|
103
|
+
To see the possible and default configuration options, just run the program
|
104
|
+
without any arguments:
|
105
|
+
|
106
|
+
scgi_ctrl [option=value, ...] (start|stop)
|
107
|
+
Options:
|
108
|
+
-b, --bind IP address to bind to [127.0.0.1]
|
109
|
+
-c, --config Location of config file [config/scgi.yaml]
|
110
|
+
-d, --directory Working directory [.]
|
111
|
+
-e, --environment Environment (for Rails) [production]
|
112
|
+
-f, --fork Number of listners on each port [1]
|
113
|
+
-l, --logfile Location of log file [log/scgi.log]
|
114
|
+
-m, --maxconns Maximum number of concurrent users [2**30-1]
|
115
|
+
-n, --number Number of ports to bind to [1]
|
116
|
+
-p, --port Starting port to bind to [9999]
|
117
|
+
-P, --pidfile Location of pid file [log/scgi.pid]
|
118
|
+
-r, --processor Type of processor to use [Rails]
|
119
|
+
|
120
|
+
Note that the -d (--directory) option changes the working directory of the
|
121
|
+
process, so the -c, -l, and -P options are relative to that.
|
122
|
+
|
123
|
+
Here's a longer explanation of the options:
|
124
|
+
|
125
|
+
-b, --bind IP address to bind to [127.0.0.1]
|
126
|
+
|
127
|
+
This is the TCP/IP networking sockets to bind to. It defaults to the
|
128
|
+
loopback address because generally the web application runs on the same
|
129
|
+
physical server as the web server. If this is not the case, change it to an
|
130
|
+
externally available IP, and make sure to lock down access to the port via a
|
131
|
+
firewall.
|
132
|
+
|
133
|
+
-c, --config Location of config file [config/scgi.yaml]
|
134
|
+
|
135
|
+
This is the configuration file for ruby-scgi. It is recommended that you use
|
136
|
+
this instead of the command line configuration, as it saves typing. This
|
137
|
+
path is relative to the working directory, so if it is not inside the working
|
138
|
+
directory, make sure you specify an absolute path. Also, note that this is
|
139
|
+
the only option that is not configurable from the configuration file.
|
140
|
+
|
141
|
+
-d, --directory Working directory [.]
|
142
|
+
|
143
|
+
This is the working directory of the process. It should generally be the
|
144
|
+
path to the root of the web application. Alternatively, you can change to
|
145
|
+
the root of the web application before hand and then not use this option.
|
146
|
+
|
147
|
+
-e, --environment Environment (for Rails) [production]
|
148
|
+
|
149
|
+
This is the only option that is Rails-specific, allowing you to specify the
|
150
|
+
Rails environment on the command line. It defaults to production because
|
151
|
+
that is the general use case for ruby-scgi.
|
152
|
+
|
153
|
+
-f, --fork Number of listners on each port [1]
|
154
|
+
|
155
|
+
This enables single-port clustering of processes, so there are multiple
|
156
|
+
processes listening on each port. This can simplify configuration of the
|
157
|
+
webserver, since only a single port need be specified, and can also eliminate
|
158
|
+
the need for a proxy such as pound or pen to handle this for you. It
|
159
|
+
defaults to one process per port. Try single port clustering first, and if
|
160
|
+
it is not stable, switch to multiple port clustering. It is possible to use
|
161
|
+
both as once.
|
162
|
+
|
163
|
+
-l, --logfile Location of log file [log/scgi.log]
|
164
|
+
|
165
|
+
This is the location of the log file, relative to the working directory.
|
166
|
+
ruby-scgi doesn't log all that much (starts, shutdowns, bad requests, other
|
167
|
+
errors, and status info when sent SIGUSR2).
|
168
|
+
|
169
|
+
-m, --maxconns Maximum number of concurrent users [2**30-1]
|
170
|
+
|
171
|
+
The maximum number of concurrent connections. If more connections that this
|
172
|
+
are sent to the server, it redirects them to the /busy.html file.
|
173
|
+
|
174
|
+
-n, --number Number of ports to bind to [1]
|
175
|
+
|
176
|
+
This enables multi-port clustering. Multi-port clustering listens on
|
177
|
+
multiple ports starting with the port specified (so port, port+1, port+2,
|
178
|
+
...). This makes webserver configuration a little more difficult, and might
|
179
|
+
also require a separate proxy such as pound or pen, so you should try
|
180
|
+
single-port clustering first. You can run both at once if you want.
|
181
|
+
|
182
|
+
-p, --port Starting port to bind to [9999]
|
183
|
+
|
184
|
+
This is the starting (or only) port that ruby-scgi will use. If multi-port
|
185
|
+
clustering is used, all ports will be greater than this one.
|
186
|
+
|
187
|
+
-P, --pidfile Location of pid file [log/scgi.pid]
|
188
|
+
|
189
|
+
This is the pid file, relative to the working directory. The pid file is
|
190
|
+
necessary, as it is what is used to specify which pids to kill when stopping
|
191
|
+
or restarting. If incorrect information is in the pid file, the processes
|
192
|
+
won't be stopped when they should be, and you will probably won't be able
|
193
|
+
to start new processes (because the ports will still be in use).
|
194
|
+
|
195
|
+
-r, --processor Type of processor to use [Rails]
|
196
|
+
|
197
|
+
This is the type of processor to use, it defaults to Rails, as that is the
|
198
|
+
only one currently supported. Adding other processers is fairly easy, just
|
199
|
+
make the processor is in a file named XXXXXSCGIProcessor.rb (where XXXXX is
|
200
|
+
in the name of the processor), and that file is located in ruby's library
|
201
|
+
path (the RUBYLIB environment variable). See RailsSCGIProcessor.rb for an
|
202
|
+
example.
|
203
|
+
|
204
|
+
Each of the options can also be specified in the config file as a symbol. An
|
205
|
+
example config file would be:
|
206
|
+
|
207
|
+
---
|
208
|
+
:port: 4000
|
209
|
+
:fork: 3
|
210
|
+
|
211
|
+
This sets up a single-port cluster on port 4000 with 3 listening processes.
|
212
|
+
|
213
|
+
== Example configurations
|
214
|
+
|
215
|
+
Note that ruby-scgi is only tested on Lighttpd. Also, note that Lighttpd
|
216
|
+
1.4.16 has a bug which breaks redirects using server.error-handler-404, so
|
217
|
+
either use mod_magnet, wait for 1.4.17, or apply the patch in ticket
|
218
|
+
1270 on Lighttpd's Trac.
|
219
|
+
|
220
|
+
Lighttpd:
|
221
|
+
|
222
|
+
server.modules = ( ... "mod_scgi" ... )
|
223
|
+
server.error-handler-404 = "/dispatch.scgi"
|
224
|
+
|
225
|
+
# For Single Process or Single-Port Clustering
|
226
|
+
scgi.server = ( "dispatch.scgi" => (
|
227
|
+
"server1" => (
|
228
|
+
"host" => "127.0.0.1",
|
229
|
+
"port" => 9999,
|
230
|
+
"check-local" => "disable",
|
231
|
+
"disable-time" => 0)
|
232
|
+
))
|
233
|
+
|
234
|
+
# For Multi-Port Clustering
|
235
|
+
scgi.server = ( "dispatch.scgi" => (
|
236
|
+
"server1" => (
|
237
|
+
"host" => "127.0.0.1",
|
238
|
+
"port" => 9997,
|
239
|
+
"check-local" => "disable",
|
240
|
+
"disable-time" => 0),
|
241
|
+
"server2" => (
|
242
|
+
"host" => "127.0.0.1",
|
243
|
+
"port" => 9998,
|
244
|
+
"check-local" => "disable",
|
245
|
+
"disable-time" => 0),
|
246
|
+
"server3" => (
|
247
|
+
"host" => "127.0.0.1",
|
248
|
+
"port" => 9999,
|
249
|
+
"check-local" => "disable",
|
250
|
+
"disable-time" => 0)
|
251
|
+
))
|
252
|
+
|
253
|
+
Apache:
|
254
|
+
|
255
|
+
<VirtualHost your-ip:80>
|
256
|
+
AddDefaultCharset utf-8
|
257
|
+
ServerName www.yourdomain
|
258
|
+
DocumentRoot /your-switchtower-root/current/public
|
259
|
+
ErrorDocument 500 /500.html
|
260
|
+
ErrorDocument 404 /404.html
|
261
|
+
# handle all requests throug SCGI
|
262
|
+
SCGIMount / 127.0.0.1:9999
|
263
|
+
# matches locations with a dot following at least one more characters,
|
264
|
+
# that is, things like *,html, *.css, *.js, which should be delivered
|
265
|
+
# directly from the filesystem
|
266
|
+
<LocationMatch \..+$>
|
267
|
+
# don't handle those with SCGI
|
268
|
+
SCGIHandler Off
|
269
|
+
</LocationMatch>
|
270
|
+
<Directory /your-switchtower-root/current/public/>
|
271
|
+
Options +FollowSymLinks
|
272
|
+
Order allow,deny
|
273
|
+
allow from all
|
274
|
+
</Directory>
|
275
|
+
</VirtualHost>
|
276
|
+
|
277
|
+
== Security
|
278
|
+
|
279
|
+
Alright, listen up. I'm not gonna have people trying to take me to court
|
280
|
+
because they think I didn't tell them about security problems. Here are the
|
281
|
+
main attack vectors you should be aware of when running this:
|
282
|
+
|
283
|
+
* POSIX signals are bad if you're in a shared hosting setup that configures all
|
284
|
+
processes to run as a common user like *nobody*. If your provider does this
|
285
|
+
then you should use something else or find a better provider.
|
286
|
+
* The config file, pid file, and log file and directories should have
|
287
|
+
appropriate permissions. The config file should ideally be readable but not
|
288
|
+
writable by the user running scgi_ctrl. The pid and log file directory
|
289
|
+
should be writable by the user running scgi_ctrl and by no other user. If
|
290
|
+
the config file is writable by a non-trusted user, they could potentially
|
291
|
+
run arbitrary code, and they could certainly open arbitrary ports and or
|
292
|
+
attempt denial of service. If the pid file is writable by a non-trusted
|
293
|
+
user, it could cause arbitrary processes to be killed by the user running
|
294
|
+
scgi_ctrl
|
295
|
+
* Never run scgi_ctrl as root. If you don't know why you should read up about
|
296
|
+
the unix security model before deploying any more software.
|
297
|
+
|
298
|
+
== Changes from previous versions
|
299
|
+
|
300
|
+
* Single-port clustering is back
|
301
|
+
* scgi_ctrl is fully configurable on the command line
|
302
|
+
* Clustering and processing are now built into scgi_ctrl
|
303
|
+
* DRb, Win32, and throttling are no longer supported
|
304
|
+
* Soft reconfiguration is no longer supported (no SIGUSR1)
|
305
|
+
* Restarting via SIGHUP is no longer supported (SIGHUP is the same as SIGINT)
|
306
|
+
* The only commands available to scgi_ctrl are start and stop
|
307
|
+
|
308
|
+
== FAQ
|
309
|
+
|
310
|
+
Q: Have you been living under a rock for the last two years? Mongrel/Nginx is
|
311
|
+
the new hotness!
|
312
|
+
|
313
|
+
A: Well, aren't you snotty. You can certainly use Mongrel if you want. The
|
314
|
+
memory/performance differences are small, and it is probably better maintained.
|
315
|
+
ruby-scgi may have simpler clustering, and may be useful for certain legacy
|
316
|
+
setups. Also, it works well and it's been working for me for the last few
|
317
|
+
years, so I haven't felt the need to change.
|
318
|
+
|
319
|
+
Q: Does it work with Capistrano yet?
|
320
|
+
|
321
|
+
A: I haven't tried. If you have luck, let me know.
|
322
|
+
|
323
|
+
Q: Is there an easy way to reload? I don't want to take the whole thing down
|
324
|
+
just to deploy new code.
|
325
|
+
|
326
|
+
A: scgi_ctrl always uses SIGINT to stop processes, which allows existing
|
327
|
+
clients to finish. Restarting may have problems if a lot of clients are
|
328
|
+
connected and the processes can't shutdown before the new listening socket is
|
329
|
+
bound. This hasn't been a problem for me yet, but it is a possible race
|
330
|
+
condition. In case it affects you, you may have to run scgi_ctrl start again
|
331
|
+
if you get an error binding to a port.
|
data/bin/scgi_ctrl
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
require 'getoptlong'
|
3
|
+
require 'yaml'
|
4
|
+
SCGI_DEFAULT_CONFIG = {:pidfile=>'log/scgi.pid', :number=>1, :port=>9999,
|
5
|
+
:processor=>'Rails', :fork=>1, :logfile=>'log/scgi.log', :maxconns=>2**30-1,
|
6
|
+
:bind=>'127.0.0.1', :command=>''}
|
7
|
+
SCGI_CONFIG = {}
|
8
|
+
|
9
|
+
def config_file_options(filename)
|
10
|
+
begin
|
11
|
+
config = YAML.load(File.read(filename))
|
12
|
+
config.is_a?(Hash) ? config : Hash.new
|
13
|
+
rescue
|
14
|
+
Hash.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_processor(string)
|
19
|
+
raise NameError, "#{string} is not a valid processor!" unless string =~ /\A[A-Z][A-ZA-z]*\z/
|
20
|
+
processor = "#{string}SCGIProcessor"
|
21
|
+
require processor
|
22
|
+
eval(processor)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_options
|
26
|
+
config = {:config => 'config/scgi.yaml'}
|
27
|
+
opts = GetoptLong.new(
|
28
|
+
[ '--bind', '-b', GetoptLong::REQUIRED_ARGUMENT ],
|
29
|
+
[ '--config', '-c', GetoptLong::REQUIRED_ARGUMENT ],
|
30
|
+
[ '--directory', '-d', GetoptLong::REQUIRED_ARGUMENT ],
|
31
|
+
[ '--environment', '-e', GetoptLong::REQUIRED_ARGUMENT ],
|
32
|
+
[ '--fork', '-f', GetoptLong::REQUIRED_ARGUMENT ],
|
33
|
+
[ '--logfile', '-l', GetoptLong::REQUIRED_ARGUMENT ],
|
34
|
+
[ '--maxconns', '-m', GetoptLong::REQUIRED_ARGUMENT ],
|
35
|
+
[ '--number', '-n', GetoptLong::REQUIRED_ARGUMENT ],
|
36
|
+
[ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT ],
|
37
|
+
[ '--pidfile', '-P', GetoptLong::REQUIRED_ARGUMENT ],
|
38
|
+
[ '--processor', '-r', GetoptLong::REQUIRED_ARGUMENT ]
|
39
|
+
)
|
40
|
+
opts.each do |opt, arg|
|
41
|
+
case opt
|
42
|
+
when '--bind'
|
43
|
+
config[:bind] = arg
|
44
|
+
when '--config'
|
45
|
+
config[:config] = arg
|
46
|
+
when '--directory'
|
47
|
+
Dir.chdir(arg)
|
48
|
+
when '--environment'
|
49
|
+
config[:environment] = arg
|
50
|
+
when '--fork'
|
51
|
+
config[:fork] = arg.to_i
|
52
|
+
when '--logfile'
|
53
|
+
config[:logile] = arg
|
54
|
+
when '--maxconns'
|
55
|
+
config[:maxconns] = arg.to_i
|
56
|
+
when '--number'
|
57
|
+
config[:number] = arg.to_i
|
58
|
+
when '--port'
|
59
|
+
config[:port] = arg.to_i
|
60
|
+
when '--pidfile'
|
61
|
+
config[:pidfile] = arg
|
62
|
+
when '--processor'
|
63
|
+
config[:processor] = arg
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Configuration Precedence: Command Line > Config File > Default
|
68
|
+
SCGI_CONFIG.merge!(SCGI_DEFAULT_CONFIG)
|
69
|
+
SCGI_CONFIG.merge!(config_file_options(config[:config]))
|
70
|
+
SCGI_CONFIG.merge!(config)
|
71
|
+
end
|
72
|
+
|
73
|
+
def process(command)
|
74
|
+
case command
|
75
|
+
when 'start'
|
76
|
+
stop
|
77
|
+
start
|
78
|
+
when 'stop'
|
79
|
+
stop
|
80
|
+
else
|
81
|
+
puts usage
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def start
|
86
|
+
processor = get_processor(SCGI_CONFIG[:processor]).new(SCGI_CONFIG)
|
87
|
+
pids = []
|
88
|
+
SCGI_CONFIG[:number].times do |i|
|
89
|
+
socket = TCPServer.new(SCGI_CONFIG[:bind], port = SCGI_CONFIG[:port]+i)
|
90
|
+
SCGI_CONFIG[:fork].times do
|
91
|
+
if pid = fork
|
92
|
+
pids << pid
|
93
|
+
else
|
94
|
+
$0 = "scgi-#{SCGI_CONFIG[:processor]} dir:#{Dir.pwd} port:#{port}"
|
95
|
+
return processor.listen(socket)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
File.open(SCGI_CONFIG[:pidfile], 'wb'){|file| file.print("#{pids.join(' ')}")}
|
100
|
+
end
|
101
|
+
|
102
|
+
def stop
|
103
|
+
if File.file?(SCGI_CONFIG[:pidfile])
|
104
|
+
pids = nil
|
105
|
+
File.open(SCGI_CONFIG[:pidfile], 'rb'){|f| pids = f.read.split.collect{|x| x.to_i if x.to_i > 0}.compact}
|
106
|
+
if pids.length > 0
|
107
|
+
Process.kill("INT", *pids) rescue return nil
|
108
|
+
File.delete(SCGI_CONFIG[:pidfile])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def usage
|
114
|
+
<<-END
|
115
|
+
scgi_ctrl [option=value, ...] (start|stop)
|
116
|
+
Options:
|
117
|
+
-b, --bind IP address to bind to [127.0.0.1]
|
118
|
+
-c, --config Location of config file [config/scgi.yaml]
|
119
|
+
-d, --directory Working directory [.]
|
120
|
+
-e, --environment Environment (for Rails) [production]
|
121
|
+
-f, --fork Number of listners on each port [1]
|
122
|
+
-l, --logfile Location of log file [log/scgi.log]
|
123
|
+
-m, --maxconns Maximum number of concurrent users [2**30-1]
|
124
|
+
-n, --number Number of ports to bind to [1]
|
125
|
+
-p, --port Starting port to bind to [9999]
|
126
|
+
-P, --pidfile Location of pid file [log/scgi.pid]
|
127
|
+
-r, --processor Type of processor to use [Rails]
|
128
|
+
END
|
129
|
+
end
|
130
|
+
|
131
|
+
parse_options
|
132
|
+
process(ARGV[0])
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
require 'scgi'
|
3
|
+
|
4
|
+
# This SCGI::Processor subclass hooks the SCGI request into Ruby on Rails.
|
5
|
+
class RailsSCGIProcessor < SCGI::Processor
|
6
|
+
# Initialzes Rails with the appropriate environment and settings
|
7
|
+
def initialize(settings)
|
8
|
+
ENV["RAILS_ENV"] = settings[:environment] || 'production'
|
9
|
+
require "config/environment"
|
10
|
+
ActiveRecord::Base.threaded_connections = false
|
11
|
+
require 'dispatcher'
|
12
|
+
super(settings)
|
13
|
+
@guard = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Submits requests to Rails in a single threaded fashion
|
17
|
+
def process_request(request, body, socket)
|
18
|
+
return if socket.closed?
|
19
|
+
cgi = SCGI::CGIFixed.new(request, body, socket)
|
20
|
+
begin
|
21
|
+
@guard.synchronize{Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)}
|
22
|
+
rescue IOError
|
23
|
+
@log.error("received IOError #$! when handling client. Your web server doesn't like me.")
|
24
|
+
rescue Object => rails_error
|
25
|
+
@log.error("calling Dispatcher.dispatch", rails_error)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/scgi.rb
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'socket'
|
5
|
+
require 'cgi'
|
6
|
+
require 'monitor'
|
7
|
+
require 'singleton'
|
8
|
+
|
9
|
+
module SCGI
|
10
|
+
# A factory that makes Log objects, making sure that one Log is associated
|
11
|
+
# with each log file.
|
12
|
+
class LogFactory < Monitor
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
super()
|
17
|
+
@@logs = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(file)
|
21
|
+
synchronize{@@logs[file] ||= Log.new(file)}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# A simple Log class that has an info and error method for output
|
26
|
+
# messages to a log file. The main thing this logger does is
|
27
|
+
# include the process ID in the logs so that you can see which child
|
28
|
+
# is creating each message.
|
29
|
+
class Log < Monitor
|
30
|
+
def initialize(file)
|
31
|
+
super()
|
32
|
+
@out = open(file, "a+")
|
33
|
+
@out.sync = true
|
34
|
+
@pid = Process.pid
|
35
|
+
@info = "[INF][#{@pid}] "
|
36
|
+
@error = "[ERR][#{@pid}] "
|
37
|
+
end
|
38
|
+
|
39
|
+
def info(msg)
|
40
|
+
synchronize{@out.print("#{@info}#{msg}\n")}
|
41
|
+
end
|
42
|
+
|
43
|
+
# If an exception is given then it will print the exception and a stack trace.
|
44
|
+
def error(msg, exc=nil)
|
45
|
+
return synchronize{@out.print("#{@error}#{msg}\n")} unless exc
|
46
|
+
synchronize{@out.print("#{@error}#{msg}: #{exc}\n#{exc.backtrace.join("\n")}\n")}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Modifies CGI so that we can use it. Main thing it does is expose
|
51
|
+
# the stdinput and stdoutput so SCGI::Processor can connect them to
|
52
|
+
# the right sources. It also exposes the env_table so that SCGI::Processor
|
53
|
+
# and hook the SCGI parameters into the environment table.
|
54
|
+
class CGIFixed < ::CGI
|
55
|
+
public :env_table
|
56
|
+
attr_reader :args, :env_table
|
57
|
+
|
58
|
+
def initialize(params, data, out, *args)
|
59
|
+
@env_table = params
|
60
|
+
@args = *args
|
61
|
+
@input = StringIO.new(data)
|
62
|
+
@out = out
|
63
|
+
super(*args)
|
64
|
+
end
|
65
|
+
|
66
|
+
def stdinput
|
67
|
+
@input
|
68
|
+
end
|
69
|
+
|
70
|
+
def stdoutput
|
71
|
+
@out
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# This is the complete guts of the SCGI system. It is designed so that
|
76
|
+
# people can take it and implement it for their own systems, not just
|
77
|
+
# Ruby on Rails. This implementation is not complete since you must
|
78
|
+
# create your own that implements the process_request method.
|
79
|
+
#
|
80
|
+
# The SCGI protocol only works with TCP/IP sockets and not domain sockets.
|
81
|
+
# It might be useful for shared hosting people to have domain sockets, but
|
82
|
+
# they aren't supported in Apache, and in lighttpd they're unreliable.
|
83
|
+
# Also, domain sockets don't work so well on Windows.
|
84
|
+
class Processor < Monitor
|
85
|
+
attr_reader :settings
|
86
|
+
|
87
|
+
def initialize(settings = {})
|
88
|
+
@total_conns = 0
|
89
|
+
@shutdown = false
|
90
|
+
@dead = false
|
91
|
+
@threads = Queue.new
|
92
|
+
@settings = settings
|
93
|
+
@log = LogFactory.instance.create(settings[:logfile])
|
94
|
+
@host = settings[:bind]
|
95
|
+
@port = settings[:port]
|
96
|
+
@maxconns = settings[:maxconns]
|
97
|
+
super()
|
98
|
+
setup_signals
|
99
|
+
end
|
100
|
+
|
101
|
+
# Starts the SCGI::Processor having it listen on either the
|
102
|
+
# given socket or listening to a new socket on the @host/@port
|
103
|
+
# configured. The option to give listen a socket is there so
|
104
|
+
# that others can create one socket, and then fork several processors
|
105
|
+
# to listen to it.
|
106
|
+
#
|
107
|
+
# This function does not return until a shutdown.
|
108
|
+
def listen(socket = nil)
|
109
|
+
@log.info("Started listening on #{@host}:#{@port} at #{@started = Time.now}")
|
110
|
+
@socket = socket || TCPServer.new(@host, @port)
|
111
|
+
|
112
|
+
# we also need a small collector thread that does nothing
|
113
|
+
# but pull threads off the thread queue and joins them
|
114
|
+
@collector = Thread.new do
|
115
|
+
while t = @threads.shift
|
116
|
+
collect_thread(t)
|
117
|
+
@total_conns += 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
thread = Thread.new do
|
122
|
+
loop do
|
123
|
+
handle_client(@socket.accept)
|
124
|
+
break if @shutdown and @threads.length <= 0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# and then collect the listener thread which blocks until it exits
|
129
|
+
collect_thread(thread)
|
130
|
+
|
131
|
+
@socket.close unless @socket.closed?
|
132
|
+
@dead = true
|
133
|
+
@log.info("Exited accept loop. Shutdown complete.")
|
134
|
+
end
|
135
|
+
|
136
|
+
def collect_thread(thread)
|
137
|
+
begin
|
138
|
+
thread.join
|
139
|
+
rescue Interrupt
|
140
|
+
@log.info("Shutting down from SIGINT.")
|
141
|
+
rescue IOError
|
142
|
+
@log.error("received IOError #$!. Web server may possibly be configured wrong.")
|
143
|
+
rescue Object
|
144
|
+
@log.error("Collecting thread", $!)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Internal function that handles a new client connection.
|
149
|
+
# It spawns a thread to handle the client and registers it in the
|
150
|
+
# @threads queue. A collector thread is responsible for joining these
|
151
|
+
# and clearing them out. This design is needed because Ruby's GC
|
152
|
+
# doesn't seem to deal with threads as well as others believe.
|
153
|
+
#
|
154
|
+
# Depending on how your system works, you may need to synchronize
|
155
|
+
# inside your process_request implementation. scgi_rails.rb
|
156
|
+
# does this so that Rails will run as if it were single threaded.
|
157
|
+
#
|
158
|
+
# It also handles calculating the current and total connections,
|
159
|
+
# and deals with the graceful shutdown. The important part
|
160
|
+
# of graceful shutdown is that new requests get redirected to
|
161
|
+
# the /busy.html file.
|
162
|
+
#
|
163
|
+
def handle_client(socket)
|
164
|
+
# ruby's GC seems to do weird things if we don't assign the thread to a local variable
|
165
|
+
@threads << Thread.new do
|
166
|
+
begin
|
167
|
+
len = ""
|
168
|
+
# we only read 10 bytes of the length. any request longer than this is invalid
|
169
|
+
while len.length <= 10
|
170
|
+
c = socket.read(1)
|
171
|
+
break if c == ':' # found the terminal, len now has a length in it so read the payload
|
172
|
+
len << c
|
173
|
+
end
|
174
|
+
|
175
|
+
# we should now either have a payload length to get
|
176
|
+
payload = socket.read(len.to_i)
|
177
|
+
if socket.read(1) == ','
|
178
|
+
read_header(socket, payload)
|
179
|
+
else
|
180
|
+
@log.error("Malformed request, does not end with ','")
|
181
|
+
end
|
182
|
+
rescue Object
|
183
|
+
@log.error("Handling client", $!)
|
184
|
+
ensure
|
185
|
+
# no matter what we have to put this thread on the bad list
|
186
|
+
socket.close if not socket.closed?
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Does three jobs: reads and parses the SCGI netstring header,
|
192
|
+
# reads any content off the socket, and then either calls process_request
|
193
|
+
# or immediately returns a redirect to /busy.html for some connections.
|
194
|
+
#
|
195
|
+
# The browser/connection that will be redirected to /busy.html if
|
196
|
+
# either SCGI::Processor is in the middle of a shutdown, or if the
|
197
|
+
# number of connections is over the @maxconns. This redirect is
|
198
|
+
# immediate and doesn't run inside the interpreter, so it will happen with
|
199
|
+
# much less processing and help keep your system responsive.
|
200
|
+
def read_header(socket, payload)
|
201
|
+
return if socket.closed?
|
202
|
+
request = Hash[*(payload.split("\0"))]
|
203
|
+
if request["CONTENT_LENGTH"]
|
204
|
+
length = request["CONTENT_LENGTH"].to_i
|
205
|
+
body = length > 0 ? socket.read(length) : ''
|
206
|
+
|
207
|
+
if @shutdown or @threads.length > @maxconns
|
208
|
+
socket.write("Location: /busy.html\r\nCache-control: no-cache, must-revalidate\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\nStatus: 307 Temporary Redirect\r\n\r\n")
|
209
|
+
else
|
210
|
+
process_request(request, body, socket)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# You must implement this yourself. The request is a Hash
|
216
|
+
# of the CGI parameters from the webserver. The body is the
|
217
|
+
# raw CGI body. The socket is where you write your results
|
218
|
+
# (properly HTTP formatted) back to the webserver.
|
219
|
+
def process_request(request, body, socket)
|
220
|
+
raise "You must implement process_request"
|
221
|
+
end
|
222
|
+
|
223
|
+
# Sets up the POSIX signals:
|
224
|
+
#
|
225
|
+
# * TERM -- Forced shutdown.
|
226
|
+
# * INT -- Graceful shutdown.
|
227
|
+
# * HUP -- Graceful shutdown.
|
228
|
+
# * USR2 -- Dumps status info to the logs. Super ugly.
|
229
|
+
def setup_signals
|
230
|
+
trap("TERM") { @log.info("SIGTERM, forced shutdown."); shutdown(force=true) }
|
231
|
+
trap("INT") { @log.info("SIGINT, graceful shutdown started."); shutdown }
|
232
|
+
trap("HUP") { @log.info("SIGHUP, graceful shutdown started."); shutdown }
|
233
|
+
trap("USR2") { @log.info(status_info) }
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns a Hash with status information. This is used
|
237
|
+
# when dumping data to the logs
|
238
|
+
def status_info
|
239
|
+
{
|
240
|
+
:time => Time.now, :pid => Process.pid, :settings => @settings,
|
241
|
+
:environment => @settings[:environment], :started => @started,
|
242
|
+
:max_conns => @maxconns, :conns => @threads.length, :systimes => Process.times,
|
243
|
+
:shutdown => @shutdown, :dead => @dead, :total_conns => @total_conns
|
244
|
+
}.inspect
|
245
|
+
end
|
246
|
+
|
247
|
+
# When called it will set the @shutdown flag indicating to the
|
248
|
+
# SCGI::Processor.listen function that all new connections should
|
249
|
+
# be set to /busy.html, and all current connections should be
|
250
|
+
# "valved" off. Once all the current connections are gone the
|
251
|
+
# SCGI::Processor.listen function will exit.
|
252
|
+
#
|
253
|
+
# Use the force=true parameter to force an immediate shutdown.
|
254
|
+
# This is done by closing the listening socket, so it's rather
|
255
|
+
# violent.
|
256
|
+
def shutdown(force = false)
|
257
|
+
synchronize do
|
258
|
+
@shutdown = true
|
259
|
+
|
260
|
+
if @threads.length == 0
|
261
|
+
@log.info("Immediate shutdown since nobody is connected.")
|
262
|
+
@socket.close
|
263
|
+
elsif force
|
264
|
+
@log.info("Forcing shutdown. You may see exceptions.")
|
265
|
+
@socket.close
|
266
|
+
else
|
267
|
+
@log.info("Shutdown requested. Beginning graceful shutdown with #{@threads.length} connected.")
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: scgi
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.6.0
|
7
|
+
date: 2007-08-13 00:00:00 -07:00
|
8
|
+
summary: Simple support for using SCGI in ruby apps, such as Rails
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: code@jeremyevans.net
|
12
|
+
homepage:
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Jeremy Evans
|
31
|
+
files:
|
32
|
+
- COPYING
|
33
|
+
- README
|
34
|
+
- lib/scgi.rb
|
35
|
+
- lib/RailsSCGIProcessor.rb
|
36
|
+
test_files: []
|
37
|
+
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
executables:
|
43
|
+
- scgi_ctrl
|
44
|
+
extensions: []
|
45
|
+
|
46
|
+
requirements: []
|
47
|
+
|
48
|
+
dependencies: []
|
49
|
+
|