blob: 8fe6518f5559dba45652778df9fa48e394c6e730 [file] [edit]
/*
* 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/rpc/unixd.h"
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <string>
#include <vector>
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/proc_utils.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/ctrl_c_handler.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/lock_free_task_runner.h"
#include "perfetto/ext/base/unix_socket.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/ext/protozero/proto_ring_buffer.h"
#include "perfetto/protozero/proto_utils.h"
#include "src/trace_processor/rpc/rpc.h"
#include "src/trace_processor/rpc/session_lifecycle.h"
#include "src/trace_processor/rpc/session_paths.h"
#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_FREEBSD) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
#define PERFETTO_TP_UNIXD_POSIX() 1
#include <unistd.h>
#else
#define PERFETTO_TP_UNIXD_POSIX() 0
#endif
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
#include <process.h> // _exit()
#endif
// base::InstallCtrlCHandler only does anything on POSIX and Windows.
#if PERFETTO_TP_UNIXD_POSIX() || PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
#define PERFETTO_TP_UNIXD_CTRL_C() 1
#else
#define PERFETTO_TP_UNIXD_CTRL_C() 0
#endif
namespace perfetto::trace_processor {
namespace {
// Wraps |value| in single quotes if it contains characters that aren't safe to
// leave bare in a shell word, so the startup record stays cut-and-pasteable.
std::string ShellQuoteIfNeeded(const std::string& value) {
bool needs_quote = value.empty();
for (char c : value) {
bool safe = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '/' || c == '.' || c == '_' ||
c == '-' || c == '\\' || c == ':';
if (!safe) {
needs_quote = true;
break;
}
}
if (!needs_quote)
return value;
std::string out = "'";
for (char c : value) {
if (c == '\'')
out += "'\\''";
else
out += c;
}
out += "'";
return out;
}
// Prints the one-line, machine-parseable startup record to |f|.
void PrintStartupRecord(FILE* f,
int pid,
const std::string& session,
const std::string& socket_path,
uint32_t idle_timeout_ms) {
std::string idle_timeout =
idle_timeout_ms == 0 ? "never" : std::to_string(idle_timeout_ms) + "ms";
fprintf(f,
"perfetto-session pid=%d session=%s socket-path=%s idle-timeout=%s\n",
pid, ShellQuoteIfNeeded(session).c_str(),
ShellQuoteIfNeeded(socket_path).c_str(), idle_timeout.c_str());
fflush(f);
}
class UnixRpcServer : public base::UnixSocket::EventListener {
public:
UnixRpcServer(Rpc& rpc, UnixServerArgs args)
: rpc_(rpc), args_(std::move(args)) {}
base::Status Run();
// base::UnixSocket::EventListener implementation.
void OnNewIncomingConnection(
base::UnixSocket*,
std::unique_ptr<base::UnixSocket> new_conn) override;
void OnDisconnect(base::UnixSocket* self) override;
void OnDataAvailable(base::UnixSocket* self) override;
private:
struct ClientConn {
std::unique_ptr<base::UnixSocket> sock;
protozero::ProtoRingBuffer rxbuf;
};
ClientConn* FindConn(base::UnixSocket* self);
void DispatchMessage(base::UnixSocket* sock, const uint8_t* data, size_t len);
void Shutdown();
Rpc& rpc_;
UnixServerArgs args_;
std::string pid_path_;
base::MaybeLockFreeTaskRunner task_runner_;
std::unique_ptr<base::UnixSocket> listen_sock_;
std::vector<std::unique_ptr<ClientConn>> clients_;
std::unique_ptr<IdleReaper> reaper_;
};
// Writes the current pid to |pid_path| so `server kill` can stop the server by
// pid. Best-effort.
void WritePidFile(const std::string& pid_path, int pid) {
if (FILE* f = fopen(pid_path.c_str(), "w")) {
fprintf(f, "%d", pid);
fclose(f);
}
}
#if PERFETTO_TP_UNIXD_CTRL_C()
// Set so the Ctrl-C handler can clean up on exit. Only ever point at strings
// that outlive the handler (the server's socket and pid-file paths).
const char* g_socket_path_for_signal = nullptr;
const char* g_pid_path_for_signal = nullptr;
// Removes the socket + pid file and exits. Runs in a signal handler on POSIX,
// so it must stay async-signal-safe: base::Unlink()/_exit() are, remove()
// isn't.
[[noreturn]] void HandleTermSignal() {
if (g_socket_path_for_signal)
base::Unlink(g_socket_path_for_signal);
if (g_pid_path_for_signal)
base::Unlink(g_pid_path_for_signal);
_exit(0);
}
#endif // PERFETTO_TP_UNIXD_CTRL_C()
base::Status UnixRpcServer::Run() {
if (args_.daemonize && !PERFETTO_TP_UNIXD_POSIX()) {
return base::ErrStatus("--daemonize is not supported on this platform yet");
}
#if PERFETTO_TP_UNIXD_CTRL_C()
// Install *before* binding: a SIGTERM/SIGINT in the window after the socket
// exists would otherwise terminate us without unlinking it, leaking the file.
// Unlinking a not-yet-bound path is harmless; the pid path is wired up later.
g_socket_path_for_signal = args_.socket_path.c_str();
base::InstallCtrlCHandler(&HandleTermSignal);
#endif
// Clean up a socket left behind by a previous server. If something still
// accepts a connection there, refuse to clobber it.
if (base::FileExists(args_.socket_path)) {
auto probe = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kUnix,
base::SockType::kStream);
if (probe && probe.Connect(args_.socket_path)) {
return base::ErrStatus(
"A live session already holds %s. Pick another --name/--path or stop "
"the existing server.",
args_.socket_path.c_str());
}
remove(args_.socket_path.c_str());
}
// Bind the listening socket up-front (before any optional fork) so that, when
// daemonizing, the parent only prints the startup record once the socket is
// actually bound.
auto raw = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kUnix,
base::SockType::kStream);
if (!raw || !raw.Bind(args_.socket_path) || !raw.Listen()) {
return base::ErrStatus("Failed to bind session socket at %s",
args_.socket_path.c_str());
}
bool printed_record = false;
#if PERFETTO_TP_UNIXD_POSIX()
if (args_.daemonize) {
// base::Daemonize forks: the parent prints the record via the callback and
// exits; the child detaches and keeps serving. The socket is already bound,
// so the printed record is accurate.
const UnixServerArgs& args = args_;
base::Daemonize([&args](pid_t pid) -> int {
PrintStartupRecord(stdout, pid, args.session_name, args.socket_path,
args.idle_timeout_ms);
return 0;
});
printed_record = true;
}
#endif
if (!printed_record) {
PrintStartupRecord(stdout, static_cast<int>(base::GetProcessId()),
args_.session_name, args_.socket_path,
args_.idle_timeout_ms);
fprintf(stderr,
"[unix] Serving warm session '%s'. Query it with:\n"
" trace_processor_shell query --remote %s \"SELECT ...\"\n"
"Stop it with Ctrl-C, or: "
"trace_processor_shell server kill %s\n",
args_.session_name.c_str(), args_.session_name.c_str(),
args_.session_name.c_str());
}
// Record our pid next to the socket so `server kill` can stop us. This
// runs in the serving process (the child, when daemonized), so the pid
// matches the one printed in the startup record.
pid_path_ = args_.socket_path + session::kPidFileSuffix;
WritePidFile(pid_path_, static_cast<int>(base::GetProcessId()));
listen_sock_ = base::UnixSocket::Listen(raw.ReleaseFd(), this, &task_runner_,
base::SockFamily::kUnix,
base::SockType::kStream);
if (!listen_sock_ || !listen_sock_->is_listening()) {
return base::ErrStatus("Failed to listen on session socket at %s",
args_.socket_path.c_str());
}
reaper_ =
std::make_unique<IdleReaper>(&task_runner_, args_.idle_timeout_ms,
args_.idle_start, [this] { Shutdown(); });
reaper_->Start();
#if PERFETTO_TP_UNIXD_CTRL_C()
// Now that the pid-file path is known, let the handler unlink it too.
g_pid_path_for_signal = pid_path_.c_str();
#endif
task_runner_.Run();
remove(args_.socket_path.c_str());
remove(pid_path_.c_str());
return base::OkStatus();
}
void UnixRpcServer::Shutdown() {
remove(args_.socket_path.c_str());
remove(pid_path_.c_str());
task_runner_.Quit();
}
void UnixRpcServer::OnNewIncomingConnection(
base::UnixSocket*,
std::unique_ptr<base::UnixSocket> new_conn) {
auto conn = std::make_unique<ClientConn>();
conn->sock = std::move(new_conn);
clients_.push_back(std::move(conn));
}
void UnixRpcServer::OnDisconnect(base::UnixSocket* self) {
for (auto it = clients_.begin(); it != clients_.end(); ++it) {
if ((*it)->sock.get() == self) {
clients_.erase(it);
return;
}
}
}
UnixRpcServer::ClientConn* UnixRpcServer::FindConn(base::UnixSocket* self) {
for (auto& c : clients_) {
if (c->sock.get() == self)
return c.get();
}
return nullptr;
}
void UnixRpcServer::OnDataAvailable(base::UnixSocket* self) {
ClientConn* conn = FindConn(self);
if (!conn)
return;
// Drain everything currently readable into this connection's framing buffer.
char buf[4096];
for (;;) {
size_t n = self->Receive(buf, sizeof(buf));
if (n == 0)
break;
conn->rxbuf.Append(buf, n);
}
// Forward only *whole* TraceProcessorRpc messages to the shared Rpc. Each is
// re-wrapped in its TraceProcessorRpcStream framing so Rpc's own tokenizer
// never sees a partial frame, even if a different connection disconnects
// mid-message.
for (;;) {
auto msg = conn->rxbuf.ReadMessage();
if (!msg.valid()) {
if (msg.fatal_framing_error)
self->Shutdown(/*notify=*/false);
break;
}
DispatchMessage(self, msg.start, msg.len);
}
}
void UnixRpcServer::DispatchMessage(base::UnixSocket* sock,
const uint8_t* data,
size_t len) {
namespace pu = protozero::proto_utils;
uint8_t preamble[16];
uint8_t* preamble_end = preamble;
preamble_end = pu::WriteVarInt(pu::MakeTagLengthDelimited(1), preamble_end);
preamble_end = pu::WriteVarInt(len, preamble_end);
if (reaper_)
reaper_->set_query_in_flight(true);
rpc_.SetRpcResponseFunction([sock](const void* resp, uint32_t resp_len) {
if (resp == nullptr) {
sock->Shutdown(/*notify=*/false);
return;
}
sock->Send(resp, resp_len);
});
rpc_.OnRpcRequest(preamble, static_cast<size_t>(preamble_end - preamble));
rpc_.OnRpcRequest(data, len);
rpc_.SetRpcResponseFunction(nullptr);
if (reaper_) {
reaper_->set_query_in_flight(false);
reaper_->OnActivity();
}
}
} // namespace
base::Status RunUnixRpcServer(Rpc& rpc, const UnixServerArgs& args) {
UnixRpcServer server(rpc, args);
return server.Run();
}
} // namespace perfetto::trace_processor