Skip to content

Commit e932d5f

Browse files
authored
Merge pull request #3978 from canonical/bugfix/sshfs-mount-handler-ensure-signal-disconnect
[sshfs-mount-handler] ensure signals are disconnected upon destruction
2 parents 261158c + c051cb1 commit e932d5f

File tree

5 files changed

+70
-58
lines changed

5 files changed

+70
-58
lines changed

include/multipass/qt_delete_later_unique_ptr.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ namespace multipass
3131

3232
struct QtDeleteLater {
3333
void operator()(QObject *o) {
34+
// Detach the signal handlers since this object doesn't live in a QT object
35+
// hierarchy, and that might cause lifetime related issues, especially
36+
// if signals are involved.
37+
o->disconnect();
38+
// Qt requires that a QObject be deleted from the thread it lives in.
3439
o->deleteLater();
3540
}
3641
};

include/multipass/sshfs_mount/sshfs_mount_handler.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ class SSHFSMountHandler : public MountHandler
3636

3737
void activate_impl(ServerVariant server, std::chrono::milliseconds timeout) override;
3838
void deactivate_impl(bool force) override;
39-
bool is_active() override;
4039

4140
private:
4241
qt_delete_later_unique_ptr<Process> process;

src/platform/update/new_release_monitor.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ void mp::NewReleaseMonitor::check_for_new_release()
149149
worker_thread.reset(new mp::LatestReleaseChecker(update_url));
150150
connect(worker_thread.get(), &mp::LatestReleaseChecker::latest_release_found, this,
151151
&mp::NewReleaseMonitor::latest_release_found);
152+
// ATTN: The worker_thread is a qt_delete_later_unique_ptr, and the deleter will invoke
153+
// disconnect() method upon calling. This is safe to do for this instance because, it's
154+
// defined behavior to call disconnect in a signal handler itself. But, it is not without
155+
// any quirks. The following signal deliveries might not be triggered if the disconnect happens
156+
// in a signal handler. In this particular case, there are none, but be wary of it future travelers.
157+
// FIXME: New release monitor code can be much simpler, needs a refactor.
152158
connect(worker_thread.get(), &QThread::finished, this, [this]() { worker_thread.reset(); });
153159
worker_thread->start();
154160
}

src/sshfs_mount/sshfs_mount_handler.cpp

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session)
5656
// Check if snap support is installed in the instance
5757
if (session.exec("which snap").exit_code() != 0)
5858
{
59-
mpl::log(mpl::Level::warning, category, fmt::format("Snap support is not installed in '{}'", name));
59+
mpl::warn(category, "Snap support is not installed in '{}'", name);
6060
throw std::runtime_error(
6161
fmt::format("Snap support needs to be installed in '{}' in order to support mounts.\n"
6262
"Please see https://docs.snapcraft.io/installing-snapd for information on\n"
@@ -70,15 +70,14 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session)
7070
// Check if multipass-sshfs is already installed
7171
if (session.exec("sudo snap list multipass-sshfs").exit_code(std::chrono::seconds(15)) == 0)
7272
{
73-
mpl::log(mpl::Level::debug, category,
74-
fmt::format("The multipass-sshfs snap is already installed on '{}'", name));
73+
mpl::debug(category, "The multipass-sshfs snap is already installed on '{}'", name);
7574
return true;
7675
}
7776

7877
// Check if /snap exists for "classic" snap support
7978
if (session.exec("[ -e /snap ]").exit_code() != 0)
8079
{
81-
mpl::log(mpl::Level::warning, category, fmt::format("Classic snap support symlink is needed in '{}'", name));
80+
mpl::warn(category, "Classic snap support symlink is needed in '{}'", name);
8281
throw std::runtime_error(
8382
fmt::format("Classic snap support is not enabled for '{}'!\n\n"
8483
"Please see https://docs.snapcraft.io/installing-snapd for information on\n"
@@ -92,22 +91,18 @@ bool has_sshfs(const std::string& name, mp::SSHSession& session)
9291
void install_sshfs_for(const std::string& name, mp::SSHSession& session, const std::chrono::milliseconds& timeout)
9392
try
9493
{
95-
mpl::log(mpl::Level::info, category, fmt::format("Installing the multipass-sshfs snap in '{}'", name));
94+
mpl::info(category, "Installing the multipass-sshfs snap in '{}'", name);
9695
auto proc = session.exec("sudo snap install multipass-sshfs");
9796
if (proc.exit_code(timeout) != 0)
9897
{
9998
auto error_msg = proc.read_std_error();
100-
mpl::log(mpl::Level::error,
101-
category,
102-
fmt::format("Failed to install 'multipass-sshfs': {}", mpu::trim_end(error_msg)));
99+
mpl::error(category, "Failed to install 'multipass-sshfs': {}", mpu::trim_end(error_msg));
103100
throw mp::SSHFSMissingError();
104101
}
105102
}
106103
catch (const mp::ExitlessSSHProcessException& e)
107104
{
108-
mpl::log(mpl::Level::error,
109-
category,
110-
fmt::format("Could not install 'multipass-sshfs' in '{}': {}", name, e.what()));
105+
mpl::error(category, "Could not install 'multipass-sshfs' in '{}': {}", name, e.what());
111106
throw mp::SSHFSMissingError();
112107
}
113108
} // namespace
@@ -130,30 +125,7 @@ SSHFSMountHandler::SSHFSMountHandler(VirtualMachine* vm,
130125
this->mount_spec.get_gid_mappings(),
131126
this->mount_spec.get_uid_mappings()}
132127
{
133-
mpl::log(
134-
mpl::Level::info,
135-
category,
136-
fmt::format("initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name));
137-
}
138-
139-
bool SSHFSMountHandler::is_active()
140-
try
141-
{
142-
if (!active || !process || !process->running())
143-
return false;
144-
145-
SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm->ssh_username(), *ssh_key_provider};
146-
147-
const auto resolved_target = mp::utils::get_resolved_target(session, target);
148-
149-
return !session.exec(fmt::format("findmnt --type fuse.sshfs | grep -E '^{} +:{}'", resolved_target, source))
150-
.exit_code();
151-
}
152-
catch (const std::exception& e)
153-
{
154-
mpl::log(mpl::Level::warning, category,
155-
fmt::format("Failed checking SSHFS mount \"{}\" in instance '{}': {}", target, vm->vm_name, e.what()));
156-
return false;
128+
mpl::info(category, "initializing mount {} => {} in '{}'", this->mount_spec.get_source_path(), target, vm->vm_name);
157129
}
158130

159131
void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::milliseconds timeout)
@@ -177,32 +149,34 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise
177149
config.host = vm->ssh_hostname();
178150
config.port = vm->ssh_port();
179151

180-
if (process)
181-
process.reset();
182152
process.reset(platform::make_sshfs_server_process(config).release());
183153

184154
QObject::connect(process.get(), &Process::finished, [this](const ProcessState& exit_state) {
185155
if (exit_state.completed_successfully())
186156
{
187-
mpl::log(mpl::Level::info, category,
188-
fmt::format("Mount \"{}\" in instance '{}' has stopped", target, vm->vm_name));
157+
mpl::info(category, "Mount \"{}\" in instance '{}' has stopped", target, vm->vm_name);
189158
}
190159
else
191160
{
192161
// not error as it failing can indicate we need to install sshfs in the VM
193-
mpl::log(mpl::Level::warning, category,
194-
fmt::format("Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}", target, vm->vm_name,
195-
exit_state.failure_message()));
162+
mpl::warn(category,
163+
"Mount \"{}\" in instance '{}' has stopped unsuccessfully: {}",
164+
target,
165+
vm->vm_name,
166+
exit_state.failure_message());
196167
}
197168
});
198169
QObject::connect(process.get(), &Process::error_occurred, [this](auto error, auto error_string) {
199-
mpl::log(mpl::Level::error, category,
200-
fmt::format("There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}",
201-
vm->vm_name, target, mpu::qenum_to_string(error), error_string));
170+
mpl::error(category,
171+
"There was an error with sshfs_server for instance '{}' with path \"{}\": {} - {}",
172+
vm->vm_name,
173+
target,
174+
mpu::qenum_to_string(error),
175+
error_string);
202176
});
203177

204-
mpl::log(mpl::Level::info, category, fmt::format("process program '{}'", process->program()));
205-
mpl::log(mpl::Level::info, category, fmt::format("process arguments '{}'", process->arguments().join(", ")));
178+
mpl::info(category, "process program '{}'", process->program());
179+
mpl::info(category, "process arguments '{}'", process->arguments().join(", "));
206180

207181
start_and_block_until_connected(process.get());
208182
// after the process is started, it must be moved to the main thread
@@ -211,9 +185,10 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise
211185
// when stopping the mount from the main thread again, qt will try to send an event from the main thread to the one
212186
// in which the process lives this will result in an error since qt can't send events from one thread to another
213187
process->moveToThread(QCoreApplication::instance()->thread());
188+
// So, for any future travelers, this^ is the main reason why we use qt_delete_later_unique_ptr thingy.
214189

215190
// Check in case sshfs_server stopped, usually due to an error
216-
auto process_state = process->process_state();
191+
const auto process_state = process->process_state();
217192
if (process_state.exit_code == 9) // Magic number returned by sshfs_server
218193
throw SSHFSMissingError();
219194
else if (process_state.exit_code || process_state.error)
@@ -223,18 +198,45 @@ void SSHFSMountHandler::activate_impl(ServerVariant server, std::chrono::millise
223198

224199
void SSHFSMountHandler::deactivate_impl(bool force)
225200
{
226-
mpl::log(mpl::Level::info, category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name));
201+
mpl::info(category, fmt::format("Stopping mount \"{}\" in instance '{}'", target, vm->vm_name));
227202
QObject::disconnect(process.get(), &Process::error_occurred, nullptr, nullptr);
228-
if (process->terminate(); !process->wait_for_finished(5000))
203+
204+
constexpr auto kProcessWaitTimeout = std::chrono::milliseconds{5000};
205+
if (process->terminate(); !process->wait_for_finished(kProcessWaitTimeout.count()))
229206
{
230-
auto err = fmt::format("Failed to terminate SSHFS mount process: {}", process->read_all_standard_error());
207+
auto fetch_stderr = [](Process& process) {
208+
return fmt::format("Failed to terminate SSHFS mount process gracefully: {}",
209+
process.read_all_standard_error());
210+
};
211+
212+
const auto err = fetch_stderr(*process.get());
213+
231214
if (force)
232-
mpl::log(
233-
mpl::Level::warning, category,
234-
fmt::format("Failed to gracefully stop mount \"{}\" in instance '{}': {}", target, vm->vm_name, err));
215+
{
216+
mpl::warn(category,
217+
"Failed to gracefully stop mount \"{}\" in instance '{}': {}, trying to stop it forcefully.",
218+
target,
219+
vm->vm_name,
220+
err);
221+
/**
222+
* Let's try brute force this time.
223+
*/
224+
process->kill();
225+
const auto result = process->wait_for_finished(kProcessWaitTimeout.count());
226+
227+
mpl::warn(category,
228+
"{} to forcefully stop mount \"{}\" in instance '{}': {}",
229+
result ? "Succeeded" : "Failed",
230+
target,
231+
vm->vm_name,
232+
result ? "" : fetch_stderr(*process.get()));
233+
}
235234
else
236235
throw std::runtime_error{err};
237236
}
237+
238+
// Finally, call reset() to disconnect all the signals and defer the deletion
239+
// to the owning thread, in this case it's the QT main.
238240
process.reset();
239241
}
240242

tests/test_sshfs_mount_handler.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ TEST_F(SSHFSMountHandlerTest, mount_creates_sshfs_process)
120120
factory->register_callback(sshfs_server_callback(sshfs_prints_connected));
121121

122122
mpt::MockVirtualMachine mock_vm{"my_instance"};
123-
EXPECT_CALL(mock_vm, ssh_port()).Times(3);
124-
EXPECT_CALL(mock_vm, ssh_hostname()).Times(3);
125-
EXPECT_CALL(mock_vm, ssh_username()).Times(3);
123+
EXPECT_CALL(mock_vm, ssh_port()).Times(2);
124+
EXPECT_CALL(mock_vm, ssh_hostname()).Times(2);
125+
EXPECT_CALL(mock_vm, ssh_username()).Times(2);
126126

127127
mp::SSHFSMountHandler sshfs_mount_handler{&mock_vm, &key_provider, target_path, mount};
128128
sshfs_mount_handler.activate(&server);

0 commit comments

Comments
 (0)