├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── example ├── Dockerfile ├── Gemfile ├── config.ru ├── config │ └── puma.rb └── etc │ └── systemd │ └── system │ └── puma.service ├── lib └── puma │ └── plugin │ └── systemd.rb ├── puma-plugin-systemd.gemspec ├── script ├── console └── setup └── test ├── plugin_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /**/Gemfile.lock 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /tmp/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3 4 | - 2.4 5 | - 2.5 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Samuel Cochran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **⚠️ Systemd support is now [built-in to Puma](https://github.com/puma/puma/blob/master/docs/systemd.md). This plugin is deprecated in favor of the native support.** 2 | 3 | # Puma Systemd Plugin 4 | 5 | [![Build results](http://img.shields.io/travis/sj26/puma-plugin-systemd/master.svg)](https://travis-ci.org/sj26/puma-plugin-systemd) 6 | [![Gem version](http://img.shields.io/gem/v/puma-plugin-systemd.svg)](https://rubygems.org/gems/puma-plugin-systemd) 7 | 8 | [Puma][puma] integration with [systemd][systemd] for better daemonising under 9 | modern Linux systemds: notify, status, watchdog. 10 | 11 | * Notify systemd when puma has booted and is [ready to handle requests][ready] 12 | * Publish puma stats as systemd [service status][status] for a quick overview 13 | * Use the [watchdog][watchdog] to make sure your puma processes are healthy 14 | and haven't locked up or run out of memory 15 | 16 | Puma already natively supports [socket activation][socket-activation]. 17 | 18 | [puma]: https://github.com/puma/puma 19 | [systemd]: https://www.freedesktop.org/wiki/Software/systemd/ 20 | [ready]: https://www.freedesktop.org/software/systemd/man/sd_notify.html#READY=1 21 | [status]: https://www.freedesktop.org/software/systemd/man/sd_notify.html#STATUS=... 22 | [watchdog]: https://www.freedesktop.org/software/systemd/man/sd_notify.html#WATCHDOG=1 23 | [socket-activation]: http://github.com/puma/puma/blob/master/docs/systemd.md#socket-activation 24 | 25 | ## Installation 26 | 27 | Add this gem to your Gemfile with puma and then bundle: 28 | 29 | ```ruby 30 | gem "puma" 31 | gem "puma-plugin-systemd" 32 | ``` 33 | 34 | Add it to your puma config: 35 | 36 | ```ruby 37 | # config/puma.rb 38 | 39 | bind "tcp://127.0.0.1:9292" 40 | 41 | workers 2 42 | threads 8, 16 43 | 44 | plugin :systemd 45 | ``` 46 | 47 | ## Usage 48 | 49 | ### Notify 50 | 51 | Make sure puma is being started using a [systemd service unit][systemd-service] 52 | with `Type=notify`, something like: 53 | 54 | ```ini 55 | # puma.service 56 | [Service] 57 | Type=notify 58 | User=puma 59 | WorkingDirectory=/app 60 | ExecStart=/app/bin/puma -C config/puma.rb -e production 61 | ExecReload=/bin/kill -USR1 $MAINPID 62 | Restart=always 63 | KillMode=mixed 64 | ``` 65 | 66 | [systemd-service]: https://www.freedesktop.org/software/systemd/man/systemd.service.html 67 | 68 | ### Status 69 | 70 | Running in notify mode as above should just start publishing puma stats as 71 | systemd status. Running `systemctl status puma.service` or similar should 72 | result in a Status line in your status output: 73 | 74 | ``` 75 | app@web:~$ sudo systemctl status puma.service 76 | ● puma.service - puma 77 | Loaded: loaded (/etc/systemd/system/puma.service; enabled; vendor preset: enabled) 78 | Active: active (running) since Mon 2016-10-24 00:26:55 UTC; 5s ago 79 | Main PID: 32234 (ruby2.7) 80 | Status: "puma 3.6.0 cluster: 2/2 workers: 8/16 threads, 8 available, 0 backlog" 81 | Tasks: 10 82 | Memory: 167.9M 83 | CPU: 7.150s 84 | CGroup: /system.slice/puma.service 85 | ├─32234 puma 3.6.0 (unix:///app/tmp/sockets/puma.sock?backlog=1024) [app] 86 | ├─32251 puma: cluster worker 0: 32234 [app] 87 | └─32253 puma: cluster worker 1: 32234 [app] 88 | 89 | Oct 24 00:26:10 web systemd[30762]: puma.service: Executing: /app/bin/puma -C config/puma.rb -e production 90 | Oct 24 00:54:58 web puma[32234]: [32234] Puma starting in cluster mode... 91 | Oct 24 00:54:58 web puma[32234]: [32234] * Version 3.6.0 (ruby 2.3.1-p112), codename: Sleepy Sunday Serenity 92 | Oct 24 00:54:58 web puma[32234]: [32234] * Min threads: 8, max threads: 64 93 | Oct 24 00:26:55 web puma[32234]: [32234] * Environment: production 94 | Oct 24 00:26:55 web puma[32234]: [32234] * Process workers: 2 95 | Oct 24 00:26:55 web puma[32234]: [32234] * Phased restart available 96 | Oct 24 00:26:55 web puma[32234]: [32234] * Listening on unix:///app/tmp/sockets/puma.sock?backlog=1024 97 | Oct 24 00:26:55 web puma[32234]: [32234] Use Ctrl-C to stop 98 | Oct 24 00:26:55 web puma[32234]: [32234] * systemd: notify ready 99 | Oct 24 00:26:55 web puma[32234]: [32251] + Gemfile in context: /app/Gemfile 100 | Oct 24 00:26:55 web systemd[1]: Started puma. 101 | Oct 24 00:26:55 web puma[32234]: [32234] * systemd: watchdog detected (30000000usec) 102 | Oct 24 00:26:55 web puma[32234]: [32253] + Gemfile in context: /app/Gemfile 103 | ``` 104 | 105 | ### Watchdog 106 | 107 | Adding a `WatchdogSec=30` or similar to your systemd service file will tell 108 | puma systemd to ping systemd at half the specified interval to ensure the 109 | service is running and healthy. 110 | 111 | ## Development 112 | 113 | After checking out the repo, run `script/setup` to install dependencies. Then, 114 | run `rake test` to run the tests. You can also run `script/console` for an 115 | interactive prompt that will allow you to experiment. 116 | 117 | To install this gem onto your local machine, run `bundle exec rake install`. To 118 | release a new version, update the version number in `version.rb`, and then run 119 | `bundle exec rake release`, which will create a git tag for the version, push 120 | git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 121 | 122 | ## Example 123 | 124 | There is an `example` directory which contains a Dockerfile and basic 125 | configuration for testing. To use it: 126 | 127 | ``` 128 | cd example 129 | 130 | # Build an image with systemd, puma and the plugin configured 131 | docker build --tag example . 132 | 133 | # Start a new container from the image in the background 134 | docker run --name example --privileged --detach --rm example 135 | 136 | # Show puma systemd integration 137 | docker exec example systemctl status puma 138 | 139 | # Stop the container 140 | docker exec example halt 141 | ``` 142 | 143 | ## Contributing 144 | 145 | Bug reports and pull requests are welcome on GitHub at 146 | https://github.com/sj26/puma-plugin-systemd. 147 | 148 | ## License 149 | 150 | The gem is available as open source under the terms of the [MIT License][license]. 151 | 152 | [license]: http://opensource.org/licenses/MIT 153 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | # Must be built from parent directory so that it can be added to the container 2 | # docker -t puma-plugin-systemd -f example/Dockerfile . 3 | 4 | FROM fedora 5 | 6 | RUN dnf -y install ruby ruby-devel redhat-rpm-config && \ 7 | dnf -y groupinstall "Development tools" 8 | 9 | WORKDIR /example 10 | 11 | ADD ./etc/ /etc/ 12 | RUN systemctl enable puma 13 | 14 | ADD . /example 15 | ENV BUNDLE_SILENCE_ROOT_WARNING=true 16 | RUN gem install bundler && \ 17 | bundle install && \ 18 | bundle binstubs puma 19 | 20 | EXPOSE 9292 21 | EXPOSE 9393 22 | 23 | CMD /sbin/init 24 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "puma" 4 | gem "puma-plugin-systemd" 5 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | run lambda { |env| [200, { "Content-Length" => "13" }, ["Hello, world!"]] } 2 | -------------------------------------------------------------------------------- /example/config/puma.rb: -------------------------------------------------------------------------------- 1 | bind ENV.fetch("PUMA_BIND", "tcp://127.0.0.1:9292") 2 | 3 | unless ENV["PUMA_CONTROL"] == "false" 4 | activate_control_app ENV.fetch("PUMA_CONTROL", "tcp://127.0.0.1:9393") 5 | end 6 | 7 | if ENV.has_key?("PUMA_WORKERS") && ENV["PUMA_WORKERS"] != "false" 8 | workers ENV["PUMA_WORKERS"].to_i 9 | end 10 | 11 | max_threads = ENV.fetch("PUMA_MAX_THREADS", 2).to_i 12 | min_threads = ENV.fetch("PUMA_MIN_THREADS") { max_threads } 13 | 14 | threads min_threads, max_threads 15 | 16 | plugin :systemd 17 | -------------------------------------------------------------------------------- /example/etc/systemd/system/puma.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Puma Web Application Server 3 | After=network.target 4 | 5 | [Install] 6 | WantedBy=default.target 7 | 8 | [Service] 9 | Type=notify 10 | WorkingDirectory=/example 11 | #Environment=PUMA_DEBUG=1 12 | #Environment=PUMA_WORKERS=2 13 | #Environment=PUMA_MIN_THREADS=2 14 | Environment=PUMA_MAX_THREADS=4 15 | ExecStart=/example/bin/puma --debug 16 | ExecReload=/bin/kill -USR1 $MAINPID 17 | ExecRestart=/bin/kill -USR2 $MAINPID 18 | WatchdogSec=30 19 | Restart=always 20 | KillMode=mixed 21 | -------------------------------------------------------------------------------- /lib/puma/plugin/systemd.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8, frozen_string_literal: true 2 | 3 | require "json" 4 | require "puma" 5 | require "puma/plugin" 6 | 7 | # Puma systemd plugin 8 | # 9 | # Uses systemd notify to let systemd know a little about what puma is doing, so 10 | # you know when your system has *actually* started and is ready to take 11 | # requests. 12 | # 13 | class Puma::Plugin::Systemd 14 | Puma::Plugins.register("systemd", self) 15 | 16 | # Puma creates the plugin when encountering `plugin` in the config. 17 | def initialize(loader) 18 | # This is a Puma::PluginLoader 19 | @loader = loader 20 | end 21 | 22 | # We can start doing something when we have a launcher: 23 | def start(launcher) 24 | @launcher = launcher 25 | 26 | # Log relevant ENV in debug 27 | @launcher.events.debug "systemd: NOTIFY_SOCKET=#{ENV["NOTIFY_SOCKET"].inspect}" 28 | @launcher.events.debug "systemd: WATCHDOG_PID=#{ENV["WATCHDOG_PID"].inspect}" 29 | @launcher.events.debug "systemd: WATCHDOG_USEC=#{ENV["WATCHDOG_USEC"].inspect}" 30 | 31 | # Only install hooks if the system is booted by systemd, and systemd has 32 | # asked us to notify it of events. 33 | @systemd = Systemd.new 34 | if @systemd.booted? && @systemd.notify? 35 | @launcher.events.debug "systemd: detected running inside systemd, registering hooks" 36 | 37 | register_hooks 38 | 39 | # In clustered mode, we can start the status loop early and watch the 40 | # workers boot 41 | start_status_loop_thread if clustered? 42 | 43 | start_watchdog_loop_thread if @systemd.watchdog? 44 | else 45 | @launcher.events.debug "systemd: not running within systemd, doing nothing" 46 | end 47 | end 48 | 49 | private 50 | 51 | # Are we a single process worker, or do we have worker processes? 52 | # 53 | # Copied from puma, it's private: 54 | # https://github.com/puma/puma/blob/v3.6.0/lib/puma/launcher.rb#L267-L269 55 | # 56 | def clustered? 57 | (@launcher.options[:workers] || 0) > 0 58 | end 59 | 60 | def register_hooks 61 | @launcher.events.on_booted(&method(:booted)) 62 | (@launcher.config.options[:on_restart] ||= []) << method(:restart) 63 | end 64 | 65 | def booted 66 | @launcher.events.log "* systemd: notify ready" 67 | begin 68 | @systemd.notify_ready 69 | rescue 70 | @launcher.events.error "! systemd: notify ready failed:\n #{$!.to_s}\n #{$!.backtrace.join("\n ")}" 71 | end 72 | 73 | # In single mode, we can only start the status loop once the server is 74 | # started after booted 75 | start_status_loop_thread 76 | end 77 | 78 | def restart(launcher) 79 | @launcher.events.log "* systemd: notify reloading" 80 | begin 81 | @systemd.notify_reloading 82 | rescue 83 | @launcher.events.error "! systemd: notify reloading failed:\n #{$!.to_s}\n #{$!.backtrace.join("\n ")}" 84 | end 85 | end 86 | 87 | def fetch_stats 88 | JSON.parse(@launcher.stats) 89 | end 90 | 91 | def status 92 | Status.new(fetch_stats) 93 | end 94 | 95 | # Update systemd status event second or so 96 | def status_loop 97 | loop do 98 | @launcher.events.debug "systemd: notify status" 99 | begin 100 | @systemd.notify_status(status.to_s) 101 | rescue 102 | @launcher.events.error "! systemd: notify status failed:\n #{$!.to_s}\n #{$!.backtrace.join("\n ")}" 103 | ensure 104 | sleep 1 105 | end 106 | end 107 | end 108 | 109 | def start_status_loop_thread 110 | # This is basically what Puma::Plugins.add_background / fire_background 111 | # does, but at a time of our choosing. 112 | @status_loop_thread ||= Thread.new(&method(:status_loop)) 113 | end 114 | 115 | # If watchdog is configured we'll send a ping at about half the timeout 116 | # configured in systemd as recommended in the docs. 117 | def watchdog_loop 118 | @launcher.events.log "* systemd: watchdog detected (#{@systemd.watchdog_usec}usec)" 119 | 120 | # Ruby wants seconds, and the docs suggest notifying halfway through the 121 | # timeout. 122 | sleep_seconds = @systemd.watchdog_usec / 1000.0 / 1000.0 / 2.0 123 | 124 | loop do 125 | begin 126 | @launcher.events.debug "systemd: notify watchdog" 127 | @systemd.notify_watchdog 128 | rescue 129 | @launcher.events.error "! systemd: notify watchdog failed:\n #{$!.to_s}\n #{$!.backtrace.join("\n ")}" 130 | ensure 131 | @launcher.events.debug "systemd: sleeping #{sleep_seconds}s" 132 | sleep sleep_seconds 133 | end 134 | end 135 | end 136 | 137 | def start_watchdog_loop_thread 138 | @watchdog_loop_thread ||= Thread.new(&method(:watchdog_loop)) 139 | end 140 | 141 | # Give us a way to talk to systemd. 142 | # 143 | # It'd be great to use systemd-notify for the whole shebang, but there's a 144 | # critical error introducing a race condition: 145 | # 146 | # https://github.com/systemd/systemd/issues/2739 147 | # 148 | # systemd-notify docs: 149 | # 150 | # https://www.freedesktop.org/software/systemd/man/systemd-notify.html 151 | # 152 | # We could use sd-daemon (sd_notify and friends) but they require a C 153 | # extension, and are really just fancy wrappers for blatting datagrams at a 154 | # socket advertised via ENV. See the docs: 155 | # 156 | # https://www.freedesktop.org/software/systemd/man/sd-daemon.html 157 | # 158 | class Systemd 159 | # Is the system currently booted with systemd? 160 | # 161 | # See also sd_booted: 162 | # 163 | # https://www.freedesktop.org/software/systemd/man/sd_booted.html 164 | # 165 | def booted? 166 | File.directory?("/run/systemd/system/") 167 | end 168 | 169 | # Are we running within a systemd unit that expects us to notify? 170 | def notify? 171 | ENV.include?("NOTIFY_SOCKET") 172 | end 173 | 174 | # Open a persistent notify socket. 175 | # 176 | # Ruby doesn't have a nicer way to open a unix socket as a datagram. 177 | # 178 | private def notify_socket 179 | @notify_socket ||= Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0).tap do |socket| 180 | socket.connect(Socket.pack_sockaddr_un(ENV["NOTIFY_SOCKET"])) 181 | socket.close_on_exec = true 182 | end 183 | end 184 | 185 | # Send a raw notify message. 186 | # 187 | # https://www.freedesktop.org/software/systemd/man/sd_notify.html 188 | # 189 | private def notify(message) 190 | notify_socket.sendmsg(message, Socket::MSG_NOSIGNAL) 191 | end 192 | 193 | # Tell systemd we are now the main pid 194 | def notify_pid 195 | notify("MAINPID=#{$$}") 196 | end 197 | 198 | # Tell systemd we're fully started and ready to handle requests 199 | def notify_ready 200 | notify("READY=1") 201 | end 202 | 203 | # Tell systemd our status 204 | def notify_status(status) 205 | notify("STATUS=#{status}") 206 | end 207 | 208 | # Tell systemd we're restarting 209 | def notify_reloading 210 | notify("RELOADING=1") 211 | end 212 | 213 | # Tell systemd we're still alive 214 | def notify_watchdog 215 | notify("WATCHDOG=1") 216 | end 217 | 218 | # Has systemd asked us to watchdog? 219 | # 220 | # https://www.freedesktop.org/software/systemd/man/sd_watchdog_enabled.html 221 | # 222 | def watchdog? 223 | ENV.include?("WATCHDOG_USEC") && 224 | (!ENV.include?("WATCHDOG_PID") || ENV["WATCHDOG_PID"].to_i == $$) 225 | end 226 | 227 | # How long between pings until the watchdog will think we're unhealthy? 228 | def watchdog_usec 229 | ENV["WATCHDOG_USEC"].to_i 230 | end 231 | end 232 | 233 | # Take puma's stats and construct a sensible status line for Systemd 234 | class Status 235 | def initialize(stats) 236 | @stats = stats 237 | end 238 | 239 | def clustered? 240 | @stats.has_key? "workers" 241 | end 242 | 243 | def workers 244 | @stats.fetch("workers", 1) 245 | end 246 | 247 | def booted_workers 248 | @stats.fetch("booted_workers", 1) 249 | end 250 | 251 | def running 252 | if clustered? 253 | @stats["worker_status"].map { |s| s["last_status"].fetch("running", 0) }.inject(0, &:+) 254 | else 255 | @stats.fetch("running", 0) 256 | end 257 | end 258 | 259 | def backlog 260 | if clustered? 261 | @stats["worker_status"].map { |s| s["last_status"].fetch("backlog", 0) }.inject(0, &:+) 262 | else 263 | @stats.fetch("backlog", 0) 264 | end 265 | end 266 | 267 | def pool_capacity 268 | if clustered? 269 | @stats["worker_status"].map { |s| s["last_status"].fetch("pool_capacity", 0) }.inject(0, &:+) 270 | else 271 | @stats.fetch("pool_capacity", 0) 272 | end 273 | end 274 | 275 | def max_threads 276 | if clustered? 277 | @stats["worker_status"].map { |s| s["last_status"].fetch("max_threads", 0) }.inject(0, &:+) 278 | else 279 | @stats.fetch("max_threads", 0) 280 | end 281 | end 282 | 283 | def to_s 284 | if clustered? 285 | "puma #{Puma::Const::VERSION} cluster: #{booted_workers}/#{workers} workers: #{running}/#{max_threads} threads, #{pool_capacity} available, #{backlog} backlog" 286 | else 287 | "puma #{Puma::Const::VERSION}: #{running}/#{max_threads} threads, #{pool_capacity} available, #{backlog} backlog" 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /puma-plugin-systemd.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "puma-plugin-systemd" 3 | spec.version = "0.1.5" 4 | spec.author = "Samuel Cochran" 5 | spec.email = "sj26@sj26.com" 6 | 7 | spec.summary = "Puma integration with systemd: notify, status, watchdog" 8 | spec.homepage = "https://github.com/sj26/puma-plugin-systemd" 9 | spec.license = "MIT" 10 | 11 | spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE"] 12 | 13 | spec.add_runtime_dependency "puma", ">= 3.6", "< 5" 14 | spec.add_runtime_dependency "json" 15 | 16 | spec.add_development_dependency "bundler", "~> 1.13" 17 | spec.add_development_dependency "rake", ">= 12.3.3" 18 | spec.add_development_dependency "minitest", "~> 5.0" 19 | end 20 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "puma/plugin/systemd" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/plugin_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PluginTest < Minitest::Test 4 | def test_registration 5 | assert_kind_of Class, Puma::Plugins.find("systemd") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "puma/plugin/systemd" 3 | 4 | require "minitest/autorun" 5 | --------------------------------------------------------------------------------