/*
     This file is part of GNUnet
     Copyright (C) 2002-2013 GNUnet e.V.

     GNUnet is free software: you can redistribute it and/or modify it
     under the terms of the GNU Affero General Public License as published
     by the Free Software Foundation, either version 3 of the License,
     or (at your option) any later version.

     GNUnet 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
     Affero General Public License for more details.

     You should have received a copy of the GNU Affero General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * @file util/plugin.c
 * @brief Methods to access plugins
 * @author Christian Grothoff
 */

#include "platform.h"
#include <ltdl.h>
#include "gnunet_util_lib.h"

#define LOG(kind, ...) GNUNET_log_from (kind, "util-plugin", __VA_ARGS__)

/**
 * Linked list of active plugins.
 */
struct PluginList
{
  /**
   * This is a linked list.
   */
  struct PluginList *next;

  /**
   * Name of the library.
   */
  char *name;

  /**
   * System handle.
   */
  void *handle;
};


/**
 * Have we been initialized?
 */
static int initialized;

/**
 * Libtool search path before we started.
 */
static char *old_dlsearchpath;

/**
 * List of plugins we have loaded.
 */
static struct PluginList *plugins;


/**
 * Setup libtool paths.
 */
static void
plugin_init ()
{
  int err;
  const char *opath;
  char *path;
  char *cpath;

  err = lt_dlinit ();
  if (err > 0)
  {
    fprintf (stderr,
             _ ("Initialization of plugin mechanism failed: %s!\n"),
             lt_dlerror ());
    return;
  }
  opath = lt_dlgetsearchpath ();
  if (NULL != opath)
    old_dlsearchpath = GNUNET_strdup (opath);
  path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_LIBDIR);
  if (NULL != path)
  {
    if (NULL != opath)
    {
      GNUNET_asprintf (&cpath, "%s:%s", opath, path);
      lt_dlsetsearchpath (cpath);
      GNUNET_free (path);
      GNUNET_free (cpath);
    }
    else
    {
      lt_dlsetsearchpath (path);
      GNUNET_free (path);
    }
  }
}


/**
 * Shutdown libtool.
 */
static void
plugin_fini ()
{
  lt_dlsetsearchpath (old_dlsearchpath);
  if (NULL != old_dlsearchpath)
  {
    GNUNET_free (old_dlsearchpath);
    old_dlsearchpath = NULL;
  }
  lt_dlexit ();
}


/**
 * Lookup a function in the plugin.
 *
 * @param plug the plugin to check
 * @param name name of the symbol to look for
 * @return NULL if the symbol was not found
 */
static GNUNET_PLUGIN_Callback
resolve_function (struct PluginList *plug, const char *name)
{
  char *initName;
  void *mptr;

  GNUNET_asprintf (&initName, "_%s_%s", plug->name, name);
  mptr = lt_dlsym (plug->handle, &initName[1]);
  if (NULL == mptr)
    mptr = lt_dlsym (plug->handle, initName);
  if (NULL == mptr)
    LOG (GNUNET_ERROR_TYPE_ERROR,
         _ ("`%s' failed to resolve method '%s' with error: %s\n"),
         "lt_dlsym",
         &initName[1],
         lt_dlerror ());
  GNUNET_free (initName);
  return mptr;
}


/**
 * Test if a plugin exists.
 *
 * Note that the library must export a symbol called
 * `library_name_init` for the test to succeed.
 *
 * @param library_name name of the plugin to test if it is installed
 * @return #GNUNET_YES if the plugin exists, #GNUNET_NO if not
 */
int
GNUNET_PLUGIN_test (const char *library_name)
{
  void *libhandle;
  GNUNET_PLUGIN_Callback init;
  struct PluginList plug;

  if (! initialized)
  {
    initialized = GNUNET_YES;
    plugin_init ();
  }
  libhandle = lt_dlopenext (library_name);
  if (NULL == libhandle)
    return GNUNET_NO;
  plug.handle = libhandle;
  plug.name = (char *) library_name;
  init = resolve_function (&plug, "init");
  if (NULL == init)
  {
    GNUNET_break (0);
    lt_dlclose (libhandle);
    return GNUNET_NO;
  }
  lt_dlclose (libhandle);
  return GNUNET_YES;
}


/**
 * Setup plugin (runs the `init` callback and returns whatever `init`
 * returned).  If `init` returns NULL, the plugin is unloaded.
 *
 * Note that the library must export symbols called
 * `library_name_init` and `library_name_done`.  These will be called
 * when the library is loaded and unloaded respectively.
 *
 * @param library_name name of the plugin to load
 * @param arg argument to the plugin initialization function
 * @return whatever the initialization function returned
 */
void *
GNUNET_PLUGIN_load (const char *library_name, void *arg)
{
  void *libhandle;
  struct PluginList *plug;
  GNUNET_PLUGIN_Callback init;
  void *ret;

  if (! initialized)
  {
    initialized = GNUNET_YES;
    plugin_init ();
  }
  libhandle = lt_dlopenext (library_name);
  if (libhandle == NULL)
  {
    LOG (GNUNET_ERROR_TYPE_ERROR,
         _ ("`%s' failed for library `%s' with error: %s\n"),
         "lt_dlopenext",
         library_name,
         lt_dlerror ());
    return NULL;
  }
  plug = GNUNET_new (struct PluginList);
  plug->handle = libhandle;
  plug->name = GNUNET_strdup (library_name);
  plug->next = plugins;
  plugins = plug;
  init = resolve_function (plug, "init");
  if ((init == NULL) || (NULL == (ret = init (arg))))
  {
    lt_dlclose (libhandle);
    GNUNET_free (plug->name);
    plugins = plug->next;
    GNUNET_free (plug);
    return NULL;
  }
  return ret;
}


/**
 * Unload plugin (runs the `done` callback and returns whatever `done`
 * returned).  The plugin is then unloaded.
 *
 * @param library_name name of the plugin to unload
 * @param arg argument to the plugin shutdown function
 * @return whatever the shutdown function returned
 */
void *
GNUNET_PLUGIN_unload (const char *library_name, void *arg)
{
  struct PluginList *pos;
  struct PluginList *prev;
  GNUNET_PLUGIN_Callback done;
  void *ret;

  prev = NULL;
  pos = plugins;
  while ((NULL != pos) && (0 != strcmp (pos->name, library_name)))
  {
    prev = pos;
    pos = pos->next;
  }
  if (NULL == pos)
    return NULL;

  done = resolve_function (pos, "done");
  ret = NULL;
  if (NULL != done)
    ret = done (arg);
  if (NULL == prev)
    plugins = pos->next;
  else
    prev->next = pos->next;
  lt_dlclose (pos->handle);
  GNUNET_free (pos->name);
  GNUNET_free (pos);
  if (NULL == plugins)
  {
    plugin_fini ();
    initialized = GNUNET_NO;
  }
  return ret;
}


/**
 * Closure for #find_libraries().
 */
struct LoadAllContext
{
  /**
   * Prefix the plugin names we find have to match.
   */
  const char *basename;

  /**
   * Argument to give to 'init' when loading the plugin.
   */
  void *arg;

  /**
   * Function to call for each plugin.
   */
  GNUNET_PLUGIN_LoaderCallback cb;

  /**
   * Closure for @e cb
   */
  void *cb_cls;
};


/**
 * Function called on each plugin in the directory.  Loads
 * the plugins that match the given basename.
 *
 * @param cls the `struct LoadAllContext` describing which
 *            plugins to load and what to do with them
 * @param filename name of a plugin library to check
 * @return #GNUNET_OK (continue loading)
 */
static int
find_libraries (void *cls, const char *filename)
{
  struct LoadAllContext *lac = cls;
  const char *slashpos;
  const char *libname;
  char *basename;
  char *dot;
  void *lib_ret;
  size_t n;

  libname = filename;
  while (NULL != (slashpos = strstr (libname, DIR_SEPARATOR_STR)))
    libname = slashpos + 1;
  n = strlen (libname);
  if (0 != strncmp (lac->basename, libname, strlen (lac->basename)))
    return GNUNET_OK; /* wrong name */
  if ((n > 3) && (0 == strcmp (&libname[n - 3], ".la")))
    return GNUNET_OK; /* .la file */
  basename = GNUNET_strdup (libname);
  if (NULL != (dot = strstr (basename, ".")))
    *dot = '\0';
  lib_ret = GNUNET_PLUGIN_load (basename, lac->arg);
  if (NULL != lib_ret)
    lac->cb (lac->cb_cls, basename, lib_ret);
  GNUNET_free (basename);
  return GNUNET_OK;
}


/**
 * Load all compatible plugins with the given base name.
 *
 * Note that the library must export symbols called
 * `basename_ANYTHING_init` and `basename_ANYTHING__done`.  These will
 * be called when the library is loaded and unloaded respectively.
 *
 * @param basename basename of the plugins to load
 * @param arg argument to the plugin initialization function
 * @param cb function to call for each plugin found
 * @param cb_cls closure for @a cb
 */
void
GNUNET_PLUGIN_load_all (const char *basename,
                        void *arg,
                        GNUNET_PLUGIN_LoaderCallback cb,
                        void *cb_cls)
{
  struct LoadAllContext lac;
  char *path;

  path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_LIBDIR);
  if (NULL == path)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Could not determine plugin installation path.\n"));
    return;
  }
  lac.basename = basename;
  lac.arg = arg;
  lac.cb = cb;
  lac.cb_cls = cb_cls;
  GNUNET_DISK_directory_scan (path, &find_libraries, &lac);
  GNUNET_free (path);
}


/* end of plugin.c */
