tiny_daemon 0.0.2
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.
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +18 -0
- data/README.md +68 -0
- data/Rakefile +7 -0
- data/lib/tiny_daemon.rb +127 -0
- data/lib/tiny_daemon/process_file.rb +30 -0
- data/lib/tiny_daemon/version.rb +3 -0
- data/tests/example.rb +22 -0
- data/tests/taemons_test.rb +112 -0
- data/tests/test_helper.rb +4 -0
- data/tests/tiny_daemon/process_file_test.rb +40 -0
- data/tiny_daemon.gemspec +20 -0
- metadata +91 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
tmp/*
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
tiny_daemon
|
2
|
+
================
|
3
|
+
|
4
|
+
* It daemonizes a Ruby block and supports graceful exiting.
|
5
|
+
* It only supports and guarantee running only one process at any moment
|
6
|
+
|
7
|
+
|
8
|
+
How to use
|
9
|
+
--------------
|
10
|
+
|
11
|
+
Please include this into your Gemfile:
|
12
|
+
|
13
|
+
```
|
14
|
+
gem 'tiny_daemon'
|
15
|
+
```
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
p = TinyDaemon.new("test_process", "pids", "log")
|
19
|
+
|
20
|
+
p.stop # Allow only one instance to run at any moment
|
21
|
+
|
22
|
+
p.exit do
|
23
|
+
is_running = false
|
24
|
+
puts "The process #{Process.pid} is being killed gracefully "
|
25
|
+
end
|
26
|
+
|
27
|
+
p.start do
|
28
|
+
while is_running
|
29
|
+
puts "Test"
|
30
|
+
sleep(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "The process #{Process.pid} exited gracefully"
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
There will be a pid file at pids/test_process.pid. The log file will be at log/test_process.log.
|
38
|
+
|
39
|
+
We can issue `kill [pid]` in order to exit the process gracefully. Please note that using `kill -9 [pid]` will terminate the process immediately.
|
40
|
+
|
41
|
+
|
42
|
+
Run example
|
43
|
+
--------------
|
44
|
+
|
45
|
+
1. Install all dependencies: `bundle install`
|
46
|
+
2. Run the example: `bundle exec ruby tests/example.rb`
|
47
|
+
|
48
|
+
|
49
|
+
Requirement
|
50
|
+
--------------
|
51
|
+
|
52
|
+
* >= Ruby 2.0
|
53
|
+
* *nix platform
|
54
|
+
|
55
|
+
|
56
|
+
How to develop
|
57
|
+
--------------
|
58
|
+
|
59
|
+
1. Install all dependencies: `bundle install`
|
60
|
+
2. Run all tests: `bundle exec rake test`
|
61
|
+
3. Start developing
|
62
|
+
|
63
|
+
|
64
|
+
Author
|
65
|
+
--------------
|
66
|
+
|
67
|
+
Tanin Na Nakorn
|
68
|
+
|
data/Rakefile
ADDED
data/lib/tiny_daemon.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'tiny_daemon/version'
|
2
|
+
require 'tiny_daemon/process_file'
|
3
|
+
|
4
|
+
class TinyDaemon
|
5
|
+
attr_accessor :app_name, :pid_dir, :log_dir
|
6
|
+
|
7
|
+
START_TIMEOUT = 5
|
8
|
+
STOP_TIMEOUT = 30
|
9
|
+
|
10
|
+
def initialize(app_name, pid_dir, log_dir)
|
11
|
+
@app_name = app_name
|
12
|
+
@pid_dir = pid_dir
|
13
|
+
@log_dir = log_dir
|
14
|
+
end
|
15
|
+
|
16
|
+
def exit(&block)
|
17
|
+
@exit_block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def process
|
21
|
+
@process ||= ProcessFile.new(self.app_name, self.pid_dir)
|
22
|
+
end
|
23
|
+
|
24
|
+
def start(&block)
|
25
|
+
raise "#{self.process.pid_file_path} already exists. The process might already run. Remove it manually if it doesn't." if self.process.get
|
26
|
+
|
27
|
+
Process.fork do
|
28
|
+
Process.daemon(true, true)
|
29
|
+
|
30
|
+
self.process.store(Process.pid)
|
31
|
+
reset_umask
|
32
|
+
set_process_name
|
33
|
+
redirect_io
|
34
|
+
|
35
|
+
if @exit_block
|
36
|
+
trap("TERM") { exit_on_double_signals } # If `kill <pid>` is called, it will be trapped here
|
37
|
+
end
|
38
|
+
|
39
|
+
at_exit { clean_process_file }
|
40
|
+
block.call
|
41
|
+
end
|
42
|
+
|
43
|
+
wait_until_start
|
44
|
+
self.process.get
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop(timeout_secs = STOP_TIMEOUT)
|
48
|
+
pid = self.process.get
|
49
|
+
|
50
|
+
if pid
|
51
|
+
begin
|
52
|
+
Process.kill('TERM', pid)
|
53
|
+
rescue => e
|
54
|
+
end
|
55
|
+
|
56
|
+
wait_until_exit(pid, timeout_secs)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def wait_until_start
|
62
|
+
start_time = Time.now.to_i
|
63
|
+
while self.process.get.nil?
|
64
|
+
sleep(0.1)
|
65
|
+
|
66
|
+
if (Time.now.to_i - start_time) > START_TIMEOUT
|
67
|
+
raise "Waiting too long (for #{START_TIMEOUT} seconds) to start. There might be an exception. Please check the log"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait_until_exit(pid, timeout_secs)
|
73
|
+
start_time = Time.now.to_i
|
74
|
+
while true
|
75
|
+
begin
|
76
|
+
Process.kill(0, pid)
|
77
|
+
rescue Errno::ESRCH
|
78
|
+
break # the process doesn't exist anymore
|
79
|
+
rescue ::Exception # for example on EPERM (process exists but does not belong to us)
|
80
|
+
end
|
81
|
+
|
82
|
+
sleep(0.1)
|
83
|
+
|
84
|
+
if (Time.now.to_i - start_time) > timeout_secs
|
85
|
+
raise "Waiting too long (for #{timeout_secs} seconds) to exit."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def reset_umask
|
91
|
+
File.umask(0000) # We don't want to inherit umask from the parent process
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_process_name
|
95
|
+
$0 = self.app_name # In `ps aux`, it will show this name
|
96
|
+
end
|
97
|
+
|
98
|
+
def log_file
|
99
|
+
File.join(self.log_dir, "#{self.app_name}.log")
|
100
|
+
end
|
101
|
+
|
102
|
+
def redirect_io
|
103
|
+
FileUtils.mkdir_p File.dirname(self.log_dir), :mode => 0755
|
104
|
+
FileUtils.touch log_file
|
105
|
+
File.chmod(0644, log_file)
|
106
|
+
$stdout.reopen(log_file, 'a')
|
107
|
+
$stderr.reopen($stdout)
|
108
|
+
$stdout.sync = true
|
109
|
+
end
|
110
|
+
|
111
|
+
def exit_on_double_signals
|
112
|
+
@kill_count ||= 0
|
113
|
+
|
114
|
+
if @kill_count == 0
|
115
|
+
@kill_count += 1
|
116
|
+
@exit_block.call
|
117
|
+
else
|
118
|
+
clean_process_file
|
119
|
+
puts "TERM is signaled twice. Therefore, we kill the process #{Process.pid} immediately."
|
120
|
+
Kernel.exit(0)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def clean_process_file
|
125
|
+
self.process.clean
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class TinyDaemon
|
2
|
+
class ProcessFile < Struct.new(:app_name, :pid_dir)
|
3
|
+
def pid_file_path
|
4
|
+
File.join(self.pid_dir, "#{self.app_name}.pid")
|
5
|
+
end
|
6
|
+
|
7
|
+
def store(pid)
|
8
|
+
raise "#{pid_file_path} already exists. The process might already run. Remove it manually if it doesn't." if File.exists?(pid_file_path)
|
9
|
+
|
10
|
+
FileUtils.mkdir_p(self.pid_dir)
|
11
|
+
File.open(pid_file_path, 'w') do |f|
|
12
|
+
f.write("#{pid}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get
|
17
|
+
pid = if File.exists?(pid_file_path)
|
18
|
+
IO.read(pid_file_path).strip.to_i
|
19
|
+
else
|
20
|
+
0
|
21
|
+
end
|
22
|
+
|
23
|
+
pid == 0 ? nil : pid
|
24
|
+
end
|
25
|
+
|
26
|
+
def clean
|
27
|
+
FileUtils.rm(pid_file_path) if File.exists?(pid_file_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/tests/example.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.push('lib')
|
2
|
+
|
3
|
+
require 'tiny_daemon'
|
4
|
+
|
5
|
+
is_running = true
|
6
|
+
|
7
|
+
daemon = TinyDaemon.new("tiny_daemon_example", "tmp", "tmp")
|
8
|
+
daemon.stop
|
9
|
+
daemon.exit do
|
10
|
+
is_running = false
|
11
|
+
puts "Being killed gracefully "
|
12
|
+
end
|
13
|
+
pid = daemon.start do
|
14
|
+
while is_running
|
15
|
+
puts Time.now.to_i.to_s
|
16
|
+
sleep(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
puts "Exit gracefully"
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "It is running in the process #{pid}"
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require './tests/test_helper'
|
2
|
+
require 'tiny_daemon'
|
3
|
+
|
4
|
+
def test_daemon(use_signal)
|
5
|
+
is_running = true
|
6
|
+
exit_invoked = false
|
7
|
+
exit_gracefully = false
|
8
|
+
|
9
|
+
File.delete('tmp/test_tiny_daemon.log')
|
10
|
+
|
11
|
+
daemon = TinyDaemon.new("test_tiny_daemon", "tmp", "tmp")
|
12
|
+
daemon.stop
|
13
|
+
daemon.exit do
|
14
|
+
is_running = false
|
15
|
+
exit_invoked = true
|
16
|
+
end
|
17
|
+
pid = daemon.start do
|
18
|
+
while is_running
|
19
|
+
puts Time.now.to_i.to_s
|
20
|
+
sleep(0.1)
|
21
|
+
end
|
22
|
+
|
23
|
+
exit_gracefully = true
|
24
|
+
end
|
25
|
+
|
26
|
+
assert(true, File.exists?('tmp/test_tiny_daemon.log'))
|
27
|
+
assert(true, File.exists?('tmp/test_tiny_daemon.pid'))
|
28
|
+
|
29
|
+
sleep(3)
|
30
|
+
|
31
|
+
if use_signal
|
32
|
+
Process.kill("TERM", pid)
|
33
|
+
else
|
34
|
+
daemon.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
assert(true, exit_invoked)
|
38
|
+
assert(true, exit_gracefully)
|
39
|
+
end
|
40
|
+
|
41
|
+
describe TinyDaemon do
|
42
|
+
it "fork a new process and signals TERM" do
|
43
|
+
test_daemon(true)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can stop programmatically" do
|
47
|
+
test_daemon(false)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "starts twice with no problem" do
|
51
|
+
is_running = true
|
52
|
+
|
53
|
+
daemon = TinyDaemon.new("test_tiny_daemon", "tmp", "tmp")
|
54
|
+
daemon.stop
|
55
|
+
daemon.exit do
|
56
|
+
is_running = false
|
57
|
+
end
|
58
|
+
pid1 = daemon.start do
|
59
|
+
while is_running
|
60
|
+
puts Time.now.to_i.to_s
|
61
|
+
sleep(0.1)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
daemon.stop
|
66
|
+
pid2 = daemon.start do
|
67
|
+
while is_running
|
68
|
+
puts Time.now.to_i.to_s
|
69
|
+
sleep(0.1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
assert_equal(false, pid1 == pid2)
|
74
|
+
assert_raises(Errno::ESRCH) {
|
75
|
+
Process.kill(0, pid1)
|
76
|
+
}
|
77
|
+
assert_equal(1, Process.kill(0, pid2))
|
78
|
+
|
79
|
+
daemon.stop
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should timeout on start" do
|
83
|
+
daemon = TinyDaemon.new("test_tiny_daemon", "tmp", "tmp")
|
84
|
+
daemon.stop
|
85
|
+
|
86
|
+
assert_raises(RuntimeError) {
|
87
|
+
daemon.start do
|
88
|
+
raise 'exception'
|
89
|
+
end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should timeout on stop" do
|
94
|
+
is_running = true
|
95
|
+
|
96
|
+
daemon = TinyDaemon.new("test_tiny_daemon", "tmp", "tmp")
|
97
|
+
daemon.stop
|
98
|
+
daemon.exit do
|
99
|
+
puts "exit invoked"
|
100
|
+
end
|
101
|
+
daemon.start do
|
102
|
+
while is_running
|
103
|
+
puts Time.now.to_i.to_s
|
104
|
+
sleep(0.1)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_raises(RuntimeError) { daemon.stop(1) }
|
109
|
+
|
110
|
+
is_running = false
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require './tests/test_helper'
|
3
|
+
require 'tiny_daemon/process_file'
|
4
|
+
|
5
|
+
describe TinyDaemon::ProcessFile do
|
6
|
+
before(:each) do
|
7
|
+
File.delete("tmp/test_app.pid") if File.exists?("tmp/test_app.pid")
|
8
|
+
assert_equal(false, File.exists?("tmp/test_app.pid"))
|
9
|
+
end
|
10
|
+
|
11
|
+
it "uses correct file path" do
|
12
|
+
assert_equal("tmp/test_app.pid", TinyDaemon::ProcessFile.new("test_app", "tmp").pid_file_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "deletes pid file" do
|
16
|
+
FileUtils.touch("tmp/test_app.pid")
|
17
|
+
assert_equal(true, File.exists?("tmp/test_app.pid"))
|
18
|
+
|
19
|
+
TinyDaemon::ProcessFile.new("test_app", "tmp").clean
|
20
|
+
assert_equal(false, File.exists?("tmp/test_app.pid"))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "stores and gets" do
|
24
|
+
pf = TinyDaemon::ProcessFile.new("test_app", "tmp")
|
25
|
+
pf.store(1111)
|
26
|
+
|
27
|
+
assert_equal(1111, pf.get)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "doesn't store" do
|
31
|
+
pf = TinyDaemon::ProcessFile.new("test_app", "tmp")
|
32
|
+
pf.store(1111)
|
33
|
+
|
34
|
+
assert_raises(RuntimeError) {
|
35
|
+
pf.store(1112)
|
36
|
+
}
|
37
|
+
|
38
|
+
assert_equal(1111, pf.get)
|
39
|
+
end
|
40
|
+
end
|
data/tiny_daemon.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require './lib/tiny_daemon/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "tiny_daemon"
|
5
|
+
s.version = TinyDaemon::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Tanin Na Nakorn"]
|
8
|
+
s.email = ["tanin47@yahoo.com"]
|
9
|
+
s.homepage = "http://github.com/tanin47/tiny_daemon"
|
10
|
+
s.summary = %q{tiny_daemon}
|
11
|
+
s.description = %q{Daemonize a Ruby code}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {tests}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_development_dependency("rake")
|
19
|
+
s.add_development_dependency("minitest")
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tiny_daemon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tanin Na Nakorn
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-01-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Daemonize a Ruby code
|
47
|
+
email:
|
48
|
+
- tanin47@yahoo.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- Gemfile
|
55
|
+
- Gemfile.lock
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- lib/tiny_daemon.rb
|
59
|
+
- lib/tiny_daemon/process_file.rb
|
60
|
+
- lib/tiny_daemon/version.rb
|
61
|
+
- tests/example.rb
|
62
|
+
- tests/taemons_test.rb
|
63
|
+
- tests/test_helper.rb
|
64
|
+
- tests/tiny_daemon/process_file_test.rb
|
65
|
+
- tiny_daemon.gemspec
|
66
|
+
homepage: http://github.com/tanin47/tiny_daemon
|
67
|
+
licenses: []
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.8.24
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: tiny_daemon
|
90
|
+
test_files: []
|
91
|
+
has_rdoc:
|