process-daemon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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