// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/crostini/crostini_sshfs.h"
#include <inttypes.h>
#include <memory>
#include <utility>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_manager_factory.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/guest_os/guest_os_session_tracker.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/dbus/cros_disks/cros_disks_client.h"
#include "content/public/browser/browser_thread.h"
#include "storage/browser/file_system/external_mount_points.h"
namespace crostini {
CrostiniSshfs::CrostiniSshfs(Profile* profile) : profile_(profile) {}
CrostiniSshfs::~CrostiniSshfs() = default;
void CrostiniSshfs::OnContainerShutdown(const guest_os::GuestId& container_id) {
container_shutdown_observer_.Reset();
SetSshfsMounted(container_id, false);
}
bool CrostiniSshfs::IsSshfsMounted(const guest_os::GuestId& container) {
return (sshfs_mounted_.count(container));
}
void CrostiniSshfs::SetSshfsMounted(const guest_os::GuestId& container,
bool mounted) {
if (mounted) {
sshfs_mounted_.emplace(container);
} else {
sshfs_mounted_.erase(container);
}
}
void CrostiniSshfs::UnmountCrostiniFiles(const guest_os::GuestId& container_id,
MountCrostiniFilesCallback callback) {
// TODO(crbug.com/40760488): Unmounting should cancel an in-progress mount.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* vmgr = file_manager::VolumeManager::Get(profile_);
if (vmgr) {
// vmgr is NULL in unit tests if not overridden.
vmgr->RemoveSshfsCrostiniVolume(
file_manager::util::GetCrostiniMountDirectory(profile_),
base::BindOnce(&CrostiniSshfs::OnRemoveSshfsCrostiniVolume,
weak_ptr_factory_.GetWeakPtr(), container_id,
std::move(callback), base::Time::Now()));
} else {
OnRemoveSshfsCrostiniVolume(container_id, std::move(callback),
base::Time::Now(), true);
}
}
void CrostiniSshfs::OnRemoveSshfsCrostiniVolume(
const guest_os::GuestId& container_id,
MountCrostiniFilesCallback callback,
base::Time started,
bool success) {
container_shutdown_observer_.Reset();
SetSshfsMounted(container_id, false);
base::UmaHistogramTimes("Crostini.Sshfs.Unmount.TimeTaken",
base::Time::Now() - started);
base::UmaHistogramBoolean("Crostini.Sshfs.Unmount.Result", success);
std::move(callback).Run(success);
}
void CrostiniSshfs::MountCrostiniFiles(const guest_os::GuestId& container_id,
MountCrostiniFilesCallback callback,
bool background) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (in_progress_mount_) {
// A run is already in progress, wait until it finishes.
pending_requests_.emplace(container_id, std::move(callback), background);
return;
}
in_progress_mount_ = std::make_unique<InProgressMount>(
container_id, std::move(callback), background);
if (IsSshfsMounted(container_id)) {
// Already mounted so skip straight to reporting success.
Finish(CrostiniSshfsResult::kSuccess);
return;
}
if (container_id != DefaultContainerId()) {
LOG(ERROR) << "Unable to mount files for non-default container";
Finish(CrostiniSshfsResult::kNotDefaultContainer);
return;
}
bool running =
guest_os::GuestOsSessionTracker::GetForProfile(profile_)->IsRunning(
in_progress_mount_->container_id);
if (!running) {
LOG(ERROR) << "Unable to mount files for a container that's not running";
Finish(CrostiniSshfsResult::kContainerNotRunning);
return;
}
auto info = guest_os::GuestOsSessionTracker::GetForProfile(profile_)->GetInfo(
in_progress_mount_->container_id);
if (!info) {
LOG(ERROR) << "Got ssh keys for a container that's not running. Aborting.";
Finish(CrostiniSshfsResult::kGetContainerInfoFailed);
return;
}
// Add ourselves as an observer so we can continue once the path is mounted.
auto* dmgr = ash::disks::DiskMountManager::GetInstance();
if (info->sftp_vsock_port != 0) {
in_progress_mount_->source_path = base::StringPrintf(
"sftp://%" PRId64 ":%u", info->cid, info->sftp_vsock_port);
} else {
LOG(ERROR) << "Container has no sftp vsock port";
Finish(CrostiniSshfsResult::kGetContainerInfoFailed);
return;
}
in_progress_mount_->container_homedir = info->homedir;
dmgr->MountPath(in_progress_mount_->source_path, "",
file_manager::util::GetCrostiniMountPointName(profile_), {},
ash::MountType::kNetworkStorage,
ash::MountAccessMode::kReadWrite,
base::BindOnce(&CrostiniSshfs::OnMountEvent,
weak_ptr_factory_.GetWeakPtr()));
}
void CrostiniSshfs::OnMountEvent(
ash::MountError error_code,
const ash::disks::DiskMountManager::MountPoint& mount_info) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (error_code != ash::MountError::kSuccess) {
LOG(ERROR) << "Error mounting crostini container: error_code=" << error_code
<< ", source_path=" << mount_info.source_path
<< ", mount_path=" << mount_info.mount_path
<< ", mount_type=" << mount_info.mount_type
<< ", mount_error=" << mount_info.mount_error;
switch (error_code) {
case ash::MountError::kInternalError:
Finish(CrostiniSshfsResult::kMountErrorInternal);
return;
case ash::MountError::kMountProgramFailed:
Finish(CrostiniSshfsResult::kMountErrorProgramFailed);
return;
default:
Finish(CrostiniSshfsResult::kMountErrorOther);
return;
}
}
base::FilePath mount_path = base::FilePath(mount_info.mount_path);
if (!storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
file_manager::util::GetCrostiniMountPointName(profile_),
storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
mount_path)) {
// We don't revoke the filesystem on unmount and this call fails if a
// filesystem of the same name already exists, so ignore errors.
// TODO(crbug.com/40760488): Should we revoke? Keeping it this way for now
// since that's how it's been for years and it's not come up as an issue
// before. Since the most common reason for unmounting is to work around an
// issue with suspend/resume where we promptly remount it's probably good
// this way.
}
auto* vmgr = file_manager::VolumeManager::Get(profile_);
if (vmgr) {
// vmgr is NULL in unit tests if not overridden.
vmgr->AddSshfsCrostiniVolume(mount_path,
in_progress_mount_->container_homedir);
}
auto* manager = CrostiniManagerFactory::GetForProfile(profile_);
container_shutdown_observer_.Observe(manager);
SetSshfsMounted(in_progress_mount_->container_id, true);
Finish(CrostiniSshfsResult::kSuccess);
}
void CrostiniSshfs::Finish(CrostiniSshfsResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(in_progress_mount_);
auto callback = std::move(in_progress_mount_->callback);
base::UmaHistogramTimes("Crostini.Sshfs.Mount.TimeTaken",
base::Time::Now() - in_progress_mount_->started);
if (in_progress_mount_->background) {
base::UmaHistogramEnumeration("Crostini.Sshfs.Mount.Result.Background",
result);
} else {
base::UmaHistogramEnumeration("Crostini.Sshfs.Mount.Result.UserVisible",
result);
}
std::move(callback).Run(result == CrostiniSshfsResult::kSuccess);
in_progress_mount_.reset();
if (!pending_requests_.empty()) {
auto next = std::move(pending_requests_.front());
pending_requests_.pop();
MountCrostiniFiles(next.container_id, std::move(next.callback),
next.background);
}
}
CrostiniSshfs::InProgressMount::InProgressMount(
const guest_os::GuestId& container,
MountCrostiniFilesCallback callback,
bool background)
: container_id(container),
callback(std::move(callback)),
started(base::Time::Now()),
background(background) {}
CrostiniSshfs::InProgressMount::InProgressMount(
InProgressMount&& other) noexcept = default;
CrostiniSshfs::InProgressMount& CrostiniSshfs::InProgressMount::operator=(
CrostiniSshfs::InProgressMount&& other) noexcept = default;
CrostiniSshfs::InProgressMount::~InProgressMount() = default;
CrostiniSshfs::PendingRequest::PendingRequest(
const guest_os::GuestId& container_id,
MountCrostiniFilesCallback callback,
bool background)
: container_id(container_id),
callback(std::move(callback)),
background(background) {}
CrostiniSshfs::PendingRequest::PendingRequest(PendingRequest&& other) noexcept =
default;
CrostiniSshfs::PendingRequest& CrostiniSshfs::PendingRequest::operator=(
CrostiniSshfs::PendingRequest&& other) noexcept = default;
CrostiniSshfs::PendingRequest::~PendingRequest() = default;
} // namespace crostini