PageRenderTime 27ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/vagrant/ui.rb

https://gitlab.com/0072016/0072016-vagrant
Ruby | 352 lines | 214 code | 61 blank | 77 comment | 40 complexity | bec3d6e49609ba14516a2ec15dfda772 MD5 | raw file
  1. require "delegate"
  2. require "io/console"
  3. require "thread"
  4. require "log4r"
  5. require "vagrant/util/platform"
  6. require "vagrant/util/safe_puts"
  7. module Vagrant
  8. module UI
  9. # Vagrant UIs handle communication with the outside world (typically
  10. # through a shell). They must respond to the following methods:
  11. #
  12. # * `info`
  13. # * `warn`
  14. # * `error`
  15. # * `success`
  16. class Interface
  17. # Opts can be used to set some options. These options are implementation
  18. # specific. See the implementation for more docs.
  19. attr_accessor :opts
  20. def initialize
  21. @logger = Log4r::Logger.new("vagrant::ui::interface")
  22. @opts = {}
  23. end
  24. def initialize_copy(original)
  25. super
  26. @opts = original.opts.dup
  27. end
  28. [:ask, :detail, :warn, :error, :info, :output, :success].each do |method|
  29. define_method(method) do |message, *opts|
  30. # Log normal console messages
  31. begin
  32. @logger.info { "#{method}: #{message}" }
  33. rescue ThreadError
  34. # We're being called in a trap-context. Wrap in a thread.
  35. Thread.new do
  36. @logger.info { "#{method}: #{message}" }
  37. end.join
  38. end
  39. end
  40. end
  41. [:clear_line, :report_progress].each do |method|
  42. # By default do nothing, these aren't logged
  43. define_method(method) { |*args| }
  44. end
  45. # For machine-readable output.
  46. #
  47. # @param [String] type The type of the data
  48. # @param [Array] data The data associated with the type
  49. def machine(type, *data)
  50. @logger.info("Machine: #{type} #{data.inspect}")
  51. end
  52. end
  53. # This is a UI implementation that does nothing.
  54. class Silent < Interface
  55. def ask(*args)
  56. super
  57. # Silent can't do this, obviously.
  58. raise Errors::UIExpectsTTY
  59. end
  60. end
  61. class MachineReadable < Interface
  62. include Util::SafePuts
  63. def initialize
  64. super
  65. @lock = Mutex.new
  66. end
  67. def ask(*args)
  68. super
  69. # Machine-readable can't ask for input
  70. raise Errors::UIExpectsTTY
  71. end
  72. def machine(type, *data)
  73. opts = {}
  74. opts = data.pop if data.last.kind_of?(Hash)
  75. target = opts[:target] || ""
  76. # Prepare the data by replacing characters that aren't outputted
  77. data.each_index do |i|
  78. data[i] = data[i].to_s.dup
  79. data[i].gsub!(",", "%!(VAGRANT_COMMA)")
  80. data[i].gsub!("\n", "\\n")
  81. data[i].gsub!("\r", "\\r")
  82. end
  83. @lock.synchronize do
  84. safe_puts("#{Time.now.utc.to_i},#{target},#{type},#{data.join(",")}")
  85. end
  86. end
  87. end
  88. # This is a UI implementation that outputs the text as is. It
  89. # doesn't add any color.
  90. class Basic < Interface
  91. include Util::SafePuts
  92. def initialize
  93. super
  94. @lock = Mutex.new
  95. end
  96. # Use some light meta-programming to create the various methods to
  97. # output text to the UI. These all delegate the real functionality
  98. # to `say`.
  99. [:detail, :info, :warn, :error, :output, :success].each do |method|
  100. class_eval <<-CODE
  101. def #{method}(message, *args)
  102. super(message)
  103. say(#{method.inspect}, message, *args)
  104. end
  105. CODE
  106. end
  107. def ask(message, opts=nil)
  108. super(message)
  109. # We can't ask questions when the output isn't a TTY.
  110. raise Errors::UIExpectsTTY if !$stdin.tty? && !Vagrant::Util::Platform.cygwin?
  111. # Setup the options so that the new line is suppressed
  112. opts ||= {}
  113. opts[:echo] = true if !opts.has_key?(:echo)
  114. opts[:new_line] = false if !opts.has_key?(:new_line)
  115. opts[:prefix] = false if !opts.has_key?(:prefix)
  116. # Output the data
  117. say(:info, message, opts)
  118. input = nil
  119. if opts[:echo]
  120. input = $stdin.gets
  121. else
  122. begin
  123. input = $stdin.noecho(&:gets)
  124. # Output a newline because without echo, the newline isn't
  125. # echoed either.
  126. say(:info, "\n", opts)
  127. rescue Errno::EBADF
  128. # This means that stdin doesn't support echoless input.
  129. say(:info, "\n#{I18n.t("vagrant.stdin_cant_hide_input")}\n ", opts)
  130. # Ask again, with echo enabled
  131. input = ask(message, opts.merge(echo: true))
  132. end
  133. end
  134. # Get the results and chomp off the newline. We do a logical OR
  135. # here because `gets` can return a nil, for example in the case
  136. # that ctrl-D is pressed on the input.
  137. (input || "").chomp
  138. end
  139. # This is used to output progress reports to the UI.
  140. # Send this method progress/total and it will output it
  141. # to the UI. Send `clear_line` to clear the line to show
  142. # a continuous progress meter.
  143. def report_progress(progress, total, show_parts=true)
  144. if total && total > 0
  145. percent = (progress.to_f / total.to_f) * 100
  146. line = "Progress: #{percent.to_i}%"
  147. line << " (#{progress} / #{total})" if show_parts
  148. else
  149. line = "Progress: #{progress}"
  150. end
  151. info(line, new_line: false)
  152. end
  153. def clear_line
  154. # See: http://en.wikipedia.org/wiki/ANSI_escape_code
  155. reset = "\r\033[K"
  156. info(reset, new_line: false)
  157. end
  158. # This method handles actually outputting a message of a given type
  159. # to the console.
  160. def say(type, message, **opts)
  161. defaults = { new_line: true, prefix: true }
  162. opts = defaults.merge(@opts).merge(opts)
  163. # Don't output if we're hiding details
  164. return if type == :detail && opts[:hide_detail]
  165. # Determine whether we're expecting to output our
  166. # own new line or not.
  167. printer = opts[:new_line] ? :puts : :print
  168. # Determine the proper IO channel to send this message
  169. # to based on the type of the message
  170. channel = type == :error || opts[:channel] == :error ? $stderr : $stdout
  171. # Output! We wrap this in a lock so that it safely outputs only
  172. # one line at a time. We wrap this in a thread because as of Ruby 2.0
  173. # we can't acquire locks in a trap context (ctrl-c), so we have to
  174. # do this.
  175. Thread.new do
  176. @lock.synchronize do
  177. safe_puts(format_message(type, message, **opts),
  178. io: channel, printer: printer)
  179. end
  180. end.join
  181. end
  182. def format_message(type, message, **opts)
  183. message
  184. end
  185. end
  186. # Prefixed wraps an existing UI and adds a prefix to it.
  187. class Prefixed < Interface
  188. # The prefix for `output` messages.
  189. OUTPUT_PREFIX = "==> "
  190. def initialize(ui, prefix)
  191. super()
  192. @prefix = prefix
  193. @ui = ui
  194. end
  195. def initialize_copy(original)
  196. super
  197. @ui = original.instance_variable_get(:@ui).dup
  198. end
  199. # Use some light meta-programming to create the various methods to
  200. # output text to the UI. These all delegate the real functionality
  201. # to `say`.
  202. [:ask, :detail, :info, :warn, :error, :output, :success].each do |method|
  203. class_eval <<-CODE
  204. def #{method}(message, *args, **opts)
  205. super(message)
  206. if !@ui.opts.has_key?(:bold) && !opts.has_key?(:bold)
  207. opts[:bold] = #{method.inspect} != :detail && \
  208. #{method.inspect} != :ask
  209. end
  210. @ui.#{method}(format_message(#{method.inspect}, message, **opts), *args, **opts)
  211. end
  212. CODE
  213. end
  214. [:clear_line, :report_progress].each do |method|
  215. # By default do nothing, these aren't formatted
  216. define_method(method) { |*args| @ui.send(method, *args) }
  217. end
  218. # For machine-readable output, set the prefix in the
  219. # options hash and continue it on.
  220. def machine(type, *data)
  221. opts = {}
  222. opts = data.pop if data.last.is_a?(Hash)
  223. opts[:target] = @prefix
  224. data << opts
  225. @ui.machine(type, *data)
  226. end
  227. # Return the parent's opts.
  228. #
  229. # @return [Hash]
  230. def opts
  231. @ui.opts
  232. end
  233. def format_message(type, message, **opts)
  234. opts = self.opts.merge(opts)
  235. prefix = ""
  236. if !opts.has_key?(:prefix) || opts[:prefix]
  237. prefix = OUTPUT_PREFIX
  238. prefix = " " * OUTPUT_PREFIX.length if \
  239. type == :detail || type == :ask || opts[:prefix_spaces]
  240. end
  241. # Fast-path if there is no prefix
  242. return message if prefix.empty?
  243. target = @prefix
  244. target = opts[:target] if opts.has_key?(:target)
  245. # Get the lines. The first default is because if the message
  246. # is an empty string, then we want to still use the empty string.
  247. lines = [message]
  248. lines = message.split("\n") if message != ""
  249. # Otherwise, make sure to prefix every line properly
  250. lines.map do |line|
  251. "#{prefix}#{target}: #{line}"
  252. end.join("\n")
  253. end
  254. end
  255. # This is a UI implementation that outputs color for various types
  256. # of messages. This should only be used with a TTY that supports color,
  257. # but is up to the user of the class to verify this is the case.
  258. class Colored < Basic
  259. # Terminal colors
  260. COLORS = {
  261. red: 31,
  262. green: 32,
  263. yellow: 33,
  264. blue: 34,
  265. magenta: 35,
  266. cyan: 36,
  267. white: 37,
  268. }
  269. # This is called by `say` to format the message for output.
  270. def format_message(type, message, **opts)
  271. # Get the format of the message before adding color.
  272. message = super
  273. opts = @opts.merge(opts)
  274. # Special case some colors for certain message types
  275. opts[:color] = :red if type == :error
  276. opts[:color] = :green if type == :success
  277. opts[:color] = :yellow if type == :warn
  278. # If it is a detail, it is not bold. Every other message type
  279. # is bolded.
  280. bold = !!opts[:bold]
  281. colorseq = "#{bold ? 1 : 0 }"
  282. if opts[:color] && opts[:color] != :default
  283. color = COLORS[opts[:color]]
  284. colorseq += ";#{color}"
  285. end
  286. # Color the message and make sure to reset the color at the end
  287. "\033[#{colorseq}m#{message}\033[0m"
  288. end
  289. end
  290. end
  291. end