// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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 "src/main/cpp/startup_options.h"

#include <assert.h>

#include <cstdio>
#include <cstdlib>

#include "src/main/cpp/blaze_util.h"
#include "src/main/cpp/blaze_util_platform.h"
#include "src/main/cpp/util/errors.h"
#include "src/main/cpp/util/exit_code.h"
#include "src/main/cpp/util/file.h"
#include "src/main/cpp/util/numbers.h"
#include "src/main/cpp/util/strings.h"
#include "src/main/cpp/workspace_layout.h"

namespace blaze {

using std::string;
using std::vector;

StartupFlag::~StartupFlag() {}

bool UnaryStartupFlag::NeedsParameter() const {
  return true;
}

bool UnaryStartupFlag::IsValid(const std::string &arg) const {
  // The second argument of GetUnaryOption is not relevant to determine
  // whether the option is unary or not, hence we set it to the empty string
  // by default.
  //
  // TODO(lpino): Improve GetUnaryOption to only require the arg and the
  // option we are looking for.
  return GetUnaryOption(arg.c_str(), "", ("--" + name_).c_str()) != NULL;
}

bool NullaryStartupFlag::NeedsParameter() const {
  return false;
}

bool NullaryStartupFlag::IsValid(const std::string &arg) const {
  return GetNullaryOption(arg.c_str(), ("--" + name_).c_str()) ||
      GetNullaryOption(arg.c_str(), ("--no" + name_).c_str());
}

StartupOptions::StartupOptions(const WorkspaceLayout* workspace_layout)
    : StartupOptions("Bazel", workspace_layout) {}

void StartupOptions::RegisterNullaryStartupFlag(const std::string &flag_name) {
  valid_startup_flags.insert(std::unique_ptr<NullaryStartupFlag>(
      new NullaryStartupFlag(flag_name)));
}

void StartupOptions::RegisterUnaryStartupFlag(const std::string &flag_name) {
  valid_startup_flags.insert(std::unique_ptr<UnaryStartupFlag>(
      new UnaryStartupFlag(flag_name)));
}

StartupOptions::StartupOptions(const string &product_name,
                               const WorkspaceLayout *workspace_layout)
    : product_name(product_name),
      deep_execroot(true),
      block_for_lock(true),
      host_jvm_debug(false),
      batch(false),
      batch_cpu_scheduling(false),
      io_nice_level(-1),
      oom_more_eagerly(false),
      oom_more_eagerly_threshold(100),
      write_command_log(true),
      watchfs(false),
      fatal_event_bus_exceptions(false),
      command_port(0),
      connect_timeout_secs(30),
      invocation_policy(NULL),
      client_debug(false),
      java_logging_formatter(
          "com.google.devtools.build.lib.util.SingleLineFormatter"),
      expand_configs_in_place(true),
      original_startup_options_(std::vector<RcStartupFlag>()) {
  bool testing = !blaze::GetEnv("TEST_TMPDIR").empty();
  if (testing) {
    output_root = MakeAbsolute(blaze::GetEnv("TEST_TMPDIR"));
    max_idle_secs = 15;
    fprintf(stderr,
            "INFO: $TEST_TMPDIR defined: output root default is '%s' and "
            "max_idle_secs default is '%d'.\n",
            output_root.c_str(),
            max_idle_secs);
  } else {
    output_root = workspace_layout->GetOutputRoot();
    max_idle_secs = 3 * 3600;
  }

#if defined(COMPILER_MSVC) || defined(__CYGWIN__)
  string windows_unix_root = WindowsUnixRoot(blaze::GetEnv("BAZEL_SH"));
  if (!windows_unix_root.empty()) {
    host_jvm_args.push_back(string("-Dbazel.windows_unix_root=") +
                            windows_unix_root);
  }
#endif  // defined(COMPILER_MSVC) || defined(__CYGWIN__)

  const string product_name_lower = GetLowercaseProductName();
  output_user_root = blaze_util::JoinPath(
      output_root, "_" + product_name_lower + "_" + GetUserName());

  // IMPORTANT: Before modifying the statements below please contact a Bazel
  // core team member that knows the internal procedure for adding/deprecating
  // startup flags.
  RegisterNullaryStartupFlag("batch");
  RegisterNullaryStartupFlag("batch_cpu_scheduling");
  RegisterNullaryStartupFlag("block_for_lock");
  RegisterNullaryStartupFlag("client_debug");
  RegisterNullaryStartupFlag("deep_execroot");
  RegisterNullaryStartupFlag("experimental_oom_more_eagerly");
  RegisterNullaryStartupFlag("fatal_event_bus_exceptions");
  RegisterNullaryStartupFlag("host_jvm_debug");
  RegisterNullaryStartupFlag("master_bazelrc");
  RegisterNullaryStartupFlag("master_blazerc");
  RegisterNullaryStartupFlag("watchfs");
  RegisterNullaryStartupFlag("write_command_log");
  RegisterNullaryStartupFlag("expand_configs_in_place");
  RegisterUnaryStartupFlag("bazelrc");
  RegisterUnaryStartupFlag("blazerc");
  RegisterUnaryStartupFlag("command_port");
  RegisterUnaryStartupFlag("connect_timeout_secs");
  RegisterUnaryStartupFlag("experimental_oom_more_eagerly_threshold");
  RegisterUnaryStartupFlag("host_javabase");
  RegisterUnaryStartupFlag("host_jvm_args");
  RegisterUnaryStartupFlag("host_jvm_profile");
  RegisterUnaryStartupFlag("invocation_policy");
  RegisterUnaryStartupFlag("io_nice_level");
  RegisterUnaryStartupFlag("install_base");
  RegisterUnaryStartupFlag("max_idle_secs");
  RegisterUnaryStartupFlag("output_base");
  RegisterUnaryStartupFlag("output_user_root");
  RegisterUnaryStartupFlag("server_jvm_out");
}

StartupOptions::~StartupOptions() {}

string StartupOptions::GetLowercaseProductName() const {
  string lowercase_product_name = product_name;
  blaze_util::ToLower(&lowercase_product_name);
  return lowercase_product_name;
}

bool StartupOptions::IsNullary(const string& arg) const {
  for (const auto& flag : valid_startup_flags) {
    if (!flag->NeedsParameter() && flag->IsValid(arg)) {
      return true;
    }
  }
  return false;
}

bool StartupOptions::IsUnary(const string& arg) const {
  for (const auto& flag : valid_startup_flags) {
    if (flag->NeedsParameter() && flag->IsValid(arg)) {
      return true;
    }
  }
  return false;
}

void StartupOptions::AddExtraOptions(vector<string> *result) const {}

blaze_exit_code::ExitCode StartupOptions::ProcessArg(
      const string &argstr, const string &next_argstr, const string &rcfile,
      bool *is_space_separated, string *error) {
  // We have to parse a specific option syntax, so GNU getopts won't do.  All
  // options begin with "--" or "-". Values are given together with the option
  // delimited by '=' or in the next option.
  const char* arg = argstr.c_str();
  const char* next_arg = next_argstr.empty() ? NULL : next_argstr.c_str();
  const char* value = NULL;

  if ((value = GetUnaryOption(arg, next_arg, "--output_base")) != NULL) {
    output_base = MakeAbsolute(value);
    option_sources["output_base"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--install_base")) != NULL) {
    install_base = MakeAbsolute(value);
    option_sources["install_base"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--output_user_root")) != NULL) {
    output_user_root = MakeAbsolute(value);
    option_sources["output_user_root"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--server_jvm_out")) != NULL) {
    server_jvm_out = MakeAbsolute(value);
    option_sources["server_jvm_out"] = rcfile;
  } else if (GetNullaryOption(arg, "--deep_execroot")) {
    deep_execroot = true;
    option_sources["deep_execroot"] = rcfile;
  } else if (GetNullaryOption(arg, "--nodeep_execroot")) {
    deep_execroot = false;
    option_sources["deep_execroot"] = rcfile;
  } else if (GetNullaryOption(arg, "--noblock_for_lock")) {
    block_for_lock = false;
    option_sources["block_for_lock"] = rcfile;
  } else if (GetNullaryOption(arg, "--host_jvm_debug")) {
    host_jvm_debug = true;
    option_sources["host_jvm_debug"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--host_jvm_profile")) != NULL) {
    host_jvm_profile = value;
    option_sources["host_jvm_profile"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--host_javabase")) != NULL) {
    // TODO(bazel-team): Consider examining the javabase and re-execing in case
    // of architecture mismatch.
    host_javabase = MakeAbsolute(value);
    option_sources["host_javabase"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg, "--host_jvm_args")) !=
             NULL) {
    host_jvm_args.push_back(value);
    option_sources["host_jvm_args"] = rcfile;  // NB: This is incorrect
  } else if ((value = GetUnaryOption(arg, next_arg, "--bazelrc")) != NULL) {
    if (!rcfile.empty()) {
      *error = "Can't specify --bazelrc in the .bazelrc file.";
      return blaze_exit_code::BAD_ARGV;
    }
  } else if ((value = GetUnaryOption(arg, next_arg, "--blazerc")) != NULL) {
    if (!rcfile.empty()) {
      *error = "Can't specify --blazerc in the .blazerc file.";
      return blaze_exit_code::BAD_ARGV;
    }
  } else if (GetNullaryOption(arg, "--nomaster_blazerc") ||
             GetNullaryOption(arg, "--master_blazerc")) {
    if (!rcfile.empty()) {
      *error = "Can't specify --[no]master_blazerc in .blazerc file.";
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["blazerc"] = rcfile;
  } else if (GetNullaryOption(arg, "--nomaster_bazelrc") ||
             GetNullaryOption(arg, "--master_bazelrc")) {
    if (!rcfile.empty()) {
      *error = "Can't specify --[no]master_bazelrc in .bazelrc file.";
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["blazerc"] = rcfile;
  } else if (GetNullaryOption(arg, "--batch")) {
    batch = true;
    option_sources["batch"] = rcfile;
  } else if (GetNullaryOption(arg, "--nobatch")) {
    batch = false;
    option_sources["batch"] = rcfile;
  } else if (GetNullaryOption(arg, "--batch_cpu_scheduling")) {
    batch_cpu_scheduling = true;
    option_sources["batch_cpu_scheduling"] = rcfile;
  } else if (GetNullaryOption(arg, "--nobatch_cpu_scheduling")) {
    batch_cpu_scheduling = false;
    option_sources["batch_cpu_scheduling"] = rcfile;
  } else if (GetNullaryOption(arg, "--fatal_event_bus_exceptions")) {
    fatal_event_bus_exceptions = true;
    option_sources["fatal_event_bus_exceptions"] = rcfile;
  } else if (GetNullaryOption(arg, "--nofatal_event_bus_exceptions")) {
    fatal_event_bus_exceptions = false;
    option_sources["fatal_event_bus_exceptions"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--io_nice_level")) != NULL) {
    if (!blaze_util::safe_strto32(value, &io_nice_level) ||
        io_nice_level > 7) {
      blaze_util::StringPrintf(error,
          "Invalid argument to --io_nice_level: '%s'. Must not exceed 7.",
          value);
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["io_nice_level"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg,
                                     "--max_idle_secs")) != NULL) {
    if (!blaze_util::safe_strto32(value, &max_idle_secs) ||
        max_idle_secs < 0) {
      blaze_util::StringPrintf(error,
          "Invalid argument to --max_idle_secs: '%s'.", value);
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["max_idle_secs"] = rcfile;
  } else if (GetNullaryOption(arg, "--experimental_oom_more_eagerly")) {
    oom_more_eagerly = true;
    option_sources["experimental_oom_more_eagerly"] = rcfile;
  } else if (GetNullaryOption(arg, "--noexperimental_oom_more_eagerly")) {
    oom_more_eagerly = false;
    option_sources["experimental_oom_more_eagerly"] = rcfile;
  } else if ((value = GetUnaryOption(
                  arg, next_arg,
                  "--experimental_oom_more_eagerly_threshold")) != NULL) {
    if (!blaze_util::safe_strto32(value, &oom_more_eagerly_threshold) ||
        oom_more_eagerly_threshold < 0) {
      blaze_util::StringPrintf(error,
                               "Invalid argument to "
                               "--experimental_oom_more_eagerly_threshold: "
                               "'%s'.",
                               value);
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["experimental_oom_more_eagerly_threshold"] = rcfile;
  } else if (GetNullaryOption(arg, "--write_command_log")) {
    write_command_log = true;
    option_sources["write_command_log"] = rcfile;
  } else if (GetNullaryOption(arg, "--nowrite_command_log")) {
    write_command_log = false;
    option_sources["write_command_log"] = rcfile;
  } else if (GetNullaryOption(arg, "--watchfs")) {
    watchfs = true;
    option_sources["watchfs"] = rcfile;
  } else if (GetNullaryOption(arg, "--nowatchfs")) {
    watchfs = false;
    option_sources["watchfs"] = rcfile;
  } else if (GetNullaryOption(arg, "--client_debug")) {
    client_debug = true;
    option_sources["client_debug"] = rcfile;
  } else if (GetNullaryOption(arg, "--noclient_debug")) {
    client_debug = false;
    option_sources["client_debug"] = rcfile;
  } else if (GetNullaryOption(arg, "--expand_configs_in_place")) {
    expand_configs_in_place = true;
    option_sources["expand_configs_in_place"] = rcfile;
  } else if (GetNullaryOption(arg, "--noexpand_configs_in_place")) {
    expand_configs_in_place = false;
    option_sources["expand_configs_in_place"] = rcfile;
  } else if ((value = GetUnaryOption(
      arg, next_arg, "--connect_timeout_secs")) != NULL) {
    if (!blaze_util::safe_strto32(value, &connect_timeout_secs) ||
        connect_timeout_secs < 1 || connect_timeout_secs > 120) {
      blaze_util::StringPrintf(error,
          "Invalid argument to --connect_timeout_secs: '%s'.\n"
          "Must be an integer between 1 and 120.\n",
          value);
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["connect_timeout_secs"] = rcfile;
  } else if ((value = GetUnaryOption(
      arg, next_arg, "--command_port")) != NULL) {
    if (!blaze_util::safe_strto32(value, &command_port) ||
        command_port < 0 || command_port > 65535) {
      blaze_util::StringPrintf(error,
          "Invalid argument to --command_port: '%s'.\n"
          "Must be a valid port number or 0.\n",
          value);
      return blaze_exit_code::BAD_ARGV;
    }
    option_sources["command_port"] = rcfile;
  } else if ((value = GetUnaryOption(arg, next_arg, "--invocation_policy"))
              != NULL) {
    if (invocation_policy == NULL) {
      invocation_policy = value;
      option_sources["invocation_policy"] = rcfile;
    } else {
      *error = "The startup flag --invocation_policy cannot be specified "
          "multiple times.";
      return blaze_exit_code::BAD_ARGV;
    }
  } else {
    bool extra_argument_processed;
    blaze_exit_code::ExitCode process_extra_arg_exit_code = ProcessArgExtra(
        arg, next_arg, rcfile, &value, &extra_argument_processed, error);
    if (process_extra_arg_exit_code != blaze_exit_code::SUCCESS) {
      return process_extra_arg_exit_code;
    }
    if (!extra_argument_processed) {
      blaze_util::StringPrintf(
          error,
          "Unknown startup option: '%s'.\n"
          "  For more info, run '%s help startup_options'.",
          arg, GetLowercaseProductName().c_str());
      return blaze_exit_code::BAD_ARGV;
    }
  }

  *is_space_separated = ((value == next_arg) && (value != NULL));
  return blaze_exit_code::SUCCESS;
}

blaze_exit_code::ExitCode StartupOptions::ProcessArgs(
    const std::vector<RcStartupFlag>& rcstartup_flags,
    std::string *error) {
  std::vector<RcStartupFlag>::size_type i = 0;
  while (i < rcstartup_flags.size()) {
    bool is_space_separated = false;
    const std::string next_value =
        (i == rcstartup_flags.size() - 1) ? "" : rcstartup_flags[i + 1].value;
    const blaze_exit_code::ExitCode process_arg_exit_code =
        ProcessArg(rcstartup_flags[i].value, next_value,
                   rcstartup_flags[i].source, &is_space_separated, error);
    // Store the provided option in --flag(=value)? form. Store these before
    // propagating any error code, since we want to have the correct
    // information for the output. The fact that the options aren't parseable
    // doesn't matter for this step.
    if (is_space_separated) {
      const std::string combined_value =
          rcstartup_flags[i].value + "=" + next_value;
      original_startup_options_.push_back(
          RcStartupFlag(rcstartup_flags[i].source, combined_value));
      i += 2;
    } else {
      original_startup_options_.push_back(
          RcStartupFlag(rcstartup_flags[i].source, rcstartup_flags[i].value));
      i++;
    }

    if (process_arg_exit_code != blaze_exit_code::SUCCESS) {
      return process_arg_exit_code;
    }
  }
  return blaze_exit_code::SUCCESS;
}

blaze_exit_code::ExitCode StartupOptions::ProcessArgExtra(
    const char *arg, const char *next_arg, const string &rcfile,
    const char **value, bool *is_processed, string *error) {
  *is_processed = false;
  return blaze_exit_code::SUCCESS;
}

string StartupOptions::GetDefaultHostJavabase() const {
  return blaze::GetDefaultHostJavabase();
}

string StartupOptions::GetHostJavabase() {
  // 1) Allow overriding the host_javabase via --host_javabase.
  if (host_javabase.empty()) {
    if (default_host_javabase.empty()) {
      string bundled_jre_path = blaze_util::JoinPath(
          install_base, "_embedded_binaries/embedded_tools/jdk");
      if (blaze_util::CanExecuteFile(blaze_util::JoinPath(
              bundled_jre_path, GetJavaBinaryUnderJavabase()))) {
        // 2) Use a bundled JVM if we have one.
        default_host_javabase = bundled_jre_path;
      } else {
        // 3) Otherwise fall back to using the default system JVM.
        default_host_javabase = GetDefaultHostJavabase();
      }
    }

    return default_host_javabase;
  } else {
    return host_javabase;
  }
}

string StartupOptions::GetExplicitHostJavabase() const {
  return host_javabase;
}

string StartupOptions::GetJvm() {
  string java_program =
      blaze_util::JoinPath(GetHostJavabase(), GetJavaBinaryUnderJavabase());
  if (!blaze_util::CanExecuteFile(java_program)) {
    if (!blaze_util::PathExists(java_program)) {
      fprintf(stderr, "Couldn't find java at '%s'.\n", java_program.c_str());
    } else {
      fprintf(stderr, "Java at '%s' exists but is not executable: %s\n",
              java_program.c_str(), blaze_util::GetLastErrorString().c_str());
    }
    exit(1);
  }
  // If the full JDK is installed
  string jdk_rt_jar = blaze_util::JoinPath(GetHostJavabase(), "jre/lib/rt.jar");
  // If just the JRE is installed
  string jre_rt_jar = blaze_util::JoinPath(GetHostJavabase(), "lib/rt.jar");
  // rt.jar does not exist in java 9+ so check for java instead
  string jre_java = blaze_util::JoinPath(GetHostJavabase(), "bin/java");
  string jre_java_exe = blaze_util::JoinPath(GetHostJavabase(), "bin/java.exe");
  if (blaze_util::CanReadFile(jdk_rt_jar) ||
      blaze_util::CanReadFile(jre_rt_jar) ||
      blaze_util::CanReadFile(jre_java) ||
      blaze_util::CanReadFile(jre_java_exe)) {
    return java_program;
  }
  fprintf(stderr,
          "Problem with java installation: "
          "couldn't find/access rt.jar or java in %s\n",
          GetHostJavabase().c_str());
  exit(1);
}

string StartupOptions::GetExe(const string &jvm, const string &jar_path) {
  return jvm;
}

void StartupOptions::AddJVMArgumentPrefix(const string &javabase,
    std::vector<string> *result) const {
}

void StartupOptions::AddJVMArgumentSuffix(const string &real_install_dir,
                                          const string &jar_path,
    std::vector<string> *result) const {
  result->push_back("-jar");
  result->push_back(
      blaze::PathAsJvmFlag(blaze_util::JoinPath(real_install_dir, jar_path)));
}

blaze_exit_code::ExitCode StartupOptions::AddJVMArguments(
    const string &host_javabase, std::vector<string> *result,
    const vector<string> &user_options, string *error) const {
  AddJVMLoggingArguments(result);
  return AddJVMMemoryArguments(host_javabase, result, user_options, error);
}

void StartupOptions::AddJVMLoggingArguments(std::vector<string> *result) const {
  // Configure logging
  const string propFile =
      blaze_util::JoinPath(output_base, "javalog.properties");
  string java_log(
      blaze::PathAsJvmFlag(blaze_util::JoinPath(output_base, "java.log")));
  if (!blaze_util::WriteFile("handlers=java.util.logging.FileHandler\n"
                             ".level=INFO\n"
                             "java.util.logging.FileHandler.level=INFO\n"
                             "java.util.logging.FileHandler.pattern=" +
                                 java_log +
                                 "\n"
                                 "java.util.logging.FileHandler.limit=1024000\n"
                                 "java.util.logging.FileHandler.count=1\n"
                                 "java.util.logging.FileHandler.formatter=" +
                                 java_logging_formatter + "\n",
                             propFile)) {
    perror(("Couldn't write logging file " + propFile).c_str());
  } else {
    result->push_back("-Djava.util.logging.config.file=" + propFile);
  }
}

blaze_exit_code::ExitCode StartupOptions::AddJVMMemoryArguments(
    const string &host_javabase, std::vector<string> *result,
    const vector<string> &user_options, string *error) const {
  return blaze_exit_code::SUCCESS;
}

#if defined(COMPILER_MSVC) || defined(__CYGWIN__)
// Extract the Windows path of "/" from $BAZEL_SH.
// $BAZEL_SH usually has the form `<prefix>/usr/bin/bash.exe` or
// `<prefix>/bin/bash.exe`, and this method returns that `<prefix>` part.
// If $BAZEL_SH doesn't end with "usr/bin/bash.exe" or "bin/bash.exe" then this
// method returns an empty string.
string StartupOptions::WindowsUnixRoot(const string &bazel_sh) {
  if (bazel_sh.empty()) {
    return string();
  }
  std::pair<string, string> split = blaze_util::SplitPath(bazel_sh);
  if (blaze_util::AsLower(split.second) != "bash.exe") {
    return string();
  }
  split = blaze_util::SplitPath(split.first);
  if (blaze_util::AsLower(split.second) != "bin") {
    return string();
  }

  std::pair<string, string> split2 = blaze_util::SplitPath(split.first);
  if (blaze_util::AsLower(split2.second) == "usr") {
    return split2.first;
  } else {
    return split.first;
  }
}
#endif  // defined(COMPILER_MSVC) || defined(__CYGWIN__)

}  // namespace blaze
