| 1 | //===- llvm/Support/Jobserver.cpp - Jobserver Client Implementation -------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "llvm/Support/Jobserver.h" |
| 10 | #include "llvm/ADT/StringExtras.h" |
| 11 | #include "llvm/Support/Error.h" |
| 12 | |
| 13 | #include <atomic> |
| 14 | #include <memory> |
| 15 | #include <mutex> |
| 16 | #include <new> |
| 17 | |
| 18 | #define DEBUG_TYPE "jobserver" |
| 19 | |
| 20 | using namespace llvm; |
| 21 | |
| 22 | namespace { |
| 23 | struct FdPair { |
| 24 | int Read = -1; |
| 25 | int Write = -1; |
| 26 | bool isValid() const { return Read >= 0 && Write >= 0; } |
| 27 | }; |
| 28 | |
| 29 | struct JobserverConfig { |
| 30 | enum Mode { |
| 31 | None, |
| 32 | PosixFifo, |
| 33 | PosixPipe, |
| 34 | Win32Semaphore, |
| 35 | }; |
| 36 | Mode TheMode = None; |
| 37 | std::string Path; |
| 38 | FdPair PipeFDs; |
| 39 | }; |
| 40 | |
| 41 | /// A helper function that checks if `Input` starts with `Prefix`. |
| 42 | /// If it does, it removes the prefix from `Input`, assigns the remainder to |
| 43 | /// `Value`, and returns true. Otherwise, it returns false. |
| 44 | bool getPrefixedValue(StringRef Input, StringRef Prefix, StringRef &Value) { |
| 45 | if (Input.consume_front(Prefix)) { |
| 46 | Value = Input; |
| 47 | return true; |
| 48 | } |
| 49 | return false; |
| 50 | } |
| 51 | |
| 52 | /// A helper function to parse a string in the format "R,W" where R and W are |
| 53 | /// non-negative integers representing file descriptors. It populates the |
| 54 | /// `ReadFD` and `WriteFD` output parameters. Returns true on success. |
| 55 | static std::optional<FdPair> getFileDescriptorPair(StringRef Input) { |
| 56 | FdPair FDs; |
| 57 | if (Input.consumeInteger(Radix: 10, Result&: FDs.Read)) |
| 58 | return std::nullopt; |
| 59 | if (!Input.consume_front(Prefix: "," )) |
| 60 | return std::nullopt; |
| 61 | if (Input.consumeInteger(Radix: 10, Result&: FDs.Write)) |
| 62 | return std::nullopt; |
| 63 | if (!Input.empty() || !FDs.isValid()) |
| 64 | return std::nullopt; |
| 65 | return FDs; |
| 66 | } |
| 67 | |
| 68 | /// Parses the `MAKEFLAGS` environment variable string to find jobserver |
| 69 | /// arguments. It splits the string into space-separated arguments and searches |
| 70 | /// for `--jobserver-auth` or `--jobserver-fds`. Based on the value of these |
| 71 | /// arguments, it determines the jobserver mode (Pipe, FIFO, or Semaphore) and |
| 72 | /// connection details (file descriptors or path). |
| 73 | Expected<JobserverConfig> parseNativeMakeFlags(StringRef MakeFlags) { |
| 74 | JobserverConfig Config; |
| 75 | if (MakeFlags.empty()) |
| 76 | return Config; |
| 77 | |
| 78 | // Split the MAKEFLAGS string into arguments. |
| 79 | SmallVector<StringRef, 8> Args; |
| 80 | SplitString(Source: MakeFlags, OutFragments&: Args); |
| 81 | |
| 82 | // If '-n' (dry-run) is present as a legacy flag (not starting with '-'), |
| 83 | // disable the jobserver. |
| 84 | if (!Args.empty() && !Args[0].starts_with(Prefix: "-" ) && Args[0].contains(C: 'n')) |
| 85 | return Config; |
| 86 | |
| 87 | // Iterate through arguments to find jobserver flags. |
| 88 | // Note that make may pass multiple --jobserver-auth flags; the last one wins. |
| 89 | for (StringRef Arg : Args) { |
| 90 | StringRef Value; |
| 91 | if (getPrefixedValue(Input: Arg, Prefix: "--jobserver-auth=" , Value)) { |
| 92 | // Try to parse as a file descriptor pair first. |
| 93 | if (auto FDPair = getFileDescriptorPair(Input: Value)) { |
| 94 | Config.TheMode = JobserverConfig::PosixPipe; |
| 95 | Config.PipeFDs = *FDPair; |
| 96 | } else { |
| 97 | StringRef FifoPath; |
| 98 | // If not FDs, try to parse as a named pipe (fifo). |
| 99 | if (getPrefixedValue(Input: Value, Prefix: "fifo:" , Value&: FifoPath)) { |
| 100 | Config.TheMode = JobserverConfig::PosixFifo; |
| 101 | Config.Path = FifoPath.str(); |
| 102 | } else { |
| 103 | // Otherwise, assume it's a Windows semaphore. |
| 104 | Config.TheMode = JobserverConfig::Win32Semaphore; |
| 105 | Config.Path = Value.str(); |
| 106 | } |
| 107 | } |
| 108 | } else if (getPrefixedValue(Input: Arg, Prefix: "--jobserver-fds=" , Value)) { |
| 109 | // This is an alternative, older syntax for the pipe-based server. |
| 110 | if (auto FDPair = getFileDescriptorPair(Input: Value)) { |
| 111 | Config.TheMode = JobserverConfig::PosixPipe; |
| 112 | Config.PipeFDs = *FDPair; |
| 113 | } else { |
| 114 | return createStringError(EC: inconvertibleErrorCode(), |
| 115 | S: "Invalid file descriptor pair in MAKEFLAGS" ); |
| 116 | } |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | // Perform platform-specific validation. |
| 121 | #ifdef _WIN32 |
| 122 | if (Config.TheMode == JobserverConfig::PosixFifo || |
| 123 | Config.TheMode == JobserverConfig::PosixPipe) |
| 124 | return createStringError( |
| 125 | inconvertibleErrorCode(), |
| 126 | "FIFO/Pipe-based jobserver is not supported on Windows" ); |
| 127 | #else |
| 128 | if (Config.TheMode == JobserverConfig::Win32Semaphore) |
| 129 | return createStringError( |
| 130 | EC: inconvertibleErrorCode(), |
| 131 | S: "Semaphore-based jobserver is not supported on this platform" ); |
| 132 | #endif |
| 133 | return Config; |
| 134 | } |
| 135 | |
| 136 | std::once_flag GJobserverOnceFlag; |
| 137 | JobserverClient *GJobserver = nullptr; |
| 138 | |
| 139 | } // namespace |
| 140 | |
| 141 | namespace llvm { |
| 142 | class JobserverClientImpl : public JobserverClient { |
| 143 | bool IsInitialized = false; |
| 144 | std::atomic<bool> HasImplicitSlot{true}; |
| 145 | unsigned NumJobs = 0; |
| 146 | |
| 147 | public: |
| 148 | JobserverClientImpl(const JobserverConfig &Config); |
| 149 | ~JobserverClientImpl() override; |
| 150 | |
| 151 | JobSlot tryAcquire() override; |
| 152 | void release(JobSlot Slot) override; |
| 153 | unsigned getNumJobs() const override { return NumJobs; } |
| 154 | |
| 155 | bool isValid() const { return IsInitialized; } |
| 156 | |
| 157 | private: |
| 158 | #if defined(LLVM_ON_UNIX) |
| 159 | int ReadFD = -1; |
| 160 | int WriteFD = -1; |
| 161 | std::string FifoPath; |
| 162 | #elif defined(_WIN32) |
| 163 | void *Semaphore = nullptr; |
| 164 | #endif |
| 165 | }; |
| 166 | } // namespace llvm |
| 167 | |
| 168 | // Include the platform-specific parts of the class. |
| 169 | #if defined(LLVM_ON_UNIX) |
| 170 | #include "Unix/Jobserver.inc" |
| 171 | #elif defined(_WIN32) |
| 172 | #include "Windows/Jobserver.inc" |
| 173 | #else |
| 174 | // Dummy implementation for unsupported platforms. |
| 175 | JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) {} |
| 176 | JobserverClientImpl::~JobserverClientImpl() = default; |
| 177 | JobSlot JobserverClientImpl::tryAcquire() { return JobSlot(); } |
| 178 | void JobserverClientImpl::release(JobSlot Slot) {} |
| 179 | #endif |
| 180 | |
| 181 | namespace llvm { |
| 182 | JobserverClient::~JobserverClient() = default; |
| 183 | |
| 184 | uint8_t JobSlot::getExplicitValue() const { |
| 185 | assert(isExplicit() && "Cannot get value of implicit or invalid slot" ); |
| 186 | return static_cast<uint8_t>(Value); |
| 187 | } |
| 188 | |
| 189 | /// This is the main entry point for acquiring a jobserver client. It uses a |
| 190 | /// std::call_once to ensure the singleton `GJobserver` instance is created |
| 191 | /// safely in a multi-threaded environment. On first call, it reads the |
| 192 | /// `MAKEFLAGS` environment variable, parses it, and attempts to construct and |
| 193 | /// initialize a `JobserverClientImpl`. If successful, the global instance is |
| 194 | /// stored in `GJobserver`. Subsequent calls will return the existing instance. |
| 195 | JobserverClient *JobserverClient::getInstance() { |
| 196 | std::call_once(once&: GJobserverOnceFlag, f: []() { |
| 197 | LLVM_DEBUG( |
| 198 | dbgs() |
| 199 | << "JobserverClient::getInstance() called for the first time.\n" ); |
| 200 | const char *MakeFlagsEnv = getenv(name: "MAKEFLAGS" ); |
| 201 | if (!MakeFlagsEnv) { |
| 202 | errs() << "Warning: failed to create jobserver client due to MAKEFLAGS " |
| 203 | "environment variable not found\n" ; |
| 204 | return; |
| 205 | } |
| 206 | |
| 207 | LLVM_DEBUG(dbgs() << "Found MAKEFLAGS = \"" << MakeFlagsEnv << "\"\n" ); |
| 208 | |
| 209 | auto ConfigOrErr = parseNativeMakeFlags(MakeFlags: MakeFlagsEnv); |
| 210 | if (Error Err = ConfigOrErr.takeError()) { |
| 211 | errs() << "Warning: failed to create jobserver client due to invalid " |
| 212 | "MAKEFLAGS environment variable: " |
| 213 | << toString(E: std::move(Err)) << "\n" ; |
| 214 | return; |
| 215 | } |
| 216 | |
| 217 | JobserverConfig Config = *ConfigOrErr; |
| 218 | if (Config.TheMode == JobserverConfig::None) { |
| 219 | errs() << "Warning: failed to create jobserver client due to jobserver " |
| 220 | "mode missing in MAKEFLAGS environment variable\n" ; |
| 221 | return; |
| 222 | } |
| 223 | |
| 224 | if (Config.TheMode == JobserverConfig::PosixPipe) { |
| 225 | #if defined(LLVM_ON_UNIX) |
| 226 | if (!areFdsValid(ReadFD: Config.PipeFDs.Read, WriteFD: Config.PipeFDs.Write)) { |
| 227 | errs() << "Warning: failed to create jobserver client due to invalid " |
| 228 | "Pipe FDs in MAKEFLAGS environment variable\n" ; |
| 229 | return; |
| 230 | } |
| 231 | #endif |
| 232 | } |
| 233 | |
| 234 | auto Client = std::make_unique<JobserverClientImpl>(args&: Config); |
| 235 | if (Client->isValid()) { |
| 236 | LLVM_DEBUG(dbgs() << "Jobserver client created successfully!\n" ); |
| 237 | GJobserver = Client.release(); |
| 238 | } else |
| 239 | errs() << "Warning: jobserver client initialization failed.\n" ; |
| 240 | }); |
| 241 | return GJobserver; |
| 242 | } |
| 243 | |
| 244 | /// For testing purposes only. This function resets the singleton instance by |
| 245 | /// destroying the existing client and re-initializing the `std::once_flag`. |
| 246 | /// This allows tests to simulate the first-time initialization of the |
| 247 | /// jobserver client multiple times. |
| 248 | void JobserverClient::resetForTesting() { |
| 249 | delete GJobserver; |
| 250 | GJobserver = nullptr; |
| 251 | // Re-construct the std::once_flag in place to reset the singleton state. |
| 252 | new (&GJobserverOnceFlag) std::once_flag(); |
| 253 | } |
| 254 | } // namespace llvm |
| 255 | |