blob: 4db414ffe1bccec8fe0143d9e3e8a4f6650b585d [file] [log] [blame]
// Copyright 2019 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 "src/developer/debug/zxdb/console/console_impl.h"
#include <fcntl.h>
#include <lib/syslog/cpp/macros.h>
#include <signal.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <filesystem>
#include "src/developer/debug/zxdb/client/process.h"
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/setting_schema_definition.h"
#include "src/developer/debug/zxdb/client/system.h"
#include "src/developer/debug/zxdb/client/target.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/console/command.h"
#include "src/developer/debug/zxdb/console/command_parser.h"
#include "src/developer/debug/zxdb/console/output_buffer.h"
#include "src/lib/files/file.h"
#include "src/lib/fxl/strings/join_strings.h"
#include "src/lib/fxl/strings/split_string.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/fxl/strings/trim.h"
#include "src/lib/line_input/line_input.h"
namespace zxdb {
namespace {
const char* kHistoryFilename = ".zxdb_history";
} // namespace
ConsoleImpl::ConsoleImpl(Session* session, line_input::ModalLineInput::Factory line_input_factory)
: Console(session), line_input_(std::move(line_input_factory)), impl_weak_factory_(this) {
line_input_.Init([this](std::string s) { ProcessInputLine(s); }, "[zxdb] ");
// Set the line input completion callback that can know about our context. OK to bind |this| since
// we own the line_input object.
FillCommandContextCallback fill_command_context([this](Command* cmd) {
context_.FillOutCommand(cmd); // Ignore errors, this is for autocomplete.
});
line_input_.SetAutocompleteCallback([fill_command_context = std::move(fill_command_context)](
std::string prefix) -> std::vector<std::string> {
return GetCommandCompletions(prefix, fill_command_context);
});
// Cancel (ctrl-c) handling.
line_input_.SetCancelCallback([this]() {
if (line_input_.GetLine().empty()) {
// Stop program execution. Do this by visibly typing the stop command so the user knows
// what is happening.
line_input_.SetCurrentInput("pause --clear-state");
line_input_.OnInput(line_input::SpecialCharacters::kKeyEnter);
} else {
// Control-C with typing on the line just clears the current state.
line_input_.SetCurrentInput(std::string());
}
});
// EOF (ctrl-d) should exit gracefully.
line_input_.SetEofCallback([this]() {
// If we're in embedded mode, we don't actually want to quit until the streaming fd has been
// closed. Detach the active process to try to return to embedded mode. The console context
// object is watching for detach notifications and will make the transition if appropriate.
if (context().GetConsoleMode() == ClientSettings::System::kConsoleMode_EmbeddedInteractive) {
context().session()->system().DetachFromAllTargets([](int) {});
} else {
Quit();
}
});
// Set stdin to async mode or OnStdinReadable will block.
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) | O_NONBLOCK);
}
ConsoleImpl::~ConsoleImpl() {
if (!SaveHistoryFile())
Console::Output(Err("Could not save history file to $HOME/%s.\n", kHistoryFilename));
}
fxl::WeakPtr<ConsoleImpl> ConsoleImpl::GetImplWeakPtr() { return impl_weak_factory_.GetWeakPtr(); }
void ConsoleImpl::Init() {
LoadHistoryFile();
context().InitConsoleMode();
}
void ConsoleImpl::LoadHistoryFile() {
std::filesystem::path path(getenv("HOME"));
if (path.empty())
return;
path /= kHistoryFilename;
std::string data;
if (!files::ReadFileToString(path, &data))
return;
auto history = fxl::SplitStringCopy(data, "\n", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
for (const std::string& cmd : history)
line_input_.AddToHistory(cmd);
}
bool ConsoleImpl::SaveHistoryFile() {
char* home = getenv("HOME");
if (!home)
return false;
// We need to invert the order the deque has the entries.
std::string history_data;
const auto& history = line_input_.GetHistory();
for (auto it = history.rbegin(); it != history.rend(); it++) {
auto trimmed = fxl::TrimString(*it, " ");
// We ignore empty entries or quit commands.
if (trimmed.empty() || trimmed == "quit" || trimmed == "q" || trimmed == "exit") {
continue;
}
history_data.append(trimmed).append("\n");
}
auto filepath = std::filesystem::path(home) / kHistoryFilename;
return files::WriteFile(filepath, history_data.data(), history_data.size());
}
void ConsoleImpl::Write(const OutputBuffer& output, bool add_newline) {
// Since most operations are asynchronous, we have to hide the input line before printing anything
// or it will get appended to whatever the user is typing on the screen.
//
// TODO(brettw) This can cause flickering. A more advanced system would do more fancy console
// stuff to output above the input line so we'd never have to hide it.
// Make sure stdout is in blocking mode since normal output won't expect non-blocking mode. We can
// get in this state if stdin and stdout are the same underlying handle because the constructor
// sets stdin to O_NONBLOCK so we can asynchronously wait for input.
int old_bits = fcntl(STDIN_FILENO, F_GETFL, 0);
if (old_bits & O_NONBLOCK)
fcntl(STDOUT_FILENO, F_SETFL, old_bits & ~O_NONBLOCK);
// If input is disabled, there will be no prompt and we want to keep it off.
if (InputEnabled())
line_input_.Hide();
output.WriteToStdout(add_newline);
if (InputEnabled())
line_input_.Show();
if (old_bits & O_NONBLOCK)
fcntl(STDOUT_FILENO, F_SETFL, old_bits);
}
void ConsoleImpl::ModalGetOption(const line_input::ModalPromptOptions& options,
OutputBuffer message, const std::string& prompt,
line_input::ModalLineInput::ModalCompletionCallback cb) {
// Input will normally be disabled before executing a command. When that command needs to read
// input as part of its operation, we need to explicitly re-enable it.
EnableInput();
// Print the message from within the "will show" callback to ensure proper serialization if there
// are multiple prompts pending.
//
// Okay to capture |this| because we own the line_input_.
line_input_.ModalGetOption(options, prompt, std::move(cb),
[this, message]() { GetWeakPtr()->Output(message); });
}
void ConsoleImpl::Quit() {
DisableInput();
debug::MessageLoop::Current()->QuitNow();
}
void ConsoleImpl::Clear() {
// We write directly instead of using Output because WriteToStdout expects to append '\n' to
// outputs and won't flush it explicitly otherwise.
if (InputEnabled())
line_input_.Hide();
const char ff[] = "\033c"; // Form feed.
write(STDOUT_FILENO, ff, sizeof(ff));
if (InputEnabled())
line_input_.Show();
}
void ConsoleImpl::ProcessInputLine(const std::string& line, fxl::RefPtr<CommandContext> cmd_context,
bool add_to_history) {
if (!cmd_context)
cmd_context = fxl::MakeRefCounted<ConsoleCommandContext>(this);
Command cmd;
if (line.empty()) {
// Repeat the previous command, don't add to history.
if (Err err = ParseCommand(previous_line_, &cmd); err.has_error())
return cmd_context->ReportError(err);
} else {
Err err = ParseCommand(line, &cmd);
if (add_to_history) {
// Add to history even in the error case so the user can press "up" and fix it.
line_input_.AddToHistory(line);
previous_line_ = line;
}
if (err.has_error())
return cmd_context->ReportError(err);
}
cmd_context->SetCommandReport(cmd.BuildReport());
if (Err err = context_.FillOutCommand(&cmd); err.has_error())
return cmd_context->ReportError(err);
// Suspend input (if setting is enabled) and register for a callback to re-enable. This will have
// the effect of blocking the UI for the duration of the command.
auto ui_timeout =
context().session()->system().settings().GetInt(ClientSettings::System::kUiTimeoutMs);
if (ui_timeout > 0 && InputEnabled()) {
DisableInput();
fit::callback<void()> resume_input = [weak_this = GetImplWeakPtr()]() {
if (weak_this)
weak_this->EnableInput();
};
cmd_context->SetConsoleCompletionObserver(fit::defer(resume_input.share()));
// Some commands will take a long time to execute, re-enable the input if this happens.
debug::MessageLoop::Current()->PostTimer(
FROM_HERE, ui_timeout,
[resume_input = std::move(resume_input), verb = cmd.verb(),
weak_this = GetImplWeakPtr()]() mutable {
if (!resume_input)
return; // Command already complete and input explicit re-enabled.
if (!weak_this)
return; // Console is gone.
if (weak_this->InputEnabled())
return; // Input is enabled in another way, e.g., ModalGetOption().
// Otherwise the command is still running after the timeout. Print a message and re-enable
// input so the user can get on with things.
if (verb == Verb::kNone) {
// Running a noun. Normally these won't take very long so we don't bother decoding the
// name.
Console::get()->Output("Command running in the background...\n");
} else {
Console::get()->Output(
OutputBuffer(Syntax::kComment, "\"" + GetVerbRecord(verb)->aliases[0] +
"\" command running in the background...\n"));
}
resume_input();
});
}
DispatchCommand(cmd, cmd_context);
if (cmd.thread() && cmd.verb() != Verb::kNone) {
// Show the right source/disassembly for the next listing.
context_.SetSourceAffinityForThread(cmd.thread(), GetVerbRecord(cmd.verb())->source_affinity);
}
}
void ConsoleImpl::DisableInput() {
// If the counter has just reached below one, we need to stop watching stdin.
if (--input_enabled_ != 0)
return;
auto behavior = context().GetConsoleMode() == ClientSettings::System::kConsoleMode_Embedded
? line_input::LineInput::InterruptHandlingBehavior::kIgnoreInterrupts
: line_input::LineInput::InterruptHandlingBehavior::kHandleInterrupts;
line_input_.Hide(behavior);
// Stop watching for stdin which will stop feeding input to the LineInput. Today, the LineInput
// class doesn't suspend processing while hidden. If we didn't disable this watching, you would
// still get commands executed even though you can't see your typing.
//
// Buffering here needs to be revisited because ideally we would make Control-C work to suspend
// the synchronous mode, while also buffering the user typing while hidden.
stdio_watch_.StopWatching();
}
void ConsoleImpl::EnableInput() {
// If the counter has just reached above zero, we need start watching stdin.
if (++input_enabled_ != 1)
return;
// Callback for input passed to WatchFD().
auto watch_fn = [this](int fd, bool readable, bool, bool error) {
if (error) {
// Happens most commonly on EOF.
//
// This handling isn't quite right when the input isn't the terminal. The error signal is
// delivered in parallel with the data which means we'll get it as soon as the calling process
// closes the pipe, whether or not we have read all the data.
//
// This means that if you do something like "echo help | zxdb", the first thing zxdb will
// see will often be the error signal because the caller closed the pipe already, and no
// input will be processed.
//
// To work around this, we may need go into a new "input drain mode" when we get the error.
// This would ignore the async events on the file hand and read from stdin until read()
// reports an error and then Quit(). The harder part is that mode will need to be integrated
// the same way that watching stdin does, where we go back to the message loop for other work
// to proceed, and stop/start according to Enable/DisableInput() calls.
Quit();
return;
}
if (!readable)
return;
// Dispatch one character at a time. The message loop will continue to notify us if the
// input continues to be readable, so we'll get future messages for buffered input.
//
// The reason we want this is to avoid starving other work in the case that input is
// buffered. If the user queued several "next" commands and we didn't return to the message
// loop, for example, we wouldn't even get a chance to process any replies from the target
// before issuing the following command.
//
// Note if we ever start dispatching more than one byte here, we should check InputEnabled()
// in case the processing of the previous command has disabled input and no data is
// expected.
// Note that we need to handle EINTR when using this low-level syscall.
char ch = 0;
int bytes_read = 0;
do {
bytes_read = read(STDIN_FILENO, &ch, 1);
} while (bytes_read == -1 && errno == EINTR);
if (bytes_read > 0) {
line_input_.OnInput(ch);
} else {
// Error or EOF.
Quit();
}
};
stdio_watch_ = debug::MessageLoop::Current()->WatchFD(debug::MessageLoop::WatchMode::kRead,
STDIN_FILENO, std::move(watch_fn));
line_input_.Show();
}
} // namespace zxdb