process-daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9fd3e1daea454199404057b9b504216e8a93a5e
4
+ data.tar.gz: 0b3858725986811d62424f5182deba146f7d96a0
5
+ SHA512:
6
+ metadata.gz: c65fb3fd1f265a5fc532331435eb2905d7ca0bcc8de4d3b8cc2c0598196a0d3c5627d1542ed8baa93839595247902e5238e79342c8ed41a05d83d3b5bbcba502
7
+ data.tar.gz: 8a121e07c4d44af7c8c2a653ec44ed2c78455b5b372057f7f14b05f52ffbc8405e7f0f4979bf1f42d6630851a5c0337aac7110c0030bafa38cf36c9fe78d3dc0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.0"
4
+ - "2.1"
data/Gemfile ADDED
@@ -0,0 +1,8 @@
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 "minitest"
8
+ end
data/README.md ADDED
@@ -0,0 +1,77 @@
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
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'process-daemon'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install process-daemon
20
+
21
+ ## Usage
22
+
23
+ Create a file for your daemon, e.g. `daemon.rb`:
24
+
25
+ #!/usr/bin/env ruby
26
+
27
+ require 'process/daemon'
28
+
29
+ # Very simple XMLRPC daemon
30
+ class XMLRPCDaemon < Process::Daemon
31
+ def base_directory
32
+ # Should be an absolute path:
33
+ File.join(__dir__, "tmp")
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
+
52
+ begin
53
+ @rpc_server.start
54
+ rescue Interrupt
55
+ puts "Daemon interrupted..."
56
+ ensure
57
+ @rpc_server.shutdown
58
+ end
59
+ end
60
+
61
+ def shutdown
62
+ puts "Stopping the RPC server..."
63
+ @rpc_server.stop
64
+ end
65
+ end
66
+
67
+ XMLRPCDaemon.daemonize
68
+
69
+ Then run `daemon.rb start`. To stop the daemon, run `daemon.rb stop`.
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork it
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Push to the branch (`git push origin my-new-feature`)
77
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
@@ -0,0 +1,139 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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/log_file'
26
+ require_relative 'daemon/process_file'
27
+
28
+ module Process
29
+ # This class is the base daemon class. If you are writing a daemon, you should inherit from this class.
30
+ #
31
+ # The basic structure of a daemon is as follows:
32
+ #
33
+ # class Server < Process::Daemon
34
+ # def startup
35
+ # # Long running process, e.g. web server, game server, etc.
36
+ # end
37
+ #
38
+ # def shutdown
39
+ # # Stop the process above, usually called on SIGINT.
40
+ # end
41
+ # end
42
+ #
43
+ # Server.daemonize
44
+ #
45
+ # The base directory specifies a path such that:
46
+ # working_directory = "."
47
+ # log_directory = #{working_directory}/log
48
+ # log_file_path = #{log_directory}/#{daemon_name}.log
49
+ # runtime_directory = #{working_directory}/run
50
+ # process_file_path = #{runtime_directory}/#{daemon_name}.pid
51
+ class Daemon
52
+ def initialize(base_directory = ".")
53
+ @base_directory = base_directory
54
+ end
55
+
56
+ # Return the name of the daemon
57
+ def daemon_name
58
+ return self.class.name.gsub(/[^a-zA-Z0-9]+/, '-')
59
+ end
60
+
61
+ # The directory the daemon will run in.
62
+ def working_directory
63
+ @base_directory
64
+ end
65
+
66
+ # Return the directory to store log files in.
67
+ def log_directory
68
+ File.join(working_directory, "log")
69
+ end
70
+
71
+ # Standard log file for stdout and stderr.
72
+ def log_file_path
73
+ File.join(log_directory, "#{daemon_name}.log")
74
+ end
75
+
76
+ # Runtime data directory for the daemon.
77
+ def runtime_directory
78
+ File.join(working_directory, "run")
79
+ end
80
+
81
+ # Standard location of process pid file.
82
+ def process_file_path
83
+ File.join(runtime_directory, "#{daemon_name}.pid")
84
+ end
85
+
86
+ # Mark the output log.
87
+ def mark_log
88
+ File.open(log_file_path, "a") do |log_file|
89
+ log_file.puts "=== Log Marked @ #{Time.now.to_s} ==="
90
+ end
91
+ end
92
+
93
+ # Prints some information relating to daemon startup problems.
94
+ def tail_log(output)
95
+ lines = LogFile.open(log_file_path).tail_log do |line|
96
+ line.match("=== Log Marked") || line.match("=== Daemon Exception Backtrace")
97
+ end
98
+
99
+ output.puts lines
100
+ end
101
+
102
+ # Check the last few lines of the log file to find out if the daemon crashed.
103
+ def crashed?
104
+ count = 3
105
+
106
+ LogFile.open(log_file_path).tail_log do |line|
107
+ return true if line.match("=== Daemon Crashed")
108
+
109
+ break if (count -= 1) == 0
110
+ end
111
+
112
+ return false
113
+ end
114
+
115
+ # The main function to setup any environment required by the daemon
116
+ def prefork
117
+ @base_directory = File.expand_path(@base_directory) if @base_directory
118
+
119
+ FileUtils.mkdir_p(log_directory)
120
+ FileUtils.mkdir_p(runtime_directory)
121
+ end
122
+
123
+ # The main function to start the daemon
124
+ def startup
125
+ end
126
+
127
+ # The main function to stop the daemon
128
+ def shutdown
129
+ # Interrupt all children processes, preferably to stop them so that they are not left behind.
130
+ Process.kill(0, :INT)
131
+ end
132
+
133
+ def self.daemonize(argv = ARGV)
134
+ @@instance ||= self.new
135
+
136
+ Controller.daemonize(@@instance, argv)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,197 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
+ module Controller
30
+ # This function is called from the daemon executable. It processes ARGV and checks whether the user is asking for `start`, `stop`, `restart`, `status`.
31
+ def self.daemonize(daemon, argv = ARGV)
32
+ case argv.shift
33
+ when 'start'
34
+ start(daemon)
35
+ status(daemon)
36
+ when 'stop'
37
+ stop(daemon)
38
+ status(daemon)
39
+ ProcessFile.cleanup(daemon)
40
+ when 'restart'
41
+ stop(daemon)
42
+ ProcessFile.cleanup(daemon)
43
+ start(daemon)
44
+ status(daemon)
45
+ when 'status'
46
+ status(daemon)
47
+ else
48
+ puts "Invalid command. Please specify start, restart, stop or status."
49
+ exit
50
+ end
51
+ end
52
+
53
+ # This function starts the supplied daemon
54
+ def self.start(daemon)
55
+ puts Rainbow("Starting daemon...").color(:blue)
56
+
57
+ case ProcessFile.status(daemon)
58
+ when :running
59
+ $stderr.puts Rainbow("Daemon already running!").color(:blue)
60
+ return
61
+ when :stopped
62
+ # We are good to go...
63
+ else
64
+ $stderr.puts Rainbow("Daemon in unknown state! Will clear previous state and continue.").color(:red)
65
+ status(daemon)
66
+ ProcessFile.clear(daemon)
67
+ end
68
+
69
+ daemon.prefork
70
+ daemon.mark_log
71
+
72
+ fork do
73
+ Process.setsid
74
+ exit if fork
75
+
76
+ ProcessFile.store(daemon, Process.pid)
77
+
78
+ File.umask 0000
79
+ Dir.chdir daemon.working_directory
80
+
81
+ $stdin.reopen "/dev/null"
82
+ $stdout.reopen daemon.log_file_path, "a"
83
+ $stdout.sync = true
84
+
85
+ $stderr.reopen $stdout
86
+ $stderr.sync = true
87
+
88
+ begin
89
+ daemon.startup
90
+
91
+ trap("INT") do
92
+ daemon.shutdown
93
+ end
94
+ rescue
95
+ $stderr.puts "=== Daemon Exception Backtrace @ #{Time.now.to_s} ==="
96
+ $stderr.puts "#{$!.class}: #{$!.message}"
97
+ $!.backtrace.each { |at| $stderr.puts at }
98
+ $stderr.puts "=== Daemon Crashed ==="
99
+ $stderr.flush
100
+ ensure
101
+ $stderr.puts "=== Daemon Stopping @ #{Time.now.to_s} ==="
102
+ $stderr.flush
103
+ end
104
+ end
105
+
106
+ puts Rainbow("Waiting for daemon to start...").color(:blue)
107
+ sleep 0.1
108
+ timer = TIMEOUT
109
+ pid = ProcessFile.recall(daemon)
110
+
111
+ while pid == nil and timer > 0
112
+ # Wait a moment for the forking to finish...
113
+ puts Rainbow("Waiting for daemon to start (#{timer}/#{TIMEOUT})").color(:blue)
114
+ sleep 1
115
+
116
+ # If the daemon has crashed, it is never going to start...
117
+ break if daemon.crashed?
118
+
119
+ pid = ProcessFile.recall(daemon)
120
+
121
+ timer -= 1
122
+ end
123
+ end
124
+
125
+ # Prints out the status of the daemon
126
+ def self.status(daemon)
127
+ case ProcessFile.status(daemon)
128
+ when :running
129
+ puts Rainbow("Daemon status: running pid=#{ProcessFile.recall(daemon)}").color(:green)
130
+ when :unknown
131
+ if daemon.crashed?
132
+ puts Rainbow("Daemon status: crashed").color(:red)
133
+
134
+ $stdout.flush
135
+ $stderr.puts Rainbow("Dumping daemon crash log:").color(:red)
136
+ daemon.tail_log($stderr)
137
+ else
138
+ puts Rainbow("Daemon status: unknown").color(:red)
139
+ end
140
+ when :stopped
141
+ puts Rainbow("Daemon status: stopped").color(:blue)
142
+ end
143
+ end
144
+
145
+ # Stops the daemon process.
146
+ def self.stop(daemon)
147
+ puts Rainbow("Stopping daemon...").color(:blue)
148
+
149
+ # Check if the pid file exists...
150
+ unless File.file?(daemon.process_file_path)
151
+ puts Rainbow("Pid file not found. Is the daemon running?").color(:red)
152
+ return
153
+ end
154
+
155
+ pid = ProcessFile.recall(daemon)
156
+
157
+ # Check if the daemon is already stopped...
158
+ unless ProcessFile.running(daemon)
159
+ puts Rainbow("Pid #{pid} is not running. Has daemon crashed?").color(:red)
160
+
161
+ daemon.tail_log($stderr)
162
+
163
+ return
164
+ end
165
+
166
+ # Interrupt the process group:
167
+ pgid = -Process.getpgid(pid)
168
+ Process.kill("INT", pgid)
169
+ sleep 0.1
170
+
171
+ sleep 1 if ProcessFile.running(daemon)
172
+
173
+ # Kill/Term loop - if the daemon didn't die easily, shoot
174
+ # it a few more times.
175
+ attempts = 5
176
+ while ProcessFile.running(daemon) and attempts > 0
177
+ sig = (attempts >= 2) ? "KILL" : "TERM"
178
+
179
+ puts Rainbow("Sending #{sig} to process group #{pgid}...").color(:red)
180
+ Process.kill(sig, pgid)
181
+
182
+ attempts -= 1
183
+ sleep 1
184
+ end
185
+
186
+ # If after doing our best the daemon is still running (pretty odd)...
187
+ if ProcessFile.running(daemon)
188
+ puts Rainbow("Daemon appears to be still running!").color(:red)
189
+ return
190
+ end
191
+
192
+ # Otherwise the daemon has been stopped.
193
+ ProcessFile.clear(daemon)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
+ class LogFile < File
24
+ # Yields the lines of a log file in reverse order, once the yield statement returns true, stops, and returns the lines in order.
25
+ def tail_log
26
+ lines = []
27
+
28
+ seek_end
29
+
30
+ reverse_each_line do |line|
31
+ lines << line
32
+
33
+ break if yield line
34
+ end
35
+
36
+ return lines.reverse
37
+ end
38
+
39
+ private
40
+
41
+ # Seek to the end of the file
42
+ def seek_end(offset = 0)
43
+ seek(offset, IO::SEEK_END)
44
+ end
45
+
46
+ # Read a chunk of data and then move the file pointer backwards.
47
+ #
48
+ # Calling this function multiple times will return new data and traverse the file backwards.
49
+ #
50
+ def read_reverse(length)
51
+ offset = tell
52
+
53
+ if offset == 0
54
+ return nil
55
+ end
56
+
57
+ start = [0, offset-length].max
58
+
59
+ seek(start, IO::SEEK_SET)
60
+
61
+ buf = read(offset-start)
62
+
63
+ seek(start, IO::SEEK_SET)
64
+
65
+ return buf
66
+ end
67
+
68
+ REVERSE_BUFFER_SIZE = 128
69
+
70
+ # This function is very similar to gets but it works in reverse.
71
+ #
72
+ # You can use it to efficiently read a file line by line backwards.
73
+ #
74
+ # It returns nil when there are no more lines.
75
+ def reverse_gets(sep_string=$/)
76
+ end_pos = tell
77
+
78
+ offset = nil
79
+ buf = ""
80
+
81
+ while offset == nil
82
+ chunk = read_reverse(REVERSE_BUFFER_SIZE)
83
+ return (buf == "" ? nil : buf) if chunk == nil
84
+
85
+ buf = chunk + buf
86
+
87
+ offset = buf.rindex(sep_string)
88
+ end
89
+
90
+ line = buf[offset...buf.size].sub(sep_string, "")
91
+
92
+ seek((end_pos - buf.size) + offset, IO::SEEK_SET)
93
+
94
+ return line
95
+ end
96
+
97
+ # Similar to each_line but works in reverse. Don't forget to call
98
+ # seek_end before you start!
99
+ def reverse_each_line(sep_string=$/, &block)
100
+ return to_enum(:reverse_each_line) unless block_given?
101
+
102
+ line = reverse_gets(sep_string)
103
+
104
+ while line != nil
105
+ yield line
106
+
107
+ line = reverse_gets(sep_string)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
+ module Priviledges
26
+ # Set the user of the current process. Supply either a user ID
27
+ # or a user name.
28
+ #
29
+ # Be aware that on Mac OS X / Ruby 1.8 there are bugs when the user id
30
+ # is negative (i.e. it doesn't work). For example "nobody" with uid -2
31
+ # won't work.
32
+ def self.change_user(user)
33
+ if user.kind_of?(String)
34
+ user = Etc.getpwnam(user).uid
35
+ end
36
+
37
+ Process::Sys.setuid(user)
38
+ end
39
+
40
+ # Get the user of the current process. Returns the user name.
41
+ def self.current_user
42
+ uid = Process::Sys.getuid
43
+
44
+ Etc.getpwuid(uid).name
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,71 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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
@@ -0,0 +1,25 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
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 = "0.1.0"
24
+ end
25
+ end
@@ -0,0 +1,24 @@
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 = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "rainbow", "~> 2.0"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (c) 2007, 2009, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
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 'minitest/autorun'
24
+
25
+ require 'process/daemon'
26
+
27
+ require 'webrick'
28
+ require 'webrick/https'
29
+
30
+ require 'xmlrpc/server'
31
+ require 'xmlrpc/client'
32
+
33
+ # Very simple XMLRPC daemon
34
+ class XMLRPCDaemon < Process::Daemon
35
+ def working_directory
36
+ File.join(__dir__, "tmp")
37
+ end
38
+
39
+ def startup
40
+ puts "Starting server..."
41
+
42
+ @rpc_server = WEBrick::HTTPServer.new(
43
+ :Port => 31337,
44
+ :BindAddress => "0.0.0.0"
45
+ )
46
+
47
+ @listener = XMLRPC::WEBrickServlet.new
48
+
49
+ @listener.add_handler("add") do |amount|
50
+ @count ||= 0
51
+ @count += amount
52
+ end
53
+
54
+ @listener.add_handler("total") do
55
+ @count
56
+ end
57
+
58
+ @rpc_server.mount("/RPC2", @listener)
59
+
60
+ $stdout.flush
61
+ $stderr.flush
62
+
63
+ begin
64
+ @rpc_server.start
65
+ rescue Interrupt
66
+ puts "Daemon interrupted..."
67
+ ensure
68
+ @rpc_server.shutdown
69
+ end
70
+ end
71
+
72
+ def shutdown
73
+ puts "Stopping the RPC server..."
74
+ @rpc_server.stop
75
+ end
76
+ end
77
+
78
+ class DaemonTest < MiniTest::Test
79
+ def setup
80
+ @daemon = XMLRPCDaemon.new
81
+ Process::Daemon::Controller.start(@daemon)
82
+ end
83
+
84
+ def teardown
85
+ Process::Daemon::Controller.stop(@daemon)
86
+ end
87
+
88
+ def test_connection
89
+ rpc = XMLRPC::Client.new_from_uri("http://localhost:31337")
90
+ rpc.call("add", 10)
91
+
92
+ total = rpc.call("total")
93
+
94
+ assert_equal 10, total
95
+ end
96
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: process-daemon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rainbow
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - samuel.williams@oriontransfer.co.nz
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - .travis.yml
64
+ - Gemfile
65
+ - README.md
66
+ - Rakefile
67
+ - lib/process/daemon.rb
68
+ - lib/process/daemon/controller.rb
69
+ - lib/process/daemon/log_file.rb
70
+ - lib/process/daemon/priviledges.rb
71
+ - lib/process/daemon/process_file.rb
72
+ - lib/process/daemon/version.rb
73
+ - process-daemon.gemspec
74
+ - test/test_daemon.rb
75
+ homepage: ''
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.0.3
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: '`Process::Daemon` is a stable and helpful base class for long running tasks
99
+ and daemons. Provides standard `start`, `stop`, `restart`, `status` operations.'
100
+ test_files:
101
+ - test/test_daemon.rb