// Copyright (C) 2020 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "common/cmd_utils.h" #include "inode2filename/inode_resolver.h" #include "inode2filename/out_of_process_inode_resolver.h" #include #include #include #include #include #include namespace rx = rxcpp; namespace iorap::inode2filename { using ::android::base::unique_fd; using ::android::base::borrowed_fd; static const char* GetCommandFileName() { // Avoid ENOENT by execve by specifying the absolute path of inode2filename. #ifdef __ANDROID__ return "/system/bin/iorap.inode2filename"; #else static const char* file_name = nullptr; if (file_name == nullptr) { char* out_dir = getenv("ANDROID_HOST_OUT"); static std::string file_name_str = out_dir; if (out_dir != nullptr) { file_name_str += "/bin/"; } else { // Assume it's in the same directory as the binary we are in. std::string self_path; CHECK(::android::base::Readlink("/proc/self/exe", /*out*/&self_path)); std::string self_dir = ::android::base::Dirname(self_path); file_name_str = self_dir + "/"; } file_name_str += "iorap.inode2filename"; file_name = file_name_str.c_str(); } return file_name; #endif } std::error_code ErrorCodeFromErrno() { int err = errno; return std::error_code(err, std::system_category()); } std::ios_base::failure IosBaseFailureWithErrno(const char* message) { std::error_code ec = ErrorCodeFromErrno(); return std::ios_base::failure(message, ec); } static constexpr bool kDebugFgets = false; int32_t ReadLineLength(FILE* stream, bool* eof) { char buf[sizeof(int32_t)]; size_t count = fread(buf, 1, sizeof(int32_t), stream); if (feof(stream)) { // If reaching the end of the stream when trying to read the first int, just // return. This is legitimate, because after reading the last line, the next // iteration will reach this. *eof = true; return 0; } int32_t length; memcpy(&length, buf, sizeof(int32_t)); return length; } // The steam is like [size1][file1][size2][file2]...[sizeN][fileN]. std::string ReadOneLine(FILE* stream, bool* eof) { DCHECK(stream != nullptr); DCHECK(eof != nullptr); int32_t length = ReadLineLength(stream, eof); if (length <= 0) { PLOG(ERROR) << "unexpected 0 length line."; *eof = true; return ""; } std::string str(length, '\0'); size_t count = fread(&str[0], sizeof(char), length, stream); if (feof(stream) || ferror(stream) || count != (uint32_t)length) { // error! :( PLOG(ERROR) << "unexpected end of the line during fread"; *eof = true; return ""; } return str; } static inline void LeftTrim(/*inout*/std::string& s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); })); } // Parses an --output-format=ipc kind of line into an InodeResult. // Returns nullopt if parsing failed. std::optional ParseFromLine(const std::string& line) { // inode <- INT:INT:INT // line_ok <- 'K ' inode ' ' STRING // line_err <- 'E ' inode ' ' INT // // result <- line_ok | line_err std::stringstream ss{line}; bool result_ok = false; std::string ok_or_error; ss >> ok_or_error; if (ss.fail()) { return std::nullopt; } if (ok_or_error == "K") { result_ok = true; } else if (ok_or_error == "E") { result_ok = false; } else { return std::nullopt; } std::string inode_s; ss >> inode_s; if (ss.fail()) { return std::nullopt; } Inode inode; std::string inode_parse_error_msg; if (!Inode::Parse(inode_s, /*out*/&inode, /*out*/&inode_parse_error_msg)) { return std::nullopt; } if (result_ok == false) { int error_code; ss >> error_code; if (ss.fail()) { return std::nullopt; } return InodeResult::makeFailure(inode, error_code); } else if (result_ok == true) { std::string rest_of_line; ss >> rest_of_line; // parse " string with potential spaces" // into "string with potential spaces" LeftTrim(/*inout*/rest_of_line); if (ss.fail()) { return std::nullopt; } const std::string& file_name = rest_of_line; return InodeResult::makeSuccess(inode, file_name); } return std::nullopt; } struct OutOfProcessInodeResolver::Impl { Impl() { } private: // Create the argv we will pass to the forked inode2filename, corresponds to #EmitAll. std::vector CreateArgvAll(const InodeResolverDependencies& deps) const { std::vector argv; argv.push_back("--all"); return CreateArgv(deps, std::move(argv)); } // Create the argv we will pass to the forked inode2filename, corresponds to // #FindFilenamesFromInodes. std::vector CreateArgvFind(const InodeResolverDependencies& deps, const std::vector& inodes) const { std::vector argv; iorap::common::AppendArgsRepeatedly(argv, inodes); return CreateArgv(deps, std::move(argv)); } std::vector CreateArgv(const InodeResolverDependencies& deps, std::vector append_argv) const { InodeResolverDependencies deps_oop = deps; deps_oop.process_mode = ProcessMode::kInProcessDirect; std::vector argv = ToArgs(deps_oop); argv.push_back("--output-format=ipc"); if (iorap::common::GetBoolEnvOrProperty("iorap.inode2filename.log.verbose", false)) { argv.push_back("--verbose"); } iorap::common::AppendArgsRepeatedly(argv, std::move(append_argv)); return argv; } public: // fork+exec into inode2filename with 'inodes' as the search list. // Each result is parsed into a dest#on_next(result). // If a fatal error occurs, dest#on_error is called once and no other callbacks are called. void EmitFromCommandFind(rxcpp::subscriber& dest, const InodeResolverDependencies& deps, const std::vector& inodes) { // Trivial case: complete immediately. // Executing inode2filename with empty search list will just print the --help menu. if (inodes.empty()) { dest.on_completed(); } std::vector argv = CreateArgvFind(deps, inodes); EmitFromCommandWithArgv(/*inout*/dest, std::move(argv), inodes.size()); } // fork+exec into inode2filename with --all (listing *all* inodes). // Each result is parsed into a dest#on_next(result). // If a fatal error occurs, dest#on_error is called once and no other callbacks are called. void EmitFromCommandAll(rxcpp::subscriber& dest, const InodeResolverDependencies& deps) { std::vector argv = CreateArgvAll(deps); EmitFromCommandWithArgv(/*inout*/dest, std::move(argv), /*result_count*/std::nullopt); } private: void EmitFromCommandWithArgv(rxcpp::subscriber& dest, std::vector argv_vec, std::optional result_count) { unique_fd pipe_reader, pipe_writer; if (!::android::base::Pipe(/*out*/&pipe_reader, /*out*/&pipe_writer)) { dest.on_error( rxcpp::util::make_error_ptr( IosBaseFailureWithErrno("Failed to create out-going pipe for inode2filename"))); return; } pid_t child = fork(); if (child == -1) { dest.on_error( rxcpp::util::make_error_ptr( IosBaseFailureWithErrno("Failed to fork process for inode2filename"))); return; } else if (child > 0) { // we are the caller of this function LOG(DEBUG) << "forked into a process for inode2filename , pid = " << child; } else { // we are the child that was forked const char* kCommandFileName = GetCommandFileName(); std::stringstream argv; // for debugging. for (std::string arg : argv_vec) { argv << arg << ' '; } LOG(DEBUG) << "fork+exec: " << kCommandFileName << " " << argv.str(); // Redirect only stdout. stdin is unused, stderr is same as parent. if (dup2(pipe_writer.get(), STDOUT_FILENO) == -1) { // Trying to call #on_error does not make sense here because we are in a forked process, // the only thing we can do is crash definitively. PLOG(FATAL) << "Failed to dup2 for inode2filename"; } std::unique_ptr argv_ptr = common::VecToArgv(kCommandFileName, argv_vec); if (execve(kCommandFileName, (char **)argv_ptr.get(), /*envp*/nullptr) == -1) { // Trying to call #on_error does not make sense here because we are in a forked process, // the only thing we can do is crash definitively. PLOG(FATAL) << "Failed to execve process for inode2filename"; } // This should never return. } // Immediately close the writer end of the pipe because we never use it. pipe_writer.reset(); // Convert pipe(reader) file descriptor into FILE*. std::unique_ptr file_reader{ ::android::base::Fdopen(std::move(pipe_reader), /*mode*/"r"), fclose}; if (!file_reader) { dest.on_error( rxcpp::util::make_error_ptr( IosBaseFailureWithErrno("Failed to fdopen for inode2filename"))); return; } size_t actual_result_count = 0; bool file_eof = false; while (!file_eof) { std::string inode2filename_line = ReadOneLine(file_reader.get(), /*out*/&file_eof); if (inode2filename_line.empty()) { if (!file_eof) { // Ignore empty lines. LOG(WARNING) << "inode2filename: got empty line"; } continue; } LOG(DEBUG) << "inode2filename output-line: " << inode2filename_line; std::optional res = ParseFromLine(inode2filename_line); if (!res) { std::string error_msg = "Invalid output: "; error_msg += inode2filename_line; dest.on_error( rxcpp::util::make_error_ptr(std::ios_base::failure(error_msg))); return; } dest.on_next(*res); ++actual_result_count; } LOG(DEBUG) << "inode2filename output-eof"; // Ensure that the # of inputs into the rx stream match the # of outputs. // This is validating the post-condition of FindFilenamesFromInodes. if (result_count && actual_result_count != *result_count) { std::stringstream ss; ss << "Invalid number of results, expected: " << *result_count; ss << ", actual: " << actual_result_count; dest.on_error( rxcpp::util::make_error_ptr(std::ios_base::failure(ss.str()))); return; } CHECK(child > 0); // we are in the parent process, parse the IPC output of inode2filename dest.on_completed(); } }; rxcpp::observable OutOfProcessInodeResolver::FindFilenamesFromInodes(std::vector inodes) const { return rxcpp::observable<>::create( [self=std::static_pointer_cast(shared_from_this()), inodes=std::move(inodes)]( rxcpp::subscriber s) { self->impl_->EmitFromCommandFind(s, self->GetDependencies(), inodes); }); } rxcpp::observable OutOfProcessInodeResolver::EmitAll() const { auto self = std::static_pointer_cast(shared_from_this()); CHECK(self != nullptr); CHECK(self->impl_ != nullptr); return rxcpp::observable<>::create( [self](rxcpp::subscriber s) { CHECK(self != nullptr); CHECK(self->impl_ != nullptr); self->impl_->EmitFromCommandAll(s, self->GetDependencies()); }); } OutOfProcessInodeResolver::OutOfProcessInodeResolver(InodeResolverDependencies dependencies) : InodeResolver{std::move(dependencies)}, impl_{new Impl{}} { } OutOfProcessInodeResolver::~OutOfProcessInodeResolver() { // std::unique_ptr requires complete types, but we hide the definition in the header. delete impl_; } } // namespace iorap::inode2filename