/* * Copyright - See the COPYRIGHT that is included with this distribution. * pvxs is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. * * qsrvMain.cpp: The main entry point for the pvxs qsrv soft IOC. * Use this as is, or as the base for your customised IOC application */ #include #include "epicsExit.h" #include "epicsThread.h" #include "iocsh.h" #include #include #include #include #include #include #include "pvxs/iochooks.h" #include "qsrvMain.h" namespace pvxs { namespace qsrv { static void exitCallback(subRecord* pRecord); // Verbose flag - if true then show verbose output bool verboseFlag = false; // Macro to use to show verbose output // e.g. VERBOSE_MESSAGE "some verbose message: " << variable << " more verbosity\n"; #define VERBOSE_MESSAGE if (verboseFlag) std::cout << // DB Loaded flag - true if the database has already been loaded bool isDbLoaded = false; /** * Print out usage information for the QSRV application * * @param executableName the name of the executable as passed to main * @param initialisationFile the name of the database initialization file. Either the one * specified on the commandline or the default one */ void usage(std::string& executableName, const std::string& initialisationFile) { std::string executableBaseName = executableName .substr(executableName.find_last_of(OSI_PATH_SEPARATOR) + 1) // NOLINT(performance-faster-string-find) ; std::cout << "PVXS configurable IOC server\n\n" "Usage: " << executableBaseName << " [-h] [-S] [-v] \n" " [-m =[,=]...] ... \n" " [-D ] [-G ] [-a ] [-d ] \n" " [-x ] []\n" "\nDescription:\n" " Start an in-memory database of PV records which can be accessed via PVAccess, and start an IOC shell.\n\n" " After configuring the in-memory database with " << initialisationFile.c_str() << "\n (or overriden with the -D option) this command starts an interactive IOC shell, unless the -S flag \n" " is specified. Group configuration can optionally be specified using the -G flag, and security can be \n" " configured using the -a flag. An initial database of PV records can be established using the -d flag. \n" " Finally some startup commands can be run if an optional script-path is specified." "\n" "\nCommon flags:\n" " -h Print this message and exit.\n" " -S Prevents an interactive shell being started. \n" " -v Verbose, display steps taken during startup.\n" " -D This overrides the default database configuration file.\n" " If specified, it must come before any (-G), (-a), or (-d) flags. Specify \n" " the path to the configuration file as well as the\n" " extension. By convention, this file has a .dbd \n" " extension. The compile-time default configuration file\n" " is " FULL_PATH_TO_INITIALISATION_FILE ".\n" " -m =[,=]... \n" " Set/replace macro definitions. Macros are used in the \n" " iocsh as well as when parsing the access security configuration \n" " (-a) and database records file (-d). You can provide a (-m) flag \n" " multiple times on the same command line. Each occurrence applies to any \n" " (-a) and (-d) options that follow it until the next (-m) overrides. The last\n" " macros defined are used in the iocsh\n" " -a Access security configuration file. Use this flag \n" " to configure access security. The security definitions specified \n" " are subject to macro substitution.\n" " -d Load database record-definitions and group-definitions from file. Each record-definition " " contains a set of field-definitions: `record(,\"\") { field(,\"\")... }...`. " " Additionally group-info-definitions are accepted in the place of field-definitions.\n" " group-info-definition := `info(Q:group, {\"\": {...}})`. \n" " These group-info-definitions define group-names and map into them the \n" " specified fields from the record-definition in which they appear. By convention, \n" " the extension is .db. The definitions specified in the given file are subject to macro substitution \n" " -G Load database group-definitions from a JSON file. If path starts with `-` then the remaining portion \n" " is treated as a group-definitions file to be removed from the list of files to load. \n" " e.g. `-G -grpFile.json` will remove grpFile.json from the list of group-definitions \n" " files to load. If `-G -*` or `-G -` is specified, then all group-definitions files \n" " that have been specified so far will be removed. By convention, the extension is .json. \n" " -x Specifies the prefix for the database exit record Load. It is used as a substitution for \n" " the $(IOC) macro in " FULL_PATH_TO_EXIT_FILE ". \n" " This file is used to configure database shutdown.\n" " script-path A command script is optional. The iocsh commands will be run *after*\n" " calling iocInit(). If you want to run the script before iocInit() then \n" " don't specify any (-d), (-G) or (-x) flags and perform all database loading in \n" " the script itself, or in the interactive shell including calling iocInit().\n" "\n" "Examples:\n" " " << executableBaseName << " -d my.db\n" " use default configuration, load database record-definitions \n" " and group-definitions from `my.db`, and start an interactive IOC shell \n" " " << executableBaseName << " -m NAME=PV -d my.db\n" " use default configuration, and load database record-definitions \n" " and group-definitions from `my.db`, after setting macro `NAME` to `PV`\n" " " << executableBaseName << " -D my-config.dbd -d my.db -G my-group-mappings.json \n" " use custom configuration `my-config.dbd` to configure the IOC, \n" " load database record-definitions and group-definitions from `my.db`, \n" " then load additional group-definitions from `my-group-mappings.json,` \n" " and start an interactive shell \n"; } /** * Configure the database if it has not been configured previously * * @param databaseConfigurationFile the name of the file containing configuration information */ void configureDatabase(const std::string& databaseConfigurationFile) { // Only load configuration file if it has been configured previously if (isDbLoaded) { return; } isDbLoaded = true; VERBOSE_MESSAGE "dbLoadDatabase(\"" << databaseConfigurationFile << "\")\n"; if (dbLoadDatabase(databaseConfigurationFile.c_str(), nullptr, nullptr)) { throw std::runtime_error( std::string("Failed to load database configuration file: ") + databaseConfigurationFile); } // Must match the dbd you've established in your header file and Makefile as the default configuration file VERBOSE_MESSAGE "qsrv_registerRecordDeviceDriver(pdbbase)\n"; qsrv_registerRecordDeviceDriver(pdbbase); registryFunctionAdd("exit", (REGISTRYFUNCTION)exitCallback); } /** * The exit callback function * * @param pRecord */ void exitCallback(subRecord* pRecord) { epicsExitLater((pRecord->a == 0.0) ? EXIT_SUCCESS : EXIT_FAILURE); } /** * Get the relative path prefix (the directory that this executable is running from) * * @return the prefix to add to any relative paths */ std::string getPrefix() { std::string prefix; char* cPrefix = epicsGetExecDir(); if (cPrefix) { try { prefix = cPrefix; } catch (...) { free(cPrefix); throw; } } free(cPrefix); return prefix; } /** * Parse the command line arguments * * @param argc argument count * @param argv argument values * @param databaseInitialisationFile the default database initialization file, overridden by (-D) option if specified * @param dbIsLoaded set to true if database is loaded by a (-d) or (-x) option * @param shouldStartAnInteractiveSession set to true if an interactive session should be started, overridden by (-S) option if specified * @param scriptName set to the scriptName if specified * * @return positive integer if unsuccessful, negative if successful but needs to exit, zero means success */ int parseOptions(int argc, char* argv[], std::string& databaseInitialisationFile, bool& dbIsLoaded, bool& shouldStartAnInteractiveSession, std::string& scriptName) { std::string iocExecutableName(argv[0]); std::string databaseShutdownFile(FULL_PATH_TO_EXIT_FILE); std::string commaSeparatedListOfMacroDefinitions; // This is set if a (-m) option is specified // compute relative paths { std::string prefix = getPrefix(); databaseInitialisationFile = prefix + RELATIVE_PATH_TO_INITIALISATION_FILE; databaseShutdownFile = prefix + RELATIVE_PATH_TO_SHUTDOWN_FILE; } // Parse the command line and configure and start the IOC int opt; // parsed option from the command line while ((opt = getopt(argc, argv, "a:D:d:G:hm:Svx:")) != -1) { switch (opt) { case 'a': configureDatabase(databaseInitialisationFile); if (!commaSeparatedListOfMacroDefinitions.empty()) { VERBOSE_MESSAGE "asSetSubstitutions(\"" << commaSeparatedListOfMacroDefinitions << "\")\n"; if (asSetSubstitutions(commaSeparatedListOfMacroDefinitions.c_str())) throw std::bad_alloc(); } VERBOSE_MESSAGE "asSetFilename(\"" << optarg << "\")\n"; if (asSetFilename(optarg)) { throw std::bad_alloc(); } break; case 'D': if (isDbLoaded) { throw std::runtime_error("database configuration file override specified " "after " FULL_PATH_TO_INITIALISATION_FILE " has already been loaded.\n" "Add the -D option prior to any -d or -x options and try again"); } databaseInitialisationFile = optarg; break; case 'd': configureDatabase(databaseInitialisationFile); VERBOSE_MESSAGE "dbLoadRecords(\"" << optarg << "\"" << ((commaSeparatedListOfMacroDefinitions.empty()) ? "" : std::string(", \"").append(commaSeparatedListOfMacroDefinitions) .append("\"")) << ")\n"; if (dbLoadRecords(optarg, commaSeparatedListOfMacroDefinitions.c_str())) { throw std::runtime_error(std::string("Failed to load: ") + optarg); } dbIsLoaded = true; break; case 'G': pvxs::ioc::dbLoadGroup(optarg); break; case 'h': usage(iocExecutableName, databaseInitialisationFile); epicsExit(0); return -1; case 'm': commaSeparatedListOfMacroDefinitions = optarg; break; case 'S': shouldStartAnInteractiveSession = false; break; case 'v': verboseFlag = true; break; case 'x': { std::string xmacro; configureDatabase(databaseInitialisationFile); xmacro = "IOC="; xmacro += optarg; if (dbLoadRecords(databaseShutdownFile.c_str(), xmacro.c_str())) { throw std::runtime_error(std::string("Failed to load: ") + databaseShutdownFile); } dbIsLoaded = true; } break; default: usage(iocExecutableName, databaseInitialisationFile); std::cerr << "Unknown argument: -" << char(optopt) << "\n"; epicsExit(2); return 2; } } // If script specified then return name if (optind < argc) { scriptName = argv[optind]; } return 0; } } } // pvxs::qsrv using namespace pvxs::qsrv; /** * Main entry point for the qsrv executable. * * @param argc the number of command line arguments * @param argv the command line arguments * @return 0 for successful exit, nonzero otherwise */ int main(int argc, char* argv[]) { try { std::string databaseInitialisationFile(FULL_PATH_TO_INITIALISATION_FILE); std::string scriptName; bool shouldStartAnInteractiveSession = true; // Default is true, unless (-S) option is specified bool dbIsLoaded = false; // Is database loaded auto status = parseOptions(argc, argv, databaseInitialisationFile, dbIsLoaded, shouldStartAnInteractiveSession, scriptName); if (status != 0) { return status > 0 ? status : 0; } // Configure the database with the specified configuration file configureDatabase(databaseInitialisationFile); // If we've loaded a database file or configured the exit callback, then do an iocInit() if (dbIsLoaded) { VERBOSE_MESSAGE "iocInit()\n"; iocInit(); epicsThreadSleep(0.2); } // If we've specified a script on the command line then run it bool userScriptHasBeenExecuted = false; if (!scriptName.empty()) { VERBOSE_MESSAGE "# Begin execution of: " << scriptName << "\n"; if (iocsh(scriptName.c_str())) { throw std::runtime_error(std::string("Error in ") + scriptName); } VERBOSE_MESSAGE "# End execution of: " << scriptName << "\n"; epicsThreadSleep(0.2); userScriptHasBeenExecuted = true; } // If we haven't disabled the interactive shell then enter it if (shouldStartAnInteractiveSession) { std::cout.flush(); std::cerr.flush(); if (iocsh(nullptr)) { // if error status then propagate error to epics and shell epicsExit(1); return 1; } } else { // If non-interactive then exit if (dbIsLoaded || userScriptHasBeenExecuted) { epicsExitCallAtExits(); epicsThreadSleep(0.1); epicsThreadSuspendSelf(); } else { // Indicate that there was probably an error if nothing was loaded or executed std::cerr << "Nothing to do!\n"; epicsExit(1); return 1; } } epicsExit(0); return (0); } catch (std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; epicsExit(2); return 2; } }