blob: 67e44a768f2f06c4e0db579e3bc6225dfce020c9 [file]
/*
* Copyright (C) 2026 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 "src/trace_processor/shell/interactive.h"
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/trace_processor/basic_types.h"
#include "perfetto/trace_processor/trace_processor.h"
#include "src/trace_processor/shell/query.h"
#include "src/trace_processor/shell/result_formatter.h"
#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && \
!PERFETTO_BUILDFLAG(PERFETTO_OS_WASM)
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#endif
#if PERFETTO_BUILDFLAG(PERFETTO_TP_LINENOISE)
#include <linenoise.h>
#include <pwd.h>
#include <sys/types.h>
#endif
namespace perfetto::trace_processor {
namespace {
#if PERFETTO_BUILDFLAG(PERFETTO_TP_LINENOISE)
bool EnsureDir(const std::string& path) {
return base::Mkdir(path) || errno == EEXIST;
}
bool EnsureFile(const std::string& path) {
return base::OpenFile(path, O_RDONLY | O_CREAT, 0644).get() != -1;
}
std::string GetConfigPath() {
const char* homedir = getenv("HOME");
#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_FREEBSD) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
if (homedir == nullptr)
homedir = getpwuid(getuid())->pw_dir;
#elif PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
if (homedir == nullptr)
homedir = getenv("USERPROFILE");
#endif
if (homedir == nullptr)
return "";
return std::string(homedir) + "/.config";
}
std::string GetPerfettoPath() {
std::string config = GetConfigPath();
if (config.empty())
return "";
return config + "/perfetto";
}
std::string GetHistoryPath() {
std::string perfetto = GetPerfettoPath();
if (perfetto.empty())
return "";
return perfetto + "/.trace_processor_shell_history";
}
void SetupLineEditor() {
linenoiseSetMultiLine(true);
linenoiseHistorySetMaxLen(1000);
bool success = !GetHistoryPath().empty();
success = success && EnsureDir(GetConfigPath());
success = success && EnsureDir(GetPerfettoPath());
success = success && EnsureFile(GetHistoryPath());
success = success && linenoiseHistoryLoad(GetHistoryPath().c_str()) != -1;
if (!success) {
PERFETTO_PLOG("Could not load history from %s", GetHistoryPath().c_str());
}
}
struct LineDeleter {
void operator()(char* p) const {
linenoiseHistoryAdd(p);
linenoiseHistorySave(GetHistoryPath().c_str());
linenoiseFree(p);
}
};
using ScopedLine = std::unique_ptr<char, LineDeleter>;
ScopedLine GetLine(const char* prompt) {
errno = 0;
auto line = ScopedLine(linenoise(prompt));
// linenoise returns a nullptr both for CTRL-C and CTRL-D, however in the
// former case it sets errno to EAGAIN.
// If the user press CTRL-C return "" instead of nullptr. We don't want the
// main loop to quit in that case as that is inconsistent with the behavior
// "CTRL-C interrupts the current query" and frustrating when hitting that
// a split second after the query is done.
if (!line && errno == EAGAIN)
return ScopedLine(strdup(""));
return line;
}
#else
void SetupLineEditor() {}
using ScopedLine = std::unique_ptr<char>;
ScopedLine GetLine(const char* prompt) {
printf("\r%80s\r%s", "", prompt);
fflush(stdout);
ScopedLine line(new char[1024]);
if (!fgets(line.get(), 1024 - 1, stdin))
return nullptr;
if (strlen(line.get()) > 0)
line.get()[strlen(line.get()) - 1] = 0;
return line;
}
#endif // PERFETTO_TP_LINENOISE
// Rows per page: the terminal's height less room for the header and the
// prompt, or 32 if it isn't known.
uint64_t PageRows() {
#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && \
!PERFETTO_BUILDFLAG(PERFETTO_OS_WASM)
struct winsize size{};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == 0 && size.ws_row > 8) {
return size.ws_row - 4u;
}
#endif
return 32;
}
void RunAndPrint(TraceProcessor* trace_processor,
const std::string& sql,
bool quiet) {
base::Status status = RunQueriesAndPrintResult(
trace_processor, sql, DefaultTableOptions(PageRows()), stdout, quiet,
nullptr);
if (!status.ok()) {
fprintf(stderr, "%s\n", status.c_message());
}
}
// Commands which no longer work. They are left out of .help but still
// recognized, so that anyone using them is told what to do instead.
struct RetiredCommand {
const char* name;
const char* note;
};
constexpr RetiredCommand kRetiredCommands[] = {
{"dump",
"was removed: use `trace_processor export sqlite -o FILE TRACE_FILE` "
"instead"},
{"load-metrics-sql",
"was removed with the deprecated v1 metrics workflow: see "
"https://perfetto.dev/docs/analysis/metrics"},
{"run-metrics",
"was removed with the deprecated v1 metrics workflow: see "
"https://perfetto.dev/docs/analysis/metrics"},
{"width",
"is deprecated and does nothing: columns are now sized to their "
"contents"},
};
void PrintRetiredNote(const char* what, const char* note) {
PERFETTO_ELOG(
"%s %s. If this affects you, please file a bug at "
"https://github.com/google/perfetto/issues",
what, note);
}
// Prints the note for |command| if it is retired. Returns whether it was.
bool MaybePrintRetiredCommand(const char* command) {
for (const auto& retired : kRetiredCommands) {
if (strcmp(command, retired.name) == 0) {
PrintRetiredNote(("." + std::string(retired.name)).c_str(), retired.note);
return true;
}
}
return false;
}
void PrintShellUsage() {
PERFETTO_ELOG(R"(
Available commands:
.quit, .q Exit the shell.
.help This text.
.read FILE Executes the queries in the FILE.
.reset Destroys all tables/view created by the user.
)");
}
} // namespace
shell::FlagSpec RetiredWideFlag() {
return {"wide", 'W', false, nullptr, nullptr, [](const char*) {
PrintRetiredNote("-W/--wide",
"is deprecated and does nothing: columns are now "
"sized to their contents");
}};
}
base::Status StartInteractiveShell(TraceProcessor* trace_processor,
const InteractiveOptions& options) {
SetupLineEditor();
for (;;) {
ScopedLine line = GetLine("> ");
if (!line)
break;
if (strcmp(line.get(), "") == 0) {
if (!options.quiet)
printf("If you want to quit either type .q or press CTRL-D (EOF)\n");
continue;
}
if (line.get()[0] == '.') {
char command[32] = {};
char arg[1024] = {};
sscanf(line.get() + 1, "%31s %1023s", command, arg);
if (strcmp(command, "quit") == 0 || strcmp(command, "q") == 0) {
break;
}
if (MaybePrintRetiredCommand(command)) {
continue;
}
if (strcmp(command, "help") == 0) {
PrintShellUsage();
} else if (strcmp(command, "reset") == 0) {
trace_processor->RestoreInitialTables();
} else if (strcmp(command, "read") == 0 && strlen(arg)) {
std::string sql;
if (!base::ReadFile(arg, &sql)) {
PERFETTO_ELOG("Unable to read file %s", arg);
continue;
}
RunAndPrint(trace_processor, sql, options.quiet);
} else {
PrintShellUsage();
}
continue;
}
RunAndPrint(trace_processor, line.get(), options.quiet);
}
return base::OkStatus();
}
} // namespace perfetto::trace_processor