//========================================================================== // CMDENV.CC - part of // OMNeT++/OMNEST // Discrete System Simulation in C++ // // // Author: Andras Varga // //========================================================================== /*--------------------------------------------------------------* Copyright (C) 1992-2017 Andras Varga Copyright (C) 2006-2017 OpenSim Ltd. This file is distributed WITHOUT ANY WARRANTY. See the file `license' for details on this and other legal matters. *--------------------------------------------------------------*/ #include #include #include #include #include #include "common/opp_ctype.h" #include "common/commonutil.h" #include "common/fileutil.h" #include "common/enumstr.h" #include "common/stringutil.h" #include "common/stringtokenizer.h" #include "envir/appreg.h" #include "envir/args.h" #include "envir/speedometer.h" #include "envir/visitor.h" #include "omnetpp/csimplemodule.h" #include "omnetpp/ccomponenttype.h" #include "omnetpp/cmessage.h" #include "omnetpp/cconfigoption.h" #include "omnetpp/checkandcast.h" #include "omnetpp/cproperties.h" #include "omnetpp/cproperty.h" #include "omnetpp/cenum.h" #include "omnetpp/cscheduler.h" #include "omnetpp/cfutureeventset.h" #include "omnetpp/cresultfilter.h" #include "omnetpp/cresultrecorder.h" #include "omnetpp/cclassdescriptor.h" #include "omnetpp/cqueue.h" #include "omnetpp/cchannel.h" #include "omnetpp/coutvector.h" #include "omnetpp/cstatistic.h" #include "omnetpp/cabstracthistogram.h" #include "omnetpp/cwatch.h" #include "omnetpp/cdisplaystring.h" #include "cmddefs.h" #include "cmdenv.h" using namespace omnetpp::common; namespace omnetpp { namespace cmdenv { Register_GlobalConfigOption(CFGID_CONFIG_NAME, "cmdenv-config-name", CFG_STRING, nullptr, "Specifies the name of the configuration to be run (for a value `Foo`, section `[Config Foo]` will be used from the ini file). See also `cmdenv-runs-to-execute`. The `-c` command line option overrides this setting.") Register_GlobalConfigOption(CFGID_RUNS_TO_EXECUTE, "cmdenv-runs-to-execute", CFG_STRING, nullptr, "Specifies which runs to execute from the selected configuration (see `cmdenv-config-name` option). It accepts a filter expression of iteration variables such as `$numHosts>10 && $iatime==1s`, or a comma-separated list of run numbers or run number ranges, e.g. `1,3..4,7..9`. If the value is missing, Cmdenv executes all runs in the selected configuration. The `-r` command line option overrides this setting.") Register_GlobalConfigOptionU(CFGID_CMDENV_EXTRA_STACK, "cmdenv-extra-stack", "B", "8KiB", "Specifies the extra amount of stack that is reserved for each `activity()` simple module when the simulation is run under Cmdenv.") Register_PerRunConfigOption(CFGID_STOP_BATCH_ON_ERROR, "cmdenv-stop-batch-on-error", CFG_BOOL, "true", "Decides whether Cmdenv should skip the rest of the runs when an error occurs during the execution of one run.") Register_PerRunConfigOption(CFGID_CMDENV_INTERACTIVE, "cmdenv-interactive", CFG_BOOL, "false", "Defines what Cmdenv should do when the model contains unassigned parameters. In interactive mode, it asks the user. In non-interactive mode (which is more suitable for batch execution), Cmdenv stops with an error.") Register_PerRunConfigOption(CFGID_CMDENV_OUTPUT_FILE, "cmdenv-output-file", CFG_FILENAME, "${resultdir}/${configname}-${iterationvarsf}#${repetition}.out", "When `cmdenv-record-output=true`: file name to redirect standard output to. See also `fname-append-host`.") Register_PerRunConfigOption(CFGID_CMDENV_REDIRECT_OUTPUT, "cmdenv-redirect-output", CFG_BOOL, "false", "Causes Cmdenv to redirect standard output of simulation runs to a file or separate files per run. This option can be useful with running simulation campaigns (e.g. using opp_runall), and also with parallel simulation. See also: `cmdenv-output-file`, `fname-append-host`."); Register_PerRunConfigOption(CFGID_EXPRESS_MODE, "cmdenv-express-mode", CFG_BOOL, "true", "Selects normal (debug/trace) or express mode.") Register_PerRunConfigOption(CFGID_AUTOFLUSH, "cmdenv-autoflush", CFG_BOOL, "false", "Call `fflush(stdout)` after each event banner or status update; affects both express and normal mode. Turning on autoflush may have a performance penalty, but it can be useful with printf-style debugging for tracking down program crashes.") Register_PerRunConfigOption(CFGID_EVENT_BANNERS, "cmdenv-event-banners", CFG_BOOL, "true", "When `cmdenv-express-mode=false`: turns printing event banners on/off.") Register_PerRunConfigOption(CFGID_EVENT_BANNER_DETAILS, "cmdenv-event-banner-details", CFG_BOOL, "false", "When `cmdenv-express-mode=false`: print extra information after event banners.") Register_PerRunConfigOptionU(CFGID_STATUS_FREQUENCY, "cmdenv-status-frequency", "s", "2s", "When `cmdenv-express-mode=true`: print status update every n seconds.") Register_PerRunConfigOption(CFGID_PERFORMANCE_DISPLAY, "cmdenv-performance-display", CFG_BOOL, "true", "When `cmdenv-express-mode=true`: print detailed performance information. Turning it on results in a 3-line entry printed on each update, containing ev/sec, simsec/sec, ev/simsec, number of messages created/still present/currently scheduled in FES.") Register_PerRunConfigOption(CFGID_LOG_PREFIX, "cmdenv-log-prefix", CFG_STRING, "[%l]\t", "Specifies the format string that determines the prefix of each log line. The format string may contain format directives in the syntax `%x` (a `%` followed by a single format character). For example `%l` stands for log level, and `%J` for source component. See the manual for the list of available format characters."); Register_PerObjectConfigOption(CFGID_CMDENV_LOGLEVEL, "cmdenv-log-level", KIND_MODULE, CFG_STRING, "TRACE", "Specifies the per-component level of detail recorded by log statements, output below the specified level is omitted. Available values are (case insensitive): `off`, `fatal`, `error`, `warn`, `info`, `detail`, `debug` or `trace`. Note that the level of detail is also controlled by the globally specified runtime log level and the `COMPILETIME_LOGLEVEL` macro that is used to completely remove log statements from the executable.") // // Register the Cmdenv user interface // Register_OmnetApp("Cmdenv", Cmdenv, 10, "command-line user interface"); // // The following function can be used to force linking with Cmdenv; specify // -u _cmdenv_lib (gcc) or /include:_cmdenv_lib (vc++) in the link command. // extern "C" CMDENV_API void cmdenv_lib() {} // on some compilers (e.g. linux gcc 4.2) the functions are generated without _ extern "C" CMDENV_API void _cmdenv_lib() {} bool Cmdenv::sigintReceived; // utility function for printing elapsed time static char *timeToStr(double t, char *buf = nullptr) { static char buf2[64]; char *b = buf ? buf : buf2; int sec = (int) floor(t); if (t < 3600) sprintf(b, "%lgs (%dm %02ds)", t, int(sec/60L), int(sec%60L)); else if (t < 86400) sprintf(b, "%lgs (%dh %02dm)", t, int(sec/3600L), int((sec%3600L)/60L)); else sprintf(b, "%lgs (%dd %02dh)", t, int(sec/86400L), int((sec%86400L)/3600L)); return b; } CmdenvOptions::CmdenvOptions() { // note: these values will be overwritten in setup()/readOptions() before taking effect stopBatchOnError = true; extraStack = 0; redirectOutput = false; autoflush = true; expressMode = false; interactive = false; printModuleMsgs = false; printEventBanners = true; detailedEventBanners = false; statusFrequencyMs = 2000; printPerformanceData = false; } Cmdenv::Cmdenv() : opt((CmdenvOptions *&)EnvirBase::opt) { // Note: ctor should only contain trivial initializations, because // the class may be instantiated only for the purpose of calling // printUISpecificHelp() on it logging = true; logStream = fopen(".cmdenv-log", "w"); if (!logStream) logStream = stdout; } Cmdenv::~Cmdenv() { } void Cmdenv::readOptions() { EnvirBase::readOptions(); cConfiguration *cfg = getConfig(); // note: configName and runFilter will possibly be overwritten // with the -c, -r command-line options in our setup() method opt->configName = cfg->getAsString(CFGID_CONFIG_NAME); opt->runFilter = cfg->getAsString(CFGID_RUNS_TO_EXECUTE); opt->extraStack = (size_t)cfg->getAsDouble(CFGID_CMDENV_EXTRA_STACK); } void Cmdenv::readPerRunOptions() { EnvirBase::readPerRunOptions(); cConfiguration *cfg = getConfig(); opt->stopBatchOnError = cfg->getAsBool(CFGID_STOP_BATCH_ON_ERROR); opt->expressMode = cfg->getAsBool(CFGID_EXPRESS_MODE); opt->interactive = cfg->getAsBool(CFGID_CMDENV_INTERACTIVE); opt->autoflush = cfg->getAsBool(CFGID_AUTOFLUSH); opt->printEventBanners = cfg->getAsBool(CFGID_EVENT_BANNERS); opt->detailedEventBanners = cfg->getAsBool(CFGID_EVENT_BANNER_DETAILS); opt->statusFrequencyMs = 1000*cfg->getAsDouble(CFGID_STATUS_FREQUENCY); opt->printPerformanceData = cfg->getAsBool(CFGID_PERFORMANCE_DISPLAY); setLogFormat(getConfig()->getAsString(CFGID_LOG_PREFIX).c_str()); opt->outputFile = cfg->getAsFilename(CFGID_CMDENV_OUTPUT_FILE).c_str(); opt->redirectOutput = cfg->getAsBool(CFGID_CMDENV_REDIRECT_OUTPUT); } void Cmdenv::doRun() { { // '-c' and '-r' option: configuration to activate, and run numbers to run. // Both command-line options take precedence over inifile settings. // (NOTE: inifile settings *already* got read at this point! as EnvirBase::setup() // invokes readOptions()). opt->configName = opp_nulltoempty(args->optionValue('c')); if (opt->configName.empty()) opt->configName = "General"; if (args->optionGiven('r')) // note: there's also a cmdenv-runs-to-execute option! opt->runFilter = args->optionValue('r'); std::vector runNumbers; try { runNumbers = resolveRunFilter(opt->configName.c_str(), opt->runFilter.c_str()); } catch (std::exception& e) { displayException(e); exitCode = 1; return; } numRuns = (int)runNumbers.size(); runsTried = 0; int numErrors = 0; for (int runNumber : runNumbers) { runsTried++; bool finishedOK = false; bool networkSetupDone = false; bool startrunDone = false; try { if (opt->verbose) out << "\nPreparing for running configuration " << opt->configName << ", run #" << runNumber << "..." << endl; cfg->activateConfig(opt->configName.c_str(), runNumber); readPerRunOptions(); const char *iterVars = cfg->getVariable(CFGVAR_ITERATIONVARS); const char *runId = cfg->getVariable(CFGVAR_RUNID); const char *repetition = cfg->getVariable(CFGVAR_REPETITION); if (!opt->verbose) out << opt->configName << " run " << runNumber << ": " << iterVars << ", $repetition=" << repetition << endl; // print before redirection; useful as progress indication from opp_runall if (opt->redirectOutput) { processFileName(opt->outputFile); if (opt->verbose) out << "Redirecting output to file \"" << opt->outputFile << "\"..." << endl; startOutputRedirection(opt->outputFile.c_str()); if (opt->verbose) out << "\nRunning configuration " << opt->configName << ", run #" << runNumber << "..." << endl; } if (opt->verbose) { if (iterVars && strlen(iterVars) > 0) out << "Scenario: " << iterVars << ", $repetition=" << repetition << endl; out << "Assigned runID=" << runId << endl; } // find network cModuleType *network = resolveNetwork(opt->networkName.c_str()); ASSERT(network); // set up network if (opt->verbose) out << "Setting up network \"" << opt->networkName.c_str() << "\"..." << endl; setupNetwork(network); networkSetupDone = true; // prepare for simulation run if (opt->verbose) out << "Initializing..." << endl; loggingEnabled = !opt->expressMode; startRun(); startrunDone = true; // run the simulation if (opt->verbose) out << "\nRunning simulation..." << endl; // simulate() should only throw exception if error occurred and // finish() should not be called. notifyLifecycleListeners(LF_ON_SIMULATION_START); simulate(); loggingEnabled = true; if (opt->verbose) out << "\nCalling finish() at end of Run #" << runNumber << "..." << endl; getSimulation()->callFinish(); cLogProxy::flushLastLine(); checkFingerprint(); notifyLifecycleListeners(LF_ON_SIMULATION_SUCCESS); finishedOK = true; } catch (std::exception& e) { loggingEnabled = true; stoppedWithException(e); notifyLifecycleListeners(LF_ON_SIMULATION_ERROR); displayException(e); } // call endRun() if (startrunDone) { try { endRun(); } catch (std::exception& e) { finishedOK = false; notifyLifecycleListeners(LF_ON_SIMULATION_ERROR); displayException(e); } } // delete network if (networkSetupDone) { try { getSimulation()->deleteNetwork(); } catch (std::exception& e) { numErrors++; notifyLifecycleListeners(LF_ON_SIMULATION_ERROR); displayException(e); } } // stop redirecting into file stopOutputRedirection(); if (!finishedOK) numErrors++; // skip further runs if signal was caught if (sigintReceived) break; if (!finishedOK && opt->stopBatchOnError) break; } if (numRuns > 1 && opt->verbose) { int numSkipped = numRuns - runsTried; int numSuccess = runsTried - numErrors; out << "\nRun statistics: total " << numRuns; if (numSuccess > 0) out << ", successful " << numSuccess; if (numErrors > 0) out << ", errors " << numErrors; if (numSkipped > 0) out << ", skipped " << numSkipped; out << endl; } exitCode = numErrors > 0 ? 1 : sigintReceived ? 2 : 0; } } // note: also updates "since" (sets it to the current time) if answer is "true" inline bool elapsed(long millis, int64_t& since) { int64_t now = opp_get_monotonic_clock_usecs(); bool ret = (now - since) > millis * 1000; if (ret) since = now; return ret; } void Cmdenv::simulate() // XXX probably not needed anymore -- take over interesting bits to other methods! { // implement graceful exit when Ctrl-C is hit during simulation. We want // to finish the current event, then normally exit via callFinish() etc // so that simulation results are not lost. installSignalHandler(); startClock(); sigintReceived = false; Speedometer speedometer; // only used by Express mode, but we need it in catch blocks too try { if (!opt->expressMode) { while (true) { cEvent *event = getSimulation()->takeNextEvent(); if (!event) throw cTerminationException("Scheduler interrupted while waiting"); // flush *between* printing event banner and event processing, so that // if event processing crashes, it can be seen which event it was if (opt->autoflush) out.flush(); // execute event getSimulation()->executeEvent(event); // flush so that output from different modules don't get mixed cLogProxy::flushLastLine(); checkTimeLimits(); if (sigintReceived) throw cTerminationException("SIGINT or SIGTERM received, exiting"); } } else { speedometer.start(getSimulation()->getSimTime()); int64_t last_update = opp_get_monotonic_clock_usecs(); doStatusUpdate(speedometer); while (true) { cEvent *event = getSimulation()->takeNextEvent(); if (!event) throw cTerminationException("Scheduler interrupted while waiting"); speedometer.addEvent(getSimulation()->getSimTime()); // XXX potential performance hog // print event banner from time to time if ((getSimulation()->getEventNumber()&0xff) == 0 && elapsed(opt->statusFrequencyMs, last_update)) doStatusUpdate(speedometer); // execute event getSimulation()->executeEvent(event); checkTimeLimits(); // XXX potential performance hog (maybe check every 256 events, unless "cmdenv-strict-limits" is on?) if (sigintReceived) throw cTerminationException("SIGINT or SIGTERM received, exiting"); } } } catch (cTerminationException& e) { if (opt->expressMode) doStatusUpdate(speedometer); loggingEnabled = true; stopClock(); deinstallSignalHandler(); stoppedWithTerminationException(e); displayException(e); return; } catch (std::exception& e) { if (opt->expressMode) doStatusUpdate(speedometer); loggingEnabled = true; stopClock(); deinstallSignalHandler(); throw; } // note: C++ lacks "finally": lines below need to be manually kept in sync with catch{...} blocks above! if (opt->expressMode) doStatusUpdate(speedometer); loggingEnabled = true; stopClock(); deinstallSignalHandler(); } void Cmdenv::printEventBanner(cEvent *event) { out << "** Event #" << getSimulation()->getEventNumber() << " t=" << getSimulation()->getSimTime() << progressPercentage() << " "; // note: IDE launcher uses this to track progress if (event->isMessage()) { cModule *mod = static_cast(event)->getArrivalModule(); out << mod->getFullPath() << " (" << mod->getComponentType()->getName() << ", id=" << mod->getId() << ")"; } else if (event->getTargetObject()) { cObject *target = event->getTargetObject(); out << target->getFullPath() << " (" << target->getClassName() << ")"; } out << "\n"; // note: "\n" not endl, because we don't want auto-flush on each event if (opt->detailedEventBanners) { out << " Elapsed: " << timeToStr(getElapsedSecs()) << " Messages: created: " << cMessage::getTotalMessageCount() << " present: " << cMessage::getLiveMessageCount() << " in FES: " << getSimulation()->getFES()->getLength() << "\n"; // note: "\n" not endl, because we don't want auto-flush on each event } } void Cmdenv::doStatusUpdate(Speedometer& speedometer) { speedometer.beginNewInterval(); if (opt->printPerformanceData) { out << "** Event #" << getSimulation()->getEventNumber() << " t=" << getSimulation()->getSimTime() << " Elapsed: " << timeToStr(getElapsedSecs()) << "" << progressPercentage() << endl; // note: IDE launcher uses this to track progress out << " Speed: ev/sec=" << speedometer.getEventsPerSec() << " simsec/sec=" << speedometer.getSimSecPerSec() << " ev/simsec=" << speedometer.getEventsPerSimSec() << endl; out << " Messages: created: " << cMessage::getTotalMessageCount() << " present: " << cMessage::getLiveMessageCount() << " in FES: " << getSimulation()->getFES()->getLength() << endl; } else { out << "** Event #" << getSimulation()->getEventNumber() << " t=" << getSimulation()->getSimTime() << " Elapsed: " << timeToStr(getElapsedSecs()) << progressPercentage() // note: IDE launcher uses this to track progress << " ev/sec=" << speedometer.getEventsPerSec() << endl; } // status update is always autoflushed (not only if opt->autoflush is on) out.flush(); } const char *Cmdenv::progressPercentage() { double simtimeRatio = -1; if (opt->simtimeLimit > 0) simtimeRatio = getSimulation()->getSimTime() / opt->simtimeLimit; double elapsedTimeRatio = -1; if (opt->realTimeLimit > 0) elapsedTimeRatio = stopwatch.getElapsedSecs() / opt->realTimeLimit; double cpuTimeRatio = -1; if (opt->cpuTimeLimit > 0) cpuTimeRatio = stopwatch.getCPUUsageSecs() / opt->cpuTimeLimit; double ratio = std::max(simtimeRatio, std::max(elapsedTimeRatio, cpuTimeRatio)); ratio = std::min(ratio, 1.0); // eliminate occasional "101% completed" message if (ratio == -1) return ""; else { double totalRatio = (ratio + runsTried - 1) / numRuns; static char buf[32]; // DO NOT change the "% completed" string. The IDE launcher plugin matches // against this string for detecting user input snprintf(buf, 32, " %d%% completed (%d%% total)", (int)(100*ratio), (int)(100*totalRatio)); return buf; } } void Cmdenv::displayException(std::exception& ex) { EnvirBase::displayException(ex); } void Cmdenv::componentInitBegin(cComponent *component, int stage) { // TODO: make this an EV_INFO in the component? if (!opt->expressMode && opt->printEventBanners && component->getLogLevel() != LOGLEVEL_OFF) out << "Initializing " << (component->isModule() ? "module" : "channel") << " " << component->getFullPath() << ", stage " << stage << endl; } void Cmdenv::simulationEvent(cEvent *event) { EnvirBase::simulationEvent(event); // print event banner if necessary if (!opt->expressMode && opt->printEventBanners) if (!event->isMessage() || static_cast(event)->getArrivalModule()->getLogLevel() != LOGLEVEL_OFF) printEventBanner(event); } void Cmdenv::signalHandler(int signum) { if (signum == SIGINT || signum == SIGTERM) sigintReceived = true; } void Cmdenv::installSignalHandler() { signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); } void Cmdenv::deinstallSignalHandler() { signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); } //----------------------------------------------------- void Cmdenv::configure(cComponent *component) { EnvirBase::configure(component); LogLevel level = cLog::resolveLogLevel(getEnvir()->getConfig()->getAsString(component->getFullPath().c_str(), CFGID_CMDENV_LOGLEVEL).c_str()); component->setLogLevel(level); } void Cmdenv::askParameter(cPar *par, bool unassigned) { bool success = false; while (!success) { cProperties *props = par->getProperties(); cProperty *prop = props->get("prompt"); std::string prompt = prop ? prop->getValue(cProperty::DEFAULTKEY) : ""; std::string reply; // ask the user. note: gets() will signal "cancel" by throwing an exception if (!prompt.empty()) reply = this->gets(prompt.c_str(), par->str().c_str()); else // DO NOT change the "Enter parameter" string. The IDE launcher plugin matches // against this string for detecting user input reply = this->gets((std::string("Enter parameter '")+par->getFullPath()+"' ("+(unassigned ? "unassigned" : "ask")+"):").c_str(), par->str().c_str()); try { par->parse(reply.c_str()); success = true; } catch (std::exception& e) { out << " " << e.what() << " -- please try again" << endl; } } } void Cmdenv::alert(const char *msg) { out << "\n " << msg << endl << endl; } void Cmdenv::log(cLogEntry *entry) { EnvirBase::log(entry); if (!logFormatter.isBlank()) out << logFormatter.formatPrefix(entry); out.write(entry->text, entry->textLength); if (opt->autoflush) out.flush(); } std::string Cmdenv::gets(const char *prompt, const char *defaultReply) { if (!opt->interactive) throw cRuntimeError("The simulation wanted to ask a question, set cmdenv-interactive=true to allow it: \"%s\"", prompt); out << prompt; if (!opp_isempty(defaultReply)) out << "(default: " << defaultReply << ") "; out.flush(); { std::string buffer; std::getline(std::cin, buffer); if (buffer == "\x1b") // ESC? throw cRuntimeError(E_CANCEL); return buffer; } } bool Cmdenv::askYesNo(const char *question) { if (!opt->interactive) throw cRuntimeError("Simulation needs user input in non-interactive mode (prompt text: \"%s (y/n)\")", question); { for (;;) { out << question <<" (y/n) "; out.flush(); std::string buffer; std::getline(std::cin, buffer); if (buffer == "\x1b") // ESC? throw cRuntimeError(E_CANCEL); if (buffer == "y" || buffer == "Y") return true; else if (buffer == "n" || buffer == "N") return false; else out << "Please type 'y' or 'n'!" << endl; } } } bool Cmdenv::idle() { return sigintReceived; } void Cmdenv::getImageSize(const char *imageName, double& outWidth, double& outHeight) { outWidth = outHeight = 32; } void Cmdenv::getTextExtent(const cFigure::Font& font, const char *text, double& outWidth, double& outHeight, double& outAscent) { if (!*text) { outWidth = outHeight = outAscent = 0; return; } outWidth = 10 * strlen(text); outHeight = 12; outAscent = 8; } void Cmdenv::appendToImagePath(const char *directory) { } void Cmdenv::loadImage(const char *fileName, const char *imageName) { } cFigure::Rectangle Cmdenv::getSubmoduleBounds(const cModule *submodule) { return cFigure::Rectangle(NAN, NAN, NAN, NAN); } double Cmdenv::getZoomLevel(const cModule *module) { return NAN; } void Cmdenv::printUISpecificHelp() { out << "Cmdenv-specific information:\n"; out << " Cmdenv executes all runs denoted by the -c and -r options. The number\n"; out << " of runs executed and the number of runs that ended with an error are\n"; out << " reported at the end.\n"; out << endl; } unsigned Cmdenv::getExtraStackForEnvir() const { return opt->extraStack; } } // namespace cmdenv } // namespace omnetpp