blob: 5ab0f8df00cfed339855aa6f42383862854adc08 [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 <fidl/fuchsia.boot/cpp/wire.h>
#include <fidl/fuchsia.device/cpp/wire.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <fidl/fuchsia.virtualconsole/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/device-watcher/cpp/device-watcher.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/namespace.h>
#include <lib/fdio/spawn.h>
#include <lib/fit/defer.h>
#include <lib/stdcompat/string_view.h>
#include <lib/syslog/cpp/log_settings.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/debuglog.h>
#include <lib/zx/process.h>
#include <lib/zx/time.h>
#include <zircon/processargs.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <algorithm>
#include <future>
#include <latch>
#include <utility>
#include <fbl/ref_ptr.h>
#include "src/bringup/bin/console-launcher/console_launcher.h"
#include "src/lib/fxl/strings/split_string.h"
#include "src/lib/loader_service/loader_service.h"
#include "src/storage/lib/vfs/cpp/managed_vfs.h"
#include "src/storage/lib/vfs/cpp/pseudo_dir.h"
#include "src/storage/lib/vfs/cpp/remote_dir.h"
#include "src/storage/lib/vfs/cpp/vfs_types.h"
#include "src/storage/lib/vfs/cpp/vnode.h"
#include "src/sys/lib/stdout-to-debuglog/cpp/stdout-to-debuglog.h"
namespace {
namespace fio = fuchsia_io;
template <typename FOnOpen, typename FOnRepresentation>
class EventHandler : public fidl::WireSyncEventHandler<fuchsia_io::Directory> {
public:
explicit EventHandler(FOnOpen on_open, FOnRepresentation on_representation)
: on_open_(std::move(on_open)), on_representation_(std::move(on_representation)) {}
void OnOpen(fidl::WireEvent<fuchsia_io::Directory::OnOpen>* event) override { on_open_(event); }
void OnRepresentation(fidl::WireEvent<fuchsia_io::Directory::OnRepresentation>* event) override {
on_representation_(event);
}
private:
FOnOpen on_open_;
FOnRepresentation on_representation_;
};
std::ostream& operator<<(std::ostream& os, const std::vector<std::string>& args) {
for (size_t i = 0; i < args.size(); ++i) {
if (i != 0) {
os << ' ';
}
os << args[i];
}
return os;
}
zx::result<fidl::ClientEnd<fuchsia_hardware_pty::Device>> ConnectToPty(
const console_launcher::Arguments& args) {
if (!args.device_topological_suffix.has_value()) {
return component::Connect<fuchsia_hardware_pty::Device>("/svc/console");
}
std::string_view suffix = args.device_topological_suffix.value();
zx::result console_directory_result = component::OpenServiceRoot("/dev/class/console");
if (console_directory_result.is_error()) {
return console_directory_result.take_error();
}
fidl::ClientEnd console_directory = std::move(console_directory_result.value());
zx::result watch_result = device_watcher::WatchDirectoryForItems<
zx::result<fidl::ClientEnd<fuchsia_hardware_pty::Device>>>(
console_directory,
[&](std::string_view file_name)
-> std::optional<zx::result<fidl::ClientEnd<fuchsia_hardware_pty::Device>>> {
std::string controller_path = std::string(file_name).append("/device_controller");
zx::result controller =
component::ConnectAt<fuchsia_device::Controller>(console_directory, controller_path);
if (controller.is_error()) {
return controller.take_error();
}
fidl::WireResult result = fidl::WireCall(controller.value())->GetTopologicalPath();
if (!result.ok()) {
return zx::error(result.status());
}
fit::result response = result.value();
if (response.is_error()) {
return response.take_error();
}
if (!cpp20::ends_with(response.value()->path.get(), suffix)) {
return std::nullopt;
}
return component::ConnectAt<fuchsia_hardware_pty::Device>(console_directory, file_name);
});
if (watch_result.is_error()) {
return watch_result.take_error();
}
return std::move(watch_result.value());
}
zx::result<fidl::ClientEnd<fuchsia_hardware_pty::Device>> CreateVirtualConsole(
const fidl::WireSyncClient<fuchsia_virtualconsole::SessionManager>& session_manager) {
auto [client, server] = fidl::Endpoints<fuchsia_hardware_pty::Device>::Create();
const fidl::Status result = session_manager->CreateSession(std::move(server));
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "failed to create virtcon session";
return zx::error(result.status());
}
return zx::ok(std::move(client));
}
std::vector<std::thread> LaunchAutorun(const console_launcher::ConsoleLauncher& launcher,
std::shared_ptr<loader::LoaderService> ldsvc,
fs::FuchsiaVfs& vfs, const fbl::RefPtr<fs::Vnode>& root,
std::unordered_map<std::string_view, std::thread>& threads,
const console_launcher::Arguments& args) {
std::tuple<const char*, const std::string&, std::vector<std::string_view>> map[] = {
// NB: //tools/emulator/emulator.go expects these to be available in its boot autorun.
{"autorun:boot", args.autorun_boot, {"/dev"}},
{"autorun:system", args.autorun_system, {"/system"}},
};
std::vector<std::thread> autorun;
for (const auto& [name, args, paths] : map) {
if (args.empty()) {
continue;
}
if (!cpp20::starts_with(std::string_view{args}, "/")) {
FX_LOGS(ERROR) << name << " failed to run '" << args << "' command must be absolute path";
continue;
}
zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (endpoints.is_error()) {
FX_PLOGS(FATAL, endpoints.status_value()) << "failed to create endpoints";
}
if (zx_status_t status =
vfs.ServeDirectory(root, std::move(endpoints->server), fs::Rights::All());
status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to serve root directory";
}
zx::result loader = ldsvc->Connect();
if (loader.is_error()) {
FX_PLOGS(FATAL, loader.status_value()) << "failed to connect to loader service";
}
// Get the full commandline by splitting on '+'.
std::vector argv = fxl::SplitStringCopy(args, "+", fxl::WhiteSpaceHandling::kTrimWhitespace,
fxl::SplitResult::kSplitWantNonEmpty);
autorun.emplace_back([paths = paths, &threads, args = std::move(argv), name = name,
loader = std::move(*loader), client_end = std::move(endpoints->client),
&job = launcher.shell_job()]() mutable {
for (std::string_view path : paths) {
if (auto it = threads.find(path); it != threads.end()) {
it->second.join();
} else {
FX_LOGS(ERROR) << "unable to run '" << name << "': could not mount required path '"
<< path << "'";
return;
}
}
const char* argv[args.size() + 1];
argv[args.size()] = nullptr;
for (size_t i = 0; i < args.size(); ++i) {
argv[i] = args[i].c_str();
}
fdio_spawn_action_t actions[] = {
{
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name =
{
.data = name,
},
},
{
.action = FDIO_SPAWN_ACTION_ADD_NS_ENTRY,
.ns =
{
.prefix = "/",
.handle = client_end.channel().get(),
},
},
{
.action = FDIO_SPAWN_ACTION_ADD_HANDLE,
.h =
{
.id = PA_HND(PA_LDSVC_LOADER, 0),
.handle = loader.TakeChannel().release(),
},
},
};
zx::process process;
char err_msg[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
constexpr uint32_t flags =
FDIO_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_NAMESPACE & ~FDIO_SPAWN_DEFAULT_LDSVC;
FX_LOGS(INFO) << "starting '" << name << "': " << args;
zx_status_t status =
fdio_spawn_etc(job.get(), flags, argv[0], argv, nullptr, std::size(actions), actions,
process.reset_and_get_address(), err_msg);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to start '" << name << "': " << err_msg;
return;
}
if (zx_status_t status =
process.wait_one(ZX_PROCESS_TERMINATED, zx::time::infinite(), nullptr);
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to wait for '" << name << "' termination";
}
FX_LOGS(INFO) << "completed '" << name << "': " << args;
});
}
return autorun;
}
[[noreturn]] void RunSerialConsole(const console_launcher::ConsoleLauncher& launcher,
std::shared_ptr<loader::LoaderService> ldsvc,
fs::FuchsiaVfs& vfs, const fbl::RefPtr<fs::Vnode>& root,
fidl::ClientEnd<fuchsia_hardware_pty::Device> stdio,
const std::string& term, const std::optional<std::string>& cmd) {
while (true) {
auto [client, server] = fidl::Endpoints<fuchsia_hardware_pty::Device>::Create();
const fidl::Status result = fidl::WireCall(stdio)->Clone2(
fidl::ServerEnd<fuchsia_unknown::Cloneable>(server.TakeChannel()));
if (!result.ok()) {
FX_PLOGS(FATAL, result.status()) << "failed to clone stdio handle";
}
zx::result directory = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (directory.is_error()) {
FX_PLOGS(FATAL, directory.status_value()) << "failed to create directory endpoints";
}
if (zx_status_t status =
vfs.ServeDirectory(root, std::move(directory->server), fs::Rights::All());
status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to serve root directory";
}
zx::result loader = ldsvc->Connect();
if (loader.is_error()) {
FX_PLOGS(FATAL, loader.status_value()) << "failed to connect to loader service";
}
zx::result process = launcher.LaunchShell(std::move(directory->client), std::move(*loader),
std::move(client), term, cmd);
if (process.is_error()) {
FX_PLOGS(FATAL, process.status_value()) << "failed to launch shell";
}
if (zx_status_t status = console_launcher::WaitForExit(std::move(process.value()));
status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to wait for shell exit";
}
}
}
} // namespace
int main(int argv, char** argc) {
fuchsia_logging::SetTags({"console-launcher"});
if (zx_status_t status = StdoutToDebuglog::Init(); status != ZX_OK) {
FX_PLOGS(ERROR, status)
<< "failed to redirect stdout to debuglog, assuming test environment and continuing";
}
FX_LOGS(INFO) << "running";
zx::result boot_args = component::Connect<fuchsia_boot::Arguments>();
if (boot_args.is_error()) {
FX_PLOGS(FATAL, boot_args.status_value())
<< "failed to connect to " << fidl::DiscoverableProtocolName<fuchsia_boot::Arguments>;
}
zx::result get_args = console_launcher::GetArguments(boot_args.value());
if (get_args.is_error()) {
FX_PLOGS(FATAL, get_args.status_value()) << "failed to get arguments";
}
console_launcher::Arguments args = get_args.value();
async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
async_dispatcher_t* dispatcher = loop.dispatcher();
fbl::RefPtr root = fbl::MakeRefCounted<fs::PseudoDir>();
std::unordered_map<std::string_view, std::thread> threads;
fdio_flat_namespace_t* flat;
if (zx_status_t status = fdio_ns_export_root(&flat); status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to get namespace root";
}
auto free_flat = fit::defer([&flat]() { fdio_ns_free_flat_ns(flat); });
// Our incoming namespace contains directories provided by fshost that may not
// yet be responding to requests. This is ordinarily fine, but can cause
// indefinite hangs in an interactive shell when storage devices fail to
// start.
//
// Rather than expose these directly to the shell, indirect through a local
// VFS to which entries are added only once they are seen to be servicing
// requests. This causes the shell to initially observe an empty root
// directory to which entries are added once they are ready for blocking
// operations.
for (size_t i = 0; i < flat->count; ++i) {
zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (endpoints.is_error()) {
FX_PLOGS(FATAL, endpoints.status_value()) << "failed to create endpoints";
}
std::string_view path = flat->path[i];
const fidl::Status result =
fidl::WireCall(fidl::UnownedClientEnd<fuchsia_io::Directory>(flat->handle[i]))
->Clone(fuchsia_io::wire::OpenFlags::kDescribe |
fuchsia_io::wire::OpenFlags::kCloneSameRights,
fidl::ServerEnd<fuchsia_io::Node>(endpoints->server.TakeChannel()));
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "failed to clone '" << path << "'";
continue;
}
// TODO(https://fxbug.dev/42147799): Replace the use of threads with async clients when it is
// possible to extract the channel from the client.
auto [thread,
inserted] = threads.emplace(path, [&root, client_end = std::move(endpoints->client),
dispatcher, path]() mutable {
EventHandler handler(
[&](fidl::WireEvent<fuchsia_io::Directory::OnOpen>* event) {
if (event->s != ZX_OK) {
FX_PLOGS(ERROR, event->s) << "failed to open '" << path << "'";
return;
}
// Must run on the dispatcher thread to avoid racing with VFS dispatch.
std::latch mounted(1);
async::PostTask(dispatcher, [&mounted, &root, path,
client_end = std::move(client_end)]() mutable {
const std::vector components = fxl::SplitString(path, "/", fxl::kKeepWhitespace,
fxl::SplitResult::kSplitWantNonEmpty);
fbl::RefPtr<fs::Vnode> current = root;
for (size_t i = 0; i < components.size(); i++) {
const std::string_view& component = components[i];
const std::string_view fragment = [&]() {
const ssize_t fragment_len = std::distance(path.begin(), component.end());
if (fragment_len < 0) {
const void* path_ptr = path.data();
const void* component_ptr = component.data();
FX_LOGS(FATAL) << "expected overlapping memory:" << " path@" << path_ptr << "="
<< path << " component@" << component_ptr << "=" << component;
}
return std::string_view{path.data(), static_cast<size_t>(fragment_len)};
}();
fbl::RefPtr<fs::Vnode> next;
if (i == components.size() - 1) {
next = fbl::MakeRefCounted<fs::RemoteDir>(std::move(client_end));
} else {
switch (zx_status_t status = current->Lookup(component, &current); status) {
case ZX_OK:
continue;
case ZX_ERR_NOT_FOUND:
next = fbl::MakeRefCounted<fs::PseudoDir>();
break;
default:
FX_PLOGS(FATAL, status) << "Lookup(" << fragment << ")";
}
}
if (zx_status_t status =
fbl::RefPtr<fs::PseudoDir>::Downcast(current)->AddEntry(component, next);
status != ZX_OK) {
FX_PLOGS(FATAL, status) << "failed to add entry for '" << fragment << "'";
}
current = next;
}
FX_LOGS(INFO) << "mounted '" << path << "'";
mounted.count_down();
});
mounted.wait();
},
[&](fidl::WireEvent<fuchsia_io::Directory::OnRepresentation>* event) {
FX_PLOGS(FATAL, ZX_ERR_NOT_SUPPORTED) << "unexpected OnRepresentation";
});
if (fidl::Status status = handler.HandleOneEvent(client_end); !status.ok()) {
FX_PLOGS(ERROR, status.status()) << "failed to receive OnOpen event for '" << path << "'";
}
});
if (!inserted) {
FX_LOGS(FATAL) << "duplicate namespace entry: " << path;
}
}
std::thread thread([&loop]() {
if (zx_status_t status = loop.Run(); status != ZX_OK) {
FX_PLOGS(ERROR, status) << "VFS loop exited";
}
});
fs::ManagedVfs vfs(dispatcher);
fbl::unique_fd lib_fd;
if (zx_status_t status =
fdio_open_fd("/boot/lib/",
static_cast<uint32_t>(fio::wire::OpenFlags::kDirectory |
fio::wire::OpenFlags::kRightReadable |
fio::wire::OpenFlags::kRightExecutable),
lib_fd.reset_and_get_address());
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "VFS loop exited";
}
auto ldsvc = loader::LoaderService::Create(dispatcher, std::move(lib_fd), "console-launcher");
zx::result result = console_launcher::ConsoleLauncher::Create();
if (result.is_error()) {
FX_PLOGS(FATAL, result.status_value()) << "failed to create console launcher";
}
const auto& launcher = result.value();
std::vector<std::thread> workers;
if (!args.virtcon_disable) {
zx_status_t status = [&]() {
zx::result virtcon = component::Connect<fuchsia_virtualconsole::SessionManager>();
if (virtcon.is_error()) {
FX_PLOGS(ERROR, virtcon.status_value())
<< "failed to connect to "
<< fidl::DiscoverableProtocolName<fuchsia_virtualconsole::SessionManager>;
return virtcon.status_value();
}
fidl::WireSyncClient client{std::move(virtcon.value())};
if (args.virtual_console_need_debuglog) {
zx::result session = CreateVirtualConsole(client);
if (session.is_error()) {
return session.status_value();
}
workers.emplace_back([&, stdio = std::move(session.value())]() mutable {
RunSerialConsole(launcher, ldsvc, vfs, root, std::move(stdio), args.term, "dlog -f -t");
});
}
zx::result session = CreateVirtualConsole(client);
if (session.is_error()) {
return session.status_value();
}
workers.emplace_back([&, stdio = std::move(session.value())]() mutable {
RunSerialConsole(launcher, ldsvc, vfs, root, std::move(stdio), "TERM=xterm-256color", {});
});
return ZX_OK;
}();
if (status != ZX_OK) {
// If launching virtcon fails, we still should continue so that the autorun programs
// and serial console are launched.
FX_PLOGS(ERROR, status) << "failed to set up virtcon";
}
}
if (args.run_shell) {
FX_LOGS(INFO) << "console.shell: enabled";
{
std::vector<std::thread> autorun = LaunchAutorun(launcher, ldsvc, vfs, root, threads, args);
workers.insert(workers.end(), std::make_move_iterator(autorun.begin()),
std::make_move_iterator(autorun.end()));
}
zx::result pty_result = ConnectToPty(args);
if (pty_result.is_error()) {
FX_PLOGS(FATAL, pty_result.error_value()) << "Failed to connect to PTY";
}
workers.emplace_back([&, stdio = std::move(pty_result.value())]() mutable {
RunSerialConsole(launcher, ldsvc, vfs, root, std::move(stdio), args.term, {});
});
} else {
if (!args.autorun_boot.empty()) {
FX_LOGS(ERROR) << "cannot launch autorun command '" << args.autorun_boot << "'";
}
FX_LOGS(INFO) << "console.shell: disabled";
for (auto& [_, thread] : threads) {
thread.join();
}
thread.join();
}
for (auto& thread : workers) {
thread.join();
}
// TODO(https://fxbug.dev/42179909): Hang around. If we exit before archivist has started, our
// logs will be lost, and this log is load bearing in shell_disabled_test.
std::promise<void>().get_future().wait();
}