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.
@@ -0,0 +1 @@
1
+ tmp/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,18 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tiny_daemon (0.0.2)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.2.0)
10
+ rake (10.1.0)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ minitest
17
+ rake
18
+ tiny_daemon!
@@ -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
+
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'lib'
5
+ t.test_files = FileList['tests/**/*_test.rb']
6
+ t.verbose = true
7
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ class TinyDaemon
2
+ VERSION = '0.0.2'
3
+ end
@@ -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,4 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/spec'
3
+
4
+ FileUtils.mkdir_p 'tmp'
@@ -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
@@ -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: