diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index cca8c0184..920828407 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -4,12 +4,14 @@ require 'optparse' module Vagrant # Manages the command line interface to Vagrant. class CLI < Vagrant.plugin("2", :command) + def initialize(argv, env) super @logger = Log4r::Logger.new("vagrant::cli") @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + Util::CheckpointClient.instance.setup(env).check @logger.info("CLI: #{@main_args.inspect} #{@sub_command.inspect} #{@sub_args.inspect}") end @@ -36,6 +38,8 @@ module Vagrant command_class = command_plugin[0].call @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + Util::CheckpointClient.instance.display + # Initialize and execute the command class, returning the exit status. result = 0 begin diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 92be3d13a..581a835c9 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -4,7 +4,6 @@ require 'pathname' require 'set' require 'thread' -require "checkpoint" require 'log4r' require 'vagrant/util/file_mode' @@ -128,34 +127,6 @@ module Vagrant # Prepare the directories setup_home_path - # Run checkpoint in a background thread on every environment - # initialization. The cache file will cause this to mostly be a no-op - # most of the time. - @checkpoint_thr = Thread.new do - Thread.current[:result] = nil - - # If we disabled checkpoint via env var, don't run this - if ENV["VAGRANT_CHECKPOINT_DISABLE"].to_s != "" - @logger.info("checkpoint: disabled from env var") - next - end - - # If we disabled state and knowing what alerts we've seen, then - # disable the signature file. - signature_file = @data_dir.join("checkpoint_signature") - if ENV["VAGRANT_CHECKPOINT_NO_STATE"].to_s != "" - @logger.info("checkpoint: will not store state") - signature_file = nil - end - - Thread.current[:result] = Checkpoint.check( - product: "vagrant", - version: VERSION, - signature_file: signature_file, - cache_file: @data_dir.join("checkpoint_cache"), - ) - end - # Setup the local data directory. If a configuration path is given, # it is expanded relative to the root path. Otherwise, we use the # default (which is also expanded relative to the root path). @@ -289,16 +260,6 @@ module Vagrant end end - # Checkpoint returns the checkpoint result data. If checkpoint is - # disabled, this will return nil. See the hashicorp-checkpoint gem - # for more documentation on the return value. - # - # @return [Hash] - def checkpoint - @checkpoint_thr.join(THREAD_MAX_JOIN_TIMEOUT) - return @checkpoint_thr[:result] - end - # Makes a call to the CLI with the given arguments as if they # came from the real command line (sometimes they do!). An example: # diff --git a/lib/vagrant/util.rb b/lib/vagrant/util.rb index cef24e8ad..32babe50d 100644 --- a/lib/vagrant/util.rb +++ b/lib/vagrant/util.rb @@ -1,6 +1,7 @@ module Vagrant module Util autoload :Busy, 'vagrant/util/busy' + autoload :CheckpointClient, 'vagrant/util/checkpoint_client' autoload :CommandDeprecation, 'vagrant/util/command_deprecation' autoload :Counter, 'vagrant/util/counter' autoload :CredentialScrubber, 'vagrant/util/credential_scrubber' diff --git a/lib/vagrant/util/checkpoint_client.rb b/lib/vagrant/util/checkpoint_client.rb new file mode 100644 index 000000000..17dcfe868 --- /dev/null +++ b/lib/vagrant/util/checkpoint_client.rb @@ -0,0 +1,175 @@ +require "log4r" +require "singleton" + +module Vagrant + module Util + class CheckpointClient + + include Singleton + + # Maximum number of seconds to wait for check to complete + CHECKPOINT_TIMEOUT = 10 + + # @return [Log4r::Logger] + attr_reader :logger + + # @return [Boolean] + attr_reader :enabled + + # @return [Hash] + attr_reader :files + + # @return [Vagrant::Environment] + attr_reader :env + + def initialize + @logger = Log4r::Logger.new("vagrant::checkpoint_client") + @enabled = false + end + + # Setup will attempt to load the checkpoint library and define + # required paths + # + # @param [Vagrant::Environment] env + # @return [self] + def setup(env) + begin + require "checkpoint" + @enabled = true + rescue LoadError + @logger.warn("checkpoint library not found. disabling.") + end + @files = { + signature: env.data_dir.join("checkpoint_signature"), + cache: env.data_dir.join("checkpoint_cache") + } + @checkpoint_thread = nil + @env = env + self + end + + # Check has completed + def complete? + !@checkpoint_thread.nil? && !@checkpoint_thread.alive? + end + + # Result of check + # + # @return [Hash, nil] + def result + if !enabled || @checkpoint_thread.nil? + nil + elsif !defined?(@result) + @checkpoint_thread.join(CHECKPOINT_TIMEOUT) + @result = @checkpoint_thread[:result] + else + @result + end + end + + # Run check + # + # @return [self] + def check + if enabled && @checkpoint_thread.nil? + logger.debug("starting plugin check") + @checkpoint_thread = Thread.new do + begin + Thread.current[:result] = Checkpoint.check( + product: "vagrant", + version: VERSION, + signature_file: files[:signature], + cache_file: files[:cache] + ) + if !Thread.current[:result].is_a?(Hash) + Thread.current[:result] = nil + end + logger.debug("plugin check complete") + rescue => e + logger.debug("plugin check failure - #{e}") + end + end + end + self + end + + # Display any alerts or version update information + # + # @return [boolean] true if displayed, false if not + def display + if !defined?(@displayed) + if !complete? + @logger.debug("waiting for checkpoint to complete...") + end + # Don't display if information is cached + if result && !result["cached"] + version_check + alerts_check + else + @logger.debug("no information received from checkpoint") + end + @displayed = true + else + false + end + end + + def alerts_check + if result["alerts"] && !result["alerts"].empty? + result["alerts"].group_by{|a| a["level"]}.each_pair do |_, alerts| + alerts.each do |alert| + date = nil + begin + date = Time.at(alert["date"]) + rescue + date = Time.now + end + output = I18n.t("vagrant.alert", + message: alert["message"], + date: date, + url: alert["url"] + ) + case alert["level"] + when "info" + alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") + alert_ui.info(output) + when "warn" + alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant-warning") + alert_ui.warn(output) + when "critical" + alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant-alert") + alert_ui.error(output) + end + end + env.ui.info("") + end + else + @logger.debug("no alert notifications to display") + end + end + + def version_check + latest_version = Gem::Version.new(result["current_version"]) + installed_version = Gem::Version.new(VERSION) + ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") + if latest_version > installed_version + @logger.info("new version of Vagrant available - #{latest_version}") + ui.info(I18n.t("vagrant.version_upgrade_available", latest_version: latest_version)) + env.ui.info("") + else + @logger.debug("vagrant is currently up to date") + end + end + + # @private + # Reset the cached values for platform. This is not considered a public + # API and should only be used for testing. + def reset! + logger = @logger + instance_variables.each(&method(:remove_instance_variable)) + @logger = logger + @enabled = false + end + end + end +end diff --git a/plugins/commands/version/command.rb b/plugins/commands/version/command.rb index c3c47acfa..95c818b48 100644 --- a/plugins/commands/version/command.rb +++ b/plugins/commands/version/command.rb @@ -22,7 +22,7 @@ module VagrantPlugins @env.ui.machine("version-installed", Vagrant::VERSION) # Load the latest information - cp = @env.checkpoint + cp = Vagrant::Util::CheckpointClient.instance.result if !cp @env.ui.output("\n"+I18n.t( "vagrant.version_no_checkpoint")) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 99f5dad2e..0f591b94c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1,5 +1,10 @@ en: vagrant: + alert: |- + [%{date}]: + %{message} + + - %{url} boot_completed: |- Machine booted and ready! boot_waiting: |- @@ -283,6 +288,10 @@ en: version_no_checkpoint: |- Vagrant was unable to check for the latest version of Vagrant. Please check manually at https://www.vagrantup.com + version_upgrade_available: |- + A new version of Vagrant is available: %{latest_version}! + To upgrade visit: https://www.vagrantup.com/downloads.html + version_upgrade_howto: |- To upgrade to the latest version, visit the downloads page and download and install the latest version of Vagrant from the URL diff --git a/test/unit/vagrant/cli_test.rb b/test/unit/vagrant/cli_test.rb index fb33ac62f..5d079278f 100644 --- a/test/unit/vagrant/cli_test.rb +++ b/test/unit/vagrant/cli_test.rb @@ -1,6 +1,7 @@ require_relative "../base" require "vagrant/cli" +require "vagrant/util" describe Vagrant::CLI do include_context "unit" @@ -9,9 +10,21 @@ describe Vagrant::CLI do let(:commands) { {} } let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } + let(:checkpoint) { double("checkpoint") } before do allow(Vagrant.plugin("2").manager).to receive(:commands).and_return(commands) + allow(Vagrant::Util::CheckpointClient).to receive(:instance).and_return(checkpoint) + allow(checkpoint).to receive(:setup).and_return(checkpoint) + allow(checkpoint).to receive(:check) + allow(checkpoint).to receive(:display) + end + + describe "#initialize" do + it "should setup checkpoint" do + expect(checkpoint).to receive(:check) + described_class.new(["destroy"], env) + end end describe "#execute" do @@ -35,6 +48,12 @@ describe Vagrant::CLI do subject = described_class.new(["destroy"], env) expect(subject.execute).to eql(1) end + + it "displays any checkpoint information" do + commands[:destroy] = [command_lambda("destroy", 42), {}] + expect(checkpoint).to receive(:display) + described_class.new(["destroy"], env).execute + end end describe "#help" do diff --git a/test/unit/vagrant/util/checkpoint_client_test.rb b/test/unit/vagrant/util/checkpoint_client_test.rb new file mode 100644 index 000000000..c0e038c93 --- /dev/null +++ b/test/unit/vagrant/util/checkpoint_client_test.rb @@ -0,0 +1,174 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/checkpoint_client" + +describe Vagrant::Util::CheckpointClient do + include_context "unit" + + let(:iso_env) { isolated_environment } + let(:env) { iso_env.create_vagrant_env } + let(:result) { {} } + let(:prefixed_ui) { double("prefixed_ui") } + + subject{ Vagrant::Util::CheckpointClient.instance } + + after{ subject.reset! } + before do + allow(subject).to receive(:result).and_return(result) + allow(Vagrant::UI::Prefixed).to receive(:new).and_return(prefixed_ui) + end + + it "should not be enabled by default" do + expect(subject.enabled).to be(false) + end + + describe "#setup" do + before{ subject.setup(env) } + + it "should enable after setup" do + expect(subject.enabled).to be(true) + end + + it "should generate required paths" do + expect(subject.files).not_to be_empty + end + end + + describe "#check" do + context "without #setup" do + it "should not start the check" do + expect(Thread).not_to receive(:new) + subject.check + end + end + + context "with setup" do + before{ subject.setup(env) } + + it "should start the check" do + expect(Thread).to receive(:new) + subject.check + end + + it "should call checkpoint" do + expect(Thread).to receive(:new).and_yield + expect(Checkpoint).to receive(:check) + subject.check + end + end + end + + describe "#display" do + it "should only dislay once" do + expect(subject).to receive(:version_check).once + expect(subject).to receive(:alerts_check).once + + 2.times{ subject.display } + end + + it "should not display cached information" do + expect(subject).to receive(:result).and_return("cached" => true).at_least(:once) + expect(subject).not_to receive(:version_check) + expect(subject).not_to receive(:alerts_check) + + subject.display + end + end + + describe "#alerts_check" do + let(:critical){ + [{"level" => "critical", "message" => "critical message", + "url" => "http://example.com", "date" => Time.now.to_i}] + } + let(:warn){ + [{"level" => "warn", "message" => "warn message", + "url" => "http://example.com", "date" => Time.now.to_i}] + } + let(:info){ + [{"level" => "info", "message" => "info message", + "url" => "http://example.com", "date" => Time.now.to_i}] + } + + before{ subject.setup(env) } + + context "with no alerts" do + it "should not display alerts" do + expect(prefixed_ui).not_to receive(:info) + subject.alerts_check + end + end + + context "with critical alerts" do + let(:result) { {"alerts" => critical} } + + it "should display critical alert" do + expect(prefixed_ui).to receive(:error) + subject.alerts_check + end + end + + context "with warn alerts" do + let(:result) { {"alerts" => warn} } + + it "should display warn alerts" do + expect(prefixed_ui).to receive(:warn) + subject.alerts_check + end + end + + context "with info alerts" do + let(:result) { {"alerts" => info} } + + it "should display info alerts" do + expect(prefixed_ui).to receive(:info) + subject.alerts_check + end + end + + context "with mixed alerts" do + let(:result) { {"alerts" => info + warn + critical} } + + it "should display all alert types" do + expect(prefixed_ui).to receive(:info) + expect(prefixed_ui).to receive(:warn) + expect(prefixed_ui).to receive(:error) + + subject.alerts_check + end + end + end + + describe "#version_check" do + before{ subject.setup(env) } + + let(:new_version){ Gem::Version.new(Vagrant::VERSION).bump.to_s } + let(:old_version){ Gem::Version.new("1.0.0") } + + context "latest version is same as current version" do + let(:result) { {"current_version" => Vagrant::VERSION } } + + it "should not display upgrade information" do + expect(prefixed_ui).not_to receive(:info) + subject.version_check + end + end + + context "latest version is older than current version" do + let(:result) { {"current_version" => old_version} } + + it "should not display upgrade information" do + expect(prefixed_ui).not_to receive(:info) + subject.version_check + end + end + + context "latest version is newer than current version" do + let(:result) { {"current_version" => new_version} } + + it "should not display upgrade information" do + expect(prefixed_ui).not_to receive(:info).at_least(:once) + subject.version_check + end + end + end +end diff --git a/vagrant.gemspec b/vagrant.gemspec index 13a08c513..d26b32adf 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.add_dependency "erubis", "~> 2.7.0" s.add_dependency "i18n", ">= 0.6.0", "<= 0.8.0" s.add_dependency "listen", "~> 3.1.5" - s.add_dependency "hashicorp-checkpoint", "~> 0.1.1" + s.add_dependency "hashicorp-checkpoint", "~> 0.1.5" s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11" s.add_dependency "net-ssh", "~> 4.2.0" s.add_dependency "net-sftp", "~> 2.1"