diff --git a/src/ebusd/CMakeLists.txt b/src/ebusd/CMakeLists.txt index 85b41e30..db96bcb4 100644 --- a/src/ebusd/CMakeLists.txt +++ b/src/ebusd/CMakeLists.txt @@ -7,7 +7,7 @@ set(ebusd_SOURCES network.h network.cpp mainloop.h mainloop.cpp scan.h scan.cpp - main.h main.cpp + main.h main.cpp main_args.cpp ) if(HAVE_MQTT) diff --git a/src/ebusd/Makefile.am b/src/ebusd/Makefile.am index 673ef653..ee169cf6 100644 --- a/src/ebusd/Makefile.am +++ b/src/ebusd/Makefile.am @@ -11,8 +11,7 @@ ebusd_SOURCES = \ network.h network.cpp \ mainloop.h mainloop.cpp \ scan.h scan.cpp \ - main.h main.cpp - + main.h main.cpp main_args.cpp if MQTT ebusd_SOURCES += mqtthandler.cpp mqtthandler.h endif diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index 3e5f74f5..2269a570 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -31,7 +31,6 @@ #include #include "ebusd/mainloop.h" #include "ebusd/network.h" -#include "lib/utils/arg.h" #include "lib/utils/log.h" #include "lib/utils/httpclient.h" #include "ebusd/scan.h" @@ -45,78 +44,14 @@ using std::setw; using std::nouppercase; using std::cout; -/** the config path part behind the scheme (scheme without "://"). */ -#define CONFIG_PATH_SUFFIX "://cfg.ebusd.eu/" - /** the previous config path part to rewrite to the current one. */ #define PREVIOUS_CONFIG_PATH_SUFFIX "://ebusd.eu/config/" -/** the default path of the configuration files. */ -#ifdef HAVE_SSL -#define CONFIG_PATH "https" CONFIG_PATH_SUFFIX -#else -#define CONFIG_PATH "http" CONFIG_PATH_SUFFIX -#endif - /** the opened PID file, or nullptr. */ static FILE* s_pidFile = nullptr; /** the program options. */ -static struct options s_opt = { - "/dev/ttyUSB0", // device - false, // noDeviceCheck - false, // readOnly - false, // initialSend - 0, // extraLatency - - false, // scanConfig - 0, // initialScan - 5, // scanRetries - getenv("LANG"), // preferLanguage - false, // checkConfig - OF_NONE, // dumpConfig - nullptr, // dumpConfigTo - 5, // pollInterval - false, // injectMessages - false, // stopAfterInject - nullptr, // caFile - nullptr, // caPath - - 0x31, // address - false, // answer - 10, // acquireTimeout - 3, // acquireRetries - 2, // sendRetries - SLAVE_RECV_TIMEOUT*5/3, // receiveTimeout - 0, // masterCount - false, // generateSyn - - "", // accessLevel - "", // aclFile - false, // foreground - false, // enableHex - false, // enableDefine - PACKAGE_PIDFILE, // pidFile - 8888, // port - false, // localOnly - 0, // httpPort - "/var/" PACKAGE "/html", // htmlPath - true, // updateCheck - - PACKAGE_LOGFILE, // logFile - -1, // logAreas - ll_COUNT, // logLevel - false, // multiLog - - 0, // logRaw - PACKAGE_LOGFILE, // logRawFile - 100, // logRawSize - - false, // dump - "/tmp/" PACKAGE "_dump.bin", // dumpFile - 100, // dumpSize - false, // dumpFlush -}; +static struct options s_opt; /** the @a MessageMap instance, or nullptr. */ static MessageMap* s_messageMap = nullptr; @@ -133,525 +68,6 @@ static MainLoop* s_mainLoop = nullptr; /** the @a Network instance, or nullptr. */ static Network* s_network = nullptr; -/** the (optionally corrected) config path for retrieving configuration files from. */ -static string s_configPath = CONFIG_PATH; - -/** whether scanConfig or configPath were set by arguments. */ -static bool s_scanConfigOrPathSet = false; - -#define O_INISND -2 -#define O_DEVLAT (O_INISND-1) -#define O_SCNRET (O_DEVLAT-1) -#define O_CFGLNG (O_SCNRET-1) -#define O_CHKCFG (O_CFGLNG-1) -#define O_DMPCFG (O_CHKCFG-1) -#define O_DMPCTO (O_DMPCFG-1) -#define O_POLINT (O_DMPCTO-1) -#define O_CAFILE (O_POLINT-1) -#define O_CAPATH (O_CAFILE-1) -#define O_ANSWER (O_CAPATH-1) -#define O_ACQTIM (O_ANSWER-1) -#define O_ACQRET (O_ACQTIM-1) -#define O_SNDRET (O_ACQRET-1) -#define O_RCVTIM (O_SNDRET-1) -#define O_MASCNT (O_RCVTIM-1) -#define O_GENSYN (O_MASCNT-1) -#define O_ACLDEF (O_GENSYN-1) -#define O_ACLFIL (O_ACLDEF-1) -#define O_HEXCMD (O_ACLFIL-1) -#define O_DEFCMD (O_HEXCMD-1) -#define O_PIDFIL (O_DEFCMD-1) -#define O_LOCAL (O_PIDFIL-1) -#define O_HTTPPT (O_LOCAL-1) -#define O_HTMLPA (O_HTTPPT-1) -#define O_UPDCHK (O_HTMLPA-1) -#define O_LOG (O_UPDCHK-1) -#define O_LOGARE (O_LOG-1) -#define O_LOGLEV (O_LOGARE-1) -#define O_RAW (O_LOGLEV-1) -#define O_RAWFIL (O_RAW-1) -#define O_RAWSIZ (O_RAWFIL-1) -#define O_DMPFIL (O_RAWSIZ-1) -#define O_DMPSIZ (O_DMPFIL-1) -#define O_DMPFLU (O_DMPSIZ-1) - -/** the definition of the known program arguments. */ -static const argDef argDefs[] = { - {nullptr, 0, nullptr, 0, "Device options:"}, - {"device", 'd', "DEV", 0, "Use DEV as eBUS device (" - "prefix \"ens:\" for enhanced high speed device or " - "\"enh:\" for enhanced device, with " - "\"IP:PORT\" for network device or " - "\"DEVICE\" for serial device" - ") [/dev/ttyUSB0]"}, - {"nodevicecheck", 'n', nullptr, 0, "Skip serial eBUS device test"}, - {"readonly", 'r', nullptr, 0, "Only read from device, never write to it"}, - {"initsend", O_INISND, nullptr, 0, "Send an initial escape symbol after connecting device"}, - {"latency", O_DEVLAT, "MSEC", 0, "Extra transfer latency in ms [0]"}, - - {nullptr, 0, nullptr, 0, "Message configuration options:"}, - {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTPS URL) [" - CONFIG_PATH "]"}, - {"scanconfig", 's', "ADDR", af_optional, "Pick CSV config files matching initial scan ADDR: " - "empty for broadcast ident message (default when configpath is not given), " - "\"none\" for no initial scan message, " - "\"full\" for full scan, " - "a single hex address to scan, or " - "\"off\" for not picking CSV files by scan result (default when configpath is given).\n" - "If combined with --checkconfig, you can add scan message data as " - "arguments for checking a particular scan configuration, e.g. \"FF08070400/0AB5454850303003277201\"."}, - {"scanretries", O_SCNRET, "COUNT", 0, "Retry scanning devices COUNT times [5]"}, - {"configlang", O_CFGLNG, "LANG", 0, - "Prefer LANG in multilingual configuration files [system default language]"}, - {"checkconfig", O_CHKCFG, nullptr, 0, "Check config files, then stop"}, - {"dumpconfig", O_DMPCFG, "FORMAT", af_optional, - "Check and dump config files in FORMAT (\"json\" or \"csv\"), then stop"}, - {"dumpconfigto", O_DMPCTO, "FILE", 0, "Dump config files to FILE"}, - {"pollinterval", O_POLINT, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]"}, - {"inject", 'i', "stop", af_optional, "Inject remaining arguments as already seen messages (e.g. " - "\"FF08070400/0AB5454850303003277201\"), optionally stop afterwards"}, -#ifdef HAVE_SSL - {"cafile", O_CAFILE, "FILE", 0, "Use CA FILE for checking certificates (uses defaults," - " \"#\" for insecure)"}, - {"capath", O_CAPATH, "PATH", 0, "Use CA PATH for checking certificates (uses defaults)"}, -#endif // HAVE_SSL - - {nullptr, 0, nullptr, 0, "eBUS options:"}, - {"address", 'a', "ADDR", 0, "Use hex ADDR as own master bus address [31]"}, - {"answer", O_ANSWER, nullptr, 0, "Actively answer to requests from other masters"}, - {"acquiretimeout", O_ACQTIM, "MSEC", 0, "Stop bus acquisition after MSEC ms [10]"}, - {"acquireretries", O_ACQRET, "COUNT", 0, "Retry bus acquisition COUNT times [3]"}, - {"sendretries", O_SNDRET, "COUNT", 0, "Repeat failed sends COUNT times [2]"}, - {"receivetimeout", O_RCVTIM, "MSEC", 0, "Expect a slave to answer within MSEC ms [25]"}, - {"numbermasters", O_MASCNT, "COUNT", 0, "Expect COUNT masters on the bus, 0 for auto detection [0]"}, - {"generatesyn", O_GENSYN, nullptr, 0, "Enable AUTO-SYN symbol generation"}, - - {nullptr, 0, nullptr, 0, "Daemon options:"}, - {"accesslevel", O_ACLDEF, "LEVEL", 0, "Set default access level to LEVEL (\"*\" for everything) [\"\"]"}, - {"aclfile", O_ACLFIL, "FILE", 0, "Read access control list from FILE"}, - {"foreground", 'f', nullptr, 0, "Run in foreground"}, - {"enablehex", O_HEXCMD, nullptr, 0, "Enable hex command"}, - {"enabledefine", O_DEFCMD, nullptr, 0, "Enable define command"}, - {"pidfile", O_PIDFIL, "FILE", 0, "PID file name (only for daemon) [" PACKAGE_PIDFILE "]"}, - {"port", 'p', "PORT", 0, "Listen for command line connections on PORT [8888]"}, - {"localhost", O_LOCAL, nullptr, 0, "Listen for command line connections on 127.0.0.1 interface only"}, - {"httpport", O_HTTPPT, "PORT", 0, "Listen for HTTP connections on PORT, 0 to disable [0]"}, - {"htmlpath", O_HTMLPA, "PATH", 0, "Path for HTML files served by HTTP port [/var/ebusd/html]"}, - {"updatecheck", O_UPDCHK, "MODE", 0, "Set automatic update check to MODE (on|off) [on]"}, - - {nullptr, 0, nullptr, 0, "Log options:"}, - {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon, empty string for using syslog) [" - PACKAGE_LOGFILE "]"}, - {"log", O_LOG, "AREAS:LEVEL", 0, "Only write log for matching AREA(S) below or equal to LEVEL" - " (alternative to --logareas/--logevel, may be used multiple times) [all:notice]"}, - {"logareas", O_LOGARE, "AREAS", 0, "Only write log for matching AREA(S): main|network|bus|update|other" - "|all [all]"}, - {"loglevel", O_LOGLEV, "LEVEL", 0, "Only write log below or equal to LEVEL: error|notice|info|debug" - " [notice]"}, - - {nullptr, 0, nullptr, 0, "Raw logging options:"}, - {"lograwdata", O_RAW, "bytes", af_optional, - "Log messages or all received/sent bytes on the bus"}, - {"lograwdatafile", O_RAWFIL, "FILE", 0, "Write raw log to FILE [" PACKAGE_LOGFILE "]"}, - {"lograwdatasize", O_RAWSIZ, "SIZE", 0, "Make raw log file no larger than SIZE kB [100]"}, - - {nullptr, 0, nullptr, 0, "Binary dump options:"}, - {"dump", 'D', nullptr, 0, "Enable binary dump of received bytes"}, - {"dumpfile", O_DMPFIL, "FILE", 0, "Dump received bytes to FILE [/tmp/" PACKAGE "_dump.bin]"}, - {"dumpsize", O_DMPSIZ, "SIZE", 0, "Make dump file no larger than SIZE kB [100]"}, - {"dumpflush", O_DMPFLU, nullptr, 0, "Flush each byte"}, - - {nullptr, 0, nullptr, 0, nullptr}, -}; - -/** - * The program argument parsing function. - * @param key the key from @a argDefs. - * @param arg the option argument, or nullptr. - * @param parseOpt the parse options. - */ -static int parse_opt(int key, char *arg, const argParseOpt *parseOpt) { - struct options *opt = (struct options*)parseOpt->userArg; - result_t result = RESULT_OK; - unsigned int value; - - switch (key) { - // Device options: - case 'd': // --device=/dev/ttyUSB0 - if (arg == nullptr || arg[0] == 0) { - argParseError(parseOpt, "invalid device"); - return EINVAL; - } - opt->device = arg; - break; - case 'n': // --nodevicecheck - opt->noDeviceCheck = true; - break; - case 'r': // --readonly - opt->readOnly = true; - break; - case O_INISND: // --initsend - opt->initialSend = true; - break; - case O_DEVLAT: // --latency=10 - value = parseInt(arg, 10, 0, 200000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 200)) { // backwards compatible (micros) - argParseError(parseOpt, "invalid latency"); - return EINVAL; - } - opt->extraLatency = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - - // Message configuration options: - case 'c': // --configpath=https://cfg.ebusd.eu/ - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid configpath"); - return EINVAL; - } - s_configPath = arg; - s_scanConfigOrPathSet = true; - break; - case 's': // --scanconfig[=ADDR] (ADDR=|none|full||off) - { - symbol_t initialScan = 0; - if (!arg || arg[0] == 0) { - initialScan = BROADCAST; // default for no or empty argument - } else if (strcmp("none", arg) == 0) { - initialScan = ESC; - } else if (strcmp("full", arg) == 0) { - initialScan = SYN; - } else if (strcmp("off", arg) == 0) { - // zero turns scanConfig off - } else { - auto address = (symbol_t)parseInt(arg, 16, 0x00, 0xff, &result); - if (result != RESULT_OK || !isValidAddress(address)) { - argParseError(parseOpt, "invalid initial scan address"); - return EINVAL; - } - if (isMaster(address)) { - initialScan = getSlaveAddress(address); - } else { - initialScan = address; - } - } - opt->scanConfig = initialScan != 0; - opt->initialScan = initialScan; - s_scanConfigOrPathSet = true; - break; - } - case O_SCNRET: // --scanretries=10 - value = parseInt(arg, 10, 0, 100, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid scanretries"); - return EINVAL; - } - opt->scanRetries = value; - break; - case O_CFGLNG: // --configlang=LANG - opt->preferLanguage = arg; - break; - case O_CHKCFG: // --checkconfig - opt->checkConfig = true; - break; - case O_DMPCFG: // --dumpconfig[=json|csv] - if (!arg || arg[0] == 0 || strcmp("csv", arg) == 0) { - // no further flags - opt->dumpConfig = OF_DEFINITION; - } else if (strcmp("json", arg) == 0) { - opt->dumpConfig = OF_DEFINITION | OF_NAMES | OF_UNITS | OF_COMMENTS | OF_VALUENAME | OF_ALL_ATTRS | OF_JSON; - } else { - argParseError(parseOpt, "invalid dumpconfig"); - return EINVAL; - } - opt->checkConfig = true; - break; - case O_DMPCTO: // --dumpconfigto=FILE - if (!arg || arg[0] == 0) { - argParseError(parseOpt, "invalid dumpconfigto"); - return EINVAL; - } - opt->dumpConfigTo = arg; - break; - case O_POLINT: // --pollinterval=5 - value = parseInt(arg, 10, 0, 3600, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid pollinterval"); - return EINVAL; - } - opt->pollInterval = value; - break; - case 'i': // --inject[=stop] - opt->injectMessages = true; - opt->stopAfterInject = arg && strcmp("stop", arg) == 0; - break; - case O_CAFILE: // --cafile=FILE - opt->caFile = arg; - break; - case O_CAPATH: // --capath=PATH - opt->caPath = arg; - break; - - // eBUS options: - case 'a': // --address=31 - { - auto address = (symbol_t)parseInt(arg, 16, 0, 0xff, &result); - if (result != RESULT_OK || !isMaster(address)) { - argParseError(parseOpt, "invalid address"); - return EINVAL; - } - opt->address = address; - break; - } - case O_ANSWER: // --answer - opt->answer = true; - break; - case O_ACQTIM: // --acquiretimeout=10 - value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) - argParseError(parseOpt, "invalid acquiretimeout"); - return EINVAL; - } - opt->acquireTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - case O_ACQRET: // --acquireretries=3 - value = parseInt(arg, 10, 0, 10, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid acquireretries"); - return EINVAL; - } - opt->acquireRetries = value; - break; - case O_SNDRET: // --sendretries=2 - value = parseInt(arg, 10, 0, 10, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid sendretries"); - return EINVAL; - } - opt->sendRetries = value; - break; - case O_RCVTIM: // --receivetimeout=25 - value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) - if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) - argParseError(parseOpt, "invalid receivetimeout"); - return EINVAL; - } - opt->receiveTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) - break; - case O_MASCNT: // --numbermasters=0 - value = parseInt(arg, 10, 0, 25, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid numbermasters"); - return EINVAL; - } - opt->masterCount = value; - break; - case O_GENSYN: // --generatesyn - opt->generateSyn = true; - break; - - // Daemon options: - case O_ACLDEF: // --accesslevel=* - if (arg == nullptr) { - argParseError(parseOpt, "invalid accesslevel"); - return EINVAL; - } - opt->accessLevel = arg; - break; - case O_ACLFIL: // --aclfile=/etc/ebusd/acl - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid aclfile"); - return EINVAL; - } - opt->aclFile = arg; - break; - case 'f': // --foreground - opt->foreground = true; - break; - case O_HEXCMD: // --enablehex - opt->enableHex = true; - break; - case O_DEFCMD: // --enabledefine - opt->enableDefine = true; - break; - case O_PIDFIL: // --pidfile=/var/run/ebusd.pid - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid pidfile"); - return EINVAL; - } - opt->pidFile = arg; - break; - case 'p': // --port=8888 - value = parseInt(arg, 10, 1, 65535, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid port"); - return EINVAL; - } - opt->port = (uint16_t)value; - break; - case O_LOCAL: // --localhost - opt->localOnly = true; - break; - case O_HTTPPT: // --httpport=0 - value = parseInt(arg, 10, 1, 65535, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid httpport"); - return EINVAL; - } - opt->httpPort = (uint16_t)value; - break; - case O_HTMLPA: // --htmlpath=/var/ebusd/html - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid htmlpath"); - return EINVAL; - } - opt->htmlPath = arg; - break; - case O_UPDCHK: // --updatecheck=on - if (arg == nullptr || arg[0] == 0) { - argParseError(parseOpt, "invalid updatecheck"); - return EINVAL; - } - if (strcmp("on", arg) == 0) { - opt->updateCheck = true; - } else if (strcmp("off", arg) == 0) { - opt->updateCheck = false; - } else { - argParseError(parseOpt, "invalid updatecheck"); - return EINVAL; - } - break; - - // Log options: - case 'l': // --logfile=/var/log/ebusd.log - if (arg == nullptr || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid logfile"); - return EINVAL; - } - opt->logFile = arg; - break; - case O_LOG: // --log=area(s):level - { - char* pos = strchr(arg, ':'); - if (pos == nullptr) { - pos = strchr(arg, ' '); - if (pos == nullptr) { - argParseError(parseOpt, "invalid log"); - return EINVAL; - } - } - *pos = 0; - int facilities = parseLogFacilities(arg); - if (facilities == -1) { - argParseError(parseOpt, "invalid log: areas"); - return EINVAL; - } - LogLevel level = parseLogLevel(pos + 1); - if (level == ll_COUNT) { - argParseError(parseOpt, "invalid log: level"); - return EINVAL; - } - if (opt->logAreas != -1 || opt->logLevel != ll_COUNT) { - argParseError(parseOpt, "invalid log (combined with logareas or loglevel)"); - return EINVAL; - } - setFacilitiesLogLevel(facilities, level); - opt->multiLog = true; - break; - } - case O_LOGARE: // --logareas=all - { - int facilities = parseLogFacilities(arg); - if (facilities == -1) { - argParseError(parseOpt, "invalid logareas"); - return EINVAL; - } - if (opt->multiLog) { - argParseError(parseOpt, "invalid logareas (combined with log)"); - return EINVAL; - } - opt->logAreas = facilities; - break; - } - case O_LOGLEV: // --loglevel=notice - { - LogLevel logLevel = parseLogLevel(arg); - if (logLevel == ll_COUNT) { - argParseError(parseOpt, "invalid loglevel"); - return EINVAL; - } - if (opt->multiLog) { - argParseError(parseOpt, "invalid loglevel (combined with log)"); - return EINVAL; - } - opt->logLevel = logLevel; - break; - } - - // Raw logging options: - case O_RAW: // --lograwdata - opt->logRaw = arg && strcmp("bytes", arg) == 0 ? 2 : 1; - break; - case O_RAWFIL: // --lograwdatafile=/var/log/ebusd.log - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid lograwdatafile"); - return EINVAL; - } - opt->logRawFile = arg; - break; - case O_RAWSIZ: // --lograwdatasize=100 - value = parseInt(arg, 10, 1, 1000000, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid lograwdatasize"); - return EINVAL; - } - opt->logRawSize = value; - break; - - - // Binary dump options: - case 'D': // --dump - opt->dump = true; - break; - case O_DMPFIL: // --dumpfile=/tmp/ebusd_dump.bin - if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { - argParseError(parseOpt, "invalid dumpfile"); - return EINVAL; - } - opt->dumpFile = arg; - break; - case O_DMPSIZ: // --dumpsize=100 - value = parseInt(arg, 10, 1, 1000000, &result); - if (result != RESULT_OK) { - argParseError(parseOpt, "invalid dumpsize"); - return EINVAL; - } - opt->dumpSize = value; - break; - case O_DMPFLU: // --dumpflush - opt->dumpFlush = true; - break; - - default: - return ESRCH; - } - - // check for invalid arg combinations - if (opt->readOnly && (opt->answer || opt->generateSyn || opt->initialSend - || (opt->scanConfig && opt->initialScan != ESC))) { - argParseError(parseOpt, "cannot combine readonly with answer/generatesyn/initsend/scanconfig"); - return EINVAL; - } - if (opt->scanConfig && opt->pollInterval == 0) { - argParseError(parseOpt, "scanconfig without polling may lead to invalid files included for certain products!"); - return EINVAL; - } - if (opt->injectMessages && (opt->checkConfig || opt->dumpConfig)) { - argParseError(parseOpt, "cannot combine inject with checkconfig/dumpconfig"); - return EINVAL; - } - return 0; -} - void shutdown(bool error = false); void daemonize() { @@ -806,62 +222,8 @@ void signalHandler(int sig) { * @return the exit code. */ int main(int argc, char* argv[], char* envp[]) { - const argParseOpt parseOpt = { - argDefs, - parse_opt, - 0, - "" PACKAGE_NAME, - "[INJECT...]", - "A daemon for communication with eBUS heating systems.", - "Report bugs to " PACKAGE_BUGREPORT " .", - datahandler_getargs(), - &s_opt - }; - - char envname[32] = "--"; // needs to cover at least max length of any option name plus "--" - char* envopt = envname+2; - for (char ** env = envp; *env; env++) { - char* pos = strchr(*env, '='); - if (!pos || strncmp(*env, "EBUSD_", sizeof("EBUSD_")-1) != 0) { - continue; - } - char* start = *env+sizeof("EBUSD_")-1; - size_t len = pos-start; - if (len <= 1 || len > sizeof(envname)-3) { // no single char long args - continue; - } - for (size_t i=0; i < len; i++) { - envopt[i] = static_cast(tolower(start[i])); - } - envopt[len] = 0; - if (strcmp(envopt, "version") == 0 || strcmp(envopt, "image") == 0 || strcmp(envopt, "arch") == 0 - || strcmp(envopt, "opts") == 0 || strcmp(envopt, "inject") == 0 - || strcmp(envopt, "checkconfig") == 0 || strncmp(envopt, "dumpconfig", 10) == 0 - ) { - // ignore those defined in Dockerfile, EBUSD_OPTS, those with final args, and interactive ones - continue; - } - char* envargv[] = {argv[0], envname, pos+1}; - int cnt = pos[1] ? 2 : 1; - if (pos[1] && strlen(*env) < sizeof(envname)-3 - && (strcmp(envopt, "scanconfig") == 0 || strcmp(envopt, "lograwdata") == 0)) { - // only really special case: OPTION_ARG_OPTIONAL with non-empty arg needs to use "=" syntax - cnt = 1; - strcat(envopt, pos); - } - int idx = -1; - int err = argParse(&parseOpt, 1+cnt, envargv, &idx); - if (err != 0 && idx == -1) { // ignore args for non-arg boolean options - if (err == ESRCH) { // special value to abort immediately - logWrite(lf_main, ll_error, "invalid argument in env: %s", *env); // force logging on exit - return EINVAL; - } - logWrite(lf_main, ll_error, "invalid/unknown argument in env (ignored): %s", *env); // force logging - } - } - int arg_index = -1; - switch (argParse(&parseOpt, argc, argv, &arg_index)) { + switch (parse_main_args(argc, argv, envp, &s_opt, &arg_index)) { case 0: // OK break; case '?': // help printed @@ -886,35 +248,29 @@ int main(int argc, char* argv[], char* envp[]) { setFacilitiesLogLevel(s_opt.logAreas, s_opt.logLevel); } - if (!s_opt.readOnly && !s_scanConfigOrPathSet) { - s_opt.scanConfig = true; - s_opt.initialScan = BROADCAST; - } - if (!s_configPath.empty() && s_configPath[s_configPath.length()-1] != '/') { - s_configPath += "/"; - } const string lang = MappedFileReader::normalizeLanguage( s_opt.preferLanguage == nullptr || !s_opt.preferLanguage[0] ? "" : s_opt.preferLanguage); string configLocalPrefix, configUriPrefix; HttpClient::initialize(s_opt.caFile, s_opt.caPath); HttpClient* configHttpClient = nullptr; - if (s_configPath.find("://") == string::npos) { - configLocalPrefix = s_configPath; + string configPath = s_opt.configPath; + if (configPath.find("://") == string::npos) { + configLocalPrefix = s_opt.configPath; } else { if (!s_opt.scanConfig) { logWrite(lf_main, ll_error, "invalid configpath without scanconfig"); // force logging on exit return EINVAL; } - size_t pos = s_configPath.find(PREVIOUS_CONFIG_PATH_SUFFIX); + size_t pos = configPath.find(PREVIOUS_CONFIG_PATH_SUFFIX); if (pos != string::npos) { - string newPath = s_configPath.substr(0, pos) + CONFIG_PATH_SUFFIX - + s_configPath.substr(pos+strlen(PREVIOUS_CONFIG_PATH_SUFFIX)); - logNotice(lf_main, "replaced old configPath %s with new one: %s", s_configPath.c_str(), newPath.c_str()); - s_configPath = newPath; + string newPath = configPath.substr(0, pos) + CONFIG_PATH_SUFFIX + + configPath.substr(pos+strlen(PREVIOUS_CONFIG_PATH_SUFFIX)); + logNotice(lf_main, "replaced old configPath %s with new one: %s", s_opt.configPath, newPath.c_str()); + configPath = newPath; } uint16_t configPort = 80; string proto, configHost; - if (!HttpClient::parseUrl(s_configPath, &proto, &configHost, &configPort, &configUriPrefix)) { + if (!HttpClient::parseUrl(configPath, &proto, &configHost, &configPort, &configUriPrefix)) { #ifndef HAVE_SSL if (proto == "https") { logWrite(lf_main, ll_error, "invalid configPath URL (HTTPS not supported)"); // force logging on exit @@ -940,7 +296,7 @@ int main(int argc, char* argv[], char* envp[]) { } s_messageMap = new MessageMap(s_opt.checkConfig, lang); - s_scanHelper = new ScanHelper(s_messageMap, s_configPath, configLocalPrefix, configUriPrefix, + s_scanHelper = new ScanHelper(s_messageMap, configPath, configLocalPrefix, configUriPrefix, lang.empty() ? lang : "?l=" + lang, configHttpClient, s_opt.checkConfig); s_messageMap->setResolver(s_scanHelper); if (s_opt.checkConfig) { diff --git a/src/ebusd/main.h b/src/ebusd/main.h old mode 100644 new mode 100755 index a085d0e3..03543e24 --- a/src/ebusd/main.h +++ b/src/ebusd/main.h @@ -32,6 +32,9 @@ namespace ebusd { * The main entry method doing all the startup handling. */ +/** the config path part behind the scheme (scheme without "://"). */ +#define CONFIG_PATH_SUFFIX "://cfg.ebusd.eu/" + /** A structure holding all program options. */ typedef struct options { const char* device; //!< eBUS device (serial device or [udp:]ip:port) [/dev/ttyUSB0] @@ -40,6 +43,8 @@ typedef struct options { bool initialSend; //!< send an initial escape symbol after connecting device unsigned int extraLatency; //!< extra transfer latency in ms [0 for USB, 10 for IP] + const char* configPath; //!< the configuration files path or URL + bool scanConfigOrPathSet; //!< whether scanConfig or configPath were set by arguments. bool scanConfig; //!< pick configuration files matching initial scan /** * initial address(es) to scan: @@ -96,6 +101,18 @@ typedef struct options { bool dumpFlush; //!< flush each byte } options_t; +/** + * Parse the main command line arguments in @a argv. + * @param argc the number of command line arguments. + * @param argv the command line arguments. + * @param envp the environment variables to parse before the args, or nullptr. + * @param opt pointer to the parsed arguments (will be initialized to defaults first). + * @param argIndex optional pointer for storing the index to the first non-argument found in argv. + * @return 0 on success, '!' for an invalid argument value, ':' for a missing argument value, + * '?' when "-?" was given, or the result of the parse function if non-zero. + */ +int parse_main_args(int argc, char* argv[], char* envp[], options_t* opt, int* argIndex); + } // namespace ebusd #endif // EBUSD_MAIN_H_ diff --git a/src/ebusd/main_args.cpp b/src/ebusd/main_args.cpp new file mode 100755 index 00000000..e7ad5ef5 --- /dev/null +++ b/src/ebusd/main_args.cpp @@ -0,0 +1,695 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2023 John Baier + * + * 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, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "ebusd/main.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "ebusd/datahandler.h" +#include "lib/utils/log.h" +#include "lib/utils/arg.h" +#include "ebusd/scan.h" + +namespace ebusd { + +/** the default path of the configuration files. */ +#ifdef HAVE_SSL +#define CONFIG_PATH "https" CONFIG_PATH_SUFFIX +#else +#define CONFIG_PATH "http" CONFIG_PATH_SUFFIX +#endif + +/** the default program options. */ +static const options_t s_default_opt = { + .device = "/dev/ttyUSB0", + .noDeviceCheck = false, + .readOnly = false, + .initialSend = false, + .extraLatency = 0, + + .configPath = nullptr, + .scanConfigOrPathSet = false, + .scanConfig = false, + .initialScan = 0, + .scanRetries = 5, + .preferLanguage = getenv("LANG"), + .checkConfig = false, + .dumpConfig = OF_NONE, + .dumpConfigTo = nullptr, + .pollInterval = 5, + .injectMessages = false, + .stopAfterInject = false, + .caFile = nullptr, + .caPath = nullptr, + + .address = 0x31, + .answer = false, + .acquireTimeout = 10, + .acquireRetries = 3, + .sendRetries = 2, + .receiveTimeout = SLAVE_RECV_TIMEOUT*5/3, + .masterCount = 0, + .generateSyn = false, + + .accessLevel = "", + .aclFile = "", + .foreground = false, + .enableHex = false, + .enableDefine = false, + .pidFile = PACKAGE_PIDFILE, + .port = 8888, + .localOnly = false, + .httpPort = 0, + .htmlPath = "/var/" PACKAGE "/html", + .updateCheck = true, + + .logFile = PACKAGE_LOGFILE, + .logAreas = -1, + .logLevel = ll_COUNT, + .multiLog = false, + + .logRaw = 0, + .logRawFile = PACKAGE_LOGFILE, + .logRawSize = 100, + + .dump = false, + .dumpFile = "/tmp/" PACKAGE "_dump.bin", + .dumpSize = 100, + .dumpFlush = false +}; + +/** the (optionally corrected) config path for retrieving configuration files from. */ +static string s_configPath = CONFIG_PATH; + +#define O_INISND -2 +#define O_DEVLAT (O_INISND-1) +#define O_SCNRET (O_DEVLAT-1) +#define O_CFGLNG (O_SCNRET-1) +#define O_CHKCFG (O_CFGLNG-1) +#define O_DMPCFG (O_CHKCFG-1) +#define O_DMPCTO (O_DMPCFG-1) +#define O_POLINT (O_DMPCTO-1) +#define O_CAFILE (O_POLINT-1) +#define O_CAPATH (O_CAFILE-1) +#define O_ANSWER (O_CAPATH-1) +#define O_ACQTIM (O_ANSWER-1) +#define O_ACQRET (O_ACQTIM-1) +#define O_SNDRET (O_ACQRET-1) +#define O_RCVTIM (O_SNDRET-1) +#define O_MASCNT (O_RCVTIM-1) +#define O_GENSYN (O_MASCNT-1) +#define O_ACLDEF (O_GENSYN-1) +#define O_ACLFIL (O_ACLDEF-1) +#define O_HEXCMD (O_ACLFIL-1) +#define O_DEFCMD (O_HEXCMD-1) +#define O_PIDFIL (O_DEFCMD-1) +#define O_LOCAL (O_PIDFIL-1) +#define O_HTTPPT (O_LOCAL-1) +#define O_HTMLPA (O_HTTPPT-1) +#define O_UPDCHK (O_HTMLPA-1) +#define O_LOG (O_UPDCHK-1) +#define O_LOGARE (O_LOG-1) +#define O_LOGLEV (O_LOGARE-1) +#define O_RAW (O_LOGLEV-1) +#define O_RAWFIL (O_RAW-1) +#define O_RAWSIZ (O_RAWFIL-1) +#define O_DMPFIL (O_RAWSIZ-1) +#define O_DMPSIZ (O_DMPFIL-1) +#define O_DMPFLU (O_DMPSIZ-1) + +/** the definition of the known program arguments. */ +static const argDef argDefs[] = { + {nullptr, 0, nullptr, 0, "Device options:"}, + {"device", 'd', "DEV", 0, "Use DEV as eBUS device (" + "prefix \"ens:\" for enhanced high speed device or " + "\"enh:\" for enhanced device, with " + "\"IP:PORT\" for network device or " + "\"DEVICE\" for serial device" + ") [/dev/ttyUSB0]"}, + {"nodevicecheck", 'n', nullptr, 0, "Skip serial eBUS device test"}, + {"readonly", 'r', nullptr, 0, "Only read from device, never write to it"}, + {"initsend", O_INISND, nullptr, 0, "Send an initial escape symbol after connecting device"}, + {"latency", O_DEVLAT, "MSEC", 0, "Extra transfer latency in ms [0]"}, + + {nullptr, 0, nullptr, 0, "Message configuration options:"}, + {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTPS URL) [" + CONFIG_PATH "]"}, + {"scanconfig", 's', "ADDR", af_optional, "Pick CSV config files matching initial scan ADDR: " + "empty for broadcast ident message (default when configpath is not given), " + "\"none\" for no initial scan message, " + "\"full\" for full scan, " + "a single hex address to scan, or " + "\"off\" for not picking CSV files by scan result (default when configpath is given).\n" + "If combined with --checkconfig, you can add scan message data as " + "arguments for checking a particular scan configuration, e.g. \"FF08070400/0AB5454850303003277201\"."}, + {"scanretries", O_SCNRET, "COUNT", 0, "Retry scanning devices COUNT times [5]"}, + {"configlang", O_CFGLNG, "LANG", 0, + "Prefer LANG in multilingual configuration files [system default language]"}, + {"checkconfig", O_CHKCFG, nullptr, 0, "Check config files, then stop"}, + {"dumpconfig", O_DMPCFG, "FORMAT", af_optional, + "Check and dump config files in FORMAT (\"json\" or \"csv\"), then stop"}, + {"dumpconfigto", O_DMPCTO, "FILE", 0, "Dump config files to FILE"}, + {"pollinterval", O_POLINT, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]"}, + {"inject", 'i', "stop", af_optional, "Inject remaining arguments as already seen messages (e.g. " + "\"FF08070400/0AB5454850303003277201\"), optionally stop afterwards"}, +#ifdef HAVE_SSL + {"cafile", O_CAFILE, "FILE", 0, "Use CA FILE for checking certificates (uses defaults," + " \"#\" for insecure)"}, + {"capath", O_CAPATH, "PATH", 0, "Use CA PATH for checking certificates (uses defaults)"}, +#endif // HAVE_SSL + + {nullptr, 0, nullptr, 0, "eBUS options:"}, + {"address", 'a', "ADDR", 0, "Use hex ADDR as own master bus address [31]"}, + {"answer", O_ANSWER, nullptr, 0, "Actively answer to requests from other masters"}, + {"acquiretimeout", O_ACQTIM, "MSEC", 0, "Stop bus acquisition after MSEC ms [10]"}, + {"acquireretries", O_ACQRET, "COUNT", 0, "Retry bus acquisition COUNT times [3]"}, + {"sendretries", O_SNDRET, "COUNT", 0, "Repeat failed sends COUNT times [2]"}, + {"receivetimeout", O_RCVTIM, "MSEC", 0, "Expect a slave to answer within MSEC ms [25]"}, + {"numbermasters", O_MASCNT, "COUNT", 0, "Expect COUNT masters on the bus, 0 for auto detection [0]"}, + {"generatesyn", O_GENSYN, nullptr, 0, "Enable AUTO-SYN symbol generation"}, + + {nullptr, 0, nullptr, 0, "Daemon options:"}, + {"accesslevel", O_ACLDEF, "LEVEL", 0, "Set default access level to LEVEL (\"*\" for everything) [\"\"]"}, + {"aclfile", O_ACLFIL, "FILE", 0, "Read access control list from FILE"}, + {"foreground", 'f', nullptr, 0, "Run in foreground"}, + {"enablehex", O_HEXCMD, nullptr, 0, "Enable hex command"}, + {"enabledefine", O_DEFCMD, nullptr, 0, "Enable define command"}, + {"pidfile", O_PIDFIL, "FILE", 0, "PID file name (only for daemon) [" PACKAGE_PIDFILE "]"}, + {"port", 'p', "PORT", 0, "Listen for command line connections on PORT [8888]"}, + {"localhost", O_LOCAL, nullptr, 0, "Listen for command line connections on 127.0.0.1 interface only"}, + {"httpport", O_HTTPPT, "PORT", 0, "Listen for HTTP connections on PORT, 0 to disable [0]"}, + {"htmlpath", O_HTMLPA, "PATH", 0, "Path for HTML files served by HTTP port [/var/ebusd/html]"}, + {"updatecheck", O_UPDCHK, "MODE", 0, "Set automatic update check to MODE (on|off) [on]"}, + + {nullptr, 0, nullptr, 0, "Log options:"}, + {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon, empty string for using syslog) [" + PACKAGE_LOGFILE "]"}, + {"log", O_LOG, "AREAS:LEVEL", 0, "Only write log for matching AREA(S) below or equal to LEVEL" + " (alternative to --logareas/--logevel, may be used multiple times) [all:notice]"}, + {"logareas", O_LOGARE, "AREAS", 0, "Only write log for matching AREA(S): main|network|bus|update|other" + "|all [all]"}, + {"loglevel", O_LOGLEV, "LEVEL", 0, "Only write log below or equal to LEVEL: error|notice|info|debug" + " [notice]"}, + + {nullptr, 0, nullptr, 0, "Raw logging options:"}, + {"lograwdata", O_RAW, "bytes", af_optional, + "Log messages or all received/sent bytes on the bus"}, + {"lograwdatafile", O_RAWFIL, "FILE", 0, "Write raw log to FILE [" PACKAGE_LOGFILE "]"}, + {"lograwdatasize", O_RAWSIZ, "SIZE", 0, "Make raw log file no larger than SIZE kB [100]"}, + + {nullptr, 0, nullptr, 0, "Binary dump options:"}, + {"dump", 'D', nullptr, 0, "Enable binary dump of received bytes"}, + {"dumpfile", O_DMPFIL, "FILE", 0, "Dump received bytes to FILE [/tmp/" PACKAGE "_dump.bin]"}, + {"dumpsize", O_DMPSIZ, "SIZE", 0, "Make dump file no larger than SIZE kB [100]"}, + {"dumpflush", O_DMPFLU, nullptr, 0, "Flush each byte"}, + + {nullptr, 0, nullptr, 0, nullptr}, +}; + +/** + * The program argument parsing function. + * @param key the key from @a argDefs. + * @param arg the option argument, or nullptr. + * @param parseOpt the parse options. + */ +static int parse_opt(int key, char *arg, const argParseOpt *parseOpt) { + struct options *opt = (struct options*)parseOpt->userArg; + result_t result = RESULT_OK; + unsigned int value; + + switch (key) { + // Device options: + case 'd': // --device=/dev/ttyUSB0 + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid device"); + return EINVAL; + } + opt->device = arg; + break; + case 'n': // --nodevicecheck + opt->noDeviceCheck = true; + break; + case 'r': // --readonly + opt->readOnly = true; + break; + case O_INISND: // --initsend + opt->initialSend = true; + break; + case O_DEVLAT: // --latency=10 + value = parseInt(arg, 10, 0, 200000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 200)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid latency"); + return EINVAL; + } + opt->extraLatency = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + + // Message configuration options: + case 'c': // --configpath=https://cfg.ebusd.eu/ + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid configpath"); + return EINVAL; + } + s_configPath = arg; + opt->scanConfigOrPathSet = true; + break; + case 's': // --scanconfig[=ADDR] (ADDR=|none|full||off) + { + symbol_t initialScan = 0; + if (!arg || arg[0] == 0) { + initialScan = BROADCAST; // default for no or empty argument + } else if (strcmp("none", arg) == 0) { + initialScan = ESC; + } else if (strcmp("full", arg) == 0) { + initialScan = SYN; + } else if (strcmp("off", arg) == 0) { + // zero turns scanConfig off + } else { + auto address = (symbol_t)parseInt(arg, 16, 0x00, 0xff, &result); + if (result != RESULT_OK || !isValidAddress(address)) { + argParseError(parseOpt, "invalid initial scan address"); + return EINVAL; + } + if (isMaster(address)) { + initialScan = getSlaveAddress(address); + } else { + initialScan = address; + } + } + opt->scanConfig = initialScan != 0; + opt->initialScan = initialScan; + opt->scanConfigOrPathSet = true; + break; + } + case O_SCNRET: // --scanretries=10 + value = parseInt(arg, 10, 0, 100, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid scanretries"); + return EINVAL; + } + opt->scanRetries = value; + break; + case O_CFGLNG: // --configlang=LANG + opt->preferLanguage = arg; + break; + case O_CHKCFG: // --checkconfig + opt->checkConfig = true; + break; + case O_DMPCFG: // --dumpconfig[=json|csv] + if (!arg || arg[0] == 0 || strcmp("csv", arg) == 0) { + // no further flags + opt->dumpConfig = OF_DEFINITION; + } else if (strcmp("json", arg) == 0) { + opt->dumpConfig = OF_DEFINITION | OF_NAMES | OF_UNITS | OF_COMMENTS | OF_VALUENAME | OF_ALL_ATTRS | OF_JSON; + } else { + argParseError(parseOpt, "invalid dumpconfig"); + return EINVAL; + } + opt->checkConfig = true; + break; + case O_DMPCTO: // --dumpconfigto=FILE + if (!arg || arg[0] == 0) { + argParseError(parseOpt, "invalid dumpconfigto"); + return EINVAL; + } + opt->dumpConfigTo = arg; + break; + case O_POLINT: // --pollinterval=5 + value = parseInt(arg, 10, 0, 3600, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid pollinterval"); + return EINVAL; + } + opt->pollInterval = value; + break; + case 'i': // --inject[=stop] + opt->injectMessages = true; + opt->stopAfterInject = arg && strcmp("stop", arg) == 0; + break; + case O_CAFILE: // --cafile=FILE + opt->caFile = arg; + break; + case O_CAPATH: // --capath=PATH + opt->caPath = arg; + break; + + // eBUS options: + case 'a': // --address=31 + { + auto address = (symbol_t)parseInt(arg, 16, 0, 0xff, &result); + if (result != RESULT_OK || !isMaster(address)) { + argParseError(parseOpt, "invalid address"); + return EINVAL; + } + opt->address = address; + break; + } + case O_ANSWER: // --answer + opt->answer = true; + break; + case O_ACQTIM: // --acquiretimeout=10 + value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid acquiretimeout"); + return EINVAL; + } + opt->acquireTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + case O_ACQRET: // --acquireretries=3 + value = parseInt(arg, 10, 0, 10, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid acquireretries"); + return EINVAL; + } + opt->acquireRetries = value; + break; + case O_SNDRET: // --sendretries=2 + value = parseInt(arg, 10, 0, 10, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid sendretries"); + return EINVAL; + } + opt->sendRetries = value; + break; + case O_RCVTIM: // --receivetimeout=25 + value = parseInt(arg, 10, 1, 100000, &result); // backwards compatible (micros) + if (result != RESULT_OK || (value <= 1000 && value > 100)) { // backwards compatible (micros) + argParseError(parseOpt, "invalid receivetimeout"); + return EINVAL; + } + opt->receiveTimeout = value > 1000 ? value/1000 : value; // backwards compatible (micros) + break; + case O_MASCNT: // --numbermasters=0 + value = parseInt(arg, 10, 0, 25, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid numbermasters"); + return EINVAL; + } + opt->masterCount = value; + break; + case O_GENSYN: // --generatesyn + opt->generateSyn = true; + break; + + // Daemon options: + case O_ACLDEF: // --accesslevel=* + if (arg == nullptr) { + argParseError(parseOpt, "invalid accesslevel"); + return EINVAL; + } + opt->accessLevel = arg; + break; + case O_ACLFIL: // --aclfile=/etc/ebusd/acl + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid aclfile"); + return EINVAL; + } + opt->aclFile = arg; + break; + case 'f': // --foreground + opt->foreground = true; + break; + case O_HEXCMD: // --enablehex + opt->enableHex = true; + break; + case O_DEFCMD: // --enabledefine + opt->enableDefine = true; + break; + case O_PIDFIL: // --pidfile=/var/run/ebusd.pid + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid pidfile"); + return EINVAL; + } + opt->pidFile = arg; + break; + case 'p': // --port=8888 + value = parseInt(arg, 10, 1, 65535, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid port"); + return EINVAL; + } + opt->port = (uint16_t)value; + break; + case O_LOCAL: // --localhost + opt->localOnly = true; + break; + case O_HTTPPT: // --httpport=0 + value = parseInt(arg, 10, 1, 65535, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid httpport"); + return EINVAL; + } + opt->httpPort = (uint16_t)value; + break; + case O_HTMLPA: // --htmlpath=/var/ebusd/html + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid htmlpath"); + return EINVAL; + } + opt->htmlPath = arg; + break; + case O_UPDCHK: // --updatecheck=on + if (arg == nullptr || arg[0] == 0) { + argParseError(parseOpt, "invalid updatecheck"); + return EINVAL; + } + if (strcmp("on", arg) == 0) { + opt->updateCheck = true; + } else if (strcmp("off", arg) == 0) { + opt->updateCheck = false; + } else { + argParseError(parseOpt, "invalid updatecheck"); + return EINVAL; + } + break; + + // Log options: + case 'l': // --logfile=/var/log/ebusd.log + if (arg == nullptr || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid logfile"); + return EINVAL; + } + opt->logFile = arg; + break; + case O_LOG: // --log=area(s):level + { + char* pos = strchr(arg, ':'); + if (pos == nullptr) { + pos = strchr(arg, ' '); + if (pos == nullptr) { + argParseError(parseOpt, "invalid log"); + return EINVAL; + } + } + *pos = 0; + int facilities = parseLogFacilities(arg); + if (facilities == -1) { + argParseError(parseOpt, "invalid log: areas"); + return EINVAL; + } + LogLevel level = parseLogLevel(pos + 1); + if (level == ll_COUNT) { + argParseError(parseOpt, "invalid log: level"); + return EINVAL; + } + if (opt->logAreas != -1 || opt->logLevel != ll_COUNT) { + argParseError(parseOpt, "invalid log (combined with logareas or loglevel)"); + return EINVAL; + } + setFacilitiesLogLevel(facilities, level); + opt->multiLog = true; + break; + } + case O_LOGARE: // --logareas=all + { + int facilities = parseLogFacilities(arg); + if (facilities == -1) { + argParseError(parseOpt, "invalid logareas"); + return EINVAL; + } + if (opt->multiLog) { + argParseError(parseOpt, "invalid logareas (combined with log)"); + return EINVAL; + } + opt->logAreas = facilities; + break; + } + case O_LOGLEV: // --loglevel=notice + { + LogLevel logLevel = parseLogLevel(arg); + if (logLevel == ll_COUNT) { + argParseError(parseOpt, "invalid loglevel"); + return EINVAL; + } + if (opt->multiLog) { + argParseError(parseOpt, "invalid loglevel (combined with log)"); + return EINVAL; + } + opt->logLevel = logLevel; + break; + } + + // Raw logging options: + case O_RAW: // --lograwdata + opt->logRaw = arg && strcmp("bytes", arg) == 0 ? 2 : 1; + break; + case O_RAWFIL: // --lograwdatafile=/var/log/ebusd.log + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid lograwdatafile"); + return EINVAL; + } + opt->logRawFile = arg; + break; + case O_RAWSIZ: // --lograwdatasize=100 + value = parseInt(arg, 10, 1, 1000000, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid lograwdatasize"); + return EINVAL; + } + opt->logRawSize = value; + break; + + // Binary dump options: + case 'D': // --dump + opt->dump = true; + break; + case O_DMPFIL: // --dumpfile=/tmp/ebusd_dump.bin + if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { + argParseError(parseOpt, "invalid dumpfile"); + return EINVAL; + } + opt->dumpFile = arg; + break; + case O_DMPSIZ: // --dumpsize=100 + value = parseInt(arg, 10, 1, 1000000, &result); + if (result != RESULT_OK) { + argParseError(parseOpt, "invalid dumpsize"); + return EINVAL; + } + opt->dumpSize = value; + break; + case O_DMPFLU: // --dumpflush + opt->dumpFlush = true; + break; + + default: + return ESRCH; + } + + // check for invalid arg combinations + if (opt->readOnly && (opt->answer || opt->generateSyn || opt->initialSend + || (opt->scanConfig && opt->initialScan != ESC))) { + argParseError(parseOpt, "cannot combine readonly with answer/generatesyn/initsend/scanconfig"); + return EINVAL; + } + if (opt->scanConfig && opt->pollInterval == 0) { + argParseError(parseOpt, "scanconfig without polling may lead to invalid files included for certain products!"); + return EINVAL; + } + if (opt->injectMessages && (opt->checkConfig || opt->dumpConfig)) { + argParseError(parseOpt, "cannot combine inject with checkconfig/dumpconfig"); + return EINVAL; + } + return 0; +} + +int parse_main_args(int argc, char* argv[], char* envp[], options_t *opt, int *arg_index) { + *opt = s_default_opt; + const argParseOpt parseOpt = { + argDefs, + parse_opt, + 0, + "" PACKAGE_NAME, + "[INJECT...]", + "A daemon for communication with eBUS heating systems.", + "Report bugs to " PACKAGE_BUGREPORT " .", + datahandler_getargs(), + opt + }; + + char envname[32] = "--"; // needs to cover at least max length of any option name plus "--" + char* envopt = envname+2; + for (char ** env = envp; *env; env++) { + char* pos = strchr(*env, '='); + if (!pos || strncmp(*env, "EBUSD_", sizeof("EBUSD_")-1) != 0) { + continue; + } + char* start = *env+sizeof("EBUSD_")-1; + size_t len = pos-start; + if (len <= 1 || len > sizeof(envname)-3) { // no single char long args + continue; + } + for (size_t i=0; i < len; i++) { + envopt[i] = static_cast(tolower(start[i])); + } + envopt[len] = 0; + if (strcmp(envopt, "version") == 0 || strcmp(envopt, "image") == 0 || strcmp(envopt, "arch") == 0 + || strcmp(envopt, "opts") == 0 || strcmp(envopt, "inject") == 0 + || strcmp(envopt, "checkconfig") == 0 || strncmp(envopt, "dumpconfig", 10) == 0 + ) { + // ignore those defined in Dockerfile, EBUSD_OPTS, those with final args, and interactive ones + continue; + } + char* envargv[] = {argv[0], envname, pos+1}; + int cnt = pos[1] ? 2 : 1; + if (pos[1] && strlen(*env) < sizeof(envname)-3 + && (strcmp(envopt, "scanconfig") == 0 || strcmp(envopt, "lograwdata") == 0)) { + // only really special case: OPTION_ARG_OPTIONAL with non-empty arg needs to use "=" syntax + cnt = 1; + strcat(envopt, pos); + } + int idx = -1; + int err = argParse(&parseOpt, 1+cnt, envargv, &idx); + if (err != 0 && idx == -1) { // ignore args for non-arg boolean options + if (err == ESRCH) { // special value to abort immediately + logWrite(lf_main, ll_error, "invalid argument in env: %s", *env); // force logging on exit + return EINVAL; + } + logWrite(lf_main, ll_error, "invalid/unknown argument in env (ignored): %s", *env); // force logging + } + } + + *arg_index = -1; + int ret = argParse(&parseOpt, argc, argv, arg_index); + if (ret != 0) { + return ret; + } + + if (!opt->readOnly && !opt->scanConfigOrPathSet) { + opt->scanConfig = true; + opt->initialScan = BROADCAST; + } + if (!s_configPath.empty()) { + if (s_configPath[s_configPath.length()-1] != '/') { + s_configPath += "/"; + } + opt->configPath = s_configPath.c_str(); // OK as s_configPath is kept + } + return 0; +} + +} // namespace ebusd