From f57dd71b913e20d08a42a5b353f36601aef38237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Hundeb=C3=B8ll?= Date: Fri, 17 May 2024 10:13:28 +0200 Subject: [PATCH 1/2] Implement GNU Make 4.4+ jobserver fifo / semaphore client support The principle of such a job server is rather simple: Before starting a new job (edge in ninja-speak), a token must be acquired from an external entity. On posix systems, that entity is simply a fifo filled with N characters. On win32 systems it is a semaphore initialized to N. Once a job is finished, the token must be returned to the external entity. This functionality is desired when ninja is used as part of a bigger build, such as builds with Yocto/OpenEmbedded, Buildroot and Android. Here, multiple compile jobs are executed in parallel to maximize cpu utilization, but if each compile job uses all available cores, the system is over loaded. --- CMakeLists.txt | 7 ++- configure.py | 3 ++ src/build.cc | 39 +++++++++++++--- src/build.h | 9 +++- src/build_test.cc | 6 ++- src/graph.h | 1 + src/jobserver-posix.cc | 71 ++++++++++++++++++++++++++++ src/jobserver-win32.cc | 60 ++++++++++++++++++++++++ src/jobserver.cc | 101 +++++++++++++++++++++++++++++++++++++++ src/jobserver.h | 104 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 src/jobserver-posix.cc create mode 100644 src/jobserver-win32.cc create mode 100644 src/jobserver.cc create mode 100644 src/jobserver.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b8fdee7d3a..0032f515d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,7 @@ add_library(libninja OBJECT src/eval_env.cc src/graph.cc src/graphviz.cc + src/jobserver.cc src/json.cc src/line_printer.cc src/manifest_parser.cc @@ -153,6 +154,7 @@ add_library(libninja OBJECT if(WIN32) target_sources(libninja PRIVATE src/subprocess-win32.cc + src/jobserver-win32.cc src/includes_normalize-win32.cc src/msvc_helper-win32.cc src/msvc_helper_main-win32.cc @@ -169,7 +171,10 @@ if(WIN32) # errors by telling windows.h to not define those two. add_compile_definitions(NOMINMAX) else() - target_sources(libninja PRIVATE src/subprocess-posix.cc) + target_sources(libninja PRIVATE + src/subprocess-posix.cc + src/jobserver-posix.cc + ) if(CMAKE_SYSTEM_NAME STREQUAL "OS400" OR CMAKE_SYSTEM_NAME STREQUAL "AIX") target_sources(libninja PRIVATE src/getopt.c) # Build getopt.c, which can be compiled as either C or C++, as C++ diff --git a/configure.py b/configure.py index c88daad508..1a12e5c0c8 100755 --- a/configure.py +++ b/configure.py @@ -542,6 +542,7 @@ def has_re2c() -> bool: 'eval_env', 'graph', 'graphviz', + 'jobserver', 'json', 'line_printer', 'manifest_parser', @@ -556,6 +557,7 @@ def has_re2c() -> bool: objs += cxx(name, variables=cxxvariables) if platform.is_windows(): for name in ['subprocess-win32', + 'jobserver-win32', 'includes_normalize-win32', 'msvc_helper-win32', 'msvc_helper_main-win32']: @@ -565,6 +567,7 @@ def has_re2c() -> bool: objs += cc('getopt') else: objs += cxx('subprocess-posix') + objs += cxx('jobserver-posix') if platform.is_aix(): objs += cc('getopt') if platform.is_msvc(): diff --git a/src/build.cc b/src/build.cc index deb8f04c8b..18e51558a1 100644 --- a/src/build.cc +++ b/src/build.cc @@ -51,7 +51,7 @@ struct DryRunCommandRunner : public CommandRunner { virtual ~DryRunCommandRunner() {} // Overridden from CommandRunner: - virtual size_t CanRunMore() const; + size_t CanRunMore(bool jobserver_enabled) const override; virtual bool StartCommand(Edge* edge); virtual bool WaitForCommand(Result* result); @@ -59,7 +59,7 @@ struct DryRunCommandRunner : public CommandRunner { queue finished_; }; -size_t DryRunCommandRunner::CanRunMore() const { +size_t DryRunCommandRunner::CanRunMore(bool jobserver_enabled) const { return SIZE_MAX; } @@ -164,8 +164,18 @@ Edge* Plan::FindWork() { if (ready_.empty()) return NULL; + // Don't initiate more work if the jobserver cannot acquire more tokens + if (jobserver_.Enabled() && !jobserver_.Acquire()) { + return nullptr; + } + Edge* work = ready_.top(); ready_.pop(); + + // Mark this edge as holding a token to release the token once the edge work + // is finished. + work->acquired_job_server_token_ = jobserver_.Enabled(); + return work; } @@ -201,6 +211,12 @@ bool Plan::EdgeFinished(Edge* edge, EdgeResult result, string* err) { edge->pool()->EdgeFinished(*edge); edge->pool()->RetrieveReadyEdges(&ready_); + // Return the token acquired for this very edge to the jobserver, but only + // if it holds a token. + if (edge->acquired_job_server_token_) { + jobserver_.Release(); + } + // The rest of this function only applies to successful commands. if (result != kEdgeSucceeded) return true; @@ -579,10 +595,15 @@ void Plan::ScheduleInitialEdges() { } void Plan::PrepareQueue() { + jobserver_.Init(); ComputeCriticalPath(); ScheduleInitialEdges(); } +bool Plan::JobserverEnabled() const { + return jobserver_.Enabled(); +} + void Plan::Dump() const { printf("pending: %d\n", (int)want_.size()); for (map::const_iterator e = want_.begin(); e != want_.end(); ++e) { @@ -596,7 +617,7 @@ void Plan::Dump() const { struct RealCommandRunner : public CommandRunner { explicit RealCommandRunner(const BuildConfig& config) : config_(config) {} virtual ~RealCommandRunner() {} - virtual size_t CanRunMore() const; + size_t CanRunMore(bool jobserver_enabled) const override; virtual bool StartCommand(Edge* edge); virtual bool WaitForCommand(Result* result); virtual vector GetActiveEdges(); @@ -619,7 +640,13 @@ void RealCommandRunner::Abort() { subprocs_.Clear(); } -size_t RealCommandRunner::CanRunMore() const { +size_t RealCommandRunner::CanRunMore(bool jobserver_enabled) const { + // Return "infinite" capacity if a jobserver is used to limit the number + // of parallel subprocesses instead. + if (jobserver_enabled) { + return SIZE_MAX; + } + size_t subproc_number = subprocs_.running_.size() + subprocs_.finished_.size(); @@ -792,7 +819,7 @@ bool Builder::Build(string* err) { while (plan_.more_to_do()) { // See if we can start any more commands. if (failures_allowed) { - size_t capacity = command_runner_->CanRunMore(); + size_t capacity = command_runner_->CanRunMore(plan_.JobserverEnabled()); while (capacity > 0) { Edge* edge = plan_.FindWork(); if (!edge) @@ -820,7 +847,7 @@ bool Builder::Build(string* err) { --capacity; // Re-evaluate capacity. - size_t current_capacity = command_runner_->CanRunMore(); + size_t current_capacity = command_runner_->CanRunMore(plan_.JobserverEnabled()); if (current_capacity < capacity) capacity = current_capacity; } diff --git a/src/build.h b/src/build.h index 9bb0c70b5c..fe11a6ea32 100644 --- a/src/build.h +++ b/src/build.h @@ -24,6 +24,7 @@ #include "depfile_parser.h" #include "exit_status.h" #include "graph.h" +#include "jobserver.h" #include "util.h" // int64_t struct BuildLog; @@ -52,6 +53,9 @@ struct Plan { /// Returns true if there's more work to be done. bool more_to_do() const { return wanted_edges_ > 0 && command_edges_ > 0; } + /// Jobserver status used to skip capacity based on load average + bool JobserverEnabled() const; + /// Dumps the current state of the plan. void Dump() const; @@ -139,6 +143,9 @@ struct Plan { /// Total remaining number of wanted edges. int wanted_edges_; + + /// Jobserver client + Jobserver jobserver_; }; /// CommandRunner is an interface that wraps running the build @@ -146,7 +153,7 @@ struct Plan { /// RealCommandRunner is an implementation that actually runs commands. struct CommandRunner { virtual ~CommandRunner() {} - virtual size_t CanRunMore() const = 0; + virtual size_t CanRunMore(bool jobserver_enabled) const = 0; virtual bool StartCommand(Edge* edge) = 0; /// The result of waiting for a command. diff --git a/src/build_test.cc b/src/build_test.cc index c84190a040..60305cf2c6 100644 --- a/src/build_test.cc +++ b/src/build_test.cc @@ -521,7 +521,7 @@ struct FakeCommandRunner : public CommandRunner { max_active_edges_(1), fs_(fs) {} // CommandRunner impl - virtual size_t CanRunMore() const; + size_t CanRunMore(bool jobserver_enabled) const override; virtual bool StartCommand(Edge* edge); virtual bool WaitForCommand(Result* result); virtual vector GetActiveEdges(); @@ -622,7 +622,9 @@ void BuildTest::RebuildTarget(const string& target, const char* manifest, builder.command_runner_.release(); } -size_t FakeCommandRunner::CanRunMore() const { +size_t FakeCommandRunner::CanRunMore(bool jobserver_enabled) const { + assert(!jobserver_enabled); + if (active_edges_.size() < max_active_edges_) return SIZE_MAX; diff --git a/src/graph.h b/src/graph.h index 314c44296a..f908d75d27 100644 --- a/src/graph.h +++ b/src/graph.h @@ -227,6 +227,7 @@ struct Edge { bool deps_loaded_ = false; bool deps_missing_ = false; bool generated_by_dep_loader_ = false; + bool acquired_job_server_token_ = false; TimeStamp command_start_time_ = 0; const Rule& rule() const { return *rule_; } diff --git a/src/jobserver-posix.cc b/src/jobserver-posix.cc new file mode 100644 index 0000000000..d040a8b63e --- /dev/null +++ b/src/jobserver-posix.cc @@ -0,0 +1,71 @@ +// Copyright 2024 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 "jobserver.h" + +#include +#include + +#include +#include + +#include "util.h" + +void Jobserver::Init() { + assert(fd_ < 0); + + if (!ParseJobserverAuth("fifo")) { + return; + } + + const char* jobserver = jobserver_name_.c_str(); + + fd_ = open(jobserver, O_NONBLOCK | O_CLOEXEC | O_RDWR); + if (fd_ < 0) { + Fatal("failed to open jobserver: %s: %s", jobserver, strerror(errno)); + } + + Info("using jobserver: %s", jobserver); +} + +Jobserver::~Jobserver() { + assert(token_count_ == 0); + + if (fd_ >= 0) { + close(fd_); + } +} + +bool Jobserver::Enabled() const { + return fd_ >= 0; +} + +bool Jobserver::AcquireToken() { + char token; + int res = read(fd_, &token, 1); + if (res < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { + Fatal("failed to read jobserver token: %s", strerror(errno)); + } + + return res > 0; +} + +void Jobserver::ReleaseToken() { + char token = '+'; + int res = write(fd_, &token, 1); + if (res != 1) { + Fatal("failed to write token: %s: %s", jobserver_name_.c_str(), + strerror(errno)); + } +} diff --git a/src/jobserver-win32.cc b/src/jobserver-win32.cc new file mode 100644 index 0000000000..494dac72fc --- /dev/null +++ b/src/jobserver-win32.cc @@ -0,0 +1,60 @@ +// Copyright 2024 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 "jobserver.h" + +#include + +#include + +#include "util.h" + +void Jobserver::Init() { + assert(sem_ == INVALID_HANDLE_VALUE); + + if (!ParseJobserverAuth("sem")) { + return; + } + + const char* name = jobserver_name_.c_str(); + + sem_ = OpenSemaphore(SYNCHRONIZE|SEMAPHORE_MODIFY_STATE, false, name); + if (sem_ == nullptr) { + Win32Fatal("OpenSemaphore", name); + } + + Info("using jobserver: %s", name); +} + +Jobserver::~Jobserver() { + assert(token_count_ == 0); + + if (sem_ != INVALID_HANDLE_VALUE) { + CloseHandle(sem_); + } +} + +bool Jobserver::Enabled() const { + return sem_ != INVALID_HANDLE_VALUE; +} + +bool Jobserver::AcquireToken() { + return WaitForSingleObject(sem_, 0) == WAIT_OBJECT_0; +} + +void Jobserver::ReleaseToken() { + if (!ReleaseSemaphore(sem_, 1, nullptr)) { + Win32Fatal("ReleaseSemaphore"); + } +} diff --git a/src/jobserver.cc b/src/jobserver.cc new file mode 100644 index 0000000000..15a91b40a5 --- /dev/null +++ b/src/jobserver.cc @@ -0,0 +1,101 @@ +// Copyright 2024 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 "jobserver.h" + +#include +#include + +#include "util.h" + +bool Jobserver::Acquire() { + // The first token is implicitly handed to the ninja process, so don't + // acquire it from the jobserver + if (token_count_ == 0 || AcquireToken()) { + token_count_++; + return true; + } + + return false; +} + +void Jobserver::Release() { + assert(token_count_ >= 1); + token_count_--; + + // Don't return first token to the jobserver, as it is implicitly handed + // to the ninja process + if (token_count_ > 0) { + ReleaseToken(); + } +} + +bool Jobserver::ParseJobserverAuth(const char* type) { + // Return early if no make flags are passed in the environment + const char* makeflags = getenv("MAKEFLAGS"); + if (makeflags == nullptr) { + return false; + } + + const char* jobserver_auth = "--jobserver-auth="; + const char* str_begin = strstr(makeflags, jobserver_auth); + if (str_begin == nullptr) { + return false; + } + + // Advance the string pointer to just past the = character + str_begin += strlen(jobserver_auth); + + // Find end of argument to reject findings from following arguments + const char* str_end = strchr(str_begin, ' '); + + // Find the length of the type value by searching for the following colon + const char* str_colon = strchr(str_begin, ':'); + if (str_colon == nullptr || (str_end && str_colon > str_end)) { + if (str_end != nullptr) { + Warning("invalid --jobserver-auth value: '%.*s'", str_end - str_begin, str_begin); + } else { + Warning("invalid --jobserver-auth value: '%s'", str_begin); + } + + return false; + } + + // Ignore the argument if the length or the value of the type value doesn't + // match the requested type (i.e. "fifo" on posix or "sem" on windows). + if (strlen(type) != static_cast(str_colon - str_begin) || + strncmp(str_begin, type, str_colon - str_begin)) { + Warning("invalid jobserver type: got %.*s; expected %s", + str_colon - str_begin, str_begin, type); + return false; + } + + // Advance the string pointer to just after the : character + str_begin = str_colon + 1; + + // Save either the remaining value until a space, or just the rest of the + // string. + if (str_end == nullptr) { + jobserver_name_ = std::string(str_begin); + } else { + jobserver_name_ = std::string(str_begin, str_end - str_begin); + } + + if (jobserver_name_.empty()) { + Warning("invalid --jobserver-auth value: ''"); + return false; + } + + return true; +} diff --git a/src/jobserver.h b/src/jobserver.h new file mode 100644 index 0000000000..cdebdcc990 --- /dev/null +++ b/src/jobserver.h @@ -0,0 +1,104 @@ +// Copyright 2024 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. + +#pragma once + +#ifdef _WIN32 +#include +#endif + +#include +#include + +/// Jobserver limits parallelism by acquiring tokens from an external +/// pool before running commands. On posix systems the pool is a fifo filled +/// with N characters. On windows systems the pool is a semaphore initialized +/// to N. When a command is finished, the acquired token is released by writing +/// it back to fifo / increasing the semaphore count. +/// +/// The jobserver functionality is enabled by passing +/// --jobserver-auth=: in the MAKEFLAGS environment variable. On +/// posix systems, is 'fifo' and is a path to a fifo. On windows +/// systems, is 'sem' and is the name of a semaphore. +/// +/// The class is used in build.cc by calling Init() to parse the MAKEFLAGS +/// argument and open the fifo / semaphore. Once enabled, Acquire() must be +/// called to try to acquire a token from the pool. If a token is acquired, a +/// new command can be started. Once the command is completed, Release() must +/// be called to return the token to the pool. +/// +/// Note that this class implements the jobserver functionality from GNU make +/// v4.4 and later. Older versions of make passes open pipe file descriptors +/// to sub-makes and specifies the file descriptor numbers using +/// --jobserver-auth=R,W in the MAKEFLAGS environment variable. The older pipe +/// method is deliberately not implemented here, as it is not as simple as the +/// fifo method. +struct Jobserver { + ~Jobserver(); + + /// Parse the MAKEFLAGS environment variable to receive the path / name of the + /// token pool, and open the handle to the pool. If a jobserver argument is + /// found in the MAKEFLAGS environment variable, and the handle is + /// successfully opened, subsequent calls to Enable() returns true. + /// If a jobserver argument is found, but the handle fails to be opened, the + /// ninja process is aborted with an error. + void Init(); + + /// Return true if jobserver functionality is enabled and initialized. + bool Enabled() const; + + /// Try to to acquire a token from the external token pool (without blocking). + /// Should be called every time Ninja needs to start a command process. + /// Return true on success (token acquired), and false on failure (no tokens + /// available). First call always succeeds. + bool Acquire(); + + /// Return a previously acquired token to the external token pool. Must be + /// called for any _successful_ call to Acquire(). Normally when a command + /// subprocess completes, or when Ninja itself exits, even in case of errors. + void Release(); + +protected: + /// Parse the --jobserver-auth argument from the MAKEFLAGS environment + /// variable. Return true if the argument is found and correctly parsed. + /// Return false if the argument is not found, or fails to parse. + bool ParseJobserverAuth(const char* type); + + /// Path to the fifo on posix systems or name of the semaphore on windows + /// systems. + std::string jobserver_name_; + +private: + /// Implementation specific method to acquire a token from the external pool, + /// which is called for all but the first requested tokens. + bool AcquireToken(); + + /// Implementation specific method to release a token to the external pool, + /// which is called for all but the last released tokens. + void ReleaseToken(); + + /// Number of currently acquired tokens. Used to know when the first (free) + /// token has been acquired / released, and to verify that all acquired tokens + /// have been released before exiting. + size_t token_count_ = 0; + +#ifdef _WIN32 + /// Handle to the opened semaphore used as external token pool. + HANDLE sem_ = INVALID_HANDLE_VALUE; +#else + /// Non-blocking file descriptor for the opened fifo used as the external + /// token pool. + int fd_ = -1; +#endif +}; From 82230976a909f02e5229f85e2642876c2fcf35ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Hundeb=C3=B8ll?= Date: Wed, 29 May 2024 15:16:02 +0200 Subject: [PATCH 2/2] jobserver: add unit tests Implement proper testing of the MAKEFLAGS parsing, and the token acquire/release logic in the jobserver class. --- CMakeLists.txt | 1 + src/jobserver_test.cc | 356 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 src/jobserver_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 0032f515d4..c4a7114540 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -272,6 +272,7 @@ if(BUILD_TESTING) src/edit_distance_test.cc src/explanations_test.cc src/graph_test.cc + src/jobserver_test.cc src/json_test.cc src/lexer_test.cc src/manifest_parser_test.cc diff --git a/src/jobserver_test.cc b/src/jobserver_test.cc new file mode 100644 index 0000000000..ca1cee9764 --- /dev/null +++ b/src/jobserver_test.cc @@ -0,0 +1,356 @@ +// Copyright 2024 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 "jobserver.h" + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +#include +#include + +#include + +/// Wrapper class to provide access to protected members of the Jobserver class. +struct JobserverWrapper : public Jobserver { + /// Forwards calls to the protected Jobserver::ParseJobserverAuth() method. + bool ParseJobserverAuth(const char* type) { + return Jobserver::ParseJobserverAuth(type); + } + + /// Provides access to the protected Jobserver::jobserver_name_ member. + const char* GetJobserverName() const { + return jobserver_name_.c_str(); + } +}; + +/// Jobserver state class that provides helpers to create, configure, and remove +/// "external" jobserver pools. +struct JobserverTest : public testing::Test { + /// Save the initial MAKEFLAGS environment variable value to allow restoring + /// it upon destruction. + JobserverTest(); + + /// Restores the MAKEFLAGS environment variable value recorded upon + /// construction. + ~JobserverTest(); + + /// Configure the --jobserver-auth=: argument in the MAKEFLAGS + /// environment value. + void ServerConfigure(const char* name); + + /// Creates an external token pool with the given \a name and \a count number + /// of tokens. Also configures the MAKEFLAGS environment variable with the + /// correct --jobserver-auth argument to make the Jobserver class use the + /// created external token pool. + void ServerCreate(const char* name, size_t count); + + /// Return the number of tokens currently available in the external token + /// pool. + int ServerCount(); + + /// Remove/close the handle to external token pool. + void ServerRemove(); + + /// Wrapped jobserver object to test on. + JobserverWrapper jobserver_; + + /// Stored makeflags read before starting tests. + const char* makeflags_ = nullptr; + + /// Name of created external jobserver token pool. + const char* name_ = nullptr; + +#ifdef _WIN32 + /// Implementation of posix setenv() for windows that forwards calls to + /// _putenv(). + int setenv(const char* name, const char* value, int _) { + std::string envstring; + + // _putenv() requires a single = string as argument. + envstring += name; + envstring += '='; + envstring += value; + + return _putenv(envstring.c_str()); + }; + + /// Implementation of posix unsetenv() for windows that forwards calls to + /// _putenv(). + int unsetenv(const char* name) { + /// Call _putenv() with ="" to unset the env variable. + return setenv(name, "", 0); + } + + /// Handle of the semaphore used as external token pool. + HANDLE sem_ = INVALID_HANDLE_VALUE; +#else + /// File descriptor of the fifo used as external token pool. + int fd_ = -1; +#endif +}; + +JobserverTest::JobserverTest() { + makeflags_ = getenv("MAKEFLAGS"); + unsetenv("MAKEFLAGS"); +} + +JobserverTest::~JobserverTest() { + if (name_ != nullptr) { + ServerRemove(); + } + + if (makeflags_ != nullptr) { + setenv("MAKEFLAGS", makeflags_, 1); + } else { + unsetenv("MAKEFLAGS"); + } +} + +void JobserverTest::ServerConfigure(const char* name) +{ + std::string makeflags("--jobserver-auth="); +#ifdef _WIN32 + makeflags += "sem:"; +#else + makeflags += "fifo:"; +#endif + makeflags += name; + + ASSERT_FALSE(setenv("MAKEFLAGS", makeflags.c_str(), 1)) << "failed to set make flags"; +} + +#ifdef _WIN32 + +void JobserverTest::ServerCreate(const char* name, size_t count) { + ASSERT_EQ(name_, nullptr) << "external token pool server already created"; + ServerConfigure(name); + name_ = name; + + // One cannot create a semaphore with a max value of 0 on windows + sem_ = CreateSemaphoreA(nullptr, count, count ? count : 1, name); + ASSERT_NE(sem_, nullptr) << "failed to create semaphore"; +} + +int JobserverTest::ServerCount() { + if (name_ == nullptr) { + return -1; + } + + size_t count = 0; + + // First acquire all the available tokens to count them + while (WaitForSingleObject(sem_, 0) == WAIT_OBJECT_0) { + count++; + } + + // Then return the acquired tokens to let the client continue + ReleaseSemaphore(sem_, count, nullptr); + + return count; +} + +void JobserverTest::ServerRemove() { + ASSERT_NE(name_, nullptr) << "external token pool not created"; + CloseHandle(sem_); + name_ = nullptr; +} + +#else // _WIN32 + +void JobserverTest::ServerCreate(const char* name, size_t count) { + ASSERT_EQ(name_, nullptr) << "external token pool already created"; + ServerConfigure(name); + name_ = name; + + if (access(name, R_OK | W_OK)) { + unlink(name); + } + + // Create and open the fifo + ASSERT_FALSE(mkfifo(name, S_IWUSR | S_IRUSR)) << "failed to create fifo"; + fd_ = open(name, O_RDWR | O_NONBLOCK); + ASSERT_GE(fd_, 0) << "failed to open fifo"; + + // Fill the fifo the requested number of tokens + std::vector tokens(count, '+'); + ASSERT_EQ(write(fd_, tokens.data(), count), count) << "failed to populate fifo"; +} + +int JobserverTest::ServerCount() { + if (name_ == nullptr) { + return -1; + } + + size_t count = 0; + char token; + + // First acquire all the available tokens to count them + while (read(fd_, &token, 1) == 1) { + count++; + } + + // Then return the acquired tokens to let the client continue + std::vector tokens(count, '+'); + if (write(fd_, tokens.data(), tokens.size()) != tokens.size()) { + return -1; + } + + return count; +} + +void JobserverTest::ServerRemove() { + ASSERT_NE(name_, nullptr) << "external token pool not created"; + close(fd_); + fd_ = -1; + unlink(name_); + name_ = nullptr; +} + +#endif // _WIN32 + +TEST_F(JobserverTest, MakeFlags) { + // Test with no make flags configured + ASSERT_FALSE(unsetenv("MAKEFLAGS")); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem")); + + // Test with no --jobserver-auth in make flags + ASSERT_FALSE(setenv("MAKEFLAGS", "--other-arg=val", 0)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem")); + + // Test fifo type + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:jobserver-1.fifo", 1)); + ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_STREQ(jobserver_.GetJobserverName(), "jobserver-1.fifo"); + + // Test sem type + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=sem:jobserver-2-sem", 1)); + ASSERT_TRUE(jobserver_.ParseJobserverAuth("sem")); + ASSERT_STREQ(jobserver_.GetJobserverName(), "jobserver-2-sem"); + + // Test preceding arguments + ASSERT_FALSE(setenv("MAKEFLAGS", "--other=val --jobserver-auth=fifo:jobserver-3.fifo", 1)); + ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_STREQ(jobserver_.GetJobserverName(), "jobserver-3.fifo"); + + // Test following arguments + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:jobserver-4.fifo", 1)); + ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_STREQ(jobserver_.GetJobserverName(), "jobserver-4.fifo"); + + // Test surrounding arguments + ASSERT_FALSE(setenv("MAKEFLAGS", "--preceeding-arg=val --jobserver-auth=fifo:jobserver-5.fifo --following-arg=val", 1)); + ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_STREQ(jobserver_.GetJobserverName(), "jobserver-5.fifo"); + + // Test invalid type + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=bad:jobserver-6", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem")); + + // Test missing type + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem")); + + // Test missing colon + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + + // Test missing colon following by another argument + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo --other-arg=val", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + + // Test missing colon following by another argument with a colon + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo --other-arg=val0:val1", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); + + // Test missing value + ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:", 1)); + ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo")); +} + +TEST_F(JobserverTest, InitNoServer) { + // Verify that the jobserver isn't enabled when no configuration is given + jobserver_.Init(); + ASSERT_FALSE(jobserver_.Enabled()); +} + +TEST_F(JobserverTest, InitServer) { + // Verify that the jobserver is enabled when a (valid) configuration is given + ServerCreate("jobserver-init", 0); + jobserver_.Init(); + ASSERT_TRUE(jobserver_.Enabled()); +} + +TEST_F(JobserverTest, InitFail) { + // Verify that the jobserver exits with an error if a non-existing jobserver + // is configured + ServerConfigure("jobserver-missing"); + ASSERT_DEATH(jobserver_.Init(), "ninja: fatal: "); +} + +TEST_F(JobserverTest, NoTokens) { + // Verify that an empty token pool does in fact provide a "default" token + ServerCreate("jobserver-empty", 0); + + jobserver_.Init(); + ASSERT_TRUE(jobserver_.Acquire()); + ASSERT_FALSE(jobserver_.Acquire()); + jobserver_.Release(); +} + +TEST_F(JobserverTest, OneToken) { + // Verify that a token pool with exactly one token allows acquisition of one + // "default" token and one "external" token + ServerCreate("jobserver-one", 1); + jobserver_.Init(); + + for (int i = 0; i < 2; i++) { + ASSERT_TRUE(jobserver_.Acquire()); + } + + ASSERT_FALSE(jobserver_.Acquire()); + + for (int i = 0; i < 2; i++) { + jobserver_.Release(); + } +} + +TEST_F(JobserverTest, AcquireRelease) { + // Verify that Acquire() takes a token from the external pool, and that + // Release() returns it again. + ServerCreate("jobserver-acquire-release", 5); + jobserver_.Init(); + + ASSERT_TRUE(jobserver_.Acquire()); + ASSERT_EQ(ServerCount(), 5); + + ASSERT_TRUE(jobserver_.Acquire()); + ASSERT_EQ(ServerCount(), 4); + + jobserver_.Release(); + ASSERT_EQ(ServerCount(), 5); + + jobserver_.Release(); + ASSERT_EQ(ServerCount(), 5); +}