blob: 09e987b321a9db0b41943863b31d332e3a5e6b6d [file]
#include <cassert>
#include <cstddef>
#include <deque>
#include <functional>
#include <iostream>
#include <vector>
#include <cm/optional>
#include <cm3p/uv.h>
#ifdef _WIN32
# include <windows.h>
#else
# include <unistd.h>
#endif
#include "cmGetPipes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmUVHandlePtr.h"
#include "cmUVJobServerClient.h"
namespace {
std::size_t const kTOTAL_JOBS = 10;
std::size_t const kTOTAL_TOKENS = 3;
#ifdef _WIN32
std::size_t NextSemaphoreId = 0;
struct JobServerSemaphore
{
std::string Name;
HANDLE Handle = nullptr;
JobServerSemaphore(LONG initialCount, LONG maximumCount)
: Name(cmStrCat("cmake_test_jobserver_", GetCurrentProcessId(), '_',
++NextSemaphoreId))
{
SetLastError(ERROR_SUCCESS);
this->Handle = CreateSemaphoreA(nullptr, initialCount, maximumCount,
this->Name.c_str());
if (this->Handle && GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(this->Handle);
this->Handle = nullptr;
}
}
~JobServerSemaphore()
{
if (this->Handle) {
CloseHandle(this->Handle);
}
}
JobServerSemaphore(JobServerSemaphore const&) = delete;
JobServerSemaphore& operator=(JobServerSemaphore const&) = delete;
explicit operator bool() const { return this->Handle != nullptr; }
bool CountAvailableTokens(std::size_t& count) const
{
count = 0;
for (;;) {
DWORD const result = WaitForSingleObject(this->Handle, 0);
if (result == WAIT_OBJECT_0) {
++count;
} else if (result == WAIT_TIMEOUT) {
break;
} else {
return false;
}
}
return count == 0 ||
ReleaseSemaphore(this->Handle, static_cast<LONG>(count), nullptr);
}
};
struct MakeFlagsGuard
{
cm::optional<std::string> Original = cmSystemTools::GetEnvVar("MAKEFLAGS");
~MakeFlagsGuard()
{
if (this->Original) {
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=", *this->Original));
} else {
cmSystemTools::UnsetEnv("MAKEFLAGS");
}
}
};
void SetJobServer(JobServerSemaphore const& jobServer)
{
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=--flags-before"
" --jobserver-auth=bogus"
" --flags-between"
" --jobserver-auth=",
jobServer.Name, " --flags-after"));
}
bool CheckTokenCount(JobServerSemaphore const& jobServer, std::size_t expected)
{
std::size_t count;
if (!jobServer.CountAvailableTokens(count)) {
std::cerr << "Failed to inspect job server semaphore\n";
return false;
}
if (count != expected) {
std::cerr << "Expected " << expected << " job server tokens, got " << count
<< '\n';
return false;
}
return true;
}
#endif
struct Job
{
cm::uv_timer_ptr Timer;
};
struct JobRunner
{
cm::uv_loop_ptr Loop;
cm::optional<cmUVJobServerClient> JSC;
std::vector<Job> Jobs;
std::size_t NextJobIndex = 0;
std::size_t ActiveJobs = 0;
std::deque<std::size_t> Queue;
bool Okay = true;
JobRunner()
: Jobs(kTOTAL_JOBS)
{
this->Loop.init(nullptr);
this->JSC = cmUVJobServerClient::Connect(
*this->Loop, [this]() { this->StartQueuedJob(); }, nullptr);
if (!this->JSC) {
std::cerr << "Failed to connect to job server.\n";
this->Okay = false;
}
}
~JobRunner() {}
bool Run()
{
if (this->Okay) {
this->QueueNextJobs();
uv_run(this->Loop, UV_RUN_DEFAULT);
std::cerr << "HeldTokens: " << this->JSC->GetHeldTokens() << '\n';
std::cerr << "NeedTokens: " << this->JSC->GetNeedTokens() << '\n';
}
return this->Okay;
}
void QueueNextJobs()
{
std::cerr << "QueueNextJobs()\n";
std::size_t queued = 0;
while (queued < 2 && this->NextJobIndex < this->Jobs.size()) {
this->QueueJob(this->NextJobIndex);
++this->NextJobIndex;
++queued;
}
std::cerr << "QueueNextJobs done\n";
}
void StartQueuedJob()
{
std::cerr << "StartQueuedJob()\n";
assert(!this->Queue.empty());
std::size_t index = this->Queue.front();
this->Queue.pop_front();
this->StartJob(index);
std::cerr << "StartQueuedJob done\n";
}
void StartJob(std::size_t index)
{
cm::uv_timer_ptr& job = this->Jobs[index].Timer;
job.init(*this->Loop, this);
uv_timer_start(
job,
[](uv_timer_t* handle) {
uv_timer_stop(handle);
auto self = static_cast<JobRunner*>(handle->data);
self->FinishJob();
},
/*timeout_ms=*/10 * (1 + (index % 3)), /*repeat_ms=*/0);
++this->ActiveJobs;
std::cerr << " StartJob(" << index
<< "): Active jobs: " << this->ActiveJobs << '\n';
if (this->ActiveJobs > kTOTAL_TOKENS) {
std::cerr << "Started more than " << kTOTAL_TOKENS << " jobs at once!\n";
this->Okay = false;
return;
}
}
void QueueJob(std::size_t index)
{
this->JSC->RequestToken();
this->Queue.push_back(index);
std::cerr << " QueueJob(" << index
<< "): Queue length: " << this->Queue.size() << '\n';
}
void FinishJob()
{
--this->ActiveJobs;
std::cerr << "FinishJob: Active jobs: " << this->ActiveJobs << '\n';
this->JSC->ReleaseToken();
this->QueueNextJobs();
}
};
bool testJobServer()
{
#ifdef _WIN32
JobServerSemaphore jobServer(kTOTAL_TOKENS - 1, kTOTAL_TOKENS - 1);
if (!jobServer) {
std::cerr << "Failed to create job server semaphore\n";
return false;
}
SetJobServer(jobServer);
#else
// Create a job server pipe.
int jobServerPipe[2];
if (cmGetPipes(jobServerPipe) < 0) {
std::cerr << "Failed to create job server pipe\n";
return false;
}
// Write N-1 tokens to the pipe.
std::vector<char> jobServerInit(kTOTAL_TOKENS - 1, '.');
if (write(jobServerPipe[1], jobServerInit.data(), jobServerInit.size()) !=
kTOTAL_TOKENS - 1) {
std::cerr << "Failed to initialize job server pipe\n";
return false;
}
// Establish the job server client context.
// Add a bogus server spec to verify we use the last spec.
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=--flags-before"
" --jobserver-auth=bogus"
" --flags-between"
" --jobserver-fds=",
jobServerPipe[0], ',', jobServerPipe[1],
" --flags-after"));
#endif
bool passed;
{
JobRunner jobRunner;
passed = jobRunner.Run();
}
#ifdef _WIN32
passed = CheckTokenCount(jobServer, kTOTAL_TOKENS - 1) && passed;
#endif
return passed;
}
#ifdef _WIN32
bool ConnectsWithMakeFlags(std::string const& makeFlags)
{
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=", makeFlags));
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
cm::optional<cmUVJobServerClient> client =
cmUVJobServerClient::Connect(*loop, nullptr, nullptr);
return client.has_value();
}
bool testJobServerParsing()
{
JobServerSemaphore jobServer(1, 1);
if (!jobServer) {
std::cerr << "Failed to create parsing test semaphore\n";
return false;
}
bool passed = true;
passed =
ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=", jobServer.Name)) &&
passed;
passed = ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=bogus "
"--jobserver-fds=1,2 "
"--jobserver-auth=",
jobServer.Name)) &&
passed;
passed = !ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=", jobServer.Name,
" --jobserver-fds=1,2")) &&
passed;
passed = !ConnectsWithMakeFlags("--jobserver-auth=fifo:somewhere") && passed;
passed = !ConnectsWithMakeFlags("--jobserver-auth=") && passed;
cmSystemTools::UnsetEnv("MAKEFLAGS");
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
passed = !cmUVJobServerClient::Connect(*loop, nullptr, nullptr) && passed;
if (!passed) {
std::cerr << "Job server MAKEFLAGS parsing test failed\n";
}
return passed;
}
bool testDeferredImplicitTokenOrdering()
{
JobServerSemaphore jobServer(1, 1);
if (!jobServer) {
return false;
}
SetJobServer(jobServer);
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
std::size_t deliveries = 0;
cm::optional<cmUVJobServerClient> client;
client = cmUVJobServerClient::Connect(
*loop,
[&]() {
++deliveries;
client->ReleaseToken();
},
nullptr);
if (!client) {
return false;
}
client->RequestToken();
client->RequestToken();
bool passed = deliveries == 0;
uv_run(loop, UV_RUN_DEFAULT);
passed = deliveries == 2 && client->GetHeldTokens() == 0 &&
client->GetNeedTokens() == 0 && CheckTokenCount(jobServer, 1) && passed;
if (!passed) {
std::cerr << "Deferred implicit-token ordering test failed\n";
}
return passed;
}
bool testTimerRestartFromCallback()
{
JobServerSemaphore jobServer(2, 2);
if (!jobServer) {
return false;
}
SetJobServer(jobServer);
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
std::size_t deliveries = 0;
cm::optional<cmUVJobServerClient> client;
client = cmUVJobServerClient::Connect(
*loop,
[&]() {
++deliveries;
if (deliveries == 2) {
client->ReleaseToken();
client->RequestToken();
} else if (deliveries == 3) {
client->ReleaseToken();
}
},
nullptr);
if (!client) {
return false;
}
client->RequestToken();
uv_run(loop, UV_RUN_DEFAULT);
bool passed = deliveries == 1 && client->GetHeldTokens() == 1;
client->RequestToken();
uv_run(loop, UV_RUN_DEFAULT);
passed = deliveries == 3 && client->GetHeldTokens() == 1 &&
client->GetNeedTokens() == 0 && passed;
client->ReleaseToken();
passed = CheckTokenCount(jobServer, 2) && passed;
if (!passed) {
std::cerr << "Timer restart test failed\n";
}
return passed;
}
bool testBoundedTokenDrain()
{
std::size_t const explicitTokens = 40;
JobServerSemaphore jobServer(explicitTokens, explicitTokens);
if (!jobServer) {
return false;
}
SetJobServer(jobServer);
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
std::size_t deliveries = 0;
cm::optional<cmUVJobServerClient> client =
cmUVJobServerClient::Connect(*loop, [&]() { ++deliveries; }, nullptr);
if (!client) {
return false;
}
client->RequestToken();
uv_run(loop, UV_RUN_DEFAULT);
for (std::size_t i = 0; i < explicitTokens; ++i) {
client->RequestToken();
}
uv_run(loop, UV_RUN_NOWAIT);
bool passed = deliveries == 33 && client->GetNeedTokens() == 8;
uv_run(loop, UV_RUN_DEFAULT);
passed = deliveries == explicitTokens + 1 &&
client->GetHeldTokens() == static_cast<int>(explicitTokens + 1) &&
client->GetNeedTokens() == 0 && passed;
for (std::size_t i = 0; i < explicitTokens + 1; ++i) {
client->ReleaseToken();
}
passed = CheckTokenCount(jobServer, explicitTokens) && passed;
if (!passed) {
std::cerr << "Bounded token drain test failed\n";
}
return passed;
}
bool testPendingRequestTeardown()
{
JobServerSemaphore jobServer(0, 1);
if (!jobServer) {
return false;
}
SetJobServer(jobServer);
cm::uv_loop_ptr loop;
if (loop.init(nullptr) != 0) {
return false;
}
std::size_t deliveries = 0;
{
cm::optional<cmUVJobServerClient> client =
cmUVJobServerClient::Connect(*loop, [&]() { ++deliveries; }, nullptr);
if (!client) {
return false;
}
client->RequestToken();
uv_run(loop, UV_RUN_NOWAIT);
client->RequestToken();
uv_run(loop, UV_RUN_NOWAIT);
if (deliveries != 1 || client->GetHeldTokens() != 1 ||
client->GetNeedTokens() != 1) {
std::cerr << "Pending request setup failed\n";
return false;
}
}
uv_run(loop, UV_RUN_DEFAULT);
bool const passed = CheckTokenCount(jobServer, 0);
if (!passed) {
std::cerr << "Pending request teardown test failed\n";
}
return passed;
}
#endif
}
int testUVJobServerClient(int, char** const)
{
#ifdef _WIN32
MakeFlagsGuard makeFlagsGuard;
#endif
bool passed = true;
passed = testJobServer() && passed;
#ifdef _WIN32
passed = testJobServerParsing() && passed;
passed = testDeferredImplicitTokenOrdering() && passed;
passed = testTimerRestartFromCallback() && passed;
passed = testBoundedTokenDrain() && passed;
passed = testPendingRequestTeardown() && passed;
#endif
return passed ? 0 : -1;
}