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
8 changes: 6 additions & 2 deletions include/multipass/delayed_shutdown_timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifndef MULTIPASS_DELAYED_SHUTDOWN_TIMER_H
#define MULTIPASS_DELAYED_SHUTDOWN_TIMER_H

#include <multipass/ssh/ssh_session.h>
#include <multipass/virtual_machine.h>

#include <QObject>
Expand All @@ -32,10 +33,11 @@ class DelayedShutdownTimer : public QObject
Q_OBJECT

public:
DelayedShutdownTimer(VirtualMachine* virtual_machine);
DelayedShutdownTimer(VirtualMachine* virtual_machine, SSHSession&& session);
~DelayedShutdownTimer();

void start(std::chrono::milliseconds delay);
void start(const std::chrono::milliseconds delay);
std::chrono::seconds get_time_remaining();

signals:
void finished();
Expand All @@ -45,7 +47,9 @@ class DelayedShutdownTimer : public QObject

QTimer shutdown_timer;
VirtualMachine* virtual_machine;
SSHSession ssh_session;
std::chrono::milliseconds delay;
std::chrono::milliseconds time_remaining;
};
} // namespace multipass
#endif // MULTIPASS_DELAYED_SHUTDOWN_TIMER_H
25 changes: 20 additions & 5 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1334,17 +1334,30 @@ try // clang-format on
"");
}

if (!mp::utils::is_running(it->second->current_state()))
auto& vm = it->second;
if (!mp::utils::is_running(vm->current_state()))
{
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
fmt::format("instance \"{}\" is not running", name), "");
}

if (vm->state == VirtualMachine::State::delayed_shutdown)
{
if (delayed_shutdown_instances[name]->get_time_remaining() <= std::chrono::minutes(1))
{
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
fmt::format("\"{}\" is scheduled to shut down in less than a minute, use "
"'multipass stop --cancel {}' to cancel the shutdown.",
name, name),
"");
}
}

mp::SSHInfo ssh_info;
ssh_info.set_host(it->second->ssh_hostname());
ssh_info.set_port(it->second->ssh_port());
ssh_info.set_host(vm->ssh_hostname());
ssh_info.set_port(vm->ssh_port());
ssh_info.set_priv_key_base64(config->ssh_key_provider->private_key_as_base64());
ssh_info.set_username(it->second->ssh_username());
ssh_info.set_username(vm->ssh_username());
(*response.mutable_ssh_info())[name] = ssh_info;
}

Expand Down Expand Up @@ -1525,7 +1538,9 @@ try // clang-format on
{
delayed_shutdown_instances.erase(name);
}
delayed_shutdown_instances[name] = std::make_unique<DelayedShutdownTimer>(it->second.get());
auto& vm = it->second;
mp::SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *config->ssh_key_provider};
delayed_shutdown_instances[name] = std::make_unique<DelayedShutdownTimer>(vm.get(), std::move(session));
QObject::connect(delayed_shutdown_instances[name].get(), &DelayedShutdownTimer::finished,
[this, name]() { delayed_shutdown_instances.erase(name); });
delayed_shutdown_instances[name]->start(std::chrono::minutes(request->time_minutes()));
Expand Down
59 changes: 54 additions & 5 deletions src/daemon/delayed_shutdown_timer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,40 @@
namespace mp = multipass;
namespace mpl = multipass::logging;

mp::DelayedShutdownTimer::DelayedShutdownTimer(VirtualMachine* virtual_machine) : virtual_machine{virtual_machine}
namespace
{
void write_shutdown_message(mp::SSHSession& ssh_session, const std::chrono::minutes& time_left, const std::string& name)
{
if (time_left > std::chrono::milliseconds::zero())
{
ssh_session.exec(fmt::format("wall \"The system is going down for poweroff in {} minute{}, use 'multipass stop "
"--cancel {}' to cancel the shutdown.\"",
time_left.count(), time_left > std::chrono::minutes(1) ? "s" : "", name));
}
else
{
ssh_session.exec(fmt::format("wall The system is going down for poweroff now"));
}
}
} // namespace

mp::DelayedShutdownTimer::DelayedShutdownTimer(VirtualMachine* virtual_machine, SSHSession&& session)
: virtual_machine{virtual_machine}, ssh_session{std::move(session)}
{
}

mp::DelayedShutdownTimer::~DelayedShutdownTimer()
{
if (shutdown_timer.isActive())
{
// exit_code() is here to make sure the command finishes before continuing in the dtor
ssh_session.exec("wall The system shutdown has been cancelled").exit_code();
mpl::log(mpl::Level::info, virtual_machine->vm_name, fmt::format("Cancelling delayed shutdown"));
virtual_machine->state = VirtualMachine::State::running;
}
}

void mp::DelayedShutdownTimer::start(std::chrono::milliseconds delay)
void mp::DelayedShutdownTimer::start(const std::chrono::milliseconds delay)
{
if (virtual_machine->state == VirtualMachine::State::stopped ||
virtual_machine->state == VirtualMachine::State::off)
Expand All @@ -48,19 +68,48 @@ void mp::DelayedShutdownTimer::start(std::chrono::milliseconds delay)
fmt::format("Shutdown request delayed for {} minute{}",
std::chrono::duration_cast<std::chrono::minutes>(delay).count(),
delay > std::chrono::minutes(1) ? "s" : ""));
QObject::connect(&shutdown_timer, &QTimer::timeout, [this]() { shutdown_instance(); });
write_shutdown_message(ssh_session, std::chrono::duration_cast<std::chrono::minutes>(delay),
virtual_machine->vm_name);

time_remaining = delay;
std::chrono::minutes time_elapsed{1};
QObject::connect(&shutdown_timer, &QTimer::timeout, [this, delay, time_elapsed]() mutable {
time_remaining = delay - time_elapsed;

if (time_remaining <= std::chrono::minutes(5) ||
time_remaining % std::chrono::minutes(5) == std::chrono::minutes::zero())
{
write_shutdown_message(ssh_session, std::chrono::duration_cast<std::chrono::minutes>(time_remaining),
virtual_machine->vm_name);
}

if (time_elapsed >= delay)
{
shutdown_timer.stop();
shutdown_instance();
}
else
{
time_elapsed += std::chrono::minutes(1);
}
});

virtual_machine->state = VirtualMachine::State::delayed_shutdown;

shutdown_timer.setSingleShot(true);
shutdown_timer.start(delay);
shutdown_timer.start(delay < std::chrono::minutes(1) ? delay : std::chrono::minutes(1));
}
else
{
write_shutdown_message(ssh_session, std::chrono::minutes::zero(), virtual_machine->vm_name);
shutdown_instance();
}
}

std::chrono::seconds mp::DelayedShutdownTimer::get_time_remaining()
{
return std::chrono::duration_cast<std::chrono::minutes>(time_remaining);
}

void mp::DelayedShutdownTimer::shutdown_instance()
{
virtual_machine->shutdown();
Expand Down
49 changes: 45 additions & 4 deletions tests/test_delayed_shutdown.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*
*/

#include "mock_ssh.h"
#include "signal.h"
#include "stub_virtual_machine.h"

Expand All @@ -36,18 +37,30 @@ struct DelayedShutdown : public Test
{
DelayedShutdown()
{
connect.returnValue(SSH_OK);
is_connected.returnValue(true);
open_session.returnValue(SSH_OK);
request_exec.returnValue(SSH_OK);

vm = std::make_unique<mpt::StubVirtualMachine>();
vm->state = mp::VirtualMachine::State::running;
}

decltype(MOCK(ssh_connect)) connect{MOCK(ssh_connect)};
decltype(MOCK(ssh_is_connected)) is_connected{MOCK(ssh_is_connected)};
decltype(MOCK(ssh_channel_open_session)) open_session{MOCK(ssh_channel_open_session)};
decltype(MOCK(ssh_channel_request_exec)) request_exec{MOCK(ssh_channel_request_exec)};

mp::VirtualMachine::UPtr vm;
mp::SSHSession session{"a", 42};
QEventLoop loop;
ssh_channel_callbacks callbacks{nullptr};
};

TEST_F(DelayedShutdown, emits_finished_after_timer_expires)
{
mpt::Signal finished;
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get()};
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get(), std::move(session)};

QObject::connect(&delayed_shutdown_timer, &mp::DelayedShutdownTimer::finished, [this, &finished] {
loop.quit();
Expand All @@ -64,7 +77,7 @@ TEST_F(DelayedShutdown, emits_finished_after_timer_expires)
TEST_F(DelayedShutdown, emits_finished_with_no_timer)
{
mpt::Signal finished;
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get()};
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get(), std::move(session)};

QObject::connect(&delayed_shutdown_timer, &mp::DelayedShutdownTimer::finished, [&finished] { finished.signal(); });

Expand All @@ -75,17 +88,45 @@ TEST_F(DelayedShutdown, emits_finished_with_no_timer)

TEST_F(DelayedShutdown, vm_state_delayed_shutdown_when_timer_running)
{
auto add_channel_cbs = [this](ssh_channel, ssh_channel_callbacks cb) {
callbacks = cb;
return SSH_OK;
};
REPLACE(ssh_add_channel_callbacks, add_channel_cbs);

auto event_dopoll = [this](auto...) {
if (!callbacks)
return SSH_ERROR;
callbacks->channel_exit_status_function(nullptr, nullptr, 0, callbacks->userdata);
return SSH_OK;
};
REPLACE(ssh_event_dopoll, event_dopoll);

EXPECT_TRUE(vm->state == mp::VirtualMachine::State::running);
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get()};
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get(), std::move(session)};
delayed_shutdown_timer.start(std::chrono::milliseconds(1));

EXPECT_TRUE(vm->state == mp::VirtualMachine::State::delayed_shutdown);
}

TEST_F(DelayedShutdown, vm_state_running_after_cancel)
{
auto add_channel_cbs = [this](ssh_channel, ssh_channel_callbacks cb) {
callbacks = cb;
return SSH_OK;
};
REPLACE(ssh_add_channel_callbacks, add_channel_cbs);

auto event_dopoll = [this](auto...) {
if (!callbacks)
return SSH_ERROR;
callbacks->channel_exit_status_function(nullptr, nullptr, 0, callbacks->userdata);
return SSH_OK;
};
REPLACE(ssh_event_dopoll, event_dopoll);

{
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get()};
mp::DelayedShutdownTimer delayed_shutdown_timer{vm.get(), std::move(session)};
delayed_shutdown_timer.start(std::chrono::milliseconds(1));
EXPECT_TRUE(vm->state == mp::VirtualMachine::State::delayed_shutdown);
}
Expand Down