| // Copyright 2022 The Centipede Authors. | |
| // | |
| // 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 | |
| // | |
| // https://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 "./centipede/config_file.h" | |
| #include <cstdlib> | |
| #include <filesystem> // NOLINT | |
| #include <memory> | |
| #include <set> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/nullability.h" | |
| #include "absl/flags/declare.h" | |
| #include "absl/flags/flag.h" | |
| #include "absl/flags/parse.h" | |
| #include "absl/flags/reflection.h" | |
| #include "absl/log/check.h" | |
| #include "absl/log/log.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/str_replace.h" | |
| #include "absl/strings/substitute.h" | |
| #include "./centipede/config_init.h" | |
| #include "./centipede/config_util.h" | |
| #include "./centipede/logging.h" | |
| #include "./centipede/remote_file.h" | |
| #include "./centipede/util.h" | |
| // TODO(ussuri): Move these flags next to main() ASAP. They are here | |
| // only temporarily to simplify the APIs and implementation in V1. | |
| ABSL_FLAG(std::string, config, "", | |
| "Read flags from the specified file. The file can be either local or " | |
| "remote. Relative paths are referenced from the CWD. The format " | |
| "should be:\n" | |
| "--flag=value\n" | |
| "--another_flag=value\n" | |
| "...\n" | |
| "Lines that start with '#' or '//' are comments. Note that this " | |
| "format is compatible with the built-in --flagfile flag (defined by " | |
| "Abseil Flags library); however, unlike this flag, --flagfile " | |
| "supports only local files.\n" | |
| "Nested --load_config's won't work (but nested --flagfile's will," | |
| "provided they point at a local file, e.g. $HOME/.centipede_rc).\n" | |
| "The flag is position-sensitive: flags read from it override (or " | |
| "append, in case of std::vector flags) any previous occurrences of " | |
| "the same flags on the command line, and vice versa."); | |
| ABSL_FLAG(std::string, save_config, "", | |
| "Saves Centipede flags to the specified file and exits the program." | |
| "The file can be either local or remote. Relative paths are " | |
| "referenced from the CWD. Both the command-line flags and defaulted " | |
| "flags are saved (the defaulted flags are commented out). The format " | |
| "is:\n" | |
| "# --flag's help string.\n" | |
| "# --flag's default value.\n" | |
| "--flag=value\n" | |
| "...\n" | |
| "This format can be parsed back by both --config and --flagfile. " | |
| "Unlike those two flags, this flag is not position-sensitive and " | |
| "always saves the final resolved config.\n" | |
| "Special case: if the file's extension is .sh, a runnable shell " | |
| "script is saved instead."); | |
| ABSL_FLAG(bool, update_config, false, | |
| "Must be used in combination with --config=<file>. Writes the final " | |
| "resolved config back to the same file."); | |
| ABSL_FLAG(bool, print_config, false, | |
| "Print the config to stderr upon starting Centipede."); | |
| // Declare --flagfile defined by the Abseil Flags library. The flag should point | |
| // at a _local_ file is always automatically parsed by Abseil Flags. | |
| ABSL_DECLARE_FLAG(std::vector<std::string>, flagfile); | |
| #define DASHED_FLAG_NAME(name) "--" << FLAGS_##name.Name() | |
| namespace centipede::config { | |
| AugmentedArgvWithCleanup::AugmentedArgvWithCleanup( | |
| const std::vector<std::string>& orig_argv, const Replacements& replacements, | |
| BackingResourcesCleanup&& cleanup) | |
| : was_augmented_{false}, cleanup_{cleanup} { | |
| argv_.reserve(orig_argv.size()); | |
| for (const auto& old_arg : orig_argv) { | |
| const std::string& new_arg = | |
| argv_.emplace_back(absl::StrReplaceAll(old_arg, replacements)); | |
| if (new_arg != old_arg) { | |
| VLOG(1) << "Augmented argv arg:\n" << VV(old_arg) << "\n" << VV(new_arg); | |
| was_augmented_ = true; | |
| } | |
| } | |
| } | |
| AugmentedArgvWithCleanup::AugmentedArgvWithCleanup( | |
| AugmentedArgvWithCleanup&& rhs) noexcept { | |
| *this = std::move(rhs); | |
| } | |
| AugmentedArgvWithCleanup& AugmentedArgvWithCleanup::operator=( | |
| AugmentedArgvWithCleanup&& rhs) noexcept { | |
| argv_ = std::move(rhs.argv_); | |
| was_augmented_ = rhs.was_augmented_; | |
| cleanup_ = std::move(rhs.cleanup_); | |
| // Prevent rhs from calling the cleanup in dtor (moving an std::function | |
| // leaves the moved object in a valid, but undefined, state). | |
| rhs.cleanup_ = {}; | |
| return *this; | |
| } | |
| AugmentedArgvWithCleanup::~AugmentedArgvWithCleanup() { | |
| if (cleanup_) cleanup_(); | |
| } | |
| AugmentedArgvWithCleanup LocalizeConfigFilesInArgv( | |
| const std::vector<std::string>& argv) { | |
| const std::filesystem::path path = absl::GetFlag(FLAGS_config); | |
| if (!path.empty()) { | |
| CHECK_NE(path, absl::GetFlag(FLAGS_save_config)) | |
| << "To update config in place, use " << DASHED_FLAG_NAME(update_config); | |
| } | |
| // Always need these (--config=<path> can be passed with a local <path>). | |
| AugmentedArgvWithCleanup::Replacements replacements = { | |
| // "-". not "--" to support the shortened "-flag" form as well. | |
| // TODO(ussuri): Fix for usage without =, i.e. `--config <file>`. | |
| {absl::StrCat("-", FLAGS_config.Name(), "="), | |
| absl::StrCat("-", FLAGS_flagfile.Name(), "=")}, | |
| }; | |
| AugmentedArgvWithCleanup::BackingResourcesCleanup cleanup; | |
| // Copy the remote config file to a temporary local mirror. | |
| if (!path.empty() && !std::filesystem::exists(path)) { // assume remote | |
| // Read the remote file. | |
| std::string contents; | |
| RemoteFileGetContents(path, contents); | |
| // Save a temporary local copy. | |
| const std::filesystem::path tmp_dir = TemporaryLocalDirPath(); | |
| const std::filesystem::path local_path = tmp_dir / path.filename(); | |
| LOG(INFO) << "Localizing remote config: " << VV(path) << VV(local_path); | |
| // NOTE: Ignore "Remote" in the API names here: the paths are always local. | |
| RemoteMkdir(tmp_dir.c_str()); | |
| RemoteFileSetContents(local_path, contents); | |
| // Augment the argv to point at the local copy and ensure it is cleaned up. | |
| replacements.emplace_back(path.c_str(), local_path.c_str()); | |
| cleanup = [local_path]() { std::filesystem::remove(local_path); }; | |
| } | |
| return AugmentedArgvWithCleanup{argv, replacements, std::move(cleanup)}; | |
| } | |
| std::filesystem::path MaybeSaveConfigToFile( | |
| const std::vector<std::string>& leftover_argv) { | |
| std::filesystem::path path; | |
| // Initialize `path` if --save_config or --update_config is passed. | |
| if (!absl::GetFlag(FLAGS_save_config).empty()) { | |
| path = absl::GetFlag(FLAGS_save_config); | |
| CHECK_NE(path, absl::GetFlag(FLAGS_config)) | |
| << "To update config in place, use " << DASHED_FLAG_NAME(update_config); | |
| CHECK(!absl::GetFlag(FLAGS_update_config)) | |
| << DASHED_FLAG_NAME(save_config) << " and " | |
| << DASHED_FLAG_NAME(update_config) << " are mutually exclusive"; | |
| } else if (absl::GetFlag(FLAGS_update_config)) { | |
| path = absl::GetFlag(FLAGS_config); | |
| CHECK(!path.empty()) << DASHED_FLAG_NAME(update_config) | |
| << " must be used in combination with " | |
| << DASHED_FLAG_NAME(config); | |
| } | |
| // Save or update the config file. | |
| if (!path.empty()) { | |
| const std::set<std::string_view> excluded_flags = { | |
| FLAGS_config.Name(), | |
| FLAGS_save_config.Name(), | |
| FLAGS_update_config.Name(), | |
| FLAGS_print_config.Name(), | |
| }; | |
| const FlagInfosPerSource flags = | |
| GetFlagsPerSource("centipede", excluded_flags); | |
| const std::string flags_str = FormatFlagfileString( | |
| flags, DefaultedFlags::kCommentedOut, FlagComments::kHelpAndDefault); | |
| std::string file_contents; | |
| if (path.extension() == ".sh") { | |
| // NOTES: 1) The first element of `leftover_argv` is expected to be the | |
| // /path/to/centipede, so the $1 in the stub will run it. | |
| // 2) absl::Substitute() replaces the escaped $$ with a $. | |
| constexpr std::string_view kScriptStub = | |
| R"(#!/bin/bash -eu | |
| declare -ra flags=( | |
| $0) | |
| if [[ -n "$1" ]]; then | |
| wd=$1 | |
| else | |
| wd=$$PWD | |
| fi | |
| read -e -p "Clear workdir (which is '$$wd') [y/N]? " yn | |
| # Tip: To default to 'y', change 'yY' to 'nN' below. | |
| if [[ "$${yn}" =~ [yY] ]]; then | |
| rm -rf "$$wd"/corpus* "$$wd"/*report*.txt "$$wd"/*/features* | |
| fi | |
| set -x | |
| $2 "$${flags[@]}" | |
| )"; | |
| const auto workdir = absl::GetAllFlags()["workdir"]->CurrentValue(); | |
| const auto argv_str = absl::StrJoin(leftover_argv, " "); | |
| file_contents = | |
| absl::Substitute(kScriptStub, flags_str, workdir, argv_str); | |
| } else { | |
| file_contents = flags_str; | |
| } | |
| RemoteFileSetContents(path, file_contents); | |
| } | |
| return path; | |
| } | |
| std::unique_ptr<RuntimeState> InitCentipede( // | |
| int argc, absl::Nonnull<char**> argv) { | |
| std::vector<std::string> leftover_argv; | |
| // main_runtime_init() is allowed to remove recognized flags from `argv`, so | |
| // we need a copy. | |
| const std::vector<std::string> saved_argv = CastArgv(argc, argv); | |
| // Among other things, this performs the initial command line parsing. | |
| std::unique_ptr<config::RuntimeState> runtime_state = InitRuntime(argc, argv); | |
| // If --config=<path> was passed, replace it with the Abseil Flags' built-in | |
| // --flagfile=<localized_path> and reparse the command line. NOTE: It would be | |
| // incorrect to just parse the contents of <path>, because --config (and | |
| // --flagfile for that matter) are position-sensitive, i.e. they may override | |
| // flags that come before on the command line, and vice versa. | |
| const AugmentedArgvWithCleanup localized_argv = | |
| LocalizeConfigFilesInArgv(saved_argv); | |
| if (localized_argv.was_augmented()) { | |
| LOG(INFO) << "Command line was augmented; reparsing"; | |
| runtime_state->leftover_argv() = CastArgv(absl::ParseCommandLine( | |
| localized_argv.argc(), CastArgv(localized_argv.argv()).data())); | |
| } | |
| // Log the final resolved config. | |
| if (absl::GetFlag(FLAGS_print_config)) { | |
| const FlagInfosPerSource flags = GetFlagsPerSource("centipede"); | |
| const std::string flags_str = FormatFlagfileString( | |
| flags, DefaultedFlags::kCommentedOut, FlagComments::kNone); | |
| LOG(INFO) << "Final resolved config:\n" << flags_str; | |
| } | |
| // If --save_config was passed, save the final resolved flags to the requested | |
| // file and exit the program. | |
| const auto path = MaybeSaveConfigToFile(leftover_argv); | |
| if (!path.empty()) { | |
| LOG(INFO) << "Config written to file: " << VV(path); | |
| LOG(INFO) << "Nothing left to do; exiting"; | |
| exit(EXIT_SUCCESS); | |
| } | |
| return runtime_state; | |
| } | |
| } // namespace centipede::config |