Subversion Repositories tpanel

Rev

Rev 11 | Blame | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2020, 2021 by Andreas Theofilu <andreas@theosys.at>
 *
 * 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, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
#include "tconfig.h"

#include <fstream>
#include <vector>
#include <iterator>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "terror.h"

using std::string;
using std::ifstream;
using std::ofstream;
using std::fstream;
using std::vector;
using std::cout;
using std::cerr;
using std::endl;

/**
 * @struct SETTINGS
 * @brief The SETTINGS struct bundles the configuration options.
 *
 * This structure contains variables for all possible configuration options.
 * It is used by the class TConfig. Through this class it's possible to
 * access all configuration options.
 */
struct SETTINGS
{
    string pname{"tpanel"};     //!< Name of the program (default "tpanel")
    string path;                //!< The path where the configuration file is located
    string name;                //!< The name of the configuration file
    string project;             //!< The path where the original project files are located
    string server;              //!< The name or IP address of the server to connect
    int system{0};              //!< The number of the AMX system
    int port{0};                //!< The port number
    int ID{0};                  //!< the panel ID (a number starting by 10000)
    string ptype;               //!< The type of the panel (android, ipad, iphone, ...)
    string version;             //!< The "firmware" version
    string logFile;             //!< Optional path and name of a logfile
    string logLevel;            //!< The log level(s).
    bool longformat{false};     //!< TRUE = long format
    bool noBanner{false};       //!< Startup without showing a banner on the command line.
    bool certCheck{false};      //!< TRUE = Check certificate for SSL connection
};

typedef struct SETTINGS settings_t;
static settings_t localSettings;    //!< Global defines settings used in class TConfig.

/**
 * @brief TConfig::TConfig constructor
 *
 * @param path  A path and name of a configuration file.
 */
TConfig::TConfig(const std::string& path)
        : mPath(path)
{
        if (findConfig())
                readConfig();
}

/**
 * @brief TConfig::setProgName Sets the name of the application.
 * @param pname The name of the application.
 */
void TConfig::setProgName(const std::string& pname)
{
        localSettings.pname = pname;
}

/**
 * @brief TConfig::getProgName Retrieves the prevously stored application name.
 * @return The name of this application.
 */
std::string & TConfig::getProgName()
{
        return localSettings.pname;
}

/**
 * @brief TConfig::getChannel returns the AMX channel to use.
 *
 * The AMX channels an AMX panel can use start at 10000. This method returns
 * the channel number found in the configuration file. If there was no
 * channel defination found, it returns the default channel 10001.
 *
 * @return The AMX channel number to use.
 */
int TConfig::getChannel()
{
        return localSettings.ID;
}

/**
 * @brief TConfig::getConfigFileName returns the name of the configuration file.
 *
 * @return The name of the configuration file.
 */
std::string& TConfig::getConfigFileName()
{
        return localSettings.name;
}

/**
 * @brief TConfig::getConfigPath returns the path configuration file.
 *
 * The path was defined on the command line or found by searching the standard
 * directories.
 *
 * @return The path of the configuration file.
 */
std::string& TConfig::getConfigPath()
{
        return localSettings.path;
}

/**
 * @brief TConfig::getController returns the network name or IP address of the AMX controller.
 *
 * The network name or the IP address was read from the configuration file.
 *
 * @return The network name of the AMX controller.
 */
std::string& TConfig::getController()
{
        return localSettings.server;
}

/**
 * @brief TConfig::getSystem return the AMX system number.
 *
 * This number was read from the configuration file. If there was no system
 * number defined in the configuration file, then the default number 0 is
 * returned.
 *
 * @return The AMX system number.
 */
int TConfig::getSystem()
{
    return localSettings.system;
}

/**
 * @brief TConfig::getFirmVersion returns the version of the firmware.
 *
 * This option was read from the configuration file. There can be any version
 * number defined. But you must keep in mind, that the AMX controller may not
 * accept any number. If there was no version number defined, the standard
 * version number 1.0 is returned.
 *
 * @return The firmware version of this panel.
 */
std::string& TConfig::getFirmVersion()
{
        return localSettings.version;
}

/**
 * @brief TConfig::getLogFile the path and name of a logfile.
 *
 * If there is a logfile name defined in the configuration file, it is used
 * to write messages there. It depends on the _log level_ what is logged.
 *
 * @return The path and name of a logfile.
 */
std::string& TConfig::getLogFile()
{
        return localSettings.logFile;
}

/**
 * @brief TConfig::getLogLevel returns the defined log level.
 *
 * The loglevel can be one of the following values:
 *
 *     NONE         Logs nothing (default for Android)
 *     INFO         Logs only informations
 *     WARNING      Logs only warnings
 *     ERROR        Logs onls errors
 *     TRACE        Logs only trace messages
 *     DEBUG        Logs only debug messages
 *     PROTOCOL     Logs only INFO and ERROR (default if NOT Android)
 *     ALL          Logs everything
 *
 * All log levels can be combined by concatenating them with the | symbol.
 *
 * @return The log level(s) as a string.
 */
string& TConfig::getLogLevel()
{
        return localSettings.logLevel;
}

/**
 * @brief TConfig::getPanelType the AMX type name of the panel.
 *
 * The type name of the panel is defined in the configuration file. If this
 * option was not defined, the default panel _android_ is returned.
 *
 * @return The type name of the panel.
 */
std::string& TConfig::getPanelType()
{
        return localSettings.ptype;
}

/**
 * @brief TConfig::getPort returnes the AMX port number to connect to.
 *
 * The port number can be defined in the configuration file. If there is no
 * configuration the default number 1319 is returned.
 *
 * @return The AMX network port number.
 */
int TConfig::getPort()
{
        return localSettings.port;
}

/**
 * @brief TConfig::getProjectPath returns the path to the AMX configuration files.
 *
 * The path was read from the configuration file. This path contains all the
 * files needed to display the elements of the surface.
 *
 * @return The path to the AMX configuration files.
 */
std::string& TConfig::getProjectPath()
{
        return localSettings.project;
}

/**
 * @brief TConfig::isLongFormat defines the format in the logfile.
 *
 * If this returns `true` the format in the logfile is a long format. This
 * means, that in front of each message is an additional timestamp.
 *
 * @return `true` = long format, `false` = short format (default).
 */
bool TConfig::isLongFormat()
{
        return localSettings.longformat;
}

/**
 * @brief TConfig::showBanner defines whether the banner should be showed or not.
 *
 * If this method returns `false` the banner on startup is not displayed.
 *
 * @return `true` = display the banner (default), `false` = show no banner.
 */
bool TConfig::showBanner()
{
        return !localSettings.noBanner;
}

/**
 * @brief TConfig::certCheck check the certificate if the connection is encrypted.
 *
 * Currently not implemented!
 *
 * @return `true` = check the certificate, `false` = accept any certificate (default).
 */
bool TConfig::certCheck()
{
    return localSettings.certCheck;
}

/**
 * @brief TConfig::findConfig search for the location of the configuration file.
 *
 * If there was no configuration file given on the command line, this method
 * searches for a configuration file on a few standard directories. This are:
 *
 *     /etc/tpanel.conf
 *     /etc/tpanel/tpanel.conf
 *     /usr/etc/tpanel.conf
 *     $HOME/.tpanel.conf
 *
 * On macOS the following additional directories are searched:
 *
 *     /opt/local/etc/tpanel.conf
 *     /opt/local/etc/tpanel/tpanel.conf
 *     /opt/local/usr/etc/tpanel.conf
 *
 * @return On success `true`, otherwise `false`.
 */
bool TConfig::findConfig()
{
        char *HOME = nullptr;
        string sFileName;

        if (!mPath.empty())
        {
                size_t pos = mPath.find_last_of("/");

                if (pos != string::npos)
                {
                        localSettings.path = mPath.substr(0, pos);
                        localSettings.name = mPath.substr(pos+1);
                        mCFile = mPath;
                        return !mCFile.empty();
                }

                localSettings.name = mPath;
                mCFile = mPath;
                return !mCFile.empty();
        }

        localSettings.name = "tpanel.conf";
#ifdef __ANDROID__
    char *androidData = getenv("ANDROID_DATA");
    char *androidRoot = getenv("ANDROID_ROOT");

    if (!androidData)
    {
        TError::setError();
        cerr << "Missing environment variable ANDROID_DATA!" << endl;
        TError::setErrorMsg(TERRERROR, "Missing environment variable ANDROID_DATA!");
        return false;
    }

    if (!androidRoot)
    {
        TError::setError();
        cerr << "Missing environment variable ANDROID_ROOT!" << endl;
        TError::setErrorMsg(TERRERROR, "Missing environment variable ANDROID_ROOT!");
        return false;
    }

    localSettings.path = androidData;
    mRoot = androidRoot;

    if (access(androidData, F_OK) == -1)    // Does the configuration file exist?
    {                                       // No, than create it and also the directory structure
        try
        {
            sFileName = localSettings.path + "/" + localSettings.name;
            ofstream cfg(sFileName);

            string content = "LogFile=" + localSettings.path + "/tpanel.log\n";
            content += "LogLevel=PROTOCOL\n";
            content += "ProjectPath=" + localSettings.path + "/tpanel\n";
            content += "LongFormat=false\n";
            content += "Address=0.0.0.0\n";
            content += "Port=1319\n";
            content += "Channel=10001\n";
            content += "PanelType=Android\n";
            content += "Firmware=1.0.0\n";
            cfg.write(content.c_str(), content.size());
            cfg.close();

            string path = localSettings.path + "/tpanel";
            mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
            string syspath = path + "/fonts";
            mkdir(syspath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
            syspath = path + "/images";
            mkdir(syspath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
            syspath = path + "/sounds";
            mkdir(syspath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
            syspath = path + "/__system";
            mkdir(syspath.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
        }
        catch (std::exception& e)
        {
            cerr << "Error: " << e.what() << endl;
            TError::setErrorMsg(TERRERROR, string("Error: ") + e.what());
            return false;
        }
    }
#else
        if (!(HOME = getenv("HOME")))
                std::cerr << "TConfig::findConfig: No environment variable HOME!" << std::endl;

        if (!access("/etc/tpanel.conf", R_OK))
        {
                sFileName = "/etc/tpanel.conf";
                localSettings.path = "/etc";
        }
        else if (!access("/etc/tpanel/tpanel.conf", R_OK))
        {
                sFileName = "/etc/tpanel/tpanel.conf";
                localSettings.path = "/etc/tpanel";
        }
        else if (!access("/usr/etc/tpanel.conf", R_OK))
        {
                sFileName = "/usr/etc/tpanel.conf";
                localSettings.path = "/usr/etc";
        }
#ifdef __APPLE__
        if (!access("/opt/local/etc/tpanel.conf", R_OK))
        {
                sFileName = "/opt/local/etc/tpanel.conf";
                localSettings.path = "/opt/local/etc";
        }
        else if (!access("/opt/local/etc/tpanel/tpanel.conf", R_OK))
        {
                sFileName = "/opt/local/etc/tpanel/tpanel.conf";
                localSettings.path = "/opt/local/etc/tpanel";
        }
        else if (!access("/opt/local/usr/etc/tpanel.conf", R_OK))
        {
                sFileName = "/opt/local/usr/etc/tpanel.conf";
                localSettings.path = "/opt/local/usr/etc";
        }
#endif
        else if (HOME)
        {
                sFileName = HOME;
                sFileName += "/.amxpanel.conf";
                localSettings.path = HOME;

                if (access(sFileName.data(), R_OK))
                {
                        std::cerr << "TConfig::findConfig: Can't find any configuration file!" << std::endl;
                        TError::setError();
                        sFileName.clear();
                        localSettings.name.clear();
                }
                else
                        localSettings.name = ".amxpanel.conf";
        }
        else
        {
                sFileName.clear();
                localSettings.name.clear();
                localSettings.path.clear();
        }
#endif
        mCFile = sFileName;
        return !sFileName.empty();
}

/**
 * @brief TConfig::readConfig reads a config file.
 *
 * This method reads a config file and stores the known options into the
 * struct localSettings.
 *
 * @return `true` on success.\n
 * Returns `false` on error and sets the internal error.
 */
bool TConfig::readConfig()
{
        ifstream fs;

        // First initialize the defaults
        localSettings.ID = 0;
        localSettings.port = 1397;
        localSettings.ptype = "android";
        localSettings.version = "1.0";
        localSettings.longformat = false;

        // Now get the settings from file
        try
        {
                fs.open(mCFile.c_str(), fstream::in);
        }
        catch (const fstream::failure e)
        {
                std::cerr << "TConfig::readConfig: Error on file " << mCFile << ": " << e.what() << std::endl;
                TError::setError();
                return false;
        }

        for (string line; getline(fs, line);)
        {
                size_t pos;

                if ((pos = line.find("#")) != string::npos)
                {
                        if (pos == 0)
                                line.clear();
                        else
                                line = line.substr(0, pos);
                }

                if (line.empty() || line.find("=") == string::npos)
                        continue;

                vector<string> parts = split(line, "=", true);

                if (parts.size() == 2)
                {
                        string left = parts[0];
                        string right = ltrim(parts[1]);

                        if (caseCompare(left, "PORT") == 0 && !right.empty())
                                localSettings.port = atoi(right.c_str());
                        else if (caseCompare(left, "LOGFILE") == 0 && !right.empty())
                        {
                                localSettings.logFile = right;
                                TStreamError::setLogFile(right);
                        }
                        else if (caseCompare(left, "LOGLEVEL") == 0 && !right.empty())
                        {
                                TStreamError::setLogLevel(right);
                                localSettings.logLevel = right;
                        }
                        else if (caseCompare(left, "ProjectPath") == 0 && !right.empty())
                                localSettings.project = right;
            else if (caseCompare(left, "System") == 0 && !right.empty())
                localSettings.system = atoi(right.c_str());
            else if (caseCompare(left, "PanelType") == 0 && !right.empty())
                                localSettings.ptype = right;
                        else if (caseCompare(left, "Address") == 0 && !right.empty())
                                localSettings.server = right;
                        else if (caseCompare(left, "Firmware") == 0 && !right.empty())
                                localSettings.version = right;
                        else if (caseCompare(left, "LongFormat") == 0 && !right.empty())
                        {
                                if (caseCompare(right, "1") == 0 || caseCompare(right, "yes") == 0 ||
                                        caseCompare(right, "true") == 0)
                                        localSettings.longformat = true;
                                else
                                        localSettings.longformat = false;
                        }
                        else if (caseCompare(left, "NoBanner") == 0 && !right.empty())
                        {
                                if (caseCompare(right, "1") == 0 || caseCompare(right, "yes") == 0 ||
                                        caseCompare(right, "true") == 0)
                                        localSettings.noBanner = true;
                                else
                                        localSettings.noBanner = false;
                        }
                        else if (caseCompare(left, "CertCheck") == 0 && !right.empty())
            {
                if (caseCompare(right, "1") == 0 || caseCompare(right, "yes") == 0 ||
                    caseCompare(right, "true") == 0 || caseCompare(right, "on") == 0)
                    localSettings.certCheck = true;
                else
                    localSettings.certCheck = false;
            }
            else if (caseCompare(left, "Channel") == 0 && !right.empty())
                        {
                                localSettings.ID = atoi(right.c_str());

                                if (localSettings.ID < 10000 || localSettings.ID >= 11000)
                                {
                                        std::cerr << "TConfig::readConfig: Invalid port number " << right << std::endl;
                                        localSettings.ID = 0;
                                }
                        }
                }
        }

        fs.close();

        if (TStreamError::checkFilter(LOG_DEBUG))
        {
                MSG_INFO("Selected Parameters:");
                MSG_INFO("    Path to cfg.: " << localSettings.path);
                MSG_INFO("    Name of cfg.: " << localSettings.name);
                MSG_INFO("    Logfile:      " << localSettings.logFile);
                MSG_INFO("    LogLevel:     " << localSettings.logLevel);
                MSG_INFO("    Long format:  " << (localSettings.longformat ? "YES" : "NO"));
                MSG_INFO("    Project path: " << localSettings.project);
                MSG_INFO("    Show banner:  " << (localSettings.noBanner ? "NO" : "YES"));
                MSG_INFO("    Controller:   " << localSettings.server);
                MSG_INFO("    Port:         " << localSettings.port);
                MSG_INFO("    Channel:      " << localSettings.ID);
                MSG_INFO("    Panel type:   " << localSettings.ptype);
                MSG_INFO("    Firmware ver. " << localSettings.version);
        }

        return true;
}

/**
 * @brief TConfig::split splitts a string into parts.
 *
 * The method splitts a string into parts separated by \p seps. It puts the
 * parts into a vector array.
 *
 * @param str           The string to split
 * @param seps          The separator(s)
 * @param trimEmpty     `true` = trum the parts.
 *
 * @return A vector array containing the parts of the string \p str.
 */
vector<string> TConfig::split(const string& str, const string& seps, const bool trimEmpty)
{
        size_t pos = 0, mark = 0;
        vector<string> parts;

        for (auto it = str.begin(); it != str.end(); ++it)
        {
                for (auto sepIt = seps.begin(); sepIt != seps.end(); ++sepIt)
                {
                        if (*it == *sepIt)
                        {
                                size_t len = pos - mark;
                                parts.push_back(str.substr(mark, len));
                                mark = pos + 1;
                                break;
                        }
                }

                pos++;
        }

        parts.push_back(str.substr(mark));

        if (trimEmpty)
        {
                vector<string> nparts;

                for (auto it = parts.begin(); it != parts.end(); ++it)
                {
                        if (it->empty())
                                continue;

                        nparts.push_back(*it);
                }

                return nparts;
        }

        return parts;
}

/**
 * @brief TConfig::caseCompare compares 2 strings
 *
 * This method compares 2 strings case insensitive. This means that it ignores
 * the case of the letters. For example:
 *
 *     BLAME
 *     blame
 *     Blame
 *
 * are all the same and would return 0, which means _equal_.
 *
 * @param str1  1st string to compare
 * @param str2  2nd string to compare
 *
 * @return 0 if the strings are equal\n
 * less than 0 if the byte of \p str1 is bigger than the byte of \p str2\n
 * grater than 0 if the byte of \p str1 is smaller than the byte of \p str2.
 */
int TConfig::caseCompare(const string& str1, const string& str2)
{
        size_t i = 0;

        if (str1.length() != str2.length())
                return ((str1.length() < str2.length()) ? -1 : 1);

        for (auto it = str1.begin(); it != str1.end(); ++it)
        {
                if (tolower(*it) != tolower(str2.at(i)))
                        return (int)(*it - str2.at(i));

                i++;
        }

        return 0;
}