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 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
|
# Phusion Passenger - http://www.modrails.com/
# Copyright (c) 2010 Phusion
#
# "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'socket'
require 'thread'
require 'etc'
require 'phusion_passenger'
require 'phusion_passenger/plugin'
require 'phusion_passenger/standalone/command'
# We lazy load as many libraries as possible not only to improve startup performance,
# but also to ensure that we don't require libraries before we've passed the dependency
# checking stage of the runtime installer.
#
# IMPORTANT: do not directly or indirectly require native_support; we can't compile
# it yet until we have a compiler, and the runtime installer is supposed to check whether
# a compiler is installed.
module PhusionPassenger
module Standalone
class StartCommand < Command
def self.description
return "Start Phusion Passenger Standalone."
end
def initialize(args)
super(args)
@console_mutex = Mutex.new
@termination_pipe = IO.pipe
@threads = []
@interruptable_threads = []
@plugin = PhusionPassenger::Plugin.new('standalone/start_command', self, @options)
end
def run
parse_my_options
sanity_check_options
ensure_nginx_installed
determine_various_resource_locations
require_app_finder
@app_finder = AppFinder.new(@args, @options)
@apps = @app_finder.scan
@plugin.call_hook(:found_apps, @apps)
extra_controller_options = {}
@plugin.call_hook(:before_creating_nginx_controller, extra_controller_options)
create_nginx_controller(extra_controller_options)
begin
start_nginx
show_intro_message
daemonize if @options[:daemonize]
Thread.abort_on_exception = true
@plugin.call_hook(:nginx_started, @nginx)
########################
########################
watch_log_files_in_background if should_watch_logs?
wait_until_nginx_has_exited
rescue Interrupt
stop_threads
stop_nginx
exit 2
rescue SignalException => signal
stop_threads
stop_nginx
if signal.message == 'SIGINT' || signal.message == 'SIGTERM'
exit 2
else
raise
end
rescue Exception => e
stop_threads
stop_nginx
raise
ensure
stop_threads
end
ensure
if @temp_dir
FileUtils.rm_rf(@temp_dir) rescue nil
end
@plugin.call_hook(:cleanup)
end
private
def require_file_utils
require 'fileutils' unless defined?(FileUtils)
end
def require_app_finder
require 'phusion_passenger/standalone/app_finder' unless defined?(AppFinder)
end
def parse_my_options
description = "Starts Phusion Passenger Standalone and serve one or more Ruby web applications."
parse_options!("start [directory]", description) do |opts|
opts.on("-a", "--address HOST", String,
wrap_desc("Bind to HOST address (default: #{@options[:address]})")) do |value|
@options[:address] = value
@options[:tcp_explicitly_given] = true
end
opts.on("-p", "--port NUMBER", Integer,
wrap_desc("Use the given port number (default: #{@options[:port]})")) do |value|
@options[:port] = value
@options[:tcp_explicitly_given] = true
end
opts.on("-S", "--socket FILE", String,
wrap_desc("Bind to Unix domain socket instead of TCP socket")) do |value|
@options[:socket_file] = value
end
opts.separator ""
opts.on("-e", "--environment ENV", String,
wrap_desc("Framework environment (default: #{@options[:env]})")) do |value|
@options[:env] = value
end
opts.on("-R", "--rackup FILE", String,
wrap_desc("If Rack application detected, run this rackup file")) do |value|
ENV["RACKUP_FILE"] = value
end
opts.on("--max-pool-size NUMBER", Integer,
wrap_desc("Maximum number of application processes (default: #{@options[:max_pool_size]})")) do |value|
@options[:max_pool_size] = value
end
opts.on("--min-instances NUMBER", Integer,
wrap_desc("Minimum number of processes per application (default: #{@options[:min_instances]})")) do |value|
@options[:min_instances] = value
end
opts.on("--spawn-method NAME", String,
wrap_desc("The spawn method to use (default: #{@options[:spawn_method]})")) do |value|
@options[:spawn_method] = value
end
opts.on("--union-station-gateway HOST:PORT", String,
wrap_desc("Specify Union Station Gateway host and port")) do |value|
host, port = value.split(":", 2)
port = port.to_i
port = 443 if port == 0
@options[:union_station_gateway_address] = host
@options[:union_station_gateway_port] = port.to_i
end
opts.on("--union-station-key KEY", String,
wrap_desc("Specify Union Station key")) do |value|
@options[:union_station_key] = value
end
opts.separator ""
opts.on("--ping-port NUMBER", Integer,
wrap_desc("Use the given port number for checking whether Nginx is alive (default: same as the normal port)")) do |value|
@options[:ping_port] = value
end
@plugin.call_hook(:parse_options, opts)
opts.separator ""
opts.on("-d", "--daemonize",
wrap_desc("Daemonize into the background")) do
@options[:daemonize] = true
end
opts.on("--user USERNAME", String,
wrap_desc("User to run as. Ignored unless running as root.")) do |value|
@options[:user] = value
end
opts.on("--log-file FILENAME", String,
wrap_desc("Where to write log messages (default: console, or /dev/null when daemonized)")) do |value|
@options[:log_file] = value
end
opts.on("--pid-file FILENAME", String,
wrap_desc("Where to store the PID file")) do |value|
@options[:pid_file] = value
end
opts.on("--nginx-bin FILENAME", String,
wrap_desc("Nginx binary to use as core")) do |value|
@options[:nginx_bin] = value
end
opts.on("--nginx-version VERSION", String,
wrap_desc("Nginx version to use as core (default: #{@options[:nginx_version]})")) do |value|
@options[:nginx_version] = value
end
opts.on("--nginx-tarball FILENAME", String,
wrap_desc("If Nginx needs to be installed, then the given tarball will " +
"be used instead of downloading from the Internet")) do |value|
@options[:nginx_tarball] = File.expand_path(value)
end
opts.on("--binaries-url-root URL", String,
wrap_desc("If Nginx needs to be installed, then the specified URL will be " +
"checked for binaries prior to a local build.")) do |value|
@options[:binaries_url_root] = value
end
end
@plugin.call_hook(:done_parsing_options)
end
def sanity_check_options
if @options[:tcp_explicitly_given] && @options[:socket_file]
error "You cannot specify both --address/--port and --socket. Please choose either one."
exit 1
end
check_port_bind_permission_and_display_sudo_suggestion
check_port_availability
end
# Most platforms don't allow non-root processes to bind to a port lower than 1024.
# Check whether this is the case for the current platform and if so, tell the user
# that it must re-run Phusion Passenger Standalone with sudo.
def check_port_bind_permission_and_display_sudo_suggestion
if !@options[:socket_file] && @options[:port] < 1024 && Process.euid != 0
begin
TCPServer.new('127.0.0.1', @options[:port]).close
rescue Errno::EACCES
require 'phusion_passenger/platform_info/ruby'
myself = `whoami`.strip
error "Only the 'root' user can run this program on port #{@options[:port]}. " <<
"You are currently running as '#{myself}'. Please re-run this program " <<
"with root privileges with the following command:\n\n" <<
" #{PlatformInfo.ruby_sudo_command} passenger start #{@original_args.join(' ')} --user=#{myself}\n\n" <<
"Don't forget the '--user' part! That will make Phusion Passenger Standalone " <<
"drop root privileges and switch to '#{myself}' after it has obtained " <<
"port #{@options[:port]}."
exit 1
end
end
end
def check_port(address, port)
begin
socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
sockaddr = Socket.pack_sockaddr_in(port, address)
begin
socket.connect_nonblock(sockaddr)
rescue Errno::ENOENT, Errno::EINPROGRESS, Errno::EAGAIN, Errno::EWOULDBLOCK
if select(nil, [socket], nil, 0.1)
begin
socket.connect_nonblock(sockaddr)
rescue Errno::EISCONN
end
else
raise Errno::ECONNREFUSED
end
end
return true
rescue Errno::ECONNREFUSED
return false
ensure
socket.close if socket
end
end
def check_port_availability
if !@options[:socket_file] && check_port(@options[:address], @options[:port])
error "The address #{@options[:address]}:#{@options[:port]} is already " <<
"in use by another process, perhaps another Phusion Passenger " <<
"Standalone instance.\n\n" <<
"If you want to run this Phusion Passenger Standalone instance on " <<
"another port, use the -p option, like this:\n\n" <<
" passenger start -p #{@options[:port] + 1}"
exit 1
end
end
def should_watch_logs?
return !@options[:daemonize] && @options[:log_file] != "/dev/null"
end
# Returns the URL that Nginx will be listening on.
def listen_url
if @options[:socket_file]
return @options[:socket_file]
else
result = "http://#{@options[:address]}"
if @options[:port] != 80
result << ":#{@options[:port]}"
end
result << "/"
return result
end
end
def install_runtime
require 'phusion_passenger/standalone/runtime_installer'
installer = RuntimeInstaller.new(
:source_root => SOURCE_ROOT,
:support_dir => passenger_support_files_dir,
:nginx_dir => nginx_dir,
:version => @options[:nginx_version],
:tarball => @options[:nginx_tarball],
:binaries_url_root => @options[:binaries_url_root],
:plugin => @plugin)
installer.start
end
def passenger_support_files_dir
return "#{@runtime_dir}/support"
end
def nginx_dir
return "#{@runtime_dir}/nginx-#{@options[:nginx_version]}"
end
def ensure_nginx_installed
if @options[:nginx_bin] && !File.exist?(@options[:nginx_bin])
error "The given Nginx binary '#{@options[:nginx_bin]}' does not exist."
exit 1
end
home = Etc.getpwuid.dir
@runtime_dir = "#{GLOBAL_STANDALONE_RESOURCE_DIR}/#{runtime_version_string}"
if !File.exist?("#{nginx_dir}/sbin/nginx")
if Process.euid == 0
install_runtime
else
@runtime_dir = "#{home}/#{LOCAL_STANDALONE_RESOURCE_DIR}/#{runtime_version_string}"
if !File.exist?("#{nginx_dir}/sbin/nginx")
install_runtime
end
end
end
end
def ensure_directory_exists(dir)
if !File.exist?(dir)
require_file_utils
FileUtils.mkdir_p(dir)
end
end
def start_nginx
begin
@nginx.start
rescue DaemonController::AlreadyStarted
begin
pid = @nginx.pid
rescue SystemCallError, IOError
pid = nil
end
if pid
error "Phusion Passenger Standalone is already running on PID #{pid}."
else
error "Phusion Passenger Standalone is already running."
end
exit 1
rescue DaemonController::StartError => e
error "Could not start Passenger Nginx core:\n#{e}"
exit 1
end
end
def show_intro_message
puts "=============== Phusion Passenger Standalone web server started ==============="
puts "PID file: #{@options[:pid_file]}"
puts "Log file: #{@options[:log_file]}"
puts "Environment: #{@options[:env]}"
puts "Accessible via: #{listen_url}"
puts
if @options[:daemonize]
puts "Serving in the background as a daemon."
else
puts "You can stop Phusion Passenger Standalone by pressing Ctrl-C."
end
puts "==============================================================================="
end
def daemonize
pid = fork
if pid
# Parent
exit!(0)
else
# Child
trap "HUP", "IGNORE"
STDIN.reopen("/dev/null", "r")
STDOUT.reopen(@options[:log_file], "a")
STDERR.reopen(@options[:log_file], "a")
STDOUT.sync = true
STDERR.sync = true
Process.setsid
end
end
# Wait until the termination pipe becomes readable (a hint for threads
# to shut down), or until the timeout has been reached. Returns true if
# the termination pipe became readable, false if the timeout has been reached.
def wait_on_termination_pipe(timeout)
ios = select([@termination_pipe[0]], nil, nil, timeout)
return !ios.nil?
end
def watch_log_file(log_file)
if File.exist?(log_file)
backward = 0
else
# tail bails out if the file doesn't exist, so wait until it exists.
while !File.exist?(log_file)
sleep 1
end
backward = 10
end
IO.popen("tail -f -n #{backward} \"#{log_file}\"", "rb") do |f|
begin
while true
begin
line = f.readline
@console_mutex.synchronize do
STDOUT.write(line)
STDOUT.flush
end
rescue EOFError
break
end
end
ensure
Process.kill('TERM', f.pid)
end
end
end
def watch_log_files_in_background
@apps.each do |app|
thread = Thread.new do
watch_log_file("#{app[:root]}/log/#{@options[:env]}.log")
end
@threads << thread
@interruptable_threads << thread
end
thread = Thread.new do
watch_log_file(@options[:log_file])
end
@threads << thread
@interruptable_threads << thread
end
def wait_until_nginx_has_exited
# Since Nginx is not our child process (it daemonizes or we daemonize)
# we cannot use Process.waitpid to wait for it. A busy-sleep-loop with
# Process.kill(0, pid) isn't very efficient. Instead we do this:
#
# Connect to Nginx and wait until Nginx disconnects the socket because of
# timeout. Keep doing this until we can no longer connect.
while true
if @options[:socket_file]
socket = UNIXSocket.new(@options[:socket_file])
else
socket = TCPSocket.new(@options[:address], nginx_ping_port)
end
begin
socket.read rescue nil
ensure
socket.close rescue nil
end
end
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
end
def stop_nginx
@console_mutex.synchronize do
STDOUT.write("Stopping web server...")
STDOUT.flush
@nginx.stop
STDOUT.puts " done"
STDOUT.flush
end
end
def stop_threads
if !@termination_pipe[1].closed?
@termination_pipe[1].write("x")
@termination_pipe[1].close
end
@interruptable_threads.each do |thread|
thread.terminate
end
@interruptable_threads = []
@threads.each do |thread|
thread.join
end
@threads = []
end
#### Config file template helpers ####
def nginx_listen_address(options = @options, for_ping_port = false)
if options[:socket_file]
return "unix:" + File.expand_path(options[:socket_file])
else
if for_ping_port
port = options[:ping_port]
else
port = options[:port]
end
return "#{options[:address]}:#{port}"
end
end
def default_group_for(username)
user = Etc.getpwnam(username)
group = Etc.getgrgid(user.gid)
return group.name
end
#################
end
end # module Standalone
end # module PhusionPassenger
|