Skip to content

File plugin.cpp

File List > src > plugin.cpp

Go to the documentation of this file

#include "illixr.hpp"
#include "illixr/error_util.hpp"
#include "illixr/switchboard.hpp"

#ifdef __ANDROID__
#    include 
#    include 
#    include  
#    define _STR(y)      #y
#    define STRINGIZE(x) _STR(x)

#else
#    ifndef ILLIXR_INSTALL_PATH
#        error "ILLIXR_INSTALL_PATH must be defined"
#    endif
#    ifndef BOOST_DATE_TIME_NO_LIB
#        define BOOST_DATE_TIME_NO_LIB
#    endif
#    if defined(ILLIXR_ENABLE_BOBA) && defined(ILLIXR_ENABLE_QUEST_CONTROLLERS)
#        include "illixr/network/udpsocket.hpp"
#    endif

#    include 
#    include 
#    include 
#    include 
#    include 
#    include 
#    ifdef __linux__
#        include 
#        include 
#    endif
#    include 
#    include 
#    include 
#    include 
#endif

#ifndef __ANDROID__
namespace ILLIXR {
struct Dependency {
    std::string                                     name;
    std::map<std::string, std::vector<std::string>> deps;

    bool operator==(const Dependency& rhs) const {
        return name == rhs.name;
    }

    bool operator==(const std::string& rhs) const {
        return name == rhs;
    }
};
} // namespace ILLIXR

#    if !defined(__ANDROID__) && defined(ILLIXR_ENABLE_BOBA) && defined(ILLIXR_ENABLE_QUEST_CONTROLLERS)
namespace {
constexpr int         QUEST_CONFIGURATION_PORT = 9010;
constexpr const char* QUEST_CONNECT_REQUEST    = "ILLIXR_CONNECT_V1";
constexpr const char* QUEST_CONNECT_RESPONSE   = "ILLIXR_READY_V1";

void configure_native_quest(const cxxopts::ParseResult& options) {
    if (!options.count("quest-ip")) {
        return;
    }

    const std::string quest_ip = options["quest-ip"].as<std::string>();
    sockaddr_in       parsed_address{};
    if (inet_pton(AF_INET, quest_ip.c_str(), &parsed_address.sin_addr) != 1) {
        throw std::runtime_error("Invalid Quest IPv4 address: " + quest_ip);
    }

    const int timeout_seconds = options["quest-connect-timeout"].as<int>();
    if (timeout_seconds <= 0 || timeout_seconds > 3600) {
        throw std::runtime_error("--quest-connect-timeout must be between 1 and 3600 seconds");
    }

    ILLIXR::network::UDPSocket socket;
    socket.socket_set_reuseaddr();
    socket.socket_bind(0);
    socket.socket_set_receive_timeout(500);
    socket.set_peer(quest_ip, QUEST_CONFIGURATION_PORT);

    std::cout << "Waiting for ILLIXRApp at " << quest_ip << ':' << QUEST_CONFIGURATION_PORT
              << ". Open the app on the Quest if it is not already running." << std::endl;

    const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{timeout_seconds};
    while (std::chrono::steady_clock::now() < deadline) {
        if (!socket.write_data(QUEST_CONNECT_REQUEST)) {
            throw std::runtime_error("Could not send wireless configuration to Quest " + quest_ip);
        }

        sockaddr_in source{};
        std::string response = socket.read_data(&source);
        if (response == QUEST_CONNECT_RESPONSE && source.sin_addr.s_addr == parsed_address.sin_addr.s_addr) {
            std::cout << "ILLIXRApp acknowledged the desktop at " << quest_ip << ". Starting ILLIXR." << std::endl;
            errno = 0;
            return;
        }
    }

    throw std::runtime_error("Timed out waiting for ILLIXRApp at " + quest_ip +
                             ". Confirm that the app is open and both devices are on the same network.");
}
} // namespace

#    endif

namespace YAML {
template<>
struct convert<ILLIXR::Dependency> {
    static Node encode(const ILLIXR::Dependency& rhs) {
        Node node;
        node["plugin"] = rhs.name;
        for (const auto& [key, value] : rhs.deps) {
            Node dep_node;
            dep_node["needs"] = key;
            for (const auto& v : value) {
                dep_node["provided_by"].push_back(v);
            }
            node["dependencies"].push_back(dep_node);
        }
        return node;
    }

    static bool decode(const Node& node, ILLIXR::Dependency& rhs) {
        if (node.size() != 2) {
            return false;
        }
        rhs.name  = node["plugin"].as<std::string>();
        Node deps = node["dependencies"];
        if (!deps.IsSequence()) {
            return false;
        }
        for (const auto& nd : deps) {
            if (nd.size() != 2) {
                return false;
            }
            auto dep_name = nd["needs"].as<std::string>();
            auto prov     = nd["provided_by"].as<std::vector<std::string>>();

            rhs.deps[dep_name] = prov;
        }
        return true;
    }
};
} // namespace YAML
#endif

MY_EXPORT_API ILLIXR::runtime* runtime_ = nullptr;

using namespace ILLIXR;

#ifndef __ANDROID__
std::string get_home_dir() {
#    if defined(_WIN32) || defined(_WIN64)
    char* path = getenv("USERPROFILE");
    return {path};
#    else
    struct passwd* pw = getpwuid(getuid());
    return {pw->pw_dir};
#    endif
}

void check_plugins(std::vector<std::string>& plugins, const std::vector<ILLIXR::Dependency>& dep_map) {
    std::vector<std::string> ordered_plugins;
    ordered_plugins.reserve(plugins.size());

    auto resolve = [&dep_map, &ordered_plugins, &plugins](std::vector<std::string>::iterator it, auto&& resolve) {
        auto find_it = std::find(dep_map.begin(), dep_map.end(), *it);
        // if the plugin does not have any dependencies, then just add it to the list
        if (find_it == dep_map.end()) {
            if (std::find(ordered_plugins.begin(), ordered_plugins.end(), *it) == ordered_plugins.end())
                ordered_plugins.push_back(*it);
            return false;
        }
        bool mod = false;
        // go through each dependency and see if it is specified
        for (const auto& [item, needs] : find_it->deps) {
            bool dep_found = false;
            // first check plugins which are already in the list, if found, then we are good.
            for (const auto& provided_by : needs) {
                if (std::find(ordered_plugins.begin(), ordered_plugins.end(), provided_by) != ordered_plugins.end()) {
                    dep_found = true;
                    break;
                }
            }
            // try finding it in the rest of the list
            if (!dep_found) {
                bool rdep_found = false;
                for (const auto& provided_by : needs) {
                    auto r_find_it = std::find(plugins.begin(), plugins.end(), provided_by);
                    if (r_find_it != plugins.end()) {
                        rdep_found = true;
                        mod        = true;
                        resolve(r_find_it, resolve);
                        if (std::find(ordered_plugins.begin(), ordered_plugins.end(), provided_by) == ordered_plugins.end())
                            ordered_plugins.push_back(provided_by);
                        break;
                    }
                }
                if (!rdep_found) {
                    spdlog::get("illixr")->warn(
                        "Potential missing plugin dependency. Plugin " + *it + " requests a provider of " + item +
                        " to be included in the plugin list. This can be provided by one of the following plugins: " +
                        boost::algorithm::join(needs, ", "));
                }
            }
        }
        return mod;
    };

    bool modified = false;
    for (auto iter = plugins.begin(); iter != plugins.end(); iter++) {
        std::string input = *iter;
        modified |= resolve(iter, resolve);

        if (std::find(ordered_plugins.begin(), ordered_plugins.end(), *iter) == ordered_plugins.end())
            ordered_plugins.push_back(*iter);
    }
    if (modified)
        plugins = ordered_plugins;
}
#endif

int ILLIXR::run(
#ifdef __ANDROID__
    const std::vector<std::string>& plugins, struct android_app* app
#else
    const cxxopts::ParseResult& options
#endif
) {
    std::chrono::seconds run_duration;
#ifndef __ANDROID__
    std::vector<std::string> plugins;
#endif
    try {
        runtime_ = ILLIXR::runtime_factory();

#if !defined(__ANDROID__) && defined(ILLIXR_ENABLE_BOBA) && defined(ILLIXR_ENABLE_QUEST_CONTROLLERS)
        configure_native_quest(options);
#endif

        // set internal env_vars
        std::shared_ptr<switchboard> switchboard_ = runtime_->get_switchboard();
#ifdef __ANDROID__
        switchboard_->set_android_app(app);
#else

        // read in yaml config file
        YAML::Node config;

        setenv("ILLIXR_BINARY_PATH", ILLIXR_INSTALL_PATH, 1);
        std::string home_dir = get_home_dir();
        if (options.count("yaml")) {
            std::cout << "Reading " << options["yaml"].as<std::string>() << std::endl;
            auto                     config_file_full = options["yaml"].as<std::string>();
            std::string              config_file      = config_file_full.substr(config_file_full.find_last_of("/\\") + 1);
            std::vector<std::string> config_list      = {config_file,
                                                         config_file_full,
                                                         home_dir + "/.illixr/profiles/" + config_file_full,
                                                         home_dir + "/.illixr/profiles/" + config_file,
                                                         home_dir + "/" + config_file_full,
                                                         home_dir + "/" + config_file,
                                                         std::string(ILLIXR_INSTALL_PATH) + "/share/illixr/profiles/" +
                                                             config_file_full,
                                                         std::string(ILLIXR_INSTALL_PATH) + "/share/illixr/profiles/" + config_file};
            for (auto& filepath : config_list) {
                try {
                    config = YAML::LoadFile(filepath);
                    break;
                } catch (YAML::BadFile&) { }
            }
            if (config.size() == 0) {
                spdlog::get("illixr")->error("Could not load given config file: " + config_file_full);
                throw std::runtime_error("Could not load given config file: " + config_file_full);
            }
            config_list.clear();
        }

        // set env vars from config file first, as command line args will override
        for (const auto& item : config["env_vars"]) {
            const auto val = item.first.as<std::string>();
            if (std::find(ignore_vars.begin(), ignore_vars.end(), val) == ignore_vars.end())
                switchboard_->set_env(val, item.second.as<std::string>());
        }
        // command line specified env_vars
        for (auto& item : options.unmatched()) {
            bool                                   matched = false;
            cxxopts::values::parser_tool::ArguDesc ad      = cxxopts::values::parser_tool::ParseArgument(item.c_str(), matched);

            if (switchboard_->get_env(ad.arg_name, "").empty()) {
                if (!ad.set_value)
                    ad.value = "True";
                switchboard_->set_env(ad.arg_name, ad.value);
                setenv(ad.arg_name.c_str(), ad.value.c_str(), 1); // env vars from command line take precedence
            }
        }
#endif
#ifndef NDEBUG
        const bool enable_pre_sleep = switchboard_->get_env_bool("ILLIXR_ENABLE_PRE_SLEEP", "False");
        if (enable_pre_sleep) {
#    if defined(_WIN32) || defined(_WIN64)
            DWORD pid = GetCurrentProcessId();
#    else
            const pid_t pid = getpid();
#    endif
            spdlog::get("illixr")->info("[main] Pre-sleep enabled.");
            spdlog::get("illixr")->info("[main] PID: {}", pid);
            spdlog::get("illixr")->info("[main] Sleeping for {} seconds...", ILLIXR_PRE_SLEEP_DURATION);
#    if defined(_WIN32) || defined(_WIN64)
            Sleep(ILLIXR_PRE_SLEEP_DURATION * 1000);
#    else
            sleep(ILLIXR_PRE_SLEEP_DURATION);
#    endif
            spdlog::get("illixr")->info("[main] Resuming...");
        }
#endif 
#ifdef __ANDROID__
        run_duration = (!switchboard_->get_env("ILLIXR_RUN_DURATION").empty())
            ? std::chrono::seconds{std::stol(std::string{switchboard_->get_env("ILLIXR_RUN_DURATION")})}
            : ILLIXR_RUN_DURATION_DEFAULT;
#else
        if (options.count("duration")) {
            run_duration = std::chrono::seconds{options["duration"].as<long>()};
        } else if (config["env_vars"]["duration"]) {
            run_duration = std::chrono::seconds{config["env_vars"]["duration"].as<long>()};
        } else {
            run_duration = (!switchboard_->get_env("ILLIXR_RUN_DURATION").empty())
                ? std::chrono::seconds{std::stol(std::string{switchboard_->get_env("ILLIXR_RUN_DURATION")})}
                : ILLIXR_RUN_DURATION_DEFAULT;
        }
        GET_STRING(data, ILLIXR_DATA)
        GET_STRING(demo_data, ILLIXR_DEMO_DATA)
        GET_BOOL(enable_offload, ILLIXR_OFFLOAD_ENABLE)
        GET_BOOL(alignment_enable, ILLIXR_ALIGNMENT_ENABLE)
        GET_BOOL(enable_verbose_errors, ILLIXR_ENABLE_VERBOSE_ERRORS)
        GET_BOOL(enable_pre_sleep, ILLIXR_ENABLE_PRE_SLEEP)
        GET_BOOL(openxr, ILLIXR_OPENXR)
        GET_STRING(realsense_cam, REALSENSE_CAM)

        if (switchboard_->get_env_char("ILLIXR_DISPLAY_MODE") == nullptr) {
            spdlog::get("illixr")->info("[main] Display mode not selected, defaulting to GLFW.");
            switchboard_->set_env("ILLIXR_DISPLAY_MODE", "glfw");
        }

        setenv("__GL_MaxFramesAllowed", "1", false);
        setenv("__GL_SYNC_TO_VBLANK", "1", false);

        std::vector<ILLIXR::Dependency> dep_map;
        std::vector<std::string>        dep_list   = {"plugin_deps.yaml", home_dir + "/.illixr/profiles/plugin_deps.yaml",
                                                      std::string(ILLIXR_INSTALL_PATH) + "/share/illixr/profiles/plugin_deps.yaml"

        };
        bool                            dep_loaded = false;
        for (auto& dep_file : dep_list) {
            try {
                YAML::Node plugin_deps = YAML::LoadFile(dep_file);
#    ifndef NDEBUG
                spdlog::get("illixr")->info("Located plugin dependency map file (" + dep_file +
                                            "), verifying plugin dependencies.");
#    endif
                dep_map.reserve(plugin_deps["dep_map"].size());
                for (const auto& node : plugin_deps["dep_map"])
                    dep_map.push_back(node.as<ILLIXR::Dependency>());
                dep_loaded = true;
                break;
            } catch (YAML::BadFile& bf) { }
        }

        if (!dep_loaded)
            spdlog::get("illixr")->info("Could not load plugin dependency map file, cannot verify plugin dependencies.");

        bool have_plugins = false;
        // run entry supersedes plugins entry
        for (auto item : {"plugins"}) {
            if (options.count(item)) {
                plugins      = options[item].as<std::vector<std::string>>();
                have_plugins = true;
            } else if (config[item]) {
                std::stringstream tss(config[item].as<std::string>());
                while (tss.good()) {
                    std::string substr;
                    getline(tss, substr, ',');
                    plugins.push_back(substr);
                }
                have_plugins = true;
            }
        }

        if (!have_plugins) {
            std::cout << "No plugins specified." << std::endl;
            std::cout << "A list of plugins must be given on the command line or in a YAML file" << std::endl;
            return EXIT_FAILURE;
        }

        check_plugins(plugins, dep_map);
        if (config["install_prefix"]) {
            std::string temp_path(switchboard_->get_env("LD_LIBRARY_PATH"));
            temp_path = config["install_prefix"].as<std::string>() + ":" + temp_path;
            setenv("LD_LIBRARY_PATH", temp_path.c_str(), true);
        }

        // prevent double free
        switchboard_.reset();
#endif
        RAC_ERRNO_MSG("main after creating runtime");

        std::vector<std::string> lib_paths;
        std::transform(plugins.begin(), plugins.end(), std::back_inserter(lib_paths), [](const std::string& arg) {
#if defined(_WIN32) || defined(_WIN64)
            return "plugin." + arg + STRINGIZE(ILLIXR_BUILD_SUFFIX) + ".dll";
#else
            return "libplugin." + arg + STRINGIZE(ILLIXR_BUILD_SUFFIX) + ".so";
#endif
        });

        RAC_ERRNO_MSG("main before loading dynamic libraries");
        runtime_->load_so(lib_paths);

        cancellable_sleep cs;
        std::thread       th{[&] {
            cs.sleep(run_duration);
            runtime_->stop();
        }};

        runtime_->wait(); // blocks until shutdown is runtime_->stop()

        // cancel our sleep, so we can join the other thread
        cs.cancel();
        th.join();

        delete runtime_;
    } catch (const std::exception& ex) {
        std::cout << "ERROR: Exception caught in main: " << ex.what() << std::endl;
        delete runtime_;
    }
    return 0;
}