nanoc-cli 4.11.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc::CLI
4
+ # Catches errors and prints nice diagnostic messages, then exits.
5
+ #
6
+ # @api private
7
+ class ErrorHandler
8
+ # Enables error handling in the given block.
9
+ #
10
+ # @return [void]
11
+ def self.handle_while(exit_on_error: true)
12
+ if @disabled
13
+ yield
14
+ else
15
+ new.handle_while(exit_on_error: exit_on_error) { yield }
16
+ end
17
+ end
18
+
19
+ # Disables error handling. This is used by the test cases to prevent error
20
+ # from being handled by the CLI while tests are running.
21
+ def self.disable
22
+ @disabled = true
23
+ end
24
+
25
+ # Re-enables error handling after it was disabled. This is used by the test
26
+ # cases to prevent error from being handled by the CLI while tests are
27
+ # running.
28
+ def self.enable
29
+ @disabled = false
30
+ end
31
+
32
+ # Enables error handling in the given block. This method should not be
33
+ # called directly; use {Nanoc::CLI::ErrorHandler.handle_while} instead.
34
+ #
35
+ # @return [void]
36
+ def handle_while(exit_on_error:)
37
+ # Set exit handler
38
+ %w[INT TERM].each do |signal|
39
+ Signal.trap(signal) do
40
+ puts
41
+ exit!(0)
42
+ end
43
+ end
44
+
45
+ # Set stack trace dump handler
46
+ if !defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby'
47
+ begin
48
+ Signal.trap('USR1') do
49
+ puts 'Caught USR1; dumping a stack trace'
50
+ puts caller.map { |i| " #{i}" }.join("\n")
51
+ end
52
+ rescue ArgumentError
53
+ end
54
+ end
55
+
56
+ # Run
57
+ yield
58
+ rescue Interrupt
59
+ exit(1)
60
+ rescue StandardError, ScriptError => e
61
+ handle_error(e, exit_on_error: exit_on_error)
62
+ end
63
+
64
+ def handle_error(error, exit_on_error:)
65
+ if trivial?(error)
66
+ $stderr.puts
67
+ $stderr.puts "Error: #{error.message}"
68
+ resolution = resolution_for(error)
69
+ if resolution
70
+ $stderr.puts
71
+ $stderr.puts resolution
72
+ end
73
+ else
74
+ print_error(error)
75
+ end
76
+ exit(1) if exit_on_error
77
+ end
78
+
79
+ # Prints the given error to stderr. Includes message, possible resolution
80
+ # (see {#resolution_for}), compilation stack, backtrace, etc.
81
+ #
82
+ # @param [Error] error The error that should be described
83
+ #
84
+ # @return [void]
85
+ def self.print_error(error)
86
+ new.print_error(error)
87
+ end
88
+
89
+ # Prints the given error to stderr. Includes message, possible resolution
90
+ # (see {#resolution_for}), compilation stack, backtrace, etc.
91
+ #
92
+ # @param [Error] error The error that should be described
93
+ #
94
+ # @return [void]
95
+ def print_error(error)
96
+ write_compact_error(error, $stderr)
97
+
98
+ File.open('crash.log', 'w') do |io|
99
+ cio = Nanoc::CLI.wrap_in_cleaning_stream(io)
100
+ cio.add_stream_cleaner(::Nanoc::CLI::StreamCleaners::ANSIColors)
101
+ write_verbose_error(error, cio)
102
+ end
103
+ end
104
+
105
+ # Writes a compact representation of the error, suitable for a terminal, on
106
+ # the given stream (probably stderr).
107
+ #
108
+ # @param [Error] error The error that should be described
109
+ #
110
+ # @param [IO] stream The stream to write the description too
111
+ #
112
+ # @return [void]
113
+ def write_compact_error(error, stream)
114
+ stream.puts
115
+ stream.puts 'Captain! We’ve been hit!'
116
+
117
+ if forwards_stack_trace?
118
+ write_stack_trace(stream, error)
119
+ write_error_message(stream, error)
120
+ write_item_rep(stream, error)
121
+ else
122
+ write_error_message(stream, error)
123
+ write_item_rep(stream, error)
124
+ write_stack_trace(stream, error)
125
+ end
126
+
127
+ stream.puts
128
+ stream.puts 'A detailed crash log has been written to ./crash.log.'
129
+ end
130
+
131
+ # Writes a verbose representation of the error on the given stream.
132
+ #
133
+ # @param [Error] error The error that should be described
134
+ #
135
+ # @param [IO] stream The stream to write the description too
136
+ #
137
+ # @return [void]
138
+ def write_verbose_error(error, stream)
139
+ stream.puts "Crashlog created at #{Time.now}"
140
+
141
+ write_error_message(stream, error, verbose: true)
142
+ write_item_rep(stream, error, verbose: true)
143
+ write_stack_trace(stream, error, verbose: true)
144
+ write_version_information(stream, verbose: true)
145
+ write_system_information(stream, verbose: true)
146
+ write_installed_gems(stream, verbose: true)
147
+ write_gemfile_lock(stream, verbose: true)
148
+ write_load_paths(stream, verbose: true)
149
+ end
150
+
151
+ # @api private
152
+ def forwards_stack_trace?
153
+ ruby_version.start_with?('2.5')
154
+ end
155
+
156
+ # @api private
157
+ def trivial?(error)
158
+ case error
159
+ when Nanoc::Core::TrivialError, Errno::EADDRINUSE
160
+ true
161
+ when LoadError
162
+ GEM_NAMES.key?(gem_name_from_load_error(error))
163
+ else
164
+ false
165
+ end
166
+ end
167
+
168
+ protected
169
+
170
+ # @return [Hash<String, Array>] A hash containing the gem names as keys and gem versions as value
171
+ def gems_and_versions
172
+ gems = {}
173
+ Gem::Specification.find_all.sort_by { |s| [s.name, s.version] }.each do |spec|
174
+ gems[spec.name] ||= []
175
+ gems[spec.name] << spec.version.to_s
176
+ end
177
+ gems
178
+ end
179
+
180
+ # A hash that contains the name of the gem for a given required file. If a
181
+ # `#require` fails, the gem name is looked up in this hash.
182
+ GEM_NAMES = {
183
+ 'adsf' => 'adsf',
184
+ 'asciidoctor' => 'asciidoctor',
185
+ 'bluecloth' => 'bluecloth',
186
+ 'builder' => 'builder',
187
+ 'coderay' => 'coderay',
188
+ 'coffee-script' => 'coffee-script',
189
+ 'cri' => 'cri',
190
+ 'erubi' => 'erubi',
191
+ 'erubis' => 'erubis',
192
+ 'escape' => 'escape',
193
+ 'fog' => 'fog',
194
+ 'haml' => 'haml',
195
+ 'json' => 'json',
196
+ 'kramdown' => 'kramdown',
197
+ 'less' => 'less',
198
+ 'listen' => 'listen',
199
+ 'markaby' => 'markaby',
200
+ 'maruku' => 'maruku',
201
+ 'mime/types' => 'mime-types',
202
+ 'mustache' => 'mustache',
203
+ 'nanoc/live' => 'nanoc-live',
204
+ 'nokogiri' => 'nokogiri',
205
+ 'nokogumbo' => 'nokogumbo',
206
+ 'pandoc-ruby' => 'pandoc-ruby',
207
+ 'pry' => 'pry',
208
+ 'rack' => 'rack',
209
+ 'rack/cache' => 'rack-cache',
210
+ 'rainpress' => 'rainpress',
211
+ 'rdiscount' => 'rdiscount',
212
+ 'redcarpet' => 'redcarpet',
213
+ 'redcloth' => 'RedCloth',
214
+ 'ruby-handlebars' => 'hbs',
215
+ 'rubypants' => 'rubypants',
216
+ 'sass' => 'sass',
217
+ 'slim' => 'slim',
218
+ 'typogruby' => 'typogruby',
219
+ 'uglifier' => 'uglifier',
220
+ 'w3c_validators' => 'w3c_validators',
221
+ 'yuicompressor' => 'yuicompressor',
222
+ }.freeze
223
+
224
+ # Attempts to find a resolution for the given error, or nil if no
225
+ # resolution can be automatically obtained.
226
+ #
227
+ # @param [Error] error The error to find a resolution for
228
+ #
229
+ # @return [String] The resolution for the given error
230
+ def resolution_for(error)
231
+ error = unwrap_error(error)
232
+
233
+ case error
234
+ when LoadError
235
+ gem_name = gem_name_from_load_error(error)
236
+
237
+ if gem_name
238
+ if using_bundler?
239
+ <<~RES
240
+ 1. Add `gem '#{gem_name}'` to your Gemfile
241
+ 2. Run `bundle install`
242
+ 3. Re-run this command
243
+ RES
244
+ else
245
+ "Install the '#{gem_name}' gem using `gem install #{gem_name}`."
246
+ end
247
+ end
248
+ when RuntimeError
249
+ if /^can't modify frozen/.match?(error.message)
250
+ 'You attempted to modify immutable data. Some data cannot ' \
251
+ 'be modified once compilation has started. Such data includes ' \
252
+ 'content and attributes of items and layouts, and filter arguments.'
253
+ end
254
+ when Errno::EADDRINUSE
255
+ 'There already is a server running. Either shut down that one, or ' \
256
+ 'specify a different port to run this server on.'
257
+ end
258
+ end
259
+
260
+ def gem_name_from_load_error(error)
261
+ matches = error.message.match(/(no such file to load|cannot load such file) -- ([^\s]+)/)
262
+ return nil if matches.nil?
263
+
264
+ GEM_NAMES[matches[2]]
265
+ end
266
+
267
+ def using_bundler?
268
+ defined?(Bundler) && Bundler::SharedHelpers.in_bundle?
269
+ end
270
+
271
+ def ruby_version
272
+ RUBY_VERSION
273
+ end
274
+
275
+ def write_section_header(stream, title, verbose: false)
276
+ stream.puts
277
+
278
+ if verbose
279
+ stream.puts '===== ' + title.upcase + ':'
280
+ stream.puts
281
+ end
282
+ end
283
+
284
+ def write_error_message(stream, error, verbose: false)
285
+ write_section_header(stream, 'Message', verbose: verbose)
286
+
287
+ error = unwrap_error(error)
288
+
289
+ message = "#{error.class}: #{message_for_error(error)}"
290
+ unless verbose
291
+ message = "\e[1m\e[31m" + message + "\e[0m"
292
+ end
293
+ stream.puts message
294
+ resolution = resolution_for(error)
295
+ stream.puts resolution.to_s if resolution
296
+ end
297
+
298
+ def message_for_error(error)
299
+ case error
300
+ when JsonSchema::AggregateError
301
+ "\n" + error.errors.map { |e| " * #{e.pointer}: #{e.message}" }.join("\n")
302
+ else
303
+ error.message
304
+ end
305
+ end
306
+
307
+ def write_item_rep(stream, error, verbose: false)
308
+ return unless error.is_a?(Nanoc::Core::Errors::CompilationError)
309
+
310
+ write_section_header(stream, 'Item being compiled', verbose: verbose)
311
+
312
+ item_rep = error.item_rep
313
+ stream.puts "Current item: #{item_rep.item.identifier} (#{item_rep.name.inspect} representation)"
314
+ end
315
+
316
+ def write_stack_trace(stream, error, verbose: false)
317
+ write_section_header(stream, 'Stack trace', verbose: verbose)
318
+
319
+ writer = Nanoc::CLI::StackTraceWriter.new(stream, forwards: forwards_stack_trace?)
320
+ writer.write(unwrap_error(error), verbose: verbose)
321
+ end
322
+
323
+ def write_version_information(stream, verbose: false)
324
+ write_section_header(stream, 'Version information', verbose: verbose)
325
+ stream.puts Nanoc::Core.version_information
326
+ end
327
+
328
+ def write_system_information(stream, verbose: false)
329
+ uname = `uname -a`
330
+ write_section_header(stream, 'System information', verbose: verbose)
331
+ stream.puts uname
332
+ rescue Errno::ENOENT
333
+ end
334
+
335
+ def write_installed_gems(stream, verbose: false)
336
+ write_section_header(stream, 'Installed gems', verbose: verbose)
337
+ gems_and_versions.each do |g|
338
+ stream.puts " #{g.first} #{g.last.join(', ')}"
339
+ end
340
+ end
341
+
342
+ def write_gemfile_lock(stream, verbose: false)
343
+ if File.exist?('Gemfile.lock')
344
+ write_section_header(stream, 'Gemfile.lock', verbose: verbose)
345
+ stream.puts File.read('Gemfile.lock')
346
+ end
347
+ end
348
+
349
+ def write_load_paths(stream, verbose: false)
350
+ write_section_header(stream, 'Load paths', verbose: verbose)
351
+ $LOAD_PATH.each_with_index do |i, index|
352
+ stream.puts " #{index}. #{i}"
353
+ end
354
+ end
355
+
356
+ def unwrap_error(e)
357
+ case e
358
+ when Nanoc::Core::Errors::CompilationError
359
+ e.unwrap
360
+ else
361
+ e
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module CLI
5
+ # Nanoc::CLI::Logger is a singleton class responsible for generating
6
+ # feedback in the terminal.
7
+ #
8
+ # @api private
9
+ class Logger
10
+ # Maps actions (`:create`, `:update`, `:identical`, `:cached`, `:skip` and `:delete`)
11
+ # onto their ANSI color codes.
12
+ ACTION_COLORS = {
13
+ create: "\e[32m", # green
14
+ update: "\e[33m", # yellow
15
+ identical: '', # (nothing)
16
+ cached: '', # (nothing)
17
+ skip: '', # (nothing)
18
+ delete: "\e[31m", # red
19
+ }.freeze
20
+
21
+ include Singleton
22
+
23
+ # Returns the log level, which can be :high, :low or :off (which will log
24
+ # all messages, only high-priority messages, or no messages at all,
25
+ # respectively).
26
+ #
27
+ # @return [Symbol] The log level
28
+ attr_accessor :level
29
+
30
+ def initialize
31
+ @level = :high
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ # Logs a file-related action.
36
+ #
37
+ # @param [:high, :low] level The importance of this action
38
+ #
39
+ # @param [:create, :update, :identical, :cached, :skip, :delete] action The kind of file action
40
+ #
41
+ # @param [String] name The name of the file the action was performed on
42
+ #
43
+ # @return [void]
44
+ def file(level, action, name, duration = nil)
45
+ log(
46
+ level,
47
+ format(
48
+ '%s%12s%s %s%s',
49
+ ACTION_COLORS[action.to_sym],
50
+ action,
51
+ "\e[0m",
52
+ duration.nil? ? '' : format('[%2.2fs] ', duration),
53
+ name,
54
+ ),
55
+ )
56
+ end
57
+
58
+ # Logs a message.
59
+ #
60
+ # @param [:high, :low] level The importance of this message
61
+ #
62
+ # @param [String] message The message to be logged
63
+ #
64
+ # @param [#puts] io The stream to which the message should be written
65
+ #
66
+ # @return [void]
67
+ def log(level, message, io = $stdout)
68
+ return if @level == :off
69
+ return if @level != :low && @level != level
70
+
71
+ @mutex.synchronize do
72
+ io.puts(message)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nanoc
4
+ module CLI
5
+ class StackTraceWriter
6
+ def initialize(stream, forwards:)
7
+ @stream = stream
8
+ @forwards = forwards
9
+ end
10
+
11
+ def write(error, verbose:)
12
+ if @forwards
13
+ write_forwards(error, verbose: verbose)
14
+ else
15
+ write_backwards(error, verbose: verbose)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def write_backwards(error, verbose:)
22
+ count = verbose ? -1 : 10
23
+
24
+ error.backtrace[0...count].each_with_index do |item, index|
25
+ @stream.puts " #{index}. #{item}"
26
+ end
27
+
28
+ if !verbose && error.backtrace.size > count
29
+ @stream.puts " ... #{error.backtrace.size - count} lines omitted (see crash.log for details)"
30
+ end
31
+ end
32
+
33
+ def write_forwards(error, verbose:)
34
+ count = 10
35
+ backtrace = verbose ? error.backtrace : error.backtrace.take(count)
36
+
37
+ if !verbose && error.backtrace.size > count
38
+ @stream.puts " ... #{error.backtrace.size - count} lines omitted (see crash.log for details)"
39
+ end
40
+
41
+ backtrace.each_with_index.to_a.reverse_each do |(item, index)|
42
+ if index.zero?
43
+ @stream.puts " #{item}"
44
+ else
45
+ @stream.puts " #{index}. from #{item}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end