/
nyara.rb
291 lines (257 loc) · 7.57 KB
/
nyara.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# patch core classes first
require_relative "patches/mini_support"
# master require
require "fiber"
require "cgi"
require "uri"
require "openssl"
require "socket"
require "tilt"
require "time"
require "logger"
require_relative "../../ext/nyara"
require_relative "hashes/param_hash"
require_relative "hashes/header_hash"
require_relative "hashes/config_hash"
require_relative "mime_types"
require_relative "controller"
require_relative "request"
require_relative "cookie"
require_relative "session"
require_relative "flash"
require_relative "config"
require_relative "route"
require_relative "view"
require_relative "cpu_counter"
require_relative "part"
require_relative "command"
module Nyara
HTTP_STATUS_FIRST_LINES = Hash[HTTP_STATUS_CODES.map{|k,v|[k, "HTTP/1.1 #{k} #{v}\r\n".freeze]}].freeze
HTTP_REDIRECT_STATUS = [300, 301, 302, 303, 307]
# Base header response for 200<br>
# Caveat: these entries can not be deleted
OK_RESP_HEADER = HeaderHash.new
OK_RESP_HEADER['Content-Type'] = 'text/html; charset=UTF-8'
OK_RESP_HEADER['Cache-Control'] = 'no-cache'
OK_RESP_HEADER['Transfer-Encoding'] = 'chunked'
OK_RESP_HEADER['X-XSS-Protection'] = '1; mode=block'
OK_RESP_HEADER['X-Content-Type-Options'] = 'nosniff'
OK_RESP_HEADER['X-Frame-Options'] = 'SAMEORIGIN'
START_CTX = {
0 => $0.dup,
argv: ARGV.map(&:dup),
cwd: (begin
a = File.stat(pwd = ENV['PWD'])
b = File.stat(Dir.pwd)
a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
rescue
Dir.pwd
end)
}
class << self
def config
raise ArgumentError, 'block not accepted, did you mean Nyara::Config.config?' if block_given?
Config
end
%w[logger env production? test? development? project_path assets_path views_path public_path].each do |m|
eval <<-RUBY
def #{m} *xs
Config.#{m} *xs
end
RUBY
end
def setup
Session.init
Config.init
Route.compile
# todo lint if SomeController#request, send_header are re-defined
View.init
end
# load with Config['app_files']
def load_app
app_files = Config['app_files']
return unless app_files
Dir.chdir Config.root do
# NOTE app_files can be an array
Dir.glob Config['app_files'] do |file|
require Config.project_path file
end
if Config.development?
require_relative "reload"
Reload.listen
@reload = Reload
end
end
end
def start_server
port = Config['port']
env = Config['env']
if l = logger
l.info "starting #{env} server at 0.0.0.0:#{port}"
end
case env.to_s
when 'production'
patch_tcp_socket
start_production_server port
when 'test'
# don't
else
start_watch_assets
patch_tcp_socket
start_development_server port
end
end
def start_watch_assets
return if Config[:assets].blank?
Process.fork do
exec("bundle exec sass --scss --watch #{Config.assets_path('css')}:public/css --cache-location tmp/cache/sass")
end
Process.fork do
exec("bundle exec coffee -w -b -c -o public/js #{Config.assets_path('js')}")
end
end
def patch_tcp_socket
if l = logger
l.info "patching TCPSocket"
end
require_relative "patches/tcp_socket"
end
def start_development_server port
create_tcp_server port
@workers = []
incr_workers nil
trap :INT, &method(:kill_all)
trap :QUIT, &method(:kill_all)
trap :TERM, &method(:kill_all)
Process.waitall
end
# Signals:
#
# * `INT` - kill -9 all workers, and exit
# * `QUIT` - graceful quit all workers, and exit if all children terminated
# * `TERM` - same as QUIT
# * `USR1` - restore worker number
# * `USR2` - graceful spawn a new master and workers, with all content respawned
# * `TTIN` - increase worker number
# * `TTOUT` - decrease worker number
#
# To make a graceful hot-restart:
#
# 1. USR2 -> old master
# 2. if good (workers are up, etc), QUIT -> old master, else QUIT -> new master and fail
# 3. if good (requests are working, etc), INT -> old master
# else QUIT -> new master and USR1 -> old master to restore workers
#
# * NOTE in step 2/3 if an additional fork executed in new master and hangs,<br>
# you may need send an additional INT to terminate it.
# * NOTE hot-restart reloads almost everything, including Gemfile changes and configures except port.<br>
# but, if some critical environment variable or port configure needs change, you still need cold-restart.
# * TODO write to a file to show workers are good
# * TODO detect port config change
def start_production_server port
workers = Config[:workers]
puts "workers: #{workers}"
create_tcp_server port
GC.start
@workers = []
workers.times do
incr_workers nil
end
trap :INT, &method(:kill_all)
trap :QUIT, &method(:quit_all)
trap :TERM, &method(:quit_all)
trap :USR2, &method(:spawn_new_master)
trap :USR1, &method(:restore_workers)
trap :TTIN do
if Config[:workers] > 1
Config[:workers] -= 1
decr_workers nil
end
end
trap :TTOU do
Config[:workers] += 1
incr_workers nil
end
Process.waitall
end
private
def create_tcp_server port
if (server_fd = ENV['NYARA_FD'].to_i) > 0
puts "inheriting server fd #{server_fd}"
@server = TCPServer.for_fd server_fd
end
unless @server
@server = TCPServer.new '0.0.0.0', port
@server.listen 1000
ENV['NYARA_FD'] = @server.fileno.to_s
end
end
# Kill all workers and exit
def kill_all sig
@workers.each do |w|
Process.kill :KILL, w
end
@reload.stop if @reload
exit!
end
# Graceful quit all workers and exit
def quit_all sig
until @workers.empty?
decr_workers sig
end
# wait will finish the wait-and-quit job
end
# Spawn a new master
def spawn_new_master sig
fork do
@server.close_on_exec = false
reload_all
end
end
# Reload everything
def reload_all
# todo set 1-1024 close_on_exec
Dir.chdir START_CTX[:cwd]
if File.executable?(START_CTX[0])
exec START_CTX[0], *START_CTX[:argv], close_others: false
else
# gemset env should be correct because env is inherited
require "rbconfig"
ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
exec ruby, START_CTX[0], *START_CTX[:argv], close_others: false
end
end
# Restore number of workers as Config
def restore_workers sig
(Config[:workers] - @workers.size).times do
incr_workers sig
end
end
# Graceful decrease worker number by 1
def decr_workers sig
w = @workers.shift
puts "killing worker #{w}"
Process.kill :QUIT, w
end
# Increase worker number by 1
def incr_workers sig
Config['before_fork'].call if Config['before_fork']
pid = fork {
$0 = "(nyara:worker) ruby #{$0}"
Config['after_fork'].call if Config['after_fork']
trap :QUIT do
Ext.graceful_quit @server.fileno
end
trap :TERM do
Ext.graceful_quit @server.fileno
end
t = Thread.new do
Ext.init_queue
Ext.run_queue @server.fileno
end
t.join
}
@workers << pid
end
end
end