rackup 1.0.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,462 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+
6
+ require 'optparse'
7
+ require 'fileutils'
8
+
9
+ require 'rack/builder'
10
+ require 'rack/common_logger'
11
+ require 'rack/content_length'
12
+ require 'rack/show_exceptions'
13
+ require 'rack/lint'
14
+ require 'rack/tempfile_reaper'
15
+
16
+ require 'rack/version'
17
+
18
+ require_relative 'version'
19
+ require_relative 'handler'
20
+
21
+ module Rackup
22
+ class Server
23
+ class Options
24
+ def parse!(args)
25
+ options = {}
26
+ opt_parser = OptionParser.new("", 24, ' ') do |opts|
27
+ opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
28
+
29
+ opts.separator ""
30
+ opts.separator "Ruby options:"
31
+
32
+ lineno = 1
33
+ opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
34
+ eval line, TOPLEVEL_BINDING, "-e", lineno
35
+ lineno += 1
36
+ }
37
+
38
+ opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
39
+ options[:debug] = true
40
+ }
41
+ opts.on("-w", "--warn", "turn warnings on for your script") {
42
+ options[:warn] = true
43
+ }
44
+ opts.on("-q", "--quiet", "turn off logging") {
45
+ options[:quiet] = true
46
+ }
47
+
48
+ opts.on("-I", "--include PATH",
49
+ "specify $LOAD_PATH (may be used more than once)") { |path|
50
+ (options[:include] ||= []).concat(path.split(":"))
51
+ }
52
+
53
+ opts.on("-r", "--require LIBRARY",
54
+ "require the library, before executing your script") { |library|
55
+ (options[:require] ||= []) << library
56
+ }
57
+
58
+ opts.separator ""
59
+ opts.separator "Rack options:"
60
+ opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
61
+ options[:builder] = line
62
+ }
63
+
64
+ opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick)") { |s|
65
+ options[:server] = s
66
+ }
67
+
68
+ opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host|
69
+ options[:Host] = host
70
+ }
71
+
72
+ opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
73
+ options[:Port] = port
74
+ }
75
+
76
+ opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name|
77
+ name, value = name.split('=', 2)
78
+ value = true if value.nil?
79
+ options[name.to_sym] = value
80
+ }
81
+
82
+ opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
83
+ options[:environment] = e
84
+ }
85
+
86
+ opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
87
+ options[:daemonize] ||= true
88
+ }
89
+
90
+ opts.on("--daemonize-noclose", "run daemonized in the background without closing stdout/stderr") {
91
+ options[:daemonize] = :noclose
92
+ }
93
+
94
+ opts.on("-P", "--pid FILE", "file to store PID") { |f|
95
+ options[:pid] = ::File.expand_path(f)
96
+ }
97
+
98
+ opts.separator ""
99
+ opts.separator "Profiling options:"
100
+
101
+ opts.on("--heap HEAPFILE", "Build the application, then dump the heap to HEAPFILE") do |e|
102
+ options[:heapfile] = e
103
+ end
104
+
105
+ opts.on("--profile PROFILE", "Dump CPU or Memory profile to PROFILE (defaults to a tempfile)") do |e|
106
+ options[:profile_file] = e
107
+ end
108
+
109
+ opts.on("--profile-mode MODE", "Profile mode (cpu|wall|object)") do |e|
110
+ unless %w[cpu wall object].include?(e)
111
+ raise OptionParser::InvalidOption, "unknown profile mode: #{e}"
112
+ end
113
+ options[:profile_mode] = e.to_sym
114
+ end
115
+
116
+ opts.separator ""
117
+ opts.separator "Common options:"
118
+
119
+ opts.on_tail("-h", "-?", "--help", "Show this message") do
120
+ puts opts
121
+ puts handler_opts(options)
122
+
123
+ exit
124
+ end
125
+
126
+ opts.on_tail("--version", "Show version") do
127
+ puts "Rack #{Rack::RELEASE}"
128
+ exit
129
+ end
130
+ end
131
+
132
+ begin
133
+ opt_parser.parse! args
134
+ rescue OptionParser::InvalidOption => e
135
+ warn e.message
136
+ abort opt_parser.to_s
137
+ end
138
+
139
+ options[:config] = args.last if args.last && !args.last.empty?
140
+ options
141
+ end
142
+
143
+ def handler_opts(options)
144
+ info = []
145
+ server = Rackup::Handler.get(options[:server]) || Rackup::Handler.default
146
+ if server && server.respond_to?(:valid_options)
147
+ info << ""
148
+ info << "Server-specific options for #{server.name}:"
149
+
150
+ has_options = false
151
+ server.valid_options.each do |name, description|
152
+ next if /^(Host|Port)[^a-zA-Z]/.match?(name.to_s) # ignore handler's host and port options, we do our own.
153
+ info << sprintf(" -O %-21s %s", name, description)
154
+ has_options = true
155
+ end
156
+ return "" if !has_options
157
+ end
158
+ info.join("\n")
159
+ rescue NameError, LoadError
160
+ return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
161
+ end
162
+ end
163
+
164
+ # Start a new rack server (like running rackup). This will parse ARGV and
165
+ # provide standard ARGV rackup options, defaulting to load 'config.ru'.
166
+ #
167
+ # Providing an options hash will prevent ARGV parsing and will not include
168
+ # any default options.
169
+ #
170
+ # This method can be used to very easily launch a CGI application, for
171
+ # example:
172
+ #
173
+ # Rack::Server.start(
174
+ # :app => lambda do |e|
175
+ # [200, {'content-type' => 'text/html'}, ['hello world']]
176
+ # end,
177
+ # :server => 'cgi'
178
+ # )
179
+ #
180
+ # Further options available here are documented on Rack::Server#initialize
181
+ def self.start(options = nil)
182
+ new(options).start
183
+ end
184
+
185
+ attr_writer :options
186
+
187
+ # Options may include:
188
+ # * :app
189
+ # a rack application to run (overrides :config and :builder)
190
+ # * :builder
191
+ # a string to evaluate a Rack::Builder from
192
+ # * :config
193
+ # a rackup configuration file path to load (.ru)
194
+ # * :environment
195
+ # this selects the middleware that will be wrapped around
196
+ # your application. Default options available are:
197
+ # - development: CommonLogger, ShowExceptions, and Lint
198
+ # - deployment: CommonLogger
199
+ # - none: no extra middleware
200
+ # note: when the server is a cgi server, CommonLogger is not included.
201
+ # * :server
202
+ # choose a specific Rackup::Handler, e.g. cgi, fcgi, webrick
203
+ # * :daemonize
204
+ # if truthy, the server will daemonize itself (fork, detach, etc)
205
+ # if :noclose, the server will not close STDOUT/STDERR
206
+ # * :pid
207
+ # path to write a pid file after daemonize
208
+ # * :Host
209
+ # the host address to bind to (used by supporting Rackup::Handler)
210
+ # * :Port
211
+ # the port to bind to (used by supporting Rackup::Handler)
212
+ # * :AccessLog
213
+ # webrick access log options (or supporting Rackup::Handler)
214
+ # * :debug
215
+ # turn on debug output ($DEBUG = true)
216
+ # * :warn
217
+ # turn on warnings ($-w = true)
218
+ # * :include
219
+ # add given paths to $LOAD_PATH
220
+ # * :require
221
+ # require the given libraries
222
+ #
223
+ # Additional options for profiling app initialization include:
224
+ # * :heapfile
225
+ # location for ObjectSpace.dump_all to write the output to
226
+ # * :profile_file
227
+ # location for CPU/Memory (StackProf) profile output (defaults to a tempfile)
228
+ # * :profile_mode
229
+ # StackProf profile mode (cpu|wall|object)
230
+ def initialize(options = nil)
231
+ @ignore_options = []
232
+
233
+ if options
234
+ @use_default_options = false
235
+ @options = options
236
+ @app = options[:app] if options[:app]
237
+ else
238
+ @use_default_options = true
239
+ @options = parse_options(ARGV)
240
+ end
241
+ end
242
+
243
+ def options
244
+ merged_options = @use_default_options ? default_options.merge(@options) : @options
245
+ merged_options.reject { |k, v| @ignore_options.include?(k) }
246
+ end
247
+
248
+ def default_options
249
+ environment = ENV['RACK_ENV'] || 'development'
250
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
251
+
252
+ {
253
+ environment: environment,
254
+ pid: nil,
255
+ Port: 9292,
256
+ Host: default_host,
257
+ AccessLog: [],
258
+ config: "config.ru"
259
+ }
260
+ end
261
+
262
+ def app
263
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
264
+ end
265
+
266
+ class << self
267
+ def logging_middleware
268
+ lambda { |server|
269
+ /CGI/.match?(server.server.name) || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr]
270
+ }
271
+ end
272
+
273
+ def default_middleware_by_environment
274
+ m = Hash.new {|h, k| h[k] = []}
275
+ m["deployment"] = [
276
+ [Rack::ContentLength],
277
+ logging_middleware,
278
+ [Rack::TempfileReaper]
279
+ ]
280
+ m["development"] = [
281
+ [Rack::ContentLength],
282
+ logging_middleware,
283
+ [Rack::ShowExceptions],
284
+ [Rack::Lint],
285
+ [Rack::TempfileReaper]
286
+ ]
287
+
288
+ m
289
+ end
290
+
291
+ def middleware
292
+ default_middleware_by_environment
293
+ end
294
+ end
295
+
296
+ def middleware
297
+ self.class.middleware
298
+ end
299
+
300
+ def start(&block)
301
+ if options[:warn]
302
+ $-w = true
303
+ end
304
+
305
+ if includes = options[:include]
306
+ $LOAD_PATH.unshift(*includes)
307
+ end
308
+
309
+ Array(options[:require]).each do |library|
310
+ require library
311
+ end
312
+
313
+ if options[:debug]
314
+ $DEBUG = true
315
+ require 'pp'
316
+ p options[:server]
317
+ pp wrapped_app
318
+ pp app
319
+ end
320
+
321
+ check_pid! if options[:pid]
322
+
323
+ # Touch the wrapped app, so that the config.ru is loaded before
324
+ # daemonization (i.e. before chdir, etc).
325
+ handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
326
+ wrapped_app
327
+ end
328
+
329
+ daemonize_app if options[:daemonize]
330
+
331
+ write_pid if options[:pid]
332
+
333
+ trap(:INT) do
334
+ if server.respond_to?(:shutdown)
335
+ server.shutdown
336
+ else
337
+ exit
338
+ end
339
+ end
340
+
341
+ server.run(wrapped_app, **options, &block)
342
+ end
343
+
344
+ def server
345
+ @_server ||= Handler.get(options[:server]) || Handler.default
346
+ end
347
+
348
+ private
349
+ def build_app_and_options_from_config
350
+ if !::File.exist? options[:config]
351
+ abort "configuration #{options[:config]} not found"
352
+ end
353
+
354
+ return Rack::Builder.parse_file(self.options[:config])
355
+ end
356
+
357
+ def handle_profiling(heapfile, profile_mode, filename)
358
+ if heapfile
359
+ require "objspace"
360
+ ObjectSpace.trace_object_allocations_start
361
+ yield
362
+ GC.start
363
+ ::File.open(heapfile, "w") { |f| ObjectSpace.dump_all(output: f) }
364
+ exit
365
+ end
366
+
367
+ if profile_mode
368
+ require "stackprof"
369
+ require "tempfile"
370
+
371
+ make_profile_name(filename) do |filename|
372
+ ::File.open(filename, "w") do |f|
373
+ StackProf.run(mode: profile_mode, out: f) do
374
+ yield
375
+ end
376
+ puts "Profile written to: #{filename}"
377
+ end
378
+ end
379
+ exit
380
+ end
381
+
382
+ yield
383
+ end
384
+
385
+ def make_profile_name(filename)
386
+ if filename
387
+ yield filename
388
+ else
389
+ ::Dir::Tmpname.create("profile.dump") do |tmpname, _, _|
390
+ yield tmpname
391
+ end
392
+ end
393
+ end
394
+
395
+ def build_app_from_string
396
+ Rack::Builder.new_from_string(self.options[:builder])
397
+ end
398
+
399
+ def parse_options(args)
400
+ # Don't evaluate CGI ISINDEX parameters.
401
+ args.clear if ENV.include?(Rack::REQUEST_METHOD)
402
+
403
+ @options = opt_parser.parse!(args)
404
+ @options[:config] = ::File.expand_path(options[:config])
405
+ ENV["RACK_ENV"] = options[:environment]
406
+ @options
407
+ end
408
+
409
+ def opt_parser
410
+ Options.new
411
+ end
412
+
413
+ def build_app(app)
414
+ middleware[options[:environment]].reverse_each do |middleware|
415
+ middleware = middleware.call(self) if middleware.respond_to?(:call)
416
+ next unless middleware
417
+ klass, *args = middleware
418
+ app = klass.new(app, *args)
419
+ end
420
+ app
421
+ end
422
+
423
+ def wrapped_app
424
+ @wrapped_app ||= build_app app
425
+ end
426
+
427
+ def daemonize_app
428
+ # Cannot be covered as it forks
429
+ # :nocov:
430
+ Process.daemon(true, options[:daemonize] == :noclose)
431
+ # :nocov:
432
+ end
433
+
434
+ def write_pid
435
+ ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") }
436
+ at_exit { ::FileUtils.rm_f(options[:pid]) }
437
+ rescue Errno::EEXIST
438
+ check_pid!
439
+ retry
440
+ end
441
+
442
+ def check_pid!
443
+ return unless ::File.exist?(options[:pid])
444
+
445
+ pid = ::File.read(options[:pid]).to_i
446
+ raise Errno::ESRCH if pid == 0
447
+
448
+ Process.kill(0, pid)
449
+ exit_with_pid(pid)
450
+ rescue Errno::ESRCH
451
+ ::File.delete(options[:pid])
452
+ rescue Errno::EPERM
453
+ exit_with_pid(pid)
454
+ end
455
+
456
+ def exit_with_pid(pid)
457
+ $stderr.puts "A server is already running (pid: #{pid}, file: #{options[:pid]})."
458
+ exit(1)
459
+ end
460
+ end
461
+
462
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023-2024, by Samuel Williams.
5
+
6
+ module Rackup
7
+ # The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be “ASCII-8BIT” and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.
8
+ class Stream
9
+ def initialize(input = nil, output = Buffered.new)
10
+ @input = input
11
+ @output = output
12
+
13
+ raise ArgumentError, "Non-writable output!" unless output.respond_to?(:write)
14
+
15
+ # Will hold remaining data in `#read`.
16
+ @buffer = nil
17
+ @closed = false
18
+ end
19
+
20
+ attr :input
21
+ attr :output
22
+
23
+ # This provides a read-only interface for data, which is surprisingly tricky to implement correctly.
24
+ module Reader
25
+ # rack.hijack_io must respond to:
26
+ # read, write, read_nonblock, write_nonblock, flush, close, close_read, close_write, closed?
27
+
28
+ # read behaves like IO#read. Its signature is read([length, [buffer]]). If given, length must be a non-negative Integer (>= 0) or nil, and buffer must be a String and may not be nil. If length is given and not nil, then this method reads at most length bytes from the input stream. If length is not given or nil, then this method reads all data until EOF. When EOF is reached, this method returns nil if length is given and not nil, or “” if length is not given or is nil. If buffer is given, then the read data will be placed into buffer instead of a newly created String object.
29
+ # @param length [Integer] the amount of data to read
30
+ # @param buffer [String] the buffer which will receive the data
31
+ # @return a buffer containing the data
32
+ def read(length = nil, buffer = nil)
33
+ return '' if length == 0
34
+
35
+ buffer ||= String.new.force_encoding(Encoding::BINARY)
36
+
37
+ # Take any previously buffered data and replace it into the given buffer.
38
+ if @buffer
39
+ buffer.replace(@buffer)
40
+ @buffer = nil
41
+ else
42
+ buffer.clear
43
+ end
44
+
45
+ if length
46
+ while buffer.bytesize < length and chunk = read_next
47
+ buffer << chunk
48
+ end
49
+
50
+ # This ensures the subsequent `slice!` works correctly.
51
+ buffer.force_encoding(Encoding::BINARY)
52
+
53
+ # This will be at least one copy:
54
+ @buffer = buffer.byteslice(length, buffer.bytesize)
55
+
56
+ # This should be zero-copy:
57
+ buffer.slice!(length, buffer.bytesize)
58
+
59
+ if buffer.empty?
60
+ return nil
61
+ else
62
+ return buffer
63
+ end
64
+ else
65
+ while chunk = read_next
66
+ buffer << chunk
67
+ end
68
+
69
+ return buffer
70
+ end
71
+ end
72
+
73
+ # Read at most `length` bytes from the stream. Will avoid reading from the underlying stream if possible.
74
+ def read_partial(length = nil)
75
+ if @buffer
76
+ buffer = @buffer
77
+ @buffer = nil
78
+ else
79
+ buffer = read_next
80
+ end
81
+
82
+ if buffer and length
83
+ if buffer.bytesize > length
84
+ # This ensures the subsequent `slice!` works correctly.
85
+ buffer.force_encoding(Encoding::BINARY)
86
+
87
+ @buffer = buffer.byteslice(length, buffer.bytesize)
88
+ buffer.slice!(length, buffer.bytesize)
89
+ end
90
+ end
91
+
92
+ return buffer
93
+ end
94
+
95
+ def gets
96
+ read_partial
97
+ end
98
+
99
+ def each
100
+ while chunk = read_partial
101
+ yield chunk
102
+ end
103
+ end
104
+
105
+ def read_nonblock(length, buffer = nil)
106
+ @buffer ||= read_next
107
+ chunk = nil
108
+
109
+ unless @buffer
110
+ buffer&.clear
111
+ return
112
+ end
113
+
114
+ if @buffer.bytesize > length
115
+ chunk = @buffer.byteslice(0, length)
116
+ @buffer = @buffer.byteslice(length, @buffer.bytesize)
117
+ else
118
+ chunk = @buffer
119
+ @buffer = nil
120
+ end
121
+
122
+ if buffer
123
+ buffer.replace(chunk)
124
+ else
125
+ buffer = chunk
126
+ end
127
+
128
+ return buffer
129
+ end
130
+ end
131
+
132
+ include Reader
133
+
134
+ def write(buffer)
135
+ if @output
136
+ @output.write(buffer)
137
+ return buffer.bytesize
138
+ else
139
+ raise IOError, "Stream is not writable, output has been closed!"
140
+ end
141
+ end
142
+
143
+ def write_nonblock(buffer)
144
+ write(buffer)
145
+ end
146
+
147
+ def <<(buffer)
148
+ write(buffer)
149
+ end
150
+
151
+ def flush
152
+ end
153
+
154
+ def close_read
155
+ @input&.close
156
+ @input = nil
157
+ end
158
+
159
+ # close must never be called on the input stream. huh?
160
+ def close_write
161
+ if @output.respond_to?(:close)
162
+ @output&.close
163
+ end
164
+
165
+ @output = nil
166
+ end
167
+
168
+ # Close the input and output bodies.
169
+ def close(error = nil)
170
+ self.close_read
171
+ self.close_write
172
+
173
+ return nil
174
+ ensure
175
+ @closed = true
176
+ end
177
+
178
+ # Whether the stream has been closed.
179
+ def closed?
180
+ @closed
181
+ end
182
+
183
+ # Whether there are any output chunks remaining?
184
+ def empty?
185
+ @output.empty?
186
+ end
187
+
188
+ private
189
+
190
+ def read_next
191
+ if @input
192
+ return @input.read
193
+ else
194
+ @input = nil
195
+ raise IOError, "Stream is not readable, input has been closed!"
196
+ end
197
+ end
198
+ end
199
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
5
 
6
6
  module Rackup
7
- VERSION = "1.0.1"
7
+ VERSION = "2.2.0"
8
8
  end
data/lib/rackup.rb CHANGED
@@ -3,4 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2022-2024, by Samuel Williams.
5
5
 
6
+ require_relative 'rackup/handler'
7
+ require_relative 'rackup/server'
6
8
  require_relative 'rackup/version'