blob: 6b004b835f5e30340368193f0f0771da01ee84f1 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// 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 <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/location.h>
#include <base/logging.h>
#include <base/memory/ptr_util.h>
#include <base/strings/stringprintf.h>
#include <base/strings/string_util.h>
#include <base/threading/thread_task_runner_handle.h>
#include <base/time/time.h>
#include "vm_tools/common/constants.h"
#include "vm_tools/garcon/desktop_file.h"
#include "vm_tools/garcon/host_notifier.h"
namespace {
constexpr char kHostIpFile[] = "/dev/.host_ip";
constexpr char kSecurityTokenFile[] = "/dev/.container_token";
constexpr int kSecurityTokenLength = 36;
// File extension for desktop files.
constexpr char kDesktopFileExtension[] = ".desktop";
// Duration over which we coalesce changes to the desktop file system.
constexpr base::TimeDelta kFilesystemChangeCoalesceTime =
base::TimeDelta::FromSeconds(5);
std::string GetHostIp() {
char host_addr[INET_ADDRSTRLEN + 1];
base::FilePath host_ip_path(kHostIpFile);
int num_read = base::ReadFile(host_ip_path, host_addr, sizeof(host_addr) - 1);
if (num_read <= 0) {
LOG(ERROR) << "Failed reading the host IP from: "
<< host_ip_path.MaybeAsASCII();
return "";
}
host_addr[num_read] = '\0';
return std::string(host_addr);
}
std::string GetSecurityToken() {
char token[kSecurityTokenLength + 1];
base::FilePath security_token_path(kSecurityTokenFile);
int num_read = base::ReadFile(security_token_path, token, sizeof(token) - 1);
if (num_read <= 0) {
LOG(ERROR) << "Failed reading the container token from: "
<< security_token_path.MaybeAsASCII();
return "";
}
token[num_read] = '\0';
return std::string(token);
}
} // namespace
namespace vm_tools {
namespace garcon {
// static
std::unique_ptr<HostNotifier> HostNotifier::Create(
std::shared_ptr<grpc::Server> grpc_server, base::Closure shutdown_closure) {
auto notifier = base::WrapUnique(
new HostNotifier(std::move(grpc_server), std::move(shutdown_closure)));
if (!notifier->Init()) {
notifier.reset();
}
return notifier;
}
HostNotifier::HostNotifier(std::shared_ptr<grpc::Server> grpc_server,
base::Closure shutdown_closure)
: shutdown_closure_(std::move(shutdown_closure)),
grpc_server_(std::move(grpc_server)),
signal_controller_(FROM_HERE),
weak_ptr_factory_(this) {}
HostNotifier::~HostNotifier() {
grpc_server_->Shutdown();
}
void HostNotifier::OnFileCanReadWithoutBlocking(int fd) {
DCHECK_EQ(fd, signal_fd_.get());
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();
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, shutdown_closure_);
}
void HostNotifier::OnFileCanWriteWithoutBlocking(int fd) {
NOTREACHED();
}
bool HostNotifier::Init() {
std::string host_ip = GetHostIp();
token_ = GetSecurityToken();
if (token_.empty() || host_ip.empty()) {
return false;
}
stub_ = std::make_unique<vm_tools::container::ContainerListener::Stub>(
grpc::CreateChannel(
base::StringPrintf("%s:%u", host_ip.c_str(), vm_tools::kGarconPort),
grpc::InsecureChannelCredentials()));
if (!NotifyHostGarconIsReady()) {
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;
}
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
signal_fd_.get(), true /*persistent*/,
base::MessageLoopForIO::WATCH_READ, &signal_controller_, this)) {
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, true,
base::Bind(&HostNotifier::DesktopPathsChanged,
weak_ptr_factory_.GetWeakPtr()))) {
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));
}
// If this fails, don't terminate ourself, this could be some kind of
// transient failure.
SendAppListToHost();
return true;
}
bool HostNotifier::NotifyHostGarconIsReady() {
// Notify the host system that we are ready.
grpc::ClientContext ctx;
vm_tools::container::ContainerStartupInfo startup_info;
startup_info.set_token(token_);
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::SendAppListToHost() {
// Generate the protobufs for the list of all the installed applications and
// make the gRPC call to the host to update them.
vm_tools::container::UpdateApplicationListRequest request;
request.set_token(token_);
vm_tools::EmptyMessage empty;
// If we hit duplicate IDs, then we are supposed to use the first one only.
std::set<std::string> unique_app_ids;
// 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;
}
// 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() ||
!unique_app_ids.insert(desktop_file->app_id()).second) {
continue;
}
// Add this app to the list in the protobuf and populate all of its
// fields.
vm_tools::container::Application* app = 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);
}
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());
}
}
// Clear this in case it was set, this all happens on the same thread.
update_app_list_posted_ = false;
// Now make the gRPC call to send this list to the host.
grpc::ClientContext ctx;
grpc::Status status = stub_->UpdateApplicationList(&ctx, request, &empty);
if (!status.ok()) {
LOG(WARNING) << "Failed to notify host of the application list: "
<< status.error_message();
}
}
void HostNotifier::DesktopPathsChanged(const base::FilePath& path, bool error) {
if (error) {
// TODO(jkardatzke): Determine best how to handle errors here. We may want
// to restart this specific watcher in that case.
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;
}
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::Bind(&HostNotifier::SendAppListToHost,
weak_ptr_factory_.GetWeakPtr()),
kFilesystemChangeCoalesceTime);
update_app_list_posted_ = true;
}
} // namespace garcon
} // namespace vm_tools