├── .gitignore ├── .rspec ├── .simplecov ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── lib └── process │ ├── daemon.rb │ └── daemon │ ├── controller.rb │ ├── listen.rb │ ├── log_file.rb │ ├── notification.rb │ ├── privileges.rb │ ├── process_file.rb │ └── version.rb ├── process-daemon.gemspec └── spec └── process └── daemon ├── crashed_daemon_spec.rb ├── daemon.rb ├── daemon_spec.rb ├── daemonize_spec.rb ├── log_file_spec.rb ├── notification_spec.rb ├── privileges_spec.rb ├── process_file_spec.rb └── terminate_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | 2 | SimpleCov.start do 3 | add_filter "/spec/" 4 | end 5 | 6 | # Work correctly across forks: 7 | pid = Process.pid 8 | SimpleCov.at_exit do 9 | SimpleCov.result.format! if Process.pid == pid 10 | end 11 | 12 | if ENV['TRAVIS'] 13 | require 'coveralls' 14 | Coveralls.wear! 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | rvm: 4 | - 2.0.0 5 | - 2.1.8 6 | - 2.2.4 7 | - 2.3.0 8 | - ruby-head 9 | - rbx-2 10 | env: COVERAGE=true 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in process-daemon.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'simplecov' 8 | gem 'coveralls', require: false 9 | end 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Process::Daemon 2 | 3 | `Process::Daemon` is a stable and helpful base class for long running tasks and daemons. Provides standard `start`, `stop`, `restart`, `status` operations. 4 | 5 | [![Build Status](https://travis-ci.org/ioquatix/process-daemon.svg)](https://travis-ci.org/ioquatix/process-daemon) 6 | [![Code Climate](https://codeclimate.com/github/ioquatix/process-daemon.svg)](https://codeclimate.com/github/ioquatix/process-daemon) 7 | [![Coverage Status](https://coveralls.io/repos/ioquatix/process-daemon/badge.svg)](https://coveralls.io/r/ioquatix/process-daemon) 8 | [![Documentation](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/process-daemon) 9 | [![Code](http://img.shields.io/badge/github-code-blue.svg)](https://github.com/ioquatix/process-daemon) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'process-daemon' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install process-daemon 24 | 25 | ## Usage 26 | 27 | A process daemon has a specific structure: 28 | 29 | class MyDaemon < Process::Daemon 30 | def startup 31 | # Called when the daemon is initialized in it's own process. Should return quickly. 32 | end 33 | 34 | def run 35 | # Do the actual work. Does not need to be implemented, e.g. if using threads or other background processing mechanisms which were kicked off in #startup. 36 | end 37 | 38 | def shutdown 39 | # Stop everything that was setup in startup. Called as part of main daemon thread/process, not in trap context (e.g. SIGINT). 40 | # Asynchronous code can call self.request_shutdown from a trap context to interrupt the main process, provided you aren't doing work in #run. 41 | end 42 | end 43 | 44 | # Make this file executable and have a command line interface: 45 | MyDaemon.daemonize 46 | 47 | ### Working directory 48 | 49 | By default, daemons run in the current working directory. They setup paths according to the following logic: 50 | 51 | working_directory = "." 52 | log_directory = #{working_directory}/log 53 | log_file_path = #{log_directory}/#{name}.log 54 | runtime_directory = #{working_directory}/run 55 | process_file_path = #{runtime_directory}/#{name}.pid 56 | 57 | After calling `prefork`, the working directory is expanded to a full path and should not be changed. 58 | 59 | ### WEBRick Server 60 | 61 | Some servers must run on the main process thread. In this case, the normal interrupt mechanism won't be used and we will handle signals directly. 62 | 63 | Create a file for your daemon, e.g. `daemon.rb`: 64 | 65 | #!/usr/bin/env ruby 66 | 67 | require 'process/daemon' 68 | 69 | # Very simple XMLRPC daemon 70 | class XMLRPCDaemon < Process::Daemon 71 | def startup 72 | @rpc_server = WEBrick::HTTPServer.new( 73 | :Port => 31337, 74 | :BindAddress => "0.0.0.0" 75 | ) 76 | 77 | @listener = XMLRPC::WEBrickServlet.new 78 | 79 | @listener.add_handler("fourty-two") do |amount| 80 | "Hello World" 81 | end 82 | 83 | @rpc_server.mount("/RPC2", @listener) 84 | end 85 | 86 | def run 87 | # This is the correct way to cleanly shutdown the server: 88 | trap(:INT) do 89 | @rpc_server.shutdown 90 | end 91 | 92 | @rpc_server.start 93 | ensure 94 | @rpc_server.shutdown 95 | end 96 | end 97 | 98 | XMLRPCDaemon.daemonize 99 | 100 | Then run `daemon.rb start`. To stop the daemon, run `daemon.rb stop`. 101 | 102 | ### Celluloid Actor 103 | 104 | `Process::Daemon` is the perfect place to spawn your [celluloid](https://celluloid.io) actors. 105 | 106 | require 'celluloid' 107 | require 'process/daemon' 108 | 109 | class MyActor 110 | include Celluloid::Actor 111 | 112 | def long_running 113 | sleep 1000 114 | end 115 | end 116 | 117 | class MyDaemon < Process::Daemon 118 | def startup 119 | @actor = MyActor.new 120 | 121 | @actor.async.long_running_process 122 | end 123 | 124 | def shutdown 125 | @actor.terminate 126 | end 127 | end 128 | 129 | MyDaemon.daemonize 130 | 131 | ## Contributing 132 | 133 | 1. Fork it 134 | 2. Create your feature branch (`git checkout -b my-new-feature`) 135 | 3. Commit your changes (`git commit -am 'Add some feature'`) 136 | 4. Push to the branch (`git push origin my-new-feature`) 137 | 5. Create new Pull Request 138 | 139 | ## License 140 | 141 | Released under the MIT license. 142 | 143 | Copyright, 2015, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 144 | 145 | Permission is hereby granted, free of charge, to any person obtaining a copy 146 | of this software and associated documentation files (the "Software"), to deal 147 | in the Software without restriction, including without limitation the rights 148 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 149 | copies of the Software, and to permit persons to whom the Software is 150 | furnished to do so, subject to the following conditions: 151 | 152 | The above copyright notice and this permission notice shall be included in 153 | all copies or substantial portions of the Software. 154 | 155 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 156 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 157 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 158 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 159 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 160 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 161 | THE SOFTWARE. 162 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |task| 5 | task.rspec_opts = ["--require", "simplecov"] if ENV['COVERAGE'] 6 | end 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /lib/process/daemon.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'fileutils' 22 | 23 | require_relative 'daemon/controller' 24 | 25 | require_relative 'daemon/notification' 26 | 27 | require_relative 'daemon/log_file' 28 | require_relative 'daemon/process_file' 29 | 30 | module Process 31 | # Provides the infrastructure for spawning a daemon. 32 | class Daemon 33 | # Initialize the daemon in the given working root. 34 | def initialize(working_directory = ".") 35 | @working_directory = working_directory 36 | 37 | @shutdown_notification = Notification.new 38 | end 39 | 40 | # Return the name of the daemon 41 | def name 42 | return self.class.name.gsub(/[^a-zA-Z0-9]+/, '-') 43 | end 44 | 45 | # The directory the daemon will run in. 46 | attr :working_directory 47 | 48 | # Return the directory to store log files in. 49 | def log_directory 50 | File.join(working_directory, "log") 51 | end 52 | 53 | # Standard log file for stdout and stderr. 54 | def log_file_path 55 | File.join(log_directory, "#{name}.log") 56 | end 57 | 58 | # Runtime data directory for the daemon. 59 | def runtime_directory 60 | File.join(working_directory, "run") 61 | end 62 | 63 | # Standard location of process pid file. 64 | def process_file_path 65 | File.join(runtime_directory, "#{name}.pid") 66 | end 67 | 68 | # Mark the output log. 69 | def mark_log 70 | File.open(log_file_path, "a") do |log_file| 71 | log_file.puts "=== Log Marked @ #{Time.now.to_s} [#{Process.pid}] ===" 72 | end 73 | end 74 | 75 | # Prints some information relating to daemon startup problems. 76 | def tail_log(output) 77 | lines = LogFile.open(log_file_path).tail_log do |line| 78 | line.match("=== Log Marked") || line.match("=== Daemon Exception Backtrace") 79 | end 80 | 81 | output.puts lines 82 | end 83 | 84 | # Check the last few lines of the log file to find out if the daemon crashed. 85 | def crashed? 86 | count = 3 87 | 88 | LogFile.open(log_file_path).tail_log do |line| 89 | return true if line.match("=== Daemon Crashed") 90 | 91 | break if (count -= 1) == 0 92 | end 93 | 94 | return false 95 | end 96 | 97 | # The main function to setup any environment required by the daemon 98 | def prefork 99 | # Ignore any previously setup signal handler for SIGINT: 100 | trap(:INT, :DEFAULT) 101 | 102 | # We update the working directory to a full path: 103 | @working_directory = File.expand_path(working_directory) 104 | 105 | FileUtils.mkdir_p(log_directory) 106 | FileUtils.mkdir_p(runtime_directory) 107 | end 108 | 109 | # The process title of the daemon. 110 | attr :title 111 | 112 | # Set the process title - only works after daemon has forked. 113 | def title= title 114 | @title = title 115 | 116 | if Process.respond_to? :setproctitle 117 | Process.setproctitle(@title) 118 | else 119 | $0 = @title 120 | end 121 | end 122 | 123 | # Request that the sleep_until_interrupted function call returns. 124 | def request_shutdown 125 | @shutdown_notification.signal 126 | end 127 | 128 | # Call this function to sleep until the daemon is sent SIGINT. 129 | def sleep_until_interrupted 130 | trap(:INT) do 131 | self.request_shutdown 132 | end 133 | 134 | @shutdown_notification.wait 135 | end 136 | 137 | # This function must setup the daemon quickly and return. 138 | def startup 139 | end 140 | 141 | # If you want to implement a long running process you override this method. You may like to call super but it is not necessary to use the supplied interruption machinery. 142 | def run 143 | sleep_until_interrupted 144 | end 145 | 146 | # This function should terminate any active processes in the daemon and return as quickly as possible. 147 | def shutdown 148 | end 149 | 150 | # The entry point from the newly forked process. 151 | def spawn 152 | self.title = self.name 153 | 154 | self.startup 155 | 156 | begin 157 | self.run 158 | rescue Interrupt 159 | $stderr.puts "Daemon interrupted, proceeding to shutdown." 160 | end 161 | 162 | self.shutdown 163 | end 164 | 165 | class << self 166 | # A shared instance of the daemon. 167 | def instance 168 | @instance ||= self.new 169 | end 170 | 171 | # The process controller, responsible for managing the daemon process start, stop, restart, etc. 172 | def controller(options = {}) 173 | @controller ||= Controller.new(instance, options) 174 | end 175 | 176 | # The main entry point for daemonized scripts. 177 | def daemonize(*args, **options) 178 | args = ARGV if args.empty? 179 | 180 | controller(options).daemonize(args) 181 | end 182 | 183 | # Start the shared daemon instance. 184 | def start 185 | controller.start 186 | end 187 | 188 | # Stop the shared daemon instance. 189 | def stop 190 | controller.stop 191 | end 192 | 193 | # Check if the shared daemon instance is runnning or not. 194 | def status 195 | controller.status 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /lib/process/daemon/controller.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'rainbow' 22 | 23 | module Process 24 | class Daemon 25 | # Daemon startup timeout 26 | TIMEOUT = 5 27 | 28 | # This module contains functionality related to starting and stopping the @daemon, and code for processing command line input. 29 | class Controller 30 | # `options[:output]` specifies where to write textual output describing what is going on. 31 | def initialize(daemon, options = {}) 32 | @daemon = daemon 33 | 34 | @output = options[:output] || $stdout 35 | 36 | # How long to wait until sending SIGTERM and eventually SIGKILL to the daemon process group when asking it to stop: 37 | @stop_timeout = options[:stop_timeout] || 10.0 38 | end 39 | 40 | # This function is called from the daemon executable. It processes ARGV and checks whether the user is asking for `start`, `stop`, `restart`, `status`. 41 | def daemonize(argv = ARGV) 42 | case (argv.shift || :default).to_sym 43 | when :start 44 | start 45 | show_status 46 | when :stop 47 | stop 48 | show_status 49 | ProcessFile.cleanup(@daemon) 50 | when :restart 51 | stop 52 | ProcessFile.cleanup(@daemon) 53 | start 54 | show_status 55 | when :status 56 | show_status 57 | else 58 | @output.puts Rainbow("Invalid command. Please specify start, restart, stop or status.").red 59 | end 60 | end 61 | 62 | # Fork a child process, detatch it and run the daemon code. 63 | def spawn 64 | @daemon.prefork 65 | @daemon.mark_log 66 | 67 | fork do 68 | Process.setsid 69 | exit if fork 70 | 71 | ProcessFile.store(@daemon, Process.pid) 72 | 73 | File.umask 0000 74 | Dir.chdir @daemon.working_directory 75 | 76 | $stdin.reopen '/dev/null' 77 | $stdout.reopen @daemon.log_file_path, 'a' 78 | $stdout.sync = true 79 | 80 | $stderr.reopen $stdout 81 | $stderr.sync = true 82 | 83 | begin 84 | @daemon.spawn 85 | rescue Exception => error 86 | $stderr.puts "=== Daemon Exception Backtrace @ #{Time.now.to_s} ===" 87 | $stderr.puts "#{error.class}: #{error.message}" 88 | $!.backtrace.each { |at| $stderr.puts at } 89 | $stderr.puts "=== Daemon Crashed ===" 90 | $stderr.flush 91 | ensure 92 | $stderr.puts "=== Daemon Stopping @ #{Time.now.to_s} ===" 93 | $stderr.flush 94 | end 95 | end 96 | end 97 | 98 | # This function starts the daemon process in the background. 99 | def start 100 | @output.puts Rainbow("Starting #{@daemon.name} daemon...").blue 101 | 102 | case self.status 103 | when :running 104 | @output.puts Rainbow("Daemon already running!").blue 105 | return 106 | when :stopped 107 | # We are good to go... 108 | else 109 | @output.puts Rainbow("Daemon in unknown state! Will clear previous state and continue.").red 110 | ProcessFile.clear(@daemon) 111 | end 112 | 113 | spawn 114 | 115 | sleep 0.1 116 | timer = TIMEOUT 117 | pid = ProcessFile.recall(@daemon) 118 | 119 | while pid == nil and timer > 0 120 | # Wait a moment for the forking to finish... 121 | @output.puts Rainbow("Waiting for daemon to start (#{timer}/#{TIMEOUT})").blue 122 | sleep 1 123 | 124 | # If the @daemon has crashed, it is never going to start... 125 | break if @daemon.crashed? 126 | 127 | pid = ProcessFile.recall(@daemon) 128 | 129 | timer -= 1 130 | end 131 | end 132 | 133 | # Prints out the status of the daemon 134 | def status 135 | ProcessFile.status(@daemon) 136 | end 137 | 138 | def show_status 139 | case self.status 140 | when :running 141 | @output.puts Rainbow("Daemon status: running pid=#{ProcessFile.recall(@daemon)}").green 142 | when :unknown 143 | if @daemon.crashed? 144 | @output.puts Rainbow("Daemon status: crashed").red 145 | 146 | @output.flush 147 | @output.puts Rainbow("Dumping daemon crash log:").red 148 | @daemon.tail_log(@output) 149 | else 150 | @output.puts Rainbow("Daemon status: unknown").red 151 | end 152 | when :stopped 153 | @output.puts Rainbow("Daemon status: stopped").blue 154 | end 155 | end 156 | 157 | # The pid of the daemon if it is available. The pid may be invalid if the daemon has crashed. 158 | def pid 159 | ProcessFile.recall(@daemon) 160 | end 161 | 162 | # How long to wait between checking the daemon process when shutting down: 163 | STOP_PERIOD = 0.1 164 | 165 | # The number of attempts to stop the daemon using SIGTERM. On the last attempt, SIGKILL is used. 166 | STOP_ATTEMPTS = 5 167 | 168 | # The factor which controls how long we sleep between attempts to kill the process. Only applies to processes which don't stop immediately. 169 | STOP_WAIT_FACTOR = 3.0 170 | 171 | # Stops the daemon process. This function initially sends SIGINT. It waits STOP_PERIOD and checks if the daemon is still running. If it is, it sends SIGTERM, and then waits a bit longer. It tries STOP_ATTEMPTS times until it basically assumes the daemon is stuck and sends SIGKILL. 172 | def stop 173 | @output.puts Rainbow("Stopping #{@daemon.name} daemon...").blue 174 | 175 | # Check if the pid file exists... 176 | unless File.file?(@daemon.process_file_path) 177 | @output.puts Rainbow("Pid file not found. Is the daemon running?").red 178 | return 179 | end 180 | 181 | pid = ProcessFile.recall(@daemon) 182 | 183 | # Check if the @daemon is already stopped... 184 | unless ProcessFile.running(@daemon) 185 | @output.puts Rainbow("Pid #{pid} is not running. Has daemon crashed?").red 186 | @daemon.tail_log($stderr) 187 | return 188 | end 189 | 190 | pgid = -Process.getpgid(pid) 191 | 192 | # Stop by interrupt sends a single interrupt and waits for the process to terminate: 193 | unless stop_by_interrupt(pgid) 194 | # If the process is still running, we try sending SIGTERM followed by SIGKILL: 195 | @output.puts Rainbow("** Daemon did not stop gracefully after #{@stop_timeout}s **").red 196 | 197 | stop_by_terminate_or_kill(pgid) 198 | end 199 | 200 | # If after doing our best the @daemon is still running (pretty odd)... 201 | if ProcessFile.running(@daemon) 202 | @output.puts Rainbow("Daemon appears to be still running!").red 203 | return 204 | else 205 | @output.puts Rainbow("Daemon has left the building.").green 206 | end 207 | 208 | # Otherwise the @daemon has been stopped. 209 | ProcessFile.clear(@daemon) 210 | end 211 | 212 | private 213 | 214 | # Returns true if the process was stopped. 215 | def stop_by_interrupt(pgid) 216 | running = true 217 | 218 | # Interrupt the process group: 219 | Process.kill("INT", pgid) 220 | 221 | (@stop_timeout / STOP_PERIOD).ceil.times do 222 | if running = ProcessFile.running(@daemon) 223 | sleep STOP_PERIOD 224 | end 225 | end 226 | 227 | return !running 228 | end 229 | 230 | def stop_by_terminate_or_kill(pgid) 231 | # TERM/KILL loop - if the daemon didn't die easily, shoot it a few more times. 232 | (STOP_ATTEMPTS+1).times do |attempt| 233 | break unless ProcessFile.running(@daemon) 234 | 235 | # SIGKILL gets sent on the last attempt. 236 | signal_name = (attempt < STOP_ATTEMPTS) ? "TERM" : "KILL" 237 | 238 | @output.puts Rainbow("Sending #{signal_name} to process group #{pgid}...").red 239 | 240 | Process.kill(signal_name, pgid) 241 | 242 | # We iterate quickly to start with, and slow down if the process seems unresponsive. 243 | timeout = STOP_PERIOD + (attempt.to_f / STOP_ATTEMPTS) * STOP_WAIT_FACTOR 244 | @output.puts Rainbow("Waiting for #{timeout.round(1)}s for daemon to terminate...").blue 245 | sleep(timeout) 246 | end 247 | end 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /lib/process/daemon/listen.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2016, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Process 22 | class Daemon 23 | # Access incoming file descriptors from daemons started by systemd. 24 | class Listen 25 | LISTEN_PID = 'LISTEN_PID' 26 | LISTEN_FDS = 'LISTEN_FDS' 27 | LISTEN_FDNAMES = 'LISTEN_FDNAMES' 28 | 29 | FD_START = 3 30 | SEPERATOR = ':' 31 | 32 | def self.set_close_at_exec(fd) 33 | fd.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD 34 | end 35 | 36 | def self.open(fd) 37 | set_close_at_exec(fd) 38 | 39 | return IO.for_fd(fd) 40 | end 41 | 42 | # Returns a Array or Hash of file descriptors. If LISTEN_FDNAMES is set, a Hash is returned which includes key => value pairs for named file descriptors. 43 | def self.file_descriptors(env = ENV) 44 | pid, fds, names = env.values_at(LISTEN_PID, LISTEN_FDS, LISTEN_FDNAMES) 45 | 46 | # Are the PIDs valid for this process? 47 | unless pid and Integer(pid) == Process.pid 48 | return nil 49 | end 50 | 51 | files = Integer(fds).times.collect do |i| 52 | self.open(FD_START + i) 53 | end 54 | 55 | if names 56 | names = names.split(SEPARATOR, -1) 57 | end 58 | 59 | self.new(files, names) 60 | end 61 | 62 | def initialize(files, names) 63 | @files = files 64 | @names = names 65 | 66 | @named = {} 67 | @unnamed = [] 68 | 69 | @names.each_with_index do |name, index| 70 | if name 71 | @named[name] = @files[index] 72 | else 73 | @unnamed << @files[index] 74 | end 75 | end 76 | end 77 | 78 | attr :files 79 | attr :names 80 | 81 | attr :named 82 | attr :unnamed 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/process/daemon/log_file.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Process 22 | class Daemon 23 | # This is a special file instance which provides the ability to read a log file from the end backwards. 24 | class LogFile < File 25 | # Yields the lines of a log file in reverse order, once the yield statement returns true, stops, and returns the lines in order. 26 | def tail_log 27 | lines = [] 28 | 29 | seek_end 30 | 31 | reverse_each_line do |line| 32 | lines << line 33 | 34 | break if block_given? and yield line 35 | end 36 | 37 | return lines.reverse 38 | end 39 | 40 | private 41 | 42 | # Seek to the end of the file 43 | def seek_end(offset = 0) 44 | seek(offset, IO::SEEK_END) 45 | end 46 | 47 | # Read a chunk of data and then move the file pointer backwards. 48 | # 49 | # Calling this function multiple times will return new data and traverse the file backwards. 50 | # 51 | def read_reverse(length) 52 | offset = tell 53 | 54 | if offset == 0 55 | return nil 56 | end 57 | 58 | start = [0, offset-length].max 59 | 60 | seek(start, IO::SEEK_SET) 61 | 62 | buf = read(offset-start) 63 | 64 | seek(start, IO::SEEK_SET) 65 | 66 | return buf 67 | end 68 | 69 | REVERSE_BUFFER_SIZE = 128 70 | 71 | # This function is very similar to gets but it works in reverse. 72 | # 73 | # You can use it to efficiently read a file line by line backwards. 74 | # 75 | # It returns nil when there are no more lines. 76 | def reverse_gets(sep_string=$/) 77 | end_pos = tell 78 | 79 | offset = nil 80 | buf = "" 81 | 82 | while offset == nil 83 | chunk = read_reverse(REVERSE_BUFFER_SIZE) 84 | 85 | return (buf == "" ? nil : buf) if chunk == nil 86 | 87 | buf = chunk + buf 88 | 89 | # Don't consider the last newline. 90 | offset = buf.rindex(sep_string, -(sep_string.length + 1)) 91 | end 92 | 93 | # Don't include newline: 94 | offset += 1 95 | 96 | line = buf[offset...buf.size] 97 | 98 | seek((end_pos - buf.size) + offset, IO::SEEK_SET) 99 | 100 | return line 101 | end 102 | 103 | # Similar to each_line but works in reverse. Don't forget to call 104 | # seek_end before you start! 105 | def reverse_each_line(sep_string=$/, &block) 106 | return to_enum(:reverse_each_line) unless block_given? 107 | 108 | line = reverse_gets(sep_string) 109 | 110 | while line != nil 111 | yield line 112 | 113 | line = reverse_gets(sep_string) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/process/daemon/notification.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2015, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Process 22 | class Daemon 23 | # This is a one shot cross-process notification mechanism using pipes. It can also be used in the same process if required, e.g. the self-pipe trick. 24 | class Notification 25 | def initialize 26 | @output, @input = IO.pipe 27 | 28 | @signalled = false 29 | end 30 | 31 | # Signal the notification. 32 | def signal 33 | @signalled = true 34 | 35 | @input.puts 36 | end 37 | 38 | # Was this notification signalled? 39 | def signalled? 40 | @signalled 41 | end 42 | 43 | # Wait/block until a signal is received. Optional timeout. 44 | # @param timeout [Integer] the time to wait in seconds. 45 | def wait(timeout: nil) 46 | if timeout 47 | read_ready, _, _ = IO.select([@output], [], [], timeout) 48 | 49 | return false unless read_ready and read_ready.any? 50 | end 51 | 52 | @signalled or @output.read(1) 53 | 54 | # Just in case that this was split across multiple processes. 55 | @signalled = true 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/process/daemon/privileges.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'etc' 22 | 23 | module Process 24 | class Daemon 25 | # Provides functions for changing the current user. 26 | module Privileges 27 | # Set the user of the current process. Supply either a user ID 28 | # or a user name. 29 | def self.change_user(user) 30 | if user.kind_of?(String) 31 | user = Etc.getpwnam(user).uid 32 | end 33 | 34 | Process::Sys.setuid(user) 35 | end 36 | 37 | # Get the user of the current process. Returns the user name. 38 | def self.current_user 39 | uid = Process::Sys.getuid 40 | 41 | Etc.getpwuid(uid).name 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/process/daemon/process_file.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'fileutils' 22 | 23 | module Process 24 | class Daemon 25 | # This module controls the storage and retrieval of process id files. 26 | class ProcessFile 27 | # Saves the pid for the given daemon 28 | def self.store(daemon, pid) 29 | File.write(daemon.process_file_path, pid) 30 | end 31 | 32 | # Retrieves the pid for the given daemon 33 | def self.recall(daemon) 34 | File.read(daemon.process_file_path).to_i rescue nil 35 | end 36 | 37 | # Removes the pid saved for a particular daemon 38 | def self.clear(daemon) 39 | if File.exist? daemon.process_file_path 40 | FileUtils.rm(daemon.process_file_path) 41 | end 42 | end 43 | 44 | # Checks whether the daemon is running by checking the saved pid and checking the corresponding process 45 | def self.running(daemon) 46 | pid = recall(daemon) 47 | 48 | return false if pid == nil 49 | 50 | gpid = Process.getpgid(pid) rescue nil 51 | 52 | return gpid != nil ? true : false 53 | end 54 | 55 | # Remove the pid file if the daemon is not running 56 | def self.cleanup(daemon) 57 | clear(daemon) unless running(daemon) 58 | end 59 | 60 | # This function returns the status of the daemon. This can be one of +:running+, +:unknown+ (pid file exists but no 61 | # corresponding process can be found) or +:stopped+. 62 | def self.status(daemon) 63 | if File.exist? daemon.process_file_path 64 | return ProcessFile.running(daemon) ? :running : :unknown 65 | else 66 | return :stopped 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/process/daemon/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Process 22 | class Daemon 23 | VERSION = "1.0.1" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /process-daemon.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'process/daemon/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "process-daemon" 8 | spec.version = Process::Daemon::VERSION 9 | spec.authors = ["Samuel Williams"] 10 | spec.email = ["samuel.williams@oriontransfer.co.nz"] 11 | spec.summary = %q{`Process::Daemon` is a stable and helpful base class for long running tasks and daemons. Provides standard `start`, `stop`, `restart`, `status` operations.} 12 | spec.homepage = "https://github.com/ioquatix/process-daemon" 13 | spec.license = "MIT" 14 | spec.has_rdoc = "yard" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = '>= 1.9.3' 22 | 23 | spec.add_dependency "rainbow", "~> 2.0" 24 | 25 | spec.add_development_dependency "bundler", "~> 1.3" 26 | spec.add_development_dependency "rspec", "~> 3.4.0" 27 | spec.add_development_dependency "rake" 28 | end 29 | -------------------------------------------------------------------------------- /spec/process/daemon/crashed_daemon_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2007, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | 23 | require 'webrick' 24 | require 'webrick/https' 25 | 26 | require 'xmlrpc/server' 27 | require 'xmlrpc/client' 28 | 29 | module Process::Daemon::DaemonizeSpec 30 | class CrashDaemon < Process::Daemon 31 | def working_directory 32 | File.expand_path("../tmp", __FILE__) 33 | end 34 | 35 | def run 36 | and_bobs_your_uncle 37 | end 38 | end 39 | 40 | describe Process::Daemon do 41 | it "should crash daemon and print exception" do 42 | output = StringIO.new 43 | 44 | controller = Process::Daemon::Controller.new(CrashDaemon.instance, :output => output) 45 | 46 | controller.start 47 | 48 | sleep 1 49 | 50 | controller.show_status 51 | 52 | expect(CrashDaemon.status).to be == :unknown 53 | expect(output.string).to include("=== Daemon Exception Backtrace", "=== Daemon Crashed") 54 | expect(output.string).to include("NameError", "and_bobs_your_uncle") 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/process/daemon/daemon.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Copyright, 2012, by Samuel G. D. Williams. 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 | 23 | require 'process/daemon' 24 | 25 | require 'webrick' 26 | require 'webrick/https' 27 | 28 | require 'xmlrpc/server' 29 | 30 | # Very simple XMLRPC daemon 31 | class XMLRPCDaemon < Process::Daemon 32 | def working_directory 33 | File.expand_path("../tmp", __FILE__) 34 | end 35 | 36 | def startup 37 | puts "Starting server..." 38 | 39 | @rpc_server = WEBrick::HTTPServer.new( 40 | :Port => 31337, 41 | :BindAddress => "0.0.0.0" 42 | ) 43 | 44 | @listener = XMLRPC::WEBrickServlet.new 45 | 46 | @listener.add_handler("fourty-two") do |amount| 47 | "Hello World" 48 | end 49 | 50 | @rpc_server.mount("/RPC2", @listener) 51 | end 52 | 53 | def run 54 | # This is the correct way to cleanly shutdown the server: 55 | trap(:INT) do 56 | @rpc_server.shutdown 57 | end 58 | 59 | @rpc_server.start 60 | ensure 61 | @rpc_server.shutdown 62 | end 63 | end 64 | 65 | XMLRPCDaemon.daemonize 66 | -------------------------------------------------------------------------------- /spec/process/daemon/daemon_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2007, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | 23 | require 'webrick' 24 | require 'webrick/https' 25 | 26 | require 'xmlrpc/server' 27 | require 'xmlrpc/client' 28 | 29 | module Process::Daemon::DaemonSpec 30 | # Very simple XMLRPC daemon 31 | class XMLRPCDaemon < Process::Daemon 32 | def working_directory 33 | File.expand_path("../tmp", __FILE__) 34 | end 35 | 36 | def startup 37 | puts "Starting server..." 38 | 39 | @rpc_server = WEBrick::HTTPServer.new( 40 | :Port => 31337, 41 | :BindAddress => "0.0.0.0" 42 | ) 43 | 44 | @listener = XMLRPC::WEBrickServlet.new 45 | 46 | @listener.add_handler("add") do |amount| 47 | @count ||= 0 48 | @count += amount 49 | end 50 | 51 | @listener.add_handler("total") do 52 | @count 53 | end 54 | 55 | @rpc_server.mount("/RPC2", @listener) 56 | end 57 | 58 | def run 59 | # This is the correct way to cleanly shutdown the server, apparently: 60 | trap(:INT) do 61 | @rpc_server.shutdown 62 | end 63 | 64 | puts "RPC server starting..." 65 | @rpc_server.start 66 | ensure 67 | puts "Stop accepting new connections to RPC server..." 68 | @rpc_server.shutdown 69 | end 70 | end 71 | 72 | class DefaultDaemon < Process::Daemon 73 | def working_directory 74 | File.expand_path("../tmp", __FILE__) 75 | end 76 | end 77 | 78 | describe Process::Daemon do 79 | before do 80 | XMLRPCDaemon.start 81 | end 82 | 83 | after do 84 | XMLRPCDaemon.stop 85 | end 86 | 87 | it "should be running" do 88 | expect(XMLRPCDaemon.status).to be == :running 89 | expect(XMLRPCDaemon.instance.crashed?).to be_falsey 90 | end 91 | 92 | it "should respond to connections" do 93 | rpc = XMLRPC::Client.new_from_uri("http://localhost:31337") 94 | rpc.call("add", 10) 95 | 96 | total = rpc.call("total") 97 | 98 | expect(total).to be == 10 99 | end 100 | 101 | it "should be a unique instance" do 102 | expect(XMLRPCDaemon.instance).to_not be == DefaultDaemon.instance 103 | end 104 | 105 | it "should produce useful output" do 106 | output = StringIO.new 107 | 108 | controller = Process::Daemon::Controller.new(XMLRPCDaemon.instance, :output => output) 109 | 110 | expect(controller.status).to be == :running 111 | 112 | controller.show_status 113 | expect(output.string).to match /Daemon status: running pid=\d+/ 114 | 115 | output.rewind 116 | controller.stop 117 | 118 | expect(output.string).to match /Stopping/ 119 | 120 | output.rewind 121 | controller.start 122 | 123 | expect(output.string).to match /Starting/ 124 | end 125 | 126 | it "should have correct process title" do 127 | pid = XMLRPCDaemon.controller.pid 128 | 129 | title = `ps -p #{pid} -o command=`.strip 130 | 131 | expect(title).to match /XMLRPCDaemon/ 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/process/daemon/daemonize_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2007, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | 23 | require 'webrick' 24 | require 'webrick/https' 25 | 26 | require 'xmlrpc/server' 27 | require 'xmlrpc/client' 28 | 29 | module Process::Daemon::DaemonizeSpec 30 | class SleepDaemon < Process::Daemon 31 | def working_directory 32 | File.expand_path("../tmp", __FILE__) 33 | end 34 | end 35 | 36 | describe Process::Daemon do 37 | it "should start daemon" do 38 | SleepDaemon.daemonize(:start) 39 | 40 | expect(SleepDaemon.status).to be == :running 41 | 42 | SleepDaemon.daemonize(:stop) 43 | 44 | expect(SleepDaemon.status).to be == :stopped 45 | end 46 | 47 | it "should restart daemon" do 48 | SleepDaemon.daemonize(:restart) 49 | 50 | expect(SleepDaemon.status).to be == :running 51 | 52 | SleepDaemon.daemonize(:stop) 53 | 54 | expect(SleepDaemon.status).to be == :stopped 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/process/daemon/log_file_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | require 'process/daemon/log_file' 23 | 24 | module Process::Daemon::LogFileSpec 25 | describe Process::Daemon::LogFile do 26 | let(:daemon) {SleepDaemon.instance} 27 | 28 | it "should give the several last lines" do 29 | lines = File.readlines(__FILE__) 30 | 31 | log_file = Process::Daemon::LogFile.open(__FILE__) 32 | 33 | expect(log_file.tail_log).to be == lines 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/process/daemon/notification_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon/notification' 22 | 23 | module Process::Daemon::NotificationSpec 24 | describe Process::Daemon::Notification do 25 | it "can be signalled multiple times" do 26 | notification = Process::Daemon::Notification.new 27 | 28 | expect(notification.signalled?).to be_falsey 29 | 30 | notification.signal 31 | 32 | expect(notification.signalled?).to be_truthy 33 | 34 | notification.signal 35 | notification.signal 36 | 37 | expect(notification.signalled?).to be_truthy 38 | end 39 | 40 | it "can be signalled within trap context and across processes" do 41 | ready = Process::Daemon::Notification.new 42 | notification = Process::Daemon::Notification.new 43 | 44 | pid = fork do 45 | trap(:INT) do 46 | notification.signal 47 | exit(0) 48 | end 49 | 50 | ready.signal 51 | 52 | sleep 53 | end 54 | 55 | ready.wait(timeout: 5.0) 56 | 57 | Process.kill(:INT, pid) 58 | 59 | notification.wait(timeout: 1.0) 60 | 61 | expect(notification.signalled?).to be_truthy 62 | 63 | # Clean up zombie process 64 | Process.waitpid(pid) 65 | end 66 | 67 | it "should receive signal in child process" do 68 | notification = Process::Daemon::Notification.new 69 | 70 | pid = fork do 71 | if notification.wait(timeout: 60) 72 | exit(0) 73 | else 74 | exit(1) 75 | end 76 | end 77 | 78 | notification.signal 79 | 80 | Process.waitpid(pid) 81 | 82 | expect($?.exitstatus).to be == 0 83 | end 84 | 85 | it "should not receive signal in child process and time out" do 86 | notification = Process::Daemon::Notification.new 87 | 88 | pid = fork do 89 | if notification.wait(timeout: 0.01) 90 | exit(0) 91 | else 92 | exit(1) 93 | end 94 | end 95 | 96 | Process.waitpid(pid) 97 | 98 | expect($?.exitstatus).to be == 1 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/process/daemon/privileges_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | require 'process/daemon/privileges' 23 | 24 | module Process::Daemon::PrivilegesSpec 25 | describe Process::Daemon::Privileges do 26 | let(:daemon) {SleepDaemon.instance} 27 | 28 | it "should save report current user" do 29 | expect(Process::Daemon::Privileges.current_user).to be == `whoami`.chomp 30 | end 31 | 32 | it "should change current user" do 33 | current_user = Process::Daemon::Privileges.current_user 34 | 35 | Process::Daemon::Privileges.change_user(current_user) 36 | 37 | expect(Process::Daemon::Privileges.current_user).to be == current_user 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/process/daemon/process_file_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | require 'process/daemon/process_file' 23 | 24 | module Process::Daemon::ProcessFileSpec 25 | class SleepDaemon < Process::Daemon 26 | def working_directory 27 | File.expand_path("../tmp", __FILE__) 28 | end 29 | end 30 | 31 | describe Process::Daemon::ProcessFile do 32 | let(:daemon) {SleepDaemon.instance} 33 | 34 | it "should save pid" do 35 | Process::Daemon::ProcessFile.store(daemon, $$) 36 | 37 | expect(Process::Daemon::ProcessFile.recall(daemon)).to be == $$ 38 | end 39 | 40 | it "should clear pid" do 41 | Process::Daemon::ProcessFile.clear(daemon) 42 | 43 | expect(Process::Daemon::ProcessFile.recall(daemon)).to be nil 44 | end 45 | 46 | it "should be running" do 47 | Process::Daemon::ProcessFile.store(daemon, $$) 48 | 49 | expect(Process::Daemon::ProcessFile.status(daemon)).to be :running 50 | end 51 | 52 | it "should not be running" do 53 | Process::Daemon::ProcessFile.clear(daemon) 54 | 55 | expect(Process::Daemon::ProcessFile.status(daemon)).to be :stopped 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/process/daemon/terminate_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/daemon' 22 | require 'process/daemon/process_file' 23 | 24 | module Process::Daemon::TerminateSpec 25 | class SleepDaemon < Process::Daemon 26 | def working_directory 27 | File.expand_path("../tmp", __FILE__) 28 | end 29 | 30 | def startup 31 | setup_signals 32 | end 33 | 34 | def setup_signals 35 | trap('INT') do 36 | puts 'INT' 37 | end 38 | 39 | trap('TERM') do 40 | puts 'TERM' 41 | end 42 | end 43 | 44 | def run 45 | sleep 1 while true 46 | end 47 | end 48 | 49 | describe Process::Daemon do 50 | let(:daemon) {SleepDaemon.instance} 51 | let(:controller) {Process::Daemon::Controller.new(daemon)} 52 | 53 | # Print out the daemon log file: 54 | #after(:each) do 55 | # system('cat', daemon.log_file_path) 56 | #end 57 | 58 | it "should be killed" do 59 | controller.start 60 | 61 | expect(controller.status).to be == :running 62 | 63 | controller.stop 64 | 65 | expect(controller.status).to be == :stopped 66 | 67 | output = File.readlines(daemon.log_file_path).last(6) 68 | expect(output).to be == ["INT\n", "TERM\n", "TERM\n", "TERM\n", "TERM\n", "TERM\n"] 69 | end 70 | end 71 | end 72 | --------------------------------------------------------------------------------