shcore::Prompt_result Command_line_shell::do_prompt()

in src/mysqlsh/cmdline_shell.cc [949:1447]


shcore::Prompt_result Command_line_shell::do_prompt(bool is_password,
                                                    const char *text,
                                                    std::string *ret) {
  char *tmp = nullptr;

  const auto passwords_from_stdin = this->options().passwords_from_stdin;

  char *(*get_response)(const char *) =
      is_password ? (passwords_from_stdin ? shcore::mysh_get_stdin_password
                                          : mysh_get_tty_password)
                  : Command_line_shell::readline;

  shcore::on_leave_scope cleanup([&tmp, is_password]() {
    if (!tmp) return;

    if (is_password) {
      // BUG#28915716: Cleans up the memory containing the password
      shcore::clear_buffer(tmp, ::strlen(tmp));
    }

    free(std::exchange(tmp, nullptr));
  });

  shcore::Interrupt_handler second_handler([get_response]() {
#ifdef _WIN32
    // this handler is not called if shell is running interactively
    // this handler is called when shell is running interactively with
    // --passwords-from-stdin option and a password prompt is canceled
    // this handler is called when shell is running in non-interactive mode
    // (i.e. launched by another process)

    // cancel any pending I/O, if signal was sent using
    // GenerateConsoleCtrlEvent() these are not going to be interrupted on its
    // own
    CancelIoEx(GetStdHandle(STD_INPUT_HANDLE), nullptr);

    // mysh_get_tty_password() reads directly from the console, CancelIoEx()
    // will not interrupt this, need to send CTRL-C
    if (mysh_get_tty_password == get_response) {
#else   // !_WIN32
    // if signal is sent to us from another process and we're reading from
    // terminal, we need to generate CTRL-C sequence to interrupt read loops
    if (mysh_get_tty_password == get_response || is_stdin_a_tty()) {
#endif  // !_WIN32
      send_ctrl_c_to_terminal();
    }

    // always suppress further processing of the interrupt handlers, if a call
    // to prompt*() throws in response to Prompt_result::Cancel, we want to
    // make sure that this exception is properly propagated
    return false;
  });

  shcore::Interrupt_handler first_handler([this]() {
    // we need another Interrupt_handler, which is going to be executed first
    // and returns true, to make sure that any interrupts generated by the
    // handler registered first (and executed second) are suppressed
    handle_interrupt();
    return true;
  });

  _interrupted = false;

  tmp = get_response(text);

#ifdef _WIN32
  if (!tmp && !_interrupted) {
    // If prompt returned no result but it was not interrupted, it means that
    // either user pressed CTRL-D/CTRL-Z or there was a signal which
    // interrupted read(), but was not yet delivered (signals are asynchronous
    // on Windows) - this may happen i.e. if shell was started with
    // --passwords-from-stdin and password prompt is interrupted with CTRL-C.
    // Wait a bit here, if signal arrives, sleep is going to be interrupted.
    // Worst case scenario: CTRL-D/CTRL-Z was pressed or signal arrived after
    // flag was checked but before we went to sleep.
    shcore::sleep_ms(100);
  }
#endif  // _WIN32

  if (tmp && strcmp(tmp, CTRL_C_STR) == 0) _interrupted = true;

  if (!tmp || _interrupted) {
    *ret = "";

    if (_interrupted)
      return shcore::Prompt_result::Cancel;
    else
      return shcore::Prompt_result::CTRL_D;
  }

  *ret = tmp;

  return shcore::Prompt_result::Ok;
}

std::string Command_line_shell::history_file(shcore::Shell_core::Mode mode) {
  return shcore::path::join_path(
      shcore::get_user_config_path(),
      "history." + to_string(mode == shcore::Shell_core::Mode::None
                                 ? interactive_mode()
                                 : mode));
}

void Command_line_shell::pre_command_loop() {
  display_info(m_default_cluster, "cluster", "cluster");
  display_info(m_default_clusterset, "clusterset", "clusterset");

  // information about replicaset name is displayed by the dba
  display_info(m_default_replicaset, "rs", "replicaset", false);
}

void Command_line_shell::command_loop() {
  bool using_tty = is_stdin_a_tty();

  if (_deferred_output && !_deferred_output->empty()) {
    println(*_deferred_output);
    _deferred_output.reset();
  }
  if (options().full_interactive && using_tty) {
    std::string message;
    auto session = _shell->get_dev_session();

    message = " Use \\sql to switch to SQL mode and execute queries.";

    switch (_shell->interactive_mode()) {
      case shcore::Shell_core::Mode::SQL:
#ifdef HAVE_JS
        message =
            "Currently in SQL mode. Use \\js or \\py to switch the shell to "
            "a scripting language.";
#else
        message =
            "Currently in SQL mode. Use \\py to switch the shell to python "
            "scripting.";
#endif
        break;
      case shcore::Shell_core::Mode::JavaScript:
        message = "Currently in JavaScript mode." + message;
        break;
      case shcore::Shell_core::Mode::Python:
        message = "Currently in Python mode." + message;
        break;
      default:
        break;
    }

    println(message);
  }

  m_current_session_uri = get_current_session_uri();

  while (options().interactive) {
    std::string cmd;
    {
      shcore::Interrupt_handler handler([this]() {
        handle_interrupt();
        return true;
      });
      if (using_tty) {
        // Ensure prompt is in its own line, in case there was any output
        // without a \n
        if (m_prompt_requires_newline) {
          mysqlsh::current_console()->raw_print(
              "\n", mysqlsh::Output_stream::STDOUT, false);
        }

        m_set_pending_command = true;

        char *tmp = Command_line_shell::readline(prompt().c_str());
        const std::unique_ptr<char, decltype(&free)> tmp_deleter{tmp, free};

        m_set_pending_command = false;

        if (tmp && strcmp(tmp, CTRL_C_STR) != 0) {
          cmd = tmp;
        } else {
          if (tmp) {
            if (strcmp(tmp, CTRL_C_STR) == 0) _interrupted = true;
          }
          if (_interrupted) {
            clear_input();
            _interrupted = false;
            continue;
          }
          break;
        }
      } else {
        if (options().full_interactive) {
          if (m_prompt_requires_newline) {
            mysqlsh::current_console()->raw_print(
                "\n", mysqlsh::Output_stream::STDOUT, false);
          }

          mysqlsh::current_console()->raw_print(
              prompt(), mysqlsh::Output_stream::STDOUT, false);
        }
        if (!shcore::getline(std::cin, cmd)) {
          if (_interrupted || !std::cin.eof()) {
            _interrupted = false;
            continue;
          }
          break;
        }
      }
      if (options().full_interactive)
        mysqlsh::current_console()->raw_print(
            cmd + "\n", mysqlsh::Output_stream::STDOUT, false);
    }
    process_line(cmd);
    reconnect_if_needed();
    detect_session_change();
  }

  std::cout << "Bye!\n";
}

void Command_line_shell::print_banner() {
  std::string welcome_msg("MySQL Shell ");
  welcome_msg += MYSH_FULL_VERSION;
  welcome_msg += "\n\n";
  welcome_msg += "Copyright (c) 2016, " PACKAGE_YEAR
                 ", Oracle and/or its affiliates.\n"
                 "Oracle is a registered trademark of Oracle Corporation "
                 "and/or its affiliates.\n"
                 "Other names may be trademarks of their respective owners.";
  println(welcome_msg);
  println();
  println("Type '\\help' or '\\?' for help; '\\quit' to exit.");
}

void Command_line_shell::print_cmd_line_helper() {
  // clang-format off
  std::string help_msg("MySQL Shell ");
  help_msg += MYSH_FULL_VERSION;
  println(help_msg);
  println("");
  // Splitting line in two so the git hook does not complain
  println("Copyright (c) 2016, " PACKAGE_YEAR ", "
          "Oracle and/or its affiliates.");
  println("Oracle is a registered trademark of Oracle Corporation and/or its affiliates.");
  println("Other names may be trademarks of their respective owners.");
  println("");
  println("Usage: mysqlsh [OPTIONS] [URI]");
  println("       mysqlsh [OPTIONS] [URI] -f <path> [<script-args>...]");
  println("       mysqlsh [OPTIONS] [URI] --cluster|--replicaset");
  println("       mysqlsh [OPTIONS] [URI] -- <object> <method> [<method-args>...]");
  println("       mysqlsh [OPTIONS] [URI] --dba enableXProtocol");
  println("       mysqlsh [OPTIONS] [URI] --import {<file>|-} [<collection>|<table> <column>]");
  println("");
  // clang-format on
  std::vector<std::string> details = Shell_options(0, nullptr).get_details();
  for (std::string line : details) println("  " + line);

  // options handled by libmysqlclient (taken from my_default.cc)
  println(
      "\nThe following options may be given as the first argument:\n\
--print-defaults        Print the program argument list and exit.\n\
--no-defaults           Don't read default options from any option file,\n\
                        except for login file.\n\
--defaults-file=#       Only read default options from the given file #.\n\
--defaults-extra-file=# Read this file after the global files are read.\n\
--defaults-group-suffix=#\n\
                        Also read groups with concat(group, suffix)\n\
--login-path=#          Read this path from the login file.");

  my_print_default_files("my");
  {
    std::string groups;
    if (my_defaults_group_suffix) {
      for (const auto *g : {"mysqlsh", "client"}) {
        groups.append(" ").append(g).append(my_defaults_group_suffix);
      }
    }
    println("The following groups are read: mysqlsh client" + groups);
  }

  println("");
  println("Usage examples:");
  println("$ mysqlsh --login-path=server1 --sql");
  println("$ mysqlsh root@localhost/schema");
  println("$ mysqlsh mysqlx://root@some.server:3307/world_x");
  println("$ mysqlsh --uri root@localhost --py -f sample.py sample param");
  println("$ mysqlsh root@targethost:33070 -s world_x -f sample.js");
  println(
      "$ mysqlsh -- util check-for-server-upgrade root@localhost "
      "--output-format=JSON");
  println("");
}

bool Command_line_shell::cmd_process_file(
    const std::vector<std::string> &params) {
  pause_history(true);
  shcore::on_leave_scope resume_history{[this]() { pause_history(false); }};

  return Mysql_shell::cmd_process_file(params);
}

void Command_line_shell::handle_notification(
    const std::string &name, const shcore::Object_bridge_ref & /* sender */,
    shcore::Value::Map_type_ref data) {
  if (name == "SN_STATEMENT_EXECUTED") {
    const auto executed = shcore::str_strip(data->get_string("statement"));
    auto mode = interactive_mode();
    auto sql = executed;
    if (shcore::str_beginswith(executed, "\\sql ") && executed.length() > 5) {
      mode = shcore::Shell_core::Mode::SQL;
      sql = executed.substr(5);
    }
    if (mode != shcore::Shell_core::Mode::SQL || sql_safe_for_logging(sql)) {
      _history.add(executed);

      if (shcore::Shell_core::Mode::SQL == mode) {
        syslog(sql);
      }
    } else {
      // add but delete after the next command and
      // don't let it get saved to disk either
      _history.add_temporary(executed);
    }
    if (m_previous_mode != shcore::Shell_core::Mode::None) {
      save_state(m_previous_mode);
      load_state();
      m_previous_mode = shcore::Shell_core::Mode::None;
    }
  } else if (name == SN_SHELL_OPTION_CHANGED) {
    const auto option = data->get_string("option");

    if (SHCORE_HISTORY_MAX_SIZE == option) {
      _history.set_limit(data->get_int("value"));
    } else if (SHCORE_HISTIGNORE == option) {
      set_sql_safe_for_logging(data->get_string("value"));
    } else if (SHCORE_PAGER == option) {
      const auto console = current_console();

      console->print_info(get_pager_message(data->get_string("value")));

      if (console->is_global_pager_enabled()) {
        // if global pager is enabled, disable and re-enable it to use new
        // pager
        console->disable_global_pager();
        console->enable_global_pager();
      }
    } else if (SHCORE_VERBOSE == option) {
      const auto console = current_console();

      console->set_verbose(data->get_int("value"));
    } else if (SHCORE_HISTORY_SQL_SYSLOG == option) {
      m_syslog.enable(data->get_bool("value"));
    }
  }
}

std::string Command_line_shell::get_current_session_uri() const {
  if (const auto session = _shell->get_dev_session(); session) {
    if (const auto core_session = session->get_core_session();
        core_session && core_session->is_open())
      return core_session->get_connection_options().as_uri();
  }

  return {};
}

void Command_line_shell::detect_session_change() {
  const auto session_uri = get_current_session_uri();

  if (session_uri != m_current_session_uri) {
    m_current_session_uri = session_uri;
    request_prompt_variables_update(true);
  }
}

void Command_line_shell::set_next_input(const std::string &input) {
  if (!input.empty()) {
    auto lines = shcore::str_split(input, "\n");

    // trim whitespace from the right side of the lines
    std::transform(
        lines.begin(), lines.end(), lines.begin(),
        [](const std::string &line) { return shcore::str_rstrip(line); });

    // remove consecutive empty lines at the end
    while (lines.size() > 1 && lines.back().empty() &&
           std::prev(lines.end(), 2)->empty()) {
      lines.pop_back();
    }

    // if there are only two lines, and the last one is empty, remove it as
    // well
    if (lines.size() == 2 && lines.back().empty()) {
      lines.pop_back();
    }

    // the last line goes to the linenoise and can be edited
    const auto last_line = lines.back();
    lines.pop_back();

    const auto console = current_console();

    // emulate user writing the input line by line
    _input_buffer = "";
    _input_mode = shcore::Input_state::Ok;

    for (const auto &line : lines) {
      console->raw_print(prompt() + line + "\n", mysqlsh::Output_stream::STDOUT,
                         false);

      _input_buffer += line + "\n";
      _input_mode = shcore::Input_state::ContinuedBlock;
    }

    // feed the last line to the linenoise
    if (!last_line.empty()) {
      linenoisePreloadBuffer(last_line.c_str());
    }
  }
}

bool Command_line_shell::set_pending_command(const Pending_command &command) {
  if (m_set_pending_command) {
    m_pending_command = command;
  }

  return m_set_pending_command;
}

void Command_line_shell::process_line(const std::string &line) {
  if (m_pending_command) {
    m_pending_command(line);
    m_pending_command = nullptr;
  } else {
    auto mode = _shell->interactive_mode();
    const char *mode_name = nullptr;
    switch (mode) {
      case shcore::IShell_core::Mode::None:
        mode_name = "none";
        break;
      case shcore::IShell_core::Mode::SQL:
        mode_name = "sql";
        break;
      case shcore::IShell_core::Mode::JavaScript:
        mode_name = "js";
        break;
      case shcore::IShell_core::Mode::Python:
        mode_name = "py";
        break;
    }
    shcore::Log_sql_guard g(mode_name);
    Mysql_shell::process_line(line);
  }
}

void Command_line_shell::syslog(const std::string &statement) {
  // log SQL statements and \source commands
  if (m_syslog.active() &&
      ('\\' != statement[0] || shcore::str_beginswith(statement, "\\source ") ||
       shcore::str_beginswith(statement, "\\. "))) {
    m_syslog.log(shcore::syslog::Level::INFO, syslog_format(statement));
  }
}

std::string Command_line_shell::syslog_format(const std::string &statement) {
  const std::size_t max_log_length = 1024;
  const auto variables = prompt_variables();

  std::vector<std::string> data;

  const auto add_data = [&data](const char *key, const std::string &value) {
    data.emplace_back(shcore::make_kvp(key, value.empty() ? "--" : value));
  };

  const auto add_data_from_prompt =
      [&variables, &add_data](const char *key, const std::string &var) {
        const auto value = variables->find(var);

        add_data(key, variables->end() != value ? value->second : "");
      };

  add_data_from_prompt("SYSTEM_USER", "system_user");
  add_data_from_prompt("MYSQL_USER", "user");
  add_data_from_prompt("CONNECTION_ID", "connection_id");
  add_data_from_prompt("DB_SERVER", "host");
  add_data_from_prompt("DB", "schema");
  add_data("QUERY", statement);

  return shcore::truncate(shcore::str_join(data, " "), max_log_length);
}

void Command_line_shell::pause_history(bool flag) {
  _history.pause(flag);
  m_syslog.pause(flag);
}

std::vector<std::string> Command_line_shell::auto_complete(
    const std::string &line, size_t *completion_offset) {
  return completer()->complete(shell_context()->interactive_mode(),
                               _input_buffer, line, completion_offset);
}

}  // namespace mysqlsh