perennial 0.2.2.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/bin/perennial +36 -0
- data/lib/perennial/application.rb +173 -0
- data/lib/perennial/argument_parser.rb +60 -0
- data/lib/perennial/core_ext/attribute_accessors.rb +129 -0
- data/lib/perennial/core_ext/blank.rb +51 -0
- data/lib/perennial/core_ext/misc.rb +118 -0
- data/lib/perennial/core_ext.rb +4 -0
- data/lib/perennial/daemon.rb +123 -0
- data/lib/perennial/dispatchable.rb +110 -0
- data/lib/perennial/exceptions.rb +6 -0
- data/lib/perennial/generator.rb +89 -0
- data/lib/perennial/hookable.rb +63 -0
- data/lib/perennial/loader.rb +103 -0
- data/lib/perennial/loggable.rb +15 -0
- data/lib/perennial/logger.rb +109 -0
- data/lib/perennial/manifest.rb +34 -0
- data/lib/perennial/option_parser.rb +105 -0
- data/lib/perennial/settings.rb +95 -0
- data/lib/perennial.rb +21 -0
- data/templates/application.erb +15 -0
- data/templates/boot.erb +2 -0
- data/templates/rakefile.erb +31 -0
- data/templates/setup.erb +4 -0
- data/templates/test.erb +9 -0
- data/templates/test_helper.erb +35 -0
- data/test/dispatchable_test.rb +129 -0
- data/test/hookable_test.rb +61 -0
- data/test/loader_test.rb +1 -0
- data/test/loggable_test.rb +38 -0
- data/test/logger_test.rb +57 -0
- data/test/settings_test.rb +99 -0
- data/test/test_helper.rb +38 -0
- data/vendor/fakefs/LICENSE +20 -0
- data/vendor/fakefs/README.markdown +37 -0
- data/vendor/fakefs/Rakefile +3 -0
- data/vendor/fakefs/lib/fakefs.rb +448 -0
- data/vendor/fakefs/test/fakefs_test.rb +511 -0
- data/vendor/fakefs/test/verify.rb +27 -0
- metadata +92 -0
data/bin/perennial
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "perennial")
|
3
|
+
|
4
|
+
Perennial::Application.processing(ARGV) do |a|
|
5
|
+
|
6
|
+
a.banner = "Perennial v#{Perennial::VERSION} CLI"
|
7
|
+
|
8
|
+
a.generator!
|
9
|
+
|
10
|
+
a.option(:force, "force the creation of the application")
|
11
|
+
a.add("create PATH [APP-NAME]", "Creates a Perennial-based library with a given PATH and, if provided, APP-NAME.") do |path, *args|
|
12
|
+
# Get the app name, path etc.
|
13
|
+
opts = args.extract_options!
|
14
|
+
app_name = args.empty? ? File.basename(path) : args.shift
|
15
|
+
path = File.expand_path(path)
|
16
|
+
# Check if the folder exists
|
17
|
+
if File.exist?(path) && !opts[:force]
|
18
|
+
puts "The path you tried to use, #{path}, already exists. Please try another or pass --force"
|
19
|
+
exit
|
20
|
+
end
|
21
|
+
# Convert the name and class name.
|
22
|
+
app_path = app_name.underscore
|
23
|
+
app_module = app_name.camelize
|
24
|
+
# Actually do the generation.
|
25
|
+
env = {:application_module => app_module, :application_path => app_path}
|
26
|
+
setup_generator path
|
27
|
+
folders 'tmp', 'config', 'lib', 'handlers', 'test'
|
28
|
+
template 'application.erb', "lib/#{app_path}.rb", env
|
29
|
+
template 'boot.erb', 'config/boot.rb', env
|
30
|
+
template 'setup.erb', 'config/setup.rb', env
|
31
|
+
template 'rakefile.erb', 'Rakefile', env
|
32
|
+
template 'test_helper.erb', 'test/test_helper.rb', env
|
33
|
+
template 'test.erb', "test/#{app_path}_test.rb", env
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Perennial
|
2
|
+
class Application
|
3
|
+
|
4
|
+
class CommandEnv
|
5
|
+
include Loggable
|
6
|
+
|
7
|
+
# Executes a given block with given
|
8
|
+
# arguments in a specified environment.
|
9
|
+
# Used to provide the logger functionality
|
10
|
+
# as well as the ability to do things
|
11
|
+
# such as easily write generators.
|
12
|
+
def self.execute(blk, arguments)
|
13
|
+
klass = Class.new(self)
|
14
|
+
klass.class_eval { define_method(:apply, &blk) }
|
15
|
+
klass.new.apply(*arguments)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :options, :banner, :command_env
|
21
|
+
|
22
|
+
def initialize(opts = {})
|
23
|
+
@options = opts
|
24
|
+
@commands = {}
|
25
|
+
@descriptions = {}
|
26
|
+
@option_parsers = {}
|
27
|
+
@option_parser = nil
|
28
|
+
@command_options = {}
|
29
|
+
@command_env = (opts[:command_env] || CommandEnv)
|
30
|
+
@banners = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def option(name, description = nil)
|
34
|
+
option_parser.add(name, description) { |v| @command_options[name] = v }
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_default_options!
|
38
|
+
option_parser.add_defaults!
|
39
|
+
end
|
40
|
+
|
41
|
+
def generator!
|
42
|
+
if defined?(Generator)
|
43
|
+
self.command_env = Generator::CommandEnv
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def controller!(controller, description)
|
48
|
+
return unless defined?(Loader)
|
49
|
+
add_default_options!
|
50
|
+
option :kill, "Kill any runninng instances"
|
51
|
+
controller_name = controller.to_s.underscore
|
52
|
+
controller = controller.to_sym
|
53
|
+
command_name = controller_name.gsub("_", "-")
|
54
|
+
add("#{command_name} [PATH]", description) do |*args|
|
55
|
+
options = args.extract_options!
|
56
|
+
path = File.expand_path(args[0] || ".")
|
57
|
+
Settings.root = path
|
58
|
+
if options.delete(:kill)
|
59
|
+
puts "Attempting to kill processess..."
|
60
|
+
Daemon.kill_all(controller)
|
61
|
+
else
|
62
|
+
Loader.run!(controller, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
def add(raw_command, description = nil, &blk)
|
70
|
+
raise ArgumentError, "You must provide a block with an #{self.class.name}#add" if blk.nil?
|
71
|
+
raise ArgumentError, "Your block must accept atleast one argument (a hash of options)" if blk.arity == 0
|
72
|
+
command, _ = raw_command.split(" ", 2)
|
73
|
+
@banners[command] = raw_command
|
74
|
+
@commands[command] = blk
|
75
|
+
@descriptions[command] = description if description.present?
|
76
|
+
# Add the default help message for a command
|
77
|
+
option_parser.add(:help, "Show this message") { help_for(command) }
|
78
|
+
@option_parsers[command] = option_parser
|
79
|
+
@option_parser = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def execute(arguments)
|
83
|
+
return usage if arguments.empty?
|
84
|
+
arguments = arguments.dup
|
85
|
+
command = arguments.shift
|
86
|
+
if @commands.has_key?(command)
|
87
|
+
execute_command(command, arguments)
|
88
|
+
else
|
89
|
+
puts "Unknown command '#{command}', please try again."
|
90
|
+
return usage
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def usage
|
95
|
+
if banner.present?
|
96
|
+
puts banner
|
97
|
+
puts ""
|
98
|
+
end
|
99
|
+
puts "Usage:"
|
100
|
+
max_width = @banners.values.map { |b| b.length }.max
|
101
|
+
@commands.keys.sort.each do |command|
|
102
|
+
next unless @descriptions.has_key?(command)
|
103
|
+
formatted_command = "#{@banners[command]} [OPTIONS]".ljust(max_width + 10)
|
104
|
+
command = "%s - %s" % [formatted_command, @descriptions[command]]
|
105
|
+
puts command
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def help_for(command)
|
110
|
+
if banner.present?
|
111
|
+
puts banner
|
112
|
+
puts ""
|
113
|
+
end
|
114
|
+
puts @descriptions[command]
|
115
|
+
puts "Usage: #{$0} #{@banners[command]} [options]"
|
116
|
+
puts "Options:"
|
117
|
+
puts @option_parsers[command].summary
|
118
|
+
exit
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.processing(args, &blk)
|
122
|
+
application = self.new
|
123
|
+
if blk.arity == 1
|
124
|
+
blk.call(application)
|
125
|
+
else
|
126
|
+
application.instance_eval(&blk)
|
127
|
+
end
|
128
|
+
application.execute args
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
def execute_command(command, arguments)
|
134
|
+
command_proc = @commands[command]
|
135
|
+
args, opts = extract_arguments(command, arguments)
|
136
|
+
if valid_arity?(command_proc, args)
|
137
|
+
args << opts
|
138
|
+
@command_env.execute(command_proc, args)
|
139
|
+
else
|
140
|
+
usage
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_arguments(command, arguments)
|
145
|
+
option_parser = @option_parsers[command]
|
146
|
+
if option_parser.present?
|
147
|
+
option_parser.parse(arguments)
|
148
|
+
return option_parser.arguments, @command_options
|
149
|
+
else
|
150
|
+
return arguments, {}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def option_parser(reset = false)
|
155
|
+
@option_parser = nil if reset
|
156
|
+
@option_parser ||= OptionParser.new
|
157
|
+
end
|
158
|
+
|
159
|
+
def valid_arity?(blk, arguments)
|
160
|
+
needed_count = blk.arity - 1
|
161
|
+
provided_count = arguments.size
|
162
|
+
if needed_count > 0 && needed_count != provided_count
|
163
|
+
puts "You didn't provide the correct number of arguments (needed #{needed_count}, provided #{provided_count})"
|
164
|
+
elsif needed_count < 0 && (-needed_count - 2) > provided_count
|
165
|
+
puts "You didn't provide enough arguments - a minimum of #{-needed_count} are needed."
|
166
|
+
else
|
167
|
+
return true
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Perennial
|
4
|
+
class ArgumentParser
|
5
|
+
|
6
|
+
attr_reader :arguments
|
7
|
+
|
8
|
+
def initialize(array)
|
9
|
+
@raw_arguments = array.dup
|
10
|
+
@arguments = []
|
11
|
+
@results = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
@results ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse!
|
19
|
+
while !@raw_arguments.empty?
|
20
|
+
current = @raw_arguments.shift
|
21
|
+
if option?(current)
|
22
|
+
process_option(current, @raw_arguments.first)
|
23
|
+
else
|
24
|
+
@arguments.push current
|
25
|
+
end
|
26
|
+
end
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.parse(args = ARGV)
|
31
|
+
parser = self.new(args)
|
32
|
+
parser.parse!
|
33
|
+
return parser.arguments, parser.to_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def process_option(current, next_arg)
|
39
|
+
name = clean(current)
|
40
|
+
if name.include?("=")
|
41
|
+
real_name, raw_value = name.split("=", 2)
|
42
|
+
@results[real_name] = Shellwords.shellwords(raw_value).join(" ")
|
43
|
+
elsif !(next_arg.nil? || option?(next_arg))
|
44
|
+
@raw_arguments.shift
|
45
|
+
@results[name] = next_arg
|
46
|
+
else
|
47
|
+
@results[name] = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def clean(name)
|
52
|
+
name.gsub /^\-+/, ''
|
53
|
+
end
|
54
|
+
|
55
|
+
def option?(argument)
|
56
|
+
argument.strip =~ /^\-+/
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# cattr_* and class_inheritable_* are taken from
|
2
|
+
# ActiveSupport. Included here to help keep the
|
3
|
+
# codebase simple / clean.
|
4
|
+
class Class
|
5
|
+
def cattr_reader(*syms)
|
6
|
+
syms.flatten.each do |sym|
|
7
|
+
next if sym.is_a?(Hash)
|
8
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
9
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
10
|
+
@@#{sym} = nil # @@hair_colors = nil
|
11
|
+
end # end
|
12
|
+
#
|
13
|
+
def self.#{sym} # def self.hair_colors
|
14
|
+
@@#{sym} # @@hair_colors
|
15
|
+
end # end
|
16
|
+
#
|
17
|
+
def #{sym} # def hair_colors
|
18
|
+
@@#{sym} # @@hair_colors
|
19
|
+
end # end
|
20
|
+
EOS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def cattr_writer(*syms)
|
25
|
+
options = syms.extract_options!
|
26
|
+
syms.flatten.each do |sym|
|
27
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
28
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
29
|
+
@@#{sym} = nil # @@hair_colors = nil
|
30
|
+
end # end
|
31
|
+
#
|
32
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
33
|
+
@@#{sym} = obj # @@hair_colors = obj
|
34
|
+
end # end
|
35
|
+
#
|
36
|
+
#{" #
|
37
|
+
def #{sym}=(obj) # def hair_colors=(obj)
|
38
|
+
@@#{sym} = obj # @@hair_colors = obj
|
39
|
+
end # end
|
40
|
+
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
41
|
+
EOS
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cattr_accessor(*syms)
|
46
|
+
cattr_reader(*syms)
|
47
|
+
cattr_writer(*syms)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines class-level inheritable attribute reader. Attributes are available to subclasses,
|
51
|
+
# each subclass has a copy of parent's attribute.
|
52
|
+
#
|
53
|
+
# @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
|
54
|
+
# @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
#
|
58
|
+
# @todo Do we want to block instance_reader via :instance_reader => false
|
59
|
+
# @todo It would be preferable that we do something with a Hash passed in
|
60
|
+
# (error out or do the same as other methods above) instead of silently
|
61
|
+
# moving on). In particular, this makes the return value of this function
|
62
|
+
# less useful.
|
63
|
+
def class_inheritable_reader(*ivars)
|
64
|
+
instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
|
65
|
+
|
66
|
+
ivars.each do |ivar|
|
67
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
68
|
+
def self.#{ivar}
|
69
|
+
return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
|
70
|
+
ivar = superclass.#{ivar}
|
71
|
+
return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
|
72
|
+
@#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) ? ivar.dup : ivar
|
73
|
+
end
|
74
|
+
RUBY
|
75
|
+
unless instance_reader == false
|
76
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
77
|
+
def #{ivar}
|
78
|
+
self.class.#{ivar}
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Defines class-level inheritable attribute writer. Attributes are available to subclasses,
|
86
|
+
# each subclass has a copy of parent's attribute.
|
87
|
+
#
|
88
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
89
|
+
# define inheritable writer for.
|
90
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
91
|
+
# @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
#
|
95
|
+
# @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
|
96
|
+
# class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
|
97
|
+
def class_inheritable_writer(*ivars)
|
98
|
+
instance_writer = ivars.pop[:writer] if ivars.last.is_a?(Hash)
|
99
|
+
ivars.each do |ivar|
|
100
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
101
|
+
def self.#{ivar}=(obj)
|
102
|
+
@#{ivar} = obj
|
103
|
+
end
|
104
|
+
RUBY
|
105
|
+
unless instance_writer == false
|
106
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
107
|
+
def #{ivar}=(obj) self.class.#{ivar} = obj end
|
108
|
+
RUBY
|
109
|
+
end
|
110
|
+
|
111
|
+
self.send("#{ivar}=", yield) if block_given?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
|
116
|
+
# each subclass has a copy of parent's attribute.
|
117
|
+
#
|
118
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
119
|
+
# define inheritable accessor for.
|
120
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
121
|
+
# @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
|
122
|
+
#
|
123
|
+
# @api public
|
124
|
+
def class_inheritable_accessor(*syms, &block)
|
125
|
+
class_inheritable_reader(*syms)
|
126
|
+
class_inheritable_writer(*syms, &block)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# blank? parts taken from the active support source
|
2
|
+
# code, used under the MIT License.
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def blank?
|
6
|
+
respond_to?(:empty?) ? empty? : !self
|
7
|
+
end
|
8
|
+
|
9
|
+
def present?
|
10
|
+
!blank?
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class NilClass #:nodoc:
|
16
|
+
def blank?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FalseClass #:nodoc:
|
22
|
+
def blank?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class TrueClass #:nodoc:
|
28
|
+
def blank?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Array #:nodoc:
|
34
|
+
alias_method :blank?, :empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
class Hash #:nodoc:
|
38
|
+
alias_method :blank?, :empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
class String #:nodoc:
|
42
|
+
def blank?
|
43
|
+
self !~ /\S/
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Numeric #:nodoc:
|
48
|
+
def blank?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
def metaclass
|
4
|
+
class << self; self; end
|
5
|
+
end
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class Inflector
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def underscore(camel_cased_word)
|
13
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
14
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
15
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
16
|
+
tr("-", "_").
|
17
|
+
downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
21
|
+
if first_letter_in_uppercase
|
22
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
23
|
+
else
|
24
|
+
lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Kernel
|
32
|
+
|
33
|
+
# From http://oldrcrs.rubypal.com/rcr/show/309
|
34
|
+
def __DIR__(offset = 0)
|
35
|
+
(/^(.+)?:\d+/ =~ caller[offset + 1]) ? File.dirname($1) : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Shorthand for lambda
|
39
|
+
# e.g. L{|r| puts r}
|
40
|
+
def L(&blk)
|
41
|
+
lambda(&blk)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Shorthand for Proc.new
|
45
|
+
# e.g. P{|r| puts r}
|
46
|
+
def P(&blk)
|
47
|
+
Proc.new(&blk)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class String
|
52
|
+
def /(*args)
|
53
|
+
File.join(self, *args)
|
54
|
+
end
|
55
|
+
|
56
|
+
def underscore
|
57
|
+
Inflector.underscore(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def camelize(capitalize_first_letter = true)
|
61
|
+
Inflector.camelize(self, capitalize_first_letter)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_pathname
|
65
|
+
Pathname.new(self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Array
|
70
|
+
def extract_options!
|
71
|
+
last.is_a?(Hash) ? pop : {}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Hash
|
76
|
+
def symbolize_keys
|
77
|
+
hash = self.dup
|
78
|
+
hash.symbolize_keys!
|
79
|
+
return hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def symbolize_keys!
|
83
|
+
hash = {}
|
84
|
+
self.each_pair { |k,v| hash[k.to_sym] = v }
|
85
|
+
replace hash
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Module
|
90
|
+
|
91
|
+
def has_libary(*items)
|
92
|
+
namespace = self.to_s.underscore
|
93
|
+
items.each do |item|
|
94
|
+
require File.join(namespace, item.to_s.underscore)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def extends_library(*items)
|
99
|
+
namespace = self.to_s.underscore
|
100
|
+
items.each do |item|
|
101
|
+
klass = item.to_s.camelize.to_sym
|
102
|
+
# Load if it isn't loaded already.
|
103
|
+
const_get(klass) unless const_defined?(klass)
|
104
|
+
# And finally load the file.
|
105
|
+
require File.join(namespace, item.to_s.underscore)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def attempt_require(*files)
|
110
|
+
files.each do |file|
|
111
|
+
begin
|
112
|
+
require file
|
113
|
+
rescue LoadError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Perennial
|
4
|
+
# = Perennial::Daemon provides a relatively simple
|
5
|
+
# interface to creating unix daemons from a given process.
|
6
|
+
# Namely, it focuses on three providing tools for
|
7
|
+
# a couple of simple purposes.
|
8
|
+
#
|
9
|
+
# == Turning a process into a daemon
|
10
|
+
#
|
11
|
+
# This approach is as as simple as calling
|
12
|
+
# Perennial::Daemon.daemonize! - the current process
|
13
|
+
# will then be a daemon, the non-daemon processes
|
14
|
+
# will be killed and if started by the loader, it will
|
15
|
+
# also write a pid out.
|
16
|
+
#
|
17
|
+
# == Checking whether a process is alive
|
18
|
+
#
|
19
|
+
# Perennial::Daemon.alive?(pid) is a super simple way
|
20
|
+
# to check if a pid (as an integer) is current active / alive,
|
21
|
+
#
|
22
|
+
# Perennial::Daemon.any_alive? takes a type and tells you if
|
23
|
+
# any processes with the associated type are alive.
|
24
|
+
#
|
25
|
+
# == Storing / Retrieving Pids for a Given Loader Type
|
26
|
+
#
|
27
|
+
# Perennial::Daemon.daemonize! will store the pid for
|
28
|
+
# the current process if Perennial::Loader.current_type
|
29
|
+
# is present.
|
30
|
+
#
|
31
|
+
# Perennial::Daemon.any_alive?(type = :all) and
|
32
|
+
# Perennial::Daemon.kill_all(type = :all) can be
|
33
|
+
# used to deal with checking the status of and killing
|
34
|
+
# processes associated with a given loader type.
|
35
|
+
class Daemon
|
36
|
+
class << self
|
37
|
+
|
38
|
+
def any_alive?(type = :all)
|
39
|
+
!pid_file_for(type).empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns true / false depending on whether
|
43
|
+
# a process with the given pid exists.
|
44
|
+
def alive?(pid)
|
45
|
+
return Process.getpgid(pid) != -1
|
46
|
+
rescue Errno::ESRCH
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Kills all processes associated with a certain app type.
|
51
|
+
# E.g. Given a loader is starting a :client, kill_all(:client)
|
52
|
+
# would kill all associated processes (usually after daemonizing)
|
53
|
+
# and kill_all would do the same - but if we also started
|
54
|
+
# a process for :server, kill_all(:client wouldn't kill the
|
55
|
+
# the process where as kill_all would.
|
56
|
+
def kill_all(type = :all)
|
57
|
+
kill_all_from(pid_file_for(type))
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
|
61
|
+
# Converts the current process into a Unix-daemon using
|
62
|
+
# the double fork approach. Also, changes process file
|
63
|
+
# mask to 000 and reopens STDIN / OUT to /dev/null
|
64
|
+
def daemonize!
|
65
|
+
exit if fork
|
66
|
+
Process.setsid
|
67
|
+
exit if fork
|
68
|
+
self.write_pid
|
69
|
+
File.umask 0000
|
70
|
+
STDIN.reopen "/dev/null"
|
71
|
+
STDOUT.reopen "/dev/null", "a"
|
72
|
+
STDERR.reopen STDOUT
|
73
|
+
Perennial::Settings.verbose = false
|
74
|
+
end
|
75
|
+
|
76
|
+
# Cleans up processes for the current application type
|
77
|
+
# (deteremined by the loader) and then removes the pid file.
|
78
|
+
def cleanup!
|
79
|
+
f = pids_file_for(Loader.current_type)
|
80
|
+
FileUtils.rm_f(f) if (pids_from(f) - Process.pid).blank?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns an array of pid's associated with a given type.
|
84
|
+
def pids_for_type(type)
|
85
|
+
pids_from(pid_file_for(type))
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def kill_all_from(file)
|
91
|
+
pids = pids_from(file)
|
92
|
+
pids.each { |p| Process.kill("TERM", p) unless p == Process.pid }
|
93
|
+
FileUtils.rm_f(file)
|
94
|
+
rescue => e
|
95
|
+
STDOUT.puts e.inspect
|
96
|
+
end
|
97
|
+
|
98
|
+
def pid_file_for(type)
|
99
|
+
type = "*" if type == :all
|
100
|
+
Settings.root / "tmp" / "pids" / "#{type.to_s.underscore}.pid"
|
101
|
+
end
|
102
|
+
|
103
|
+
def pids_from(files)
|
104
|
+
pids = []
|
105
|
+
Dir[files].each do |file|
|
106
|
+
pids += File.read(file).split("\n").map { |l| l.strip.to_i(10) }
|
107
|
+
end
|
108
|
+
return pids.uniq.select { |p| alive?(p) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def write_pid
|
112
|
+
type = Loader.current_type
|
113
|
+
return if type.blank?
|
114
|
+
f = pid_file_for(type)
|
115
|
+
pids = pids_from(f)
|
116
|
+
pids << Process.pid unless pids.include?(Process.pid)
|
117
|
+
FileUtils.mkdir_p(File.dirname(f))
|
118
|
+
File.open(f, "w+") { |f| f.puts pids.join("\n") }
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|