require "delegate" require "io/console" require "thread" require "log4r" require "vagrant/util/platform" require "vagrant/util/safe_puts" module Vagrant module UI # Vagrant UIs handle communication with the outside world (typically # through a shell). They must respond to the following methods: # # * `info` # * `warn` # * `error` # * `success` class Interface # Opts can be used to set some options. These options are implementation # specific. See the implementation for more docs. attr_accessor :opts # @return [IO] UI input. Defaults to `$stdin`. attr_accessor :stdin # @return [IO] UI output. Defaults to `$stdout`. attr_accessor :stdout # @return [IO] UI error output. Defaults to `$stderr`. attr_accessor :stderr def initialize @logger = Log4r::Logger.new("vagrant::ui::interface") @opts = {} @stdin = $stdin @stdout = $stdout @stderr = $stderr end def initialize_copy(original) super @opts = original.opts.dup end [:ask, :detail, :warn, :error, :info, :output, :success].each do |method| define_method(method) do |message, *opts| # Log normal console messages begin @logger.info { "#{method}: #{message}" } rescue ThreadError # We're being called in a trap-context. Wrap in a thread. Thread.new do @logger.info { "#{method}: #{message}" } end.join(THREAD_MAX_JOIN_TIMEOUT) end end end [:clear_line, :report_progress].each do |method| # By default do nothing, these aren't logged define_method(method) { |*args| } end # @return [false] def color? return false end # For machine-readable output. # # @param [String] type The type of the data # @param [Array] data The data associated with the type def machine(type, *data) @logger.info("Machine: #{type} #{data.inspect}") end end # This is a UI implementation that does nothing. class Silent < Interface def ask(*args) super # Silent can't do this, obviously. raise Errors::UIExpectsTTY end end class MachineReadable < Interface include Util::SafePuts def initialize super @lock = Mutex.new end def ask(*args) super # Machine-readable can't ask for input raise Errors::UIExpectsTTY end [:detail, :warn, :error, :info, :output, :success].each do |method| define_method(method) do |message, *args, **opts| machine("ui", method.to_s, message, *args, **opts) end end def machine(type, *data) opts = {} opts = data.pop if data.last.kind_of?(Hash) target = opts[:target] || "" # Prepare the data by replacing characters that aren't outputted data.each_index do |i| data[i] = data[i].to_s.dup data[i].gsub!(",", "%!(VAGRANT_COMMA)") data[i].gsub!("\n", "\\n") data[i].gsub!("\r", "\\r") end # Avoid locks in a trap context introduced from Ruby 2.0 Thread.new do @lock.synchronize do safe_puts("#{Time.now.utc.to_i},#{target},#{type},#{data.join(",")}") end end.join(THREAD_MAX_JOIN_TIMEOUT) end end # This is a UI implementation that outputs the text as is. It # doesn't add any color. class Basic < Interface include Util::SafePuts def initialize super @lock = Mutex.new end # Use some light meta-programming to create the various methods to # output text to the UI. These all delegate the real functionality # to `say`. [:detail, :info, :warn, :error, :output, :success].each do |method| class_eval <<-CODE def #{method}(message, *args) super(message) say(#{method.inspect}, message, *args) end CODE end def ask(message, opts=nil) super(message) # We can't ask questions when the output isn't a TTY. raise Errors::UIExpectsTTY if !@stdin.tty? && !Vagrant::Util::Platform.windows? # Setup the options so that the new line is suppressed opts ||= {} opts[:echo] = true if !opts.key?(:echo) opts[:new_line] = false if !opts.key?(:new_line) opts[:prefix] = false if !opts.key?(:prefix) # Output the data say(:info, message, opts) input = nil if opts[:echo] || !@stdin.respond_to?(:noecho) input = @stdin.gets else begin input = @stdin.noecho(&:gets) # Output a newline because without echo, the newline isn't # echoed either. say(:info, "\n", opts) rescue Errno::EBADF # This means that stdin doesn't support echoless input. say(:info, "\n#{I18n.t("vagrant.stdin_cant_hide_input")}\n ", opts) # Ask again, with echo enabled input = ask(message, opts.merge(echo: true)) end end # Get the results and chomp off the newline. We do a logical OR # here because `gets` can return a nil, for example in the case # that ctrl-D is pressed on the input. (input || "").chomp end # This is used to output progress reports to the UI. # Send this method progress/total and it will output it # to the UI. Send `clear_line` to clear the line to show # a continuous progress meter. def report_progress(progress, total, show_parts=true) if total && total > 0 percent = (progress.to_f / total.to_f) * 100 line = "Progress: #{percent.to_i}%" line << " (#{progress} / #{total})" if show_parts else line = "Progress: #{progress}" end info(line, new_line: false) end def clear_line # See: https://en.wikipedia.org/wiki/ANSI_escape_code reset = "\r\033[K" info(reset, new_line: false) end # This method handles actually outputting a message of a given type # to the console. def say(type, message, **opts) defaults = { new_line: true, prefix: true } opts = defaults.merge(@opts).merge(opts) # Don't output if we're hiding details return if type == :detail && opts[:hide_detail] # Determine whether we're expecting to output our # own new line or not. printer = opts[:new_line] ? :puts : :print # Determine the proper IO channel to send this message # to based on the type of the message channel = type == :error || opts[:channel] == :error ? @stderr : @stdout # Output! We wrap this in a lock so that it safely outputs only # one line at a time. We wrap this in a thread because as of Ruby 2.0 # we can't acquire locks in a trap context (ctrl-c), so we have to # do this. Thread.new do @lock.synchronize do safe_puts(format_message(type, message, **opts), io: channel, printer: printer) end end.join(THREAD_MAX_JOIN_TIMEOUT) end def format_message(type, message, **opts) message end end # Prefixed wraps an existing UI and adds a prefix to it. class Prefixed < Interface # The prefix for `output` messages. OUTPUT_PREFIX = "==> " def initialize(ui, prefix) super() @prefix = prefix @ui = ui end def initialize_copy(original) super @ui = original.instance_variable_get(:@ui).dup end # Use some light meta-programming to create the various methods to # output text to the UI. These all delegate the real functionality # to `say`. [:ask, :detail, :info, :warn, :error, :output, :success].each do |method| class_eval <<-CODE def #{method}(message, *args, **opts) super(message) if !@ui.opts.key?(:bold) && !opts.key?(:bold) opts[:bold] = #{method.inspect} != :detail && \ #{method.inspect} != :ask end if !opts.key?(:target) opts[:target] = @prefix end @ui.#{method}(format_message(#{method.inspect}, message, **opts), *args, **opts) end CODE end [:clear_line, :report_progress].each do |method| # By default do nothing, these aren't formatted define_method(method) { |*args| @ui.send(method, *args) } end # For machine-readable output, set the prefix in the # options hash and continue it on. def machine(type, *data) opts = {} opts = data.pop if data.last.is_a?(Hash) opts[:target] = @prefix data << opts @ui.machine(type, *data) end # Return the parent's opts. # # @return [Hash] def opts @ui.opts end def format_message(type, message, **opts) opts = self.opts.merge(opts) prefix = "" if !opts.key?(:prefix) || opts[:prefix] prefix = OUTPUT_PREFIX prefix = " " * OUTPUT_PREFIX.length if \ type == :detail || type == :ask || opts[:prefix_spaces] end # Fast-path if there is no prefix return message if prefix.empty? target = @prefix target = opts[:target] if opts.key?(:target) target = "#{target}:" if target != "" # Get the lines. The first default is because if the message # is an empty string, then we want to still use the empty string. lines = [message] lines = message.split("\n") if message != "" # Otherwise, make sure to prefix every line properly lines.map do |line| "#{prefix}#{target} #{line}" end.join("\n") end end # This is a UI implementation that outputs color for various types # of messages. This should only be used with a TTY that supports color, # but is up to the user of the class to verify this is the case. class Colored < Basic # Terminal colors COLORS = { red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, } # @return [true] def color? return true end # This is called by `say` to format the message for output. def format_message(type, message, **opts) # Get the format of the message before adding color. message = super opts = @opts.merge(opts) # Special case some colors for certain message types opts[:color] = :red if type == :error opts[:color] = :green if type == :success opts[:color] = :yellow if type == :warn # If it is a detail, it is not bold. Every other message type # is bolded. bold = !!opts[:bold] colorseq = "#{bold ? 1 : 0 }" if opts[:color] && opts[:color] != :default color = COLORS[opts[:color]] colorseq += ";#{color}" end # Color the message and make sure to reset the color at the end "\033[#{colorseq}m#{message}\033[0m" end end end end