From b3677f3d102e73f71c566211f68d3d83d4b9acaa Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 12 Mar 2025 19:37:40 +0300 Subject: [PATCH 1/5] [sshfs-mount-handler] ensure signals are disconnected upon destr. Right now, the signal handlers outlive the process and the mount handler which causes object lifetime related undefined behavior to happen. This caused a crash when I was trying to unmount a sshfs mount in Windows. This patch fixes that by ensuring all connected signals are disconnected in destructor. Signed-off-by: Mustafa Kemal Gilor --- src/sshfs_mount/sshfs_mount_handler.cpp | 86 ++++++++++++++++++++----- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/src/sshfs_mount/sshfs_mount_handler.cpp b/src/sshfs_mount/sshfs_mount_handler.cpp index 6daa8e0c1e..fb79c4f31e 100644 --- a/src/sshfs_mount/sshfs_mount_handler.cpp +++ b/src/sshfs_mount/sshfs_mount_handler.cpp @@ -70,7 +70,8 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session) // Check if multipass-sshfs is already installed if (session.exec("sudo snap list multipass-sshfs").exit_code(std::chrono::seconds(15)) == 0) { - mpl::log(mpl::Level::debug, category, + mpl::log(mpl::Level::debug, + category, fmt::format("The multipass-sshfs snap is already installed on '{}'", name)); return true; } @@ -151,7 +152,8 @@ try } catch (const std::exception& e) { - mpl::log(mpl::Level::warning, category, + mpl::log(mpl::Level::warning, + category, fmt::format("Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what())); return false; } @@ -184,21 +186,29 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise QObject::connect(process.get(), &Process::finished, [this](const ProcessState& exit_state) { if (exit_state.completed_successfully()) { - mpl::log(mpl::Level::info, category, + mpl::log(mpl::Level::info, + category, fmt::format("Mount \"{}\" in instance '{}' has stopped", target, vm->vm_name)); } else { // not error as it failing can indicate we need to install sshfs in the VM - mpl::log(mpl::Level::warning, category, - fmt::format("Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}", target, vm->vm_name, + mpl::log(mpl::Level::warning, + category, + fmt::format("Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}", + target, + vm->vm_name, exit_state.failure_message())); } }); QObject::connect(process.get(), &Process::error_occurred, [this](auto error, auto error_string) { - mpl::log(mpl::Level::error, category, + mpl::log(mpl::Level::error, + category, fmt::format("There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}", - vm->vm_name, target, mpu::qenum_to_string(error), error_string)); + vm->vm_name, + target, + mpu::qenum_to_string(error), + error_string)); }); mpl::log(mpl::Level::info, category, fmt::format("process program '{}'", process->program())); @@ -225,15 +235,30 @@ void SSHFSMountHandler::deactivate_impl(bool force) { mpl::log(mpl::Level::info, category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name)); QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr); - if (process->terminate(); !process->wait_for_finished(5000)) + + try { - auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error()); - if (force) - mpl::log( - mpl::Level::warning, category, - fmt::format("Failed to gracefully stop mount \"{}\" in instance '{}': {}", target, vm->vm_name, err)); - else - throw std::runtime_error{err}; + if (process->terminate(); !process->wait_for_finished(5000)) + { + auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error()); + if (force) + mpl::log(mpl::Level::warning, + category, + fmt::format("Failed to gracefully stop mount \"{}\" in instance '{}': {}", + target, + vm->vm_name, + err)); + else + throw std::runtime_error{err}; + } + } + catch (...) + { + // The finished signal should've triggered by now. We can disconnect it. + // Doing it this way to ensure that it gets disconnected even when the body + // throws. + QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr); + throw; } process.reset(); } @@ -241,5 +266,36 @@ void SSHFSMountHandler::deactivate_impl(bool force) SSHFSMountHandler::~SSHFSMountHandler() { deactivate(/*force=*/true); + + /** + * Ensure that the signals are disconnected. + * + * The `is_active` function has some interesting logic going on, which in turn + * prevents `deactivate_impl` to be run. That means the disconnect for `error_occured` + * does not happen, and it can possibly be triggered while the mount handler & process + * objects are not alive. Sounds fun? I don't think so either. + * + * We're disconnecting the signals here because it should be done as a part + * of the destructor regardless of other stuff. + * + * FIXME: Find a way to associate slot lifetime with the process itself. + */ + if (process) + { + // The disconnect() is a no-op when nothing is connected. + if (QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr)) + { + mpl::warn(category, + "SSHFSMountHandler is going to be destroyed, but Process still has `finished` signal connected. " + "Disconnected it to prevent errors."); + } + if (QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr)) + { + mpl::warn( + category, + "SSHFSMountHandler is going to be destroyed, but Process still has `error_occured` signal connected. " + "Disconnected it to prevent errors."); + } + } } } // namespace multipass From a651bca65488b93d449988f4cdc78d6d64d17909 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Wed, 12 Mar 2025 19:46:03 +0300 Subject: [PATCH 2/5] [sshfs-mount-handler] chore: modernize log calls Signed-off-by: Mustafa Kemal Gilor --- src/sshfs_mount/sshfs_mount_handler.cpp | 74 ++++++++++--------------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/src/sshfs_mount/sshfs_mount_handler.cpp b/src/sshfs_mount/sshfs_mount_handler.cpp index fb79c4f31e..b34aeacb60 100644 --- a/src/sshfs_mount/sshfs_mount_handler.cpp +++ b/src/sshfs_mount/sshfs_mount_handler.cpp @@ -56,7 +56,7 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session) // Check if snap support is installed in the instance if (session.exec("which snap").exit_code() != 0) { - mpl::log(mpl::Level::warning, category, fmt::format("Snap support is not installed in '{}'", name)); + mpl::warn(category, "Snap support is not installed in '{}'", name); throw std::runtime_error( fmt::format("Snap support needs to be installed in '{}' in order to support mounts.\n" "Please see https://docs.snapcraft.io/installing-snapd for information on\n" @@ -70,16 +70,14 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session) // Check if multipass-sshfs is already installed if (session.exec("sudo snap list multipass-sshfs").exit_code(std::chrono::seconds(15)) == 0) { - mpl::log(mpl::Level::debug, - category, - fmt::format("The multipass-sshfs snap is already installed on '{}'", name)); + mpl::debug(category, "The multipass-sshfs snap is already installed on '{}'", name); return true; } // Check if /snap exists for "classic" snap support if (session.exec("[ -e /snap ]").exit_code() != 0) { - mpl::log(mpl::Level::warning, category, fmt::format("Classic snap support symlink is needed in '{}'", name)); + mpl::warn(category, "Classic snap support symlink is needed in '{}'", name); throw std::runtime_error( fmt::format("Classic snap support is not enabled for '{}'!\n\n" "Please see https://docs.snapcraft.io/installing-snapd for information on\n" @@ -93,22 +91,18 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session) void install_sshfs_for(const std::string& name, mp::SSHSession& session, const std::chrono::milliseconds& timeout) try { - mpl::log(mpl::Level::info, category, fmt::format("Installing the multipass-sshfs snap in '{}'", name)); + mpl::info(category, "Installing the multipass-sshfs snap in '{}'", name); auto proc = session.exec("sudo snap install multipass-sshfs"); if (proc.exit_code(timeout) != 0) { auto error_msg = proc.read_std_error(); - mpl::log(mpl::Level::error, - category, - fmt::format("Failed to install 'multipass-sshfs': {}", mpu::trim_end(error_msg))); + mpl::error(category, "Failed to install 'multipass-sshfs': {}", mpu::trim_end(error_msg)); throw mp::SSHFSMissingError(); } } catch (const mp::ExitlessSSHProcessException& e) { - mpl::log(mpl::Level::error, - category, - fmt::format("Could not install 'multipass-sshfs' in '{}': {}", name, e.what())); + mpl::error(category, "Could not install 'multipass-sshfs' in '{}': {}", name, e.what()); throw mp::SSHFSMissingError(); } } // namespace @@ -131,10 +125,7 @@ SSHFSMountHandler::SSHFSMountHandler(VirtualMachine* vm, this->mount_spec.get_gid_mappings(), this->mount_spec.get_uid_mappings()} { - mpl::log( - mpl::Level::info, - category, - fmt::format("initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name)); + mpl::info(category, "initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name); } bool SSHFSMountHandler::is_active() @@ -152,9 +143,7 @@ try } catch (const std::exception& e) { - mpl::log(mpl::Level::warning, - category, - fmt::format("Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what())); + mpl::warn(category, "Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what()); return false; } @@ -186,33 +175,29 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise QObject::connect(process.get(), &Process::finished, [this](const ProcessState& exit_state) { if (exit_state.completed_successfully()) { - mpl::log(mpl::Level::info, - category, - fmt::format("Mount \"{}\" in instance '{}' has stopped", target, vm->vm_name)); + mpl::info(category, "Mount \"{}\" in instance '{}' has stopped", target, vm->vm_name); } else { // not error as it failing can indicate we need to install sshfs in the VM - mpl::log(mpl::Level::warning, - category, - fmt::format("Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}", - target, - vm->vm_name, - exit_state.failure_message())); + mpl::warn(category, + "Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}", + target, + vm->vm_name, + exit_state.failure_message()); } }); QObject::connect(process.get(), &Process::error_occurred, [this](auto error, auto error_string) { - mpl::log(mpl::Level::error, - category, - fmt::format("There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}", - vm->vm_name, - target, - mpu::qenum_to_string(error), - error_string)); + mpl::error(category, + "There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}", + vm->vm_name, + target, + mpu::qenum_to_string(error), + error_string); }); - mpl::log(mpl::Level::info, category, fmt::format("process program '{}'", process->program())); - mpl::log(mpl::Level::info, category, fmt::format("process arguments '{}'", process->arguments().join(", "))); + mpl::info(category, "process program '{}'", process->program()); + mpl::info(category, "process arguments '{}'", process->arguments().join(", ")); start_and_block_until_connected(process.get()); // after the process is started, it must be moved to the main thread @@ -233,7 +218,7 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise void SSHFSMountHandler::deactivate_impl(bool force) { - mpl::log(mpl::Level::info, category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name)); + mpl::info(category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name)); QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr); try @@ -242,12 +227,11 @@ void SSHFSMountHandler::deactivate_impl(bool force) { auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error()); if (force) - mpl::log(mpl::Level::warning, - category, - fmt::format("Failed to gracefully stop mount \"{}\" in instance '{}': {}", - target, - vm->vm_name, - err)); + mpl::warn(category, + "Failed to gracefully stop mount \"{}\" in instance '{}': {}", + target, + vm->vm_name, + err); else throw std::runtime_error{err}; } @@ -274,7 +258,7 @@ SSHFSMountHandler::~SSHFSMountHandler() * prevents `deactivate_impl` to be run. That means the disconnect for `error_occured` * does not happen, and it can possibly be triggered while the mount handler & process * objects are not alive. Sounds fun? I don't think so either. - * + * * We're disconnecting the signals here because it should be done as a part * of the destructor regardless of other stuff. * From 69b228e23cd071adcc4fec8147f6d2aec26e7f38 Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Tue, 18 Mar 2025 13:54:42 +0300 Subject: [PATCH 3/5] [sshfs-mount-handler] review fixes - wording and typing changes Signed-off-by: Mustafa Kemal Gilor --- src/sshfs_mount/sshfs_mount_handler.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/sshfs_mount/sshfs_mount_handler.cpp b/src/sshfs_mount/sshfs_mount_handler.cpp index b34aeacb60..41b8d6b2a2 100644 --- a/src/sshfs_mount/sshfs_mount_handler.cpp +++ b/src/sshfs_mount/sshfs_mount_handler.cpp @@ -238,9 +238,7 @@ void SSHFSMountHandler::deactivate_impl(bool force) } catch (...) { - // The finished signal should've triggered by now. We can disconnect it. - // Doing it this way to ensure that it gets disconnected even when the body - // throws. + // Give up waiting for the `finished` signal before the process is destroyed. QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr); throw; } @@ -251,7 +249,7 @@ SSHFSMountHandler::~SSHFSMountHandler() { deactivate(/*force=*/true); - /** + /* * Ensure that the signals are disconnected. * * The `is_active` function has some interesting logic going on, which in turn @@ -266,19 +264,16 @@ SSHFSMountHandler::~SSHFSMountHandler() */ if (process) { + constexpr std::string_view warn_msg_fmtstr = + "Stopped listening to sshfs_server process only upon SSHFSMountHandler destruction"; // The disconnect() is a no-op when nothing is connected. if (QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr)) { - mpl::warn(category, - "SSHFSMountHandler is going to be destroyed, but Process still has `finished` signal connected. " - "Disconnected it to prevent errors."); + mpl::warn(category, warn_msg_fmtstr); } if (QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr)) { - mpl::warn( - category, - "SSHFSMountHandler is going to be destroyed, but Process still has `error_occured` signal connected. " - "Disconnected it to prevent errors."); + mpl::warn(category, warn_msg_fmtstr); } } } From 0f8665d7a2a0c264b980f1e718182c073faa3dec Mon Sep 17 00:00:00 2001 From: Mustafa Kemal Gilor Date: Fri, 21 Mar 2025 13:42:34 +0300 Subject: [PATCH 4/5] [qt_delete_later/sshfs_mount_handler] ensure signal disconnection - Moved the disconnect() to the QtDeleteLater custom deleter. - Removed SSHFSMountHandler::is_active override - Removed a redundant process.reset() - SSHFSMountHandler::deactivate_impl will now kill() when force==true Signed-off-by: Mustafa Kemal Gilor --- .../multipass/qt_delete_later_unique_ptr.h | 5 + .../sshfs_mount/sshfs_mount_handler.h | 1 - src/platform/update/new_release_monitor.cpp | 6 ++ src/sshfs_mount/sshfs_mount_handler.cpp | 101 ++++++------------ tests/test_sshfs_mount_handler.cpp | 6 +- 5 files changed, 48 insertions(+), 71 deletions(-) diff --git a/include/multipass/qt_delete_later_unique_ptr.h b/include/multipass/qt_delete_later_unique_ptr.h index 7ea5d7a58b..58d5d09c8d 100644 --- a/include/multipass/qt_delete_later_unique_ptr.h +++ b/include/multipass/qt_delete_later_unique_ptr.h @@ -31,6 +31,11 @@ namespace multipass struct QtDeleteLater { void operator()(QObject *o) { + // Detach the signal handlers since this object don't live in a QT object + // hierarchy, and that might cause lifetime related issues, especially + // if signals are involved. + o->disconnect(); + // Qt requires that a QObject be deleted from the thread it lives in. o->deleteLater(); } }; diff --git a/include/multipass/sshfs_mount/sshfs_mount_handler.h b/include/multipass/sshfs_mount/sshfs_mount_handler.h index fe483b62e6..aa1dff4b69 100644 --- a/include/multipass/sshfs_mount/sshfs_mount_handler.h +++ b/include/multipass/sshfs_mount/sshfs_mount_handler.h @@ -36,7 +36,6 @@ class SSHFSMountHandler : public MountHandler void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override; void deactivate_impl(bool force) override; - bool is_active() override; private: qt_delete_later_unique_ptr process; diff --git a/src/platform/update/new_release_monitor.cpp b/src/platform/update/new_release_monitor.cpp index 46542d49cf..d36db97645 100644 --- a/src/platform/update/new_release_monitor.cpp +++ b/src/platform/update/new_release_monitor.cpp @@ -149,6 +149,12 @@ void mp::NewReleaseMonitor::check_for_new_release() worker_thread.reset(new mp::LatestReleaseChecker(update_url)); connect(worker_thread.get(), &mp::LatestReleaseChecker::latest_release_found, this, &mp::NewReleaseMonitor::latest_release_found); + // ATTN: The worker_thread is a qt_delete_later_unique_ptr, and the deleter will invoke + // disconnect() method upon calling. This is safe to do for this instance because, it's + // defined behavior to call disconnect in a signal handler itself. But, it is not without + // any quirks. The following signal deliveries might not be triggered if the disconnect happens + // in a signal handler. In this particular case, there are none, but be wary of it future travelers. + // FIXME: New release monitor code can be much simpler, needs a refactor. connect(worker_thread.get(), &QThread::finished, this, [this]() { worker_thread.reset(); }); worker_thread->start(); } diff --git a/src/sshfs_mount/sshfs_mount_handler.cpp b/src/sshfs_mount/sshfs_mount_handler.cpp index 41b8d6b2a2..11ce6e1c66 100644 --- a/src/sshfs_mount/sshfs_mount_handler.cpp +++ b/src/sshfs_mount/sshfs_mount_handler.cpp @@ -128,25 +128,6 @@ SSHFSMountHandler::SSHFSMountHandler(VirtualMachine* vm, mpl::info(category, "initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name); } -bool SSHFSMountHandler::is_active() -try -{ - if (!active || !process || !process->running()) - return false; - - SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *ssh_key_provider}; - - const auto resolved_target = mp::utils::get_resolved_target(session, target); - - return !session.exec(fmt::format("findmnt --type fuse.sshfs | grep -E '^{} +:{}'", resolved_target, source)) - .exit_code(); -} -catch (const std::exception& e) -{ - mpl::warn(category, "Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what()); - return false; -} - void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::milliseconds timeout) { SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *ssh_key_provider}; @@ -168,8 +149,6 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise config.host = vm->ssh_hostname(); config.port = vm->ssh_port(); - if (process) - process.reset(); process.reset(platform::make_sshfs_server_process(config).release()); QObject::connect(process.get(), &Process::finished, [this](const ProcessState& exit_state) { @@ -206,9 +185,10 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise // when stopping the mount from the main thread again, qt will try to send an event from the main thread to the one // in which the process lives this will result in an error since qt can't send events from one thread to another process->moveToThread(QCoreApplication::instance()->thread()); + // So, for any future travelers, this^ is the main reason why we use qt_delete_later_unique_ptr thingy. // Check in case sshfs_server stopped, usually due to an error - auto process_state = process->process_state(); + const auto process_state = process->process_state(); if (process_state.exit_code == 9) // Magic number returned by sshfs_server throw SSHFSMissingError(); else if (process_state.exit_code || process_state.error) @@ -221,60 +201,47 @@ void SSHFSMountHandler::deactivate_impl(bool force) mpl::info(category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name)); QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr); - try + constexpr auto kProcessWaitTimeout = std::chrono::milliseconds{5000}; + if (process->terminate(); !process->wait_for_finished(kProcessWaitTimeout.count())) { - if (process->terminate(); !process->wait_for_finished(5000)) + auto fetch_stderr = [](Process& process) { + return fmt::format("Failed to terminate SSHFS mount process gracefully: {}", + process.read_all_standard_error()); + }; + + const auto err = fetch_stderr(*process.get()); + + if (force) { - auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error()); - if (force) - mpl::warn(category, - "Failed to gracefully stop mount \"{}\" in instance '{}': {}", - target, - vm->vm_name, - err); - else - throw std::runtime_error{err}; + mpl::warn(category, + "Failed to gracefully stop mount \"{}\" in instance '{}': {}, trying to stop it forcefully.", + target, + vm->vm_name, + err); + /** + * Let's try brute force this time. + */ + process->kill(); + const auto result = process->wait_for_finished(kProcessWaitTimeout.count()); + + mpl::warn(category, + "{} to forcefully stop mount \"{}\" in instance '{}': {}", + result ? "Succeeded" : "Failed", + target, + vm->vm_name, + result ? "" : fetch_stderr(*process.get())); } + else + throw std::runtime_error{err}; } - catch (...) - { - // Give up waiting for the `finished` signal before the process is destroyed. - QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr); - throw; - } + + // Finally, call reset() to disconnect all the signals and defer the deletion + // to the owning thread, in this case it's the QT main. process.reset(); } SSHFSMountHandler::~SSHFSMountHandler() { deactivate(/*force=*/true); - - /* - * Ensure that the signals are disconnected. - * - * The `is_active` function has some interesting logic going on, which in turn - * prevents `deactivate_impl` to be run. That means the disconnect for `error_occured` - * does not happen, and it can possibly be triggered while the mount handler & process - * objects are not alive. Sounds fun? I don't think so either. - * - * We're disconnecting the signals here because it should be done as a part - * of the destructor regardless of other stuff. - * - * FIXME: Find a way to associate slot lifetime with the process itself. - */ - if (process) - { - constexpr std::string_view warn_msg_fmtstr = - "Stopped listening to sshfs_server process only upon SSHFSMountHandler destruction"; - // The disconnect() is a no-op when nothing is connected. - if (QObject::disconnect(process.get(), &Process::finished, nullptr, nullptr)) - { - mpl::warn(category, warn_msg_fmtstr); - } - if (QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr)) - { - mpl::warn(category, warn_msg_fmtstr); - } - } } } // namespace multipass diff --git a/tests/test_sshfs_mount_handler.cpp b/tests/test_sshfs_mount_handler.cpp index 3e7fd5840e..da3123d83e 100644 --- a/tests/test_sshfs_mount_handler.cpp +++ b/tests/test_sshfs_mount_handler.cpp @@ -120,9 +120,9 @@ TEST_F(SSHFSMountHandlerTest, mount_creates_sshfs_process) factory->register_callback(sshfs_server_callback(sshfs_prints_connected)); mpt::MockVirtualMachine mock_vm{"my_instance"}; - EXPECT_CALL(mock_vm, ssh_port()).Times(3); - EXPECT_CALL(mock_vm, ssh_hostname()).Times(3); - EXPECT_CALL(mock_vm, ssh_username()).Times(3); + EXPECT_CALL(mock_vm, ssh_port()).Times(2); + EXPECT_CALL(mock_vm, ssh_hostname()).Times(2); + EXPECT_CALL(mock_vm, ssh_username()).Times(2); mp::SSHFSMountHandler sshfs_mount_handler{&mock_vm, &key_provider, target_path, mount}; sshfs_mount_handler.activate(&server); From c051cb18a25fad41ce5c4498839aefcff3c784e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Kemal=20G=C4=B1lor?= Date: Wed, 26 Mar 2025 18:03:38 +0300 Subject: [PATCH 5/5] Update include/multipass/qt_delete_later_unique_ptr.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trevor-shoe Signed-off-by: Mustafa Kemal Gılor --- include/multipass/qt_delete_later_unique_ptr.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/multipass/qt_delete_later_unique_ptr.h b/include/multipass/qt_delete_later_unique_ptr.h index 58d5d09c8d..8bab3ddb72 100644 --- a/include/multipass/qt_delete_later_unique_ptr.h +++ b/include/multipass/qt_delete_later_unique_ptr.h @@ -31,7 +31,7 @@ namespace multipass struct QtDeleteLater { void operator()(QObject *o) { - // Detach the signal handlers since this object don't live in a QT object + // Detach the signal handlers since this object doesn't live in a QT object // hierarchy, and that might cause lifetime related issues, especially // if signals are involved. o->disconnect();