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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +8 -0
- data/README.md +77 -0
- data/Rakefile +9 -0
- data/lib/process/daemon.rb +139 -0
- data/lib/process/daemon/controller.rb +197 -0
- data/lib/process/daemon/log_file.rb +112 -0
- data/lib/process/daemon/priviledges.rb +48 -0
- data/lib/process/daemon/process_file.rb +71 -0
- data/lib/process/daemon/version.rb +25 -0
- data/process-daemon.gemspec +24 -0
- data/test/test_daemon.rb +96 -0
- metadata +101 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](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,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
|
data/test/test_daemon.rb
ADDED
@@ -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
|