diff --git a/include/multipass/platform_unix.h b/include/multipass/platform_unix.h index a53cbfc139..981330e894 100644 --- a/include/multipass/platform_unix.h +++ b/include/multipass/platform_unix.h @@ -32,11 +32,11 @@ namespace multipass::platform class SignalWrapper : public Singleton { public: - SignalWrapper(const PrivatePass&) noexcept; + SignalWrapper(const PrivatePass&) noexcept; - virtual int mask_signals(int how, const sigset_t* sigset, sigset_t* old_set = nullptr) const; - virtual int send(pthread_t target, int signal) const; - virtual int wait(const sigset_t& sigset, int& got) const; + virtual int mask_signals(int how, const sigset_t* sigset, sigset_t* old_set = nullptr) const; + virtual int send(pthread_t target, int signal) const; + virtual int wait(const sigset_t& sigset, int& got) const; }; sigset_t make_sigset(const std::vector& sigs); diff --git a/src/platform/platform_unix.cpp b/src/platform/platform_unix.cpp index ee7343b77d..2e7c367eb6 100644 --- a/src/platform/platform_unix.cpp +++ b/src/platform/platform_unix.cpp @@ -158,9 +158,9 @@ int mp::platform::symlink_attr_from(const char* path, sftp_attributes_struct* at return 0; } -mp::platform::SignalWrapper::SignalWrapper(const PrivatePass& pass) noexcept -: Singleton(pass){} - +mp::platform::SignalWrapper::SignalWrapper(const PrivatePass& pass) noexcept : Singleton(pass) +{ +} int mp::platform::SignalWrapper::mask_signals(int how, const sigset_t* sigset, sigset_t* old_set) const { @@ -207,11 +207,8 @@ std::function(const std::function&)> mp::platform::ma { return [sigset = make_and_block_signals({SIGQUIT, SIGTERM, SIGHUP, SIGUSR2}), period](const std::function& condition) -> std::optional { - // create a timer to periodically send SIGUSR2 - utils::Timer signal_generator{period, [signalee = pthread_self()] { - MP_POSIX_SIGNAL.send(signalee, SIGUSR2); - }}; + utils::Timer signal_generator{period, [signalee = pthread_self()] { MP_POSIX_SIGNAL.send(signalee, SIGUSR2); }}; // wait on signals and condition int latest_signal = SIGUSR2; diff --git a/tests/unix/mock_signal_wrapper.h b/tests/unix/mock_signal_wrapper.h new file mode 100644 index 0000000000..9701f278a0 --- /dev/null +++ b/tests/unix/mock_signal_wrapper.h @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +#ifndef MULTIPASS_MOCK_SIGNAL_WRAPPER_H +#define MULTIPASS_MOCK_SIGNAL_WRAPPER_H + +#include "../common.h" +#include "../mock_singleton_helpers.h" + +#include + +namespace multipass::test +{ + +class MockSignalWrapper : public platform::SignalWrapper +{ +public: + using SignalWrapper::SignalWrapper; + + MOCK_METHOD(int, mask_signals, (int, const sigset_t*, sigset_t*), (const, override)); + MOCK_METHOD(int, send, (pthread_t, int), (const, override)); + MOCK_METHOD(int, wait, (const sigset_t&, int&), (const, override)); + + MP_MOCK_SINGLETON_BOILERPLATE(MockSignalWrapper, platform::SignalWrapper); +}; + +} // namespace multipass::test + +#endif // MULTIPASS_MOCK_SIGNAL_WRAPPER_H diff --git a/tests/unix/test_platform_unix.cpp b/tests/unix/test_platform_unix.cpp index 02c129e33d..bcc7215582 100644 --- a/tests/unix/test_platform_unix.cpp +++ b/tests/unix/test_platform_unix.cpp @@ -21,6 +21,7 @@ #include #include "mock_libc_functions.h" +#include "mock_signal_wrapper.h" #include #include @@ -142,3 +143,138 @@ TEST_F(TestPlatformUnix, multipassStorageLocationNotSetReturnsEmpty) EXPECT_TRUE(storage_path.isEmpty()); } + +void test_sigset_empty(const sigset_t& set) +{ + // there is no standard empty check to try a few different signals + EXPECT_EQ(sigismember(&set, SIGABRT), 0); + EXPECT_EQ(sigismember(&set, SIGALRM), 0); + EXPECT_EQ(sigismember(&set, SIGCHLD), 0); + EXPECT_EQ(sigismember(&set, SIGINT), 0); + EXPECT_EQ(sigismember(&set, SIGSEGV), 0); + EXPECT_EQ(sigismember(&set, SIGKILL), 0); + EXPECT_EQ(sigismember(&set, SIGQUIT), 0); + EXPECT_EQ(sigismember(&set, SIGTERM), 0); + EXPECT_EQ(sigismember(&set, SIGUSR1), 0); + EXPECT_EQ(sigismember(&set, SIGUSR2), 0); +} + +bool test_sigset_has(const sigset_t& set, const std::vector& sigs) +{ + bool good = true; + for (auto sig : sigs) + { + auto has_sig = sigismember(&set, sig) == 1; + if (!has_sig) + good = false; + + EXPECT_TRUE(has_sig); + } + + return good; +} + +TEST_F(TestPlatformUnix, make_sigset_returns_emptyset) +{ + auto set = mp::platform::make_sigset({}); + test_sigset_empty(set); +} + +TEST_F(TestPlatformUnix, make_sigset_makes_sigset) +{ + auto set = mp::platform::make_sigset({SIGINT, SIGUSR2}); + + // check signals are set + test_sigset_has(set, {SIGINT, SIGUSR2}); + + // unset set signals + sigdelset(&set, SIGUSR2); + sigdelset(&set, SIGINT); + + // check other signals aren't set + test_sigset_empty(set); +} + +TEST_F(TestPlatformUnix, make_and_block_signals_works) +{ + auto [mock_signals, guard] = mpt::MockSignalWrapper::inject(); + + EXPECT_CALL( + *mock_signals, + mask_signals(SIG_BLOCK, Pointee(Truly([](const auto& set) { return test_sigset_has(set, {SIGINT}); })), _)); + + auto set = mp::platform::make_and_block_signals({SIGINT}); + + test_sigset_has(set, {SIGINT}); + + sigdelset(&set, SIGINT); + test_sigset_empty(set); +} + +TEST_F(TestPlatformUnix, make_quit_watchdog_blocks_signals) +{ + auto [mock_signals, guard] = mpt::MockSignalWrapper::inject(); + + EXPECT_CALL(*mock_signals, + mask_signals(SIG_BLOCK, + Pointee(Truly([](const auto& set) { + return test_sigset_has(set, {SIGQUIT, SIGTERM, SIGHUP, SIGUSR2}); + })), + _)); + + mp::platform::make_quit_watchdog(std::chrono::milliseconds{1}); +} + +TEST_F(TestPlatformUnix, quit_watchdog_quits_on_condition) +{ + auto [mock_signals, guard] = mpt::MockSignalWrapper::inject(); + + EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _)); + EXPECT_CALL(*mock_signals, wait(_, _)).WillRepeatedly(DoAll(SetArgReferee<1>(SIGUSR2), Return(0))); + ON_CALL(*mock_signals, send(pthread_self(), SIGUSR2)).WillByDefault(Return(0)); + + auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1}); + EXPECT_EQ(watchdog([] { return false; }), std::nullopt); +} + +TEST_F(TestPlatformUnix, quit_watchdog_quits_on_signal) +{ + auto [mock_signals, guard] = mpt::MockSignalWrapper::inject(); + + EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _)); + EXPECT_CALL(*mock_signals, wait(_, _)) + .WillOnce(DoAll(SetArgReferee<1>(SIGUSR2), Return(0))) + .WillOnce(DoAll(SetArgReferee<1>(SIGTERM), Return(0))); + ON_CALL(*mock_signals, send(pthread_self(), SIGUSR2)).WillByDefault(Return(0)); + + auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1}); + EXPECT_EQ(watchdog([] { return true; }), SIGTERM); +} + +TEST_F(TestPlatformUnix, quit_watchdog_signals_itself_asynchronously) +{ + auto [mock_signals, guard] = mpt::MockSignalWrapper::inject(); + + std::atomic signaled = false; + std::atomic times = 0; + + EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _)); + EXPECT_CALL(*mock_signals, wait(_, _)) + .WillRepeatedly(DoAll( + [&signaled, ×] { + while (!signaled.load(std::memory_order_acquire)) + { + } + times.fetch_add(1, std::memory_order_release); + signaled.store(false, std::memory_order_release); + }, + SetArgReferee<1>(SIGUSR2), + Return(0))); + + EXPECT_CALL(*mock_signals, send(pthread_self(), SIGUSR2)) + .WillRepeatedly(DoAll([&signaled] { signaled.store(true, std::memory_order_release); }, Return(0))); + + auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1}); + EXPECT_EQ(watchdog([×] { return times.load(std::memory_order_acquire) < 10; }), std::nullopt); + EXPECT_GE(times.load(std::memory_order_acquire), 10); +}