/* Copyright (C) 2021-2024 Free Software Foundation, Inc.
   Contributed by Oracle.

   This file is part of GNU Binutils.

   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, 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, write to the Free Software
   Foundation, 51 Franklin Street - Fifth Floor, Boston,
   MA 02110-1301, USA.  */

#include "config.h"
#include <assert.h>
#include <ctype.h>
#include <sys/param.h>
#include <unistd.h>

#include "gp-defs.h"
#include "util.h"
#include "collctrl.h"
#include "collect.h"
#include "StringBuilder.h"
#include "Settings.h"

#define	STDEBUFSIZE	24000

#define LIBGP_COLLECTOR             "libgp-collector.so"
#define GPROFNG_PRELOAD_LIBDIRS     "GPROFNG_PRELOAD_LIBDIRS"
#define SP_COLLECTOR_EXPNAME        "SP_COLLECTOR_EXPNAME"
#define SP_COLLECTOR_FOLLOW_SPEC    "SP_COLLECTOR_FOLLOW_SPEC"
#define SP_COLLECTOR_PARAMS         "SP_COLLECTOR_PARAMS"
#define SP_COLLECTOR_FOUNDER        "SP_COLLECTOR_FOUNDER"
#define SP_COLLECTOR_ORIGIN_COLLECT "SP_COLLECTOR_ORIGIN_COLLECT"

static const char *LD_AUDIT[] = {
  //    "LD_AUDIT",	Do not set LD_AUDIT on Linux
  NULL
};

static const char *LD_PRELOAD[] = {
  "LD_PRELOAD",
  NULL
};

static const char *SP_PRELOAD[] = {
  "SP_COLLECTOR_PRELOAD",
  NULL
};

static const char *LD_LIBRARY_PATH[] = {
  "LD_LIBRARY_PATH",
  NULL,
};

static int
add_env (char *ev)
{
  int r = putenv (ev);
  if (r != 0)
    {
      dbe_write (2, GTXT ("Can't putenv of %s: run aborted\n"), ev);
      free (ev);
    }
  return r;
}

int
collect::putenv_libcollector_ld_audits ()
{
  StringBuilder sb;
  for (unsigned int ii = 0; ii < ARR_SIZE (LD_AUDIT) && LD_AUDIT[ii]; ++ii)
    {
      sb.sprintf ("%s=%s", LD_AUDIT[ii], SP_LIBAUDIT_NAME);
      // Append the current value. Check if already set
      char *old_val = getenv (LD_AUDIT[ii]);
      if (old_val != NULL)
	{
	  while (isspace (*old_val))
	    ++old_val;
	  if (*old_val != (char) 0)
	    {
	      int fromIdx = sb.length ();
	      sb.append (" ");
	      sb.append (old_val);
	      if (sb.indexOf (SP_LIBAUDIT_NAME, fromIdx) >= 0)
		continue;       // Already set. Do nothing.
	    }
	}
      if (add_env (sb.toString ()))
	return 1;
    }
  return 0;
}

int
collect::putenv_libcollector_ld_preloads ()
{
  // for those data types that get extra libs LD_PRELOAD'd, add them
  if (cc->get_synctrace_mode () != 0)
    add_ld_preload ("libgp-sync.so");
  if (cc->get_heaptrace_mode () != 0)
    add_ld_preload ("libgp-heap.so");
  if (cc->get_iotrace_mode () != 0)
    add_ld_preload ("libgp-iotrace.so");
  add_ld_preload (SP_LIBCOLLECTOR_NAME);

  // --- putenv SP_COLLECTOR_PRELOAD*
  int ii;
  for (ii = 0; SP_PRELOAD[ii]; ii++)
    {
      // construct the SP_PRELOAD_* environment variables
      // and put them into the environment
      if (add_env (dbe_sprintf ("%s=%s", SP_PRELOAD[ii], sp_preload_list[ii])))
	return 1;
    }
  // --- putenv LD_PRELOADS
  /* purge LD_PRELOAD* of values containing contents of SP_LIBCOLLECTOR_NAME */
  if (putenv_purged_ld_preloads (SP_LIBCOLLECTOR_NAME))
    dbe_write (2, GTXT ("Warning: %s is already defined in one or more LD_PRELOAD environment variables\n"),
	       SP_LIBCOLLECTOR_NAME);
  if (putenv_ld_preloads ())
    return 1;
  return 0;
}

int
collect::putenv_libcollector_ld_misc ()
{
#if 0 // XXX 1 turns on LD_DEBUG
  putenv (strdup ("LD_DEBUG=audit,bindings,detail"));
#endif
  // workaround to have the dynamic linker use absolute names
  if (add_env (dbe_strdup ("LD_ORIGIN=yes")))
    return 1;

  // On Linux we have to provide SP_COLLECTOR_LIBRARY_PATH and LD_LIBRARY_PATH
  // so that -agentlib:gp-collector works
  // and so that collect -F works with 32/64-bit mix of processes

  StringBuilder sb;
  sb.append ("SP_COLLECTOR_LIBRARY_PATH=");
  int len = sb.length ();
  int cnt = 0;
  char *fname;
  char *ev = getenv (GPROFNG_PRELOAD_LIBDIRS);
  char *libpath_list = NULL;
  if (ev)
    { /* GPROFNG_PRELOAD_LIBDIRS is used only in the gprofng testing.
       * Use these directories first.  */
      ev = strdup (ev);
      for (char *s = ev; s;)
	{
	  char *s1 = strchr (s, ':');
	  if (s1)
	    *(s1++) = 0;
	  fname = dbe_sprintf ("%s/%s", s, LIBGP_COLLECTOR);
	  if (access (fname, R_OK | F_OK) == 0)
	    {
	      if (++cnt != 1)
		sb.append (':');
	      sb.append (s);
	    }
	  free (fname);
	  s = s1;
	}
      free (ev);
      ev = NULL;
    }
  if (settings->preload_libdirs == NULL)
    {
      settings->read_rc (false);
      ev = settings->preload_libdirs;
    }
  ev = dbe_strdup (ev);
  fname = dbe_sprintf ("%s/%s/%s", LIBDIR, PACKAGE, LIBGP_COLLECTOR);
  if (access (fname, R_OK | F_OK) == 0)
    {
      ++cnt;
      sb.appendf ("%s/%s", LIBDIR, PACKAGE);
    }
  free (fname);
  for (char *s = ev; s;)
    {
      char *s1 = strchr (s, ':');
      if (s1)
	*(s1++) = 0;
      if (*s == '/')
	{
	  fname = dbe_sprintf ("%s/%s/%s", s, PACKAGE, LIBGP_COLLECTOR);
	  if (access (fname, R_OK | F_OK) == 0)
	    {
	      if (++cnt != 1)
		sb.append (':');
	      sb.appendf ("%s", s);
	    }
	}
      else
	{
	  fname = dbe_sprintf ("%s/%s/%s/%s", run_dir, s, PACKAGE, LIBGP_COLLECTOR);
	  if (access (fname, R_OK | F_OK) == 0)
	    {
	      if (++cnt != 1)
		sb.append (':');
	      sb.appendf ("%s/%s/%s", run_dir, s, PACKAGE);
	    }
	}
      free (fname);
      s = s1;
    }
  free (ev);
  if (cnt == 0)
    {
      dbe_write (2, GTXT ("configuration error: can not find %s. run aborted\n"),
		 LIBGP_COLLECTOR);
      return 1;
    }
  libpath_list = sb.toString ();
  if (add_env (libpath_list))
    return 1;
  libpath_list += len;

  // --- set LD_LIBRARY_PATH using libpath_list
  char *old = getenv (LD_LIBRARY_PATH[0]);
  if (old)
    ev = dbe_sprintf ("%s=%s:%s", LD_LIBRARY_PATH[0], libpath_list, old);
  else
    ev = dbe_sprintf ("%s=%s", LD_LIBRARY_PATH[0], libpath_list);
  if (add_env (ev))
    return 1;
  return 0;
}

void
collect::add_ld_preload (const char *lib)
{
  for (int ii = 0; SP_PRELOAD[ii]; ii++)
    {
      char *old_sp = sp_preload_list[ii];
      if (old_sp == NULL)
	sp_preload_list[ii] = strdup (lib);
      else
	{
	  sp_preload_list[ii] = dbe_sprintf ("%s %s", old_sp, lib);
	  free (old_sp);
	}
    }
}

int
collect::putenv_memso ()
{
  // Set environment variable "MEM_USE_LOG" to 1, to keep it out of stderr
  if (add_env (dbe_strdup ("MEM_USE_LOG=1")))
    return 1;
  // Set environment variable "MEM_ABORT_ON_ERROR", to force a core dump
  if (add_env (dbe_strdup ("MEM_ABORT_ON_ERROR=1")))
    return 1;
  add_ld_preload ("mem.so");
  return putenv_ld_preloads ();
}

// set LD_PRELOAD and friends to prepend the given library or libraries

int
collect::putenv_ld_preloads ()
{
  for (int ii = 0; LD_PRELOAD[ii]; ii++)
    {
      char *old_val = getenv (LD_PRELOAD[ii]);
      int sp_num = ii;
      assert (SP_PRELOAD[sp_num]);
      char *preload_def;
      if (old_val)
	preload_def = dbe_sprintf ("%s=%s %s", LD_PRELOAD[ii], sp_preload_list[sp_num], old_val);
      else
	preload_def = dbe_sprintf ("%s=%s", LD_PRELOAD[ii], sp_preload_list[sp_num]);
      if (add_env (preload_def))
	return 1;
    }
  return 0;
}

/* copied from linetrace.c */
/*
   function: env_strip()
     Finds str in env; Removes
     all characters from previous ':' or ' '
     up to and including any trailing ':' or ' '.
   params:
     env: environment variable
     str: substring to find
     return: count of instances removed from env
 */
int
collect::env_strip (char *env, const char *str)
{
  int removed = 0;
  char *p, *q;
  if (env == NULL || str == NULL || *str == 0)
    return 0;
  size_t maxlen = strlen (env);
  size_t len = strlen (str);
  q = env;
  while ((p = strstr (q, str)) != NULL)
    {
      q = p;
      p += len;
      if (*p)
	{
	  while ((*p) && (*p != ':') && (*p != ' '))
	    p++;      /* skip the rest of the name*/
	  while ((*p == ':') || (*p == ' '))
	    p++; /* strip trailing separator */
	}
      while (*q != ':' && *q != ' ' && *q != '=' && q != env)
	q--; /* strip path */
      if (*p)
	{ /* copy the rest of the string */
	  if (q != env)
	    q++; /* restore leading separator (if any) */
	  size_t n = (maxlen - (q - env));
	  strncpy (q, p, n);
	}
      else
	*q = 0;
      removed++;
    }
  return removed;
}
/*
   function: putenv_purged_ld_preloads()
     Remove selected preload strings from all LD_PRELOAD* env vars.
   params:
     var: executable name (leading characters don't have to match)
     return: number of instances removed from all PRELOAD vars.
 */
int
collect::putenv_purged_ld_preloads (const char *var)
{
  int total_removed = 0;
  if (!var || *var == 0)
    return 0;
  for (int ii = 0; LD_PRELOAD[ii]; ii++)
    {
      char *ev = getenv (LD_PRELOAD[ii]);
      int removed = 0;
      if (!ev)
	continue;
      removed = env_strip (ev, var);
      if (!removed)
	continue;
      if (putenv (ev) != 0)
	dbe_write (2, GTXT ("Can't putenv of %s\n"), ev);
      total_removed += removed;
    }
  return total_removed;
}
/*
   function: putenv_append()
     append string to current enviroment variable setting and then do a putenv()
   params:
     var: environment variable name
     val: string to append
 */
int
collect::putenv_append (const char *var, const char *val)
{
  char *ev;
  if (!var || !val)
    return 1;
  const char *old_val = getenv (var);
  if (old_val == NULL || *old_val == 0)
    ev = dbe_sprintf ("%s=%s", var, val);
  else
    ev = dbe_sprintf ("%s=%s %s", var, old_val, val);

  // now put the new variable into the environment
  if (add_env (ev))
    return 1;
  return 0;
}

int
collect::putenv_libcollector (void)
{
  char buf[MAXPATHLEN + 1];
  // --- set SP_COLLECTOR_EXPNAME
  // fetch the experiment name and CWD
  char *exp = cc->get_experiment ();
  char *cwd = getcwd (buf, MAXPATHLEN);
  char *ev;

  // format the environment variable for the experiment directory name
  if (cwd != NULL && exp[0] != '/')  // experiment is a relative path
    ev = dbe_sprintf ("%s=%s/%s", SP_COLLECTOR_EXPNAME, cwd, exp);
  else      // getcwd failed or experiment is a fullpath
    ev = dbe_sprintf ("%s=%s", SP_COLLECTOR_EXPNAME, exp);

  // set the experiment directory name
  if (add_env (ev))
    return 1;

  // --- set SP_COLLECTOR_PARAMS
  // set the data descriptor
  exp = cc->get_data_desc ();
  if (add_env (dbe_sprintf ("%s=%s", SP_COLLECTOR_PARAMS, exp)))
    return 1;

  // --- set SP_COLLECTOR_FOLLOW_SPEC
  const char *follow_spec = cc->get_follow_cmp_spec ();
  if (follow_spec)
    // selective following has been enabled
    if (add_env (dbe_sprintf ("%s=%s", SP_COLLECTOR_FOLLOW_SPEC, follow_spec)))
      return 1;

  if (add_env (dbe_sprintf ("%s=%d", SP_COLLECTOR_FOUNDER, getpid ())))
    return 1;
  if (add_env (dbe_sprintf ("%s=1", SP_COLLECTOR_ORIGIN_COLLECT)))
    return 1;

  // --- set LD_*
  if (putenv_libcollector_ld_misc ())
    return 1;

  // --- set LD_PRELOAD*
  if (putenv_libcollector_ld_preloads () != 0)
    return 1;

  // --- set JAVA_TOOL_OPTIONS
  if (cc->get_java_mode () == 1)
    if (putenv_append ("JAVA_TOOL_OPTIONS", "-agentlib:gp-collector"))
	exit (1);
#if 0
  // --- set LD_AUDIT*
  if (putenv_libcollector_ld_audits () != 0)
    return 1;
#endif
  return 0;
}
