Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions include/multipass/qt_delete_later_unique_ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};
Expand Down
1 change: 0 additions & 1 deletion include/multipass/sshfs_mount/sshfs_mount_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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> process;
Expand Down
6 changes: 6 additions & 0 deletions src/platform/update/new_release_monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
110 changes: 56 additions & 54 deletions src/sshfs_mount/sshfs_mount_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
// 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"
Expand All @@ -70,15 +70,14 @@
// 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"
Expand All @@ -92,22 +91,18 @@
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
Expand All @@ -130,30 +125,7 @@
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));
}

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::log(mpl::Level::warning, category,
fmt::format("Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what()));
return false;
mpl::info(category, "initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name);
}

void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::milliseconds timeout)
Expand All @@ -177,32 +149,34 @@
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) {
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,

Check warning on line 170 in src/sshfs_mount/sshfs_mount_handler.cpp

View check run for this annotation

Codecov / codecov/patch

src/sshfs_mount/sshfs_mount_handler.cpp#L170

Added line #L170 was not covered by tests
"There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}",
vm->vm_name,
target,

Check warning on line 173 in src/sshfs_mount/sshfs_mount_handler.cpp

View check run for this annotation

Codecov / codecov/patch

src/sshfs_mount/sshfs_mount_handler.cpp#L172-L173

Added lines #L172 - L173 were not covered by tests
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
Expand All @@ -211,9 +185,10 @@
// 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)
Expand All @@ -223,18 +198,45 @@

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);
if (process->terminate(); !process->wait_for_finished(5000))

constexpr auto kProcessWaitTimeout = std::chrono::milliseconds{5000};
if (process->terminate(); !process->wait_for_finished(kProcessWaitTimeout.count()))
{
auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error());
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)
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 '{}': {}, 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};
}

// 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();
}

Expand Down
6 changes: 3 additions & 3 deletions tests/test_sshfs_mount_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member Author

@xmkg xmkg Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because is_active no longer creates an SSHSession.

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);
Expand Down
Loading