OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / opencog / cogserver / server / ModuleManager.cc

341 lines · cc ref:
/*
 * opencog/cogserver/server/ModuleManager.cc
 *
 * Copyright (C) 2002-2007 Novamente LLC
 * Copyright (C) 2008 by OpenCog Foundation
 * Written by Andre Senna <senna@vettalabs.com>
 *            Gustavo Gama <gama@vettalabs.com>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * The Cogserver Module system is deprecated; users are encouraged to
 * explore writing guile (scheme) or python modules instead.
 */

#include <cstdlib>
#include <dlfcn.h>

#include <filesystem>

#include <opencog/util/Logger.h>
#include <opencog/util/exceptions.h>
#include <opencog/util/misc.h>

#include "ModuleManager.h"

using namespace opencog;

ModuleManager::ModuleManager(void)
{
    // Check for COGSERVER_MODULE_PATH environment variable.
    // This is a colon-separated list of directories to search for modules.
    // These paths are searched first, before the install prefix.
    const char* env_path = std::getenv("COGSERVER_MODULE_PATH");
    if (env_path)
    {
        std::vector<std::string> paths;
        tokenize(env_path, std::back_inserter(paths), ":");
        for (const std::string& p : paths)
        {
            if (not p.empty())
                module_paths.push_back(p);
        }
    }

    // Finally, search the install prefix.
    module_paths.push_back(PROJECT_INSTALL_PREFIX "/lib/opencog/modules/");
}

ModuleManager::~ModuleManager()
{
    logger().debug("[ModuleManager] enter destructor");

    std::vector<std::string> moduleKeys;

    for (ModuleMap::iterator it = modules.begin(); it != modules.end(); ++it)
        moduleKeys.push_back(it->first);

    // unload all modules
    for (std::vector<std::string>::iterator k = moduleKeys.begin(); k != moduleKeys.end(); ++k) {
        // retest the key because it might have been removed already
        ModuleMap::iterator it = modules.find(*k);
        if (it != modules.end()) {
            logger().debug("[ModuleManager] removing module \"%s\"", it->first.c_str());
            ModuleData mdata = it->second;

            // cache filename and id to erase the entries from the modules map
            std::string filename = mdata.filename;
            std::string id = mdata.id;

            // invoke the module's unload function
            (*mdata.unloadFunction)(mdata.module);

            // erase the map entries (one with the filename as key, and
            // one with the module id as key)
            modules.erase(filename);
            modules.erase(id);
        }
    }

    logger().debug("[ModuleManager] exit destructor");
}

// ====================================================================

/// If `filepath` contains directory slashes, return only
/// the fragment of the string after the last slash.
static std::string get_filename(const std::string& fullpath)
{
#define PATH_SEP '/'
    size_t path_sep = fullpath.rfind(PATH_SEP);
    if (path_sep != std::string::npos)
        return fullpath.substr(path_sep+1);
    return fullpath;
}

/// Get the directory portion of the fullpath.
static std::string get_filepath(const std::string& fullpath)
{
    size_t path_sep = fullpath.rfind(PATH_SEP);
    if (path_sep != std::string::npos)
        return fullpath.substr(0, path_sep);
    return fullpath;
}

bool ModuleManager::loadAbsPath(const std::string& path,
                               const Handle& hcsn)
{
    std::string fi = get_filename(path);
    if (modules.find(fi) !=  modules.end()) {
        logger().debug("Module \"%s\" is already loaded.", fi.c_str());
        return true;
    }

    // reset error
    dlerror();

    logger().debug("Loading module \"%s\"", path.c_str());
#ifdef __APPLE__
    // Tell dyld to search runpath.
    std::string withRPath("@rpath/");
    withRPath += path;
    // Check to see if so extension is specified, replace with .dylib if it is.
    if (withRPath.substr(withRPath.size()-3,3) == ".so") {
        withRPath.replace(withRPath.size()-3,3,".dylib");
    }
    void *dynLibrary = dlopen(withRPath.c_str(), RTLD_LAZY | RTLD_GLOBAL);
#else
    void *dynLibrary = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
#endif
    const char* dlsymError = dlerror();
    if ((dynLibrary == NULL) || (dlsymError)) {
        // This is almost surely due to a user configuration error.
        // User errors are always logged as warnings.
        logger().warn("Unable to load module \"%s\": %s",
                       path.c_str(), dlsymError);
        return false;
    }

    // Reset error state.
    dlerror();

    // Search for id function.
    Module::IdFunction* id_func =
        (Module::IdFunction*) dlsym(dynLibrary, Module::id_function_name());
    dlsymError = dlerror();
    if (dlsymError) {
        logger().error("Unable to find symbol \"%s\": %s (module %s)",
                        Module::id_function_name(), dlsymError, path.c_str());
        return false;
    }

    // Get and check module id.
    const char *module_id = (*id_func)();
    if (module_id == NULL) {
        logger().warn("Invalid module id (module \"%s\")", path.c_str());
        return false;
    }

    // Search for 'load', 'unload' and 'config' symbols.
    Module::LoadFunction* load_func =
        (Module::LoadFunction*) dlsym(dynLibrary, Module::load_function_name());
    dlsymError = dlerror();
    if (dlsymError) {
        logger().error("Unable to find symbol \"%s\": %s",
                       Module::load_function_name(), dlsymError);
        return false;
    }

    Module::UnloadFunction* unload_func =
        (Module::UnloadFunction*) dlsym(dynLibrary, Module::unload_function_name());
    dlsymError = dlerror();
    if (dlsymError) {
        logger().error("Unable to find symbol \"%s\": %s",
                       Module::unload_function_name(), dlsymError);
        return false;
    }

    // Load and init module
    Module* module = (Module*) (*load_func)(hcsn);

    // Store two entries in the module map:
    //    1: filename => <struct module data>
    //    2: moduleid => <struct module data>
    // We rely on the assumption that no module id will match the
    // filename of another module (and vice-versa). This is probably
    // reasonable since most module filenames should have a .dll or
    // .dylib or .so suffix.
    std::string i = module_id;
    std::string f = get_filename(path);
    std::string p = get_filepath(path);
    ModuleData mdata = {module, i, f, p, load_func, unload_func, dynLibrary};
    modules[i] = mdata;
    modules[f] = mdata;

    // after registration, call the module's init() method
    module->init();

    return true;
}

// ====================================================================

std::string ModuleManager::listModules()
{
    std::string rv =
        "   Module Name           Library            Module Directory Path\n"
        "   -----------           -------            ---------------------\n";
    for (const auto& modpr : modules)
    {
        // The list holds both lib.so's, and names.
        // Loop only over the so's.
        const std::string& module_id = modpr.first;
        if (module_id.find(".so", 0) == std::string::npos)
            continue;

        ModuleData mdata = modpr.second;

        // Truncate the filepath. Sorry! Is there a better way?
        std::string trunc = mdata.dirpath;
        size_t tlen = trunc.size();
        if (38 < tlen)
            trunc = "..." + trunc.substr(tlen-35);

        char buff[120];
        snprintf(buff, 120, "%-21s %-18s %s\n", mdata.id.c_str(),
                 mdata.filename.c_str(), trunc.c_str());
        rv += buff;
    }

    return rv;
}

// ====================================================================

bool ModuleManager::unloadModule(const std::string& moduleId)
{
    ModuleData mdata = getModuleData(moduleId);

    // Unable to find the module!
    if (nullptr == mdata.module) return false;

    // Cache filename, id and handle; we'll need these in just a moment.
    std::string filename = mdata.filename;
    std::string id       = mdata.id;
    void*       handle   = mdata.handle;

    // Invoke the module's unload function.
    (*mdata.unloadFunction)(mdata.module);

    // erase the map entries (one with the filename as key,
    // and one with the module id as key
    modules.erase(filename);
    modules.erase(id);

    // Unload dynamically loadable library.
    logger().debug("Unloading module \"%s\"", filename.c_str());

    dlerror(); // Reset error state.
    if (dlclose(handle) != 0) {
        const char* dlsymError = dlerror();
        if (dlsymError) {
            logger().warn("Unable to unload module \"%s\": %s",
                           filename.c_str(), dlsymError);
            return false;
        }
    }

    return true;
}

// ====================================================================

ModuleManager::ModuleData ModuleManager::getModuleData(const std::string& moduleId)
{
    std::string f = get_filename(moduleId);
    ModuleMap::const_iterator it = modules.find(f);
    if (it == modules.end()) {
        logger().info("[ModuleManager] module \"%s\" was not found.", f.c_str());
        static ModuleData nulldata = {NULL, "", "", "", NULL, NULL, NULL};
        return nulldata;
    }
    return it->second;
}

Module* ModuleManager::getModule(const std::string& moduleId)
{
    return getModuleData(moduleId).module;
}

// ====================================================================

bool ModuleManager::loadModule(const std::string& path, const Handle& hcsn)
{
    if (0 == path.size()) return false;
    if ('/' == path[0])
        return loadAbsPath(path, hcsn);

    // Loop over the different possible module paths.
    bool rc = false;
    for (const std::string& module_path : module_paths) {
        std::filesystem::path modulePath(module_path);
        modulePath /= path;
        if (std::filesystem::exists(modulePath)) {
            rc = loadModule(modulePath.string(), hcsn);
            if (rc) break;
        }
    }
    return rc;
}

void ModuleManager::loadModules(const Handle& hcsn)
{
    // Search the build dirs first, then the install dirs.
    std::string modlist =
        "libbuiltinreqs.so, "
        "libtop-shell.so, "
        "libscheme-shell.so, "
        "libsexpr-shell.so, "
        "libjson-shell.so, "
        "libmcp-shell.so, "
        "libpy-shell.so";

    std::vector<std::string> modules;
    tokenize(modlist, std::back_inserter(modules), ", ");
    bool load_failure = false;
    for (const std::string& module : modules) {
        bool rc = loadModule(module, hcsn);
        if (not rc)
        {
            logger().warn("Failed to load module %s", module.c_str());
            load_failure = true;
        }
    }
    if (load_failure) {
        for (auto p : module_paths)
            logger().warn("Searched for module at %s", p.c_str());
    }
}

// ========================= END OF FILE ==============================
// ====================================================================