dctl 1.0.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.
- 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
|