/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you 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.
 */

#include "traffic_crashlog.h"
#include "tscore/ink_args.h"
#include "tscore/ink_cap.h"
#include "tscore/I_Version.h"
#include "tscore/I_Layout.h"
#include "tscore/ink_syslog.h"
#include "records/I_RecProcess.h"
#include "RecordsConfig.h"
#include "tscore/BaseLogFile.h"
#include "tscore/runroot.h"

static int syslog_mode    = false;
static int debug_mode     = false;
static int wait_mode      = false;
static char *host_triplet = nullptr;
static int target_pid     = getppid();
static char *user         = nullptr;

// If pid_t is not sizeof(int), we will have to jiggle argument parsing.
extern char __pid_size_static_assert[sizeof(pid_t) == sizeof(int) ? 0 : -1];

static AppVersionInfo appVersionInfo;
static const ArgumentDescription argument_descriptions[] = {
  {"target", '-', "Target process ID", "I", &target_pid, nullptr, nullptr},
  {"host", '-', "Host triplet for the process being logged", "S*", &host_triplet, nullptr, nullptr},
  {"wait", '-', "Stop until signalled at startup", "F", &wait_mode, nullptr, nullptr},
  {"syslog", '-', "Syslog after writing a crash log", "F", &syslog_mode, nullptr, nullptr},
  {"debug", '-', "Enable debugging mode", "F", &debug_mode, nullptr, nullptr},
  {"user", '-', "Username used to set privileges", "S*", &user, nullptr, nullptr},
  HELP_ARGUMENT_DESCRIPTION(),
  VERSION_ARGUMENT_DESCRIPTION(),
  RUNROOT_ARGUMENT_DESCRIPTION()};

static struct tm
timestamp()
{
  time_t now = time(nullptr);
  struct tm tm;

  localtime_r(&now, &tm);
  return tm;
}

static char *
crashlog_name()
{
  char filename[64];
  struct tm now = timestamp();
  std::string logdir(RecConfigReadLogDir());
  ats_scoped_str pathname;

  strftime(filename, sizeof(filename), "crash-%Y-%m-%d-%H%M%S.log", &now);
  pathname = Layout::relative_to(logdir, filename);

  return pathname.release();
}

static FILE *
crashlog_open(const char *path)
{
  int fd;

  fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, 0400);
  return (fd == -1) ? nullptr : fdopen(fd, "w");
}

static unsigned
max_passwd_size()
{
#if defined(_SC_GETPW_R_SIZE_MAX)
  long val = sysconf(_SC_GETPW_R_SIZE_MAX);
  if (val > 0) {
    return static_cast<unsigned>(val);
  }
#endif

  return 4096;
}

static void
change_privileges()
{
  struct passwd *pwd;
  struct passwd pbuf;
  char buf[max_passwd_size()];

  if (getpwnam_r(user, &pbuf, buf, sizeof(buf), &pwd) != 0) {
    Error("missing password database entry for username '%s': %s", user, strerror(errno));
    return;
  }

  if (pwd == nullptr) {
    // Password entry not found ...
    Error("missing password database entry for '%s'", user);
    return;
  }

  if (setegid(pwd->pw_gid) != 0) {
    Error("setegid(%d) failed: %s", pwd->pw_gid, strerror(errno));
    return;
  }

  if (setreuid(pwd->pw_uid, 0) != 0) {
    Error("setreuid(%d, %d) failed: %s", pwd->pw_uid, 0, strerror(errno));
    return;
  }
}

int
main(int /* argc ATS_UNUSED */, const char **argv)
{
  FILE *fp;
  char *logname;
  TSMgmtError mgmterr;
  crashlog_target target;
  pid_t parent = getppid();

  diags = new Diags("traffic_crashlog", "" /* tags */, "" /* actions */, new BaseLogFile("stderr"));

  appVersionInfo.setup(PACKAGE_NAME, "traffic_crashlog", PACKAGE_VERSION, __DATE__, __TIME__, BUILD_MACHINE, BUILD_PERSON, "");

  // Process command line arguments and dump into variables
  process_args(&appVersionInfo, argument_descriptions, countof(argument_descriptions), argv);

  // XXX This is a hack. traffic_manager starts traffic_server with the euid of the admin user. We are still
  // privileged, but won't be able to open files in /proc or ptrace the target. This really should be fixed
  // in traffic_manager.
  if (getuid() == 0) {
    change_privileges();
  }

  if (wait_mode) {
    EnableDeathSignal(SIGKILL);
    kill(getpid(), SIGSTOP);
  }

  // If our parent changed, then we were woken after traffic_server exited. There's no point trying to
  // emit a crashlog because traffic_server is gone.
  if (getppid() != parent) {
    return 0;
  }

  runroot_handler(argv);
  Layout::create();
  RecProcessInit(RECM_STAND_ALONE, nullptr /* diags */);
  LibRecordsConfigInit();

  if (syslog_mode) {
    RecString name;
    int facility = -1;

    if (REC_ReadConfigStringAlloc(name, "proxy.config.syslog_facility") == REC_ERR_OKAY) {
      facility = facility_string_to_int(name);
      ats_free(name);
    }

    if (facility < 0) {
      facility = LOG_DAEMON;
    }

    openlog(appVersionInfo.AppStr, LOG_PID | LOG_NDELAY | LOG_NOWAIT, facility);
    diags->config.outputs[DL_Debug].to_syslog     = true;
    diags->config.outputs[DL_Status].to_syslog    = true;
    diags->config.outputs[DL_Note].to_syslog      = true;
    diags->config.outputs[DL_Warning].to_syslog   = true;
    diags->config.outputs[DL_Error].to_syslog     = true;
    diags->config.outputs[DL_Fatal].to_syslog     = true;
    diags->config.outputs[DL_Alert].to_syslog     = true;
    diags->config.outputs[DL_Emergency].to_syslog = true;
  }

  Note("crashlog started, target=%ld, debug=%s syslog=%s, uid=%ld euid=%ld", static_cast<long>(target_pid),
       debug_mode ? "true" : "false", syslog_mode ? "true" : "false", (long)getuid(), (long)geteuid());

  mgmterr = TSInit(nullptr, (TSInitOptionT)(TS_MGMT_OPT_NO_EVENTS | TS_MGMT_OPT_NO_SOCK_TESTS));
  if (mgmterr != TS_ERR_OKAY) {
    char *msg = TSGetErrorMessage(mgmterr);
    Warning("failed to initialize management API: %s", msg);
    TSfree(msg);
  }

  ink_zero(target);
  target.pid       = static_cast<pid_t>(target_pid);
  target.timestamp = timestamp();

  if (host_triplet && strncmp(host_triplet, "x86_64-unknown-linux", sizeof("x86_64-unknown-linux") - 1) == 0) {
    ssize_t nbytes;
    target.flags |= CRASHLOG_HAVE_THREADINFO;

    nbytes = read(STDIN_FILENO, &target.siginfo, sizeof(target.siginfo));
    if (nbytes < static_cast<ssize_t>(sizeof(target.siginfo))) {
      Warning("received %zd of %zu expected signal info bytes", nbytes, sizeof(target.siginfo));
      target.flags &= ~CRASHLOG_HAVE_THREADINFO;
    }

    nbytes = read(STDIN_FILENO, &target.ucontext, sizeof(target.ucontext));
    if (nbytes < static_cast<ssize_t>(sizeof(target.ucontext))) {
      Warning("received %zd of %zu expected thread context bytes", nbytes, sizeof(target.ucontext));
      target.flags &= ~CRASHLOG_HAVE_THREADINFO;
    }
  }

  logname = crashlog_name();

  fp = debug_mode ? stdout : crashlog_open(logname);
  if (fp == nullptr) {
    Error("failed to create '%s': %s", logname, strerror(errno));
    ats_free(logname);
    return 1;
  }

  Note("logging to %p", fp);

  crashlog_write_procname(fp, target);
  crashlog_write_exename(fp, target);
  fprintf(fp, LABELFMT "Traffic Server %s\n", "Version:", PACKAGE_VERSION);
  crashlog_write_uname(fp, target);
  crashlog_write_datime(fp, target);

  fprintf(fp, "\n");
  crashlog_write_siginfo(fp, target);

  fprintf(fp, "\n");
  crashlog_write_registers(fp, target);

  fprintf(fp, "\n");
  crashlog_write_backtrace(fp, target);

  fprintf(fp, "\n");
  crashlog_write_procstatus(fp, target);

  fprintf(fp, "\n");
  crashlog_write_proclimits(fp, target);

  fprintf(fp, "\n");
  crashlog_write_regions(fp, target);

  fprintf(fp, "\n");
  crashlog_write_records(fp, target);

  Error("wrote crash log to %s", logname);

  ats_free(logname);

  fflush(fp);
  fclose(fp);
  return 0;
}
