/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis
 *
 * gimpbacktrace-linux.c
 * Copyright (C) 2018 Ell
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */


#define _GNU_SOURCE


#include "config.h"

#include <gio/gio.h>

#include "gimpbacktrace-backend.h"


#ifdef GIMP_BACKTRACE_BACKEND_LINUX


#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <signal.h>
#include <execinfo.h>
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>

#ifdef HAVE_LIBBACKTRACE
#include <backtrace.h>
#endif

#ifdef HAVE_LIBUNWIND
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#endif

#include "core-types.h"

#include "gimpbacktrace.h"


#define MAX_N_THREADS        256
#define MAX_N_FRAMES         256
#define MAX_THREAD_NAME_SIZE 32
#define N_SKIPPED_FRAMES     2
#define MAX_WAIT_TIME        (G_TIME_SPAN_SECOND / 20)
#define BACKTRACE_SIGNAL     SIGUSR1


typedef struct _GimpBacktraceThread GimpBacktraceThread;


struct _GimpBacktraceThread
{
  pid_t    tid;
  gchar    name[MAX_THREAD_NAME_SIZE];
  gchar    state;

  guintptr frames[MAX_N_FRAMES];
  gint     n_frames;
};

struct _GimpBacktrace
{
  GimpBacktraceThread *threads;
  gint                 n_threads;
};


/*  local function prototypes  */

static inline gint   gimp_backtrace_normalize_frame   (GimpBacktrace *backtrace,
                                                       gint           thread,
                                                       gint           frame);

static gint          gimp_backtrace_enumerate_threads (gboolean       include_current_thread,
                                                       pid_t         *threads,
                                                       gint           size);
static void          gimp_backtrace_read_thread_name  (pid_t          tid,
                                                       gchar         *name,
                                                       gint           size);
static gchar         gimp_backtrace_read_thread_state (pid_t          tid);

static void          gimp_backtrace_signal_handler    (gint           signum);


/*  static variables  */

static GMutex            mutex;
static gint              n_initializations;
static gboolean          initialized;
static struct sigaction  orig_action;
static pid_t             blacklisted_threads[MAX_N_THREADS];
static gint              n_blacklisted_threads;
static GimpBacktrace    *handler_backtrace;
static gint              handler_n_remaining_threads;
static gint              handler_lock;

#ifdef HAVE_LIBBACKTRACE
static struct backtrace_state *backtrace_state;
#endif

static const gchar * const blacklisted_thread_names[] =
{
  "gmain",
  "threaded-ml"
};


/*  private functions  */


static inline gint
gimp_backtrace_normalize_frame (GimpBacktrace *backtrace,
                                gint           thread,
                                gint           frame)
{
  if (frame >= 0)
    return frame + N_SKIPPED_FRAMES;
  else
    return backtrace->threads[thread].n_frames + frame;
}

static gint
gimp_backtrace_enumerate_threads (gboolean  include_current_thread,
                                  pid_t    *threads,
                                  gint      size)
{
  DIR           *dir;
  struct dirent *dirent;
  pid_t          tid;
  gint           n_threads;

  dir = opendir ("/proc/self/task");

  if (! dir)
    return 0;

  tid = syscall (SYS_gettid);

  n_threads = 0;

  while (n_threads < size && (dirent = readdir (dir)))
    {
      pid_t id = g_ascii_strtoull (dirent->d_name, NULL, 10);

      if (id)
        {
          if (! include_current_thread && id == tid)
            id = 0;
        }

      if (id)
        {
          gint i;

          for (i = 0; i < n_blacklisted_threads; i++)
            {
              if (id == blacklisted_threads[i])
                {
                  id = 0;

                  break;
                }
            }
        }

      if (id)
        threads[n_threads++] = id;
    }

  closedir (dir);

  return n_threads;
}

static void
gimp_backtrace_read_thread_name (pid_t  tid,
                                 gchar *name,
                                 gint   size)
{
  gchar filename[64];
  gint  fd;

  if (size <= 0)
    return;

  name[0] = '\0';

  g_snprintf (filename, sizeof (filename),
              "/proc/self/task/%llu/comm",
              (unsigned long long) tid);

  fd = open (filename, O_RDONLY);

  if (fd >= 0)
    {
      gint n = read (fd, name, size);

      if (n > 0)
        name[n - 1] = '\0';

      close (fd);
    }
}

static gchar
gimp_backtrace_read_thread_state (pid_t tid)
{
  gchar buffer[64];
  gint  fd;
  gchar state = '\0';

  g_snprintf (buffer, sizeof (buffer),
              "/proc/self/task/%llu/stat",
              (unsigned long long) tid);

  fd = open (buffer, O_RDONLY);

  if (fd >= 0)
    {
      gint n = read (fd, buffer, sizeof (buffer));

      if (n > 0)
        buffer[n - 1] = '\0';

      sscanf (buffer, "%*d %*s %c", &state);

      close (fd);
    }

  return state;
}

static void
gimp_backtrace_signal_handler (gint signum)
{
  GimpBacktrace *curr_backtrace;
  gint           lock;

  do
    {
      lock = g_atomic_int_get (&handler_lock);

      if (lock < 0)
        continue;
    }
  while (! g_atomic_int_compare_and_exchange (&handler_lock, lock, lock + 1));

  curr_backtrace = g_atomic_pointer_get (&handler_backtrace);

  if (curr_backtrace)
    {
      pid_t tid = syscall (SYS_gettid);
      gint  i;

      for (i = 0; i < curr_backtrace->n_threads; i++)
        {
          GimpBacktraceThread *thread = &curr_backtrace->threads[i];

          if (thread->tid == tid)
            {
              thread->n_frames = backtrace ((gpointer *) thread->frames,
                                            MAX_N_FRAMES);

              g_atomic_int_dec_and_test (&handler_n_remaining_threads);

              break;
            }
        }
    }

  g_atomic_int_dec_and_test (&handler_lock);
}


/*  public functions  */


void
gimp_backtrace_init (void)
{
#ifdef HAVE_LIBBACKTRACE
  backtrace_state = backtrace_create_state (NULL, 0, NULL, NULL);
#endif
}

gboolean
gimp_backtrace_start (void)
{
  g_mutex_lock (&mutex);

  if (n_initializations == 0)
    {
      struct sigaction action = {};

      action.sa_handler = gimp_backtrace_signal_handler;
      action.sa_flags   = SA_RESTART;

      sigemptyset (&action.sa_mask);

      if (sigaction (BACKTRACE_SIGNAL, &action, &orig_action) == 0)
        {
          pid_t *threads;
          gint   n_threads;
          gint   i;

          n_blacklisted_threads = 0;

          threads = g_new (pid_t, MAX_N_THREADS);

          n_threads = gimp_backtrace_enumerate_threads (TRUE,
                                                        threads, MAX_N_THREADS);

          for (i = 0; i < n_threads; i++)
            {
              gchar name[MAX_THREAD_NAME_SIZE];
              gint  j;

              gimp_backtrace_read_thread_name (threads[i],
                                               name, MAX_THREAD_NAME_SIZE);

              for (j = 0; j < G_N_ELEMENTS (blacklisted_thread_names); j++)
                {
                  if (! strcmp (name, blacklisted_thread_names[j]))
                    {
                      blacklisted_threads[n_blacklisted_threads++] = threads[i];
                    }
                }
            }

          g_free (threads);

          initialized = TRUE;
        }
    }

  n_initializations++;

  g_mutex_unlock (&mutex);

  return initialized;
}

void
gimp_backtrace_stop (void)
{
  g_return_if_fail (n_initializations > 0);

  g_mutex_lock (&mutex);

  n_initializations--;

  if (n_initializations == 0 && initialized)
    {
      if (sigaction (BACKTRACE_SIGNAL, &orig_action, NULL) < 0)
        g_warning ("failed to restore origianl backtrace signal handler");

      initialized = FALSE;
    }

  g_mutex_unlock (&mutex);
}

GimpBacktrace *
gimp_backtrace_new (gboolean include_current_thread)
{
  GimpBacktrace *backtrace;
  pid_t          pid;
  pid_t         *threads;
  gint           n_threads;
  gint64         start_time;
  gint           i;

  if (! initialized)
    return NULL;

  pid = getpid ();

  threads = g_new (pid_t, MAX_N_THREADS);

  n_threads = gimp_backtrace_enumerate_threads (include_current_thread,
                                                threads, MAX_N_THREADS);

  if (n_threads == 0)
    {
      g_free (threads);

      return NULL;
    }

  g_mutex_lock (&mutex);

  backtrace = g_slice_new (GimpBacktrace);

  backtrace->threads   = g_new (GimpBacktraceThread, n_threads);
  backtrace->n_threads = n_threads;

  while (! g_atomic_int_compare_and_exchange (&handler_lock, 0, -1));

  g_atomic_pointer_set (&handler_backtrace,           backtrace);
  g_atomic_int_set     (&handler_n_remaining_threads, n_threads);

  g_atomic_int_set (&handler_lock, 0);

  for (i = 0; i < n_threads; i++)
    {
      GimpBacktraceThread *thread = &backtrace->threads[i];

      thread->tid      = threads[i];
      thread->n_frames = 0;

      gimp_backtrace_read_thread_name (thread->tid,
                                       thread->name, MAX_THREAD_NAME_SIZE);

      thread->state = gimp_backtrace_read_thread_state (thread->tid);

      syscall (SYS_tgkill, pid, threads[i], BACKTRACE_SIGNAL);
    }

  g_free (threads);

  start_time = g_get_monotonic_time ();

  while (g_atomic_int_get (&handler_n_remaining_threads) > 0)
    {
      gint64 time = g_get_monotonic_time ();

      if (time - start_time > MAX_WAIT_TIME)
        break;

      g_usleep (1000);
    }

  while (! g_atomic_int_compare_and_exchange (&handler_lock, 0, -1));

  g_atomic_pointer_set (&handler_backtrace, NULL);

  g_atomic_int_set (&handler_lock, 0);

#if 0
  if (handler_n_remaining_threads > 0)
    {
      gint j = 0;

      for (i = 0; i < n_threads; i++)
        {
          if (backtrace->threads[i].n_frames == 0)
            {
              if (n_blacklisted_threads < MAX_N_THREADS)
                {
                  blacklisted_threads[n_blacklisted_threads++] =
                    backtrace->threads[i].tid;
                }
            }
          else
            {
              if (j < i)
                backtrace->threads[j] = backtrace->threads[i];

              j++;
            }
        }

      n_threads = j;
    }
#endif

  g_mutex_unlock (&mutex);

  if (n_threads == 0)
    {
      gimp_backtrace_free (backtrace);

      return NULL;
    }

  return backtrace;
}

void
gimp_backtrace_free (GimpBacktrace *backtrace)
{
  if (! backtrace)
    return;

  g_free (backtrace->threads);

  g_slice_free (GimpBacktrace, backtrace);
}

gint
gimp_backtrace_get_n_threads (GimpBacktrace *backtrace)
{
  g_return_val_if_fail (backtrace != NULL, 0);

  return backtrace->n_threads;
}

guintptr
gimp_backtrace_get_thread_id (GimpBacktrace *backtrace,
                              gint           thread)
{
  g_return_val_if_fail (backtrace != NULL, 0);
  g_return_val_if_fail (thread >= 0 && thread < backtrace->n_threads, 0);

  return backtrace->threads[thread].tid;
}

const gchar *
gimp_backtrace_get_thread_name (GimpBacktrace *backtrace,
                                gint           thread)
{
  g_return_val_if_fail (backtrace != NULL, NULL);
  g_return_val_if_fail (thread >= 0 && thread < backtrace->n_threads, NULL);

  if (backtrace->threads[thread].name[0])
    return backtrace->threads[thread].name;
  else
    return NULL;
}

gboolean
gimp_backtrace_is_thread_running (GimpBacktrace *backtrace,
                                  gint           thread)
{
  g_return_val_if_fail (backtrace != NULL, FALSE);
  g_return_val_if_fail (thread >= 0 && thread < backtrace->n_threads, FALSE);

  return backtrace->threads[thread].state == 'R';
}

gint
gimp_backtrace_find_thread_by_id (GimpBacktrace *backtrace,
                                  guintptr       thread_id,
                                  gint           thread_hint)
{
  pid_t tid = thread_id;
  gint  i;

  g_return_val_if_fail (backtrace != NULL, -1);

  if (thread_hint >= 0                    &&
      thread_hint <  backtrace->n_threads &&
      backtrace->threads[thread_hint].tid == tid)
    {
      return thread_hint;
    }

  for (i = 0; i < backtrace->n_threads; i++)
    {
      if (backtrace->threads[i].tid == tid)
        return i;
    }

  return -1;
}

gint
gimp_backtrace_get_n_frames (GimpBacktrace *backtrace,
                             gint           thread)
{
  g_return_val_if_fail (backtrace != NULL, 0);
  g_return_val_if_fail (thread >= 0 && thread < backtrace->n_threads, 0);

  return MAX (backtrace->threads[thread].n_frames - N_SKIPPED_FRAMES, 0);
}

guintptr
gimp_backtrace_get_frame_address (GimpBacktrace *backtrace,
                                  gint           thread,
                                  gint           frame)
{
  g_return_val_if_fail (backtrace != NULL, 0);
  g_return_val_if_fail (thread >= 0 && thread < backtrace->n_threads, 0);

  frame = gimp_backtrace_normalize_frame (backtrace, thread, frame);

  g_return_val_if_fail (frame >= N_SKIPPED_FRAMES &&
                        frame <  backtrace->threads[thread].n_frames, 0);

  return backtrace->threads[thread].frames[frame];
}

#ifdef HAVE_LIBBACKTRACE
static void
gimp_backtrace_syminfo_callback (GimpBacktraceAddressInfo *info,
                                 guintptr                  pc,
                                 const gchar              *symname,
                                 guintptr                  symval,
                                 guintptr                  symsize)
{
  if (symname)
    g_strlcpy (info->symbol_name, symname, sizeof (info->symbol_name));

  info->symbol_address = symval;
}

static gint
gimp_backtrace_pcinfo_callback (GimpBacktraceAddressInfo *info,
                                guintptr                  pc,
                                const gchar              *filename,
                                gint                      lineno,
                                const gchar              *function)
{
  if (function)
    g_strlcpy (info->symbol_name, function, sizeof (info->symbol_name));

  if (filename)
    g_strlcpy (info->source_file, filename, sizeof (info->source_file));

  info->source_line = lineno;

  return 0;
}
#endif /* HAVE_LIBBACKTRACE */

gboolean
gimp_backtrace_get_address_info (guintptr                  address,
                                 GimpBacktraceAddressInfo *info)
{
  Dl_info  dl_info;
  gboolean result = FALSE;

  g_return_val_if_fail (info != NULL, FALSE);

  info->object_name[0] = '\0';

  info->symbol_name[0] = '\0';
  info->symbol_address = 0;

  info->source_file[0] = '\0';
  info->source_line    = 0;

  if (dladdr ((gpointer) address, &dl_info))
    {
      if (dl_info.dli_fname)
        {
          g_strlcpy (info->object_name, dl_info.dli_fname,
                     sizeof (info->object_name));
        }

      if (dl_info.dli_sname)
        {
          g_strlcpy (info->symbol_name, dl_info.dli_sname,
                     sizeof (info->symbol_name));
        }

      info->symbol_address = (guintptr) dl_info.dli_saddr;

      result = TRUE;
    }

#ifdef HAVE_LIBBACKTRACE
  if (backtrace_state)
    {
      backtrace_syminfo (
        backtrace_state, address,
        (backtrace_syminfo_callback) gimp_backtrace_syminfo_callback,
        NULL,
        info);

      backtrace_pcinfo (
        backtrace_state, address,
        (backtrace_full_callback) gimp_backtrace_pcinfo_callback,
        NULL,
        info);

      result = TRUE;
    }
#endif /* HAVE_LIBBACKTRACE */

#ifdef HAVE_LIBUNWIND
/* we use libunwind to get the symbol name, when available, even if dladdr() or
 * libbacktrace already found one, since it provides more descriptive names in
 * some cases, and, in particular, full symbol names for C++ lambdas.
 *
 * note that, in some cases, this can result in a discrepancy between the
 * symbol name, and the corresponding source location.
 */
#if 0
  if (! info->symbol_name[0])
#endif
    {
      unw_context_t context = {};
      unw_cursor_t  cursor;
      unw_word_t    offset;

      if (unw_init_local (&cursor, &context)         == 0 &&
          unw_set_reg (&cursor, UNW_REG_IP, address) == 0 &&
          unw_get_proc_name (&cursor,
                             info->symbol_name, sizeof (info->symbol_name),
                             &offset)                == 0)
        {
          info->symbol_address = address - offset;

          result = TRUE;
        }
    }
#endif /* HAVE_LIBUNWIND */

  return result;
}


#endif /* GIMP_BACKTRACE_BACKEND_LINUX */
