| // 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; |