Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add framework for an update checker and prompter #688

Merged
merged 13 commits into from
Mar 28, 2019
Merged
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
[submodule "3rd-party/xz-decoder/xz-embedded"]
path = 3rd-party/xz-decoder/xz-embedded
url = https://git.tukaani.org/xz-embedded.git
[submodule "3rd-party/semver"]
path = 3rd-party/semver
url = https://github.com/CanonicalLtd/semver.git
7 changes: 7 additions & 0 deletions 3rd-party/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,12 @@ add_library(premock INTERFACE)
target_include_directories(premock INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/premock)

# semver library
# Set option to disable tests, avoid a Boost dependency that is test-only
set(ENABLE_TESTS OFF CACHE BOOL "Disable tests in Semver lib" FORCE)
add_subdirectory(semver EXCLUDE_FROM_ALL)
ricab marked this conversation as resolved.
Show resolved Hide resolved
target_include_directories(semver INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/semver/include)

# xz-decoder config
add_subdirectory(xz-decoder EXCLUDE_FROM_ALL)
1 change: 1 addition & 0 deletions 3rd-party/semver
Submodule semver added at e7d4ab
37 changes: 37 additions & 0 deletions include/multipass/new_release_info.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2019 Canonical, Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_NEW_RELEASE_INFO_H
#define MULTIPASS_NEW_RELEASE_INFO_H

#include <QMetaType>
#include <QUrl>

namespace multipass
{

struct NewReleaseInfo
{
QString version;
QUrl url;
};

} // namespace multipass

Q_DECLARE_METATYPE(multipass::NewReleaseInfo)

#endif // MULTIPASS_NEW_RELEASE_INFO_H
4 changes: 3 additions & 1 deletion include/multipass/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#define MULTIPASS_PLATFORM_H

#include <multipass/logging/logger.h>
#include <multipass/update_prompt.h>
#include <multipass/virtual_machine_factory.h>

#include <libssh/sftp.h>
Expand All @@ -34,6 +35,7 @@ namespace platform
std::string default_server_address();
VirtualMachineFactory::UPtr vm_backend(const Path& data_dir);
logging::Logger::UPtr make_logger(logging::Level level);
UpdatePrompt::UPtr make_update_prompt();
int chown(const char* path, unsigned int uid, unsigned int gid);
bool symlink(const char* target, const char* link, bool is_dir);
bool link(const char* target, const char* link);
Expand All @@ -43,5 +45,5 @@ bool is_alias_supported(const std::string& alias, const std::string& remote);
bool is_remote_supported(const std::string& remote);
bool is_image_url_supported();
} // namespace platform
}
} // namespace multipass
#endif // MULTIPASS_PLATFORM_H
43 changes: 43 additions & 0 deletions include/multipass/qt_delete_later_unique_ptr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 Canonical, Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

#ifndef QT_DELETE_LATER_UNIQUE_PTR_H
#define QT_DELETE_LATER_UNIQUE_PTR_H

#include <memory>
#include <QObject>

namespace multipass
{

/*
* A unique_ptr for Qt objects that are more safely cleaned up on the event loop
* e.g. QThread
*/

struct QtDeleteLater {
void operator()(QObject *o) {
o->deleteLater();
}
};

template<typename T>
using qt_delete_later_unique_ptr = std::unique_ptr<T, QtDeleteLater>;

} // namespace

#endif // QT_DELETE_LATER_UNIQUE_PTR_H
39 changes: 39 additions & 0 deletions include/multipass/update_prompt.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 Canonical, Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_UPDATE_PROMPT_H
#define MULTIPASS_UPDATE_PROMPT_H

#include <memory>

namespace multipass
{
class UpdateInfo;

class UpdatePrompt
{
public:
using UPtr = std::unique_ptr<UpdatePrompt>;
virtual ~UpdatePrompt() = default;

virtual bool is_time_to_show() = 0;
virtual void populate(UpdateInfo *update_info) = 0;
virtual void populate_if_time_to_show(UpdateInfo *update_info) = 0;
};
} // namespace multipass

#endif // MULTIPASS_UPDATE_PROMPT_H
31 changes: 31 additions & 0 deletions src/client/cmd/common_cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <multipass/cli/format_utils.h>

#include <fmt/ostream.h>
#include <sstream>

namespace mp = multipass;
namespace cmd = multipass::cmd;
Expand All @@ -32,6 +33,23 @@ mp::ReturnCode return_code_for(const grpc::StatusCode& code)
{
return code == grpc::StatusCode::UNAVAILABLE ? mp::ReturnCode::DaemonFail : mp::ReturnCode::CommandFail;
}

std::string message_box(const std::string& message)
{
std::string::size_type divider_length = 50;
{
std::istringstream m(message);
std::string s;
while (getline(m, s, '\n'))
{
divider_length = std::max(divider_length, s.length());
}
}

const auto divider = std::string(divider_length, '#');

return '\n' + divider + '\n' + message + '\n' + divider + '\n';
}
} // namespace

mp::ParseCode cmd::check_for_name_and_all_option_conflict(mp::ArgParser* parser, std::ostream& cerr)
Expand Down Expand Up @@ -102,3 +120,16 @@ mp::ReturnCode cmd::standard_failure_handler_for(const std::string& command, std

return return_code_for(status.error_code());
}

bool cmd::update_available(const mp::UpdateInfo& update_info)
{
return update_info.version() != "";
Saviq marked this conversation as resolved.
Show resolved Hide resolved
}

std::string cmd::update_notice(const mp::UpdateInfo& update_info)
{
return ::message_box("A new Multipass version " + update_info.version() +
" is available!\n"
"Find out more: " +
update_info.url());
}
4 changes: 4 additions & 0 deletions src/client/cmd/common_cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ ParseCode handle_format_option(ArgParser* parser, Formatter** chosen_formatter,
std::string instance_action_message_for(const InstanceNames& instance_names, const std::string& action_name);
ReturnCode standard_failure_handler_for(const std::string& command, std::ostream& cerr, const grpc::Status& status,
const std::string& error_details = std::string());

// helpers for update handling
bool update_available(const multipass::UpdateInfo& update_info);
std::string update_notice(const multipass::UpdateInfo& update_info);
} // namespace cmd
} // namespace multipass

Expand Down
8 changes: 8 additions & 0 deletions src/client/cmd/launch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ mp::ReturnCode cmd::Launch::request_launch()
}

cout << "Launched: " << reply.vm_instance_name() << "\n";

if (term->is_live() && update_available(reply.update_info()))
{
// TODO: daemon doesn't know if client actually shows this notice. Need to be able
// to tell daemon that the notice will be displayed or not.
cout << update_notice(reply.update_info());
}

return ReturnCode::Ok;
};

Expand Down
3 changes: 3 additions & 0 deletions src/client/cmd/list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ mp::ReturnCode cmd::List::run(mp::ArgParser* parser)
auto on_success = [this](ListReply& reply) {
cout << chosen_formatter->format(reply);

if (term->is_live() && update_available(reply.update_info()))
cout << update_notice(reply.update_info());

return ReturnCode::Ok;
};

Expand Down
9 changes: 7 additions & 2 deletions src/client/cmd/start.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ mp::ReturnCode cmd::Start::run(mp::ArgParser* parser)

AnimatedSpinner spinner{cout};

auto on_success = [&spinner](mp::StartReply& reply) {
auto on_success = [&spinner, this](mp::StartReply& reply) {
spinner.stop();
if (term->is_live() && update_available(reply.update_info()))
cout << update_notice(reply.update_info());
return ReturnCode::Ok;
};

Expand Down Expand Up @@ -72,7 +74,10 @@ mp::ReturnCode cmd::Start::run(mp::ArgParser* parser)
return dispatch(&RpcMethod::start, request, on_success, on_failure, streaming_callback);
}

std::string cmd::Start::name() const { return "start"; }
std::string cmd::Start::name() const
{
return "start";
}

QString cmd::Start::short_help() const
{
Expand Down
13 changes: 9 additions & 4 deletions src/client/cmd/version.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
*/

#include "version.h"
#include <multipass/version.h>
#include "common_cli.h"
#include <multipass/cli/argparser.h>
#include <multipass/version.h>

namespace mp = multipass;
namespace cmd = multipass::cmd;
Expand All @@ -34,8 +35,9 @@ mp::ReturnCode cmd::Version::run(mp::ArgParser* parser)
cout << "multipass " << multipass::version_string << "\n";

auto on_success = [this](mp::VersionReply& reply) {
cout << "multipassd " << reply.version();
cout << "\n";
cout << "multipassd " << reply.version() << "\n";
if (term->is_live() && update_available(reply.update_info()))
cout << update_notice(reply.update_info());
return ReturnCode::Ok;
};

Expand All @@ -46,7 +48,10 @@ mp::ReturnCode cmd::Version::run(mp::ArgParser* parser)
return dispatch(&RpcMethod::version, request, on_success, on_failure);
}

std::string cmd::Version::name() const { return "version"; }
std::string cmd::Version::name() const
{
return "version";
}

QString cmd::Version::short_help() const
{
Expand Down
20 changes: 15 additions & 5 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ try // clang-format on
{
mpl::ClientLogger<ListReply> logger{mpl::level_from(request->verbosity_level()), *config->logger, server};
ListReply response;
config->update_prompt->populate_if_time_to_show(response.mutable_update_info());

auto status_for = [](mp::VirtualMachine::State state) {
switch (state)
Expand Down Expand Up @@ -1249,17 +1250,17 @@ try // clang-format on
mpl::ClientLogger<RecoverReply> logger{mpl::level_from(request->verbosity_level()), *config->logger, server};

const auto instances_and_status =
find_requested_instances(request->instance_names().instance_name(), deleted_instances,
std::bind(&Daemon::check_instance_exists, this, std::placeholders::_1));
find_requested_instances(request->instance_names().instance_name(), deleted_instances,
std::bind(&Daemon::check_instance_exists, this, std::placeholders::_1));
const auto& instances = instances_and_status.first; // use structured bindings instead in C++17
const auto& status = instances_and_status.second; // idem

if(status.ok())
if (status.ok())
ricab marked this conversation as resolved.
Show resolved Hide resolved
{
for (const auto& name : instances)
{
auto it = deleted_instances.find(name);
if(it != std::end(deleted_instances))
if (it != std::end(deleted_instances))
{
assert(vm_instance_specs[name].deleted);
vm_instance_specs[name].deleted = false;
Expand Down Expand Up @@ -1450,6 +1451,13 @@ try // clang-format on
if (update_instance_db)
persist_instances();

if (config->update_prompt->is_time_to_show())
{
StartReply start_reply;
config->update_prompt->populate(start_reply.mutable_update_info());
server->Write(start_reply);
}

return grpc_status_for(errors);
}
catch (const std::exception& e)
Expand Down Expand Up @@ -1709,6 +1717,7 @@ grpc::Status mp::Daemon::version(grpc::ServerContext* context, const VersionRequ

VersionReply reply;
reply.set_version(multipass::version_string);
config->update_prompt->populate(reply.mutable_update_info());
ricab marked this conversation as resolved.
Show resolved Hide resolved
server->Write(reply);
return grpc::Status::OK;
}
ricab marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -1904,7 +1913,7 @@ std::string mp::Daemon::check_instance_operational(const std::string& instance_n

std::string mp::Daemon::check_instance_exists(const std::string& instance_name) const
{
if(vm_instances.find(instance_name) == std::cend(vm_instances) &&
if (vm_instances.find(instance_name) == std::cend(vm_instances) &&
deleted_instances.find(instance_name) == std::cend(deleted_instances))
return fmt::format("instance \"{}\" does not exist\n", instance_name);

Expand Down Expand Up @@ -2010,6 +2019,7 @@ grpc::Status mp::Daemon::create_vm(grpc::ServerContext* context, const CreateReq
vm->wait_until_ssh_up(std::chrono::minutes(5));

reply.set_vm_instance_name(name);
config->update_prompt->populate_if_time_to_show(reply.mutable_update_info());
server->Write(reply);
}

Expand Down
4 changes: 3 additions & 1 deletion src/daemon/daemon_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
url_downloader = std::make_unique<URLDownloader>(cache_directory, std::chrono::seconds{10});
if (factory == nullptr)
factory = platform::vm_backend(data_directory);
if (update_prompt == nullptr)
update_prompt = platform::make_update_prompt();
if (image_hosts.empty())
{
image_hosts.push_back(std::make_unique<mp::CustomVMImageHost>(url_downloader.get(), manifest_ttl));
Expand Down Expand Up @@ -115,6 +117,6 @@ std::unique_ptr<const mp::DaemonConfig> mp::DaemonConfigBuilder::build()
return std::unique_ptr<const DaemonConfig>(
new DaemonConfig{std::move(url_downloader), std::move(factory), std::move(image_hosts), std::move(vault),
std::move(name_generator), std::move(ssh_key_provider), std::move(cert_provider),
std::move(client_cert_store), multiplexing_logger, cache_directory,
std::move(client_cert_store), std::move(update_prompt), multiplexing_logger, cache_directory,
data_directory, server_address, ssh_username, connection_type, image_refresh_timer});
}
Loading