blob: 16ef3bc97f4d8ab0f27e8aabb0ce5c714162f74a [file] [log] [blame]
// Copyright 2023 Google Inc. 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 "persistent_mode.h"
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include "debug_flags.h"
#include "interrupt_handling.h"
#include "ipc_handle.h"
#include "ipc_utils.h"
#include "metrics.h"
#include "process_utils.h"
#include "stdio_redirection.h"
#include "util.h"
#include "version.h"
#ifndef _WIN32
#include <sys/stat.h>
#include <unistd.h>
#endif
#define DEBUG 0
#define SERVER_LOG(...) \
do { \
fprintf(stderr, "NINJA_SERVER_LOG: "); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
} while (0)
#if DEBUG
#define CLIENT_LOG(...) \
do { \
fprintf(stderr, "NINJA_CLIENT_LOG: "); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
} while (0)
#else
#define CLIENT_LOG(...) (void)0
#endif
namespace {
// Print error message, then return false
bool PrintError(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
fputs("ERROR: ", stderr);
vfprintf(stderr, fmt, args);
fputc('\n', stderr);
va_end(args);
return false;
}
extern "C" char** environ;
// Return the name of the IPC service for a given current build
// directory. This allows several servers to co-exist on the same
// machine, if they are invoked / used from different build
// directories.
std::string GetServiceName(const std::string& build_dir) {
std::string real_dir = build_dir.empty() ? GetCurrentDir() : build_dir;
return StringFormat("ninja-server-%08zx", std::hash<std::string>()(real_dir));
}
// Name of the environment variable used to control the feature at runtime.
const char kPersistentModeEnv[] = "NINJA_PERSISTENT_MODE";
// Name of the environment variable used to control the server timeout.
const char kPersistentTimeoutSecondsEnv[] = "NINJA_PERSISTENT_TIMEOUT_SECONDS";
} // namespace
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
/////
///// C O M P A T I B I L I T Y
/////
/////
PersistentMode::Compatibility::Compatibility()
: ninja_version_(GetDefaultNinjaVersion()), build_dir_(GetCurrentDir()),
input_file_("build.ninja") {}
PersistentMode::Compatibility& PersistentMode::Compatibility::SetInputFile(
const std::string& input_file) {
input_file_ = input_file;
return *this;
}
// static
std::string PersistentMode::Compatibility::GetDefaultNinjaVersion() {
// Ignore the error returned by GetFileTimestamp() since the executable
// is known to exist.
std::string err;
return StringFormat("%s-%" PRId64, kNinjaVersion,
::GetFileTimestamp(GetCurrentExecutable(), &err));
}
PersistentMode::Compatibility&
PersistentMode::Compatibility::SetNinjaVersionForTest(
const std::string& ninja_version) {
ninja_version_ = ninja_version;
return *this;
}
PersistentMode::Compatibility& PersistentMode::Compatibility::SetBuildDir(
const std::string& build_dir) {
build_dir_ = build_dir;
return *this;
}
PersistentMode::Compatibility&
PersistentMode::Compatibility::SetFlagDupeEdgesShouldErr(bool enabled) {
dupe_edges_should_err_ = enabled;
return *this;
}
PersistentMode::Compatibility&
PersistentMode::Compatibility::SetFlagPhonyCycleShouldErr(bool enabled) {
phony_cycle_should_err_ = enabled;
return *this;
}
bool PersistentMode::Compatibility::CheckBuildDir(std::string* err) const {
if (build_dir_.empty())
return true;
if (build_dir_[0] != '/' && build_dir_[0] != '\\') {
*err = StringFormat("build directory path is not absolute: %s",
build_dir_.c_str());
return false;
}
// TODO(digit): What about Win32 drive letters?
return true;
}
// static
PersistentMode::Compatibility PersistentMode::Compatibility::FromEncodedString(
const std::string& str, std::string* error) {
error->clear();
WireDecoder decoder(str);
Compatibility result = {};
decoder.Read(result.ninja_version_);
decoder.Read(result.build_dir_);
decoder.Read(result.input_file_);
decoder.Read(result.dupe_edges_should_err_);
decoder.Read(result.phony_cycle_should_err_);
if (decoder.has_error()) {
*error = "Truncated PersistentMode::Compatibility encoded string";
result = {};
}
return result;
}
std::string PersistentMode::Compatibility::ToEncodedString() const {
WireEncoder encoder;
encoder.Write(ninja_version_);
encoder.Write(build_dir_);
encoder.Write(input_file_);
encoder.Write(dupe_edges_should_err_);
encoder.Write(phony_cycle_should_err_);
return encoder.TakeResult();
}
std::string PersistentMode::Compatibility::ToString() const {
return StringFormat(
"build_dir=%s input_file=%s dupe_edges_should_err=%s "
"phony_cycle_shoud_err=%s ninja_version=%s",
build_dir_.c_str(), input_file_.c_str(),
dupe_edges_should_err_ ? "true" : "false",
phony_cycle_should_err_ ? "true" : "false", ninja_version_.c_str());
}
bool PersistentMode::Compatibility::IsCompatibleWith(
const PersistentMode::Compatibility& other, std::string* reason) const {
if (ninja_version_ != other.ninja_version_) {
*reason =
StringFormat("Ninja version mismatch, expected [%s] vs [%s]",
ninja_version_.c_str(), other.ninja_version_.c_str());
return false;
}
if (build_dir_ != other.build_dir_) {
*reason = StringFormat("Working dir mismatch, expected [%s] vs [%s]",
build_dir_.c_str(), other.build_dir_.c_str());
return false;
}
if (input_file_ != other.input_file_) {
*reason = StringFormat("Input file mismatch, expected [%s] vs [%s]",
input_file_.c_str(), other.input_file_.c_str());
return false;
}
if (dupe_edges_should_err_ != other.dupe_edges_should_err_) {
*reason =
StringFormat("Flag dupe_edges_should_err mismatch, expected %s vs %s",
dupe_edges_should_err_ ? "true" : "false",
other.dupe_edges_should_err_ ? "true" : "false");
return false;
}
if (phony_cycle_should_err_ != other.phony_cycle_should_err_) {
*reason =
StringFormat("Flag phony_cycle_should_err mismatch, expected %s vs %s",
phony_cycle_should_err_ ? "true" : "false",
other.phony_cycle_should_err_ ? "true" : "false");
return false;
}
return true;
}
std::vector<std::string> PersistentMode::Compatibility::ToServerArgs(
const std::string& server_executable) const {
std::vector<std::string> result;
result.push_back(server_executable);
result.push_back("-C");
result.push_back(build_dir_);
if (input_file_ != "build.ninja") {
result.push_back("-f");
result.push_back(input_file_);
}
if (dupe_edges_should_err_) {
result.push_back("-wdupbuild=err");
} else {
result.push_back("-wdupbuild=warn");
}
if (phony_cycle_should_err_) {
result.push_back("-wphonycycle=err");
} else {
result.push_back("-wphonycycle=warn");
}
return result;
}
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
/////
///// B U I L D Q U E R Y
/////
/////
// static
PersistentMode::BuildQuery PersistentMode::BuildQuery::FromEncodedString(
const std::string& str, std::string* error) {
BuildQuery result;
std::string encoded;
WireDecoder decoder(str);
// Read BuildConfig.
decoder.Read(encoded);
result.config = BuildConfig::FromEncodedString(encoded, error);
if (!error->empty())
return {};
decoder.Read(result.debug_explaining);
decoder.Read(result.debug_keep_depfile);
decoder.Read(result.debug_keep_rsp);
decoder.Read(result.debug_experimental_statcache);
decoder.Read(result.dump_metrics);
decoder.Read(result.tool);
size_t num_args = 0;
decoder.Read(num_args);
result.args.resize(num_args);
for (size_t n = 0; n < num_args; ++n)
decoder.Read(result.args[n]);
if (decoder.has_error()) {
*error = "Truncated BuildQuery encoded string";
result = {};
}
return result;
}
std::string PersistentMode::BuildQuery::ToEncodedString() const {
WireEncoder encoder;
encoder.Write(config.ToEncodedString());
encoder.Write(debug_explaining);
encoder.Write(debug_keep_depfile);
encoder.Write(debug_keep_rsp);
encoder.Write(debug_experimental_statcache);
encoder.Write(dump_metrics);
encoder.Write(tool);
encoder.Write(args.size());
for (const auto& arg : args)
encoder.Write(arg);
return encoder.TakeResult();
}
std::string PersistentMode::BuildQuery::ToString() const {
std::string result = config.ToString();
StringAppendFormat(result,
" explain=%s keep_depfile=%s keep_rsp=%s "
"experimental_statcache=%s dump_metrics=%s",
debug_explaining ? "true" : "false",
debug_keep_depfile ? "true" : "false",
debug_keep_rsp ? "true" : "false",
debug_experimental_statcache ? "true" : "false",
dump_metrics ? "true" : "false");
if (!tool.empty())
StringAppendFormat(result, " tool=%s", tool.c_str());
for (const auto& arg : args) {
result += " ";
result += arg;
}
return result;
}
void PersistentMode::BuildQuery::WriteGlobalVariables() const {
g_explaining = debug_explaining;
g_keep_depfile = debug_keep_depfile;
g_keep_rsp = debug_keep_rsp;
g_experimental_statcache = debug_experimental_statcache;
}
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
/////
///// P E R S I S T E N T M O D E
/////
/////
// static
PersistentMode::Status PersistentMode::GetCurrentProcessStatus() {
const char* env = getenv(kPersistentModeEnv);
std::string mode(env ? env : "0");
if (mode == "0" || mode == "off")
return PersistentMode::Disabled;
if (mode == "1" || mode == "on" || mode == "client")
return PersistentMode::IsClient;
if (mode == "server") // Used internally when spawning the server.
return PersistentMode::IsServer;
fprintf(stderr,
"WARNING: Unknown %s value '%s', must be one of: 0, 1, on, off, "
"client, server\n",
kPersistentModeEnv, mode.c_str());
return PersistentMode::Disabled;
}
PersistentMode::Client::Client() : server_executable_(GetCurrentExecutable()) {}
void PersistentMode::Client::SetServerExecutable(
const std::string& server_executable) {
server_executable_ = server_executable;
}
// static
PersistentService::Client PersistentMode::Client::GetService(
const std::string& build_dir) {
CLIENT_LOG("GetService(%s)", build_dir.c_str());
return PersistentService::Client(GetServiceName(build_dir));
}
bool PersistentMode::Client::IsServerRunning(const std::string& build_dir) {
return GetService(build_dir).HasServer();
}
int PersistentMode::Client::GetServerPidFor(const std::string& build_dir) {
return GetService(build_dir).GetServerPid();
}
bool PersistentMode::Client::StopServer(const std::string& build_dir,
std::string* err) {
return GetService(build_dir).StopServer(err);
}
bool PersistentMode::Client::RunQuery(const Compatibility& compatibility,
const BuildQuery& query,
int* exit_code_ptr, std::string* error) {
// Return an error early if the build directory is not valid.
if (!compatibility.CheckBuildDir(error))
return false;
// Ensure the persistent mode status is set to 'server' in spawned servers.
PersistentService::Config server_config =
PersistentService::Config(compatibility.ToServerArgs(server_executable_))
.SetWorkingDir(compatibility.build_dir())
.SetVersionInfo(compatibility.ToEncodedString())
.AddEnvVariable(kPersistentModeEnv, "server");
// Set log file path from env. This is a debugging aid.
{
const char* env = getenv("NINJA_PERSISTENT_LOG_FILE");
if (env && env[0]) {
server_config.SetLogFile(std::string(env));
}
}
// Ensure that the persistent timeout value from the client environment
// is passed to new server instances.
{
const char* env = getenv(kPersistentTimeoutSecondsEnv);
if (env)
server_config.AddEnvVariable(kPersistentTimeoutSecondsEnv, env);
}
CLIENT_LOG("Connecting to server.");
PersistentService::Client client = GetService(compatibility.build_dir());
IpcHandle connection = client.Connect(server_config, error);
if (!connection) {
CLIENT_LOG("Could not connect to server: %s", error->c_str());
return false;
}
// Send the query now.
CLIENT_LOG("Sending query to server.");
if (!RemoteWrite(query.ToEncodedString(), connection, error)) {
*error = "Could not send build query: " + *error;
return false;
}
// Receive the server PID, to redirect signals to it.
#ifdef _WIN32
DWORD server_pid = 0;
#else // !_WIN32
pid_t server_pid = -1;
#endif // !_WIN32
if (!RemoteRead(server_pid, connection, error)) {
*error = "Could not receive server pid: " + *error;
return false;
}
CLIENT_LOG("Received remote server pid: %d\n", server_pid);
StdioRedirector stdio_redirect(connection);
if (!stdio_redirect.SendStandardDescriptors(error))
return false;
InterruptForwarder interrupt_forwarder(server_pid);
// Wait for the corresponding exit code.
CLIENT_LOG("Waiting for query exit code from server.");
int exit_code = -1;
if (!RemoteRead(exit_code, connection, error)) {
*error = "Could not receive build request exit code: " + *error;
return false;
}
CLIENT_LOG("Query ended with exit code %d", exit_code);
*exit_code_ptr = exit_code;
return true;
}
PersistentMode::Server::Server(const Compatibility& compatibility)
: compatibility_(compatibility),
server_(GetServiceName(compatibility_.build_dir())) {
SERVER_LOG("Server for %s", compatibility_.build_dir().c_str());
// Compute the server connection timeout, default value is 5 minutes
// and can be overriden with kPersistentTimeoutSecondsEnv in the
// environment.
int64_t connection_timeout_ms = 5 * 60 * 1000;
const char* env = getenv(kPersistentTimeoutSecondsEnv);
if (env) {
int64_t timeout_secs = static_cast<int64_t>(atoll(env));
if (timeout_secs < 0) {
SERVER_LOG(
"Ignoring invalid timeout value (%s): must be strictly positive!",
env);
} else {
connection_timeout_ms = timeout_secs * 1000;
}
}
server_.SetConnectionTimeoutMs(connection_timeout_ms);
}
bool PersistentMode::Server::StartLocalServer(std::string* error) {
// Return an error early if the build directory is not valid.
if (!compatibility_.CheckBuildDir(error))
return false;
return server_.BindService(error);
}
void PersistentMode::Server::RunServerThenExit(
RestartChecker ninja_restart_check, BuildQueryHandler query_handler) {
// PersistentService::Server::VersionCheckHandler used to verify compatibility
// and invoke the restart check callback before each query.
auto version_check_handler =
[this, &ninja_restart_check](const std::string& version) -> std::string {
std::string error;
auto client_compatibility =
Compatibility::FromEncodedString(version, &error);
if (!error.empty())
return error;
std::string reason;
if (!compatibility_.IsCompatibleWith(client_compatibility, &reason))
return StringFormat("Incompatible build plan: %s", reason.c_str());
if (ninja_restart_check())
return "Build manifest files updated!";
return {};
};
// A Persistent::Server::RequestHandler instance to run queries on the
// server.
auto request_handler = [this, &query_handler](IpcHandle connection) -> bool {
return RunQueryOnServer(std::move(connection), query_handler);
};
// Ensure that NINJA_PERSISTENT_MODE is disabled by default when
// running queries, since Ninja commands can themselves invoke Ninja
// (e.g. to perform sub-builds in other directories).
//
// Persistent mode for these sub-builds is disabled by default, but can
// be enabled by the user by adding NINJA_PERSISTENT_MODE to the list in
// NINJA_PERSISTENT_PASS_VARIABLES.
ScopedEnvironmentVariable no_persistent_mode(kPersistentModeEnv, "0");
server_.RunServerThenExit(version_check_handler, request_handler);
}
bool PersistentMode::Server::RunQueryOnServer(
IpcHandle connection, const BuildQueryHandler& query_handler) {
int64_t query_start_ms = AsyncLoop::NowMs();
printf("Starting client request\n");
std::string error;
SERVER_LOG("New client request from handle %s", connection.display().c_str());
// Receive build query.
std::string encoded_query;
if (!RemoteRead(encoded_query, connection, &error)) {
return PrintError("Could not receive build query: %s", error.c_str());
}
auto query =
PersistentMode::BuildQuery::FromEncodedString(encoded_query, &error);
if (!error.empty())
return PrintError("Could not receive build query: %s\n", error.c_str());
query.WriteGlobalVariables();
// Send our pid to allow the client to redirect signals to us.
#ifdef _WIN32
DWORD pid = GetCurrentProcessId();
#else // !_WIN32
pid_t pid = getpid();
#endif // !_WIN32
if (!RemoteWrite(pid, connection, &error))
return PrintError("Could not send server pid: %s", error.c_str());
// Temporarily redirect stdin/stdout/stderr from client-provided handles.
int exit_code;
{
StdioRedirector stdio_redirect(connection);
if (!stdio_redirect.ReceiveStandardDescriptors(&error))
return PrintError("Could not receive standard descriptors: %s",
error.c_str());
exit_code = query_handler(query);
}
if (!RemoteWrite(exit_code, connection, &error)) {
return PrintError("Could not send exit_code=%d back to client: %s",
error.c_str());
}
// Keep server running until next client request.
SERVER_LOG("Request completed with exit_code=%d", exit_code);
// Print statistics about the request.
int64_t query_time_ms = AsyncLoop::NowMs() - query_start_ms;
printf("Request took %s to complete\n",
StringFormatDurationMs(query_time_ms).c_str());
if (exit_code == kServerExit) {
printf("Server exiting after query.");
return false;
}
return true;
}
// Avoid compiler warnings in debug mode.
constexpr int PersistentMode::kServerExit;