Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/client/cli/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "cmd/umount.h"
#include "cmd/unalias.h"
#include "cmd/version.h"
#include "cmd/wait_ready.h"

#include <multipass/cli/argparser.h>
#include <multipass/cli/client_common.h>
Expand Down Expand Up @@ -111,6 +112,7 @@ mp::Client::Client(ClientConfig& config)
add_command<cmd::Umount>();
add_command<cmd::Version>();
add_command<cmd::Clone>();
add_command<cmd::WaitReady>();

sort_commands();

Expand Down
1 change: 1 addition & 0 deletions src/client/cli/cmd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ add_library(commands STATIC
umount.cpp
unalias.cpp
version.cpp
wait_ready.cpp
)

target_link_libraries(commands
Expand Down
28 changes: 22 additions & 6 deletions src/client/cli/cmd/common_cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,35 @@ QString multipass::cmd::describe_common_settings_keys()
" get --keys` to obtain the full list of available settings at any given time.";
}

void multipass::cmd::add_timeout(multipass::ArgParser* parser)
namespace
{
void add_timeout_option(multipass::ArgParser* parser, const QString& extra_description = {})
{
QCommandLineOption timeout_option(
"timeout",
QString("Maximum time, in seconds, to wait for the command to complete. "
"Note that some background operations may continue beyond that. "
"By default, instance startup and initialization is limited to "
"%1 minutes each.")
.arg(std::chrono::duration_cast<std::chrono::minutes>(multipass::default_timeout)
.count()),
"Note that some background operations may continue beyond that.") +
extra_description,
"timeout");
parser->addOption(timeout_option);
}
} // namespace

void multipass::cmd::add_instance_timeout(multipass::ArgParser* parser)
{
const auto instance_timeout_str =
QString(" By default, instance startup and initialization is limited to "
"%1 minutes each.")
.arg(std::chrono::duration_cast<std::chrono::minutes>(multipass::default_timeout)
.count());

add_timeout_option(parser, instance_timeout_str);
}

void multipass::cmd::add_timeout(multipass::ArgParser* parser)
{
add_timeout_option(parser);
}

int multipass::cmd::parse_timeout(const multipass::ArgParser* parser)
{
Expand Down
1 change: 1 addition & 0 deletions src/client/cli/cmd/common_cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ ReturnCode return_code_from(const SettingsException& e);
QString describe_common_settings_keys();

// parser helpers
void add_instance_timeout(multipass::ArgParser*);
void add_timeout(multipass::ArgParser*);
int parse_timeout(const multipass::ArgParser* parser);
std::unique_ptr<multipass::utils::Timer> make_timer(int timeout,
Expand Down
2 changes: 1 addition & 1 deletion src/client/cli/cmd/launch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser)
bridgedOption,
mountOption});

mp::cmd::add_timeout(parser);
mp::cmd::add_instance_timeout(parser);

auto status = parser->commandParse(this);

Expand Down
2 changes: 1 addition & 1 deletion src/client/cli/cmd/restart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ mp::ParseCode cmd::Restart::parse_args(mp::ArgParser* parser)
QCommandLineOption all_option(all_option_name, "Restart all instances");
parser->addOption(all_option);

mp::cmd::add_timeout(parser);
mp::cmd::add_instance_timeout(parser);

auto status = parser->commandParse(this);

Expand Down
2 changes: 1 addition & 1 deletion src/client/cli/cmd/shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ mp::ParseCode cmd::Shell::parse_args(mp::ArgParser* parser)

parser->addPositionalArgument("name", description, syntax);

mp::cmd::add_timeout(parser);
mp::cmd::add_instance_timeout(parser);

auto status = parser->commandParse(this);

Expand Down
2 changes: 1 addition & 1 deletion src/client/cli/cmd/start.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ mp::ParseCode cmd::Start::parse_args(mp::ArgParser* parser)
QCommandLineOption all_option(all_option_name, "Start all instances");
parser->addOption(all_option);

mp::cmd::add_timeout(parser);
mp::cmd::add_instance_timeout(parser);

auto status = parser->commandParse(this);

Expand Down
133 changes: 133 additions & 0 deletions src/client/cli/cmd/wait_ready.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (C) 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/>.
*
*/

#include "wait_ready.h"
#include "animated_spinner.h"
#include "common_cli.h"

#include <multipass/cli/argparser.h>
#include <multipass/exceptions/cmd_exceptions.h>
#include <multipass/timer.h>

#include <chrono>
#include <thread>

namespace mp = multipass;
namespace cmd = multipass::cmd;

mp::ReturnCode cmd::WaitReady::run(mp::ArgParser* parser)
{
auto ret = parse_args(parser);
if (ret != ParseCode::Ok)
{
return parser->returnCodeFrom(ret);
}

mp::AnimatedSpinner spinner{cout};
spinner.start("Waiting for the Multipass daemon to be ready");

std::unique_ptr<mp::utils::Timer> timer;

// If the user has specified a timeout, we will create a timer
if (parser->isSet("timeout"))
{
timer = cmd::make_timer(parser->value("timeout").toInt(),
&spinner,
cerr,
"Timed out waiting for the Multipass daemon to be ready.");
timer->start();
}

auto on_success = [&spinner, &timer](WaitReadyReply& reply) {
if (timer)
timer->stop();
spinner.stop();
return ReturnCode::Ok;
};

auto on_failure = [this, &spinner, &timer](grpc::Status& status) {
if (status.error_code() == grpc::StatusCode::NOT_FOUND)
{
// This is the expected state for when the daemon is not yet ready
// Sleep for a short duration and signal to retry
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return ReturnCode::Retry;
}

if (timer)
timer->stop();
spinner.stop();

// For any other error, we will handle it as a standard failure
return standard_failure_handler_for(name(), cerr, status);
};

request.set_verbosity_level(parser->verbosityLevel());

ReturnCode return_code;

while ((return_code = dispatch(&RpcMethod::wait_ready, request, on_success, on_failure)) ==
ReturnCode::Retry)
;

return return_code;
}

std::string cmd::WaitReady::name() const
{
return "wait-ready";
}

QString cmd::WaitReady::short_help() const
{
return QStringLiteral("Wait for the Multipass daemon to be ready");
}

QString cmd::WaitReady::description() const
{
return QStringLiteral(
"Wait for the Multipass daemon to be ready. This command will block until the\n"
"daemon has initialized, fetched up-to-date image information, and is ready to\n"
"accept requests. Its main use is to prevent failures caused by incomplete\n"
"initialization in batch operations. An optional timeout aborts the command if\n"
"reached.");
}

mp::ParseCode cmd::WaitReady::parse_args(mp::ArgParser* parser)
{
mp::cmd::add_timeout(parser);

auto status = parser->commandParse(this);

// Check if the command was parsed successfully
if (status != ParseCode::Ok)
{
return status;
}

try
{
mp::cmd::parse_timeout(parser);
}
catch (const mp::ValidationException& e)
{
cerr << "error: " << e.what() << "\n";
return ParseCode::CommandLineError;
}

return status;
}
46 changes: 46 additions & 0 deletions src/client/cli/cmd/wait_ready.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 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_WAIT_READY_H
#define MULTIPASS_WAIT_READY_H

#include <multipass/cli/command.h>

namespace multipass
{
class Formatter;

namespace cmd
{
class WaitReady final : public Command
{
public:
using Command::Command;
ReturnCode run(ArgParser* parser) override;

std::string name() const override;
QString short_help() const override;
QString description() const override;

private:
WaitReadyRequest request;

ParseCode parse_args(ArgParser* parser);
};
} // namespace cmd
} // namespace multipass
#endif // MULTIPASS_WAIT_READY_H
41 changes: 41 additions & 0 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ auto connect_rpc(mp::DaemonRpc& rpc, mp::Daemon& daemon)
QObject::connect(&rpc, &mp::DaemonRpc::on_snapshot, &daemon, &mp::Daemon::snapshot);
QObject::connect(&rpc, &mp::DaemonRpc::on_restore, &daemon, &mp::Daemon::restore);
QObject::connect(&rpc, &mp::DaemonRpc::on_daemon_info, &daemon, &mp::Daemon::daemon_info);
QObject::connect(&rpc, &mp::DaemonRpc::on_wait_ready, &daemon, &mp::Daemon::wait_ready);
}

enum class InstanceGroup
Expand Down Expand Up @@ -3130,6 +3131,46 @@ catch (const std::exception& e)
status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), ""));
}

void mp::Daemon::wait_ready(
const WaitReadyRequest* request,
grpc::ServerReaderWriterInterface<WaitReadyReply, WaitReadyRequest>* server,
std::promise<grpc::Status>* status_promise)
try
{
WaitReadyReply response;

mpl::ClientLogger<WaitReadyReply, WaitReadyRequest> logger{
mpl::level_from(request->verbosity_level()),
*config->logger,
server};

logger.log(mpl::Level::debug, "daemon", "Checking connection to image servers...");

// We use wait_update_manifests_all_and_optionally_applied_force to check connectivity to image
// servers.
try
{
wait_update_manifests_all_and_optionally_applied_force(
/*force_manifest_network_download=*/false);
logger.log(mpl::Level::debug, "daemon", "Successfully connected to image servers.");
status_promise->set_value(grpc::Status::OK);
}
catch (const mp::DownloadException& e)
{
logger.log(mpl::Level::warning,
"daemon",
fmt::format("Failed to connect to image servers: {}", e.what()));
grpc::Status download_error_status{grpc::StatusCode::NOT_FOUND,
"cannot connect to the image servers",
""};
status_promise->set_value(download_error_status);
}
}
catch (const std::exception& e)
{
status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), ""));
}

void mp::Daemon::on_shutdown()
{
}
Expand Down
5 changes: 5 additions & 0 deletions src/daemon/daemon.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ public slots:
grpc::ServerReaderWriterInterface<DaemonInfoReply, DaemonInfoRequest>* server,
std::promise<grpc::Status>* status_promise);

virtual void wait_ready(
const WaitReadyRequest* request,
grpc::ServerReaderWriterInterface<WaitReadyReply, WaitReadyRequest>* server,
std::promise<grpc::Status>* status_promise);

private:
void release_resources(const std::string& instance);
void create_vm(const CreateRequest* request,
Expand Down
2 changes: 2 additions & 0 deletions src/daemon/daemon_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ int main_impl(int argc, char* argv[], mp::Signal& app_ready_signal)
// relevant settings handlers

mp::Daemon daemon(std::move(config));

QObject::connect(&app,
&QCoreApplication::aboutToQuit,
&daemon,
Expand All @@ -112,6 +113,7 @@ int main_impl(int argc, char* argv[], mp::Signal& app_ready_signal)
// Signal the signal handler that app has completed its basic initialization, and
// ready to process signals.
app_ready_signal.signal();

auto exit_code = QCoreApplication::exec();
// QConcurrent::run() invocations are dispatched through the global
// thread pool. Wait until all threads in the pool are properly cleaned up.
Expand Down
12 changes: 12 additions & 0 deletions src/daemon/daemon_rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,18 @@ grpc::Status mp::DaemonRpc::daemon_info(
client_cert_from(context));
}

grpc::Status mp::DaemonRpc::wait_ready(
grpc::ServerContext* context,
grpc::ServerReaderWriter<WaitReadyReply, WaitReadyRequest>* server)
{
WaitReadyRequest request;
server->Read(&request);

return verify_client_and_dispatch_operation(
std::bind(&DaemonRpc::on_wait_ready, this, &request, server, std::placeholders::_1),
client_cert_from(context));
}

template <typename OperationSignal>
grpc::Status mp::DaemonRpc::verify_client_and_dispatch_operation(OperationSignal signal,
const std::string& client_cert)
Expand Down
Loading
Loading