chef-apply 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +423 -0
- data/LICENSE +201 -0
- data/README.md +41 -0
- data/Rakefile +32 -0
- data/bin/chef-run +23 -0
- data/chef-apply.gemspec +67 -0
- data/i18n/en.yml +513 -0
- data/lib/chef_apply.rb +20 -0
- data/lib/chef_apply/action/base.rb +158 -0
- data/lib/chef_apply/action/converge_target.rb +173 -0
- data/lib/chef_apply/action/install_chef.rb +30 -0
- data/lib/chef_apply/action/install_chef/base.rb +137 -0
- data/lib/chef_apply/action/install_chef/linux.rb +38 -0
- data/lib/chef_apply/action/install_chef/windows.rb +54 -0
- data/lib/chef_apply/action/reporter.rb +39 -0
- data/lib/chef_apply/cli.rb +470 -0
- data/lib/chef_apply/cli_options.rb +145 -0
- data/lib/chef_apply/config.rb +150 -0
- data/lib/chef_apply/error.rb +108 -0
- data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
- data/lib/chef_apply/file_fetcher.rb +70 -0
- data/lib/chef_apply/log.rb +42 -0
- data/lib/chef_apply/recipe_lookup.rb +117 -0
- data/lib/chef_apply/startup.rb +162 -0
- data/lib/chef_apply/status_reporter.rb +42 -0
- data/lib/chef_apply/target_host.rb +233 -0
- data/lib/chef_apply/target_resolver.rb +202 -0
- data/lib/chef_apply/telemeter.rb +162 -0
- data/lib/chef_apply/telemeter/patch.rb +32 -0
- data/lib/chef_apply/telemeter/sender.rb +121 -0
- data/lib/chef_apply/temp_cookbook.rb +159 -0
- data/lib/chef_apply/text.rb +77 -0
- data/lib/chef_apply/ui/error_printer.rb +261 -0
- data/lib/chef_apply/ui/plain_text_element.rb +75 -0
- data/lib/chef_apply/ui/terminal.rb +94 -0
- data/lib/chef_apply/version.rb +20 -0
- metadata +376 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2018 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "tmpdir"
|
19
|
+
require "fileutils"
|
20
|
+
require "chef_apply/log"
|
21
|
+
require "chef_apply/error"
|
22
|
+
|
23
|
+
module ChefApply
|
24
|
+
# This class knows how to create a local cookbook in a temp file, populate
|
25
|
+
# it with various recipes, attributes, config, etc. and delete it when the
|
26
|
+
# cookbook is no longer necessary
|
27
|
+
class TempCookbook
|
28
|
+
attr_reader :path
|
29
|
+
|
30
|
+
# We expect name to come in as a list of strings - resource/resource_name
|
31
|
+
# or cookbook/recipe combination
|
32
|
+
def initialize
|
33
|
+
@path = Dir.mktmpdir("cw")
|
34
|
+
end
|
35
|
+
|
36
|
+
def from_existing_recipe(existing_recipe_path)
|
37
|
+
ext_name = File.extname(existing_recipe_path)
|
38
|
+
raise UnsupportedExtension.new(ext_name) unless ext_name == ".rb"
|
39
|
+
cb = cookbook_for_recipe(existing_recipe_path)
|
40
|
+
if cb
|
41
|
+
# Full existing cookbook - only needs policyfile
|
42
|
+
ChefApply::Log.debug("Found full cookbook at path '#{cb[:path]}' and using recipe '#{cb[:recipe_name]}'")
|
43
|
+
name = cb[:name]
|
44
|
+
recipe_name = cb[:recipe_name]
|
45
|
+
FileUtils.cp_r(cb[:path], path)
|
46
|
+
# cp_r copies the whole existing cookbook into the tempdir so need to reset our path
|
47
|
+
@path = File.join(path, File.basename(cb[:path]))
|
48
|
+
generate_policyfile(name, recipe_name)
|
49
|
+
else
|
50
|
+
# Cookbook from single recipe not in a cookbook. We create the full cookbook
|
51
|
+
# structure including metadata, then generate policyfile. We set the cookbook
|
52
|
+
# name to the recipe name so hopefully this gives us better reporting info
|
53
|
+
# in the future
|
54
|
+
ChefApply::Log.debug("Found single recipe at path '#{existing_recipe_path}'")
|
55
|
+
recipe = File.basename(existing_recipe_path)
|
56
|
+
recipe_name = File.basename(existing_recipe_path, ext_name)
|
57
|
+
name = "cw_recipe"
|
58
|
+
recipes_dir = generate_recipes_dir
|
59
|
+
# This has the potential to break if they specify a recipe without a .rb
|
60
|
+
# extension, but lets wait to deal with that bug until we encounter it
|
61
|
+
FileUtils.cp(existing_recipe_path, File.join(recipes_dir, recipe))
|
62
|
+
generate_metadata(name)
|
63
|
+
generate_policyfile(name, recipe_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def from_resource(resource_type, resource_name, properties)
|
68
|
+
# Generate a cookbook containing a single default recipe with the specified
|
69
|
+
# resource in it. Incloud the resource type in the cookbook name so hopefully
|
70
|
+
# this gives us better reporting info in the future.
|
71
|
+
ChefApply::Log.debug("Generating cookbook for single resource '#{resource_type}[#{resource_name}]'")
|
72
|
+
name = "cw_#{resource_type}"
|
73
|
+
recipe_name = "default"
|
74
|
+
recipes_dir = generate_recipes_dir
|
75
|
+
File.open(File.join(recipes_dir, "#{recipe_name}.rb"), "w+") do |f|
|
76
|
+
f.print(create_resource(resource_type, resource_name, properties))
|
77
|
+
end
|
78
|
+
generate_metadata(name)
|
79
|
+
generate_policyfile(name, recipe_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete
|
83
|
+
FileUtils.remove_entry path
|
84
|
+
end
|
85
|
+
|
86
|
+
def cookbook_for_recipe(existing_recipe_path)
|
87
|
+
metadata = File.expand_path(File.join(existing_recipe_path, "../../metadata.rb"))
|
88
|
+
if File.file?(metadata)
|
89
|
+
require "chef/cookbook/metadata"
|
90
|
+
m = Chef::Cookbook::Metadata.new
|
91
|
+
m.from_file(metadata)
|
92
|
+
{
|
93
|
+
name: m.name,
|
94
|
+
recipe_name: File.basename(existing_recipe_path, File.extname(existing_recipe_path)),
|
95
|
+
path: File.expand_path(File.join(metadata, "../"))
|
96
|
+
}
|
97
|
+
else
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def generate_recipes_dir
|
103
|
+
recipes_path = File.join(path, "recipes")
|
104
|
+
FileUtils.mkdir_p(recipes_path)
|
105
|
+
recipes_path
|
106
|
+
end
|
107
|
+
|
108
|
+
def generate_metadata(name)
|
109
|
+
metadata_file = File.join(path, "metadata.rb")
|
110
|
+
File.open(metadata_file, "w+") do |f|
|
111
|
+
f.print("name \"#{name}\"\n")
|
112
|
+
end
|
113
|
+
metadata_file
|
114
|
+
end
|
115
|
+
|
116
|
+
def generate_policyfile(name, recipe_name)
|
117
|
+
policy_file = File.join(path, "Policyfile.rb")
|
118
|
+
if File.exist?(policy_file)
|
119
|
+
File.open(policy_file, "a") do |f|
|
120
|
+
# We override the specified run_list with the run_list we want.
|
121
|
+
# We append and put this at the end of the file so it overrides
|
122
|
+
# any specified run_list.
|
123
|
+
f.print("\n# Overriding run_list with command line specified value\n")
|
124
|
+
f.print("run_list \"#{name}::#{recipe_name}\"\n")
|
125
|
+
end
|
126
|
+
else
|
127
|
+
File.open(policy_file, "w+") do |f|
|
128
|
+
f.print("name \"#{name}_policy\"\n")
|
129
|
+
ChefApply::Config.chef.cookbook_repo_paths.each do |p|
|
130
|
+
f.print("default_source :chef_repo, \"#{p}\"\n")
|
131
|
+
end
|
132
|
+
f.print("default_source :supermarket\n")
|
133
|
+
f.print("run_list \"#{name}::#{recipe_name}\"\n")
|
134
|
+
f.print("cookbook \"#{name}\", path: \".\"\n")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
policy_file
|
138
|
+
end
|
139
|
+
|
140
|
+
def create_resource(resource_type, resource_name, properties)
|
141
|
+
r = "#{resource_type} '#{resource_name}'"
|
142
|
+
# lets format the properties into the correct syntax Chef expects
|
143
|
+
unless properties.empty?
|
144
|
+
r += " do\n"
|
145
|
+
properties.each do |k, v|
|
146
|
+
v = "'#{v}'" if v.is_a? String
|
147
|
+
r += " #{k} #{v}\n"
|
148
|
+
end
|
149
|
+
r += "end"
|
150
|
+
end
|
151
|
+
r += "\n"
|
152
|
+
r
|
153
|
+
end
|
154
|
+
|
155
|
+
class UnsupportedExtension < ChefApply::ErrorNoLogs
|
156
|
+
def initialize(ext); super("CHEFVAL009", ext); end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2017 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "r18n-desktop"
|
19
|
+
|
20
|
+
# A very thin wrapper around R18n, the idea being that we're likely to replace r18n
|
21
|
+
# down the road and don't want to have to change all of our commands.
|
22
|
+
module ChefApply
|
23
|
+
class Text
|
24
|
+
R18n.from_env(File.join(File.dirname(__FILE__), "../..", "i18n/"))
|
25
|
+
t = R18n.get.t
|
26
|
+
t.translation_keys.each do |k|
|
27
|
+
k = k.to_sym
|
28
|
+
define_singleton_method k do |*args|
|
29
|
+
TextWrapper.new(t.send(k, *args))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Our text spinner class really doesn't like handling the TranslatedString or Untranslated classes returned
|
35
|
+
# by the R18n library. So instead we return these TextWrapper instances which have dynamically defined methods
|
36
|
+
# corresponding to the known structure of the R18n text file. Most importantly, if a user has accessed
|
37
|
+
# a leaf node in the code we return a regular String instead of the R18n classes.
|
38
|
+
class TextWrapper
|
39
|
+
def initialize(translation_tree)
|
40
|
+
@tree = translation_tree
|
41
|
+
@tree.translation_keys.each do |k|
|
42
|
+
k = k.to_sym
|
43
|
+
define_singleton_method k do |*args|
|
44
|
+
subtree = @tree.send(k, *args)
|
45
|
+
if subtree.translation_keys.empty?
|
46
|
+
# If there are no more possible children, just return the translated value
|
47
|
+
subtree.to_s
|
48
|
+
else
|
49
|
+
TextWrapper.new(subtree)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(name, *args)
|
56
|
+
raise InvalidKey.new(@tree.instance_variable_get(:@path), name)
|
57
|
+
end
|
58
|
+
|
59
|
+
class InvalidKey < RuntimeError
|
60
|
+
def initialize(path, terminus)
|
61
|
+
line = caller(3, 1).first # 1 - TextWrapper.method_missing
|
62
|
+
# 2 - TextWrapper.initialize
|
63
|
+
# 3 - actual caller
|
64
|
+
if line =~ /.*\/lib\/(.*\.rb):(\d+)/
|
65
|
+
line = "File: #{$1} Line: #{$2}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Calling back into Text here seems icky, this is an error
|
69
|
+
# that only engineering should see.
|
70
|
+
message = "i18n key #{path}.#{terminus} does not exist.\n"
|
71
|
+
message << "Referenced from #{line}"
|
72
|
+
super(message)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2017 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require "train/errors"
|
19
|
+
require "pastel"
|
20
|
+
require "chef_apply/error"
|
21
|
+
require "chef_apply/config"
|
22
|
+
require "chef_apply/text"
|
23
|
+
require "chef_apply/ui/terminal"
|
24
|
+
|
25
|
+
module ChefApply::UI
|
26
|
+
class ErrorPrinter
|
27
|
+
attr_reader :id, :pastel, :show_log, :show_stack, :exception, :target_host
|
28
|
+
# TODO define 't' as a method is a temporary workaround
|
29
|
+
# to ensure that text key lookups are testable.
|
30
|
+
def t
|
31
|
+
ChefApply::Text.errors
|
32
|
+
end
|
33
|
+
|
34
|
+
DEFAULT_ERROR_NO = "CHEFINT001"
|
35
|
+
|
36
|
+
def self.show_error(e)
|
37
|
+
# Name is misleading - it's unwrapping but also doing further
|
38
|
+
# error resolution for common errors:
|
39
|
+
unwrapped = ChefApply::StandardErrorResolver.unwrap_exception(e)
|
40
|
+
if unwrapped.class == ChefApply::MultiJobFailure
|
41
|
+
capture_multiple_failures(unwrapped)
|
42
|
+
end
|
43
|
+
formatter = ErrorPrinter.new(e, unwrapped)
|
44
|
+
Terminal.output(formatter.format_error)
|
45
|
+
rescue => e
|
46
|
+
dump_unexpected_error(e)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.capture_multiple_failures(e)
|
50
|
+
out_file = ChefApply::Config.error_output_path
|
51
|
+
e.params << out_file # Tell the operator where to find this info
|
52
|
+
File.open(out_file, "w") do |out|
|
53
|
+
e.jobs.each do |j|
|
54
|
+
wrapped = ChefApply::StandardErrorResolver.wrap_exception(j.exception, j.target_host)
|
55
|
+
ep = ErrorPrinter.new(wrapped)
|
56
|
+
msg = ep.format_body().tr("\n", " ").gsub(/ {2,}/, " ").chomp.strip
|
57
|
+
out.write("Host: #{j.target_host.hostname} ")
|
58
|
+
if ep.exception.respond_to? :id
|
59
|
+
out.write("Error: #{ep.exception.id}: ")
|
60
|
+
else
|
61
|
+
out.write(": ")
|
62
|
+
end
|
63
|
+
out.write("#{msg}\n")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.write_backtrace(e, args)
|
69
|
+
formatter = ErrorPrinter.new(e)
|
70
|
+
out = StringIO.new
|
71
|
+
formatter.add_backtrace_header(out, args)
|
72
|
+
formatter.add_formatted_backtrace(out)
|
73
|
+
formatter.save_backtrace(out)
|
74
|
+
rescue => ex
|
75
|
+
dump_unexpected_error(ex)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Use this to dump an an exception to output. useful
|
79
|
+
# if an error occurs in the error handling itself.
|
80
|
+
def self.dump_unexpected_error(e)
|
81
|
+
Terminal.output "INTERNAL ERROR"
|
82
|
+
Terminal.output "-=" * 30
|
83
|
+
Terminal.output "Message:"
|
84
|
+
Terminal.output e.message if e.respond_to?(:message)
|
85
|
+
Terminal.output "Backtrace:"
|
86
|
+
Terminal.output e.backtrace if e.respond_to?(:backtrace)
|
87
|
+
Terminal.output "=-" * 30
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(wrapper, unwrapped = nil, target_host = nil)
|
91
|
+
@exception = unwrapped || wrapper.contained_exception
|
92
|
+
@target_host = wrapper.target_host || target_host
|
93
|
+
@pastel = Pastel.new
|
94
|
+
@show_log = exception.respond_to?(:show_log) ? exception.show_log : true
|
95
|
+
@show_stack = exception.respond_to?(:show_stack) ? exception.show_stack : true
|
96
|
+
@content = StringIO.new
|
97
|
+
@command = exception.respond_to?(:command) ? exception.command : nil
|
98
|
+
@id = DEFAULT_ERROR_NO
|
99
|
+
if exception.respond_to?(:id) && exception.id =~ /CHEF.*/
|
100
|
+
@id = exception.id
|
101
|
+
end
|
102
|
+
if exception.respond_to?(:decorate)
|
103
|
+
@decorate = exception.decorate
|
104
|
+
else
|
105
|
+
@decorate = true
|
106
|
+
end
|
107
|
+
rescue => e
|
108
|
+
ErrorPrinter.dump_unexpected_error(e)
|
109
|
+
exit! 128
|
110
|
+
end
|
111
|
+
|
112
|
+
def format_error
|
113
|
+
if @decorate
|
114
|
+
format_decorated
|
115
|
+
else
|
116
|
+
format_undecorated
|
117
|
+
end
|
118
|
+
@content.string
|
119
|
+
end
|
120
|
+
|
121
|
+
def format_undecorated
|
122
|
+
@content << "\n"
|
123
|
+
@content << format_body()
|
124
|
+
if @command
|
125
|
+
@content << "\n"
|
126
|
+
@content << @command.usage
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def format_decorated
|
131
|
+
@content << "\n"
|
132
|
+
@content << format_header()
|
133
|
+
@content << "\n\n"
|
134
|
+
@content << format_body()
|
135
|
+
@content << "\n"
|
136
|
+
@content << format_footer()
|
137
|
+
@content << "\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
def format_header
|
141
|
+
pastel.decorate(@id, :bold)
|
142
|
+
end
|
143
|
+
|
144
|
+
def format_body
|
145
|
+
if exception.kind_of? ChefApply::Error
|
146
|
+
format_workstation_exception
|
147
|
+
elsif exception.kind_of? Train::Error
|
148
|
+
format_train_exception
|
149
|
+
else
|
150
|
+
format_other_exception
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def format_footer
|
155
|
+
if show_log
|
156
|
+
if show_stack
|
157
|
+
t.footer.both(ChefApply::Config.log.location,
|
158
|
+
ChefApply::Config.stack_trace_path)
|
159
|
+
else
|
160
|
+
t.footer.log_only(ChefApply::Config.log.location)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
if show_stack
|
164
|
+
t.footer.stack_only
|
165
|
+
else
|
166
|
+
t.footer.neither
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def add_backtrace_header(out, args)
|
172
|
+
out.write("\n#{"-" * 80}\n")
|
173
|
+
out.print("#{Time.now}: Error encountered while running the following:\n")
|
174
|
+
out.print(" #{args.join(' ')}\n")
|
175
|
+
out.print("Backtrace:\n")
|
176
|
+
end
|
177
|
+
|
178
|
+
def save_backtrace(output)
|
179
|
+
File.open(ChefApply::Config.stack_trace_path, "ab+") do |f|
|
180
|
+
f.write(output.string)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.error_summary(e)
|
185
|
+
if e.kind_of? ChefApply::Error
|
186
|
+
# By convention, all of our defined messages have a short summary on the first line.
|
187
|
+
ChefApply::Text.errors.send(e.id, *e.params).split("\n").first
|
188
|
+
elsif e.kind_of? String
|
189
|
+
e
|
190
|
+
else
|
191
|
+
if e.respond_to? :message
|
192
|
+
e.message
|
193
|
+
else
|
194
|
+
ChefApply::Text.errors.UNKNOWN
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def format_workstation_exception
|
200
|
+
params = exception.params
|
201
|
+
t.send(@id, *params)
|
202
|
+
end
|
203
|
+
|
204
|
+
def format_train_exception
|
205
|
+
backend, host = formatted_host()
|
206
|
+
if host.nil?
|
207
|
+
t.CHEFTRN002(exception.message)
|
208
|
+
else
|
209
|
+
t.CHEFTRN001(backend, host, exception.message)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def format_other_exception
|
214
|
+
t.send(DEFAULT_ERROR_NO, exception.message)
|
215
|
+
end
|
216
|
+
|
217
|
+
def formatted_host
|
218
|
+
return nil if target_host.nil?
|
219
|
+
cfg = target_host.config
|
220
|
+
port = cfg[:port].nil? ? "" : ":#{cfg[:port]}"
|
221
|
+
user = cfg[:user].nil? ? "" : "#{cfg[:user]}@"
|
222
|
+
"#{user}#{target_host.hostname}#{port}"
|
223
|
+
end
|
224
|
+
|
225
|
+
# mostly copied from
|
226
|
+
# https://gist.github.com/stanio/13d74294ca1868fed7fb
|
227
|
+
def add_formatted_backtrace(out)
|
228
|
+
_format_single(out, exception)
|
229
|
+
current_backtrace = exception.backtrace
|
230
|
+
cause = exception.cause
|
231
|
+
until cause.nil?
|
232
|
+
cause_trace = _unique_trace(cause.backtrace.to_a, current_backtrace)
|
233
|
+
out.print "Caused by: "
|
234
|
+
_format_single(out, cause, cause_trace)
|
235
|
+
backtrace_length = cause.backtrace.length
|
236
|
+
if backtrace_length > cause_trace.length
|
237
|
+
out.print "\t... #{backtrace_length - cause_trace.length} more"
|
238
|
+
end
|
239
|
+
out.print "\n"
|
240
|
+
current_backtrace = cause.backtrace
|
241
|
+
cause = cause.cause
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def _format_single(out, exception, backtrace = nil)
|
246
|
+
out.puts "#{exception.class}: #{exception.message}"
|
247
|
+
backtrace ||= exception.backtrace.to_a
|
248
|
+
backtrace.each { |trace| out.puts "\t#{trace}" }
|
249
|
+
end
|
250
|
+
|
251
|
+
def _unique_trace(backtrace1, backtrace2)
|
252
|
+
i = 1
|
253
|
+
while i <= backtrace1.size && i <= backtrace2.size
|
254
|
+
break if backtrace1[-i] != backtrace2[-i]
|
255
|
+
i += 1
|
256
|
+
end
|
257
|
+
backtrace1[0..-i]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|