| // Copyright 2018 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <arpa/inet.h> | |
| #include <signal.h> | |
| #include <sys/signalfd.h> | |
| #include <sys/socket.h> | |
| #include <linux/vm_sockets.h> // Needs to come after sys/socket.h | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <map> | |
| #include <set> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/check.h> | |
| #include <base/check_op.h> | |
| #include <base/files/file_enumerator.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/functional/bind.h> | |
| #include <base/location.h> | |
| #include <base/logging.h> | |
| #include <base/memory/ptr_util.h> | |
| #include <base/strings/string_util.h> | |
| #include <base/strings/stringprintf.h> | |
| #include <base/system/sys_info.h> | |
| #include <base/task/single_thread_task_runner.h> | |
| #include <base/time/time.h> | |
| #include <chromeos/constants/vm_tools.h> | |
| #include <vm_protos/proto_bindings/common.pb.h> | |
| #include <vm_protos/proto_bindings/container_host.pb.h> | |
| #include "vm_tools/garcon/desktop_file.h" | |
| #include "vm_tools/garcon/host_notifier.h" | |
| #include "vm_tools/garcon/mime_types_parser.h" | |
| namespace { | |
| // File extension for desktop files. | |
| constexpr char kDesktopFileExtension[] = ".desktop"; | |
| // Directory where the MIME types file is stored for watching with inotify. | |
| constexpr char kMimeTypesDir[] = "/usr/share/mime"; | |
| // User directory where the MIME types file is stored for watching with inotify. | |
| constexpr char kUserMimeTypesDir[] = ".local/share/mime"; | |
| // File where MIME type information is stored in the container. | |
| constexpr char kMimeTypesFilePath[] = "/usr/share/mime/mime.cache"; | |
| // Filename for the user's MIME types file in their home dir. | |
| constexpr char kUserMimeTypesFile[] = ".local/share/mime/mime.cache"; | |
| // Duration over which we coalesce changes to the desktop file system. | |
| constexpr base::TimeDelta kFilesystemChangeCoalesceTime = base::Seconds(3); | |
| // Delimiter for the end of a URL scheme. | |
| constexpr char kUrlSchemeDelimiter[] = "://"; | |
| // Periodic interval for checking free disk space. | |
| constexpr base::TimeDelta kDiskSpaceCheckInterval = base::Minutes(2); | |
| constexpr int64_t kDiskSpaceCheckThreshold = 1 * 1024 * 1024 * 1024; // 1GiB | |
| void SendInstallStatusToHost( | |
| vm_tools::container::ContainerListener::Stub* stub, | |
| vm_tools::container::InstallLinuxPackageProgressInfo progress_info) { | |
| grpc::ClientContext ctx; | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status grpc_status = | |
| stub->InstallLinuxPackageProgress(&ctx, progress_info, &empty); | |
| if (!grpc_status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system about install status: " | |
| << grpc_status.error_message(); | |
| } | |
| } | |
| void SendUninstallStatusToHost( | |
| vm_tools::container::ContainerListener::Stub* stub, | |
| vm_tools::container::UninstallPackageProgressInfo info) { | |
| grpc::ClientContext ctx; | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status grpc_status = stub->UninstallPackageProgress(&ctx, info, &empty); | |
| if (!grpc_status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system about uninstall status: " | |
| << grpc_status.error_message() << " (code " | |
| << grpc_status.error_code() << ")"; | |
| } | |
| } | |
| void SendApplyAnsiblePlaybookStatusToHost( | |
| vm_tools::container::ContainerListener::Stub* stub, | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo info) { | |
| grpc::ClientContext ctx; | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status grpc_status = | |
| stub->ApplyAnsiblePlaybookProgress(&ctx, info, &empty); | |
| if (!grpc_status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system about ansible playbook " | |
| << "application status: " << grpc_status.error_message() | |
| << " (code " << grpc_status.error_code() << ")"; | |
| } | |
| } | |
| } // namespace | |
| namespace vm_tools { | |
| namespace garcon { | |
| // static | |
| std::unique_ptr<HostNotifier> HostNotifier::Create(const std::string& token) { | |
| return base::WrapUnique(new HostNotifier(token)); | |
| } | |
| // static | |
| bool HostNotifier::OpenUrlInHost(const std::string& url) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::OpenUrlRequest url_request; | |
| url_request.set_token(token_); | |
| url_request.set_url(url); | |
| // If url has no scheme, but matches a local file, then convert to file://. | |
| auto front = url.find(kUrlSchemeDelimiter); | |
| if (front == std::string::npos) { | |
| base::FilePath path(url); | |
| base::FilePath abs = base::MakeAbsoluteFilePath(path); | |
| if (!abs.empty()) { | |
| url_request.set_url("file://" + abs.value()); | |
| } | |
| } | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->OpenUrl(&ctx, url_request, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to request host system to open url \"" << url | |
| << "\" error: " << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::OpenTerminal(std::vector<std::string> args) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::OpenTerminalRequest terminal_request; | |
| std::copy(std::make_move_iterator(args.begin()), | |
| std::make_move_iterator(args.end()), | |
| google::protobuf::RepeatedFieldBackInserter( | |
| terminal_request.mutable_params())); | |
| terminal_request.set_token(token_); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->OpenTerminal(&ctx, terminal_request, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed request to open terminal, error: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::SelectFile(const std::string& type, | |
| const std::string& title, | |
| const std::string& default_path, | |
| const std::string& allowed_extensions, | |
| std::vector<std::string>* files) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::SelectFileRequest select_file_request; | |
| select_file_request.set_token(token_); | |
| select_file_request.set_type(type); | |
| select_file_request.set_title(title); | |
| select_file_request.set_default_path(default_path); | |
| select_file_request.set_allowed_extensions(allowed_extensions); | |
| vm_tools::container::SelectFileResponse select_file_response; | |
| grpc::Status status = | |
| stub_->SelectFile(&ctx, select_file_request, &select_file_response); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed request to select file, error: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| std::copy( | |
| std::make_move_iterator(select_file_response.mutable_files()->begin()), | |
| std::make_move_iterator(select_file_response.mutable_files()->end()), | |
| std::back_inserter(*files)); | |
| return true; | |
| } | |
| bool HostNotifier::InstallShaderCache(uint64_t steam_app_id, | |
| bool mount, | |
| bool wait) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::InstallShaderCacheRequest request; | |
| EmptyMessage response; | |
| request.set_token(token_); | |
| request.set_steam_app_id(steam_app_id); | |
| request.set_mount(mount); | |
| request.set_wait(wait); | |
| // Request Cicerone to download and install shader cache | |
| grpc::Status status = stub_->InstallShaderCache(&ctx, request, &response); | |
| if (!status.ok()) { | |
| if (mount && wait) { | |
| LOG(ERROR) << "Failed to install and mount shader cache: " | |
| << status.error_message(); | |
| } else { | |
| LOG(ERROR) << "Failed to trigger shader cache installation: " | |
| << status.error_message(); | |
| } | |
| return false; | |
| } | |
| if (mount && wait) { | |
| // The requested mount op was waited on and returned with no errors. | |
| LOG(INFO) << "Successfully installed and mounted shader cache DLC"; | |
| } else { | |
| LOG(INFO) << "Successfully scheduled shader cache DLC for installation"; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::UninstallShaderCache(uint64_t steam_app_id) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::UninstallShaderCacheRequest request; | |
| EmptyMessage response; | |
| request.set_token(token_); | |
| request.set_steam_app_id(steam_app_id); | |
| grpc::Status status = stub_->UninstallShaderCache(&ctx, request, &response); | |
| if (!status.ok()) { | |
| LOG(ERROR) << "Failed to unmount and uninstall shader cache: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::UnmountShaderCache(uint64_t steam_app_id, bool wait) { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::UnmountShaderCacheRequest request; | |
| EmptyMessage response; | |
| request.set_token(token_); | |
| request.set_steam_app_id(steam_app_id); | |
| request.set_wait(wait); | |
| grpc::Status status = stub_->UnmountShaderCache(&ctx, request, &response); | |
| if (!status.ok()) { | |
| LOG(ERROR) << "Failed to queue unmount shader cache: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::ReportMetrics( | |
| vm_tools::container::ReportMetricsRequest request, | |
| vm_tools::container::ReportMetricsResponse* response) { | |
| grpc::ClientContext ctx; | |
| request.set_token(token_); | |
| grpc::Status status = stub_->ReportMetrics(&ctx, request, response); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to report metrics: " << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::InhibitScreensaver( | |
| vm_tools::container::InhibitScreensaverInfo info) { | |
| grpc::ClientContext ctx; | |
| info.set_token(token_); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->InhibitScreensaver(&ctx, info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to inhibit screensaver: " << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool HostNotifier::UninhibitScreensaver( | |
| vm_tools::container::UninhibitScreensaverInfo info) { | |
| vm_tools::EmptyMessage empty; | |
| grpc::ClientContext ctx; | |
| info.set_token(token_); | |
| grpc::Status status = stub_->UninhibitScreensaver(&ctx, info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to uninhibit screensaver: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| HostNotifier::HostNotifier(const std::string& token) | |
| : token_(token), | |
| update_app_list_posted_(false), | |
| send_app_list_to_host_in_progress_(false), | |
| update_mime_types_posted_(false) { | |
| SetUpContainerListenerStub(); | |
| } | |
| HostNotifier::~HostNotifier() = default; | |
| void HostNotifier::OnSignalReadable() { | |
| signalfd_siginfo info; | |
| if (read(signal_fd_.get(), &info, sizeof(info)) != sizeof(info)) { | |
| PLOG(ERROR) << "Failed to read from signalfd"; | |
| } | |
| DCHECK_EQ(info.ssi_signo, SIGTERM); | |
| // Notify the host we are shutting down, then inform our run loop to terminate | |
| // which should then shut us down, deallocate us and then also terminate the | |
| // gRPC thread. | |
| NotifyHostOfContainerShutdown(); | |
| if (shutdown_closure_) { | |
| task_runner_->PostTask(FROM_HERE, std::move(shutdown_closure_)); | |
| } | |
| } | |
| void HostNotifier::OnInstallCompletion(const std::string& command_uuid, | |
| bool success, | |
| const std::string& failure_reason) { | |
| vm_tools::container::InstallLinuxPackageProgressInfo progress_info; | |
| progress_info.set_token(token_); | |
| progress_info.set_status( | |
| success ? vm_tools::container::InstallLinuxPackageProgressInfo::SUCCEEDED | |
| : vm_tools::container::InstallLinuxPackageProgressInfo::FAILED); | |
| progress_info.set_failure_details(failure_reason); | |
| progress_info.set_command_uuid(command_uuid); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendInstallStatusToHost, base::Unretained(stub_.get()), | |
| std::move(progress_info))); | |
| } | |
| void HostNotifier::OnInstallProgress( | |
| const std::string& command_uuid, | |
| vm_tools::container::InstallLinuxPackageProgressInfo::Status status, | |
| uint32_t percent_progress) { | |
| vm_tools::container::InstallLinuxPackageProgressInfo progress_info; | |
| progress_info.set_token(token_); | |
| progress_info.set_status(status); | |
| progress_info.set_progress_percent(percent_progress); | |
| progress_info.set_command_uuid(command_uuid); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendInstallStatusToHost, base::Unretained(stub_.get()), | |
| std::move(progress_info))); | |
| } | |
| void HostNotifier::OnUninstallCompletion(bool success, | |
| const std::string& failure_reason) { | |
| LOG(INFO) << "Got HostNotifier::OnUninstallCompletion(" << success << ", " | |
| << failure_reason << ")"; | |
| vm_tools::container::UninstallPackageProgressInfo info; | |
| info.set_token(token_); | |
| if (success) { | |
| info.set_status( | |
| vm_tools::container::UninstallPackageProgressInfo::SUCCEEDED); | |
| } else { | |
| info.set_status(vm_tools::container::UninstallPackageProgressInfo::FAILED); | |
| info.set_failure_details(failure_reason); | |
| } | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendUninstallStatusToHost, base::Unretained(stub_.get()), | |
| std::move(info))); | |
| } | |
| void HostNotifier::OnUninstallProgress(uint32_t percent_progress) { | |
| VLOG(3) << "Got HostNotifier::OnUninstallProgress(" << percent_progress | |
| << ")"; | |
| vm_tools::container::UninstallPackageProgressInfo info; | |
| info.set_token(token_); | |
| info.set_status( | |
| vm_tools::container::UninstallPackageProgressInfo::UNINSTALLING); | |
| info.set_progress_percent(percent_progress); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendUninstallStatusToHost, base::Unretained(stub_.get()), | |
| std::move(info))); | |
| } | |
| void HostNotifier::OnApplyAnsiblePlaybookCompletion( | |
| bool success, const std::string& failure_reason) { | |
| LOG(INFO) << "Got HostNotifier::OnApplyAnsiblePlaybookCompletion(" << success | |
| << ", " << failure_reason << ")"; | |
| RemoveAnsiblePlaybookApplication(); | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo info; | |
| info.set_token(token_); | |
| if (success) { | |
| info.set_status( | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo::SUCCEEDED); | |
| } else { | |
| info.set_status( | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo::FAILED); | |
| info.set_failure_details(failure_reason); | |
| } | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendApplyAnsiblePlaybookStatusToHost, | |
| base::Unretained(stub_.get()), std::move(info))); | |
| } | |
| void HostNotifier::OnApplyAnsiblePlaybookProgress( | |
| const std::vector<std::string>& status_lines) { | |
| LOG(INFO) << "Got HostNotifier::OnApplyAnsaiblePlaybookProgress: " | |
| << status_lines[0]; | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo info; | |
| info.set_token(token_); | |
| info.set_status( | |
| vm_tools::container::ApplyAnsiblePlaybookProgressInfo::IN_PROGRESS); | |
| for (auto line : status_lines) | |
| info.add_status_string(line); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&SendApplyAnsiblePlaybookStatusToHost, | |
| base::Unretained(stub_.get()), std::move(info))); | |
| } | |
| void HostNotifier::CreateAnsiblePlaybookApplication( | |
| base::WaitableEvent* event, | |
| AnsiblePlaybookApplication** ansible_playbook_application_ptr) { | |
| DCHECK(!ansible_playbook_application_); | |
| ansible_playbook_application_ = | |
| std::make_unique<vm_tools::garcon::AnsiblePlaybookApplication>(); | |
| *ansible_playbook_application_ptr = ansible_playbook_application_.get(); | |
| event->Signal(); | |
| } | |
| void HostNotifier::RemoveAnsiblePlaybookApplication() { | |
| ansible_playbook_application_->RemoveObserver(this); | |
| ansible_playbook_application_.reset(); | |
| } | |
| bool HostNotifier::InitServer(base::OnceClosure shutdown_closure, | |
| uint32_t garcon_port, | |
| uint32_t sftp_port, | |
| PackageKitProxy* package_kit_proxy) { | |
| CHECK(package_kit_proxy); | |
| package_kit_proxy_ = package_kit_proxy; | |
| shutdown_closure_ = std::move(shutdown_closure); | |
| task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault(); | |
| sftp_vsock_port_ = sftp_port; | |
| if (!NotifyHostGarconIsReady(garcon_port, sftp_port)) { | |
| return false; | |
| } | |
| // Start listening for SIGTERM. | |
| sigset_t mask; | |
| sigemptyset(&mask); | |
| sigaddset(&mask, SIGTERM); | |
| signal_fd_.reset(signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK)); | |
| if (!signal_fd_.is_valid()) { | |
| PLOG(ERROR) << "Unable to create signalfd"; | |
| return false; | |
| } | |
| signal_controller_ = base::FileDescriptorWatcher::WatchReadable( | |
| signal_fd_.get(), base::BindRepeating(&HostNotifier::OnSignalReadable, | |
| base::Unretained(this))); | |
| if (!signal_controller_) { | |
| LOG(ERROR) << "Failed to watch signal file descriptor"; | |
| return false; | |
| } | |
| // Block the standard SIGTERM handler since we will be getting it via the | |
| // signalfd. We have to do this before we setup the file path watcher | |
| // because that will end up spawning another thread for each watcher. | |
| if (sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) { | |
| PLOG(ERROR) << "Failed blocking standard SIGTERM handler"; | |
| return false; | |
| } | |
| // Setup all of our watchers for changes to any of the paths where .desktop | |
| // files may reside. | |
| std::vector<base::FilePath> watch_paths = | |
| DesktopFile::GetPathsForDesktopFiles(); | |
| for (auto& path : watch_paths) { | |
| std::unique_ptr<base::FilePathWatcher> watcher = | |
| std::make_unique<base::FilePathWatcher>(); | |
| if (!watcher->Watch(path, base::FilePathWatcher::Type::kRecursive, | |
| base::BindRepeating(&HostNotifier::DesktopPathsChanged, | |
| base::Unretained(this)))) { | |
| LOG(ERROR) << "Failed setting up filesystem path watcher for dir: " | |
| << path.value(); | |
| // Probably better to just watch the dirs we can rather than terminate | |
| // garcon altogether. | |
| continue; | |
| } | |
| watchers_.emplace_back(std::move(watcher)); | |
| } | |
| // We can only watch directories and on changes we aren't notified which | |
| // file changes, so we end up watching for any changes in /etc or $HOME. | |
| // Also setup the watcher for the /usr/local/share/mime/mime.cache file. | |
| std::unique_ptr<base::FilePathWatcher> mime_type_watcher = | |
| std::make_unique<base::FilePathWatcher>(); | |
| base::FilePath mime_type_path(kMimeTypesDir); | |
| if (!mime_type_watcher->Watch( | |
| mime_type_path, base::FilePathWatcher::Type::kNonRecursive, | |
| base::BindRepeating(&HostNotifier::MimeTypesChanged, | |
| base::Unretained(this)))) { | |
| LOG(ERROR) << "Failed setting up filesystem path watcher for: " | |
| << kMimeTypesDir; | |
| } | |
| watchers_.emplace_back(std::move(mime_type_watcher)); | |
| // Also setup the watcher for the $HOME/.local/share/mime/mime.cache file. | |
| std::unique_ptr<base::FilePathWatcher> home_mime_type_watcher = | |
| std::make_unique<base::FilePathWatcher>(); | |
| if (!home_mime_type_watcher->Watch( | |
| base::GetHomeDir().Append(kUserMimeTypesDir), | |
| base::FilePathWatcher::Type::kNonRecursive, | |
| base::BindRepeating(&HostNotifier::MimeTypesChanged, | |
| base::Unretained(this)))) { | |
| LOG(ERROR) << "Failed setting up filesystem path watcher for: " | |
| << base::GetHomeDir().value(); | |
| } | |
| watchers_.emplace_back(std::move(home_mime_type_watcher)); | |
| // If this fails, don't terminate ourself, this could be some kind of | |
| // transient failure. | |
| SendAppListToHost(); | |
| SendMimeTypesToHost(); | |
| // Start the disk space watcher. | |
| free_disk_space_timer_.Start( | |
| FROM_HERE, kDiskSpaceCheckInterval, | |
| base::BindRepeating(&HostNotifier::CheckDiskSpace, | |
| weak_ptr_factory_.GetWeakPtr())); | |
| return true; | |
| } | |
| void HostNotifier::CheckDiskSpace() { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::LowDiskSpaceTriggeredInfo info; | |
| auto free_bytes = base::SysInfo::AmountOfFreeDiskSpace(base::FilePath("/")); | |
| if (free_bytes >= kDiskSpaceCheckThreshold) { | |
| // Plenty of free space, nothing more to do. | |
| return; | |
| } | |
| info.set_token(token_); | |
| info.set_free_bytes(free_bytes); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->LowDiskSpaceTriggered(&ctx, info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system that disk space is low: " | |
| << status.error_message(); | |
| } | |
| } | |
| bool HostNotifier::NotifyHostGarconIsReady(uint32_t garcon_port, | |
| uint32_t sftp_port) { | |
| // Notify the host system that we are ready. | |
| grpc::ClientContext ctx; | |
| vm_tools::container::ContainerStartupInfo startup_info; | |
| startup_info.set_token(token_); | |
| startup_info.set_garcon_port(garcon_port); | |
| startup_info.set_sftp_port(sftp_port); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->ContainerReady(&ctx, startup_info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system that container is ready: " | |
| << status.error_message(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void HostNotifier::NotifyHostOfContainerShutdown() { | |
| // Notify the host system that we are shutting down. | |
| grpc::ClientContext ctx; | |
| vm_tools::container::ContainerShutdownInfo shutdown_info; | |
| shutdown_info.set_token(token_); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = stub_->ContainerShutdown(&ctx, shutdown_info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system that container is shutting " | |
| << "down: " << status.error_message(); | |
| } | |
| } | |
| void HostNotifier::NotifyHostOfPendingAppListUpdates() { | |
| grpc::ClientContext ctx; | |
| vm_tools::container::PendingAppListUpdateCount msg; | |
| msg.set_token(token_); | |
| msg.set_count(update_app_list_posted_ + send_app_list_to_host_in_progress_); | |
| vm_tools::EmptyMessage empty; | |
| grpc::Status status = | |
| stub_->PendingUpdateApplicationListCalls(&ctx, msg, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host system of pending app list updates: " | |
| << status.error_message(); | |
| } | |
| } | |
| void HostNotifier::HandleSteamApp( | |
| std::unordered_set<uint64_t> found_steam_apps) { | |
| for (auto app_id : found_steam_apps) { | |
| if (installed_steam_apps_.find(app_id) == installed_steam_apps_.end()) { | |
| LOG(INFO) << "Attempting to install shader cache for newly installed " | |
| << "steam app"; | |
| InstallShaderCache(app_id, false, false); | |
| } | |
| } | |
| for (auto app_id : installed_steam_apps_) { | |
| if (found_steam_apps.find(app_id) == found_steam_apps.end()) { | |
| LOG(INFO) << "Attempting to uninstall shader cache for removed steam app"; | |
| UninstallShaderCache(app_id); | |
| } | |
| } | |
| installed_steam_apps_ = found_steam_apps; | |
| } | |
| void HostNotifier::SendAppListToHost() { | |
| if (send_app_list_to_host_in_progress_) { | |
| // Don't have multiple SendAppListToHost callback chains happening at the | |
| // same time. Delay the next run a little longer. | |
| // | |
| // Checking a boolean isn't a race condition because all the callbacks are | |
| // on the same thread. | |
| task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&HostNotifier::SendAppListToHost, | |
| base::Unretained(this)), | |
| kFilesystemChangeCoalesceTime); | |
| return; | |
| } | |
| auto callback_state = std::make_unique<AppListBuilderState>(); | |
| callback_state->request.set_token(token_); | |
| // If we hit duplicate IDs, then we are supposed to use the first one only. | |
| std::set<std::string> unique_app_ids; | |
| std::unordered_set<uint64_t> found_steam_apps; | |
| // Get the list of directories that we should search for .desktop files | |
| // recursively and then perform the search. | |
| std::vector<base::FilePath> search_paths = | |
| DesktopFile::GetPathsForDesktopFiles(); | |
| for (auto curr_path : search_paths) { | |
| base::FileEnumerator file_enum(curr_path, true, | |
| base::FileEnumerator::FILES); | |
| for (base::FilePath enum_path = file_enum.Next(); !enum_path.empty(); | |
| enum_path = file_enum.Next()) { | |
| if (enum_path.FinalExtension() != kDesktopFileExtension) { | |
| continue; | |
| } | |
| // We have a .desktop file path, parse it and then add it to the | |
| // protobuf if it parses successfully. | |
| std::unique_ptr<DesktopFile> desktop_file = | |
| DesktopFile::ParseDesktopFile(enum_path); | |
| if (!desktop_file) { | |
| LOG(WARNING) << "Failed parsing the .desktop file: " | |
| << enum_path.value(); | |
| continue; | |
| } | |
| // Found steam apps | |
| if (desktop_file->steam_app_id()) { | |
| found_steam_apps.emplace(desktop_file->steam_app_id()); | |
| } | |
| // If we have already seen this desktop file ID then don't analyze this | |
| // one. We want to check this before we do the filtering to allow users | |
| // to put .desktop files in local locations to hide applications in | |
| // system locations. | |
| if (!unique_app_ids.insert(desktop_file->app_id()).second) { | |
| continue; | |
| } | |
| // Make sure this .desktop file is one we should send to the host. | |
| // There are various cases where we do not want to transmit certain | |
| // .desktop files. | |
| if (!desktop_file->ShouldPassToHost()) { | |
| continue; | |
| } | |
| // Add this app to the list in the protobuf and populate all of its | |
| // fields. | |
| vm_tools::container::Application* app = | |
| callback_state->request.add_application(); | |
| app->set_desktop_file_id(desktop_file->app_id()); | |
| const std::map<std::string, std::string>& name_map = | |
| desktop_file->locale_name_map(); | |
| vm_tools::container::Application::LocalizedString* names = | |
| app->mutable_name(); | |
| for (const auto& name_entry : name_map) { | |
| vm_tools::container::Application::LocalizedString::StringWithLocale* | |
| locale_string = names->add_values(); | |
| locale_string->set_locale(name_entry.first); | |
| locale_string->set_value(name_entry.second); | |
| } | |
| const std::map<std::string, std::string>& comment_map = | |
| desktop_file->locale_comment_map(); | |
| vm_tools::container::Application::LocalizedString* comments = | |
| app->mutable_comment(); | |
| for (const auto& comment_entry : comment_map) { | |
| vm_tools::container::Application::LocalizedString::StringWithLocale* | |
| locale_string = comments->add_values(); | |
| locale_string->set_locale(comment_entry.first); | |
| locale_string->set_value(comment_entry.second); | |
| } | |
| const std::map<std::string, std::vector<std::string>>& keywords_map = | |
| desktop_file->locale_keywords_map(); | |
| vm_tools::container::Application::LocaleStrings* keyword = | |
| app->mutable_keywords(); | |
| for (const auto& keywords_entry : keywords_map) { | |
| vm_tools::container::Application::LocaleStrings::StringsWithLocale* | |
| locale_string = keyword->add_values(); | |
| locale_string->set_locale(keywords_entry.first); | |
| for (const auto& curr_keyword : keywords_entry.second) { | |
| locale_string->add_value(curr_keyword); | |
| } | |
| } | |
| for (const auto& mime_type : desktop_file->mime_types()) { | |
| app->add_mime_types(mime_type); | |
| } | |
| app->set_no_display(desktop_file->no_display()); | |
| app->set_startup_wm_class(desktop_file->startup_wm_class()); | |
| app->set_startup_notify(desktop_file->startup_notify()); | |
| app->set_exec(desktop_file->exec()); | |
| app->set_executable_file_name(desktop_file->GenerateExecutableFileName()); | |
| app->set_terminal(desktop_file->terminal()); | |
| callback_state->desktop_files_for_application.push_back(enum_path); | |
| } | |
| } | |
| CHECK_EQ(callback_state->desktop_files_for_application.size(), | |
| callback_state->request.application_size()); | |
| // We now want to query all the .desktop files to see what package owns them. | |
| // Unforuntately, this requires D-Bus calls to the PackageKit, and we are on | |
| // the D-Bus thread. So we can't receive the results until this function | |
| // returns, so we need to set up a series of callbacks. | |
| // | |
| // Query each .desktop file in turn. The callback will record the info for | |
| // that file and also kick off the query for the next file until all files | |
| // have been queried. | |
| callback_state->num_package_id_queries_completed = 0; | |
| // Clear this in case it was set, this all happens on the same thread. | |
| // Clear this now, not when the package_id callbacks are complete, in case | |
| // we get another notification while this is still in flight; we'd want to run | |
| // this function again in that case. | |
| update_app_list_posted_ = false; | |
| // Don't start another round of callbacks while still trying to finish this | |
| // round. | |
| send_app_list_to_host_in_progress_ = true; | |
| HandleSteamApp(found_steam_apps); | |
| RequestNextPackageIdOrCompleteUpdateApplicationList( | |
| std::move(callback_state)); | |
| } | |
| void HostNotifier::RequestNextPackageIdOrCompleteUpdateApplicationList( | |
| std::unique_ptr<AppListBuilderState> state) { | |
| if ((state->num_package_id_queries_completed >= | |
| state->desktop_files_for_application.size())) { | |
| // We have finished all package_id queries. This data is ready to send to | |
| // the host. | |
| send_app_list_to_host_in_progress_ = false; | |
| vm_tools::EmptyMessage empty; | |
| grpc::ClientContext ctx; | |
| grpc::Status status = | |
| stub_->UpdateApplicationList(&ctx, state->request, &empty); | |
| VLOG(3) << "UpdatedApplicationList\n" << state->request.DebugString(); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host of the application list: " | |
| << status.error_message(); | |
| } | |
| NotifyHostOfPendingAppListUpdates(); | |
| return; | |
| } | |
| // else we still need to do more package_id queries | |
| package_kit_proxy_->SearchLinuxPackagesForFile( | |
| state->desktop_files_for_application | |
| [state->num_package_id_queries_completed], | |
| base::BindOnce(&HostNotifier::PackageIdCallback, base::Unretained(this), | |
| std::move(state))); | |
| } | |
| void HostNotifier::PackageIdCallback( | |
| std::unique_ptr<AppListBuilderState> state, | |
| bool success, | |
| bool pkg_found, | |
| const PackageKitProxy::LinuxPackageInfo& pkg_info, | |
| const std::string& error) { | |
| // The data passed in the parameters is for the Application at | |
| // state->request.application[state->num_package_id_queries_completed] | |
| CHECK_LT(state->num_package_id_queries_completed, | |
| state->request.application_size()); | |
| if (success && pkg_found) { | |
| vm_tools::container::Application* application = | |
| state->request.mutable_application( | |
| state->num_package_id_queries_completed); | |
| application->set_package_id(pkg_info.package_id); | |
| } else if (!success) { | |
| LOG(ERROR) << "Failed to get Package Info: " << error; | |
| } | |
| state->num_package_id_queries_completed++; | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce( | |
| &HostNotifier::RequestNextPackageIdOrCompleteUpdateApplicationList, | |
| base::Unretained(this), std::move(state))); | |
| } | |
| void HostNotifier::SendMimeTypesToHost() { | |
| vm_tools::container::UpdateMimeTypesRequest request; | |
| request.set_token(token_); | |
| vm_tools::EmptyMessage empty; | |
| // Clear this in case it was set, this all happens on the same thread. | |
| update_mime_types_posted_ = false; | |
| MimeTypeMap mime_type_map; | |
| if (!ParseMimeTypes(kMimeTypesFilePath, &mime_type_map)) { | |
| LOG(ERROR) << "Failed parsing system mime types, will not send the list to " | |
| << "host"; | |
| return; | |
| } | |
| // The user MIME types may not be set up, so we ignore failures here. User | |
| // values override system values, so parse this one second so they get | |
| // overridden. | |
| ParseMimeTypes(base::GetHomeDir().Append(kUserMimeTypesFile).value(), | |
| &mime_type_map); | |
| request.mutable_mime_type_mappings()->insert( | |
| std::make_move_iterator(mime_type_map.begin()), | |
| std::make_move_iterator(mime_type_map.end())); | |
| // Now make the gRPC call to send this list to the host. | |
| grpc::ClientContext ctx; | |
| grpc::Status status = stub_->UpdateMimeTypes(&ctx, request, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host of the MIME types: " | |
| << status.error_message(); | |
| } | |
| } | |
| void HostNotifier::DesktopPathsChanged(const base::FilePath& path, bool error) { | |
| if (error) { | |
| // This should never occur because the implementation for Linux never calls | |
| // this with an error. | |
| LOG(ERROR) << "Error detected in file path watching for path: " | |
| << path.value(); | |
| return; | |
| } | |
| // We don't want to trigger an update every time there's a change, instead | |
| // wait a bit and coalesce potential groups of changes that may occur. We | |
| // don't want to wait too long though because then the user may feel that it | |
| // is unresponsive in newly installed applications not showing up in the | |
| // launcher when they check. | |
| if (update_app_list_posted_) { | |
| return; | |
| } | |
| task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&HostNotifier::SendAppListToHost, base::Unretained(this)), | |
| kFilesystemChangeCoalesceTime); | |
| update_app_list_posted_ = true; | |
| NotifyHostOfPendingAppListUpdates(); | |
| } | |
| void HostNotifier::MimeTypesChanged(const base::FilePath& path, bool error) { | |
| if (error) { | |
| // This should never occur because the implementation for Linux never calls | |
| // this with an error. | |
| LOG(ERROR) << "Error detected in file path watching for path: " | |
| << path.value(); | |
| return; | |
| } | |
| // Coalesce these calls if we have one pending. | |
| if (update_mime_types_posted_) { | |
| return; | |
| } | |
| task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&HostNotifier::SendMimeTypesToHost, | |
| base::Unretained(this)), | |
| kFilesystemChangeCoalesceTime); | |
| update_mime_types_posted_ = true; | |
| } | |
| void HostNotifier::SetUpContainerListenerStub() { | |
| stub_ = std::make_unique<vm_tools::container::ContainerListener::Stub>( | |
| grpc::CreateChannel(base::StringPrintf("vsock:%d:%u", VMADDR_CID_HOST, | |
| vm_tools::kGarconPort), | |
| grpc::InsecureChannelCredentials())); | |
| } | |
| bool HostNotifier::AddFileWatch(const base::FilePath& path, | |
| std::string* error_msg) { | |
| if (path.IsAbsolute() || path.ReferencesParent()) { | |
| LOG(ERROR) << "Invalid path"; | |
| *error_msg = "invalid path"; | |
| return false; | |
| } | |
| if (file_path_watchers_.count(path) > 0) { | |
| LOG(ERROR) << "Already watching path"; | |
| *error_msg = "already watching path"; | |
| return false; | |
| } | |
| std::unique_ptr<base::FilePathWatcher> watcher = | |
| std::make_unique<base::FilePathWatcher>(); | |
| base::FilePath path_in_home = base::GetHomeDir().Append(path); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| // TODO(crbug.com/1179608): after libchrome is upreved to r860220, | |
| // base::FilePathWatcher will not be overloaded and could be simplified to | |
| // base::IgnoreResult(&base::FilePathWatcher::Watch). | |
| base::BindOnce(base::IgnoreResult<bool ( // NOLINT(whitespace/parens) | |
| base::FilePathWatcher::*)( | |
| const base::FilePath&, base::FilePathWatcher::Type, | |
| const base::FilePathWatcher::Callback&)>( | |
| &base::FilePathWatcher::Watch), | |
| base::Unretained(watcher.get()), path_in_home, | |
| base::FilePathWatcher::Type::kNonRecursive, | |
| base::BindRepeating(&HostNotifier::FileWatchTriggered, | |
| base::Unretained(this)))); | |
| file_path_watchers_[path] = std::move(watcher); | |
| return true; | |
| } | |
| bool HostNotifier::RemoveFileWatch(const base::FilePath& path, | |
| std::string* error_msg) { | |
| if (path.IsAbsolute() || path.ReferencesParent()) { | |
| LOG(ERROR) << "Invalid path"; | |
| *error_msg = "invalid path"; | |
| return false; | |
| } | |
| if (file_path_watchers_.count(path) == 0) { | |
| LOG(ERROR) << "Not watching path"; | |
| *error_msg = "not watching path"; | |
| return false; | |
| } | |
| task_runner_->DeleteSoon(FROM_HERE, file_path_watchers_[path].release()); | |
| file_path_watchers_.erase(path); | |
| file_watch_last_change_.erase(path); | |
| return true; | |
| } | |
| void HostNotifier::SendFileWatchTriggeredToHost(const base::FilePath& path) { | |
| vm_tools::container::FileWatchTriggeredInfo info; | |
| info.set_token(token_); | |
| info.set_path(path.value()); | |
| vm_tools::EmptyMessage empty; | |
| // Update pending flag and time when last sent. | |
| file_watch_change_posted_.erase(path); | |
| file_watch_last_change_[path] = base::TimeTicks::Now(); | |
| // Now make the gRPC call to notify the host. | |
| grpc::ClientContext ctx; | |
| grpc::Status status = stub_->FileWatchTriggered(&ctx, info, &empty); | |
| if (!status.ok()) { | |
| LOG(WARNING) << "Failed to notify host of FilePathWatcher change: " | |
| << status.error_message(); | |
| } | |
| } | |
| void HostNotifier::FileWatchTriggered(const base::FilePath& absolute_path, | |
| bool error) { | |
| if (error) { | |
| // This should never occur because the implementation for Linux never calls | |
| // this with an error. | |
| LOG(ERROR) << "Error detected in file path watcher"; | |
| return; | |
| } | |
| base::FilePath home = base::GetHomeDir(); | |
| base::FilePath path; | |
| if (absolute_path != home && !home.AppendRelativePath(absolute_path, &path)) { | |
| LOG(ERROR) << "Unexpected path not under $HOME"; | |
| return; | |
| } | |
| // Coalesce these calls if we have one pending. | |
| if (file_watch_change_posted_.count(path) > 0) { | |
| return; | |
| } | |
| // Send right away if it has been long enough since the last one, else post | |
| // delayed task. | |
| base::TimeDelta time_since_last = | |
| base::TimeTicks::Now() - file_watch_last_change_[path]; | |
| if (time_since_last > kFilesystemChangeCoalesceTime) { | |
| SendFileWatchTriggeredToHost(path); | |
| } else { | |
| task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&HostNotifier::SendFileWatchTriggeredToHost, | |
| base::Unretained(this), path), | |
| kFilesystemChangeCoalesceTime - time_since_last); | |
| file_watch_change_posted_.insert(path); | |
| } | |
| } | |
| } // namespace garcon | |
| } // namespace vm_tools |