unicorn-fotopedia 0.99.1

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.
Files changed (163) hide show
  1. data/.CHANGELOG.old +25 -0
  2. data/.document +19 -0
  3. data/.gitignore +21 -0
  4. data/.mailmap +26 -0
  5. data/CONTRIBUTORS +32 -0
  6. data/COPYING +339 -0
  7. data/DESIGN +105 -0
  8. data/Documentation/.gitignore +5 -0
  9. data/Documentation/GNUmakefile +30 -0
  10. data/Documentation/unicorn.1.txt +171 -0
  11. data/Documentation/unicorn_rails.1.txt +172 -0
  12. data/FAQ +52 -0
  13. data/GIT-VERSION-GEN +40 -0
  14. data/GNUmakefile +292 -0
  15. data/HACKING +116 -0
  16. data/ISSUES +36 -0
  17. data/KNOWN_ISSUES +50 -0
  18. data/LICENSE +55 -0
  19. data/PHILOSOPHY +145 -0
  20. data/README +149 -0
  21. data/Rakefile +191 -0
  22. data/SIGNALS +109 -0
  23. data/Sandbox +78 -0
  24. data/TODO +5 -0
  25. data/TUNING +70 -0
  26. data/bin/unicorn +126 -0
  27. data/bin/unicorn_rails +203 -0
  28. data/examples/big_app_gc.rb +33 -0
  29. data/examples/echo.ru +27 -0
  30. data/examples/git.ru +13 -0
  31. data/examples/init.sh +58 -0
  32. data/examples/logger_mp_safe.rb +25 -0
  33. data/examples/nginx.conf +139 -0
  34. data/examples/unicorn.conf.rb +78 -0
  35. data/ext/unicorn_http/CFLAGS +13 -0
  36. data/ext/unicorn_http/c_util.h +124 -0
  37. data/ext/unicorn_http/common_field_optimization.h +111 -0
  38. data/ext/unicorn_http/ext_help.h +77 -0
  39. data/ext/unicorn_http/extconf.rb +14 -0
  40. data/ext/unicorn_http/global_variables.h +89 -0
  41. data/ext/unicorn_http/unicorn_http.rl +714 -0
  42. data/ext/unicorn_http/unicorn_http_common.rl +75 -0
  43. data/lib/unicorn.rb +847 -0
  44. data/lib/unicorn/app/exec_cgi.rb +150 -0
  45. data/lib/unicorn/app/inetd.rb +109 -0
  46. data/lib/unicorn/app/old_rails.rb +33 -0
  47. data/lib/unicorn/app/old_rails/static.rb +58 -0
  48. data/lib/unicorn/cgi_wrapper.rb +145 -0
  49. data/lib/unicorn/configurator.rb +421 -0
  50. data/lib/unicorn/const.rb +34 -0
  51. data/lib/unicorn/http_request.rb +72 -0
  52. data/lib/unicorn/http_response.rb +75 -0
  53. data/lib/unicorn/launcher.rb +65 -0
  54. data/lib/unicorn/oob_gc.rb +58 -0
  55. data/lib/unicorn/socket_helper.rb +152 -0
  56. data/lib/unicorn/tee_input.rb +217 -0
  57. data/lib/unicorn/util.rb +90 -0
  58. data/local.mk.sample +62 -0
  59. data/setup.rb +1586 -0
  60. data/t/.gitignore +2 -0
  61. data/t/GNUmakefile +67 -0
  62. data/t/README +42 -0
  63. data/t/bin/content-md5-put +36 -0
  64. data/t/bin/sha1sum.rb +23 -0
  65. data/t/bin/unused_listen +40 -0
  66. data/t/bin/utee +12 -0
  67. data/t/env.ru +3 -0
  68. data/t/my-tap-lib.sh +200 -0
  69. data/t/t0000-http-basic.sh +50 -0
  70. data/t/t0001-reload-bad-config.sh +52 -0
  71. data/t/t0002-config-conflict.sh +49 -0
  72. data/t/test-lib.sh +100 -0
  73. data/test/aggregate.rb +15 -0
  74. data/test/benchmark/README +50 -0
  75. data/test/benchmark/dd.ru +18 -0
  76. data/test/exec/README +5 -0
  77. data/test/exec/test_exec.rb +1038 -0
  78. data/test/rails/app-1.2.3/.gitignore +2 -0
  79. data/test/rails/app-1.2.3/Rakefile +7 -0
  80. data/test/rails/app-1.2.3/app/controllers/application.rb +6 -0
  81. data/test/rails/app-1.2.3/app/controllers/foo_controller.rb +36 -0
  82. data/test/rails/app-1.2.3/app/helpers/application_helper.rb +4 -0
  83. data/test/rails/app-1.2.3/config/boot.rb +11 -0
  84. data/test/rails/app-1.2.3/config/database.yml +12 -0
  85. data/test/rails/app-1.2.3/config/environment.rb +13 -0
  86. data/test/rails/app-1.2.3/config/environments/development.rb +9 -0
  87. data/test/rails/app-1.2.3/config/environments/production.rb +5 -0
  88. data/test/rails/app-1.2.3/config/routes.rb +6 -0
  89. data/test/rails/app-1.2.3/db/.gitignore +0 -0
  90. data/test/rails/app-1.2.3/public/404.html +1 -0
  91. data/test/rails/app-1.2.3/public/500.html +1 -0
  92. data/test/rails/app-2.0.2/.gitignore +2 -0
  93. data/test/rails/app-2.0.2/Rakefile +7 -0
  94. data/test/rails/app-2.0.2/app/controllers/application.rb +4 -0
  95. data/test/rails/app-2.0.2/app/controllers/foo_controller.rb +36 -0
  96. data/test/rails/app-2.0.2/app/helpers/application_helper.rb +4 -0
  97. data/test/rails/app-2.0.2/config/boot.rb +11 -0
  98. data/test/rails/app-2.0.2/config/database.yml +12 -0
  99. data/test/rails/app-2.0.2/config/environment.rb +17 -0
  100. data/test/rails/app-2.0.2/config/environments/development.rb +8 -0
  101. data/test/rails/app-2.0.2/config/environments/production.rb +5 -0
  102. data/test/rails/app-2.0.2/config/routes.rb +6 -0
  103. data/test/rails/app-2.0.2/db/.gitignore +0 -0
  104. data/test/rails/app-2.0.2/public/404.html +1 -0
  105. data/test/rails/app-2.0.2/public/500.html +1 -0
  106. data/test/rails/app-2.1.2/.gitignore +2 -0
  107. data/test/rails/app-2.1.2/Rakefile +7 -0
  108. data/test/rails/app-2.1.2/app/controllers/application.rb +4 -0
  109. data/test/rails/app-2.1.2/app/controllers/foo_controller.rb +36 -0
  110. data/test/rails/app-2.1.2/app/helpers/application_helper.rb +4 -0
  111. data/test/rails/app-2.1.2/config/boot.rb +111 -0
  112. data/test/rails/app-2.1.2/config/database.yml +12 -0
  113. data/test/rails/app-2.1.2/config/environment.rb +17 -0
  114. data/test/rails/app-2.1.2/config/environments/development.rb +7 -0
  115. data/test/rails/app-2.1.2/config/environments/production.rb +5 -0
  116. data/test/rails/app-2.1.2/config/routes.rb +6 -0
  117. data/test/rails/app-2.1.2/db/.gitignore +0 -0
  118. data/test/rails/app-2.1.2/public/404.html +1 -0
  119. data/test/rails/app-2.1.2/public/500.html +1 -0
  120. data/test/rails/app-2.2.2/.gitignore +2 -0
  121. data/test/rails/app-2.2.2/Rakefile +7 -0
  122. data/test/rails/app-2.2.2/app/controllers/application.rb +4 -0
  123. data/test/rails/app-2.2.2/app/controllers/foo_controller.rb +36 -0
  124. data/test/rails/app-2.2.2/app/helpers/application_helper.rb +4 -0
  125. data/test/rails/app-2.2.2/config/boot.rb +111 -0
  126. data/test/rails/app-2.2.2/config/database.yml +12 -0
  127. data/test/rails/app-2.2.2/config/environment.rb +17 -0
  128. data/test/rails/app-2.2.2/config/environments/development.rb +7 -0
  129. data/test/rails/app-2.2.2/config/environments/production.rb +5 -0
  130. data/test/rails/app-2.2.2/config/routes.rb +6 -0
  131. data/test/rails/app-2.2.2/db/.gitignore +0 -0
  132. data/test/rails/app-2.2.2/public/404.html +1 -0
  133. data/test/rails/app-2.2.2/public/500.html +1 -0
  134. data/test/rails/app-2.3.5/.gitignore +2 -0
  135. data/test/rails/app-2.3.5/Rakefile +7 -0
  136. data/test/rails/app-2.3.5/app/controllers/application_controller.rb +5 -0
  137. data/test/rails/app-2.3.5/app/controllers/foo_controller.rb +36 -0
  138. data/test/rails/app-2.3.5/app/helpers/application_helper.rb +4 -0
  139. data/test/rails/app-2.3.5/config/boot.rb +109 -0
  140. data/test/rails/app-2.3.5/config/database.yml +12 -0
  141. data/test/rails/app-2.3.5/config/environment.rb +17 -0
  142. data/test/rails/app-2.3.5/config/environments/development.rb +7 -0
  143. data/test/rails/app-2.3.5/config/environments/production.rb +6 -0
  144. data/test/rails/app-2.3.5/config/routes.rb +6 -0
  145. data/test/rails/app-2.3.5/db/.gitignore +0 -0
  146. data/test/rails/app-2.3.5/public/404.html +1 -0
  147. data/test/rails/app-2.3.5/public/500.html +1 -0
  148. data/test/rails/app-2.3.5/public/x.txt +1 -0
  149. data/test/rails/test_rails.rb +280 -0
  150. data/test/test_helper.rb +301 -0
  151. data/test/unit/test_configurator.rb +150 -0
  152. data/test/unit/test_http_parser.rb +555 -0
  153. data/test/unit/test_http_parser_ng.rb +443 -0
  154. data/test/unit/test_request.rb +184 -0
  155. data/test/unit/test_response.rb +110 -0
  156. data/test/unit/test_server.rb +291 -0
  157. data/test/unit/test_signals.rb +206 -0
  158. data/test/unit/test_socket_helper.rb +147 -0
  159. data/test/unit/test_tee_input.rb +257 -0
  160. data/test/unit/test_upload.rb +298 -0
  161. data/test/unit/test_util.rb +96 -0
  162. data/unicorn.gemspec +52 -0
  163. metadata +283 -0
@@ -0,0 +1,75 @@
1
+ %%{
2
+
3
+ machine unicorn_http_common;
4
+
5
+ #### HTTP PROTOCOL GRAMMAR
6
+ # line endings
7
+ CRLF = "\r\n";
8
+
9
+ # character types
10
+ CTL = (cntrl | 127);
11
+ safe = ("$" | "-" | "_" | ".");
12
+ extra = ("!" | "*" | "'" | "(" | ")" | ",");
13
+ reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+");
14
+ sorta_safe = ("\"" | "<" | ">");
15
+ unsafe = (CTL | " " | "#" | "%" | sorta_safe);
16
+ national = any -- (alpha | digit | reserved | extra | safe | unsafe);
17
+ unreserved = (alpha | digit | safe | extra | national);
18
+ escape = ("%" xdigit xdigit);
19
+ uchar = (unreserved | escape | sorta_safe);
20
+ pchar = (uchar | ":" | "@" | "&" | "=" | "+");
21
+ tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
22
+ lws = (" " | "\t");
23
+
24
+ # elements
25
+ token = (ascii -- (CTL | tspecials));
26
+
27
+ # URI schemes and absolute paths
28
+ scheme = ( "http"i ("s"i)? ) $downcase_char >mark %scheme;
29
+ hostname = (alnum | "-" | "." | "_")+;
30
+ host_with_port = (hostname (":" digit*)?) >mark %host;
31
+ userinfo = ((unreserved | escape | ";" | ":" | "&" | "=" | "+")+ "@")*;
32
+
33
+ path = ( pchar+ ( "/" pchar* )* ) ;
34
+ query = ( uchar | reserved )* %query_string ;
35
+ param = ( pchar | "/" )* ;
36
+ params = ( param ( ";" param )* ) ;
37
+ rel_path = (path? (";" params)? %request_path) ("?" %start_query query)?;
38
+ absolute_path = ( "/"+ rel_path );
39
+ path_uri = absolute_path > mark %request_uri;
40
+ Absolute_URI = (scheme "://" userinfo host_with_port path_uri);
41
+
42
+ Request_URI = ((absolute_path | "*") >mark %request_uri) | Absolute_URI;
43
+ Fragment = ( uchar | reserved )* >mark %fragment;
44
+ Method = (token){1,20} >mark %request_method;
45
+ GetOnly = "GET" >mark %request_method;
46
+
47
+ http_number = ( digit+ "." digit+ ) ;
48
+ HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ;
49
+ Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;
50
+
51
+ field_name = ( token -- ":" )+ >start_field $snake_upcase_field %write_field;
52
+
53
+ field_value = any* >start_value %write_value;
54
+
55
+ value_cont = lws+ any* >start_value %write_cont_value;
56
+
57
+ message_header = ((field_name ":" lws* field_value)|value_cont) :> CRLF;
58
+ chunk_ext_val = token*;
59
+ chunk_ext_name = token*;
60
+ chunk_extension = ( ";" " "* chunk_ext_name ("=" chunk_ext_val)? )*;
61
+ last_chunk = "0"+ chunk_extension CRLF;
62
+ chunk_size = (xdigit* [1-9a-fA-F] xdigit*) $add_to_chunk_size;
63
+ chunk_end = CRLF;
64
+ chunk_body = any >skip_chunk_data;
65
+ chunk_begin = chunk_size chunk_extension CRLF;
66
+ chunk = chunk_begin chunk_body chunk_end;
67
+ ChunkedBody := chunk* last_chunk @end_chunked_body;
68
+ Trailers := (message_header)* CRLF @end_trailers;
69
+
70
+ FullRequest = Request_Line (message_header)* CRLF @header_done;
71
+ SimpleRequest = GetOnly " " Request_URI ("#"Fragment){0,1} CRLF @header_done;
72
+
73
+ main := FullRequest | SimpleRequest;
74
+
75
+ }%%
@@ -0,0 +1,847 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'fcntl'
4
+ require 'etc'
5
+ require 'rack'
6
+ require 'unicorn/socket_helper'
7
+ require 'unicorn/const'
8
+ require 'unicorn/http_request'
9
+ require 'unicorn/configurator'
10
+ require 'unicorn/util'
11
+ require 'unicorn/tee_input'
12
+ require 'unicorn/http_response'
13
+
14
+ # Unicorn module containing all of the classes (include C extensions) for running
15
+ # a Unicorn web server. It contains a minimalist HTTP server with just enough
16
+ # functionality to service web application requests fast as possible.
17
+ module Unicorn
18
+
19
+ # raised inside TeeInput when a client closes the socket inside the
20
+ # application dispatch. This is always raised with an empty backtrace
21
+ # since there is nothing in the application stack that is responsible
22
+ # for client shutdowns/disconnects.
23
+ class ClientShutdown < EOFError
24
+ end
25
+
26
+ class << self
27
+ def run(app, options = {})
28
+ HttpServer.new(app, options).start.join
29
+ end
30
+
31
+ # This returns a lambda to pass in as the app, this does not "build" the
32
+ # app (which we defer based on the outcome of "preload_app" in the
33
+ # Unicorn config). The returned lambda will be called when it is
34
+ # time to build the app.
35
+ def builder(ru, opts)
36
+ if ru =~ /\.ru\z/
37
+ # parse embedded command-line options in config.ru comments
38
+ /^#\\(.*)/ =~ File.read(ru) and opts.parse!($1.split(/\s+/))
39
+ end
40
+
41
+ lambda do ||
42
+ inner_app = case ru
43
+ when /\.ru$/
44
+ raw = File.read(ru)
45
+ raw.sub!(/^__END__\n.*/, '')
46
+ eval("Rack::Builder.new {(#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
47
+ else
48
+ require ru
49
+ Object.const_get(File.basename(ru, '.rb').capitalize)
50
+ end
51
+
52
+ pp({ :inner_app => inner_app }) if $DEBUG
53
+
54
+ # return value, matches rackup defaults based on env
55
+ case ENV["RACK_ENV"]
56
+ when "development"
57
+ Rack::Builder.new do
58
+ use Rack::CommonLogger, $stderr
59
+ use Rack::ShowExceptions
60
+ use Rack::Lint
61
+ run inner_app
62
+ end.to_app
63
+ when "deployment"
64
+ Rack::Builder.new do
65
+ use Rack::CommonLogger, $stderr
66
+ run inner_app
67
+ end.to_app
68
+ else
69
+ inner_app
70
+ end
71
+ end
72
+ end
73
+
74
+ # returns an array of strings representing TCP listen socket addresses
75
+ # and Unix domain socket paths. This is useful for use with
76
+ # Raindrops::Middleware under Linux: http://raindrops.bogomips.org/
77
+ def listener_names
78
+ HttpServer::LISTENERS.map { |io| SocketHelper.sock_name(io) }
79
+ end
80
+ end
81
+
82
+ # This is the process manager of Unicorn. This manages worker
83
+ # processes which in turn handle the I/O and application process.
84
+ # Listener sockets are started in the master process and shared with
85
+ # forked worker children.
86
+
87
+ class HttpServer < Struct.new(:app, :soft_timeout, :timeout, :worker_processes,
88
+ :before_fork, :after_fork, :before_exec,
89
+ :logger, :pid, :listener_opts, :preload_app,
90
+ :reexec_pid, :orig_app, :init_listeners,
91
+ :master_pid, :config, :ready_pipe, :user)
92
+ include ::Unicorn::SocketHelper
93
+
94
+ # prevents IO objects in here from being GC-ed
95
+ IO_PURGATORY = []
96
+
97
+ # all bound listener sockets
98
+ LISTENERS = []
99
+
100
+ # This hash maps PIDs to Workers
101
+ WORKERS = {}
102
+
103
+ # We use SELF_PIPE differently in the master and worker processes:
104
+ #
105
+ # * The master process never closes or reinitializes this once
106
+ # initialized. Signal handlers in the master process will write to
107
+ # it to wake up the master from IO.select in exactly the same manner
108
+ # djb describes in http://cr.yp.to/docs/selfpipe.html
109
+ #
110
+ # * The workers immediately close the pipe they inherit from the
111
+ # master and replace it with a new pipe after forking. This new
112
+ # pipe is also used to wakeup from IO.select from inside (worker)
113
+ # signal handlers. However, workers *close* the pipe descriptors in
114
+ # the signal handlers to raise EBADF in IO.select instead of writing
115
+ # like we do in the master. We cannot easily use the reader set for
116
+ # IO.select because LISTENERS is already that set, and it's extra
117
+ # work (and cycles) to distinguish the pipe FD from the reader set
118
+ # once IO.select returns. So we're lazy and just close the pipe when
119
+ # a (rare) signal arrives in the worker and reinitialize the pipe later.
120
+ SELF_PIPE = []
121
+
122
+ # signal queue used for self-piping
123
+ SIG_QUEUE = []
124
+
125
+ # constant lookups are faster and we're single-threaded/non-reentrant
126
+ REQUEST = HttpRequest.new
127
+
128
+ # We populate this at startup so we can figure out how to reexecute
129
+ # and upgrade the currently running instance of Unicorn
130
+ # This Hash is considered a stable interface and changing its contents
131
+ # will allow you to switch between different installations of Unicorn
132
+ # or even different installations of the same applications without
133
+ # downtime. Keys of this constant Hash are described as follows:
134
+ #
135
+ # * 0 - the path to the unicorn/unicorn_rails executable
136
+ # * :argv - a deep copy of the ARGV array the executable originally saw
137
+ # * :cwd - the working directory of the application, this is where
138
+ # you originally started Unicorn.
139
+ #
140
+ # To change your unicorn executable to a different path without downtime,
141
+ # you can set the following in your Unicorn config file, HUP and then
142
+ # continue with the traditional USR2 + QUIT upgrade steps:
143
+ #
144
+ # Unicorn::HttpServer::START_CTX[0] = "/home/bofh/1.9.2/bin/unicorn"
145
+ START_CTX = {
146
+ :argv => ARGV.map { |arg| arg.dup },
147
+ :cwd => lambda {
148
+ # favor ENV['PWD'] since it is (usually) symlink aware for
149
+ # Capistrano and like systems
150
+ begin
151
+ a = File.stat(pwd = ENV['PWD'])
152
+ b = File.stat(Dir.pwd)
153
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
154
+ rescue
155
+ Dir.pwd
156
+ end
157
+ }.call,
158
+ 0 => $0.dup,
159
+ }
160
+
161
+ # This class and its members can be considered a stable interface
162
+ # and will not change in a backwards-incompatible fashion between
163
+ # releases of Unicorn. You may need to access it in the
164
+ # before_fork/after_fork hooks. See the Unicorn::Configurator RDoc
165
+ # for examples.
166
+ class Worker < Struct.new(:nr, :tmp, :switched)
167
+
168
+ # worker objects may be compared to just plain numbers
169
+ def ==(other_nr)
170
+ self.nr == other_nr
171
+ end
172
+
173
+ # Changes the worker process to the specified +user+ and +group+
174
+ # This is only intended to be called from within the worker
175
+ # process from the +after_fork+ hook. This should be called in
176
+ # the +after_fork+ hook after any priviledged functions need to be
177
+ # run (e.g. to set per-worker CPU affinity, niceness, etc)
178
+ #
179
+ # Any and all errors raised within this method will be propagated
180
+ # directly back to the caller (usually the +after_fork+ hook.
181
+ # These errors commonly include ArgumentError for specifying an
182
+ # invalid user/group and Errno::EPERM for insufficient priviledges
183
+ def user(user, group = nil)
184
+ # we do not protect the caller, checking Process.euid == 0 is
185
+ # insufficient because modern systems have fine-grained
186
+ # capabilities. Let the caller handle any and all errors.
187
+ uid = Etc.getpwnam(user).uid
188
+ gid = Etc.getgrnam(group).gid if group
189
+ Unicorn::Util.chown_logs(uid, gid)
190
+ tmp.chown(uid, gid)
191
+ if gid && Process.egid != gid
192
+ Process.initgroups(user, gid)
193
+ Process::GID.change_privilege(gid)
194
+ end
195
+ Process.euid != uid and Process::UID.change_privilege(uid)
196
+ self.switched = true
197
+ end
198
+
199
+ end
200
+
201
+ # Creates a working server on host:port (strange things happen if
202
+ # port isn't a Number). Use HttpServer::run to start the server and
203
+ # HttpServer.run.join to join the thread that's processing
204
+ # incoming requests on the socket.
205
+ def initialize(app, options = {})
206
+ self.app = app
207
+ self.reexec_pid = 0
208
+ self.ready_pipe = options.delete(:ready_pipe)
209
+ self.init_listeners = options[:listeners] ? options[:listeners].dup : []
210
+ self.config = Configurator.new(options.merge(:use_defaults => true))
211
+ self.listener_opts = {}
212
+
213
+ # we try inheriting listeners first, so we bind them later.
214
+ # we don't write the pid file until we've bound listeners in case
215
+ # unicorn was started twice by mistake. Even though our #pid= method
216
+ # checks for stale/existing pid files, race conditions are still
217
+ # possible (and difficult/non-portable to avoid) and can be likely
218
+ # to clobber the pid if the second start was in quick succession
219
+ # after the first, so we rely on the listener binding to fail in
220
+ # that case. Some tests (in and outside of this source tree) and
221
+ # monitoring tools may also rely on pid files existing before we
222
+ # attempt to connect to the listener(s)
223
+ config.commit!(self, :skip => [:listeners, :pid])
224
+ self.orig_app = app
225
+ end
226
+
227
+ # Runs the thing. Returns self so you can run join on it
228
+ def start
229
+ BasicSocket.do_not_reverse_lookup = true
230
+
231
+ # inherit sockets from parents, they need to be plain Socket objects
232
+ # before they become UNIXServer or TCPServer
233
+ inherited = ENV['UNICORN_FD'].to_s.split(/,/).map do |fd|
234
+ io = Socket.for_fd(fd.to_i)
235
+ set_server_sockopt(io, listener_opts[sock_name(io)])
236
+ IO_PURGATORY << io
237
+ logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
238
+ server_cast(io)
239
+ end
240
+
241
+ config_listeners = config[:listeners].dup
242
+ LISTENERS.replace(inherited)
243
+
244
+ # we start out with generic Socket objects that get cast to either
245
+ # TCPServer or UNIXServer objects; but since the Socket objects
246
+ # share the same OS-level file descriptor as the higher-level *Server
247
+ # objects; we need to prevent Socket objects from being garbage-collected
248
+ config_listeners -= listener_names
249
+ if config_listeners.empty? && LISTENERS.empty?
250
+ config_listeners << Unicorn::Const::DEFAULT_LISTEN
251
+ init_listeners << Unicorn::Const::DEFAULT_LISTEN
252
+ START_CTX[:argv] << "-l#{Unicorn::Const::DEFAULT_LISTEN}"
253
+ end
254
+ config_listeners.each { |addr| listen(addr) }
255
+ raise ArgumentError, "no listeners" if LISTENERS.empty?
256
+
257
+ # this pipe is used to wake us up from select(2) in #join when signals
258
+ # are trapped. See trap_deferred.
259
+ init_self_pipe!
260
+
261
+ # setup signal handlers before writing pid file in case people get
262
+ # trigger happy and send signals as soon as the pid file exists.
263
+ # Note that signals don't actually get handled until the #join method
264
+ QUEUE_SIGS.each { |sig| trap_deferred(sig) }
265
+ trap(:CHLD) { |_| awaken_master }
266
+ self.pid = config[:pid]
267
+
268
+ self.master_pid = $$
269
+ build_app! if preload_app
270
+ maintain_worker_count
271
+ self
272
+ end
273
+
274
+ # replaces current listener set with +listeners+. This will
275
+ # close the socket if it will not exist in the new listener set
276
+ def listeners=(listeners)
277
+ cur_names, dead_names = [], []
278
+ listener_names.each do |name|
279
+ if ?/ == name[0]
280
+ # mark unlinked sockets as dead so we can rebind them
281
+ (File.socket?(name) ? cur_names : dead_names) << name
282
+ else
283
+ cur_names << name
284
+ end
285
+ end
286
+ set_names = listener_names(listeners)
287
+ dead_names.concat(cur_names - set_names).uniq!
288
+
289
+ LISTENERS.delete_if do |io|
290
+ if dead_names.include?(sock_name(io))
291
+ IO_PURGATORY.delete_if do |pio|
292
+ pio.fileno == io.fileno && (pio.close rescue nil).nil? # true
293
+ end
294
+ (io.close rescue nil).nil? # true
295
+ else
296
+ set_server_sockopt(io, listener_opts[sock_name(io)])
297
+ false
298
+ end
299
+ end
300
+
301
+ (set_names - cur_names).each { |addr| listen(addr) }
302
+ end
303
+
304
+ def stdout_path=(path); redirect_io($stdout, path); end
305
+ def stderr_path=(path); redirect_io($stderr, path); end
306
+
307
+ def logger=(obj)
308
+ HttpRequest::DEFAULTS["rack.logger"] = super
309
+ end
310
+
311
+ # sets the path for the PID file of the master process
312
+ def pid=(path)
313
+ if path
314
+ if x = valid_pid?(path)
315
+ return path if pid && path == pid && x == $$
316
+ raise ArgumentError, "Already running on PID:#{x} " \
317
+ "(or pid=#{path} is stale)"
318
+ end
319
+ end
320
+ unlink_pid_safe(pid) if pid
321
+
322
+ if path
323
+ fp = begin
324
+ tmp = "#{File.dirname(path)}/#{rand}.#$$"
325
+ File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
326
+ rescue Errno::EEXIST
327
+ retry
328
+ end
329
+ fp.syswrite("#$$\n")
330
+ File.rename(fp.path, path)
331
+ fp.close
332
+ end
333
+ super(path)
334
+ end
335
+
336
+ # add a given address to the +listeners+ set, idempotently
337
+ # Allows workers to add a private, per-process listener via the
338
+ # after_fork hook. Very useful for debugging and testing.
339
+ # +:tries+ may be specified as an option for the number of times
340
+ # to retry, and +:delay+ may be specified as the time in seconds
341
+ # to delay between retries.
342
+ # A negative value for +:tries+ indicates the listen will be
343
+ # retried indefinitely, this is useful when workers belonging to
344
+ # different masters are spawned during a transparent upgrade.
345
+ def listen(address, opt = {}.merge(listener_opts[address] || {}))
346
+ address = config.expand_addr(address)
347
+ return if String === address && listener_names.include?(address)
348
+
349
+ delay = opt[:delay] || 0.5
350
+ tries = opt[:tries] || 5
351
+ begin
352
+ io = bind_listen(address, opt)
353
+ unless TCPServer === io || UNIXServer === io
354
+ IO_PURGATORY << io
355
+ io = server_cast(io)
356
+ end
357
+ logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
358
+ LISTENERS << io
359
+ io
360
+ rescue Errno::EADDRINUSE => err
361
+ logger.error "adding listener failed addr=#{address} (in use)"
362
+ raise err if tries == 0
363
+ tries -= 1
364
+ logger.error "retrying in #{delay} seconds " \
365
+ "(#{tries < 0 ? 'infinite' : tries} tries left)"
366
+ sleep(delay)
367
+ retry
368
+ rescue => err
369
+ logger.fatal "error adding listener addr=#{address}"
370
+ raise err
371
+ end
372
+ end
373
+
374
+ # monitors children and receives signals forever
375
+ # (or until a termination signal is sent). This handles signals
376
+ # one-at-a-time time and we'll happily drop signals in case somebody
377
+ # is signalling us too often.
378
+ def join
379
+ respawn = true
380
+ last_check = Time.now
381
+
382
+ proc_name 'master'
383
+ logger.info "master process ready" # test_exec.rb relies on this message
384
+ if ready_pipe
385
+ ready_pipe.syswrite($$.to_s)
386
+ ready_pipe.close rescue nil
387
+ self.ready_pipe = nil
388
+ end
389
+ begin
390
+ loop do
391
+ reap_all_workers
392
+ case SIG_QUEUE.shift
393
+ when nil
394
+ # avoid murdering workers after our master process (or the
395
+ # machine) comes out of suspend/hibernation
396
+ if (last_check + soft_timeout) >= (last_check = Time.now)
397
+ murder_lazy_workers
398
+ else
399
+ # wait for workers to wakeup on suspend
400
+ master_sleep(timeout/2.0 + 1)
401
+ end
402
+ maintain_worker_count if respawn
403
+ master_sleep(1)
404
+ when :QUIT # graceful shutdown
405
+ break
406
+ when :TERM, :INT # immediate shutdown
407
+ stop(false)
408
+ break
409
+ when :USR1 # rotate logs
410
+ logger.info "master reopening logs..."
411
+ Unicorn::Util.reopen_logs
412
+ logger.info "master done reopening logs"
413
+ kill_each_worker(:USR1)
414
+ when :USR2 # exec binary, stay alive in case something went wrong
415
+ reexec
416
+ when :WINCH
417
+ if Process.ppid == 1 || Process.getpgrp != $$
418
+ respawn = false
419
+ logger.info "gracefully stopping all workers"
420
+ kill_each_worker(:QUIT)
421
+ else
422
+ logger.info "SIGWINCH ignored because we're not daemonized"
423
+ end
424
+ when :TTIN
425
+ self.worker_processes += 1
426
+ when :TTOU
427
+ self.worker_processes -= 1 if self.worker_processes > 0
428
+ when :HUP
429
+ respawn = true
430
+ if config.config_file
431
+ load_config!
432
+ redo # immediate reaping since we may have QUIT workers
433
+ else # exec binary and exit if there's no config file
434
+ logger.info "config_file not present, reexecuting binary"
435
+ reexec
436
+ break
437
+ end
438
+ end
439
+ end
440
+ rescue Errno::EINTR
441
+ retry
442
+ rescue => e
443
+ logger.error "Unhandled master loop exception #{e.inspect}."
444
+ logger.error e.backtrace.join("\n")
445
+ retry
446
+ end
447
+ stop # gracefully shutdown all workers on our way out
448
+ logger.info "master complete"
449
+ unlink_pid_safe(pid) if pid
450
+ end
451
+
452
+ # Terminates all workers, but does not exit master process
453
+ def stop(graceful = true)
454
+ self.listeners = []
455
+ limit = Time.now + timeout
456
+ until WORKERS.empty? || Time.now > limit
457
+ kill_each_worker(graceful ? :QUIT : :TERM)
458
+ sleep(0.1)
459
+ reap_all_workers
460
+ end
461
+ kill_each_worker(:KILL)
462
+ end
463
+
464
+ private
465
+
466
+ # list of signals we care about and trap in master.
467
+ QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP,
468
+ :TTIN, :TTOU ]
469
+
470
+ # defer a signal for later processing in #join (master process)
471
+ def trap_deferred(signal)
472
+ trap(signal) do |sig_nr|
473
+ if SIG_QUEUE.size < 5
474
+ SIG_QUEUE << signal
475
+ awaken_master
476
+ else
477
+ logger.error "ignoring SIG#{signal}, queue=#{SIG_QUEUE.inspect}"
478
+ end
479
+ end
480
+ end
481
+
482
+ # wait for a signal hander to wake us up and then consume the pipe
483
+ # Wake up every second anyways to run murder_lazy_workers
484
+ def master_sleep(sec)
485
+ begin
486
+ ready = IO.select([SELF_PIPE.first], nil, nil, sec) or return
487
+ ready.first && ready.first.first or return
488
+ loop { SELF_PIPE.first.read_nonblock(Const::CHUNK_SIZE) }
489
+ rescue Errno::EAGAIN, Errno::EINTR
490
+ end
491
+ end
492
+
493
+ def awaken_master
494
+ begin
495
+ SELF_PIPE.last.write_nonblock('.') # wakeup master process from select
496
+ rescue Errno::EAGAIN, Errno::EINTR
497
+ # pipe is full, master should wake up anyways
498
+ retry
499
+ end
500
+ end
501
+
502
+ # reaps all unreaped workers
503
+ def reap_all_workers
504
+ begin
505
+ loop do
506
+ wpid, status = Process.waitpid2(-1, Process::WNOHANG)
507
+ wpid or break
508
+ if reexec_pid == wpid
509
+ logger.error "reaped #{status.inspect} exec()-ed"
510
+ self.reexec_pid = 0
511
+ self.pid = pid.chomp('.oldbin') if pid
512
+ proc_name 'master'
513
+ else
514
+ worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
515
+ logger.info "reaped #{status.inspect} " \
516
+ "worker=#{worker.nr rescue 'unknown'}"
517
+ end
518
+ end
519
+ rescue Errno::ECHILD
520
+ end
521
+ end
522
+
523
+ # reexecutes the START_CTX with a new binary
524
+ def reexec
525
+ if reexec_pid > 0
526
+ begin
527
+ Process.kill(0, reexec_pid)
528
+ logger.error "reexec-ed child already running PID:#{reexec_pid}"
529
+ return
530
+ rescue Errno::ESRCH
531
+ self.reexec_pid = 0
532
+ end
533
+ end
534
+
535
+ if pid
536
+ old_pid = "#{pid}.oldbin"
537
+ prev_pid = pid.dup
538
+ begin
539
+ self.pid = old_pid # clear the path for a new pid file
540
+ rescue ArgumentError
541
+ logger.error "old PID:#{valid_pid?(old_pid)} running with " \
542
+ "existing pid=#{old_pid}, refusing rexec"
543
+ return
544
+ rescue => e
545
+ logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
546
+ return
547
+ end
548
+ end
549
+
550
+ self.reexec_pid = fork do
551
+ listener_fds = LISTENERS.map { |sock| sock.fileno }
552
+ ENV['UNICORN_FD'] = listener_fds.join(',')
553
+ Dir.chdir(START_CTX[:cwd])
554
+ cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
555
+
556
+ # avoid leaking FDs we don't know about, but let before_exec
557
+ # unset FD_CLOEXEC, if anything else in the app eventually
558
+ # relies on FD inheritence.
559
+ (3..1024).each do |io|
560
+ next if listener_fds.include?(io)
561
+ io = IO.for_fd(io) rescue nil
562
+ io or next
563
+ IO_PURGATORY << io
564
+ io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
565
+ end
566
+ logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
567
+ before_exec.call(self)
568
+ exec(*cmd)
569
+ end
570
+ proc_name 'master (old)'
571
+ end
572
+
573
+ # forcibly terminate all workers that haven't checked in in timeout
574
+ # seconds. The timeout is implemented using an unlinked File
575
+ # shared between the parent process and each worker. The worker
576
+ # runs File#chmod to modify the ctime of the File. If the ctime
577
+ # is stale for >timeout seconds, then we'll kill the corresponding
578
+ # worker.
579
+ def murder_lazy_workers
580
+ WORKERS.dup.each_pair do |wpid, worker|
581
+ stat = worker.tmp.stat
582
+ # skip workers that disable fchmod or have never fchmod-ed
583
+ stat.mode == 0100600 and next
584
+ # FIXME: if the worker has not been working for soft_timeout, it will be
585
+ # killed even if it is not blocking
586
+ (diff = (Time.now - stat.ctime)) <= soft_timeout and
587
+ diff <= timeout and next
588
+ # lazy since less than timeout, attempt soft kill
589
+ if diff < timeout
590
+ logger.error "worker=#{worker.nr} PID:#{wpid} soft timeout " \
591
+ "(#{diff}s > #{soft_timeout}s), killing softly"
592
+ kill_worker(:ABRT, wpid)
593
+ else
594
+ logger.error "worker=#{worker.nr} PID:#{wpid} hard timeout " \
595
+ "(#{diff}s > #{timeout}s), killing"
596
+ kill_worker(:KILL, wpid) # take no prisoners for timeout violations
597
+ end
598
+ end
599
+ end
600
+
601
+ def spawn_missing_workers
602
+ (0...worker_processes).each do |worker_nr|
603
+ WORKERS.values.include?(worker_nr) and next
604
+ worker = Worker.new(worker_nr, Unicorn::Util.tmpio)
605
+ before_fork.call(self, worker)
606
+ WORKERS[fork {
607
+ ready_pipe.close if ready_pipe
608
+ self.ready_pipe = nil
609
+ worker_loop(worker)
610
+ }] = worker
611
+ end
612
+ end
613
+
614
+ def maintain_worker_count
615
+ (off = WORKERS.size - worker_processes) == 0 and return
616
+ off < 0 and return spawn_missing_workers
617
+ WORKERS.dup.each_pair { |wpid,w|
618
+ w.nr >= worker_processes and kill_worker(:QUIT, wpid) rescue nil
619
+ }
620
+ end
621
+
622
+ # if we get any error, try to write something back to the client
623
+ # assuming we haven't closed the socket, but don't get hung up
624
+ # if the socket is already closed or broken. We'll always ensure
625
+ # the socket is closed at the end of this function
626
+ def handle_error(client, e)
627
+ msg = case e
628
+ when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
629
+ Const::ERROR_500_RESPONSE
630
+ when HttpParserError # try to tell the client they're bad
631
+ Const::ERROR_400_RESPONSE
632
+ else
633
+ logger.error "Read error: #{e.inspect}"
634
+ logger.error e.backtrace.join("\n")
635
+ Const::ERROR_500_RESPONSE
636
+ end
637
+ client.write_nonblock(msg)
638
+ client.close
639
+ rescue
640
+ nil
641
+ end
642
+
643
+ # once a client is accepted, it is processed in its entirety here
644
+ # in 3 easy steps: read request, call app, write app response
645
+ def process_client(client)
646
+ client.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
647
+ response = app.call(env = REQUEST.read(client))
648
+
649
+ if 100 == response.first.to_i
650
+ client.write(Const::EXPECT_100_RESPONSE)
651
+ env.delete(Const::HTTP_EXPECT)
652
+ response = app.call(env)
653
+ end
654
+ HttpResponse.write(client, response, HttpRequest::PARSER.headers?)
655
+ rescue => e
656
+ handle_error(client, e)
657
+ end
658
+
659
+ # gets rid of stuff the worker has no business keeping track of
660
+ # to free some resources and drops all sig handlers.
661
+ # traps for USR1, USR2, and HUP may be set in the after_fork Proc
662
+ # by the user.
663
+ def init_worker_process(worker)
664
+ QUEUE_SIGS.each { |sig| trap(sig, nil) }
665
+ trap(:CHLD, 'DEFAULT')
666
+ SIG_QUEUE.clear
667
+ proc_name "worker[#{worker.nr}]"
668
+ START_CTX.clear
669
+ init_self_pipe!
670
+
671
+ # try to handle SIGABRT correctly
672
+ trap('ABRT') do
673
+ raise SignalException, "SIGABRT"
674
+ end
675
+
676
+ WORKERS.values.each { |other| other.tmp.close rescue nil }
677
+ WORKERS.clear
678
+ LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
679
+ worker.tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
680
+ after_fork.call(self, worker) # can drop perms
681
+ worker.user(*user) if user.kind_of?(Array) && ! worker.switched
682
+ self.timeout /= 2.0 # halve it for select()
683
+ build_app! unless preload_app
684
+ end
685
+
686
+ def reopen_worker_logs(worker_nr)
687
+ logger.info "worker=#{worker_nr} reopening logs..."
688
+ Unicorn::Util.reopen_logs
689
+ logger.info "worker=#{worker_nr} done reopening logs"
690
+ init_self_pipe!
691
+ end
692
+
693
+ # runs inside each forked worker, this sits around and waits
694
+ # for connections and doesn't die until the parent dies (or is
695
+ # given a INT, QUIT, or TERM signal)
696
+ def worker_loop(worker)
697
+ ppid = master_pid
698
+ init_worker_process(worker)
699
+ nr = 0 # this becomes negative if we need to reopen logs
700
+ alive = worker.tmp # tmp is our lifeline to the master process
701
+ ready = LISTENERS
702
+
703
+ # closing anything we IO.select on will raise EBADF
704
+ trap(:USR1) { nr = -65536; SELF_PIPE.first.close rescue nil }
705
+ trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil } }
706
+ [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
707
+ logger.info "worker=#{worker.nr} ready"
708
+ m = 0
709
+
710
+ begin
711
+ nr < 0 and reopen_worker_logs(worker.nr)
712
+ nr = 0
713
+
714
+ # we're a goner in timeout seconds anyways if alive.chmod
715
+ # breaks, so don't trap the exception. Using fchmod() since
716
+ # futimes() is not available in base Ruby and I very strongly
717
+ # prefer temporary files to be unlinked for security,
718
+ # performance and reliability reasons, so utime is out. No-op
719
+ # changes with chmod doesn't update ctime on all filesystems; so
720
+ # we change our counter each and every time (after process_client
721
+ # and before IO.select).
722
+ alive.chmod(m = 0 == m ? 1 : 0)
723
+
724
+ ready.each do |sock|
725
+ begin
726
+ process_client(sock.accept_nonblock)
727
+ nr += 1
728
+ alive.chmod(m = 0 == m ? 1 : 0)
729
+ rescue Errno::EAGAIN, Errno::ECONNABORTED
730
+ end
731
+ break if nr < 0
732
+ end
733
+
734
+ # make the following bet: if we accepted clients this round,
735
+ # we're probably reasonably busy, so avoid calling select()
736
+ # and do a speculative accept_nonblock on ready listeners
737
+ # before we sleep again in select().
738
+ redo unless nr == 0 # (nr < 0) => reopen logs
739
+
740
+ ppid == Process.ppid or return
741
+ alive.chmod(m = 0 == m ? 1 : 0)
742
+ begin
743
+ # timeout used so we can detect parent death:
744
+ ret = IO.select(LISTENERS, nil, SELF_PIPE, timeout) or redo
745
+ ready = ret.first
746
+ rescue Errno::EINTR
747
+ ready = LISTENERS
748
+ rescue Errno::EBADF
749
+ nr < 0 or return
750
+ end
751
+ rescue => e
752
+ if alive
753
+ logger.error "Unhandled listen loop exception #{e.inspect}."
754
+ logger.error e.backtrace.join("\n")
755
+ end
756
+ end while alive
757
+ end
758
+
759
+ # delivers a signal to a worker and fails gracefully if the worker
760
+ # is no longer running.
761
+ def kill_worker(signal, wpid)
762
+ begin
763
+ Process.kill(signal, wpid)
764
+ rescue Errno::ESRCH
765
+ worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
766
+ end
767
+ end
768
+
769
+ # delivers a signal to each worker
770
+ def kill_each_worker(signal)
771
+ WORKERS.keys.each { |wpid| kill_worker(signal, wpid) }
772
+ end
773
+
774
+ # unlinks a PID file at given +path+ if it contains the current PID
775
+ # still potentially racy without locking the directory (which is
776
+ # non-portable and may interact badly with other programs), but the
777
+ # window for hitting the race condition is small
778
+ def unlink_pid_safe(path)
779
+ (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
780
+ end
781
+
782
+ # returns a PID if a given path contains a non-stale PID file,
783
+ # nil otherwise.
784
+ def valid_pid?(path)
785
+ wpid = File.read(path).to_i
786
+ wpid <= 0 and return nil
787
+ begin
788
+ Process.kill(0, wpid)
789
+ wpid
790
+ rescue Errno::ESRCH
791
+ # don't unlink stale pid files, racy without non-portable locking...
792
+ end
793
+ rescue Errno::ENOENT
794
+ end
795
+
796
+ def load_config!
797
+ loaded_app = app
798
+ begin
799
+ logger.info "reloading config_file=#{config.config_file}"
800
+ config[:listeners].replace(init_listeners)
801
+ config.reload
802
+ config.commit!(self)
803
+ kill_each_worker(:QUIT)
804
+ Unicorn::Util.reopen_logs
805
+ self.app = orig_app
806
+ build_app! if preload_app
807
+ logger.info "done reloading config_file=#{config.config_file}"
808
+ rescue StandardError, LoadError, SyntaxError => e
809
+ logger.error "error reloading config_file=#{config.config_file}: " \
810
+ "#{e.class} #{e.message}"
811
+ self.app = loaded_app
812
+ end
813
+ end
814
+
815
+ # returns an array of string names for the given listener array
816
+ def listener_names(listeners = LISTENERS)
817
+ listeners.map { |io| sock_name(io) }
818
+ end
819
+
820
+ def build_app!
821
+ if app.respond_to?(:arity) && app.arity == 0
822
+ if defined?(Gem) && Gem.respond_to?(:refresh)
823
+ logger.info "Refreshing Gem list"
824
+ Gem.refresh
825
+ end
826
+ self.app = app.call
827
+ end
828
+ end
829
+
830
+ def proc_name(tag)
831
+ $0 = ([ File.basename(START_CTX[0]), tag
832
+ ]).concat(START_CTX[:argv]).join(' ')
833
+ end
834
+
835
+ def redirect_io(io, path)
836
+ File.open(path, 'ab') { |fp| io.reopen(fp) } if path
837
+ io.sync = true
838
+ end
839
+
840
+ def init_self_pipe!
841
+ SELF_PIPE.each { |io| io.close rescue nil }
842
+ SELF_PIPE.replace(IO.pipe)
843
+ SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
844
+ end
845
+
846
+ end
847
+ end