From f27feeedb3c126c7492aa9332de797b5f9408d0e Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 25 Oct 2026 23:04:26 -0400 Subject: [PATCH 1/2] An analysis can be given a time limit (4.2.41) NLP++ is a C++ extension. A grammar whose loop never ends cannot be stopped from outside -- a Python caller has no thread to interrupt, only a process to kill -- and a loop that makes a string a turn grows until the process is killed for its memory. One such loop took a document extractor down five times in half an hour on one document, and with it every analysis queued behind that document. - NLP_ENGINE::setRunLimit(ms) puts a limit on each text's analysis; 0, the default, is none. lastRunStopped() says whether the last analyze() or analyzeStream() call stopped a run at it. - Each Parse takes the limit when it is made. Parse::overBudget is asked on every while turn (reading the clock every 256th time) and before every pass. Past the limit, the loop fails its region, no more passes run, and the stop is reported once in err.log and on stderr, naming the pass and its file. The run's string table is freed with it as usual, so the memory comes back. - Compiled analyzers get the same check: the generator writes Arun::overbudget(nlppp) into every while condition. - nlp -TIMELIMIT . A run it stops exits 2: neither success nor the missing analyzer or input that exits 1. Fixture run-limit loops forever in pass 3 and checks that pass 2 ran, the line after the loop and pass 4 did not, the message is on stderr, and nlp exits 2. On the analyzer that hung in production (with the old string functions, so it still loops) -TIMELIMIT 5 stops it at 5 seconds. No limit, no change: 881 fixture inputs of 17 production analyzers give byte-identical output.json to 4.2.39, and 3 million while turns take the same time with or without a limit. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-linux.yml | 27 +++++++ .github/workflows/build-macos.yml | 26 ++++++ .github/workflows/build-windows.yml | 26 ++++++ .../workflows/tests/run-limit/input/text.txt | 1 + .../tests/run-limit/kb/user/.gitkeep | 0 .../workflows/tests/run-limit/spec/after.nlp | 3 + .../tests/run-limit/spec/analyzer.seq | 4 + .../workflows/tests/run-limit/spec/before.nlp | 3 + .../workflows/tests/run-limit/spec/loop.nlp | 23 ++++++ include/Api/lite/Arun.h | 1 + include/Api/lite/nlp.h | 12 +++ include/Api/lite/nlp_engine.h | 10 +++ include/Api/lite/parse.h | 15 ++++ lite/Arun.cpp | 14 ++++ lite/iwhilestmt.cpp | 12 ++- lite/nlp.cpp | 2 + lite/nlp_engine.cpp | 27 +++++++ lite/parse.cpp | 81 +++++++++++++++++++ nlp/main.cpp | 36 +++++++-- 19 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/tests/run-limit/input/text.txt create mode 100644 .github/workflows/tests/run-limit/kb/user/.gitkeep create mode 100644 .github/workflows/tests/run-limit/spec/after.nlp create mode 100644 .github/workflows/tests/run-limit/spec/analyzer.seq create mode 100644 .github/workflows/tests/run-limit/spec/before.nlp create mode 100644 .github/workflows/tests/run-limit/spec/loop.nlp diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 5c9f221d..88a7bdf9 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -387,6 +387,33 @@ jobs: fi echo "[str positions] every string position is a character" + - name: A run past -TIMELIMIT is stopped (Non-20.04) + if: matrix.id != 'docker-ubuntu-20.04' + timeout-minutes: 3 + shell: bash + run: | + set -o pipefail + # A loop that never ends used to run until something killed the + # process; one that makes a string a turn, until it ran out of + # memory. With -TIMELIMIT the run stops at the next while turn past + # the limit: the code after the loop and the passes after it do not + # run, the stop is reported on stderr, and nlp exits 2. + FIX=.github/workflows/tests/run-limit + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + mkdir -p "$FIX/logs" + rc=0 + ./bin/nlp -TIMELIMIT 2 -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" 2> /tmp/runlimit.err || rc=$? + if [ "$rc" != 2 ]; then + echo "FAIL(run limit): exit $rc, wanted 2"; cat /tmp/runlimit.err; exit 1 + fi + got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + if [ "$got" != "before" ]; then + echo "FAIL(run limit): out.txt was:"; echo "$got"; echo "wanted: before"; exit 1 + fi + grep -qF '[Run stopped: past its time limit of 2000 ms in pass 3 (loop)' /tmp/runlimit.err \ + || { echo "FAIL(run limit): no stop message on stderr"; cat /tmp/runlimit.err; exit 1; } + echo "[run limit] a loop that never ends is stopped at -TIMELIMIT" + - name: Run-mode globals ($dev / $silent, Non-20.04) if: matrix.id != 'docker-ubuntu-20.04' shell: bash diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 23966386..e54b3681 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -235,6 +235,32 @@ jobs: fi echo "[str positions] every string position is a character" + - name: A run past -TIMELIMIT is stopped + timeout-minutes: 3 + shell: bash + run: | + set -o pipefail + # A loop that never ends used to run until something killed the + # process; one that makes a string a turn, until it ran out of + # memory. With -TIMELIMIT the run stops at the next while turn past + # the limit: the code after the loop and the passes after it do not + # run, the stop is reported on stderr, and nlp exits 2. + FIX=.github/workflows/tests/run-limit + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + mkdir -p "$FIX/logs" + rc=0 + ./bin/nlp -TIMELIMIT 2 -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" 2> /tmp/runlimit.err || rc=$? + if [ "$rc" != 2 ]; then + echo "FAIL(run limit): exit $rc, wanted 2"; cat /tmp/runlimit.err; exit 1 + fi + got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + if [ "$got" != "before" ]; then + echo "FAIL(run limit): out.txt was:"; echo "$got"; echo "wanted: before"; exit 1 + fi + grep -qF '[Run stopped: past its time limit of 2000 ms in pass 3 (loop)' /tmp/runlimit.err \ + || { echo "FAIL(run limit): no stop message on stderr"; cat /tmp/runlimit.err; exit 1; } + echo "[run limit] a loop that never ends is stopped at -TIMELIMIT" + # INTEL MACS (#231). The steps above built and tested arm64 (Apple # Silicon). Build the same tree for x86_64 too and merge the two with # lipo, so nlpm.exe and the compile libraries are universal binaries: diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 3c0204a9..6c9bf3ff 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -322,6 +322,32 @@ jobs: fi echo "[str positions] every string position is a character" + - name: A run past -TIMELIMIT is stopped + timeout-minutes: 3 + shell: bash + run: | + set -o pipefail + # A loop that never ends used to run until something killed the + # process; one that makes a string a turn, until it ran out of + # memory. With -TIMELIMIT the run stops at the next while turn past + # the limit: the code after the loop and the passes after it do not + # run, the stop is reported on stderr, and nlp exits 2. + FIX=.github/workflows/tests/run-limit + rm -rf "$FIX/logs" "$FIX/input/text.txt_log" + mkdir -p "$FIX/logs" + rc=0 + ./bin/Release/nlp.exe -TIMELIMIT 2 -ANA "$FIX" -WORK ./ "$FIX/input/text.txt" 2> /tmp/runlimit.err || rc=$? + if [ "$rc" != 2 ]; then + echo "FAIL(run limit): exit $rc, wanted 2"; cat /tmp/runlimit.err; exit 1 + fi + got=$(tr -d '\r' < "$FIX/input/text.txt_log/out.txt") + if [ "$got" != "before" ]; then + echo "FAIL(run limit): out.txt was:"; echo "$got"; echo "wanted: before"; exit 1 + fi + grep -qF '[Run stopped: past its time limit of 2000 ms in pass 3 (loop)' /tmp/runlimit.err \ + || { echo "FAIL(run limit): no stop message on stderr"; cat /tmp/runlimit.err; exit 1; } + echo "[run limit] a loop that never ends is stopped at -TIMELIMIT" + - name: Run-mode globals ($dev / $silent) shell: bash run: | diff --git a/.github/workflows/tests/run-limit/input/text.txt b/.github/workflows/tests/run-limit/input/text.txt new file mode 100644 index 00000000..480c16cd --- /dev/null +++ b/.github/workflows/tests/run-limit/input/text.txt @@ -0,0 +1 @@ +The fixture loops; the text does not matter. diff --git a/.github/workflows/tests/run-limit/kb/user/.gitkeep b/.github/workflows/tests/run-limit/kb/user/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/tests/run-limit/spec/after.nlp b/.github/workflows/tests/run-limit/spec/after.nlp new file mode 100644 index 00000000..0805f5fc --- /dev/null +++ b/.github/workflows/tests/run-limit/spec/after.nlp @@ -0,0 +1,3 @@ +@CODE +"out.txt" << "after-pass\n"; +@@CODE diff --git a/.github/workflows/tests/run-limit/spec/analyzer.seq b/.github/workflows/tests/run-limit/spec/analyzer.seq new file mode 100644 index 00000000..70d889eb --- /dev/null +++ b/.github/workflows/tests/run-limit/spec/analyzer.seq @@ -0,0 +1,4 @@ +tokenize nil # tokenize only -- fixture needs no dictionary +nlp before # runs, and says so +nlp loop # never ends on its own +nlp after # must not run once the limit stops the loop diff --git a/.github/workflows/tests/run-limit/spec/before.nlp b/.github/workflows/tests/run-limit/spec/before.nlp new file mode 100644 index 00000000..5b4fbc22 --- /dev/null +++ b/.github/workflows/tests/run-limit/spec/before.nlp @@ -0,0 +1,3 @@ +@CODE +"out.txt" << "before\n"; +@@CODE diff --git a/.github/workflows/tests/run-limit/spec/loop.nlp b/.github/workflows/tests/run-limit/spec/loop.nlp new file mode 100644 index 00000000..466e779c --- /dev/null +++ b/.github/workflows/tests/run-limit/spec/loop.nlp @@ -0,0 +1,23 @@ +############################################### +# FILE: loop.nlp +# SUBJ: A loop that never ends is stopped by -TIMELIMIT. +# NOTE: Like the loop that killed a document extractor: every turn +# makes a new string, so without a limit it runs until the +# process is killed for its memory. With -TIMELIMIT 2 the run +# stops at the next while turn after two seconds; the line after +# the loop and the pass after this one do not run, and nlp exits 2. +# Expected out.txt: +# before +############################################### + +@CODE + +L("s") = "x"; +L("n") = 0; +while (L("n") >= 0) { + L("n")++; + L("s") = "x" + str(L("n")); +} +"out.txt" << "after-loop\n"; + +@@CODE diff --git a/include/Api/lite/Arun.h b/include/Api/lite/Arun.h index d435e8d2..2a2be584 100644 --- a/include/Api/lite/Arun.h +++ b/include/Api/lite/Arun.h @@ -553,6 +553,7 @@ class LITE_API Arun static bool iassign(int,_TCHAR*,int,long long,Nlppp*,bool); // 12/10/02 AM. static long long iassign(int,_TCHAR*,int,long long,Nlppp*,int); + static bool overbudget(Nlppp*); // Run past its time limit. // 10/25/26 DD. static bool truth(long long); static bool truth(float); static bool truth(_TCHAR *); diff --git a/include/Api/lite/nlp.h b/include/Api/lite/nlp.h index e3fd30d0..5d3cc216 100644 --- a/include/Api/lite/nlp.h +++ b/include/Api/lite/nlp.h @@ -455,6 +455,18 @@ class LITE_API NLP // Intended for notable errors. // VTLOG // 05/06/13 AM. _TCHAR *logfile_; // VTLOG // 05/06/13 AM. + // RUN LIMIT for each text's analysis, in milliseconds (0 = none), and + // whether a run since the flag was cleared was stopped at it. Each new + // Parse takes the limit; Parse::overBudget sets the flag. // 10/25/26 DD. +public: + void setRunLimit(long long ms) { runLimitMs_ = ms > 0 ? ms : 0; } + long long getRunLimit() { return runLimitMs_; } + void setRunStopped(bool stopped) { runStopped_ = stopped; } + bool getRunStopped() { return runStopped_; } +private: + long long runLimitMs_ = 0; + bool runStopped_ = false; + // When code is well debugged and stable, can remove the stuff below. #ifndef STABLE_ public: diff --git a/include/Api/lite/nlp_engine.h b/include/Api/lite/nlp_engine.h index 6a705649..09a778ee 100644 --- a/include/Api/lite/nlp_engine.h +++ b/include/Api/lite/nlp_engine.h @@ -171,6 +171,14 @@ class LITE_API NLP_ENGINE void setDatum(const _TCHAR *datum); const _TCHAR *getDatum(); + // A time limit on each text's analysis, in milliseconds; 0 (the + // default) is none. A run past it is stopped -- at its next while turn + // or pass -- instead of looping until something kills the process, and + // lastRunStopped() is true after the analyze() call that stopped one. + void setRunLimit(long long ms); + long long getRunLimit(); + bool lastRunStopped(); + private: void analyzeFile(const std::string &file, _TCHAR *outdir, std::_t_ofstream *fp); int createDir(_TCHAR *dirPath); @@ -181,7 +189,9 @@ class LITE_API NLP_ENGINE // passed as different path-forms maps to one entry -- see #658 / #380. void anaRegName(const _TCHAR *path, _TCHAR *out); _TCHAR *datumArg(); + void armRunLimit(); // At the end of the class, so its layout is unchanged before it. std::basic_string<_TCHAR> m_datum; // G("$datum") for analyses. + long long m_runLimitMs = 0; // setRunLimit; 0 = none. }; \ No newline at end of file diff --git a/include/Api/lite/parse.h b/include/Api/lite/parse.h index 6748d41b..fb7a651d 100644 --- a/include/Api/lite/parse.h +++ b/include/Api/lite/parse.h @@ -21,6 +21,7 @@ All rights reserved. #endif #include #include +#include typedef void TREE; // 07/07/03 AM. typedef void LTREE; // 07/07/03 AM. @@ -658,6 +659,20 @@ class LITE_API Parse // Pass info to analyzer, to G("$datum") variable. // 03/13/03 AM. _TCHAR *datum_; // 03/13/03 AM. + // RUN LIMIT. A time limit on this text's analysis, so an analyzer whose + // loop never ends is stopped instead of taking its process with it: every + // turn of a while loop, and every pass, asks overBudget(). 0, the + // default, is no limit. // 10/25/26 DD. +public: + void setRunLimit(long long ms); + bool overBudget(bool now=false); // True once past the limit; stays true. + bool runStopped() { return runStopped_; } +private: + long long runLimitMs_ = 0; + std::chrono::steady_clock::time_point runDeadline_; + unsigned long runTicks_ = 0; // Calls since the clock was last read. + bool runStopped_ = false; + #ifndef STABLE_ public: static int getCount(); diff --git a/lite/Arun.cpp b/lite/Arun.cpp index da2e9e25..07fbcef6 100644 --- a/lite/Arun.cpp +++ b/lite/Arun.cpp @@ -8425,6 +8425,20 @@ return iassign(typ, varname, nelt, index, nlppp, (long long)val); } +/******************************************** +* FN: OVERBUDGET +* CR: 10/25/26 DD. +* SUBJ: Has this run passed its time limit? Compiled runtime. +* NOTE: Generated into every compiled while condition, as the interpreter +* asks Parse::overBudget on every turn (Iwhilestmt::eval). +********************************************/ + +bool Arun::overbudget(Nlppp *nlppp) +{ +return nlppp && nlppp->parse_ && nlppp->parse_->overBudget(); +} + + /******************************************** * FN: TRUTH * CR: 05/13/00 AM. diff --git a/lite/iwhilestmt.cpp b/lite/iwhilestmt.cpp index 6ab4257b..b4ceb56a 100644 --- a/lite/iwhilestmt.cpp +++ b/lite/iwhilestmt.cpp @@ -260,6 +260,15 @@ _TCHAR *str; for (;;) // EXECUTE THE WHILE-LOOP. { + // A run past its time limit (Parse::setRunLimit) stops here, and fails + // the region: the code after a loop that did not finish would run on + // whatever it left half done. Parse::overBudget has said why. // 10/25/26 DD. + if (nlppp->parse_->overBudget()) + { + ok = false; + goto done; + } + ok = cond_->eval(nlppp, /*UP*/ c_val); if (!ok || !c_val) { @@ -478,7 +487,8 @@ enum RFASemtype typ = RSNULL; // function with all possible return types! Elegant! Can be an inline. // This can be a general way....(but not totally optimized.) // If var, evals it. (see Iwhilestmt::eval). -*fcode << indent << _T("while (Arun::truth("); // FIX! // 04/29/01 AM. +// The run limit applies to compiled analyzers too (see eval). // 10/25/26 DD. +*fcode << indent << _T("while (!Arun::overbudget(nlppp) && Arun::truth("); if (!cond_->genEval(gen)) { *fcode << _T("\n// ERROR in whilestmt condition.") << std::endl; diff --git a/lite/nlp.cpp b/lite/nlp.cpp index c530ee73..655b4eeb 100644 --- a/lite/nlp.cpp +++ b/lite/nlp.cpp @@ -1462,6 +1462,7 @@ if (!htab_) // 06/25/03 AM. Parse *parse; // The current text analysis instance. parse = new Parse(); // 05/13/99 AM. parse->nlp_ = this; // 06/25/03 AM. +parse->setRunLimit(runLimitMs_); // 0 = none; see Parse::overBudget. // 10/25/26 DD. parse->htab_ = this->htab_; // 06/25/03 AM. parse->setEana(eana); // 10/13/99 AM. @@ -1608,6 +1609,7 @@ if (!htab_) // 06/25/03 AM. Parse *parse; // The current text analysis instance. parse = new Parse(); // 05/13/99 AM. parse->nlp_ = this; // 06/25/03 AM. +parse->setRunLimit(runLimitMs_); // 0 = none; see Parse::overBudget. // 10/25/26 DD. parse->htab_ = this->htab_; // 06/25/03 AM. parse->setEana(eana); // 10/13/99 AM. diff --git a/lite/nlp_engine.cpp b/lite/nlp_engine.cpp index 6272d224..59c6d582 100644 --- a/lite/nlp_engine.cpp +++ b/lite/nlp_engine.cpp @@ -452,6 +452,7 @@ int NLP_ENGINE::analyze( if (!m_nlp) // defensive: never analyze without a live analyzer object. return -1; + armRunLimit(); // #639: a missing input-file argument (easy to hit with -COMPILED, whose // syntax differs from -COMPILE only by the trailing input path) used to @@ -514,6 +515,7 @@ int NLP_ENGINE::analyzeStream( if (!m_nlp) // defensive: never analyze without a live analyzer object. return -1; + armRunLimit(); std::filesystem::path fname(analyzer); fname /= _T("logs"); @@ -670,6 +672,7 @@ int NLP_ENGINE::analyze( if (!m_nlp) // defensive: never analyze without a live analyzer object. return -1; + armRunLimit(); // The analyzer's cout() writes to standard output. @@ -722,6 +725,7 @@ int NLP_ENGINE::analyze( if (!m_nlp) // defensive: never analyze without a live analyzer object. return -1; + armRunLimit(); // The analyzer's cout() writes to standard output. @@ -826,6 +830,29 @@ const _TCHAR *NLP_ENGINE::getDatum() { return m_datum.c_str(); } +// A time limit on each text's analysis, in milliseconds; 0 is none. A run +// past it stops at its next while turn or pass, and lastRunStopped() says +// so. See Parse::overBudget for why. // 10/25/26 DD. +void NLP_ENGINE::setRunLimit(long long ms) { + m_runLimitMs = ms > 0 ? ms : 0; +} + +long long NLP_ENGINE::getRunLimit() { + return m_runLimitMs; +} + +// Whether the last analyze() or analyzeStream() call stopped a run (any of +// its inputs) at the time limit. +bool NLP_ENGINE::lastRunStopped() { + return m_nlp && m_nlp->getRunStopped(); +} + +// init() may have made a new NLP; give it the limit, and clear the flag. +void NLP_ENGINE::armRunLimit() { + m_nlp->setRunLimit(m_runLimitMs); + m_nlp->setRunStopped(false); +} + // The datum to pass to an analysis: 0 when unset, as before. _TCHAR *NLP_ENGINE::datumArg() { return m_datum.empty() ? 0 : (_TCHAR *) m_datum.c_str(); diff --git a/lite/parse.cpp b/lite/parse.cpp index 630f0d96..9036e722 100644 --- a/lite/parse.cpp +++ b/lite/parse.cpp @@ -521,6 +521,11 @@ CG *cg = ana->getCG(); // Cosmetic rewrite as for loop. // 11/10/99 AM. for (seq = ana->getSeq(); seq; seq = seq->Right()) { + // A run past its time limit runs no more passes. The pass that ran + // out stopped at its next while turn; the rest are skipped whole. // 10/25/26 DD. + if (overBudget(true)) + break; + ++rulepass_; // 02/03/05 AM. if (cg && !cg->getUserkbLoaded() // 07/11/26 DD. @@ -567,6 +572,82 @@ if (gui_.IsMessage(GUI_MESSAGE_ABORT)) // 11/10/99 DD. #endif } +/******************************************** +* FN: SETRUNLIMIT +* CR: 10/25/26 DD. +* SUBJ: Give this text's analysis a time limit, from now. +* NOTE: 0 or less is no limit, as before. +********************************************/ + +void Parse::setRunLimit(long long ms) +{ +runLimitMs_ = ms > 0 ? ms : 0; +runStopped_ = false; +runTicks_ = 0; +if (runLimitMs_) + runDeadline_ = std::chrono::steady_clock::now() + + std::chrono::milliseconds(runLimitMs_); +} + + +/******************************************** +* FN: OVERBUDGET +* CR: 10/25/26 DD. +* SUBJ: Has this run passed its time limit? +* RET: True once it has, and from then on. +* NOTE: Asked on every turn of every while loop, so it reads the clock only +* every 256th call -- a loop turn is a few microseconds, and the +* limit is in seconds. `now` reads it regardless, for the check +* between passes, which come too seldom to count. +* The first time it is over, the run is marked stopped (here and on +* the NLP object, for NLP_ENGINE::lastRunStopped) and says so once, +* in err.log and on stderr. +* +* WHY: NLP++ is a C++ extension. A grammar whose loop never ends +* cannot be interrupted from outside -- a Python caller has no +* thread to stop, only a process to kill -- and a loop that interns +* a string a turn grows until the process is killed for memory. +* One such loop took a document extractor down five times in half +* an hour on one document. With a limit it stops that run; the +* string table goes with the run, so the memory comes back. +********************************************/ + +bool Parse::overBudget(bool now) +{ +if (runStopped_) + return true; +if (!runLimitMs_) + return false; +if (!now && (++runTicks_ & 0xFF)) + return false; +if (std::chrono::steady_clock::now() < runDeadline_) + return false; + +runStopped_ = true; +if (nlp_) + nlp_->setRunStopped(true); + +_TCHAR *file = 0; +Dlist *seqs = ana ? ana->getSeqlist() : 0; +for (Delt *d = seqs ? seqs->getFirst() : 0; d; d = d->Right()) + if (d->getData()->getPassnum() == currpass_) + { + file = d->getData()->getRulesfilename(); + break; + } + +std::_t_strstream gerrStr; +gerrStr << _T("[Run stopped: past its time limit of ") << runLimitMs_ + << _T(" ms in pass ") << currpass_; +if (file) + gerrStr << _T(" (") << file << _T(")"); +gerrStr << _T("; the rest of the analysis was not run.]") << std::ends; +std::_t_cerr << gerrStr.str() << std::endl; +errOut(&gerrStr,false); +return true; +} + + /******************************************** * FN: FINEXECUTE * CR: 06/13/99 AM. diff --git a/nlp/main.cpp b/nlp/main.cpp index 801da19d..e9ce58df 100644 --- a/nlp/main.cpp +++ b/nlp/main.cpp @@ -15,9 +15,9 @@ All rights reserved. #include "lite/nlp_engine.h" #include "version.h" -#define NLP_ENGINE_VERSION "4.2.40" +#define NLP_ENGINE_VERSION "4.2.41" -bool cmdReadArgs(int, _TCHAR *argv[], _TCHAR *&, _TCHAR *&, _TCHAR *&, _TCHAR *&, bool &, bool &, bool &, bool &, bool &, bool &, int &, bool &, bool &, _TCHAR *&); +bool cmdReadArgs(int, _TCHAR *argv[], _TCHAR *&, _TCHAR *&, _TCHAR *&, _TCHAR *&, bool &, bool &, bool &, bool &, bool &, bool &, int &, bool &, bool &, _TCHAR *&, double &); void cmdHelpargs(_TCHAR *); #include "lite/nlpdebug.h" @@ -48,13 +48,14 @@ int _tmain( bool stream = false; // -STDIN: read input file paths from stdin. bool dictSource = false; // -DICTSOURCE: nodes record their .dict file and line. _TCHAR *datum = 0; // -DATUM value: G("$datum") in the analyzer. + double timeLimit = 0; // -TIMELIMIT seconds: stop a run past it. 0 = none. ///////////////////////////////////////////////// // GET APP INFORMATION ///////////////////////////////////////////////// // Get analyzer name, input and output filenames from command line. - if (!cmdReadArgs(argc, argv, analyzerpath, input, output, workdir, develop, compile, compiled, silent, compileKB, compileAna, debugPort, stream, dictSource, datum)) + if (!cmdReadArgs(argc, argv, analyzerpath, input, output, workdir, develop, compile, compiled, silent, compileKB, compileAna, debugPort, stream, dictSource, datum, timeLimit)) exit(1); if (dictSource) NLP_ENGINE::setDictSource(true); // #432. @@ -73,6 +74,8 @@ int _tmain( NLP_ENGINE *nlpEngine = new NLP_ENGINE(workdir ? workdir : _T("")); if (datum) nlpEngine->setDatum(datum); + if (timeLimit > 0) + nlpEngine->setRunLimit((long long)(timeLimit * 1000.0)); if (compile) { // Compile mode: generate C++ code for the analyzer and KB. @@ -109,8 +112,11 @@ int _tmain( } NlpDebug::runEnd(); NlpDebug::shutdown(); + // A run stopped at -TIMELIMIT exits 2: it is neither a success nor the + // missing analyzer or input that exits 1. // 10/25/26 DD. + bool stopped = nlpEngine->lastRunStopped(); delete nlpEngine; - return rc < 0 ? 1 : 0; + return rc < 0 ? 1 : (stopped ? 2 : 0); } /******************************************** @@ -137,7 +143,8 @@ bool cmdReadArgs( int &debugPort, // -debug : rule-level debug server. 0 = off. bool &stream, // -STDIN: read input file paths from stdin. bool &dictSource, // -DICTSOURCE: nodes record their .dict file and line. - _TCHAR *&datum // -DATUM value: G("$datum") in the analyzer. + _TCHAR *&datum, // -DATUM value: G("$datum") in the analyzer. + double &timeLimit // -TIMELIMIT seconds: stop a run past it. ) { _TCHAR *ptr; @@ -149,6 +156,7 @@ bool cmdReadArgs( bool f_work = false; bool f_debug = false; bool f_datum = false; + bool f_timelimit = false; bool flag = false; bool compiledck = false; // If compiled/interp arg seen. bool doubledash = false; @@ -233,6 +241,8 @@ bool cmdReadArgs( f_debug = flag = true; // Expecting a TCP port. else if (!strcmp_i(ptr, _T("datum"))) f_datum = flag = true; // Expecting G("$datum"). + else if (!strcmp_i(ptr, _T("timelimit"))) + f_timelimit = flag = true; // Expecting seconds. else if (!strcmp_i(ptr, _T("dev"))) // 12/25/98 AM. { if (silent) @@ -357,6 +367,18 @@ bool cmdReadArgs( datum = ptr; f_datum = flag = false; } + else if (f_timelimit) + { + timeLimit = _tstof(ptr); + if (timeLimit <= 0) + { + std::_t_cerr << _T("[") << argv[0] + << _T(": -TIMELIMIT needs a number of seconds above 0.]") << std::endl; + cmdHelpargs(argv[0]); + return false; + } + f_timelimit = flag = false; + } else if (f_debug) { debugPort = _ttoi(ptr); @@ -389,7 +411,7 @@ bool cmdReadArgs( { // The last argument was an option that takes a value, eg "-ANA". std::_t_cerr << _T("[") << argv[0] << _T(": ") - << (f_ana ? _T("-ANA") : f_in ? _T("-IN") : f_out ? _T("-OUT") : f_work ? _T("-WORK") : f_datum ? _T("-DATUM") : _T("-DEBUG")) + << (f_ana ? _T("-ANA") : f_in ? _T("-IN") : f_out ? _T("-OUT") : f_work ? _T("-WORK") : f_datum ? _T("-DATUM") : f_timelimit ? _T("-TIMELIMIT") : _T("-DEBUG")) << _T(" needs a value.]") << std::endl; cmdHelpargs(argv[0]); return false; @@ -438,6 +460,8 @@ void cmdHelpargs(_TCHAR *name) << _T(" a blank line or EOF ends the run. Each input is analyzed") << std::endl << _T(" when the next line arrives, so $islastfile can be set.") << std::endl << _T(" [-DATUM value] pass a string the analyzer reads as G(\"$datum\")") << std::endl + << _T(" [-TIMELIMIT seconds] stop an input's analysis that runs longer, at its") << std::endl + << _T(" next while turn or pass; the run then exits 2") << std::endl << _T(" [-DICTSOURCE] nodes built from .dict entries get dictfile and dictline") << std::endl << _T(" attributes naming the entry's file and line") << std::endl << std::endl From 94bf349f66a3e06b17b89639cc93b39fd38faff8 Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Sun, 25 Oct 2026 23:36:34 -0400 Subject: [PATCH 2/2] _tstof on Linux and macOS, for -TIMELIMIT my_tchar.h maps each name nlp uses to its char function on platforms without one; _tstof was not among them, so main.cpp did not compile there. Co-Authored-By: Claude Opus 5.5 --- include/Api/my_tchar.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/Api/my_tchar.h b/include/Api/my_tchar.h index 3131ba47..48ca3e41 100644 --- a/include/Api/my_tchar.h +++ b/include/Api/my_tchar.h @@ -93,6 +93,7 @@ All rights reserved. #define _tcsnccpy strncpy #define _tgetenv getenv #define _ttoi atoi +#define _tstof atof // nlp -TIMELIMIT seconds. // 10/25/26 DD. #define _tsystem system #define _stprintf sprintf #define _ftprintf fprintf