blob: e55a075b9dd61a7a239c04dddb01185d27e0b87b [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.
#ifndef NINJA_PERSISTENT_MODE_H_
#define NINJA_PERSISTENT_MODE_H_
#include <functional>
#include <string>
#include <vector>
#include "build_config.h"
#include "ipc_utils.h"
#include "persistent_service.h"
/// Implementation for Ninja's persistent mode.
///
/// This class wraps Ninja's persistent mode implementation on top of the
/// generic PersistentService class. In particular:
///
/// - Provides static methods to determine the role of the current process
/// (based on the NNINJA_PERSISTENT_MODE environment variable).
///
/// - Provides a way to check that the client and the server correspond to
/// the same build plan (see Compatibility).
///
/// - Provides client methods to check the status of the server, or stop it
/// (to implement the `-t server` tool commands).
///
/// - Provides a client method to run a query on the server, automatically
/// starting a new instance if none is available, if the available one
/// is not compatible with the current build plan, or if any of the input
/// .ninja manifest files has been updated.
///
/// - Provides a way to start the server early, and to process Ninja build
/// or tool requests on the server.
///
/// Example Usage:
///
/// // build plan configuration data used to match client/server
/// // compatibility.
/// auto compatibility = PersistentMode::Compatibility();
/// compatibility.SetBuildDir(<absolute_build_dir>);
///
/// // An object describing the current Ninja build query or tool
/// // invocation to perform.
/// PersistentMode::BuildQuery query;
/// query.config = build_config;
/// query.args = RemoteArguments(argc, argv).args();
/// ...
///
/// // A callable used to run the query, either on the server, or in the
/// // client if launching a new server was not possible.
/// auto handle_query = [](const PersistentMode::BuildQuery& query) -> int {
/// ... // perform build or tool query.
/// return 0;
/// };
///
/// auto persistent_status = PersistentMode::GetProcessStatus();
/// is (status == PersistentMode::IsServer) {
/// PersistentMode::Server server(compatibility);
///
/// if (!server.StartLocalServer(&err))
/// Fatal("Could not start server: %s", err.c_str());
///
/// ... load build plan
///
/// // A callable used by the server before running each
/// // query to verify if the build plan changed (e.g. an
/// // input build.ninja file was updated on the filesystem).
/// auto restart_check = []() -> bool {
/// return false;
/// };
///
/// // Run the server loop till the end.
/// server.RunServerThenExit(std::move(restart_check),
/// std::move(handle_query));
/// }
///
/// if (persistent_status == PersistentMode::Client) {
/// PersistentMode::Client client;
///
/// // Run the query, this takes care of redirecting stdio
/// // and ensuring Ctrl-C will be handled by the server
/// // properly (i.e. to stop current query, and not abort
/// // the server).
/// int exit_code = -1;
/// if (!client.RunQuery(compatibility, query, &exit_code, &error)) {
/// Fatal("Could not run Ninja query: %s", error.c_str());
/// }
///
/// // query was run on the server.
/// exit(exit_code);
/// }
///
/// assert(persistent_status == PersistentMode::Disabled) {
/// ... load build plan
/// ... do a local build as usual.
/// }
///
struct PersistentMode {
/// Status of persistent mode for the current process
enum Status {
Disabled = 0,
IsClient,
IsServer,
};
/// Return the status of the current process.
static Status GetCurrentProcessStatus();
/// The configuration information that each persistent server must compare
/// with each client query to verify that they are compatible, i.e. correspond
/// to the same Ninja build plan. Note that some flags change the way
/// manifests are loaded, and thus introduce incompatibilities when they
/// are different.
struct Compatibility {
/// Create new instance from command-line options.
///
/// |input_file| is an optional pointer to the top-level Ninja manifest
/// file name. The nullptr value defaults to "build.ninja".
///
/// |build_dir| is an optional pointer to the build directory, the nullptr
/// value defaults to the current working directory.
///
/// |dupe_edges_should_err| corresponds to the `-w dupbuild=err` option,
/// and |phony_cycle_should_err| corresponds to the `-w phonycycle=err` one,
/// and both change the way manifest files are loaded.
static Compatibility FromOptions(const char* input_file,
const char* build_dir,
bool dupe_edges_should_err,
bool phony_cycle_should_err);
Compatibility();
/// Override the ninja_version() string. Only use this for tests.
Compatibility& SetNinjaVersionForTest(const std::string& new_version);
/// Change the input file, default value if "build.ninja"
Compatibility& SetInputFile(const std::string& input_file);
/// Change the build directory, and empty value matches the current working
/// directory. WARNING: It is critically important for the client and server
/// to use the same value consistenly. Outside of tests, ensure that a
/// non-empty value is always an absolute path!
Compatibility& SetBuildDir(const std::string& build_dir);
/// Set the value of the `-w dupedges` flag, which changes the way build
/// manifests are loaded. Default value is true.
Compatibility& SetFlagDupeEdgesShouldErr(bool enabled);
/// Set the value of the `-w phonycycle` flag, which changes the way
/// build manifests are loaded. Default value is false.
Compatibility& SetFlagPhonyCycleShouldErr(bool enabled);
/// Create new instance from an encoded string.
/// On success return new instance after clearing |*error|.
/// On failure return empty instance after setting |*error| to non-empty
/// string.
static Compatibility FromEncodedString(const std::string& str,
std::string* error);
/// Convert to string for sending through IPC. Not human-readable.
std::string ToEncodedString() const;
/// Convert to human-readable string for debugging.
std::string ToString() const;
/// Return true if this instance is compatiable with |other|.
/// On failure, return false after setting |*reason|.
bool IsCompatibleWith(const Compatibility& other,
std::string* reason) const;
/// Return working directory.
const std::string& build_dir() const { return build_dir_; }
/// Return a string trying to identify the Ninja version being used.
/// Note that this may include a timestamp or content hash of the current
/// executable as well. The default value is computed by the constructor
/// automatically.
const std::string& ninja_version() const { return ninja_version_; };
/// Return a command-line used to start a server compatible with this
/// instance.
std::vector<std::string> ToServerArgs(
const std::string& server_executable) const;
/// Returns true if the build directory is absolute (or empty, in which
/// case this will be interpreted as the absolute path of the current
/// directory). On failure, set |*err| then return false.
bool CheckBuildDir(std::string* err) const;
private:
/// Return the default Ninja version, called from constructor.
static std::string GetDefaultNinjaVersion();
/// Ninja version. This includes a timestamp of the Ninja executable
/// as well. Must be the first field in this struct.
std::string ninja_version_;
/// Current build directory.
std::string build_dir_;
/// Input file name, cannot be empty, relative paths are always
/// relative to |build_dir|.
std::string input_file_ = "build.ninja";
/// Options controlling how the manifest is loaded.
bool dupe_edges_should_err_ = true;
bool phony_cycle_should_err_ = false;
};
/// A callable object that returns true if any condition invalidated
/// the content of the in-memory build graph. In practice this would be
/// when any of the input .ninja files has changed. A return value of true
/// indicates that the server must be stopped, and another one restarted.
using RestartChecker = std::function<bool(void)>;
/// Describe a query from the client to the server. This can be used to
/// run either a build or a tool (when the |tool| field is not empty).
struct BuildQuery {
/// Build configuration
BuildConfig config;
/// Mirror the set of debug flags in "debug_flags.h" that can be set
/// individually with `-d <flag>` on the command line that do affect
/// each build. These are currently stored in global variables instead
/// of BuildConfig :-/
bool debug_explaining = false;
bool debug_keep_depfile = false;
bool debug_keep_rsp = false;
bool debug_experimental_statcache = false;
/// Set to true to dump metrics after a build.
bool dump_metrics = false;
/// Optional tool name, empty means a build is requested, instead of a
/// tool run.
std::string tool;
/// List of command-line arguments. If |tool| is empty, this is a list
/// of targets and should not contain any option. If |tool| is not empty
/// then this is a list of arguments for the tool.
std::vector<std::string> args;
std::string ToEncodedString() const;
std::string ToString() const;
void WriteGlobalVariables() const;
static BuildQuery FromEncodedString(const std::string& str,
std::string* error);
};
/// A special value that can be returned by a BuildQueryHandler to
/// indicate that the server should exit immediately after
/// serving the query.
static constexpr int kServerExit = -2;
/// A callable object that implements a query. The result value can
/// be the special kServerExit value to indicate that the server
/// should stop immediately. Otherwise, it must be a process exit
/// code that is >= 0, corresponding to the result of the query.
using BuildQueryHandler = std::function<int(const BuildQuery&)>;
class Client {
public:
/// Create new instance.
Client();
/// Set the path to the server executable. By default, this is the value
/// returned by GetCurrentExecutable(), but this method allows one to
/// override it for testing.
void SetServerExecutable(const std::string& server_path);
/// Return server executable path. Used for testing.
const std::string server_executable() const { return server_executable_; }
/// Return true if a server is already running, serving a given build
/// directory. An empty |build_dir| string means the current working
/// directory.
bool IsServerRunning(const std::string& build_dir);
/// Retrieve process ID of server, if it exists, for |build_dir|,
/// or -1 otherwise. NOTE: DOES NOT WORK ON WINDOWS YET.
int GetServerPidFor(const std::string& build_dir);
/// Stop the server if it exists, return true if a server was running
/// before the call for a given build directory. An empty |build_dir|
/// string means the current working directory.
bool StopServer(const std::string& build_dir, std::string* err);
/// Ask the server to run a query for us.
///
/// |compatibility| is used to connect either launch a new server instance,
/// or verify that the existing one serving |compatibility.build_dir()| is
/// compatible (if not, that instance is shut down, and a new one is
/// launched).
///
/// On success return true and set |*exit_code_ptr| to the result of the
/// query, on failure to connect to the server, set |*error| and return
/// false.
bool RunQuery(const Compatibility& compatibility, const BuildQuery& query,
int* exit_code_ptr, std::string* error);
private:
static PersistentService::Client GetService(const std::string& build_dir);
std::string server_executable_;
};
class Server {
public:
/// Create new instance. This does not do anything except recording
/// the value of |compatibility|.
explicit Server(const Compatibility& compatibility);
/// Set the timeout, in milliseconds, used by server instances waiting
/// for new client connections. On expiration, the server will shutdown
/// gracefully. A negative value (the default) means no timeout.
/// This must be called before StartLocalServer() or RunServerThenExit().
void SetConnectionTimeoutMs(int64_t connection_timeout_ms) {
server_.SetConnectionTimeoutMs(connection_timeout_ms);
}
/// Start the server in the current process. This can be called before
/// RunServerThenExit() to quickly acquire the service named pipe and fail
/// if not possible, before performing expensive operations like loading
/// the build plan. Called by RunServerThenExit() implicitly otherwise.
bool StartLocalServer(std::string* error);
/// Run the server main loop, which will handle incoming
/// build requests by first calling |restart_checker| to verify whether
/// a restart is needed (e.g. if the build plan changed), then serve
/// the request with |query_handler|. This function never returns.
void RunServerThenExit(RestartChecker restart_checker,
BuildQueryHandler query_handler);
private:
bool RunQueryOnServer(IpcHandle connection,
const BuildQueryHandler& query_handler);
Compatibility compatibility_;
int64_t connection_timeout_ms_ = -1;
PersistentService::Server server_;
};
};
#endif // NINJA_PERSISTENT_MODE_H_