// Copyright 2019 The Bazel Authors. 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.

// daemonize [-a] -l log_path -p pid_path [-c cgroup] [-s systemd_wrapper_path]
// -- binary_path binary_name [args]
//
// daemonize spawns a program as a daemon, redirecting all of its output to the
// given log_path and writing the daemon's PID to pid_path.  binary_path
// specifies the full location of the program to execute and binary_name
// indicates its display name (aka argv[0], so the optional args do not have to
// specify it again).  log_path is created/truncated unless the -a (append) flag
// is specified.  Also note that pid_path is guaranteed to exists when this
// program terminates successfully.
//
// Some important details about the implementation of this program:
//
// * No threads to ensure the use of fork below does not cause trouble.
//
// * Pure C, no C++. This is intentional to keep the program low overhead
//   and to avoid the accidental introduction of heavy dependencies that
//   could spawn threads.
//
// * Error handling is extensive but there is no error propagation.  Given
//   that the goal of this program is just to spawn another one as a daemon,
//   we take the freedom to immediatey exit from anywhere as soon as we
//   hit an error.

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include "src/main/tools/process-tools.h"

// Configures std{in,out,err} of the current process to serve as a daemon.
//
// stdin is configured to read from /dev/null.
//
// stdout and stderr are configured to write to log_path, which is created and
// truncated unless log_append is set to true, in which case it is open for
// append if it exists.
static void SetupStdio(const char* log_path, bool log_append) {
  close(STDIN_FILENO);
  int fd = open("/dev/null", O_RDONLY);
  if (fd == -1) {
    err(EXIT_FAILURE, "Failed to open /dev/null");
  }
  assert(fd == STDIN_FILENO);

  close(STDOUT_FILENO);
  int flags = O_WRONLY | O_CREAT | (log_append ? O_APPEND : O_TRUNC);
  fd = open(log_path, flags, 0666);
  if (fd == -1) {
    err(EXIT_FAILURE, "Failed to create log file %s", log_path);
  }
  assert(fd == STDOUT_FILENO);

  close(STDERR_FILENO);
  fd = dup(STDOUT_FILENO);
  if (fd == -1) {
    err(EXIT_FAILURE, "dup failed");
  }
  assert(fd == STDERR_FILENO);
}

// Writes the given pid to a new file at pid_path.
//
// Once the pid file has been created, this notifies pid_done_fd by writing a
// dummy character to it and closing it.
static void WritePidFile(pid_t pid, const char* pid_path, int pid_done_fd) {
  FILE* pid_file = fopen(pid_path, "w");
  if (pid_file == NULL) {
    err(EXIT_FAILURE, "Failed to create %s", pid_path);
  }
  if (fprintf(pid_file, "%d", pid) < 0) {
    err(EXIT_FAILURE, "Failed to write pid %d to %s", pid, pid_path);
  }
  if (fclose(pid_file) < 0) {
    err(EXIT_FAILURE, "Failed to write pid %d to %s", pid, pid_path);
  }

  char dummy = '\0';
  int ret = 0;
  while (ret == 0) {
    ret = write(pid_done_fd, &dummy, sizeof(dummy));
    if (ret == -1 && errno == EINTR) {
      ret = 0;
    }
  }
  if (ret != 1) {
    err(EXIT_FAILURE, "Failed to signal pid done");
  }
  close(pid_done_fd);
}

#ifdef __linux__
static bool ShellEscapeNeeded(const char* arg) {
  static const char kDontNeedShellEscapeChars[] =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      "abcdefghijklmnopqrstuvwxyz"
      "0123456789+-_.=/:,@";

  for (int i = 0; arg[i]; i++) {
    if (strchr(kDontNeedShellEscapeChars, arg[i]) == NULL) {
      // If any character is not in the list, we need to escape the string.
      return true;
    }
  }
  return false;
}

// Similar to what absl::ShellEscape does. We'd like to escape the arguments
// passed to the shell script, but we cannot use absl::ShellEscape because we
// want to keep this tool pure C.
static char* ShellEscape(const char* arg) {
  if (!arg) {
    return NULL;
  }

  if (!ShellEscapeNeeded(arg)) {
    return strdup(arg);
  }

  bool has_single_quotes = false;
  for (size_t i = 0; arg[i]; i++) {
    if (arg[i] == '\'') {
      has_single_quotes = true;
      break;
    }
  }

  if (!has_single_quotes) {
    // When there are no single quotes, we can just escape the string with
    // single quotes.
    char* escaped_string;
    asprintf(&escaped_string, "'%s'", arg);
    return escaped_string;
  }

  // For all other cases, we wrap everything in double quotes.
  size_t escaped_len = 0;

  for (size_t i = 0; arg[i]; i++) {
    switch (arg[i]) {
      case '\\':
      case '$':
      case '"':
      case '`':
        escaped_len++;
    }
    escaped_len++;
  }

  char* escaped_string = (char*)malloc(
      escaped_len +
      3);  // +2 for double quotes, and +1 for the null terminator.

  size_t j = 0;
  escaped_string[j++] = '"';
  for (size_t i = 0; arg[i]; i++) {
    switch (arg[i]) {
      case '\\':
      case '$':
      case '"':
      case '`':
        escaped_string[j++] = '\\';
    }
    escaped_string[j++] = arg[i];
  }
  escaped_string[j++] = '"';
  escaped_string[j] = '\0';

  return escaped_string;
}

// Prepares the shell script for systemd-run to execute.
//
// We wrap everything inside a shell script file for systemd-run in order to
// create a transient cgroup for the Java server to use. With a user cgroup
// created like this, we can control the resources without root permission.
//
// We cannot do this directly with a command because we'll get a "Filename too
// long" error, which indicates that the command is too long as we have almost
// 100 arguments in the list.
static void WriteSystemdWrapper(const char* systemd_wrapper_path,
                                const char* exe, char** argv) {
  FILE* systemd_wrapper_fp = fopen(systemd_wrapper_path, "w");
  if (systemd_wrapper_fp == NULL) {
    err(EXIT_FAILURE, "Failed to create %s", systemd_wrapper_path);
  }

  char* escaped_argv0 = ShellEscape(argv[0]);
  if (fprintf(systemd_wrapper_fp, "#!/bin/bash\nexec -a %s %s", escaped_argv0,
              exe) < 0) {
    err(EXIT_FAILURE, "Failed to write content to %s", systemd_wrapper_path);
  }
  free(escaped_argv0);

  int argc = 1;
  while (argv[argc]) {
    char* escaped_arg = ShellEscape(argv[argc]);
    if (fprintf(systemd_wrapper_fp, " %s", escaped_arg) < 0) {
      err(EXIT_FAILURE, "Failed to write content to %s", systemd_wrapper_path);
    }
    free(escaped_arg);
    argc++;
  }

  if (fclose(systemd_wrapper_fp) < 0) {
    err(EXIT_FAILURE, "Failed to fclose file %s", systemd_wrapper_path);
  }
}

static bool IsBinaryExecutable(const char* binary_path) {
  return access(binary_path, X_OK) == 0;
}
#endif

static void ExecAsDaemon(const char* log_path, bool log_append,
                         const char* systemd_wrapper_path, int pid_done_fd,
                         const char* exe, char** argv)
    __attribute__((noreturn));

// Executes the requested binary configuring it to behave as a daemon.
//
// The stdout and stderr of the current process are redirected to the given
// log_path.  See SetupStdio for details on how this is handled.
//
// This blocks execution until pid_done_fd receives a write.  We do this
// because the Bazel server process (which is what we start with this helper
// binary) requires the PID file to be present at startup time so we must
// wait until the parent process has created it.
//
// This function never returns.
static void ExecAsDaemon(const char* log_path, bool log_append,
                         const char* systemd_wrapper_path, int pid_done_fd,
                         const char* exe, char** argv) {
  char dummy;
  if (read(pid_done_fd, &dummy, sizeof(dummy)) == -1) {
    err(EXIT_FAILURE, "Failed to wait for pid file creation");
  }
  close(pid_done_fd);

  if (signal(SIGHUP, SIG_IGN) == SIG_ERR) {
    err(EXIT_FAILURE, "Failed to install SIGHUP handler");
  }

  if (setsid() == -1) {
    err(EXIT_FAILURE, "setsid failed");
  }

  SetupStdio(log_path, log_append);

#ifdef __linux__
  // When it's running on linux, and the systemd wrapper path is provided, we
  // try to replace the current process with systemd-run. In all other cases,
  // including the cases when systemd-run is not available, we replace it with
  // the original exe.
  const char* systemd_run_path = "/usr/bin/systemd-run";
  if (systemd_wrapper_path != NULL && IsBinaryExecutable(systemd_run_path)) {
    // Even if systemd-run is present and executable, we still need to run a
    // command first to check if we can use it. There are some cases when the
    // environment is not set up correctly, e.g. no DBUS available.
    char* systemd_test_command;
    asprintf(&systemd_test_command, "%s --user --scope -- /bin/true",
             systemd_run_path);
    int status = system(systemd_test_command);
    free(systemd_test_command);

    if (status == 0) {
      WriteSystemdWrapper(systemd_wrapper_path, exe, argv);

      execl(systemd_run_path, systemd_run_path, "--user", "--scope", "--",
            "/bin/bash", systemd_wrapper_path, NULL);
      err(EXIT_FAILURE, "Failed to execute %s with systemd-run.", exe);
    }
  }

#endif

  execv(exe, argv);
  err(EXIT_FAILURE, "Failed to execute %s", exe);
}

#ifdef __linux__
// Moves the bazel server into the specified cgroup for all the discovered
// cgroups. This is useful when using the cgroup features in bazel and thus the
// server must be started in a user-writable cgroup. Users can specify a
// pre-setup cgroup where the server will be moved to. This is enabled by
// the --experimental_cgroup_parent startup flag.
static void MoveToCgroup(pid_t pid, const char* cgroup_path) {
  FILE* mounts_fp = fopen("/proc/self/mounts", "r");
  if (mounts_fp == NULL) {
    err(EXIT_FAILURE, "Failed to open /proc/self/mounts");
  }

  char* line = NULL;
  size_t len = 0;
  while (getline(&line, &len, mounts_fp) != -1) {
    char* saveptr;
    strtok_r(line, " ", &saveptr);
    char* fs_file = strtok_r(NULL, " ", &saveptr);
    char* fs_vfstype = strtok_r(NULL, " ", &saveptr);
    if (strcmp(fs_vfstype, "cgroup") == 0 ||
        strcmp(fs_vfstype, "cgroup2") == 0) {
      char* procs_path;
      asprintf(&procs_path, "%s%s/cgroup.procs", fs_file, cgroup_path);
      WriteFile(procs_path, "%d", pid);
      free(procs_path);
    }
  }
  free(line);
  fclose(mounts_fp);
}
#endif

// Starts the given process as a daemon.
//
// This spawns a subprocess that will be configured to run the desired program
// as a daemon.  The program to run is supplied in exe and the arguments to it
// are given in the NULL-terminated argv.  argv[0] must be present and
// contain the program name (which may or may not match the basename of exe).
static void Daemonize(const char* log_path, bool log_append,
                      const char* pid_path, const char* cgroup_path,
                      const char* systemd_wrapper_path, const char* exe,
                      char** argv) {
  assert(argv[0] != NULL);

  int pid_done_fds[2];
  if (pipe(pid_done_fds) == -1) {
    err(EXIT_FAILURE, "pipe failed");
  }

  pid_t pid = fork();
  if (pid == -1) {
    err(EXIT_FAILURE, "fork failed");
  } else if (pid == 0) {
    close(pid_done_fds[1]);
#ifdef __linux__
    if (cgroup_path != NULL) {
      MoveToCgroup(pid, cgroup_path);
    }
#endif
    ExecAsDaemon(log_path, log_append, systemd_wrapper_path, pid_done_fds[0],
                 exe, argv);
    abort();  // NOLINT Unreachable.
  }
  close(pid_done_fds[0]);

  WritePidFile(pid, pid_path, pid_done_fds[1]);
}

// Program entry point.
//
// The primary responsibility of this function is to parse program options.
// Once that is done, delegates all work to Daemonize.
int main(int argc, char** argv) {
  bool log_append = false;
  const char* log_path = NULL;
  const char* pid_path = NULL;
  const char* cgroup_path = NULL;
  const char* systemd_wrapper_path = NULL;
  int opt;
  while ((opt = getopt(argc, argv, ":al:p:c:s:")) != -1) {
    switch (opt) {
      case 'a':
        log_append = true;
        break;

      case 'l':
        log_path = optarg;
        break;

      case 'p':
        pid_path = optarg;
        break;

      case 'c':
        cgroup_path = optarg;
        break;

      case 's':
        systemd_wrapper_path = optarg;
        break;

      case ':':
        errx(EXIT_FAILURE, "Option -%c requires an argument", optopt);

      case '?':
      default:
        errx(EXIT_FAILURE, "Unknown option -%c", optopt);
    }
  }
  argc -= optind;
  argv += optind;

  if (log_path == NULL) {
    errx(EXIT_FAILURE, "Must specify a log file with -l");
  }
  if (pid_path == NULL) {
    errx(EXIT_FAILURE, "Must specify a pid file with -p");
  }

  if (argc < 2) {
    errx(EXIT_FAILURE, "Must provide at least an executable name and arg0");
  }
  Daemonize(log_path, log_append, pid_path, cgroup_path, systemd_wrapper_path,
            argv[0], argv + 1);
  return EXIT_SUCCESS;
}
