rbdaemon 0.0.1

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 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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rbdaemon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Changli Gao
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # RBDaemon
2
+
3
+ A daemon library in Ruby
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rbdaemon'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rbdaemon
18
+
19
+ ## Usage
20
+
21
+ ````ruby
22
+ require 'rubygems'
23
+ gem 'rbdaemon'
24
+ require 'rbdaemon'
25
+
26
+ RBDaemon::Daemon.new do
27
+ loop do
28
+ sleep(3)
29
+ end
30
+ end
31
+ ````
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 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
+ t.test_files = FileList['test/**/*.rb']
7
+ end
8
+
9
+ task :default => :build
@@ -0,0 +1,7 @@
1
+
2
+ module RBDaemon
3
+ PID_FILE_DIR = File.join('/', 'var', 'run')
4
+ WORK_DIR = '/'
5
+ STDIO_PATH = File.join('/', 'dev', 'null')
6
+ UMASK = 0
7
+ end
@@ -0,0 +1,92 @@
1
+
2
+ require 'rbdaemon/conventions'
3
+ require 'rbdaemon/pidfile'
4
+ require 'etc'
5
+ require 'syslog'
6
+
7
+ module RBDaemon
8
+ class DaemonError < StandardError; end
9
+
10
+ class Daemon
11
+ def initialize(options = {}, *args)
12
+ exit!(0) if fork
13
+ Process.setsid
14
+ Signal.trap('HUP'){}
15
+ exit!(0) if fork
16
+
17
+ options[:no_change_umask] or File.umask(RBDaemon::UMASK)
18
+
19
+ unless options[:no_close]
20
+ ObjectSpace.each_object(IO) do |io|
21
+ [STDIN, STDOUT, STDERR].include?(io) and next
22
+ io.closed? or io.close rescue nil
23
+ end
24
+ STDIN.reopen(RBDaemon::STDIO_PATH)
25
+ STDOUT.reopen(RBDaemon::STDIO_PATH, 'w')
26
+ STDERR.reopen(RBDaemon::STDIO_PATH, 'w')
27
+ end
28
+
29
+ @pid_file = PidFile.new(options[:pid_file])
30
+
31
+ unless options[:no_trap_term]
32
+ Signal.trap('TERM') do
33
+ exit(0)
34
+ end
35
+ end
36
+
37
+ ident = options[:pid_file] ? options[:pid_file] : $0
38
+ ident = File.basename(ident).split(/\./)[0]
39
+
40
+ Signal.trap('EXIT') do
41
+ Syslog::log(Syslog::LOG_INFO, "#{ident} exited")
42
+ options[:change_root] or (@pid_file and @pid_file.delete) rescue nil
43
+ end
44
+
45
+ if options[:user]
46
+ user = options[:user]
47
+ options[:groups] ||= [user]
48
+ uid = user.class == String ? Etc.getpwnam(user).uid : user
49
+ end
50
+
51
+ if options[:groups]
52
+ groups = options[:groups].map do |group|
53
+ case group
54
+ when Fixnum
55
+ group
56
+ when String
57
+ Etc.getgrnam(group).gid
58
+ else
59
+ raise ArgumentError, 'invalid group: ' + group
60
+ end
61
+ end
62
+ if options[:user]
63
+ groups.concat(Process.initgroups(options[:user], groups[0]))
64
+ end
65
+ if options[:user] and groups.include?(uid)
66
+ gid = uid
67
+ else
68
+ gid = groups[0]
69
+ end
70
+ Process::groups = groups.uniq
71
+ Process::GID::change_privilege(gid)
72
+ end
73
+
74
+ Syslog::open(ident, Syslog::LOG_NDELAY | Syslog::LOG_PID,
75
+ Syslog::LOG_DAEMON)
76
+
77
+ if options[:change_root]
78
+ Dir.chdir(options[:change_root])
79
+ Dir.chroot('.')
80
+ elsif not options[:no_change_dir]
81
+ Dir.chdir(RBDaemon::WORK_DIR)
82
+ end
83
+
84
+ Process::UID::change_privilege(uid) if options[:user]
85
+
86
+ Syslog::log(Syslog::LOG_INFO, "#{ident} started")
87
+ yield self, *args
88
+
89
+ exit(0)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+
2
+ require 'rbdaemon/conventions'
3
+
4
+ module RBDaemon
5
+ class PidFileError < StandardError; end
6
+
7
+ class PidFile
8
+ def initialize(pid_file = nil)
9
+ if pid_file
10
+ @path = pid_file
11
+ else
12
+ program = File.basename($0).split(/\./)[0]
13
+ @path = File.join(RBDaemon::PID_FILE_DIR, program + '.pid')
14
+ end
15
+ @file = File.new(@path, File::WRONLY | File::CREAT, 0644)
16
+ unless @file.flock(File::LOCK_NB | File::LOCK_EX)
17
+ raise PidFileError, 'failed to lock ' + @path
18
+ end
19
+ @file.truncate(0)
20
+ @file.write("#{$$}\n")
21
+ @file.flush
22
+ end
23
+
24
+ def delete
25
+ File.delete(@path)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module RBDaemon
3
+ VERSION = '0.0.1'
4
+ end
data/lib/rbdaemon.rb ADDED
@@ -0,0 +1,5 @@
1
+
2
+ require 'rbdaemon/conventions'
3
+ require 'rbdaemon/daemon'
4
+ require 'rbdaemon/pidfile'
5
+ require 'rbdaemon/version'
data/rbdaemon.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rbdaemon/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rbdaemon"
8
+ gem.version = RBDaemon::VERSION
9
+ gem.authors = ["Changli Gao"]
10
+ gem.email = ["xiaosuo@gmail.com"]
11
+ gem.description = %q{A daemon library in Ruby}
12
+ gem.summary = %q{Yet another daemon library in Ruby}
13
+ gem.homepage = "http://github.com/xiaosuo/rbdaemon"
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+ gem.license = 'MIT'
19
+ end
data/test/tc_daemon.rb ADDED
@@ -0,0 +1,130 @@
1
+
2
+ require 'test/unit'
3
+ require 'rbdaemon/daemon'
4
+ require 'tempfile'
5
+
6
+ class TC_Daemon < Test::Unit::TestCase
7
+ def test_smoke
8
+ assert_equal(0, Process.uid)
9
+ tmpfile = ::Tempfile.new('tc_daemon', '/tmp')
10
+ File::chmod(0666, tmpfile.path)
11
+ Process.fork do
12
+ RBDaemon::Daemon.new({:user => 'nobody', :groups => 'nobody',
13
+ :change_root => '/tmp'},
14
+ 3, "ok\n", tmpfile.path) do |daemon, times, str, path|
15
+ tmpfile = File.new(File.basename(path), 'w')
16
+ times.times do
17
+ tmpfile.write(str)
18
+ end
19
+ tmpfile.flush
20
+ loop do
21
+ sleep(3)
22
+ end
23
+ end
24
+ end
25
+ # let the daemon process run
26
+ sleep(1)
27
+
28
+ # check pid
29
+ pid_file = "/var/run/#{File.basename($0).split(/\./)[0]}.pid"
30
+ assert(File.exist?(pid_file))
31
+ pid = IO.read(pid_file).rstrip.to_i
32
+ assert_nothing_raised{Process::kill(0, pid)}
33
+ proc_root = "/proc/#{pid}"
34
+ status = {}
35
+ IO.foreach("#{proc_root}/status") do |l|
36
+ k, v = l.rstrip.split(/:\s*/)
37
+ status[k] = v
38
+ end
39
+ assert_equal(pid, status['Pid'].to_i)
40
+
41
+ stat = IO.read("#{proc_root}/stat").rstrip.split(/\s+/)
42
+ assert_equal(pid, stat[0].to_i)
43
+ # the current ppid should 1
44
+ assert_equal(1, stat[3].to_i)
45
+ # must not be the group leader
46
+ assert_not_equal(pid, stat[4].to_i)
47
+ # a different session id
48
+ assert_not_equal(IO.read("/proc/self/stat").rstrip.split(/\s+/)[5], stat[5])
49
+ # pgid == sid
50
+ assert_equal(stat[4], stat[5])
51
+
52
+ # check daemon's work
53
+ lines = IO.readlines(tmpfile.path)
54
+ assert_equal(3, lines.length)
55
+ assert_equal("ok\n", lines[0])
56
+
57
+ # check uid and gid
58
+ uid = Etc.getpwnam('nobody').uid
59
+ gid = Etc.getgrnam('nobody').gid
60
+ status['Uid'].split(/\s+/).each do |s|
61
+ assert_equal(uid, s.to_i)
62
+ end
63
+ status['Gid'].split(/\s+/).each do |s|
64
+ assert_equal(gid, s.to_i)
65
+ end
66
+
67
+ # check fds
68
+ Dir["#{proc_root}/fd/*"].each do |fn|
69
+ fd = File.basename(fn).to_i
70
+ assert((0..5).to_a.include?(fd))
71
+ case fd
72
+ when 0, 1, 2
73
+ assert_equal('/dev/null', File::readlink(fn))
74
+ when 3
75
+ assert_equal(pid_file, File::readlink(fn))
76
+ when 4
77
+ assert_match(/socket:\[\d+\]/, File::readlink(fn))
78
+ when 5
79
+ assert_equal(tmpfile.path, File::readlink(fn))
80
+ end
81
+ end
82
+
83
+ # check root directory
84
+ assert_equal('/tmp', File::readlink("#{proc_root}/root"))
85
+ assert_equal('/tmp', File::readlink("#{proc_root}/cwd"))
86
+
87
+ # terminate daemon
88
+ assert_nothing_raised{Process::kill('TERM', pid)}
89
+ sleep(1)
90
+ # check pid file
91
+ assert(File.exist?(pid_file))
92
+ assert_raise(Errno::ESRCH){Process::kill(0, pid)}
93
+ end
94
+
95
+ def test_normal_use
96
+ assert_equal(0, Process.uid)
97
+ Process.fork do
98
+ RBDaemon::Daemon.new do
99
+ loop{sleep(3)}
100
+ end
101
+ end
102
+ sleep(1)
103
+ pid_file = "/var/run/#{File.basename($0).split(/\./)[0]}.pid"
104
+ assert(File.exist?(pid_file))
105
+ pid = IO.read(pid_file).rstrip.to_i
106
+ assert_nothing_raised{Process::kill(0, pid)}
107
+ proc_root = "/proc/#{pid}"
108
+ assert_equal('/', File::readlink("#{proc_root}/root"))
109
+ assert_equal('/', File::readlink("#{proc_root}/cwd"))
110
+ assert_nothing_raised{Process::kill('TERM', pid)}
111
+ sleep(1)
112
+ assert_equal(false, File.exist?(pid_file))
113
+ end
114
+
115
+ def test_non_root
116
+ Process.uid == 0 and Process.euid = 1000
117
+ pid_file = Tempfile.new('tc_daemon')
118
+ Process.fork do
119
+ RBDaemon::Daemon.new(:pid_file => pid_file.path) do
120
+ loop{sleep(3)}
121
+ end
122
+ end
123
+ sleep(1)
124
+ pid = pid_file.read.rstrip.to_i
125
+ assert_nothing_raised{Process::kill('TERM', pid)}
126
+ sleep(1)
127
+ assert_equal(false, File.exist?(pid_file.path))
128
+ Process.uid == 0 and Process.euid = 0
129
+ end
130
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require 'test/unit'
3
+ require 'rbdaemon/pidfile'
4
+
5
+ class TC_PidFile < Test::Unit::TestCase
6
+ def test_smoke
7
+ assert_equal(0, Process.uid)
8
+ path = "/var/run/#{File.basename($0).split(/\./)[0]}.pid"
9
+ pid_file = RBDaemon::PidFile.new
10
+ assert(File.exist?(path))
11
+ assert_equal($$, IO.read(path).rstrip.to_i)
12
+ assert_raise(RBDaemon::PidFileError){RBDaemon::PidFile.new}
13
+ pid_file.delete
14
+ assert_equal(false, File.exist?(path))
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbdaemon
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Changli Gao
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-11-30 00:00:00 +08:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: A daemon library in Ruby
23
+ email:
24
+ - xiaosuo@gmail.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE.txt
35
+ - README.md
36
+ - Rakefile
37
+ - lib/rbdaemon.rb
38
+ - lib/rbdaemon/conventions.rb
39
+ - lib/rbdaemon/daemon.rb
40
+ - lib/rbdaemon/pidfile.rb
41
+ - lib/rbdaemon/version.rb
42
+ - rbdaemon.gemspec
43
+ - test/tc_daemon.rb
44
+ - test/tc_pidfile.rb
45
+ has_rdoc: true
46
+ homepage: http://github.com/xiaosuo/rbdaemon
47
+ licenses:
48
+ - MIT
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Yet another daemon library in Ruby
79
+ test_files:
80
+ - test/tc_daemon.rb
81
+ - test/tc_pidfile.rb