1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Merge branch 'fix_up_config_defaults' of github.com:jalevin/puma into fix_up_config_defaults

This commit is contained in:
Jeff Levin 2020-04-12 22:42:43 -08:00
commit b257dfe422
78 changed files with 1034 additions and 1499 deletions

102
.github/workflows/puma.yml vendored Normal file
View file

@ -0,0 +1,102 @@
name: CI
on: [push, pull_request]
jobs:
build:
name: >-
${{ matrix.os }} ${{ matrix.ruby }}
env:
CI: true
TESTOPTS: -v
runs-on: ${{ matrix.os }}-latest
if: |
!(contains(github.event.pull_request.title, '[ci skip]')
|| contains(github.event.head_commit.message, '[ci skip]'))
strategy:
fail-fast: false
matrix:
os: [ ubuntu, macos, windows ]
ruby: [ 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head, jruby, truffleruby-head ]
include:
- { os: windows , ruby: mingw }
exclude:
- { os: windows , ruby: head }
- { os: windows , ruby: jruby }
- { os: windows , ruby: truffleruby-head }
steps:
- name: repo checkout
uses: actions/checkout@v2
- name: load ruby, ragel
uses: MSP-Greg/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler: 1
apt-get: ragel
brew: ragel
mingw: _upgrade_ openssl ragel
- name: bundle install
shell: pwsh
run: |
# update RubyGems in Ruby 2.2, bundle install
if ('${{ matrix.ruby }}' -lt '2.3') {
gem update --system 2.7.10 --no-document
}
bundle install --jobs 4 --retry 3 --path=.bundle/puma
- name: compile
run: bundle exec rake compile
- name: rubocop
if: startsWith(matrix.ruby, '2.')
run: bundle exec rake rubocop
- name: test
timeout-minutes: 10
run: bundle exec rake test:all
allowedFailures:
name: >-
optional: ${{ matrix.os }} ${{ matrix.ruby }}
env:
CI: true
TESTOPTS: -v
runs-on: ${{ matrix.os }}-latest
if: |
!(contains(github.event.pull_request.title, '[ci skip]')
|| contains(github.event.head_commit.message, '[ci skip]'))
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu, ruby: jruby-head }
steps:
- name: repo checkout
uses: actions/checkout@v2
- name: load ruby, ragel
uses: MSP-Greg/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
apt-get: ragel
brew: ragel
- name: bundle install
run: |
bundle install --jobs 4 --retry 3 --path=.bundle/puma
- name: compile
continue-on-error: true
run: bundle exec rake compile
- name: test
timeout-minutes: 10
continue-on-error: true
if: success()
run: bundle exec rake

View file

@ -1,106 +0,0 @@
name: Puma
on: [push, pull_request]
jobs:
build:
name: >-
${{ matrix.os }}, ${{ matrix.ruby }}
env:
CI: true
TESTOPTS: -v
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-18.04, macos ]
ruby: [ 2.3, 2.4, 2.5, 2.6, 2.7, ruby-head ]
steps:
- name: repo checkout
uses: actions/checkout@v2
- name: load ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: ubuntu & macos - install ragel
if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos')
run: |
if [ "${{ matrix.os }}" == "macos" ]; then
brew install ragel
else
sudo apt-get -qy install ragel
fi
- name: bundle install
run: bundle install --jobs 4 --retry 3 --path=.bundle/puma
- name: compile
run: bundle exec rake compile
# two test steps due to pre-installed Bundler not working with
# frozen strings before 2.0
- name: test with frozen string
timeout-minutes: 10
env:
RUBYOPT: --enable-frozen-string-literal
if: matrix.ruby >= '2.6'
run: bundle exec rake
- name: test without frozen string
timeout-minutes: 10
if: matrix.ruby < '2.6'
run: bundle exec rake
win32:
name: >-
${{ matrix.os }}, ${{ matrix.ruby }}
env:
CI: true
TESTOPTS: -v
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest ]
ruby: [ 2.3, 2.4, 2.5, 2.6, 2.7, ruby-head ]
steps:
- name: repo checkout
uses: actions/checkout@v2
- name: windows - update MSYS2, openssl, ragel
if: startsWith(matrix.os, 'windows')
uses: MSP-Greg/actions-ruby@v1
with:
base: update
mingw: openssl ragel
ruby-version: ${{ matrix.ruby }}
- name: bundle install
run: bundle install --jobs 4 --retry 3 --path=.bundle/puma
- name: compile
if: matrix.ruby >= '2.4'
run: bundle exec rake compile
# Ruby 2.3 uses a OpenSSL package that is not MSYS2
- name: compile
if: matrix.ruby == '2.3'
run: bundle exec rake compile -- --with-openssl-dir=C:/openssl-win
- name: test with frozen string
timeout-minutes: 10
env:
RUBYOPT: --enable-frozen-string-literal
if: matrix.ruby >= '2.6'
run: bundle exec rake
- name: test without frozen string
timeout-minutes: 10
if: matrix.ruby < '2.6'
run: bundle exec rake

View file

@ -6,6 +6,8 @@ AllCops:
Exclude:
- 'tmp/**/*'
- 'vendor/**/*'
- 'examples/**/*'
- 'pkg/**/*'
- 'Rakefile'
Layout/SpaceAfterColon:
@ -53,3 +55,24 @@ Style/TrailingCommaInArguments:
Performance:
Enabled: true
Metrics/ParameterLists:
Max: 7
Performance/RedundantMatch:
Enabled: true
Performance/RedundantBlockCall:
Enabled: true
Performance/StringReplacement:
Enabled: true
Layout/AccessModifierIndentation:
EnforcedStyle: indent
Style/WhileUntilModifier:
Enabled: true
Style/TernaryParentheses:
Enabled: true

View file

@ -39,19 +39,7 @@ Layout/EmptyLinesAroundModuleBody:
# 5 offenses
Layout/IndentationWidth:
Enabled: true
# 3 offenses
Layout/AccessModifierIndentation:
EnforcedStyle: indent
# 2 offenses
Style/WhileUntilModifier:
Enabled: true
# 1 offense
Style/TernaryParentheses:
Enabled: true
# >200 offenses for 80
# 58 offenses for 100
# 18 offenses for 120
@ -64,19 +52,3 @@ Metrics/LineLength:
- https
IgnoreCopDirectives: false
IgnoredPatterns: []
# 1 offense
Metrics/ParameterLists:
Max: 5
# 1 offense
Performance/RedundantMatch:
Enabled: true
# 1 offense
Performance/RedundantBlockCall:
Enabled: true
# 1 offense
Performance/StringReplacement:
Enabled: true

View file

@ -1,54 +0,0 @@
dist: bionic
language: ruby
cache: bundler
before_install:
# rubygems 2.7.8 and greater include bundler, leave 2.6.0 untouched
- |
r_eng="$(ruby -e 'STDOUT.write RUBY_ENGINE')";
rv="$(ruby -e 'STDOUT.write RUBY_VERSION')";
if [ "$r_eng" == "ruby" ]; then
if [ "$rv" \< "2.3" ]; then gem update --system 2.7.10 --no-document
elif [ "$rv" \< "2.6" ]; then gem update --system --no-document --conservative
fi
fi
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
brew update
brew install ragel
fi
- ruby -v && gem --version && bundle version
before_script:
- if [ "$jit" == "yes" ]; then export RUBYOPT=--jit ; fi ; echo RUBYOPT is $RUBYOPT
- bundle exec rake compile
script:
- bundle exec rake
matrix:
fast_finish: true
include:
- rvm: 2.2.10
dist: trusty
env:
- OS="Trusty 14.04 OpenSSL 1.0.1"
- RUBYOPT=""
- rvm: jruby-9.2.10.0
env:
- JRUBY_OPTS="--debug"
- JAVA_OPTS="--add-opens java.base/sun.nio.ch=org.jruby.dist --add-opens java.base/java.io=org.jruby.dist --add-opens java.base/java.util.zip=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.security.cert=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED"
- rvm: jruby-head
env:
- JRUBY_OPTS="--debug"
- JAVA_OPTS="--add-opens java.base/sun.nio.ch=org.jruby.dist --add-opens java.base/java.io=org.jruby.dist --add-opens java.base/java.util.zip=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.security.cert=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED"
- rvm: truffleruby
allow_failures:
- rvm: jruby-9.2.10.0
- rvm: jruby-head
- rvm: truffleruby
env:
global:
- TESTOPTS="-v"
- RUBYOPT="--enable-frozen-string-literal"

View file

@ -8,8 +8,8 @@ There are lots of ways to contribute to puma. Some examples include:
* creating a [bug report] or [feature request]
* verifying [existing bug reports] and adding [reproduction steps]
* reviewing [pull requests] and testing the changes on your own machine
* writing or editing documentation
* reviewing [pull requests] and testing the changes locally, on your own machine
* writing or editing [documentation]
* improving test coverage
* fixing a [reproducing bug] or adding a new feature
@ -17,14 +17,17 @@ There are lots of ways to contribute to puma. Some examples include:
[feature request]: https://github.com/puma/puma/issues/new?template=feature_request.md
[existing bug reports]: https://github.com/puma/puma/issues?q=is%3Aopen+is%3Aissue+label%3Aneeds-repro
[pull requests]: https://github.com/puma/puma/pulls
[documentation]: https://github.com/puma/puma/tree/master/docs
[reproduction steps]: https://github.com/puma/puma/blob/CONTRIBUTING.md#reproduction-steps
[reproducing bug]: https://github.com/puma/puma/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abug
Newbies welcome! We would be happy to help you make your first contribution to a F/OSS project.
## Setup
Clone down the Puma repository.
You will need to install [ragel] to generate puma's extension code.
You will need to install [ragel] (use Ragel version 7.0.0.9) to generate puma's extension code.
macOS:
@ -68,7 +71,7 @@ bundle exec rake test:all
To run a single test file:
```sh
ruby -Ilib test/test_binder.rb
bundle exec ruby test/test_binder.rb
```
Or use [`m`](https://github.com/qrush/m):
@ -112,7 +115,7 @@ bundle exec puma -C <path/to/config.rb> <path/to/rackup.ru>
```
As an example, using one of the test rack apps:
[`test/rackup/hello.ru`][rackup], and one of the test config files:
[`test/rackup/hello.ru`][rackup file], and one of the test config files:
[`test/config/settings.rb`][config], you would run the test app with:
```sh
@ -144,6 +147,7 @@ check](https://github.com/puma/puma/pull/1991).
Puma can be a bit intimidating for your first contribution because there's a lot of concepts here that you've probably never had to think about before - Rack, sockets, forking, threads etc. Here are some helpful links for learning more about things related to Puma:
* [Puma's Architecture docs](https://github.com/puma/puma/blob/master/docs/architecture.md)
* [The Rack specification](https://www.rubydoc.info/github/rack/rack/file/SPEC)
* The Ruby docs for IO.pipe, TCPServer/Socket.
* [nio4r documentation](https://github.com/socketry/nio4r/wiki/Getting-Started)

View file

@ -6,7 +6,7 @@ gem "rdoc"
gem "rake-compiler"
gem "nio4r", "~> 2.0"
gem "rack", "< 3.0"
gem "rack", "~> 1.6"
gem "minitest", "~> 5.11"
gem "minitest-retry"
gem "minitest-proveit"

View file

@ -1,41 +1,54 @@
## Master
* Features
* Improve SSL connection closing in Puma::ControlCLI (#2211)
* Add pumactl `thread-backtraces` command to print thread backtraces (#2053)
* Configuration: `environment` is read from `RAILS_ENV`, if `RACK_ENV` can't be found (#2022)
* Do not set user_config to quiet by default to allow for file config (#2074)
* `Puma.stats` now returns a Hash instead of a JSON string (#2086)
* `GC.compact` is called before fork if available (#2093)
* Add `requests_count` to workers stats. (#2106)
* Changed #connected_port to #connected_ports (#2076)
* Increases maximum URI path length from 2048 to 8196 bytes (#2167)
* Force shutdown responses can be overridden by using the `lowlevel_error_handler` config (#2203)
* Deprecations, Removals and Breaking API Changes
* `Puma.stats` now returns a Hash instead of a JSON string (#2086)
* `--control` has been removed. Use `--control-url` (#1487)
* `worker_directory` has been removed. Use `directory`
* min_threads now set by environment variables PUMA_MIN_THREADS and MIN_THREADS
* max_threads now set by environment variables PUMA_MAX_THREADS and MAX_THREADS
* max_threads default to 5 in MRI or 16 for all other interpretters
* preload by default if workers > 1 and interpretter supports workers
* `tcp_mode` has been removed without replacement. (#2169)
* Daemonization has been removed without replacement. (#2170)
* Changed #connected_port to #connected_ports (#2076)
* Bugfixes
* Windows update extconf.rb for use with ssp and varied Ruby/MSYS2 combinations (#2069)
* Ensure control server Unix socket is closed on shutdown (#2112)
* Preserve `BUNDLE_GEMFILE` env var when using `prune_bundler` (#1893)
* Send 408 request timeout even when queue requests is disabled (#2119)
* Rescue IO::WaitReadable instead of EAGAIN for blocking read (#2121)
* Ensure `BUNDLE_GEMFILE` is unspecified in workers if unspecified in master when using `prune_bundler` (#2154)
* Rescue and log exceptions in hooks defined by users (on_worker_boot, after_worker_fork etc) (#1551)
* Read directly from the socket in #read_and_drop to avoid raising further SSL errors (#2198)
* Set `Connection: closed` header when queue requests is disabled (#2216)
* Pass queued requests to thread pool on server shutdown (#2122)
* Refactor
* Remove unused loader argument from Plugin initializer (#2095)
* Simplify `Configuration.random_token` and remove insecure fallback (#2102)
* Simplify `Runner#start_control` URL parsing (#2111)
* Removed the IOBuffer extension and replaced with Ruby (#1980)
* Update `Rack::Handler::Puma.run` to use `**options` (#2189)
## 4.3.3 and 3.12.4 / 2020-02-28
* Bugfixes
* Fix: Fixes a problem where we weren't splitting headers correctly on newlines (#2132)
* Security
* Fix: Prevent HTTP Response splitting via CR in early hints.
## 4.3.2 and 3.12.3 / 2020-02-27
* Bugfixes
* Fix: Fixes a problem where we weren't splitting headers correctly on newlines (#2132)
* Security
* Fix: Prevent HTTP Response splitting via CR in early hints. CVE-2020-5249.
## 4.3.2 and 3.12.3 / 2020-02-27 (YANKED)
* Security
* Fix: Prevent HTTP Response splitting via CR/LF in header values. CVE-2020-5247.

View file

@ -4,12 +4,11 @@
# Puma: A Ruby Web Server Built For Concurrency
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/puma/puma?utm\_source=badge&utm\_medium=badge&utm\_campaign=pr-badge)
[![Actions Build Status](https://github.com/puma/puma/workflows/Puma/badge.svg)](https://github.com/puma/puma/actions)
[![Travis Build Status](https://travis-ci.org/puma/puma.svg?branch=master)](https://travis-ci.org/puma/puma)
[![Code Climate](https://codeclimate.com/github/puma/puma.svg)](https://codeclimate.com/github/puma/puma)
[![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=puma&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=puma&package-manager=bundler&version-scheme=semver)
[![StackOverflow](http://img.shields.io/badge/stackoverflow-Puma-blue.svg)]( http://stackoverflow.com/questions/tagged/puma )
Puma is a **simple, fast, multi-threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications**.
@ -270,7 +269,7 @@ It is common to use process monitors with Puma. Modern process monitors like sys
provide continuous monitoring and restarts for increased
reliability in production environments:
* [tools/jungle](https://github.com/puma/puma/tree/master/tools/jungle) for sysvinit (init.d) and upstart
* [docs/jungle](https://github.com/puma/puma/tree/master/docs/jungle) for rc.d and upstart
* [docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md)
## Community Extensions

View file

@ -0,0 +1,8 @@
bundle exec ruby bin/puma \
-t 4 -b "ssl://localhost:9292?keystore=examples/puma/keystore.jks&keystore-pass=blahblah&verify_mode=none" \
test/rackup/realistic_response.ru &
PID1=$!
sleep 5
wrk -c 4 -d 30 --latency https://localhost:9292
kill $PID1

View file

@ -0,0 +1,8 @@
bundle exec ruby bin/puma \
-t 4 -b "ssl://localhost:9292?key=examples%2Fpuma%2Fpuma_keypair.pem&cert=examples%2Fpuma%2Fcert_puma.pem&verify_mode=none" \
test/rackup/realistic_response.ru &
PID1=$!
sleep 5
wrk -c 4 -d 30 --latency https://localhost:9292
kill $PID1

View file

@ -74,7 +74,9 @@ thread to become available.
* haproxy: `%Th` (TLS handshake time) and `%Ti` (idle time before request) can
can also be added as headers.
## Daemonizing
## Should I daemonize?
Daemonization was removed in Puma 5.0. For alternatives, continue reading.
I prefer to not daemonize my servers and use something like `runit` or `upstart` to
monitor them as child processes. This gives them fast response to crashes and

13
docs/jungle/README.md Normal file
View file

@ -0,0 +1,13 @@
# Puma as a service
## Upstart
See `/docs/jungle/upstart` for Ubuntu's upstart scripts.
## Systemd
See [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md).
## rc.d
See `/docs/jungle/rc.d` for FreeBSD's rc.d scripts

View file

@ -13,9 +13,7 @@ desired, using an application or instance specific name.
Note that this uses the systemd preferred "simple" type where the
start command remains running in the foreground (does not fork and
exit). See also, the
[Alternative Forking Configuration](#alternative-forking-configuration)
below.
exit).
~~~~ ini
[Unit]
@ -209,66 +207,6 @@ Apr 07 08:40:19 hx puma[28320]: * Activated ssl://0.0.0.0:9234?key=key.pem&cert=
Apr 07 08:40:19 hx puma[28320]: Use Ctrl-C to stop
~~~~
## Alternative Forking Configuration
Other systems/tools might expect or need puma to be run as a
"traditional" forking server, for example so that the `pumactl`
command can be used directly and outside of systemd for
stop/start/restart. This use case is incompatible with systemd socket
activation, so it should not be configured. Below is an alternative
puma.service config sample, using `Type=forking` and the `--daemon`
flag in `ExecStart`. Here systemd is playing a role more equivalent to
SysV init.d, where it is responsible for starting Puma on boot
(multi-user.target) and stopping it on shutdown, but is not performing
continuous restarts. Therefore running Puma in cluster mode, where the
master can restart workers, is highly recommended. See the systemd
[Restart] directive for details.
~~~~ ini
[Unit]
Description=Puma HTTP Forking Server
After=network.target
[Service]
# Background process configuration (use with --daemon in ExecStart)
Type=forking
# Preferably configure a non-privileged user
# User=
# The path to the puma application root
# Also replace the "<WD>" place holders below with this path.
WorkingDirectory=
# The command to start Puma
# (replace "<WD>" below)
ExecStart=bundle exec puma -C <WD>/shared/puma.rb --daemon
# The command to stop Puma
# (replace "<WD>" below)
ExecStop=bundle exec pumactl -S <WD>/shared/tmp/pids/puma.state stop
# Path to PID file so that systemd knows which is the master process
PIDFile=<WD>/shared/tmp/pids/puma.pid
# Should systemd restart puma?
# Use "no" (the default) to ensure no interference when using
# stop/start/restart via `pumactl`. The "on-failure" setting might
# work better for this purpose, but you must test it.
# Use "always" if only `systemctl` is used for start/stop/restart, and
# reconsider if you actually need the forking config.
Restart=no
# `puma_ctl restart` wouldn't work without this. It's because `pumactl`
# changes PID on restart and systemd stops the service afterwards
# because of the PID change. This option prevents stopping after PID
# change.
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
~~~~
### capistrano3-puma
By default,

View file

@ -1,96 +0,0 @@
# TCP mode
Puma also could be used as a TCP server to process incoming TCP
connections.
## Configuration
TCP mode can be enabled with CLI option `--tcp-mode`:
```
$ puma --tcp-mode
```
Default ip and port to listen to are `0.0.0.0` and `9292`. You can configure
them with `--port` and `--bind` options:
```
$ puma --tcp-mode --bind tcp://127.0.0.1:9293
$ puma --tcp-mode --port 9293
```
TCP mode could be set with a configuration file as well with `tcp_mode`
and `tcp_mode!` methods:
```
# config/puma.rb
tcp_mode
```
When Puma starts in the TCP mode it prints the corresponding message:
```
puma --tcp-mode
Puma starting in single mode...
...
* Mode: Lopez Express (tcp)
```
## How to declare an application
An application to process TCP connections should be declared as a
callable object which accepts `env` and `socket` arguments.
`env` argument is a Hash with following structure:
```ruby
{ "thread" => {}, "REMOTE_ADDR" => "127.0.0.1:51133", "log" => "#<Proc:0x000..." }
```
It consists of:
* `thread` - a Hash for each thread in the thread pool that could be
used to store information between requests
* `REMOTE_ADDR` - a client ip address
* `log` - a proc object to write something down
`log` object could be used this way:
```ruby
env['log'].call('message to log')
#> 19/Oct/2019 20:28:53 - 127.0.0.1:51266 - message to log
```
## Example of an application
Let's look at an example of a simple application which just echoes
incoming string:
```ruby
# config/puma.rb
app do |env, socket|
s = socket.gets
socket.puts "Echo #{s}"
end
```
We can easily access the TCP server with `telnet` command and receive an
echo:
```shell
telnet 0.0.0.0 9293
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
sssss
Echo sssss
^CConnection closed by foreign host.
```
## Socket management
After the application finishes, Puma closes the socket. In order to
prevent this, the application should set `env['detach'] = true`.

View file

@ -6,7 +6,6 @@ import org.jruby.Ruby;
import org.jruby.runtime.load.BasicLibraryService;
import org.jruby.puma.Http11;
import org.jruby.puma.IOBuffer;
import org.jruby.puma.MiniSSL;
public class PumaHttp11Service implements BasicLibraryService {

View file

@ -2,7 +2,7 @@ require 'mkmf'
dir_config("puma_http11")
if $mingw
if $mingw && RUBY_VERSION >= '2.4'
append_cflags '-fstack-protector-strong -D_FORTIFY_SOURCE=2'
append_ldflags '-fstack-protector-strong -l:libssp.a'
have_library 'ssp'

View file

@ -30,8 +30,8 @@ public class Http11 extends RubyObject {
public final static String MAX_REQUEST_URI_LENGTH_ERR = "HTTP element REQUEST_URI is longer than the 12288 allowed length.";
public final static int MAX_FRAGMENT_LENGTH = 1024;
public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 1024 allowed length.";
public final static int MAX_REQUEST_PATH_LENGTH = 2048;
public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 2048 allowed length.";
public final static int MAX_REQUEST_PATH_LENGTH = 8192;
public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 8192 allowed length.";
public final static int MAX_QUERY_STRING_LENGTH = 1024 * 10;
public final static String MAX_QUERY_STRING_LENGTH_ERR = "HTTP element QUERY_STRING is longer than the 10240 allowed length.";
public final static int MAX_HEADER_LENGTH = 1024 * (80 + 32);
@ -197,7 +197,7 @@ public class Http11 extends RubyObject {
validateMaxLength(runtime, parser.nread,MAX_HEADER_LENGTH, MAX_HEADER_LENGTH_ERR);
if(hp.has_error()) {
throw newHTTPParserError(runtime, "Invalid HTTP format, parsing fails.");
throw newHTTPParserError(runtime, "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
} else {
return runtime.newFixnum(parser.nread);
}

View file

@ -54,7 +54,7 @@ DEF_MAX_LENGTH(FIELD_NAME, 256);
DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
DEF_MAX_LENGTH(REQUEST_PATH, 2048);
DEF_MAX_LENGTH(REQUEST_PATH, 8196);
DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
@ -367,7 +367,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
VALIDATE_MAX_LENGTH(puma_parser_nread(http), HEADER);
if(puma_parser_has_error(http)) {
rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails.");
rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
} else {
return INT2FIX(puma_parser_nread(http));
}

View file

@ -11,7 +11,7 @@ module Puma
class Binder
include Puma::Const
RACK_VERSION = [1,3].freeze
RACK_VERSION = [1,6].freeze
def initialize(events)
@events = events
@ -43,7 +43,8 @@ module Puma
@ios = []
end
attr_reader :ios
attr_reader :ios, :listeners, :unix_paths, :proto_env, :envs, :activated_sockets, :inherited_fds
attr_writer :ios, :listeners
def env(sock)
@envs.fetch(sock, @proto_env)
@ -53,37 +54,36 @@ module Puma
@ios.each { |i| i.close }
end
def import_from_env
remove = []
def connected_ports
ios.map { |io| io.addr[1] }.uniq
end
ENV.each do |k,v|
if k =~ /PUMA_INHERIT_\d+/
fd, url = v.split(":", 2)
@inherited_fds[url] = fd.to_i
remove << k
elsif k == 'LISTEN_FDS' && ENV['LISTEN_PID'].to_i == $$
v.to_i.times do |num|
fd = num + 3
sock = TCPServer.for_fd(fd)
begin
key = [ :unix, Socket.unpack_sockaddr_un(sock.getsockname) ]
rescue ArgumentError
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
if addr =~ /\:/
addr = "[#{addr}]"
end
key = [ :tcp, addr, port ]
end
@activated_sockets[key] = sock
@events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
end
remove << k << 'LISTEN_PID'
def create_inherited_fds(env_hash)
env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v|
fd, url = v.split(":", 2)
@inherited_fds[url] = fd.to_i
end.keys # pass keys back for removal
end
# systemd socket activation.
# LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4.
# LISTEN_PID = PID of the service process, aka us
# see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html
def create_activated_fds(env_hash)
return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
env_hash['LISTEN_FDS'].to_i.times do |index|
sock = TCPServer.for_fd(socket_activation_fd(index))
key = begin # Try to parse as a path
[:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
rescue ArgumentError # Try to parse as a port/ip
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
addr = "[#{addr}]" if addr =~ /\:/
[:tcp, addr, port]
end
@activated_sockets[key] = sock
@events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
end
remove.each do |k|
ENV.delete k
end
["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
end
def parse(binds, logger, log_msg = 'Listening')
@ -204,12 +204,6 @@ module Puma
end
end
def loopback_addresses
Socket.ip_address_list.select do |addrinfo|
addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
end.map { |addrinfo| addrinfo.ip_address }.uniq
end
# Tell the server to listen on host +host+, port +port+.
# If +optimize_for_latency+ is true (the default) then clients connecting
# will be optimized for latency over throughput.
@ -237,10 +231,6 @@ module Puma
tcp_server
end
def connected_ports
ios.map { |io| io.addr[1] }
end
def inherit_tcp_listener(host, port, fd)
if fd.kind_of? TCPServer
s = fd
@ -361,26 +351,37 @@ module Puma
end
def close_listeners
@listeners.each do |l, io|
io.close
listeners.each do |l, io|
io.close unless io.closed? # Ruby 2.2 issue
uri = URI.parse(l)
next unless uri.scheme == 'unix'
unix_path = "#{uri.host}#{uri.path}"
File.unlink unix_path if @unix_paths.include? unix_path
File.unlink unix_path if unix_paths.include? unix_path
end
end
def close_unix_paths
@unix_paths.each { |up| File.unlink(up) if File.exist? up }
end
def redirects_for_restart
redirects = {:close_others => true}
@listeners.each_with_index do |(l, io), i|
ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
redirects[io.to_i] = io.to_i
end
redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h
redirects[:close_others] = true
redirects
end
def redirects_for_restart_env
listeners.each_with_object({}).with_index do |(listen, memo), i|
memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}"
end
end
private
def loopback_addresses
Socket.ip_address_list.select do |addrinfo|
addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
end.map { |addrinfo| addrinfo.ip_address }.uniq
end
def socket_activation_fd(int)
int + 3 # 3 is the magic number you add to follow the SA protocol
end
end
end

View file

@ -80,7 +80,7 @@ module Puma
@launcher.run
end
private
private
def unsupported(str)
@events.error(str)
raise UnsupportedOption
@ -117,11 +117,6 @@ module Puma
@control_options[:auth_token] = arg
end
o.on "-d", "--daemon", "Daemonize the server into the background" do
user_config.daemonize
user_config.quiet
end
o.on "--debug", "Log lowlevel debugging information" do
user_config.debug
end
@ -187,10 +182,6 @@ module Puma
end
end
o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
user_config.tcp_mode!
end
o.on "--early-hints", "Enable early hints support" do
user_config.early_hints
end

View file

@ -246,7 +246,12 @@ module Puma
def finish(timeout)
return true if @ready
until try_to_finish
unless IO.select([@to_io], nil, nil, timeout)
can_read = begin
IO.select([@to_io], nil, nil, timeout)
rescue ThreadPool::ForceShutdown
nil
end
unless can_read
write_error(408) if in_data_phase
raise ConnectionError
end
@ -265,7 +270,7 @@ module Puma
return @peerip if @peerip
if @remote_addr_header
hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
@peerip = hdr
return hdr
end
@ -273,6 +278,18 @@ module Puma
@peerip ||= @io.peeraddr.last
end
# Returns true if the persistent connection can be closed immediately
# without waiting for the configured idle/shutdown timeout.
def can_close?
# Allow connection to close if it's received at least one full request
# and hasn't received any data for a future request.
#
# From RFC 2616 section 8.1.4:
# Servers SHOULD always respond to at least one request per connection,
# if at all possible.
@requests_served > 0 && @parsed_bytes == 0
end
private
def setup_body

View file

@ -137,7 +137,7 @@ module Puma
diff.times do
idx = next_worker_index
@launcher.config.run_hooks :before_worker_fork, idx
@launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
pid = fork { worker(idx, master) }
if !pid
@ -149,7 +149,7 @@ module Puma
debug "Spawned worker: #{pid}"
@workers << Worker.new(idx, pid, @phase, @options)
@launcher.config.run_hooks :after_worker_fork, idx
@launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
end
if diff > 0
@ -268,7 +268,7 @@ module Puma
# Invoke any worker boot hooks so they can get
# things in shape before booting the app.
@launcher.config.run_hooks :before_worker_boot, index
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
server = start_server
@ -310,7 +310,7 @@ module Puma
# Invoke any worker shutdown hooks so they can prevent the worker
# exiting until any background operations are completed
@launcher.config.run_hooks :before_worker_shutdown, index
@launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
ensure
@worker_write << "t#{Process.pid}\n" rescue nil
@worker_write.close
@ -469,12 +469,7 @@ module Puma
#
@check_pipe, @suicide_pipe = Puma::Util.pipe
if daemon?
log "* Daemonizing..."
Process.daemon(true)
else
log "Use Ctrl-C to stop"
end
log "Use Ctrl-C to stop"
redirect_io
@ -486,7 +481,7 @@ module Puma
@master_read, @worker_write = read, @wakeup
@launcher.config.run_hooks :before_fork, nil
@launcher.config.run_hooks :before_fork, nil, @launcher.events
GC.compact if GC.respond_to?(:compact)
spawn_workers

View file

@ -258,14 +258,6 @@ module Puma
def app
found = options[:app] || load_rackup
if @options[:mode] == :tcp
require 'puma/tcp_logger'
logger = @options[:logger]
quiet = !@options[:log_requests]
return TCPLogger.new(logger, found, quiet)
end
if @options[:log_requests]
require 'puma/commonlogger'
logger = @options[:logger]
@ -288,8 +280,15 @@ module Puma
@plugins.create name
end
def run_hooks(key, arg)
@options.all_of(key).each { |b| b.call arg }
def run_hooks(key, arg, events)
@options.all_of(key).each do |b|
begin
b.call arg
rescue => e
events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
events.debug e.backtrace.join("\n")
end
end
end
def self.temp_path

View file

@ -175,7 +175,6 @@ module Puma
PORT_443 = "443".freeze
LOCALHOST = "localhost".freeze
LOCALHOST_IP = "127.0.0.1".freeze
LOCALHOST_ADDR = "127.0.0.1:0".freeze
SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
HTTP_11 = "HTTP/1.1".freeze

View file

@ -146,8 +146,9 @@ module Puma
require 'openssl'
OpenSSL::SSL::SSLSocket.new(
TCPSocket.new(uri.host, uri.port),
OpenSSL::SSL::SSLContext.new
).tap(&:connect)
OpenSSL::SSL::SSLContext.new)
.tap { |ssl| ssl.sync_close = true } # default is false
.tap(&:connect)
when "tcp"
TCPSocket.new uri.host, uri.port
when "unix"
@ -191,7 +192,13 @@ module Puma
message response.last if PRINTABLE_COMMANDS.include?(@command)
end
ensure
server.close if server && !server.closed?
if server
if uri.scheme == "ssl"
server.sysclose
else
server.close unless server.closed?
end
end
end
def send_signal
@ -263,7 +270,7 @@ module Puma
exit 1
end
private
private
def start
require 'puma/cli'

View file

@ -210,20 +210,6 @@ module Puma
@options[:clean_thread_locals] = which
end
# Daemonize the server into the background. It's highly recommended to
# use this in combination with +pidfile+ and +stdout_redirect+.
#
# The default is "false".
#
# @example
# daemonize
#
# @example
# daemonize false
def daemonize(which=true)
@options[:daemon] = which
end
# When shutting down, drain the accept socket of pending
# connections and process them. This loops over the accept
# socket until there are no more read events and then stops
@ -325,12 +311,6 @@ module Puma
@options[:rackup] = path.to_s
end
# Run Puma in TCP mode
#
def tcp_mode!
@options[:mode] = :tcp
end
def early_hints(answer=true)
@options[:early_hints] = answer
end
@ -536,11 +516,6 @@ module Puma
@options[:directory] = dir.to_s
end
# Run the app as a raw TCP app instead of an HTTP rack app.
def tcp_mode
@options[:mode] = :tcp
end
# Preload the application before starting the workers; this conflicts with
# phased restart feature. This is off by default.
#

View file

@ -22,63 +22,5 @@ module Puma
execlp(cmd, *argv)
raise SystemCallError.new(FFI.errno)
end
PermKey = 'PUMA_DAEMON_PERM'
RestartKey = 'PUMA_DAEMON_RESTART'
# Called to tell things "Your now always in daemon mode,
# don't try to reenter it."
#
def self.perm_daemonize
ENV[PermKey] = "1"
end
def self.daemon?
ENV.key?(PermKey) || ENV.key?(RestartKey)
end
def self.daemon_init
return true if ENV.key?(PermKey)
return false unless ENV.key? RestartKey
master = ENV[RestartKey]
# In case the master disappears early
begin
Process.kill "SIGUSR2", master.to_i
rescue SystemCallError => e
end
ENV[RestartKey] = ""
setsid
null = File.open "/dev/null", "w+"
STDIN.reopen null
STDOUT.reopen null
STDERR.reopen null
true
end
def self.daemon_start(dir, argv)
ENV[RestartKey] = Process.pid.to_s
if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
ENV['JRUBY_OPTS'] = k
end
cmd = argv.first
argv = ([:string] * argv.size).zip(argv).flatten
argv << :string
argv << nil
chdir(dir)
ret = fork
return ret if ret != 0
execlp(cmd, *argv)
raise SystemCallError.new(FFI.errno)
end
end
end

View file

@ -48,7 +48,8 @@ module Puma
@config = conf
@binder = Binder.new(@events)
@binder.import_from_env
@binder.create_inherited_fds(ENV).each { |k| ENV.delete k }
@binder.create_activated_fds(ENV).each { |k| ENV.delete k }
@environment = conf.environment
@ -69,10 +70,6 @@ module Puma
unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
end
if @options[:daemon] && Puma.windows?
unsupported 'daemon mode not supported on Windows'
end
Dir.chdir(@restart_dir)
prune_bundler if prune_bundler?
@ -184,7 +181,7 @@ module Puma
when :exit
# nothing
end
@binder.close_unix_paths
close_binder_listeners unless @status == :restart
end
# Return all tcp ports the launcher may be using, TCP or SSL
@ -202,6 +199,7 @@ module Puma
end
def close_binder_listeners
@runner.close_control_listeners
@binder.close_listeners
end
@ -236,7 +234,7 @@ module Puma
end
def restart!
@config.run_hooks :on_restart, self
@config.run_hooks :on_restart, self, @events
if Puma.jruby?
close_binder_listeners
@ -252,6 +250,7 @@ module Puma
else
argv = restart_args
Dir.chdir(@restart_dir)
ENV.update(@binder.redirects_for_restart_env)
argv += [@binder.redirects_for_restart]
Kernel.exec(*argv)
end
@ -297,8 +296,8 @@ module Puma
log '* Pruning Bundler environment'
home = ENV['GEM_HOME']
bundle_gemfile = ENV['BUNDLE_GEMFILE']
Bundler.with_clean_env do
bundle_gemfile = Bundler.original_env['BUNDLE_GEMFILE']
with_unbundled_env do
ENV['GEM_HOME'] = home
ENV['BUNDLE_GEMFILE'] = bundle_gemfile
ENV['PUMA_BUNDLER_PRUNED'] = '1'
@ -454,10 +453,12 @@ module Puma
end
begin
Signal.trap "SIGINFO" do
thread_status do |name, backtrace|
@events.log name
@events.log backtrace.map { |bt| " #{bt}" }
unless Puma.jruby? # INFO in use by JVM already
Signal.trap "SIGINFO" do
thread_status do |name, backtrace|
@events.log name
@events.log backtrace.map { |bt| " #{bt}" }
end
end
end
rescue Exception
@ -472,5 +473,14 @@ module Puma
raise "#{feature} is not supported on your version of RubyGems. " \
"You must have RubyGems #{min_version}+ to use this feature."
end
def with_unbundled_env
bundler_ver = Gem::Version.new(Bundler::VERSION)
if bundler_ver < Gem::Version.new('2.1.0')
Bundler.with_clean_env { yield }
else
Bundler.with_unbundled_env { yield }
end
end
end
end

View file

@ -125,11 +125,14 @@ module Puma
def read_and_drop(timeout = 1)
return :timeout unless IO.select([@socket], nil, nil, timeout)
return :eof unless read_nonblock(1024)
:drop
rescue Errno::EAGAIN
# do nothing
:eagain
case @socket.read_nonblock(1024, exception: false)
when nil
:eof
when :wait_readable
:eagain
else
:drop
end
end
def should_drop_bytes?
@ -141,9 +144,7 @@ module Puma
# Read any drop any partially initialized sockets and any received bytes during shutdown.
# Don't let this socket hold this loop forever.
# If it can't send more packets within 1s, then give up.
while should_drop_bytes?
return if [:timeout, :eof].include?(read_and_drop(1))
end
return if [:timeout, :eof].include?(read_and_drop(1)) while should_drop_bytes?
rescue IOError, SystemCallError
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
# nothing

View file

@ -15,7 +15,7 @@ module Puma
# Mimics IO#read with no data.
#
def read(count = nil, _buffer = nil)
(count && count > 0) ? nil : ""
count && count > 0 ? nil : ""
end
def rewind

View file

@ -67,10 +67,6 @@ module Puma::Rack
options[:environment] = e
}
opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
options[:daemonize] = d ? true : false
}
opts.on("-P", "--pid FILE", "file to store PID") { |f|
options[:pid] = ::File.expand_path(f)
}

View file

@ -189,7 +189,12 @@ module Puma
if submon.value == @ready
false
else
submon.value.close
if submon.value.can_close?
submon.value.close
else
# Pass remaining open client connections to the thread pool.
@app_pool << submon.value
end
begin
selector.deregister submon.value
rescue IOError

View file

@ -18,10 +18,6 @@ module Puma
@started_at = Time.now
end
def daemon?
@options[:daemon]
end
def development?
@options[:environment] == "development"
end
@ -68,6 +64,10 @@ module Puma
@control = control
end
def close_control_listeners
@control.binder.close_listeners if @control
end
def ruby_engine
if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
"ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
@ -88,10 +88,6 @@ module Puma
log "* Version #{Puma::Const::PUMA_VERSION} (#{ruby_engine}), codename: #{Puma::Const::CODE_NAME}"
log "* Min threads: #{min_t}, max threads: #{max_t}"
log "* Environment: #{ENV['RACK_ENV']}"
if @options[:mode] == :tcp
log "* Mode: Lopez Express (tcp)"
end
end
def redirected_io?
@ -130,7 +126,6 @@ module Puma
exit 1
end
# Load the app before we daemonize.
begin
@app = @launcher.config.app
rescue Exception => e
@ -154,10 +149,6 @@ module Puma
server.max_threads = max_t
server.inherit_binder @launcher.binder
if @options[:mode] == :tcp
server.tcp_mode!
end
if @options[:early_hints]
server.early_hints = true
end

View file

@ -99,10 +99,6 @@ module Puma
@binder = bind
end
def tcp_mode!
@mode = :tcp
end
# On Linux, use TCP_CORK to better control how the TCP stack
# packetizes our stream. This improves both latency and throughput.
#
@ -176,107 +172,6 @@ module Puma
@thread_pool and @thread_pool.pool_capacity
end
# Lopez Mode == raw tcp apps
def run_lopez_mode(background=true)
@thread_pool = ThreadPool.new(@min_threads,
@max_threads,
Hash) do |client, tl|
io = client.to_io
addr = io.peeraddr.last
if addr.empty?
# Set unix socket addrs to localhost
addr = "127.0.0.1:0"
else
addr = "#{addr}:#{io.peeraddr[1]}"
end
env = { 'thread' => tl, REMOTE_ADDR => addr }
begin
@app.call env, client.to_io
rescue Object => e
STDERR.puts "! Detected exception at toplevel: #{e.message} (#{e.class})"
STDERR.puts e.backtrace
end
client.close unless env['detach']
end
@events.fire :state, :running
if background
@thread = Thread.new do
Puma.set_thread_name "server"
handle_servers_lopez_mode
end
return @thread
else
handle_servers_lopez_mode
end
end
def handle_servers_lopez_mode
begin
check = @check
sockets = [check] + @binder.ios
pool = @thread_pool
while @status == :run
begin
ios = IO.select sockets
ios.first.each do |sock|
if sock == check
break if handle_check
else
begin
if io = sock.accept_nonblock
client = Client.new io, nil
pool << client
end
rescue SystemCallError
# nothing
rescue Errno::ECONNABORTED
# client closed the socket even before accept
begin
io.close
rescue
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
end
end
end
end
rescue Object => e
@events.unknown_error self, e, "Listen loop"
end
end
@events.fire :state, @status
graceful_shutdown if @status == :stop || @status == :restart
rescue Exception => e
STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
STDERR.puts e.backtrace
ensure
begin
@check.close
rescue
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
end
# Prevent can't modify frozen IOError (RuntimeError)
begin
@notify.close
rescue IOError
# no biggy
end
end
@events.fire :state, :done
end
# Runs the server.
#
# If +background+ is true (the default) then a thread is spun
@ -290,12 +185,6 @@ module Puma
@status = :run
if @mode == :tcp
return run_lopez_mode(background)
end
queue_requests = @queue_requests
@thread_pool = ThreadPool.new(@min_threads,
@max_threads,
::Puma::IOBuffer) do |client, buffer|
@ -306,7 +195,7 @@ module Puma
process_now = false
begin
if queue_requests
if @queue_requests
process_now = client.eagerly_finish
else
client.finish(@first_data_timeout)
@ -339,7 +228,7 @@ module Puma
@thread_pool.clean_thread_locals = @options[:clean_thread_locals]
if queue_requests
if @queue_requests
@reactor = Reactor.new self, @thread_pool
@reactor.run_in_thread
end
@ -423,16 +312,17 @@ module Puma
@events.fire :state, @status
graceful_shutdown if @status == :stop || @status == :restart
if queue_requests
@queue_requests = false
@reactor.clear!
@reactor.shutdown
end
graceful_shutdown if @status == :stop || @status == :restart
rescue Exception => e
STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
STDERR.puts e.backtrace
ensure
@check.close
@check.close unless @check.closed? # Ruby 2.2 issue
@notify.close
end
@ -480,7 +370,6 @@ module Puma
close_socket = false
return
when true
return unless @queue_requests
buffer.reset
ThreadPool.clean_thread_locals if clean_thread_locals
@ -498,6 +387,7 @@ module Puma
end
unless client.reset(check_for_more_data)
return unless @queue_requests
close_socket = false
client.set_timeout @persistent_timeout
@reactor.add client
@ -699,17 +589,14 @@ module Puma
return :async
end
rescue ThreadPool::ForceShutdown => e
@events.log "Detected force shutdown of a thread, returning 503"
@events.unknown_error self, e, "Rack app"
status = 503
headers = {}
res_body = ["Request was internally terminated early\n"]
@events.unknown_error self, e, "Rack app", env
@events.log "Detected force shutdown of a thread"
status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@events.unknown_error self, e, "Rack app", env
status, headers, res_body = lowlevel_error(e, env)
status, headers, res_body = lowlevel_error(e, env, 500)
end
content_length = nil
@ -724,10 +611,10 @@ module Puma
line_ending = LINE_END
colon = COLON
http_11 = if env[HTTP_VERSION] == HTTP_11
http_11 = env[HTTP_VERSION] == HTTP_11
if http_11
allow_chunked = true
keep_alive = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
include_keepalive_header = false
# An optimization. The most common response is 200, so we can
# reply with the proper 200 status without having to compute
@ -741,11 +628,9 @@ module Puma
no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
end
true
else
allow_chunked = false
keep_alive = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
include_keepalive_header = keep_alive
# Same optimization as above for HTTP/1.1
#
@ -757,9 +642,12 @@ module Puma
no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
end
false
end
# regardless of what the client wants, we always close the connection
# if running without request queueing
keep_alive &&= @queue_requests
response_hijack = nil
headers.each do |k, vs|
@ -786,10 +674,15 @@ module Puma
end
end
if include_keepalive_header
lines << CONNECTION_KEEP_ALIVE
elsif http_11 && !keep_alive
lines << CONNECTION_CLOSE
# HTTP/1.1 & 1.0 assume different defaults:
# - HTTP 1.0 assumes the connection will be closed if not specified
# - HTTP 1.1 assumes the connection will be kept alive if not specified.
# Only set the header if we're doing something which is not the default
# for this protocol version
if http_11
lines << CONNECTION_CLOSE if !keep_alive
else
lines << CONNECTION_KEEP_ALIVE if keep_alive
end
if no_body
@ -916,19 +809,21 @@ module Puma
# A fallback rack response if +@app+ raises as exception.
#
def lowlevel_error(e, env)
def lowlevel_error(e, env, status=500)
if handler = @options[:lowlevel_error_handler]
if handler.arity == 1
return handler.call(e)
else
elsif handler.arity == 2
return handler.call(e, env)
else
return handler.call(e, env, status)
end
end
if @leak_stack_on_error
[500, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{e.backtrace.join("\n")}"]]
[status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{e.backtrace.join("\n")}"]]
else
[500, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
[status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
end
end

View file

@ -42,64 +42,10 @@ module Puma
@server.stop(true) if @server
end
def jruby_daemon?
daemon? and Puma.jruby?
end
def jruby_daemon_start
require 'puma/jruby_restart'
JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args)
end
def run
already_daemon = false
if jruby_daemon?
require 'puma/jruby_restart'
if JRubyRestart.daemon?
# load and bind before redirecting IO so errors show up on stdout/stderr
load_and_bind
redirect_io
end
already_daemon = JRubyRestart.daemon_init
end
output_header "single"
if jruby_daemon?
if already_daemon
JRubyRestart.perm_daemonize
else
pid = nil
Signal.trap "SIGUSR2" do
log "* Started new process #{pid} as daemon..."
# Must use exit! so we don't unwind and run the ensures
# that will be run by the new child (such as deleting the
# pidfile)
exit!(true)
end
Signal.trap "SIGCHLD" do
log "! Error starting new process as daemon, exiting"
exit 1
end
jruby_daemon_start
sleep
end
else
if daemon?
log "* Daemonizing..."
Process.daemon(true)
redirect_io
end
load_and_bind
end
load_and_bind
Plugins.fire_background
@ -109,10 +55,9 @@ module Puma
@server = server = start_server
unless daemon?
log "Use Ctrl-C to stop"
redirect_io
end
log "Use Ctrl-C to stop"
redirect_io
@launcher.events.fire_on_booted!

View file

@ -1,41 +0,0 @@
# frozen_string_literal: true
module Puma
class TCPLogger
def initialize(logger, app, quiet=false)
@logger = logger
@app = app
@quiet = quiet
end
FORMAT = "%s - %s"
def log(who, str)
now = Time.now.strftime("%d/%b/%Y %H:%M:%S")
log_str = "#{now} - #{who} - #{str}"
case @logger
when IO
@logger.puts log_str
when Events
@logger.log log_str
end
end
def call(env, socket)
who = env[Const::REMOTE_ADDR]
log who, "connected" unless @quiet
env['log'] = lambda { |str| log(who, str) }
begin
@app.call env, socket
rescue Object => e
log who, "exception: #{e.message} (#{e.class})"
else
log who, "disconnected" unless @quiet
end
end
end
end

View file

@ -59,7 +59,7 @@ module Rack
conf
end
def self.run(app, options = {})
def self.run(app, **options)
conf = self.config(app, options)
events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio

View file

@ -0,0 +1 @@
gem 'puma', path: '../../..'

View file

@ -0,0 +1 @@
run lambda { |env| [200, {'Content-Type'=>'text/plain'}, [ENV['BUNDLE_GEMFILE'].inspect]] }

View file

@ -0,0 +1 @@
directory File.expand_path("../../current", __dir__)

View file

@ -0,0 +1 @@
gem 'puma', path: '../../..'

View file

@ -0,0 +1 @@
run lambda { |env| [200, {'Content-Type'=>'text/plain'}, [ENV['BUNDLE_GEMFILE'].inspect]] }

View file

@ -0,0 +1 @@
directory File.expand_path("../../current", __dir__)

View file

@ -11,26 +11,23 @@ if %w(2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1).include? RUBY_VERSION
end
end
require "net/http"
require "timeout"
require "minitest/autorun"
require "minitest/pride"
require "minitest/proveit"
require_relative "helpers/apps"
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
Thread.abort_on_exception = true
$debugging_info = ''.dup
$debugging_hold = false # needed for TestCLI#test_control_clustered
require "puma"
require "puma/events"
require "puma/detect"
# Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
# HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
def hit(uris)
require "net/http"
uris.map do |u|
response =
if u.kind_of? String
@ -46,25 +43,21 @@ def hit(uris)
end
module UniquePort
@port = 3211
@mutex = Mutex.new
def self.call
@mutex.synchronize {
@port += 1
@port = 3307 if @port == 3306 # MySQL on Actions
@port
}
TCPServer.open('127.0.0.1', 0) do |server|
server.connect_address.ip_port
end
end
end
require "timeout"
module TimeoutEveryTestCase
# our own subclass so we never confused different timeouts
class TestTookTooLong < Timeout::Error
end
def run(*)
::Timeout.timeout(Puma.jruby? ? 120 : 60, TestTookTooLong) { super }
::Timeout.timeout(RUBY_ENGINE == 'ruby' ? 60 : 120, TestTookTooLong) { super }
end
end
@ -80,7 +73,7 @@ module TestSkips
# usage: skip NO_FORK_MSG unless HAS_FORK
# windows >= 2.6 fork is not defined, < 2.6 fork raises NotImplementedError
HAS_FORK = ::Process.respond_to? :fork
NO_FORK_MSG = "Kernel.fork isn't available on the #{RUBY_PLATFORM} platform"
NO_FORK_MSG = "Kernel.fork isn't available on #{RUBY_ENGINE} on #{RUBY_PLATFORM}"
# socket is required by puma
# usage: skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
@ -101,11 +94,11 @@ module TestSkips
# optional suffix kwarg is appended to the skip message
# optional suffix bt should generally not used
def skip_on(*engs, suffix: '', bt: caller)
skip_msg = false
engs.each do |eng|
skip_msg = case eng
when :darwin then "Skipped on darwin#{suffix}" if RUBY_PLATFORM[/darwin/]
when :jruby then "Skipped on JRuby#{suffix}" if Puma.jruby?
when :truffleruby then "Skipped on TruffleRuby#{suffix}" if RUBY_ENGINE == "truffleruby"
when :windows then "Skipped on Windows#{suffix}" if Puma.windows?
when :ci then "Skipped on ENV['CI']#{suffix}" if ENV["CI"]
when :no_bundler then "Skipped w/o Bundler#{suffix}" if !defined?(Bundler)

View file

@ -19,16 +19,21 @@ class TestIntegration < Minitest::Test
end
def teardown
if defined?(@server) && @server
if defined?(@server) && @server && @pid
stop_server @pid, signal: :INT
end
@ios_to_close.each do |io|
io.close if io.is_a?(IO) && !io.closed?
io = nil
if @ios_to_close
@ios_to_close.each do |io|
io.close if io.is_a?(IO) && !io.closed?
io = nil
end
end
if @bind_path
refute File.exist?(@bind_path), "Bind path must be removed after stop"
File.unlink(@bind_path) rescue nil
end
refute File.exist?(@bind_path), "Bind path must be removed after stop"
File.unlink(@bind_path) rescue nil
# wait until the end for OS buffering?
if defined?(@server) && @server

View file

@ -1,4 +1,13 @@
results = %w[t1 t2 t3].map do |test|
require "puma"
require "puma/detect"
TESTS_TO_RUN = if Process.respond_to?(:fork)
%w[t1 t2 t3]
else
%w[t1 t2]
end
results = TESTS_TO_RUN.map do |test|
system("ruby -rrubygems test/shell/#{test}.rb ") # > /dev/null 2>&1
end

View file

@ -1,8 +1,8 @@
system "ruby -rrubygems -Ilib bin/puma -p 10102 -C test/shell/t1_conf.rb test/rackup/hello.ru &"
sleep 5
system "curl http://localhost:10102/"
system "kill `cat t1-pid`"
sleep 1 until system "curl http://localhost:10102/"
Process.kill :TERM, Integer(File.read("t1-pid"))
sleep 1

View file

@ -1,6 +1,6 @@
system "ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb start"
sleep 5
system "curl http://localhost:10103/"
system "ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb start &"
sleep 1 until system "curl http://localhost:10103/"
out=`ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb status`

View file

@ -3,4 +3,3 @@ stdout_redirect "t2-stdout"
pidfile "t2-pid"
bind "tcp://0.0.0.0:10103"
rackup File.expand_path('../rackup/hello.ru', File.dirname(__FILE__))
daemonize

View file

@ -3,13 +3,13 @@ sleep 5
worker_pid_was_present = File.file? "t3-worker-2-pid"
system "kill `cat t3-worker-2-pid`" # kill off a worker
Process.kill :TERM, Integer(File.read("t3-worker-2-pid")) # kill off a worker
sleep 2
worker_index_within_number_of_workers = !File.file?("t3-worker-3-pid")
system "kill `cat t3-pid`"
Process.kill :TERM, Integer(File.read("t3-pid"))
File.unlink "t3-pid" if File.file? "t3-pid"
File.unlink "t3-worker-0-pid" if File.file? "t3-worker-0-pid"

View file

@ -5,6 +5,7 @@ require_relative "helpers/ssl"
require "puma/binder"
require "puma/puma_http11"
require "puma/events"
class TestBinderBase < Minitest::Test
include SSLHelper
@ -22,16 +23,42 @@ class TestBinderBase < Minitest::Test
end
class TestBinder < TestBinderBase
def test_localhost_addresses_dont_alter_listeners_for_tcp_addresses
@binder.parse ["tcp://localhost:10001"], @events
parallelize_me!
assert_equal [], @binder.instance_variable_get(:@listeners)
def test_localhost_addresses_dont_alter_listeners_for_tcp_addresses
@binder.parse ["tcp://localhost:0"], @events
assert_empty @binder.listeners
end
def test_home_alters_listeners_for_tcp_addresses
port = UniquePort.call
@binder.parse ["tcp://127.0.0.1:#{port}"], @events
assert_equal "tcp://127.0.0.1:#{port}", @binder.listeners[0][0]
assert_kind_of TCPServer, @binder.listeners[0][1]
end
def test_connected_ports
ports = (1..3).map { |_| UniquePort.call }
@binder.parse(ports.map { |p| "tcp://localhost:#{p}" }, @events)
assert_equal ports, @binder.connected_ports
end
def test_localhost_addresses_dont_alter_listeners_for_ssl_addresses
@binder.parse ["ssl://localhost:10002?#{ssl_query}"], @events
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
assert_equal [], @binder.instance_variable_get(:@listeners)
assert_empty @binder.listeners
end
def test_home_alters_listeners_for_ssl_addresses
port = UniquePort.call
@binder.parse ["ssl://127.0.0.1:#{port}?#{ssl_query}"], @events
assert_equal "ssl://127.0.0.1:#{port}?#{ssl_query}", @binder.listeners[0][0]
assert_kind_of TCPServer, @binder.listeners[0][1]
end
def test_correct_zero_port
@ -43,27 +70,33 @@ class TestBinder < TestBinderBase
refute_equal 0, port
end
def test_correct_zero_port_ssl
skip("Implement later")
ssl_regex = %r!ssl://127.0.0.1:(\d+)!
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
port = ssl_regex.match(@events.stdout.string)[1].to_i
refute_equal 0, port
end
def test_logs_all_localhost_bindings
@binder.parse ["tcp://localhost:0"], @events
assert_match %r!tcp://127.0.0.1:(\d+)!, @events.stdout.string
if @binder.loopback_addresses.include?("::1")
if Socket.ip_address_list.any? {|i| i.ipv6_loopback? }
assert_match %r!tcp://\[::1\]:(\d+)!, @events.stdout.string
end
end
def test_correct_zero_port_ssl
skip("Implement in 4.3")
def test_logs_all_localhost_bindings_ssl
skip("Incorrectly logs localhost, not 127.0.0.1")
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
stdout = @events.stdout.string
m = %r!tcp://127.0.0.1:(\d+)!.match(stdout)
port = m[1].to_i
refute_equal 0, port
assert_match %r!ssl://127.0.0.1:(\d+)!, stdout
if @binder.loopback_addresses.include? '::1'
assert_match %r!ssl://\[::1\]:(\d+)!, stdout
assert_match %r!ssl://127.0.0.1:(\d+)!, @events.stdout.string
if Socket.ip_address_list.any? {|i| i.ipv6_loopback? }
assert_match %r!ssl://\[::1\]:(\d+)!, @events.stdout.string
end
end
@ -72,6 +105,7 @@ class TestBinder < TestBinderBase
end
def test_allows_both_unix_and_tcp
skip_on :jruby # Undiagnosed thread race. TODO fix
assert_parsing_logs_uri [:unix, :tcp]
end
@ -88,9 +122,9 @@ class TestBinder < TestBinderBase
assert_match %r!unix://#{unix_path}!, @events.stdout.string
refute_includes @binder.instance_variable_get(:@unix_paths), unix_path
refute_includes @binder.unix_paths, unix_path
@binder.close_unix_paths
@binder.close_listeners
assert File.exist?(unix_path)
@ -131,8 +165,147 @@ class TestBinder < TestBinderBase
refute ssl_context_for_binder.no_tlsv1_1
end
def test_env_contains_protoenv
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
env_hash = @binder.envs[@binder.ios.first]
@binder.proto_env.each do |k,v|
assert_equal env_hash[k], v
end
end
def test_env_contains_stderr
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
env_hash = @binder.envs[@binder.ios.first]
assert_equal @events.stderr, env_hash["rack.errors"]
end
def test_close_calls_close_on_ios
@mocked_ios = [Minitest::Mock.new, Minitest::Mock.new]
@mocked_ios.each { |m| m.expect(:close, true) }
@binder.ios = @mocked_ios
@binder.close
assert @mocked_ios.map(&:verify).all?
end
def test_redirects_for_restart_creates_a_hash
@binder.parse ["tcp://127.0.0.1:0"], @events
result = @binder.redirects_for_restart
ios = @binder.listeners.map { |_l, io| io.to_i }
ios.each { |int| assert_equal int, result[int] }
assert result[:close_others]
end
def test_redirects_for_restart_env
@binder.parse ["tcp://127.0.0.1:0"], @events
result = @binder.redirects_for_restart_env
@binder.listeners.each_with_index do |l, i|
assert_equal "#{l[1].to_i}:#{l[0]}", result["PUMA_INHERIT_#{i}"]
end
end
def test_close_listeners_closes_ios
@binder.parse ["tcp://127.0.0.1:#{UniquePort.call}"], @events
refute @binder.listeners.any? { |u, l| l.closed? }
@binder.close_listeners
assert @binder.listeners.all? { |u, l| l.closed? }
end
def test_close_listeners_closes_ios_unless_closed?
@binder.parse ["tcp://127.0.0.1:0"], @events
bomb = @binder.listeners.first[1]
bomb.close
def bomb.close; raise "Boom!"; end # the bomb has been planted
assert @binder.listeners.any? { |u, l| l.closed? }
@binder.close_listeners
assert @binder.listeners.all? { |u, l| l.closed? }
end
def test_listeners_file_unlink_if_unix_listener
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
@binder.parse ["unix://test/#{name}_server.sock"], @events
assert File.socket?("test/#{name}_server.sock")
@binder.close_listeners
refute File.socket?("test/#{name}_server.sock")
end
def test_import_from_env_listen_inherit
@binder.parse ["tcp://127.0.0.1:0"], @events
removals = @binder.create_inherited_fds(@binder.redirects_for_restart_env)
@binder.listeners.each do |url, io|
assert_equal io.to_i, @binder.inherited_fds[url]
end
assert_includes removals, "PUMA_INHERIT_0"
end
# Socket activation tests. We have to skip all of these on non-UNIX platforms
# because the check that we do in the code only works if you support UNIX sockets.
# This is OK, because systemd obviously only works on Linux.
def test_socket_activation_tcp
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "127.0.0.1"
port = UniquePort.call
sock = Addrinfo.tcp(url, port).listen
assert_activates_sockets(url: url, port: port, sock: sock)
end
def test_socket_activation_tcp_ipv6
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "::"
port = UniquePort.call
sock = Addrinfo.tcp(url, port).listen
assert_activates_sockets(url: url, port: port, sock: sock)
end
def test_socket_activation_unix
skip_on :jruby # Failing with what I think is a JRuby bug
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
path = "test/unixserver.state"
sock = Addrinfo.unix(path).listen
assert_activates_sockets(path: path, sock: sock)
ensure
File.unlink(path) rescue nil # JRuby race?
end
private
def assert_activates_sockets(path: nil, port: nil, url: nil, sock: nil)
hash = { "LISTEN_FDS" => 1, "LISTEN_PID" => $$ }
@events.instance_variable_set(:@debug, true)
@binder.instance_variable_set(:@sock_fd, sock.fileno)
def @binder.socket_activation_fd(int); @sock_fd; end
@result = @binder.create_activated_fds(hash)
url = "[::]" if url == "::"
ary = path ? [:unix, path] : [:tcp, url, port]
assert_kind_of TCPServer, @binder.activated_sockets[ary]
assert_match "Registered #{ary.join(":")} for activation from LISTEN_FDS", @events.stdout.string
assert_equal ["LISTEN_FDS", "LISTEN_PID"], @result
end
def assert_parsing_logs_uri(order = [:unix, :tcp])
skip UNIX_SKT_MSG if order.include?(:unix) && !UNIX_SKT_EXIST
@ -147,10 +320,11 @@ class TestBinder < TestBinderBase
@binder.parse tested_paths, @events
stdout = @events.stdout.string
assert stdout.include?(prepared_paths[order[0]]), "\n#{stdout}\n"
assert stdout.include?(prepared_paths[order[1]]), "\n#{stdout}\n"
order.each do |prot|
assert_match prepared_paths[prot], stdout
end
ensure
@binder.close_unix_paths if order.include?(:unix) && UNIX_SKT_EXIST
@binder.close_listeners if order.include?(:unix) && UNIX_SKT_EXIST
end
end

View file

@ -64,6 +64,8 @@ class TestCLI < Minitest::Test
end
def test_control_for_ssl
skip_on :jruby # Hangs on CI, TODO fix
require "net/http"
control_port = UniquePort.call
control_host = "127.0.0.1"
control_url = "ssl://#{control_host}:#{control_port}?#{ssl_query}"
@ -94,8 +96,8 @@ class TestCLI < Minitest::Test
assert_equal([:started_at, :backlog, :running, :pool_capacity, :max_threads, :requests_count], Puma.stats.keys)
ensure
cli.launcher.stop
t.join
cli.launcher.stop if cli
t.join if t
end
def test_control_clustered

View file

@ -4,6 +4,7 @@ require_relative "helper"
require_relative "helpers/config_file"
require "puma/configuration"
require 'puma/events'
class TestConfigFile < TestConfigFileBase
parallelize_me!
@ -66,7 +67,11 @@ class TestConfigFile < TestConfigFileBase
end
conf.load
conf.app
# suppress deprecation warning of Rack (>= 2.2.0)
# > Parsing options from the first comment line is deprecated!\n
assert_output(nil, nil) do
conf.app
end
assert_equal ["tcp://127.0.0.1:9292"], conf.options[:binds]
end
@ -249,6 +254,61 @@ class TestConfigFile < TestConfigFileBase
conf.options[:raise_exception_on_sigterm] = true
assert_equal conf.options[:raise_exception_on_sigterm], true
end
def test_run_hooks_on_restart_hook
assert_run_hooks :on_restart
end
def test_run_hooks_before_worker_fork
assert_run_hooks :before_worker_fork, configured_with: :on_worker_fork
end
def test_run_hooks_after_worker_fork
assert_run_hooks :after_worker_fork
end
def test_run_hooks_before_worker_boot
assert_run_hooks :before_worker_boot, configured_with: :on_worker_boot
end
def test_run_hooks_before_worker_shutdown
assert_run_hooks :before_worker_shutdown, configured_with: :on_worker_shutdown
end
def test_run_hooks_before_fork
assert_run_hooks :before_fork
end
def test_run_hooks_and_exception
conf = Puma::Configuration.new do |c|
c.on_restart do |a|
raise RuntimeError, 'Error from hook'
end
end
conf.load
events = Puma::Events.strings
conf.run_hooks :on_restart, 'ARG', events
expected = /WARNING hook on_restart failed with exception \(RuntimeError\) Error from hook/
assert_match expected, events.stdout.string
end
private
def assert_run_hooks(hook_name, options = {})
configured_with = options[:configured_with] || hook_name
messages = []
conf = Puma::Configuration.new do |c|
c.send(configured_with) do |a|
messages << "#{hook_name} is called with #{a}"
end
end
conf.load
conf.run_hooks hook_name, 'ARG', Puma::Events.strings
assert_equal messages, ["#{hook_name} is called with ARG"]
end
end
# Thread unsafe modification of ENV

View file

@ -144,15 +144,15 @@ class Http11ParserTest < Minitest::Test
parser = Puma::HttpParser.new
req = {}
# Support URI path length to a max of 2048
path = "/" + rand_data(1000, 100)
# Support URI path length to a max of 8196
path = "/" + rand_data(7000, 100)
http = "GET #{path} HTTP/1.1\r\n\r\n"
parser.execute(req, http, 0)
assert_equal path, req['REQUEST_PATH']
parser.reset
# Raise exception if URI path length > 2048
path = "/" + rand_data(3000, 100)
# Raise exception if URI path length > 8196
path = "/" + rand_data(9000, 100)
http = "GET #{path} HTTP/1.1\r\n\r\n"
assert_raises Puma::HttpParserError do
parser.execute(req, http, 0)

View file

@ -14,10 +14,13 @@ class TestIntegrationPumactl < TestIntegration
def teardown
super
refute File.exist?(@control_path), "Control path must be removed after stop"
ensure
[@state_path, @control_path].each { |p| File.unlink(p) rescue nil }
end
def test_stop_tcp
skip_on :jruby, :truffleruby # Undiagnose thread race. TODO fix
@control_tcp_port = UniquePort.call
cli_server "-q test/rackup/sleep.ru --control-url tcp://#{HOST}:#{@control_tcp_port} --control-token #{TOKEN} -S #{@state_path}"
@ -30,10 +33,18 @@ class TestIntegrationPumactl < TestIntegration
end
def test_stop_unix
ctl_unix
end
def test_halt_unix
ctl_unix 'halt'
end
def ctl_unix(signal='stop')
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
cli_server "-q test/rackup/sleep.ru --control-url unix://#{@control_path} --control-token #{TOKEN} -S #{@state_path}", unix: true
cli_pumactl "stop", unix: true
cli_pumactl signal, unix: true
_, status = Process.wait2(@pid)
assert_equal 0, status

View file

@ -49,7 +49,7 @@ class TestIntegrationSingle < TestIntegration
cli_server 'test/rackup/sleep.ru'
_stdin, curl_stdout, _stderr, curl_wait_thread = Open3.popen3("curl http://#{HOST}:#{@tcp_port}/sleep10")
_stdin, curl_stdout, _stderr, curl_wait_thread = Open3.popen3({ 'LC_ALL' => 'C' }, "curl http://#{HOST}:#{@tcp_port}/sleep10")
sleep 1 # ensure curl send a request
Process.kill :TERM, @pid

View file

@ -7,6 +7,12 @@ class TestPreserveBundlerEnv < TestIntegration
super
end
def teardown
return if skipped?
FileUtils.rm current_release_symlink, force: true
super
end
# It does not wipe out BUNDLE_GEMFILE et al
def test_usr2_restart_preserves_bundler_environment
skip_unless_signal_exist? :USR2
@ -32,4 +38,53 @@ class TestPreserveBundlerEnv < TestIntegration
new_reply = read_body(connection)
assert_match("Gemfile.bundle_env_preservation_test", new_reply)
end
def test_phased_restart_preserves_unspecified_bundle_gemfile
skip_unless_signal_exist? :USR1
@tcp_port = UniquePort.call
env = {
"BUNDLE_GEMFILE" => nil,
"BUNDLER_ORIG_BUNDLE_GEMFILE" => nil
}
set_release_symlink File.expand_path("bundle_preservation_test/version1", __dir__)
cmd = "bundle exec puma -q -w 1 --prune-bundler -b tcp://#{HOST}:#{@tcp_port}"
Dir.chdir(current_release_symlink) do
@server = IO.popen(env, cmd.split, "r")
end
wait_for_server_to_boot
@pid = @server.pid
connection = connect
# Bundler itself sets ENV['BUNDLE_GEMFILE'] to the Gemfile it finds if ENV['BUNDLE_GEMFILE'] was unspecified
initial_reply = read_body(connection)
expected_gemfile = File.expand_path("bundle_preservation_test/version1/Gemfile", __dir__).inspect
assert_equal(expected_gemfile, initial_reply)
set_release_symlink File.expand_path("bundle_preservation_test/version2", __dir__)
start_phased_restart
connection = connect
connection.write "GET / HTTP/1.1\r\n\r\n"
new_reply = read_body(connection)
expected_gemfile = File.expand_path("bundle_preservation_test/version2/Gemfile", __dir__).inspect
assert_equal(expected_gemfile, new_reply)
end
private
def current_release_symlink
File.expand_path "bundle_preservation_test/current", __dir__
end
def set_release_symlink(target_dir)
FileUtils.rm current_release_symlink, force: true
FileUtils.symlink target_dir, current_release_symlink, force: true
end
def start_phased_restart
Process.kill :USR1, @pid
true while @server.gets !~ /booted, phase: 1/
end
end

View file

@ -1,4 +1,6 @@
require_relative "helper"
require "puma/events"
require "net/http"
class TestPumaServer < Minitest::Test
parallelize_me!
@ -261,6 +263,36 @@ EOF
assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
end
def test_force_shutdown_custom_error_message
handler = lambda {|err, env, status| [500, {"Content-Type" => "application/json"}, ["{}\n"]]}
@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => handler, :force_shutdown_after => 2}
server_run app: ->(env) do
@server.stop
sleep 5
end
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
assert_match(/Content-Type: application\/json/, data)
assert_match(/{}\n$/, data)
end
def test_force_shutdown_error_default
@server = Puma::Server.new @app, @events, {:force_shutdown_after => 2}
server_run app: ->(env) do
@server.stop
sleep 5
end
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
assert_match(/HTTP\/1.0 503 Service Unavailable/, data)
assert_match(/Puma caught this error.+Puma::ThreadPool::ForceShutdown/, data)
end
def test_prints_custom_error
re = lambda { |err| [302, {'Content-Type' => 'text', 'Location' => 'foo.html'}, ['302 found']] }
@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => re}
@ -287,6 +319,21 @@ EOF
assert_match(/HTTP\/1.0 302 Found/, data)
end
def test_leh_has_status
re = lambda { |err, env, status|
raise "Cannot find status" unless status
[302, {'Content-Type' => 'text', 'Location' => 'foo.html'}, ['302 found']]
}
@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => re}
server_run app: ->(env) { raise "don't leak me bro" }
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
assert_match(/HTTP\/1.0 302 Found/, data)
end
def test_custom_http_codes_10
server_run app: ->(env) { [449, {}, [""]] }
@ -835,4 +882,106 @@ EOF
assert_does_not_allow_http_injection(app)
end
end
# Perform a server shutdown while requests are pending (one in app-server response, one still sending client request).
def shutdown_requests(app_delay: 2, request_delay: 1, post: false, response:, **options)
@server = Puma::Server.new @app, @events, options
server_run app: ->(_) {
sleep app_delay
[204, {}, []]
}
s1 = send_http "GET / HTTP/1.1\r\n\r\n"
s2 = send_http post ?
"POST / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhi!" :
"GET / HTTP/1.1\r\n"
sleep 0.1
@server.stop
sleep request_delay
s2 << "\r\n"
assert_match /204/, s1.gets
assert IO.select([s2], nil, nil, app_delay), 'timeout waiting for response'
s2_result = begin
s2.gets
rescue Errno::ECONNABORTED, Errno::ECONNRESET
# Some platforms raise errors instead of returning a response/EOF when a TCP connection is aborted.
post ? '408' : nil
end
if response
assert_match response, s2_result
else
assert_nil s2_result
end
end
# Shutdown should allow pending requests to complete.
def test_shutdown_requests
shutdown_requests response: /204/
shutdown_requests response: /204/, queue_requests: false
end
# Requests stuck longer than `first_data_timeout` should have connection closed (408 w/pending POST body).
def test_shutdown_data_timeout
shutdown_requests request_delay: 3, first_data_timeout: 2, response: nil
shutdown_requests request_delay: 3, first_data_timeout: 2, response: nil, queue_requests: false
shutdown_requests request_delay: 3, first_data_timeout: 2, response: /408/, post: true
end
# Requests still pending after `force_shutdown_after` should have connection closed (408 w/pending POST body).
def test_force_shutdown
shutdown_requests request_delay: 4, response: nil, force_shutdown_after: 1
shutdown_requests request_delay: 4, response: nil, force_shutdown_after: 1, queue_requests: false
shutdown_requests request_delay: 4, response: /408/, force_shutdown_after: 1, post: true
end
# App-responses still pending during `force_shutdown_after` should return 503
# (uncaught Puma::ThreadPool::ForceShutdown exception).
def test_force_shutdown_app
shutdown_requests app_delay: 3, response: /503/, force_shutdown_after: 1
shutdown_requests app_delay: 3, response: /503/, force_shutdown_after: 1, queue_requests: false
end
def test_http11_connection_header_queue
server_run app: ->(_) { [200, {}, [""]] }
sock = send_http "GET / HTTP/1.1\r\n\r\n"
assert_equal ["HTTP/1.1 200 OK", "Content-Length: 0"], header(sock)
sock << "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
assert_equal ["HTTP/1.1 200 OK", "Connection: close", "Content-Length: 0"], header(sock)
sock.close
end
def test_http10_connection_header_queue
server_run app: ->(_) { [200, {}, [""]] }
sock = send_http "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
assert_equal ["HTTP/1.0 200 OK", "Connection: Keep-Alive", "Content-Length: 0"], header(sock)
sock << "GET / HTTP/1.0\r\n\r\n"
assert_equal ["HTTP/1.0 200 OK", "Content-Length: 0"], header(sock)
sock.close
end
def test_http11_connection_header_no_queue
@server = Puma::Server.new @app, @events, queue_requests: false
server_run app: ->(_) { [200, {}, [""]] }
sock = send_http "GET / HTTP/1.1\r\n\r\n"
assert_equal ["HTTP/1.1 200 OK", "Connection: close", "Content-Length: 0"], header(sock)
sock.close
end
def test_http10_connection_header_no_queue
@server = Puma::Server.new @app, @events, queue_requests: false
server_run app: ->(_) { [200, {}, [""]] }
sock = send_http "GET / HTTP/1.0\r\n\r\n"
assert_equal ["HTTP/1.0 200 OK", "Content-Length: 0"], header(sock)
sock.close
end
end

View file

@ -1,6 +1,8 @@
require_relative "helper"
require "puma/minissl"
require "puma/puma_http11"
require "puma/events"
require "net/http"
#———————————————————————————————————————————————————————————————————————————————
# NOTE: ALL TESTS BYPASSED IF DISABLE_SSL IS TRUE
@ -21,7 +23,7 @@ DISABLE_SSL = begin
Puma::MiniSSL.check
# net/http (loaded in helper) does not necessarily load OpenSSL
require "openssl" unless Object.const_defined? :OpenSSL
puts "", RUBY_DESCRIPTION,
puts "", RUBY_DESCRIPTION, "RUBYOPT: #{ENV['RUBYOPT']}",
" Puma::MiniSSL OpenSSL",
"OPENSSL_LIBRARY_VERSION: #{Puma::MiniSSL::OPENSSL_LIBRARY_VERSION.ljust 32}#{OpenSSL::OPENSSL_LIBRARY_VERSION}",
" OPENSSL_VERSION: #{Puma::MiniSSL::OPENSSL_VERSION.ljust 32}#{OpenSSL::OPENSSL_VERSION}", ""
@ -164,7 +166,7 @@ class TestPumaServerSSL < Minitest::Test
skip("TLSv1 protocol is unavailable") if Puma::MiniSSL::OPENSSL_NO_TLS1
start_server { |ctx| ctx.no_tlsv1 = true }
if @http.respond_to? :max_version=
if OpenSSL::SSL::SSLContext.private_instance_methods(false).include?(:set_minmax_proto_version)
@http.max_version = :TLS1
else
@http.ssl_version = :TLSv1
@ -184,7 +186,7 @@ class TestPumaServerSSL < Minitest::Test
def test_tls_v1_1_rejection
start_server { |ctx| ctx.no_tlsv1_1 = true }
if @http.respond_to? :max_version=
if OpenSSL::SSL::SSLContext.private_instance_methods(false).include?(:set_minmax_proto_version)
@http.max_version = :TLS1_1
else
@http.ssl_version = :TLSv1_1

View file

@ -22,13 +22,6 @@ class TestPumaControlCli < TestConfigFileBase
@ready.close
end
def find_open_port
server = TCPServer.new("127.0.0.1", 0)
server.addr[1]
ensure
server.close
end
def with_config_file(path_to_config, port)
path = Pathname.new(path_to_config)
Dir.mktmpdir do |tmp_dir|
@ -120,7 +113,7 @@ class TestPumaControlCli < TestConfigFileBase
def test_control_url_and_status
host = "127.0.0.1"
port = find_open_port
port = UniquePort.call
url = "tcp://#{host}:#{port}/"
opts = [
@ -149,8 +142,9 @@ class TestPumaControlCli < TestConfigFileBase
end
def test_control_ssl
skip_on :jruby # Hanging on JRuby, TODO fix
host = "127.0.0.1"
port = find_open_port
port = UniquePort.call
url = "ssl://#{host}:#{port}?#{ssl_query}"
opts = [

View file

@ -26,16 +26,14 @@ class TestPathHandler < Minitest::Test
@launcher = nil
thread = Thread.new do
Rack::Handler::Puma.run(app, options) do |s, p|
Rack::Handler::Puma.run(app, **options) do |s, p|
@launcher = s
end
end
# Wait for launcher to boot
Timeout.timeout(10) do
until @launcher
sleep 1
end
sleep 1 until @launcher
end
sleep 1
@ -227,6 +225,13 @@ class TestUserSuppliedOptionsIsNotPresent < Minitest::Test
end
end
def test_default_log_request_when_no_config_file
conf = Rack::Handler::Puma.config(->{}, @options)
conf.load
assert_equal false, conf.options[:log_requests]
end
def test_file_log_requests_wins_over_default_config
file_log_requests_config = true
@ -240,9 +245,7 @@ class TestUserSuppliedOptionsIsNotPresent < Minitest::Test
assert_equal file_log_requests_config, conf.options[:log_requests]
end
def test_user_log_requests_wins_over_file_config
file_log_requests_config = true
user_log_requests_config = false
@options[:log_requests] = user_log_requests_config

107
test/test_redirect_io.rb Normal file
View file

@ -0,0 +1,107 @@
require_relative "helper"
require_relative "helpers/integration"
class TestRedirectIO < TestIntegration
parallelize_me!
def setup
super
# Keep the Tempfile instances alive to avoid being GC'd
@out_file = Tempfile.new('puma-out')
@err_file = Tempfile.new('puma-err')
@out_file_path = @out_file.path
@err_file_path = @err_file.path
end
def teardown
super
paths = [@out_file_path, @err_file_path, @old_out_file_path, @old_err_file_path].compact
File.unlink(*paths)
@out_file = nil
@err_file = nil
end
def test_sighup_redirects_io_single
skip_on :jruby # Server isn't coming up in CI, TODO Fix
skip_unless_signal_exist? :HUP
cli_args = [
'--redirect-stdout', @out_file_path,
'--redirect-stderr', @err_file_path,
'test/rackup/hello.ru'
]
cli_server cli_args.join ' '
wait_until_file_has_content @out_file_path
assert_match 'puma startup', File.read(@out_file_path)
wait_until_file_has_content @err_file_path
assert_match 'puma startup', File.read(@err_file_path)
log_rotate_output_files
Process.kill :HUP, @server.pid
wait_until_file_has_content @out_file_path
assert_match 'puma startup', File.read(@out_file_path)
wait_until_file_has_content @err_file_path
assert_match 'puma startup', File.read(@err_file_path)
end
def test_sighup_redirects_io_cluster
skip NO_FORK_MSG unless HAS_FORK
skip_unless_signal_exist? :HUP
cli_args = [
'-w', '1',
'--redirect-stdout', @out_file_path,
'--redirect-stderr', @err_file_path,
'test/rackup/hello.ru'
]
cli_server cli_args.join ' '
wait_until_file_has_content @out_file_path
assert_match 'puma startup', File.read(@out_file_path)
wait_until_file_has_content @err_file_path
assert_match 'puma startup', File.read(@err_file_path)
log_rotate_output_files
Process.kill :HUP, @server.pid
wait_until_file_has_content @out_file_path
assert_match 'puma startup', File.read(@out_file_path)
wait_until_file_has_content @err_file_path
assert_match 'puma startup', File.read(@err_file_path)
end
private
def log_rotate_output_files
# rename both files to .old
@old_out_file_path = "#{@out_file_path}.old"
@old_err_file_path = "#{@err_file_path}.old"
File.rename @out_file_path, @old_out_file_path
File.rename @err_file_path, @old_err_file_path
File.new(@out_file_path, File::CREAT).close
File.new(@err_file_path, File::CREAT).close
end
def wait_until_file_has_content(path)
File.open(path) do |file|
begin
file.read_nonblock 1
file.seek 0
rescue EOFError
sleep 0.1
retry
end
end
end
end

View file

@ -1,38 +0,0 @@
require_relative "helper"
require "puma/tcp_logger"
class TestTCPLogger < Minitest::Test
def setup
@events = Puma::Events.new STDOUT, STDERR
@server = Puma::Server.new nil, @events
@server.app = proc { |env, socket|}
@server.tcp_mode!
@socket = nil
end
def test_events
# in lib/puma/launcher.rb:85
# Puma::Events is default tcp_logger for cluster mode
logger = Puma::Events.new(STDOUT, STDERR)
out, err = capture_subprocess_io do
Puma::TCPLogger.new(logger, @server.app).call({}, @socket)
end
assert_match(/connected/, out)
assert_equal('', err)
end
def test_io
# in lib/puma/configuration.rb:184
# STDOUT is default tcp_logger for single mode
logger = STDOUT
out, err = capture_subprocess_io do
Puma::TCPLogger.new(logger, @server.app).call({}, @socket)
end
assert_match(/connected/, out)
assert_equal('', err)
end
end

View file

@ -1,34 +0,0 @@
require_relative "helper"
class TestTCPRack < Minitest::Test
def setup
@port = UniquePort.call
@host = "127.0.0.1"
@events = Puma::Events.new STDOUT, STDERR
@server = Puma::Server.new nil, @events
end
def teardown
@server.stop(true)
end
def test_passes_the_socket
@server.tcp_mode!
body = "We sell hats for a discount!\n"
@server.app = proc do |env, socket|
socket << body
socket.close
end
@server.add_tcp_listener @host, @port
@server.run
sock = TCPSocket.new @host, @port
assert_equal body, sock.read
end
end

View file

@ -16,7 +16,7 @@ class TestThreadPool < Minitest::Test
end
def pause
sleep 0.2
sleep 1
end
def test_append_spawns
@ -64,6 +64,7 @@ class TestThreadPool < Minitest::Test
end
def test_trim
skip_on :jruby, :truffleruby # Undiagnose thread race. TODO fix
pool = new_pool(0, 1) do |work|
@work_mutex.synchronize do
@work_done.signal
@ -78,12 +79,15 @@ class TestThreadPool < Minitest::Test
end
pool.trim
pool.instance_variable_get(:@workers).first.join
# wait/join required here for MRI, JRuby races the access here
worker = pool.instance_variable_get(:@workers).first
worker.join if worker
assert_equal 0, pool.spawned
end
def test_trim_leaves_min
skip_on :jruby, :truffleruby # Undiagnose thread race. TODO fix
pool = new_pool(1, 2) do |work|
@work_mutex.synchronize do
@work_done.signal

View file

@ -6,12 +6,12 @@ class TestPumaUnixSocket < Minitest::Test
App = lambda { |env| [200, {}, ["Works"]] }
Path = "test/puma.sock"
PATH = "test/puma.sock"
def setup
return unless UNIX_SKT_EXIST
@server = Puma::Server.new App
@server.add_unix_listener Path
@server.add_unix_listener PATH
@server.run
end
@ -22,7 +22,7 @@ class TestPumaUnixSocket < Minitest::Test
def test_server
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
sock = UNIXSocket.new Path
sock = UNIXSocket.new PATH
sock << "GET / HTTP/1.0\r\nHost: blah.com\r\n\r\n"

View file

@ -1,19 +0,0 @@
# Puma as a service
## Upstart
See `/tools/jungle/upstart` for Ubuntu's upstart scripts.
## Systemd
See [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md).
## Init.d
Deprecatation Warning : `init.d` was replaced by `systemd` since Debian 8 and Ubuntu 16.04, you should look into [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md) unless you are on an older OS.
See `/tools/jungle/init.d` for tools to use with init.d and start-stop-daemon.
## rc.d
See `/tools/jungle/rc.d` for FreeBSD's rc.d scripts

View file

@ -1,61 +0,0 @@
# Puma daemon service
Deprecatation Warning : `init.d` was replaced by `systemd` since Debian 8 and Ubuntu 16.04, you should look into [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md) unless you are on an older OS.
Init script to manage multiple Puma servers on the same box using start-stop-daemon.
## Installation
# Copy the init script to services directory
sudo cp puma /etc/init.d
sudo chmod +x /etc/init.d/puma
# Make it start at boot time.
sudo update-rc.d -f puma defaults
# Copy the Puma runner to an accessible location
sudo cp run-puma /usr/local/bin
sudo chmod +x /usr/local/bin/run-puma
# Create an empty configuration file
sudo touch /etc/puma.conf
## Managing the jungle
Puma apps are held in /etc/puma.conf by default. It's mainly a CSV file and every line represents one app. Here's the syntax:
app-path,user,config-file-path,log-file-path,environment-variables
You can add an instance by editing the file or running the following command:
sudo /etc/init.d/puma add /path/to/app user /path/to/app/config/puma.rb /path/to/app/log/puma.log
The config and log paths, as well as the environment variables, are optional parameters and default to:
* config: /path/to/app/*config/puma.rb*
* log: /path/to/app/*log/puma.log*
* environment: (empty)
Multiple environment variables need to be separated by a semicolon, e.g.
FOO=1;BAR=2
To remove an app, simply delete the line from the config file or run:
sudo /etc/init.d/puma remove /path/to/app
The command will make sure the Puma instance stops before removing it from the jungle.
## Assumptions
* The script expects a temporary folder named /path/to/app/*tmp/puma* to exist. Create it if it's not there by default.
The pid and state files should live there and must be called: *tmp/puma/pid* and *tmp/puma/state*.
You can change those if you want but you'll have to adapt the script for it to work.
* Here's what a minimal app's config file should have:
```
pidfile "/path/to/app/tmp/puma/pid"
state_path "/path/to/app/tmp/puma/state"
activate_control_app
```

View file

@ -1,421 +0,0 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: puma
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Puma web server
# Description: A ruby web server built for concurrency http://puma.io
# initscript to be placed in /etc/init.d.
### END INIT INFO
# Author: Darío Javier Cravero <dario@exordo.com>
#
# Do NOT "set -e"
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/usr/local/bin:/usr/local/sbin/:/sbin:/usr/sbin:/bin:/usr/bin
DESC="Puma rack web server"
NAME=puma
DAEMON=$NAME
SCRIPTNAME=/etc/init.d/$NAME
CONFIG=/etc/puma.conf
JUNGLE=`cat $CONFIG`
RUNPUMA=/usr/local/bin/run-puma
USE_LOCAL_BUNDLE=0
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
#
# Function that starts the jungle
#
do_start() {
log_daemon_msg "=> Running the jungle..."
for i in $JUNGLE; do
dir=`echo $i | cut -d , -f 1`
do_start_one $dir
done
}
do_start_one() {
PIDFILE=$1/tmp/puma/pid
if [ -e $PIDFILE ]; then
PID=`cat $PIDFILE`
# If the puma is running, restart it, otherwise run it.
if ps -p $PID > /dev/null; then
do_restart_one $1
else
do_start_one_do $1
fi
else
do_start_one_do $1
fi
}
do_start_one_do() {
i=`grep $1 $CONFIG`
dir=`echo $i | cut -d , -f 1`
user=`echo $i | cut -d , -f 2`
config_file=`echo $i | cut -d , -f 3`
if [ "$config_file" = "" ]; then
config_file="$dir/config/puma.rb"
fi
log_file=`echo $i | cut -d , -f 4`
if [ "$log_file" = "" ]; then
log_file="$dir/log/puma.log"
fi
environment=`echo $i | cut -d , -f 5`
log_daemon_msg "--> Woke up puma $dir"
log_daemon_msg "user $user"
log_daemon_msg "log to $log_file"
if [ ! -z "$environment" ]; then
for e in $(echo "$environment" | tr ';' '\n'); do
log_daemon_msg "environment $e"
v=${e%%\=*} ; eval "$e" ; export $v
done
fi
start-stop-daemon --verbose --start --chdir $dir --chuid $user --background --exec $RUNPUMA -- $dir $config_file $log_file
}
#
# Function that stops the jungle
#
do_stop() {
log_daemon_msg "=> Putting all the beasts to bed..."
for i in $JUNGLE; do
dir=`echo $i | cut -d , -f 1`
do_stop_one $dir
done
}
#
# Function that stops the daemon/service
#
do_stop_one() {
log_daemon_msg "--> Stopping $1"
PIDFILE=$1/tmp/puma/pid
STATEFILE=$1/tmp/puma/state
if [ -e $PIDFILE ]; then
PID=`cat $PIDFILE`
if ps -p $PID > /dev/null; then
log_daemon_msg "---> About to kill PID `cat $PIDFILE`"
if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then
cd $1 && bundle exec pumactl --state $STATEFILE stop
else
pumactl --state $STATEFILE stop
fi
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE $STATEFILE
else
log_daemon_msg "---> Puma $1 isn't running."
fi
else
log_daemon_msg "---> No puma here..."
fi
return 0
}
#
# Function that restarts the jungle
#
do_restart() {
for i in $JUNGLE; do
dir=`echo $i | cut -d , -f 1`
do_restart_one $dir
done
}
#
# Function that sends a SIGUSR2 to the daemon/service
#
do_restart_one() {
PIDFILE=$1/tmp/puma/pid
if [ -e $PIDFILE ]; then
log_daemon_msg "--> About to restart puma $1"
kill -s USR2 `cat $PIDFILE`
# TODO Check if process exist
else
log_daemon_msg "--> Your puma was never playing... Let's get it out there first"
do_start_one $1
fi
return 0
}
#
# Function that phased restarts the jungle
#
do_phased_restart() {
for i in $JUNGLE; do
dir=`echo $i | cut -d , -f 1`
do_phased_restart_one $dir
done
}
#
# Function that sends a SIGUSR1 to the daemon/service
#
do_phased_restart_one() {
PIDFILE=$1/tmp/puma/pid
if [ -e $PIDFILE ]; then
log_daemon_msg "--> About to restart puma $1"
kill -s USR1 `cat $PIDFILE`
# TODO Check if process exist
else
log_daemon_msg "--> Your puma was never playing... Let's get it out there first"
do_start_one $1
fi
return 0
}
#
# Function that statuss the jungle
#
do_status() {
for i in $JUNGLE; do
dir=`echo $i | cut -d , -f 1`
do_status_one $dir
done
}
#
# Function that sends a SIGUSR2 to the daemon/service
#
do_status_one() {
PIDFILE=$1/tmp/puma/pid
i=`grep $1 $CONFIG`
dir=`echo $i | cut -d , -f 1`
if [ -e $PIDFILE ]; then
log_daemon_msg "--> About to status puma $1"
if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then
cd $1 && bundle exec pumactl --state $dir/tmp/puma/state stats
else
pumactl --state $dir/tmp/puma/state stats
fi
# kill -s USR2 `cat $PIDFILE`
# TODO Check if process exist
else
log_daemon_msg "--> $1 isn't there :(..."
fi
return 0
}
do_add() {
str=""
# App's directory
if [ -d "$1" ]; then
if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then
str=$1
else
echo "The app is already being managed. Remove it if you want to update its config."
exit 1
fi
else
echo "The directory $1 doesn't exist."
exit 1
fi
# User to run it as
if [ "`grep -c "^$2:" /etc/passwd`" -eq 0 ]; then
echo "The user $2 doesn't exist."
exit 1
else
str="$str,$2"
fi
# Config file
if [ "$3" != "" ]; then
if [ -e $3 ]; then
str="$str,$3"
else
echo "The config file $3 doesn't exist."
exit 1
fi
fi
# Log file
if [ "$4" != "" ]; then
str="$str,$4"
fi
# Environment variables
if [ "$5" != "" ]; then
str="$str,$5"
fi
# Add it to the jungle
echo $str >> $CONFIG
log_daemon_msg "Added a Puma to the jungle: $str. You still have to start it though."
}
do_remove() {
if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then
echo "There's no app $1 to remove."
else
# Stop it first.
do_stop_one $1
# Remove it from the config.
sed -i "\\:^$1:d" $CONFIG
log_daemon_msg "Removed a Puma from the jungle: $1."
fi
}
config_bundler() {
HOME="$(eval echo ~$(id -un))"
if [ -d "$1/.rbenv/bin" ]; then
PATH="$1/.rbenv/bin:$1/.rbenv/shims:$1"
eval "$(rbenv init -)"
USE_LOCAL_BUNDLE=1
return 0
elif [ -d "/usr/local/rbenv/bin" ]; then
PATH="/usr/local/rbenv/bin:/usr/local/rbenv/shims:$PATH"
eval "$(rbenv init -)"
USE_LOCAL_BUNDLE=1
return 0
elif [ -d "$HOME/.rbenv/bin" ]; then
PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
eval "$(rbenv init -)"
USE_LOCAL_BUNDLE=1
return 0
# TODO: test rvm
# elif [ -f /etc/profile.d/rvm.sh ]; then
# source /etc/profile.d/rvm.sh
# elif [ -f /usr/local/rvm/scripts/rvm ]; then
# source /etc/profile.d/rvm.sh
# elif [ -f "$HOME/.rvm/scripts/rvm" ]; then
# source "$HOME/.rvm/scripts/rvm"
# TODO: don't know what to do with chruby
# elif [ -f /usr/local/share/chruby/chruby.sh ]; then
# source /usr/local/share/chruby/chruby.sh
# if [ -f /usr/local/share/chruby/auto.sh ]; then
# source /usr/local/share/chruby/auto.sh
# fi
# if you aren't using auto, set your version here
# chruby 2.0.0
fi
return 1
}
config_bundler
case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
if [ "$#" -eq 1 ]; then
do_start
else
i=`grep $2 $CONFIG`
dir=`echo $i | cut -d , -f 1`
do_start_one $dir
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
if [ "$#" -eq 1 ]; then
do_stop
else
i=`grep $2 $CONFIG`
dir=`echo $i | cut -d , -f 1`
do_stop_one $dir
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
status)
# TODO Implement.
log_daemon_msg "Status $DESC" "$NAME"
if [ "$#" -eq 1 ]; then
do_status
else
i=`grep $2 $CONFIG`
dir=`echo $i | cut -d , -f 1`
do_status_one $dir
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
restart)
log_daemon_msg "Restarting $DESC" "$NAME"
if [ "$#" -eq 1 ]; then
do_restart
else
i=`grep $2 $CONFIG`
dir=`echo $i | cut -d , -f 1`
do_restart_one $dir
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
phased-restart)
log_daemon_msg "Restarting (phased) $DESC" "$NAME"
if [ "$#" -eq 1 ]; then
do_phased_restart
else
i=`grep $2 $CONFIG`
dir=`echo $i | cut -d , -f 1`
do_phased_restart_one $dir
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
add)
if [ "$#" -lt 3 ]; then
echo "Please, specify the app's directory and the user that will run it at least."
echo " Usage: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log"
echo " config and log are optionals."
exit 1
else
do_add $2 $3 $4 $5
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
remove)
if [ "$#" -lt 2 ]; then
echo "Please, specify the app's directory to remove."
exit 1
else
do_remove $2
fi
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
*)
echo "Usage:" >&2
echo " Run the jungle: $SCRIPTNAME {start|stop|status|restart|phased-restart}" >&2
echo " Add a Puma: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log"
echo " config and log are optionals."
echo " Remove a Puma: $SCRIPTNAME remove /path/to/app"
echo " On a Puma: $SCRIPTNAME {start|stop|status|restart|phased-restart} PUMA-NAME" >&2
exit 3
;;
esac
:

View file

@ -1,18 +0,0 @@
#!/bin/bash
# on system boot, and root have no rbenv installed,
# after start-stop-daemon switched to current user, we have to init rbenv
if [ -d "$HOME/.rbenv/bin" ]; then
PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
eval "$(rbenv init -)"
elif [ -d "/usr/local/rbenv/bin" ]; then
PATH="/usr/local/rbenv/bin:/usr/local/rbenv/shims:$PATH"
eval "$(rbenv init -)"
elif [ -f /usr/local/rvm/scripts/rvm ]; then
source /etc/profile.d/rvm.sh
elif [ -f "$HOME/.rvm/scripts/rvm" ]; then
source "$HOME/.rvm/scripts/rvm"
fi
app=$1; config=$2; log=$3;
cd $app && exec bundle exec puma -C $config >> $log 2>&1