diff --git a/include/multipass/delayed_shutdown_timer.h b/include/multipass/delayed_shutdown_timer.h index 9c81c2affc..50faddf51b 100644 --- a/include/multipass/delayed_shutdown_timer.h +++ b/include/multipass/delayed_shutdown_timer.h @@ -18,6 +18,7 @@ #ifndef MULTIPASS_DELAYED_SHUTDOWN_TIMER_H #define MULTIPASS_DELAYED_SHUTDOWN_TIMER_H +#include #include #include @@ -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(); @@ -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 diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index eaa0b100f7..5d6cc36989 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -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; } @@ -1525,7 +1538,9 @@ try // clang-format on { delayed_shutdown_instances.erase(name); } - delayed_shutdown_instances[name] = std::make_unique(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(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())); diff --git a/src/daemon/delayed_shutdown_timer.cpp b/src/daemon/delayed_shutdown_timer.cpp index eca5f14a6e..2e240401bd 100644 --- a/src/daemon/delayed_shutdown_timer.cpp +++ b/src/daemon/delayed_shutdown_timer.cpp @@ -23,7 +23,25 @@ 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)} { } @@ -31,12 +49,14 @@ 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) @@ -48,19 +68,48 @@ void mp::DelayedShutdownTimer::start(std::chrono::milliseconds delay) fmt::format("Shutdown request delayed for {} minute{}", std::chrono::duration_cast(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(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(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(time_remaining); +} + void mp::DelayedShutdownTimer::shutdown_instance() { virtual_machine->shutdown(); diff --git a/tests/test_delayed_shutdown.cpp b/tests/test_delayed_shutdown.cpp index 5272b3a252..cf97b9fe85 100644 --- a/tests/test_delayed_shutdown.cpp +++ b/tests/test_delayed_shutdown.cpp @@ -15,6 +15,7 @@ * */ +#include "mock_ssh.h" #include "signal.h" #include "stub_virtual_machine.h" @@ -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(); 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(); @@ -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(); }); @@ -75,8 +88,22 @@ 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); @@ -84,8 +111,22 @@ TEST_F(DelayedShutdown, vm_state_delayed_shutdown_when_timer_running) 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); }