dctl 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +14 -0
- data/README +63 -0
- data/Rakefile +211 -0
- data/TODO +5 -0
- data/bin/dctl +9 -0
- data/lib/dctl.rb +19 -0
- data/lib/dctl/cmdparser.rb +42 -0
- data/lib/dctl/command.rb +135 -0
- data/lib/dctl/daemon.rb +51 -0
- data/lib/dctl/daemonize.rb +143 -0
- data/lib/dctl/defaults.rb +7 -0
- data/lib/dctl/metainf.rb +6 -0
- data/lib/dctl/pidfile.rb +53 -0
- data/setup.rb +1360 -0
- data/test/daemon_test.rb +41 -0
- data/test/daemonize_test.rb +79 -0
- data/test/dctl_test.rb +7 -0
- data/test/infinite.sh +7 -0
- data/test/pidfile_test.rb +52 -0
- data/test/redirection_test.sh +5 -0
- metadata +74 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
*1.0.0* (07th April, 2005) Bruno Carnazzi <mailto:bcarnazzi@gmail.com>
|
2
|
+
* Full rewrite from scratch
|
3
|
+
* Multiple daemon instances does not exists anymore
|
4
|
+
* Load option does not exists anymore
|
5
|
+
* Strongly unit-tested (I hope so)
|
6
|
+
* Smaller, better, stronger :)
|
7
|
+
* Added a 'setup.rb' script
|
8
|
+
* Added a 'rakefile'
|
9
|
+
* Added a bit of docs
|
10
|
+
* ...
|
11
|
+
|
12
|
+
*0.0.8* (14th March, 2005) Bruno Carnazzi <mailto:bcarnazzi@gmail.com>
|
13
|
+
* initial release
|
14
|
+
* so ugly...
|
data/README
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= dctl - a daemon controller written in Ruby
|
2
|
+
|
3
|
+
dctl is a small script for providing daemon functionnalty (start, stop, ...)
|
4
|
+
to non-daemon process or script, with automatic handling of pidfiles.
|
5
|
+
dctl is mainly an extended rewrite of the daemons[http://rubyforge.org/projects/daemons/]
|
6
|
+
project for learning purpose.
|
7
|
+
It is designed to be small and smart and I try to apply XP's good practice (unit test)
|
8
|
+
|
9
|
+
Major features:
|
10
|
+
* Convert any executable file with endless lifecycle into a controllable daemon
|
11
|
+
* Automatic handling of pidfiles
|
12
|
+
* Command-oriented style (dctl requires cmdparser)
|
13
|
+
|
14
|
+
|
15
|
+
== Examples
|
16
|
+
|
17
|
+
=== Start a daemon with redirection of stdout
|
18
|
+
% dctl start -oout test/infinite.sh
|
19
|
+
|
20
|
+
=== Get the status of a daemon
|
21
|
+
% dctl status test/infinite.sh
|
22
|
+
|
23
|
+
=== Stop a daemon
|
24
|
+
% dctl stop test/infinite.sh
|
25
|
+
|
26
|
+
|
27
|
+
== Download
|
28
|
+
|
29
|
+
The latest version of dctl can be found at :
|
30
|
+
* http://rubyforge.org/projects/dctl/
|
31
|
+
|
32
|
+
|
33
|
+
== Installation
|
34
|
+
|
35
|
+
The prefered method of installing dctl is currently through its setup.rb file.
|
36
|
+
As dctl stands on cmdparser, you need to have cmdparser installed before.
|
37
|
+
I plan to make a GEM for dctl soon.
|
38
|
+
|
39
|
+
from dctl distribution directory:
|
40
|
+
% [sudo] ruby setup.rb
|
41
|
+
|
42
|
+
=== Notes
|
43
|
+
If you installed cmdparser as a gem, please check your RUBYOPTS which should looks like:
|
44
|
+
% echo $RUBYOPTS
|
45
|
+
-rubygems
|
46
|
+
|
47
|
+
If not, do the following :
|
48
|
+
% export RUBYOPTS="-rubygems"
|
49
|
+
|
50
|
+
|
51
|
+
== Author
|
52
|
+
|
53
|
+
Written in 2005 by Bruno Carnazzi <mailto:bcarnazzi@gmail.com>
|
54
|
+
|
55
|
+
== License
|
56
|
+
|
57
|
+
dctl is free software. You can redistribute it under the terms specified in
|
58
|
+
the COPYING file of the Ruby distribution.
|
59
|
+
|
60
|
+
|
61
|
+
== Feedback and other resources
|
62
|
+
|
63
|
+
At http://rubyforge.org/projects/dctl/
|
data/Rakefile
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/contrib/rubyforgepublisher'
|
8
|
+
require 'lib/dctl/metainf'
|
9
|
+
|
10
|
+
|
11
|
+
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
12
|
+
PKG_NAME = 'dctl'
|
13
|
+
PKG_VERSION = Dctl::VERSION.join('.') + PKG_BUILD
|
14
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
15
|
+
|
16
|
+
RELEASE_NAME = "REL #{PKG_VERSION}"
|
17
|
+
|
18
|
+
RUBY_FORGE_PROJECT = "dctl"
|
19
|
+
RUBY_FORGE_USER = "bcarnazzi"
|
20
|
+
|
21
|
+
PKG_FILES = FileList[
|
22
|
+
"lib/**/*", "test/*", "bin/*", "CHANGELOG", "README", "setup.rb", "Rakefile", "TODO"
|
23
|
+
].exclude(/\bCVS\b|~$/)
|
24
|
+
|
25
|
+
|
26
|
+
desc "Default Task"
|
27
|
+
task :default => [ :test ]
|
28
|
+
|
29
|
+
# Run the unit tests
|
30
|
+
|
31
|
+
Rake::TestTask.new("test") do |t|
|
32
|
+
t.libs << "test"
|
33
|
+
t.test_files = FileList['test/dctl_test.rb']
|
34
|
+
t.verbose = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate the RDoc documentation
|
38
|
+
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
rdoc.rdoc_dir = 'html'
|
41
|
+
rdoc.title = "dctl - a daemon controller written in Ruby"
|
42
|
+
rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
|
43
|
+
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
44
|
+
rdoc.rdoc_files.include('README', 'TODO', 'CHANGELOG')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Create compressed packages
|
50
|
+
|
51
|
+
dist_dirs = [ "lib", "bin", "test" ]
|
52
|
+
|
53
|
+
spec = Gem::Specification.new do |s|
|
54
|
+
s.name = PKG_NAME
|
55
|
+
s.version = PKG_VERSION
|
56
|
+
s.platform = Gem::Platform::RUBY
|
57
|
+
s.summary = "dctl - a daemon controller written in Ruby"
|
58
|
+
s.description = %q{dctl is a small script for providing daemon functionnalty (start, stop, ...) to non-daemon process or script, with automatic handling of pidfiles.}
|
59
|
+
|
60
|
+
s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG" ]
|
61
|
+
dist_dirs.each do |dir|
|
62
|
+
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
63
|
+
end
|
64
|
+
s.executables = ['dctl']
|
65
|
+
s.bindir = 'bin'
|
66
|
+
|
67
|
+
# s.test_suite_file = 'test/dctl_test.rb'
|
68
|
+
|
69
|
+
s.add_dependency('cmdparse', '= 1.0.0')
|
70
|
+
|
71
|
+
s.require_path = 'lib'
|
72
|
+
s.autorequire = 'dctl'
|
73
|
+
|
74
|
+
s.has_rdoc = true
|
75
|
+
s.extra_rdoc_files = %w( README TODO CHANGELOG )
|
76
|
+
s.rdoc_options.concat ['--main', 'README']
|
77
|
+
|
78
|
+
s.author = "Bruno Carnazzi"
|
79
|
+
s.email = "bcarnazzi@gmail.com"
|
80
|
+
s.homepage = "http://rubyforge.org/project/dctl/"
|
81
|
+
s.rubyforge_project = "dctl"
|
82
|
+
end
|
83
|
+
|
84
|
+
Rake::GemPackageTask.new(spec) do |p|
|
85
|
+
p.gem_spec = spec
|
86
|
+
p.need_tar = true
|
87
|
+
p.need_zip = true
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Publishing ------------------------------------------------------
|
92
|
+
|
93
|
+
desc "Publish the API documentation"
|
94
|
+
task :pdoc => [:rdoc] do
|
95
|
+
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
|
96
|
+
end
|
97
|
+
|
98
|
+
desc "Publish the release files to RubyForge."
|
99
|
+
task :release => [:package] do
|
100
|
+
files = ["gem", "tgz", "zip"].map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
|
101
|
+
|
102
|
+
if RUBY_FORGE_PROJECT then
|
103
|
+
require 'net/http'
|
104
|
+
require 'open-uri'
|
105
|
+
|
106
|
+
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
|
107
|
+
project_data = open(project_uri) { |data| data.read }
|
108
|
+
group_id = project_data[/[?&]group_id=(\d+)/, 1]
|
109
|
+
raise "Couldn't get group id" unless group_id
|
110
|
+
|
111
|
+
# This echos password to shell which is a bit sucky
|
112
|
+
if ENV["RUBY_FORGE_PASSWORD"]
|
113
|
+
password = ENV["RUBY_FORGE_PASSWORD"]
|
114
|
+
else
|
115
|
+
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
|
116
|
+
password = STDIN.gets.chomp
|
117
|
+
end
|
118
|
+
|
119
|
+
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
120
|
+
data = [
|
121
|
+
"login=1",
|
122
|
+
"form_loginname=#{RUBY_FORGE_USER}",
|
123
|
+
"form_pw=#{password}"
|
124
|
+
].join("&")
|
125
|
+
http.post("/account/login.php", data)
|
126
|
+
end
|
127
|
+
|
128
|
+
cookie = login_response["set-cookie"]
|
129
|
+
raise "Login failed" unless cookie
|
130
|
+
headers = { "Cookie" => cookie }
|
131
|
+
|
132
|
+
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
|
133
|
+
release_data = open(release_uri, headers) { |data| data.read }
|
134
|
+
package_id = release_data[/[?&]package_id=(\d+)/, 1]
|
135
|
+
raise "Couldn't get package id" unless package_id
|
136
|
+
|
137
|
+
first_file = true
|
138
|
+
release_id = ""
|
139
|
+
|
140
|
+
files.each do |filename|
|
141
|
+
basename = File.basename(filename)
|
142
|
+
file_ext = File.extname(filename)
|
143
|
+
file_data = File.open(filename, "rb") { |file| file.read }
|
144
|
+
|
145
|
+
puts "Releasing #{basename}..."
|
146
|
+
|
147
|
+
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
148
|
+
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
|
149
|
+
type_map = {
|
150
|
+
".zip" => "3000",
|
151
|
+
".tgz" => "3110",
|
152
|
+
".gz" => "3110",
|
153
|
+
".gem" => "1400"
|
154
|
+
}; type_map.default = "9999"
|
155
|
+
type = type_map[file_ext]
|
156
|
+
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
|
157
|
+
|
158
|
+
query_hash = if first_file then
|
159
|
+
{
|
160
|
+
"group_id" => group_id,
|
161
|
+
"package_id" => package_id,
|
162
|
+
"release_name" => RELEASE_NAME,
|
163
|
+
"release_date" => release_date,
|
164
|
+
"type_id" => type,
|
165
|
+
"processor_id" => "8000", # Any
|
166
|
+
"release_notes" => "",
|
167
|
+
"release_changes" => "",
|
168
|
+
"preformatted" => "1",
|
169
|
+
"submit" => "1"
|
170
|
+
}
|
171
|
+
else
|
172
|
+
{
|
173
|
+
"group_id" => group_id,
|
174
|
+
"release_id" => release_id,
|
175
|
+
"package_id" => package_id,
|
176
|
+
"step2" => "1",
|
177
|
+
"type_id" => type,
|
178
|
+
"processor_id" => "8000", # Any
|
179
|
+
"submit" => "Add This File"
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
query = "?" + query_hash.map do |(name, value)|
|
184
|
+
[name, URI.encode(value)].join("=")
|
185
|
+
end.join("&")
|
186
|
+
|
187
|
+
data = [
|
188
|
+
"--" + boundary,
|
189
|
+
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
|
190
|
+
"Content-Type: application/octet-stream",
|
191
|
+
"Content-Transfer-Encoding: binary",
|
192
|
+
"", file_data, ""
|
193
|
+
].join("\x0D\x0A")
|
194
|
+
|
195
|
+
release_headers = headers.merge(
|
196
|
+
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
|
197
|
+
)
|
198
|
+
|
199
|
+
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
|
200
|
+
http.post(target + query, data, release_headers)
|
201
|
+
end
|
202
|
+
|
203
|
+
if first_file then
|
204
|
+
release_id = release_response.body[/release_id=(\d+)/, 1]
|
205
|
+
raise("Couldn't get release id") unless release_id
|
206
|
+
end
|
207
|
+
|
208
|
+
first_file = false
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
data/TODO
ADDED
data/bin/dctl
ADDED
data/lib/dctl.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'dctl/cmdparser'
|
4
|
+
|
5
|
+
class DctlApp
|
6
|
+
def run
|
7
|
+
begin
|
8
|
+
Dctl::CommandLineParser.parse!(File.basename($0, '.*'), ARGV)
|
9
|
+
rescue
|
10
|
+
puts $!
|
11
|
+
puts 'Use -h or --help for usage.'
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
if __FILE__ == $0 then
|
18
|
+
DctlApp.new.run
|
19
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
require 'cmdparse' # require_gem fails !
|
4
|
+
rescue LoadError
|
5
|
+
require 'cmdparse'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'dctl/metainf'
|
9
|
+
require 'dctl/defaults'
|
10
|
+
require 'dctl/command'
|
11
|
+
|
12
|
+
module Dctl
|
13
|
+
|
14
|
+
# This class is used to parse user command line.
|
15
|
+
class CommandLineParser
|
16
|
+
def self.parse!(program_name, argv)
|
17
|
+
cmd_parser = CommandParser.new
|
18
|
+
|
19
|
+
cmd_parser.options { |opt|
|
20
|
+
opt.program_name = program_name
|
21
|
+
opt.version = Dctl::VERSION
|
22
|
+
opt.release = Dctl::RELEASE
|
23
|
+
opt.separator 'args contains target program to daemonize and its arguments.'
|
24
|
+
opt.separator ''
|
25
|
+
opt.separator 'Global options:'
|
26
|
+
opt.on('-d', '--dir DIR', "Path where to store pidfiles (default is '#{$DIR}')") { |$DIR| }
|
27
|
+
}
|
28
|
+
|
29
|
+
cmd_parser.add_command Dctl::Command::StartCommand.new
|
30
|
+
cmd_parser.add_command Dctl::Command::StopCommand.new
|
31
|
+
cmd_parser.add_command Dctl::Command::RunCommand.new
|
32
|
+
cmd_parser.add_command Dctl::Command::RestartCommand.new
|
33
|
+
cmd_parser.add_command Dctl::Command::ZapCommand.new
|
34
|
+
cmd_parser.add_command Dctl::Command::StatusCommand.new
|
35
|
+
cmd_parser.add_command CommandParser::HelpCommand.new
|
36
|
+
cmd_parser.add_command CommandParser::VersionCommand.new
|
37
|
+
|
38
|
+
cmd_parser.parse! argv
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/dctl/command.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
require 'cmdparse' # require_gem fails !
|
4
|
+
rescue LoadError
|
5
|
+
require 'cmdparse'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'dctl/defaults'
|
9
|
+
require 'dctl/daemon'
|
10
|
+
|
11
|
+
module Dctl
|
12
|
+
|
13
|
+
# This module contains all possible commands allowed by Dctl
|
14
|
+
module Command
|
15
|
+
|
16
|
+
# This module is used by command that redirect their standard io streams.
|
17
|
+
module Redirectible
|
18
|
+
def std_init
|
19
|
+
@stdin = $IN
|
20
|
+
@stdout = $OUT
|
21
|
+
@stderr = $ERR
|
22
|
+
options.separator "Options:"
|
23
|
+
options.on('-i', '--stdin FILE', "Use FILE as stdin when daemonized (default is '#{@stdin}').") { |@stdin| }
|
24
|
+
options.on('-o', '--stdout FILE', "Use FILE as stdout when daemonized (default is '#{@stdout}').") { |@stdout| }
|
25
|
+
options.on('-e', '--stderr FILE', "Use FILE as stderr when daemonized (default is '#{@stderr}').") { |@stderr| }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class StartCommand < CommandParser::Command
|
30
|
+
include Redirectible
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
super 'start'
|
34
|
+
std_init
|
35
|
+
end
|
36
|
+
|
37
|
+
def description
|
38
|
+
'Start a new daemonized instance of a specified application.'
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute(cmd_parser, args)
|
42
|
+
cmd_args = args.slice! 1, args.length
|
43
|
+
cmd = args[0]
|
44
|
+
Dctl::Daemon.new(cmd, $DIR).start(cmd_args, @stdin, @stdout, @stderr)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class RestartCommand < CommandParser::Command
|
49
|
+
include Redirectible
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
super 'restart'
|
53
|
+
std_init
|
54
|
+
end
|
55
|
+
|
56
|
+
def description
|
57
|
+
'Restart a daemonized instance of a specified application with new arguments.'
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(cmd_parser, args)
|
61
|
+
cmd_args = args.slice! 1, args.length
|
62
|
+
cmd = args[0]
|
63
|
+
Dctl::Daemon.new(cmd, $DIR).restart(cmd_args, @stdin, @stdout, @stderr)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class StopCommand < CommandParser::Command
|
68
|
+
def initialize
|
69
|
+
super 'stop'
|
70
|
+
options.separator "Options:"
|
71
|
+
end
|
72
|
+
|
73
|
+
def description
|
74
|
+
'Stop a daemonized instance of a specified application.'
|
75
|
+
end
|
76
|
+
|
77
|
+
def execute(cmd_parser, args)
|
78
|
+
cmd = args[0]
|
79
|
+
Dctl::Daemon.new(cmd, $DIR).stop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class ZapCommand < CommandParser::Command
|
84
|
+
def initialize
|
85
|
+
super 'zap'
|
86
|
+
options.separator "Options:"
|
87
|
+
end
|
88
|
+
|
89
|
+
def description
|
90
|
+
"Manually set a daemonized instance of a specified application to 'stopped' state."
|
91
|
+
end
|
92
|
+
|
93
|
+
def execute(cmd_parser, args)
|
94
|
+
cmd = args[0]
|
95
|
+
Dctl::Daemon.new(cmd, $DIR).zap
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class RunCommand < CommandParser::Command
|
100
|
+
def initialize
|
101
|
+
super 'run'
|
102
|
+
options.separator "Options:"
|
103
|
+
end
|
104
|
+
|
105
|
+
def description
|
106
|
+
'Run a new un-daemonized instance of a specified application.'
|
107
|
+
end
|
108
|
+
|
109
|
+
def execute(cmd_parser, args)
|
110
|
+
cmd_args = args.slice! 1, args.length
|
111
|
+
cmd = args[0]
|
112
|
+
Dctl::Daemon.new(cmd, $DIR).run(cmd_args)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class StatusCommand < CommandParser::Command
|
117
|
+
def initialize
|
118
|
+
super 'status'
|
119
|
+
options.separator "Options:"
|
120
|
+
end
|
121
|
+
|
122
|
+
def description
|
123
|
+
"Get the state of a specified application."
|
124
|
+
end
|
125
|
+
|
126
|
+
def execute(cmd_parser, args)
|
127
|
+
cmd = args[0]
|
128
|
+
puts "#{File.basename cmd}: #{Dctl::Daemon.new(cmd, $DIR).status}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|