| // 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, ¤t); 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(); |
| } |