void llbuild::basic::spawnProcess()

in lib/Basic/Subprocess.cpp [642:1108]


void llbuild::basic::spawnProcess(
    ProcessDelegate& delegate,
    ProcessContext* ctx,
    ProcessGroup& pgrp,
    ProcessHandle handle,
    ArrayRef<StringRef> commandLine,
    POSIXEnvironment environment,
    ProcessAttributes attr,
    ProcessReleaseFn&& releaseFn,
    ProcessCompletionFn&& completionFn
) {
  llbuild_pid_t pid = (llbuild_pid_t)-1;
  
#if !defined(_WIN32) && !defined(HAVE_POSIX_SPAWN)
  auto result = ProcessResult::makeFailed();
  delegate.processStarted(ctx, handle, pid);
  delegate.processHadError(ctx, handle, Twine("process spawning is unavailable"));
  delegate.processFinished(ctx, handle, result);
  completionFn(result);
  return;
#else
  // Don't use lane release feature for console workloads.
  if (attr.connectToConsole) {
    attr.controlEnabled = false;
  }

#if defined(_WIN32)
  // Control channel support is broken (thread-unsafe) on Windows.
  attr.controlEnabled = false;
#endif

  if (commandLine.size() == 0) {
    auto result = ProcessResult::makeFailed();
    delegate.processStarted(ctx, handle, pid);
    delegate.processHadError(ctx, handle, Twine("no arguments for command"));
    delegate.processFinished(ctx, handle, result);
    completionFn(result);
    return;
  }

  // Form the complete C string command line.
  std::vector<std::string> argsStorage(commandLine.begin(), commandLine.end());
#if defined(_WIN32)
  std::string args = llbuild::basic::formatWindowsCommandString(argsStorage);

  // Convert the command line string to utf16
  llvm::SmallVector<llvm::UTF16, 20> u16Executable;
  llvm::SmallVector<llvm::UTF16, 20> u16CmdLine;
  llvm::convertUTF8ToUTF16String(argsStorage[0], u16Executable);
  llvm::convertUTF8ToUTF16String(args, u16CmdLine);
#else
  std::vector<const char*> args(argsStorage.size() + 1);
  for (size_t i = 0; i != argsStorage.size(); ++i) {
    args[i] = argsStorage[i].c_str();
  }
  args[argsStorage.size()] = nullptr;
#endif

#if defined(_WIN32)
  DWORD creationFlags = NORMAL_PRIORITY_CLASS |
                        CREATE_UNICODE_ENVIRONMENT;
  PROCESS_INFORMATION processInfo = {0};

#else
  // Initialize the spawn attributes.
  posix_spawnattr_t attributes;
  posix_spawnattr_init(&attributes);

  // Unmask all signals.
  sigset_t noSignals;
  sigemptyset(&noSignals);
  posix_spawnattr_setsigmask(&attributes, &noSignals);

  // Reset all signals to default behavior.
  //
  // On Linux, this can only be used to reset signals that are legal to
  // modify, so we have to take care about the set we use.
#if defined(__linux__)
  sigset_t mostSignals;
  sigemptyset(&mostSignals);
  for (int i = 1; i < SIGSYS; ++i) {
    if (i == SIGKILL || i == SIGSTOP) continue;
    sigaddset(&mostSignals, i);
  }
  posix_spawnattr_setsigdefault(&attributes, &mostSignals);
#else
  sigset_t mostSignals;
  sigfillset(&mostSignals);
  sigdelset(&mostSignals, SIGKILL);
  sigdelset(&mostSignals, SIGSTOP);
  posix_spawnattr_setsigdefault(&attributes, &mostSignals);
#endif // else !defined(_WIN32)

  // Establish a separate process group.
  posix_spawnattr_setpgroup(&attributes, 0);

  // Set the attribute flags.
  unsigned flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF;
  if (!attr.connectToConsole) {
    flags |= POSIX_SPAWN_SETPGROUP;
  }

  // Close all other files by default.
  //
  // FIXME: Note that this is an Apple-specific extension, and we will have to
  // do something else on other platforms (and unfortunately, there isn't
  // really an easy answer other than using a stub executable).
#ifdef __APPLE__
  flags |= POSIX_SPAWN_CLOEXEC_DEFAULT;
#endif

  // On Darwin, set the QoS of launched processes to one of the current thread.
#ifdef __APPLE__
  posix_spawnattr_set_qos_class_np(&attributes, qos_class_self());
#endif

  posix_spawnattr_setflags(&attributes, flags);

  // Setup the file actions.
  posix_spawn_file_actions_t fileActions;
  posix_spawn_file_actions_init(&fileActions);

  bool usePosixSpawnChdirFallback = true;
  const auto workingDir = attr.workingDir.str();
  if (!workingDir.empty() &&
      posix_spawn_file_actions_addchdir(&fileActions, workingDir.c_str()) != ENOSYS) {
    usePosixSpawnChdirFallback = false;
  }

#endif

#if defined(_WIN32)
  /// Process startup information for Windows.
  STARTUPINFOW startupInfo = {0};
  PlatformSpecificPipesConfig& pipesConfig = startupInfo;
#else
  PlatformSpecificPipesConfig& pipesConfig = fileActions;
#endif

  // Automatically managed (released) descriptors for output and control pipes.
  // The child ends are forwarded to the child (and quickly released in the
  // parent). The parent ends are retained and read/written by the parent.
  ManagedDescriptor outputPipeParentEnd{__FILE__, __LINE__};
  ManagedDescriptor controlPipeParentEnd{__FILE__, __LINE__};

#if defined(_WIN32)
  llvm::SmallVector<llvm::UTF16, 20> u16Cwd;
  std::string workingDir = attr.workingDir.str();
  if (!workingDir.empty()) {
    llvm::convertUTF8ToUTF16String(workingDir, u16Cwd);
  }
#endif

  // Export a task ID to subprocesses.
  auto taskID = Twine::utohexstr(handle.id);
  environment.setIfMissing("LLBUILD_TASK_ID", taskID.str());

  // Resolve the executable path, if necessary.
  //
  // FIXME: This should be cached.
  if (!llvm::sys::path::is_absolute(argsStorage[0])) {
    auto res = llvm::sys::findProgramByName(argsStorage[0]);
    if (!res.getError()) {
      argsStorage[0] = *res;
#if defined(_WIN32)
      u16Executable.clear();
      llvm::convertUTF8ToUTF16String(argsStorage[0], u16Executable);
#else
      args[0] = argsStorage[0].c_str();
#endif
    }
  }

  // Spawn the command.
  bool wasCancelled;
  do {
      // We need to hold the spawn processes lock when we spawn, to ensure that
      // we don't create a process in between when we are cancelled.
      std::lock_guard<std::mutex> guard(pgrp.mutex);
      wasCancelled = pgrp.isClosed();

      // If we have been cancelled since we started, skip startup.
      if (wasCancelled) { break; }

      // The partf of the control pipes that are inherited by the child.
      ManagedDescriptor outputPipeChildEnd{__FILE__, __LINE__};
      ManagedDescriptor controlPipeChildEnd{__FILE__, __LINE__};

      // Open the communication channel under the mutex to avoid
      // leaking the wrong channel into other children started concurrently.
      auto errorPair = createCommunicationPipes(attr, pipesConfig, outputPipeParentEnd, outputPipeChildEnd, controlPipeParentEnd, controlPipeChildEnd);
      if (errorPair.first != CommunicationPipesCreationError::ERROR_NONE) {
        std::string whatPipe = errorPair.first == CommunicationPipesCreationError::OUTPUT_PIPE_FAILED ? "output pipe" : "control pipe";
#if !defined(_WIN32)
        posix_spawn_file_actions_destroy(&fileActions);
        posix_spawnattr_destroy(&attributes);
#endif
        delegate.processStarted(ctx, handle, pid);
        delegate.processHadError(ctx, handle,
            Twine("unable to open " + whatPipe + " (") + strerror(errorPair.second) + ")");
        delegate.processFinished(ctx, handle, ProcessResult::makeFailed());
        completionFn(ProcessResult(ProcessStatus::Failed));
        return;
      }

      if (controlPipeChildEnd.isValid()) {
        long long controlFd = (long long)controlPipeChildEnd.unsafeDescriptor();
        environment.setIfMissing("LLBUILD_CONTROL_FD", Twine(controlFd).str());
      }

      int result = 0;

      bool workingDirectoryUnsupported = false;

#if !defined(_WIN32)
      if (usePosixSpawnChdirFallback) {
#if defined(__APPLE__)
        thread_local std::string threadWorkingDir;

        if (workingDir.empty()) {
          if (!threadWorkingDir.empty()) {
            pthread_fchdir_np(-1);
            threadWorkingDir.clear();
          }
        } else {
          if (threadWorkingDir != workingDir) {
            if (pthread_chdir_np(workingDir.c_str()) == -1) {
              result = errno;
            } else {
              threadWorkingDir = workingDir;
            }
          }
        }
#else
        if (!workingDir.empty()) {
          workingDirectoryUnsupported = true;
          result = -1;
        }
#endif // if defined(__APPLE__)
      }
#endif // else !defined(_WIN32)

      if (result == 0) {
#if defined(_WIN32)
        auto unicodeEnv = environment.getWindowsEnvp();
        result = !CreateProcessW(
            /*lpApplicationName=*/(LPWSTR)u16Executable.data(),
            (LPWSTR)u16CmdLine.data(),
            /*lpProcessAttributes=*/NULL,
            /*lpThreadAttributes=*/NULL,
            /*bInheritHandles=*/TRUE, creationFlags,
            /*lpEnvironment=*/unicodeEnv.get(),
            /*lpCurrentDirectory=*/u16Cwd.empty() ? NULL
                                                  : (LPWSTR)u16Cwd.data(),
            &startupInfo, &processInfo);
#else
        result =
            posix_spawn(&pid, args[0], /*file_actions=*/&fileActions,
                        /*attrp=*/&attributes, const_cast<char**>(args.data()),
                        const_cast<char* const*>(environment.getEnvp()));
#endif
      }
    
      delegate.processStarted(ctx, handle, pid);

      if (result != 0) {
        auto processResult = ProcessResult::makeFailed();
#if defined(_WIN32)
        result = GetLastError();
#endif
        delegate.processHadError(
            ctx, handle,
            workingDirectoryUnsupported
                ? Twine("working-directory unsupported on this platform")
                : Twine("unable to spawn process '") + argsStorage[0] + "' (" + sys::strerror(result) +
                      ")");
        delegate.processFinished(ctx, handle, processResult);
        pid = (llbuild_pid_t)-1;
      } else {
#if defined(_WIN32)
        pid = processInfo.hProcess;
#endif
        ProcessInfo info{ attr.canSafelyInterrupt };
        pgrp.add(std::move(guard), pid, info);
      }

    // Close the child ends of the forwarded output and control pipes.
    controlPipeChildEnd.close();
    outputPipeChildEnd.close();
  } while(false);

#if !defined(_WIN32)
  posix_spawn_file_actions_destroy(&fileActions);
  posix_spawnattr_destroy(&attributes);
#endif

  // If we failed to launch a process, clean up and abort.
  if (pid == (llbuild_pid_t)-1) {
    // Manually close to avoid triggering debug-time leak check.
    outputPipeParentEnd.close();
    controlPipeParentEnd.close();
    auto result = wasCancelled ? ProcessResult::makeCancelled() : ProcessResult::makeFailed();
    completionFn(result);
    return;
  }

#if !defined(_WIN32)
  // Set up our poll() structures. We use assert() to ensure
  // the file descriptors are alive.
  pollfd readfds[] = {
    { outputPipeParentEnd.unsafeDescriptor(), 0, 0 },
    { controlPipeParentEnd.unsafeDescriptor(), 0, 0 }
  };
  bool activeEvents = false;
#endif
  const int nfds = 2;
  ControlProtocolState control(taskID.str());
  std::function<bool (StringRef)> readCbs[] = {
    // output capture callback
    [&delegate, ctx, handle](StringRef buf) -> bool {
      // Notify the client of the output.
      delegate.processHadOutput(ctx, handle, buf);
      return true;
    },
    // control callback handle
    [&delegate, &control, ctx, handle](StringRef buf) mutable -> bool {
      std::string errstr;
      int ret = control.read(buf, &errstr);
      if (ret < 0) {
        delegate.processHadError(ctx, handle,
                                 Twine("control protocol error" + errstr));
      }
      return (ret == 0);
    }
  };
#if defined(_WIN32)
  struct threadData {
    std::function<void(void*)> reader;
    const ManagedDescriptor& handle;
    std::function<bool(StringRef)> cb;
  };
  HANDLE readers[2] = {NULL, NULL};
  auto reader = [&delegate, handle, ctx](void* lpArgs) {
    threadData* args = (threadData*)lpArgs;
    for (;;) {
      char buf[4096];
      DWORD numBytes;
      bool result = ReadFile(args->handle.unsafeDescriptor(), buf, sizeof(buf), &numBytes, NULL);

      if (!result || numBytes == 0) {
        if (GetLastError() == ERROR_BROKEN_PIPE) {
          // Pipe done, exit
          return;
        } else {
          delegate.processHadError(ctx, handle,
                                   Twine("unable to read process output (") +
                                       sys::strerror(GetLastError()) + ")");
        }
      }

      if (numBytes <= 0 || !args->cb(StringRef(buf, numBytes))) {
        continue;
      }
    }
  };

  struct threadData outputThreadParams = {reader, outputPipeParentEnd, readCbs[0]};
  struct threadData controlThreadParams = {reader, controlPipeParentEnd, readCbs[1]};
  HANDLE threads[2] = {NULL, NULL};
#endif // defined(_WIN32)

  int threadCount = 0;

  // Read the command output, if capturing instead of pass-throughing.
  if (!attr.connectToConsole) {
#if defined(_WIN32)
    threads[threadCount++] = (HANDLE)_beginthread(
        [](LPVOID lpParams) { ((threadData*)lpParams)->reader(lpParams); }, 0,
        &outputThreadParams);
#else
    readfds[0].events = POLLIN;
    activeEvents = true;
    (void)threadCount;
#endif
  }

  // Process the control channel input.
  if (attr.controlEnabled) {
#if defined(_WIN32)
    threads[threadCount++] = (HANDLE)_beginthread(
        [](LPVOID lpParams) { ((threadData*)lpParams)->reader(lpParams); }, 0,
        &controlThreadParams);
#else
    readfds[1].events = POLLIN;
    activeEvents = true;
#endif
  }

#if defined(_WIN32)
  DWORD waitResult = WaitForMultipleObjects(threadCount, threads,
                                            /*bWaitAll=*/false,
                                            /*dwMilliseconds=*/INFINITE);
  if (WAIT_FAILED == waitResult || WAIT_TIMEOUT == waitResult) {
    int err = GetLastError();
    delegate.processHadError(
        ctx, handle, Twine("failed to poll (") + sys::strerror(err) + ")");
  }
#else  // !defined(_WIN32)
  while (activeEvents) {
    char buf[4096];
    activeEvents = false;

    // Ensure we haven't autoclosed the file descriptors,
    // or move()d somewhere they could be autoclosed in.
    assert(readfds[0].fd == outputPipeParentEnd.unsafeDescriptor());
    assert(readfds[1].fd == controlPipeParentEnd.unsafeDescriptor());

    if (poll(readfds, nfds, -1) == -1) {
      int err = errno;
      delegate.processHadError(ctx, handle,
          Twine("failed to poll (") + strerror(err) + ")");
      break;
    }

    for (int i = 0; i < nfds; i++) {
      if (readfds[i].revents & (POLLIN | POLLERR | POLLHUP)) {
        ssize_t numBytes = read(readfds[i].fd, buf, sizeof(buf));
        if (numBytes < 0) {
          int err = errno;
          delegate.processHadError(ctx, handle,
              Twine("unable to read process output (") + strerror(err) + ")");
        }
        if (numBytes <= 0 || !readCbs[i](StringRef(buf, numBytes))) {
          readfds[i].events = 0;
          continue;
        }
      }
      activeEvents |= readfds[i].events != 0;
    }

    if (control.shouldRelease()) {
      std::shared_ptr<ManagedDescriptor> outputFdShared
        = std::make_shared<ManagedDescriptor>(std::move(outputPipeParentEnd));
      std::shared_ptr<ManagedDescriptor> controlFdShared
        = std::make_shared<ManagedDescriptor>(std::move(controlPipeParentEnd));
      releaseFn([&delegate, &pgrp, pid, handle, ctx,
                 outputFdShared, controlFdShared,
                 completionFn=std::move(completionFn)]() mutable {
        if (outputFdShared->isValid()) {
          captureExecutedProcessOutput(delegate, *outputFdShared, handle, ctx);
        }
        cleanUpExecutedProcess(delegate, pgrp, pid, handle, ctx,
                               std::move(completionFn), *controlFdShared);
      });
      return;
    }

  }
#endif // else !defined(_WIN32)
  // If we have reached here, both the control and read pipes have given us
  // the requisite EOF/hang-up events. Safe to close the read end of the
  // output pipe.
  outputPipeParentEnd.close();
  cleanUpExecutedProcess(delegate, pgrp, pid, handle, ctx,
                         std::move(completionFn), controlPipeParentEnd);
#endif
}