tennpipes-base 3.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +294 -0
- data/Rakefile +1 -0
- data/bin/tennpipes +8 -0
- data/lib/tennpipes-base.rb +196 -0
- data/lib/tennpipes-base/application.rb +175 -0
- data/lib/tennpipes-base/application/application_setup.rb +202 -0
- data/lib/tennpipes-base/application/authenticity_token.rb +25 -0
- data/lib/tennpipes-base/application/flash.rb +229 -0
- data/lib/tennpipes-base/application/params_protection.rb +129 -0
- data/lib/tennpipes-base/application/routing.rb +1002 -0
- data/lib/tennpipes-base/application/show_exceptions.rb +50 -0
- data/lib/tennpipes-base/caller.rb +53 -0
- data/lib/tennpipes-base/cli/adapter.rb +33 -0
- data/lib/tennpipes-base/cli/base.rb +105 -0
- data/lib/tennpipes-base/cli/console.rb +20 -0
- data/lib/tennpipes-base/cli/launcher.rb +103 -0
- data/lib/tennpipes-base/cli/rake.rb +50 -0
- data/lib/tennpipes-base/cli/rake_tasks.rb +72 -0
- data/lib/tennpipes-base/command.rb +38 -0
- data/lib/tennpipes-base/ext/sinatra.rb +29 -0
- data/lib/tennpipes-base/filter.rb +52 -0
- data/lib/tennpipes-base/images/404.png +0 -0
- data/lib/tennpipes-base/images/500.png +0 -0
- data/lib/tennpipes-base/loader.rb +202 -0
- data/lib/tennpipes-base/logger.rb +492 -0
- data/lib/tennpipes-base/module.rb +58 -0
- data/lib/tennpipes-base/mounter.rb +308 -0
- data/lib/tennpipes-base/path_router.rb +119 -0
- data/lib/tennpipes-base/path_router/compiler.rb +110 -0
- data/lib/tennpipes-base/path_router/error_handler.rb +8 -0
- data/lib/tennpipes-base/path_router/matcher.rb +123 -0
- data/lib/tennpipes-base/path_router/route.rb +169 -0
- data/lib/tennpipes-base/reloader.rb +309 -0
- data/lib/tennpipes-base/reloader/rack.rb +26 -0
- data/lib/tennpipes-base/reloader/storage.rb +55 -0
- data/lib/tennpipes-base/router.rb +98 -0
- data/lib/tennpipes-base/server.rb +119 -0
- data/lib/tennpipes-base/tasks.rb +21 -0
- data/lib/tennpipes-base/version.rb +20 -0
- data/lib/tennpipes-base/version.rb~ +20 -0
- data/test/fixtures/app_gem/Gemfile +4 -0
- data/test/fixtures/app_gem/app/app.rb +3 -0
- data/test/fixtures/app_gem/app_gem.gemspec +17 -0
- data/test/fixtures/app_gem/lib/app_gem.rb +7 -0
- data/test/fixtures/app_gem/lib/app_gem/version.rb +3 -0
- data/test/fixtures/apps/complex.rb +32 -0
- data/test/fixtures/apps/demo_app.rb +7 -0
- data/test/fixtures/apps/demo_demo.rb +7 -0
- data/test/fixtures/apps/demo_project/api/app.rb +7 -0
- data/test/fixtures/apps/demo_project/api/lib/api_lib.rb +3 -0
- data/test/fixtures/apps/demo_project/app.rb +7 -0
- data/test/fixtures/apps/external_apps/fake_lib.rb +1 -0
- data/test/fixtures/apps/external_apps/fake_root.rb +2 -0
- data/test/fixtures/apps/helpers/class_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/instance_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/support.rb +1 -0
- data/test/fixtures/apps/helpers/system_helpers.rb +8 -0
- data/test/fixtures/apps/kiq.rb +3 -0
- data/test/fixtures/apps/lib/myklass.rb +2 -0
- data/test/fixtures/apps/lib/myklass/mysubklass.rb +4 -0
- data/test/fixtures/apps/models/child.rb +2 -0
- data/test/fixtures/apps/models/parent.rb +5 -0
- data/test/fixtures/apps/mountable_apps/rack_apps.rb +15 -0
- data/test/fixtures/apps/mountable_apps/static.html +1 -0
- data/test/fixtures/apps/precompiled_app.rb +19 -0
- data/test/fixtures/apps/simple.rb +32 -0
- data/test/fixtures/apps/static.rb +10 -0
- data/test/fixtures/apps/system.rb +13 -0
- data/test/fixtures/apps/system_class_methods_demo.rb +7 -0
- data/test/fixtures/apps/system_instance_methods_demo.rb +7 -0
- data/test/fixtures/dependencies/a.rb +9 -0
- data/test/fixtures/dependencies/b.rb +4 -0
- data/test/fixtures/dependencies/c.rb +1 -0
- data/test/fixtures/dependencies/circular/e.rb +13 -0
- data/test/fixtures/dependencies/circular/f.rb +2 -0
- data/test/fixtures/dependencies/circular/g.rb +2 -0
- data/test/fixtures/dependencies/d.rb +4 -0
- data/test/fixtures/reloadable_apps/external/app/app.rb +6 -0
- data/test/fixtures/reloadable_apps/external/app/controllers/base.rb +6 -0
- data/test/fixtures/reloadable_apps/main/app.rb +10 -0
- data/test/helper.rb +30 -0
- data/test/test_application.rb +185 -0
- data/test/test_core.rb +93 -0
- data/test/test_csrf_protection.rb +208 -0
- data/test/test_dependencies.rb +57 -0
- data/test/test_filters.rb +389 -0
- data/test/test_flash.rb +168 -0
- data/test/test_locale.rb +21 -0
- data/test/test_logger.rb +295 -0
- data/test/test_mounter.rb +302 -0
- data/test/test_params_protection.rb +195 -0
- data/test/test_reloader_complex.rb +74 -0
- data/test/test_reloader_external.rb +21 -0
- data/test/test_reloader_simple.rb +101 -0
- data/test/test_reloader_system.rb +113 -0
- data/test/test_restful_routing.rb +33 -0
- data/test/test_router.rb +281 -0
- data/test/test_routing.rb +2328 -0
- metadata +301 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
module Tennpipes
|
2
|
+
module PathRouter
|
3
|
+
#
|
4
|
+
# High performance engine for finding all routes which are matched with pattern
|
5
|
+
#
|
6
|
+
class Compiler
|
7
|
+
# All regexps generated by recursive compiler
|
8
|
+
attr_reader :regexps
|
9
|
+
|
10
|
+
##
|
11
|
+
# Constructs an instance of Tennpipes::PathRouter::Compiler
|
12
|
+
#
|
13
|
+
def initialize(routes)
|
14
|
+
@routes = routes
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Compiles all routes into regexps.
|
19
|
+
#
|
20
|
+
def compile!
|
21
|
+
return if compiled?
|
22
|
+
@regexps = @routes.map.with_index do |route, index|
|
23
|
+
route.index = index
|
24
|
+
/(?<_#{index}>#{route.matcher.to_regexp})/
|
25
|
+
end
|
26
|
+
@regexps = recursive_compile(@regexps)
|
27
|
+
@compiled = true
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Returns true if all routes has been compiled.
|
32
|
+
#
|
33
|
+
def compiled?
|
34
|
+
!!@compiled
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Finds routes by using request or env.
|
39
|
+
#
|
40
|
+
def find_by(request_or_env)
|
41
|
+
request = request_or_env.is_a?(Hash) ? Sinatra::Request.new(request_or_env) : request_or_env
|
42
|
+
pattern = encode_default_external(request.path_info)
|
43
|
+
verb = request.request_method
|
44
|
+
rotation { |offset| match?(offset, pattern) }.select { |route| route.verb == verb }
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Calls routes by using request.
|
49
|
+
#
|
50
|
+
def call_by_request(request)
|
51
|
+
rotation do |offset|
|
52
|
+
pattern = encode_default_external(request.path_info)
|
53
|
+
if route = match?(offset, pattern)
|
54
|
+
params = route.params_for(pattern, request.params)
|
55
|
+
yield(route, params) if route.verb == request.request_method
|
56
|
+
route
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Finds routes by using PATH_INFO.
|
63
|
+
#
|
64
|
+
def find_by_pattern(pattern)
|
65
|
+
pattern = pattern.encode(Encoding.default_external)
|
66
|
+
rotation { |offset| match?(offset, pattern) }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
##
|
72
|
+
# Returns a instance of PathRouter::Route if path is matched with current regexp
|
73
|
+
#
|
74
|
+
def match?(offset, path)
|
75
|
+
current_regexp = @regexps[offset]
|
76
|
+
return unless current_regexp === path || current_regexp === path[0..-2]
|
77
|
+
@routes[offset..-1].detect{ |route| Regexp.last_match["_#{route.index}"] }
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Runs through all regexps to find routes.
|
82
|
+
#
|
83
|
+
def rotation(offset = 0)
|
84
|
+
compile! unless compiled?
|
85
|
+
loop.with_object([]) do |_, candidacies|
|
86
|
+
return candidacies unless route = yield(offset)
|
87
|
+
candidacies << route
|
88
|
+
offset = route.index.next
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Compiles routes into regexp recursively.
|
94
|
+
#
|
95
|
+
def recursive_compile(regexps, paths = [])
|
96
|
+
return paths if regexps.length.zero?
|
97
|
+
paths << Regexp.union(regexps)
|
98
|
+
regexps.shift
|
99
|
+
recursive_compile(regexps, paths)
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Encode string with Encoding.default_external
|
104
|
+
#
|
105
|
+
def encode_default_external(string)
|
106
|
+
string.encode(Encoding.default_external)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'mustermann/sinatra'
|
2
|
+
|
3
|
+
module Tennpipes
|
4
|
+
module PathRouter
|
5
|
+
class Matcher
|
6
|
+
# To count group of regexp
|
7
|
+
GROUP_REGEXP = %r{\((?!\?:|\?!|\?<=|\?<!|\?=).+?\)}.freeze
|
8
|
+
|
9
|
+
##
|
10
|
+
# Constructs an instance of PathRouter::Matcher.
|
11
|
+
#
|
12
|
+
def initialize(path, options = {})
|
13
|
+
@path = path.is_a?(String) && path.empty? ? "/" : path
|
14
|
+
@capture = options[:capture]
|
15
|
+
@default_values = options[:default_values]
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Matches a pattern with the route matcher.
|
20
|
+
#
|
21
|
+
def match(pattern)
|
22
|
+
if match_data = handler.match(pattern)
|
23
|
+
match_data
|
24
|
+
elsif mustermann? && pattern != "/" && pattern.end_with?("/")
|
25
|
+
handler.match(pattern[0..-2])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Returns a regexp from handler.
|
31
|
+
#
|
32
|
+
def to_regexp
|
33
|
+
mustermann? ? handler.to_regexp : handler
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Expands the path by using parameters.
|
38
|
+
#
|
39
|
+
def expand(params)
|
40
|
+
params = params.merge(@default_values) if @default_values.is_a?(Hash)
|
41
|
+
expanded_path = handler.expand(:append, params)
|
42
|
+
expanded_path
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns true if handler is an instance of Mustermann.
|
47
|
+
#
|
48
|
+
def mustermann?
|
49
|
+
handler.instance_of?(Mustermann::Sinatra)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Builds a parameters, and returns them.
|
54
|
+
#
|
55
|
+
def params_for(pattern, others)
|
56
|
+
data = match(pattern)
|
57
|
+
params = indifferent_hash
|
58
|
+
if data.names.empty?
|
59
|
+
params.merge!(:captures => data.captures) unless data.captures.empty?
|
60
|
+
else
|
61
|
+
if mustermann?
|
62
|
+
new_params = handler.params(pattern, :captures => data)
|
63
|
+
params.merge!(new_params) if new_params
|
64
|
+
elsif data
|
65
|
+
params.merge!(Hash[names.zip(data.captures)])
|
66
|
+
end
|
67
|
+
params.merge!(others){ |_, old, new| old || new }
|
68
|
+
end
|
69
|
+
params
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Returns the handler which is an instance of Mustermann or Regexp.
|
74
|
+
#
|
75
|
+
def handler
|
76
|
+
@handler ||=
|
77
|
+
case @path
|
78
|
+
when String
|
79
|
+
Mustermann.new(@path, :capture => @capture)
|
80
|
+
when Regexp
|
81
|
+
/^(?:#{@path})$/
|
82
|
+
else
|
83
|
+
@path
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Converts the handler into string.
|
89
|
+
#
|
90
|
+
def to_s
|
91
|
+
handler.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns names of the handler.
|
96
|
+
# @see Regexp#names
|
97
|
+
#
|
98
|
+
def names
|
99
|
+
handler.names
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Returns captures parameter length.
|
104
|
+
#
|
105
|
+
def capture_length
|
106
|
+
if mustermann?
|
107
|
+
handler.named_captures.inject(0) { |count, (_, capture)| count += capture.length }
|
108
|
+
else
|
109
|
+
handler.inspect.scan(GROUP_REGEXP).length
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
##
|
116
|
+
# Creates a hash with indifferent access.
|
117
|
+
#
|
118
|
+
def indifferent_hash
|
119
|
+
Hash.new{ |hash, key| hash[key.to_s] if key.instance_of?(Symbol) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Tennpipes
|
2
|
+
module PathRouter
|
3
|
+
class Route
|
4
|
+
##
|
5
|
+
# The accessors are useful to access from PathRouter::Router
|
6
|
+
#
|
7
|
+
attr_accessor :name, :capture, :order, :options, :index
|
8
|
+
|
9
|
+
##
|
10
|
+
# A reader for compile option
|
11
|
+
#
|
12
|
+
attr_reader :verb, :block
|
13
|
+
|
14
|
+
##
|
15
|
+
# The router will be treated in this class
|
16
|
+
#
|
17
|
+
attr_writer :router
|
18
|
+
|
19
|
+
##
|
20
|
+
# The accessors will be used in other classes
|
21
|
+
#
|
22
|
+
attr_accessor :action, :cache, :cache_key, :cache_expires,
|
23
|
+
:parent, :use_layout, :controller, :user_agent, :path_for_generation, :default_values
|
24
|
+
|
25
|
+
##
|
26
|
+
# Constructs an instance of PathRouter::Route.
|
27
|
+
#
|
28
|
+
def initialize(path, verb, options = {}, &block)
|
29
|
+
@path = path
|
30
|
+
@verb = verb
|
31
|
+
@capture = {}
|
32
|
+
@order = 0
|
33
|
+
@block = block if block_given?
|
34
|
+
merge_with_options!(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Calls the route block with arguments.
|
39
|
+
#
|
40
|
+
def call(app, *args)
|
41
|
+
@block.call(app, *args)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Returns the route's verb as an array.
|
46
|
+
#
|
47
|
+
def request_methods
|
48
|
+
[verb.to_s.upcase]
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Returns the original path.
|
53
|
+
#
|
54
|
+
def original_path
|
55
|
+
@path
|
56
|
+
end
|
57
|
+
|
58
|
+
SIGNIFICANT_VARIABLES_REGEX = /(^|[^\\])[:\*]([a-zA-Z0-9_]+)/.freeze
|
59
|
+
|
60
|
+
##
|
61
|
+
# Returns signficant variable names.
|
62
|
+
#
|
63
|
+
def significant_variable_names
|
64
|
+
@significant_variable_names ||=
|
65
|
+
if @path.is_a?(String)
|
66
|
+
@path.scan(SIGNIFICANT_VARIABLES_REGEX).map{ |p| p.last.to_sym }
|
67
|
+
elsif @path.is_a?(Regexp) and @path.respond_to?(:named_captures)
|
68
|
+
@path.named_captures.keys.map(&:to_sym)
|
69
|
+
else
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Returns an instance of PathRouter::Matcher that is associated with the route.
|
76
|
+
#
|
77
|
+
def matcher
|
78
|
+
@matcher ||= Matcher.new(@path, :capture => @capture, :default_values => default_values)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# @see PathRouter::Matcher#match
|
83
|
+
#
|
84
|
+
def match(pattern)
|
85
|
+
matcher.match(pattern)
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Associates a block with the route, and increments current order of the router.
|
90
|
+
#
|
91
|
+
def to(&block)
|
92
|
+
@block = block if block_given?
|
93
|
+
@order = @router.current_order
|
94
|
+
@router.increment_order
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Expands the path by using parameters.
|
99
|
+
# @see PathRouter::Matcher#expand
|
100
|
+
#
|
101
|
+
def path(*args)
|
102
|
+
return @path if args.empty?
|
103
|
+
params = args[0].dup
|
104
|
+
params.delete(:captures)
|
105
|
+
matcher.expand(params) if matcher.mustermann?
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Returns parameters which is created by the matcher.
|
110
|
+
#
|
111
|
+
def params_for(pattern, others = {})
|
112
|
+
matcher.params_for(pattern, others)
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Returns before_filters as an array.
|
117
|
+
#
|
118
|
+
def before_filters(&block)
|
119
|
+
@_before_filters ||= []
|
120
|
+
@_before_filters << block if block_given?
|
121
|
+
@_before_filters
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Returns after_filters as an array.
|
126
|
+
#
|
127
|
+
def after_filters(&block)
|
128
|
+
@_after_filters ||= []
|
129
|
+
@_after_filters << block if block_given?
|
130
|
+
@_after_filters
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Returns custom_conditions as an array.
|
135
|
+
#
|
136
|
+
def custom_conditions(&block)
|
137
|
+
@_custom_conditions ||= []
|
138
|
+
@_custom_conditions << block if block_given?
|
139
|
+
@_custom_conditions
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Returns block parameter length.
|
144
|
+
#
|
145
|
+
def block_parameter_length
|
146
|
+
matcher.capture_length
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
##
|
152
|
+
# Set value to accessor if option name has been defined as an accessora.
|
153
|
+
#
|
154
|
+
def merge_with_options!(options)
|
155
|
+
@options ||= {}
|
156
|
+
options.each_pair do |key, value|
|
157
|
+
accessor?(key) ? __send__("#{key}=", value) : (@options[key] = value)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Returns true if name has been defined as an accessor.
|
163
|
+
#
|
164
|
+
def accessor?(key)
|
165
|
+
respond_to?("#{key}=") && respond_to?(key)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,309 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'tennpipes-base/reloader/rack'
|
3
|
+
require 'tennpipes-base/reloader/storage'
|
4
|
+
|
5
|
+
module Tennpipes
|
6
|
+
##
|
7
|
+
# High performance source code reloader middleware
|
8
|
+
#
|
9
|
+
module Reloader
|
10
|
+
##
|
11
|
+
# This reloader is suited for use in a many environments because each file
|
12
|
+
# will only be checked once and only one system call to stat(2) is made.
|
13
|
+
#
|
14
|
+
# Please note that this will not reload files in the background, and does so
|
15
|
+
# only when explicitly invoked.
|
16
|
+
#
|
17
|
+
extend self
|
18
|
+
|
19
|
+
# The modification times for every file in a project.
|
20
|
+
MTIMES = {}
|
21
|
+
|
22
|
+
##
|
23
|
+
# Specified folders can be excluded from the code reload detection process.
|
24
|
+
# Default excluded directories at Tennpipes.root are: test, spec, features, tmp, config, db and public
|
25
|
+
#
|
26
|
+
def exclude
|
27
|
+
@_exclude ||= Set.new %w(test spec tmp features config public db).map{ |path| Tennpipes.root(path) }
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Specified constants can be excluded from the code unloading process.
|
32
|
+
#
|
33
|
+
def exclude_constants
|
34
|
+
@_exclude_constants ||= Set.new
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Specified constants can be configured to be reloaded on every request.
|
39
|
+
# Default included constants are: [none]
|
40
|
+
#
|
41
|
+
def include_constants
|
42
|
+
@_include_constants ||= Set.new
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Reload apps and files with changes detected.
|
47
|
+
#
|
48
|
+
def reload!
|
49
|
+
rotation do |file|
|
50
|
+
next unless file_changed?(file)
|
51
|
+
reload_special(file) || reload_regular(file)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Remove files and classes loaded with stat
|
57
|
+
#
|
58
|
+
def clear!
|
59
|
+
MTIMES.clear
|
60
|
+
Storage.clear!
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns true if any file changes are detected.
|
65
|
+
#
|
66
|
+
def changed?
|
67
|
+
rotation do |file|
|
68
|
+
break true if file_changed?(file)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# We lock dependencies sets to prevent reloading of protected constants
|
74
|
+
#
|
75
|
+
def lock!
|
76
|
+
klasses = ObjectSpace.classes do |klass|
|
77
|
+
klass._orig_klass_name.split('::').first
|
78
|
+
end
|
79
|
+
klasses |= Tennpipes.mounted_apps.map(&:app_class)
|
80
|
+
exclude_constants.merge(klasses)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# A safe Kernel::require which issues the necessary hooks depending on results
|
85
|
+
#
|
86
|
+
def safe_load(file, options={})
|
87
|
+
began_at = Time.now
|
88
|
+
file = figure_path(file)
|
89
|
+
return unless options[:force] || file_changed?(file)
|
90
|
+
return require(file) if feature_excluded?(file)
|
91
|
+
|
92
|
+
Storage.prepare(file) # might call #safe_load recursively
|
93
|
+
logger.devel(file_new?(file) ? :loading : :reload, began_at, file)
|
94
|
+
begin
|
95
|
+
with_silence{ require(file) }
|
96
|
+
Storage.commit(file)
|
97
|
+
update_modification_time(file)
|
98
|
+
rescue Exception => exception
|
99
|
+
unless options[:cyclic]
|
100
|
+
logger.exception exception, :short
|
101
|
+
logger.error "Failed to load #{file}; removing partially defined constants"
|
102
|
+
end
|
103
|
+
Storage.rollback(file)
|
104
|
+
raise
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Removes the specified class and constant.
|
110
|
+
#
|
111
|
+
def remove_constant(const)
|
112
|
+
return if constant_excluded?(const)
|
113
|
+
base, _, object = const.to_s.rpartition('::')
|
114
|
+
base = base.empty? ? Object : base.constantize
|
115
|
+
base.send :remove_const, object
|
116
|
+
logger.devel "Removed constant #{const} from #{base}"
|
117
|
+
rescue NameError
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Remove a feature from $LOADED_FEATURES so it can be required again.
|
122
|
+
#
|
123
|
+
def remove_feature(file)
|
124
|
+
$LOADED_FEATURES.delete(file) unless feature_excluded?(file)
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Returns the list of special tracked files for Reloader.
|
129
|
+
#
|
130
|
+
def special_files
|
131
|
+
@special_files ||= Set.new
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Sets the list of special tracked files for Reloader.
|
136
|
+
#
|
137
|
+
def special_files=(files)
|
138
|
+
@special_files = Set.new(files)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
##
|
144
|
+
# Returns absolute path of the file.
|
145
|
+
#
|
146
|
+
def figure_path(file)
|
147
|
+
return file if Pathname.new(file).absolute?
|
148
|
+
$LOAD_PATH.each do |path|
|
149
|
+
found = File.join(path, file)
|
150
|
+
return File.expand_path(found) if File.file?(found)
|
151
|
+
end
|
152
|
+
file
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Reloads the file if it's special. For now it's only I18n locale files.
|
157
|
+
#
|
158
|
+
def reload_special(file)
|
159
|
+
return unless special_files.any?{ |special_file| File.identical?(special_file, file) }
|
160
|
+
if defined?(I18n)
|
161
|
+
began_at = Time.now
|
162
|
+
I18n.reload!
|
163
|
+
update_modification_time(file)
|
164
|
+
logger.devel :reload, began_at, file
|
165
|
+
end
|
166
|
+
true
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Reloads ruby file and applications dependent on it.
|
171
|
+
#
|
172
|
+
def reload_regular(file)
|
173
|
+
apps = mounted_apps_of(file)
|
174
|
+
if apps.empty?
|
175
|
+
reloadable_apps.each do |app|
|
176
|
+
app.app_obj.reload! if app.app_obj.dependencies.include?(file)
|
177
|
+
end
|
178
|
+
safe_load(file)
|
179
|
+
else
|
180
|
+
apps.each { |app| app.app_obj.reload! }
|
181
|
+
update_modification_time(file)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
###
|
186
|
+
# Macro for mtime update.
|
187
|
+
#
|
188
|
+
def update_modification_time(file)
|
189
|
+
MTIMES[file] = File.mtime(file)
|
190
|
+
end
|
191
|
+
|
192
|
+
###
|
193
|
+
# Returns true if the file is new or it's modification time changed.
|
194
|
+
#
|
195
|
+
def file_changed?(file)
|
196
|
+
file_new?(file) || File.mtime(file) > MTIMES[file]
|
197
|
+
end
|
198
|
+
|
199
|
+
###
|
200
|
+
# Returns true if the file is new.
|
201
|
+
#
|
202
|
+
def file_new?(file)
|
203
|
+
MTIMES[file].nil?
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Searches Ruby files in your +Tennpipes.load_paths+ , Tennpipes::Application.load_paths
|
208
|
+
# and monitors them for any changes.
|
209
|
+
#
|
210
|
+
def rotation
|
211
|
+
files_for_rotation.each do |file|
|
212
|
+
file = File.expand_path(file)
|
213
|
+
next if Reloader.exclude.any? { |base| file.start_with?(base) } || !File.file?(file)
|
214
|
+
yield file
|
215
|
+
end
|
216
|
+
nil
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# Creates an array of paths for use in #rotation.
|
221
|
+
#
|
222
|
+
def files_for_rotation
|
223
|
+
files = Set.new
|
224
|
+
files += Dir.glob("#{Tennpipes.root}/{lib,models,shared}/**/*.rb")
|
225
|
+
reloadable_apps.each do |app|
|
226
|
+
files << app.app_file
|
227
|
+
files += Dir.glob(app.app_obj.prerequisites)
|
228
|
+
files += app.app_obj.dependencies
|
229
|
+
end
|
230
|
+
files + special_files
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# Tells if a feature should be excluded from Reloader tracking.
|
235
|
+
#
|
236
|
+
def feature_excluded?(file)
|
237
|
+
!file.start_with?(Tennpipes.root) || exclude.any?{ |excluded_path| file.start_with?(excluded_path) }
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Tells if a constant should be excluded from Reloader routines.
|
242
|
+
#
|
243
|
+
def constant_excluded?(const)
|
244
|
+
external_constant?(const) || (exclude_constants - include_constants).any?{ |excluded_constant| const._orig_klass_name.start_with?(excluded_constant) }
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# Tells if a constant is defined only outside of Tennpipes project path.
|
249
|
+
# If a constant has any methods defined inside of the project path it's
|
250
|
+
# considered internal and will be included in further testing.
|
251
|
+
#
|
252
|
+
def external_constant?(const)
|
253
|
+
sources = object_sources(const)
|
254
|
+
begin
|
255
|
+
if sample = ObjectSpace.each_object(const).first
|
256
|
+
sources += object_sources(sample)
|
257
|
+
end
|
258
|
+
rescue RuntimeError => error # JRuby 1.7.12 fails to ObjectSpace.each_object
|
259
|
+
raise unless RUBY_PLATFORM =='java' && error.message.start_with?("ObjectSpace is disabled")
|
260
|
+
end
|
261
|
+
!sources.any?{ |source| source.start_with?(Tennpipes.root) }
|
262
|
+
end
|
263
|
+
|
264
|
+
##
|
265
|
+
# Gets all the sources in which target's class or instance methods are defined.
|
266
|
+
#
|
267
|
+
# Note: Method#source_location is for Ruby 1.9.3+ only.
|
268
|
+
#
|
269
|
+
def object_sources(target)
|
270
|
+
sources = Set.new
|
271
|
+
target.methods.each do |method_name|
|
272
|
+
next unless method_name.kind_of?(Symbol)
|
273
|
+
method_object = target.method(method_name)
|
274
|
+
if method_object.owner == (target.class == Class ? target.singleton_class : target.class)
|
275
|
+
sources << method_object.source_location.first
|
276
|
+
end
|
277
|
+
end
|
278
|
+
sources
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Return the mounted_apps providing the app location.
|
283
|
+
# Can be an array because in one app.rb we can define multiple Tennpipes::Application.
|
284
|
+
#
|
285
|
+
def mounted_apps_of(file)
|
286
|
+
Tennpipes.mounted_apps.select { |app| File.identical?(file, app.app_file) }
|
287
|
+
end
|
288
|
+
|
289
|
+
##
|
290
|
+
# Return the apps that allow reloading.
|
291
|
+
#
|
292
|
+
def reloadable_apps
|
293
|
+
Tennpipes.mounted_apps.select do |app|
|
294
|
+
next unless app.app_file.start_with?(Tennpipes.root)
|
295
|
+
app.app_obj.respond_to?(:reload) && app.app_obj.reload?
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Disables output, yields block, switches output back.
|
301
|
+
#
|
302
|
+
def with_silence
|
303
|
+
verbosity_level, $-v = $-v, nil
|
304
|
+
yield
|
305
|
+
ensure
|
306
|
+
$-v = verbosity_level
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|