/** @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/Version.h"
#include "tscore/Layout.h"
#include "tscore/ink_syslog.h"
#include "records/RecCore.h"
#include "records/RecordsConfig.h"
#include "tscore/BaseLogFile.h"
#include "tscore/runroot.h"
#include "iocore/eventsystem/RecProcess.h"

#include <csignal>
#include <unistd.h>

namespace
{
// Timeout in seconds for backtrace collection. If ptrace/waitpid hangs, this
// prevents the crashlog helper from blocking indefinitely.
constexpr unsigned BACKTRACE_TIMEOUT_SECS = 10;

volatile sig_atomic_t backtrace_timed_out = 0;

void
backtrace_alarm_handler(int /* sig */)
{
  backtrace_timed_out = 1;
}
} // namespace

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;
static char *exec_pgm     = 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 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},
  {"exec",   '-', "Program to execute at crash time (takes 1 pid parameter)", "S*", &exec_pgm,     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");
}

extern int ServerBacktrace(unsigned /* options */, pid_t pid, pid_t crashing_tid, char **trace);

bool
crashlog_write_backtrace(FILE *fp, const crashlog_target &target)
{
  char *trace   = nullptr;
  int   mgmterr = -1;

  if (target.pid > 0) {
    // Set up a timeout to prevent indefinite hangs in ptrace/waitpid.
    backtrace_timed_out = 0;
    struct sigaction new_action;
    struct sigaction old_action;
    new_action.sa_handler = backtrace_alarm_handler;
    sigemptyset(&new_action.sa_mask);
    new_action.sa_flags = 0;
    sigaction(SIGALRM, &new_action, &old_action);
    alarm(BACKTRACE_TIMEOUT_SECS);

    mgmterr = ServerBacktrace(0, target.pid, target.crashing_tid, &trace);

    // Cancel the alarm and restore the old handler.
    alarm(0);
    sigaction(SIGALRM, &old_action, nullptr);

    if (backtrace_timed_out) {
      fprintf(fp, "Backtrace collection timed out after %u seconds\n", BACKTRACE_TIMEOUT_SECS);
      free(trace);
      return false;
    }
  }

  // NOTE: sometimes we can't get a backtrace because the ptrace attach will fail with
  // EPERM. I've seen this happen when a debugger is attached, which makes sense, but it
  // can also happen without a debugger. Possibly in that case, there is a race with the
  // kernel locking the process information?

  if (mgmterr == 0 && trace != nullptr && trace[0] != '\0') {
    // ServerBacktrace succeeded - this gives us backtraces for all threads.
    fprintf(fp, "%s", trace);
    free(trace);
    return true;
  }
  free(trace);

  // ServerBacktrace reports success but yields no frames when the target's thread list is
  // unreadable -- e.g. a fast-aborting target has already exited by the time the forked
  // helper attaches. Fall back to the in-process backtrace from the crashing thread rather
  // than emitting an empty report.
  if ((target.flags & CRASHLOG_HAVE_BACKTRACE) && !target.backtrace.empty()) {
    fprintf(fp, "Crashing Thread Backtrace:\n%s", target.backtrace.c_str());
    return true;
  }

  // No backtrace available from either source.
  if (mgmterr != 0) {
    fprintf(fp, "Unable to retrieve backtrace: ServerBacktrace returned %d\n", mgmterr);
  } else if (target.pid <= 0) {
    fprintf(fp, "Unable to retrieve backtrace: process ID not available\n");
  } else {
    fprintf(fp, "Unable to retrieve backtrace: no backtrace data available\n");
  }
  return false;
}

void
crashlog_exec_pgm(FILE *fp, pid_t pid)
{
  if (exec_pgm) {
    fprintf(fp, "Executing Program `%s %d`:\n", exec_pgm, pid);
    fflush(fp);
    if (int chpid = fork(); chpid == 0) {
      char tmp[32];
      snprintf(tmp, sizeof(tmp), "%d", pid);
      char *args[3] = {exec_pgm, tmp, nullptr};

      dup2(fileno(fp), STDOUT_FILENO);
      dup2(fileno(fp), STDERR_FILENO);

      if (execv(exec_pgm, args) == -1) {
        Error("Failed to exec pgm: %s\n", strerror(errno));
      }
    } else {
      int stat = 0;
      waitpid(chpid, &stat, 0);
      fflush(fp);
      Note("Exec program returned status %d (pid %d)\n", stat, chpid);
    }
  }
}

int
main(int /* argc ATS_UNUSED */, const char **argv)
{
  FILE           *fp;
  char           *logname;
  crashlog_target target;

  DiagsPtr::set(new Diags("traffic_crashlog", "" /* tags */, "" /* actions */, new BaseLogFile("stderr")));

  auto &version = AppVersionInfo::setup_version("traffic_crashlog");

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

  if (wait_mode) {
    EnableDeathSignal(SIGCONT);
    kill(getpid(), SIGSTOP);

    // A real crash sends us a notification on this socket; any other parent exit just closes
    // it, so EOF (or a short read) means there is nothing to log. getppid() is unreliable here:
    // PR_SET_PDEATHSIG can fire while our parent pid is still alive.
    //
    //
    //
    int     crash_signo = 0; // value unused; an int keeps the socket framing aligned with the writer
    ssize_t nbytes;
    do {
      nbytes = read(STDIN_FILENO, &crash_signo, sizeof(crash_signo));
    } while (nbytes < 0 && errno == EINTR);

    if (nbytes != static_cast<ssize_t>(sizeof(crash_signo))) {
      return 0;
    }
  }

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

  if (syslog_mode) {
    int facility = -1;
    if (auto name{RecGetRecordStringAlloc("proxy.config.syslog_facility")}; name) {
      // coverity[fun_call_w_exception] - guarded by if(name) check above
      facility = facility_string_to_int(ats_as_c_str(name));
    }

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

    openlog(version.application(), 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());

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

  // Read crash context on Linux platforms. The siginfo_t and ucontext_t
  // structures are platform-specific but should be defined for all Linux
  // architectures.
  if (host_triplet && (strstr(host_triplet, "linux") != nullptr || strstr(host_triplet, "Linux") != nullptr)) {
    ssize_t nbytes;
    target.flags |= CRASHLOG_HAVE_THREADINFO;

    nbytes = read(STDIN_FILENO, &target.crashing_tid, sizeof(target.crashing_tid));
    if (nbytes < static_cast<ssize_t>(sizeof(target.crashing_tid))) {
      Warning("received %zd of %zu expected crashing thread ID bytes", nbytes, sizeof(target.crashing_tid));
      target.flags &= ~CRASHLOG_HAVE_THREADINFO;
    } else {
      Note("received crashing thread ID: %ld", static_cast<long>(target.crashing_tid));
    }

    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;
    }

    // Read the in-process backtrace from the crashing thread.
    uint32_t bt_len = 0;
    nbytes          = read(STDIN_FILENO, &bt_len, sizeof(bt_len));
    if (nbytes == static_cast<ssize_t>(sizeof(bt_len)) && bt_len > 0 && bt_len < 1024 * 1024) {
      target.backtrace.resize(bt_len);
      nbytes = read(STDIN_FILENO, target.backtrace.data(), bt_len);
      if (nbytes == static_cast<ssize_t>(bt_len)) {
        target.flags |= CRASHLOG_HAVE_BACKTRACE;
        Note("received %u bytes of in-process backtrace", bt_len);
      } else {
        Warning("received %zd of %u expected backtrace bytes", nbytes, bt_len);
        target.backtrace.clear();
      }
    }
  }

  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 "%s\n", "Version:", version.full_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_exec_pgm(fp, target.pid);

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

  ats_free(logname);

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