// Copyright (C) 2015 The Android Open Source Project
//
// 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.

// makeparallel communicates with the GNU make jobserver
// (http://make.mad-scientist.net/papers/jobserver-implementation/)
// in order claim all available jobs, and then passes the number of jobs
// claimed to a subprocess with -j<jobs>.

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <string>
#include <vector>

#ifdef __linux__
#include <error.h>
#endif

#ifdef __APPLE__
#include <err.h>
#define error(code, eval, fmt, ...) errc(eval, code, fmt, ##__VA_ARGS__)
// Darwin does not interrupt syscalls by default.
#define TEMP_FAILURE_RETRY(exp) (exp)
#endif

// Throw an error if fd is not valid.
static void CheckFd(int fd) {
  int ret = fcntl(fd, F_GETFD);
  if (ret < 0) {
    if (errno == EBADF) {
      error(errno, 0, "no jobserver pipe, prefix recipe command with '+'");
    } else {
      error(errno, errno, "fnctl failed");
    }
  }
}

// Extract flags from MAKEFLAGS that need to be propagated to subproccess
static std::vector<std::string> ReadMakeflags() {
  std::vector<std::string> args;

  const char* makeflags_env = getenv("MAKEFLAGS");
  if (makeflags_env == nullptr) {
    return args;
  }

  // The MAKEFLAGS format is pretty useless.  The first argument might be empty
  // (starts with a leading space), or it might be a set of one-character flags
  // merged together with no leading space, or it might be a variable
  // definition.

  std::string makeflags = makeflags_env;

  // Split makeflags into individual args on spaces.  Multiple spaces are
  // elided, but an initial space will result in a blank arg.
  size_t base = 0;
  size_t found;
  do {
    found = makeflags.find_first_of(" ", base);
    args.push_back(makeflags.substr(base, found - base));
    base = found + 1;
  } while (found != makeflags.npos);

  // Drop the first argument if it is empty
  while (args.size() > 0 && args[0].size() == 0) {
	  args.erase(args.begin());
  }

  // Prepend a - to the first argument if it does not have one and is not a
  // variable definition
  if (args.size() > 0 && args[0][0] != '-') {
    if (args[0].find('=') == makeflags.npos) {
      args[0] = '-' + args[0];
    }
  }

  return args;
}

static bool ParseMakeflags(std::vector<std::string>& args,
    int* in_fd, int* out_fd, bool* parallel, bool* keep_going) {

  std::vector<char*> getopt_argv;
  // getopt starts reading at argv[1]
  getopt_argv.reserve(args.size() + 1);
  getopt_argv.push_back(strdup(""));
  for (std::string& v : args) {
    getopt_argv.push_back(strdup(v.c_str()));
  }

  opterr = 0;
  optind = 1;
  while (1) {
    const static option longopts[] = {
        {"jobserver-fds", required_argument, 0, 0},
        {0, 0, 0, 0},
    };
    int longopt_index = 0;

    int c = getopt_long(getopt_argv.size(), getopt_argv.data(), "kj",
        longopts, &longopt_index);

    if (c == -1) {
      break;
    }

    switch (c) {
    case 0:
      switch (longopt_index) {
      case 0:
      {
        // jobserver-fds
        if (sscanf(optarg, "%d,%d", in_fd, out_fd) != 2) {
          error(EXIT_FAILURE, 0, "incorrect format for --jobserver-fds: %s", optarg);
        }
        // TODO: propagate in_fd, out_fd
        break;
      }
      default:
        abort();
      }
      break;
    case 'j':
      *parallel = true;
      break;
    case 'k':
      *keep_going = true;
      break;
    case '?':
      // ignore unknown arguments
      break;
    default:
      abort();
    }
  }

  for (char *v : getopt_argv) {
    free(v);
  }

  return true;
}

// Read a single byte from fd, with timeout in milliseconds.  Returns true if
// a byte was read, false on timeout.  Throws away the read value.
// Non-reentrant, uses timer and signal handler global state, plus static
// variable to communicate with signal handler.
//
// Uses a SIGALRM timer to fire a signal after timeout_ms that will interrupt
// the read syscall if it hasn't yet completed.  If the timer fires before the
// read the read could block forever, so read from a dup'd fd and close it from
// the signal handler, which will cause the read to return EBADF if it occurs
// after the signal.
// The dup/read/close combo is very similar to the system described to avoid
// a deadlock between SIGCHLD and read at
// http://make.mad-scientist.net/papers/jobserver-implementation/
static bool ReadByteTimeout(int fd, int timeout_ms) {
  // global variable to communicate with the signal handler
  static int dup_fd = -1;

  // dup the fd so the signal handler can close it without losing the real one
  dup_fd = dup(fd);
  if (dup_fd < 0) {
    error(errno, errno, "dup failed");
  }

  // set up a signal handler that closes dup_fd on SIGALRM
  struct sigaction action = {};
  action.sa_flags = SA_SIGINFO,
  action.sa_sigaction = [](int, siginfo_t*, void*) {
    close(dup_fd);
  };
  struct sigaction oldaction = {};
  int ret = sigaction(SIGALRM, &action, &oldaction);
  if (ret < 0) {
    error(errno, errno, "sigaction failed");
  }

  // queue a SIGALRM after timeout_ms
  const struct itimerval timeout = {{}, {0, timeout_ms * 1000}};
  ret = setitimer(ITIMER_REAL, &timeout, NULL);
  if (ret < 0) {
    error(errno, errno, "setitimer failed");
  }

  // start the blocking read
  char buf;
  int read_ret = read(dup_fd, &buf, 1);
  int read_errno = errno;

  // cancel the alarm in case it hasn't fired yet
  const struct itimerval cancel = {};
  ret = setitimer(ITIMER_REAL, &cancel, NULL);
  if (ret < 0) {
    error(errno, errno, "reset setitimer failed");
  }

  // remove the signal handler
  ret = sigaction(SIGALRM, &oldaction, NULL);
  if (ret < 0) {
    error(errno, errno, "reset sigaction failed");
  }

  // clean up the dup'd fd in case the signal never fired
  close(dup_fd);
  dup_fd = -1;

  if (read_ret == 0) {
    error(EXIT_FAILURE, 0, "EOF on jobserver pipe");
  } else if (read_ret > 0) {
    return true;
  } else if (read_errno == EINTR || read_errno == EBADF) {
    return false;
  } else {
    error(read_errno, read_errno, "read failed");
  }
  abort();
}

// Measure the size of the jobserver pool by reading from in_fd until it blocks
static int GetJobserverTokens(int in_fd) {
  int tokens = 0;
  pollfd pollfds[] = {{in_fd, POLLIN, 0}};
  int ret;
  while ((ret = TEMP_FAILURE_RETRY(poll(pollfds, 1, 0))) != 0) {
    if (ret < 0) {
      error(errno, errno, "poll failed");
    } else if (pollfds[0].revents != POLLIN) {
      error(EXIT_FAILURE, 0, "unexpected event %d\n", pollfds[0].revents);
    }

    // There is probably a job token in the jobserver pipe.  There is a chance
    // another process reads it first, which would cause a blocking read to
    // block forever (or until another process put a token back in the pipe).
    // The file descriptor can't be set to O_NONBLOCK as that would affect
    // all users of the pipe, including the parent make process.
    // ReadByteTimeout emulates a non-blocking read on a !O_NONBLOCK socket
    // using a SIGALRM that fires after a short timeout.
    bool got_token = ReadByteTimeout(in_fd, 10);
    if (!got_token) {
      // No more tokens
      break;
    } else {
      tokens++;
    }
  }

  // This process implicitly gets a token, so pool size is measured size + 1
  return tokens;
}

// Return tokens to the jobserver pool.
static void PutJobserverTokens(int out_fd, int tokens) {
  // Return all the tokens to the pipe
  char buf = '+';
  for (int i = 0; i < tokens; i++) {
    int ret = TEMP_FAILURE_RETRY(write(out_fd, &buf, 1));
    if (ret < 0) {
      error(errno, errno, "write failed");
    } else if (ret == 0) {
      error(EXIT_FAILURE, 0, "EOF on jobserver pipe");
    }
  }
}

int main(int argc, char* argv[]) {
  int in_fd = -1;
  int out_fd = -1;
  bool parallel = false;
  bool keep_going = false;
  bool ninja = false;
  int tokens = 0;

  if (argc > 1 && strcmp(argv[1], "--ninja") == 0) {
    ninja = true;
    argv++;
    argc--;
  }

  if (argc < 2) {
    error(EXIT_FAILURE, 0, "expected command to run");
  }

  const char* path = argv[1];
  std::vector<char*> args({argv[1]});

  std::vector<std::string> makeflags = ReadMakeflags();
  if (ParseMakeflags(makeflags, &in_fd, &out_fd, &parallel, &keep_going)) {
    if (in_fd >= 0 && out_fd >= 0) {
      CheckFd(in_fd);
      CheckFd(out_fd);
      fcntl(in_fd, F_SETFD, FD_CLOEXEC);
      fcntl(out_fd, F_SETFD, FD_CLOEXEC);
      tokens = GetJobserverTokens(in_fd);
    }
  }

  std::string jarg = "-j" + std::to_string(tokens + 1);

  if (ninja) {
    if (!parallel) {
      // ninja is parallel by default, pass -j1 to disable parallelism if make wasn't parallel
      args.push_back(strdup("-j1"));
    } else if (tokens > 0) {
      args.push_back(strdup(jarg.c_str()));
    }
    if (keep_going) {
      args.push_back(strdup("-k0"));
    }
  } else {
    args.push_back(strdup(jarg.c_str()));
  }

  args.insert(args.end(), &argv[2], &argv[argc]);

  args.push_back(nullptr);

  pid_t pid = fork();
  if (pid < 0) {
    error(errno, errno, "fork failed");
  } else if (pid == 0) {
    // child
    unsetenv("MAKEFLAGS");
    unsetenv("MAKELEVEL");
    int ret = execvp(path, args.data());
    if (ret < 0) {
      error(errno, errno, "exec %s failed", path);
    }
    abort();
  }

  // parent
  siginfo_t status = {};
  int exit_status = 0;
  int ret = waitid(P_PID, pid, &status, WEXITED);
  if (ret < 0) {
    error(errno, errno, "waitpid failed");
  } else if (status.si_code == CLD_EXITED) {
    exit_status = status.si_status;
  } else {
    exit_status = -(status.si_status);
  }

  if (tokens > 0) {
    PutJobserverTokens(out_fd, tokens);
  }
  exit(exit_status);
}
