scgi 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|