tinymux/mux/modules/engine/cque.cpp

2280 lines
62 KiB
C++
Raw Permalink Normal View History

/*! \file cque.cpp
* \brief Commands and functions for manipulating the command queue.
*
* This forms the upper-level command list queue, and includes timed commands
* and semaphores. The lower-level task implementation is found in timer.cpp.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
bool break_called = false;
static CLinearTimeDelta GetProcessorUsage(void)
{
CLinearTimeDelta ltd;
2008-01-29 12:16:54 -08:00
#if defined(WINDOWS_PROCESSES)
FILETIME ftCreate;
FILETIME ftExit;
FILETIME ftKernel;
FILETIME ftUser;
GetProcessTimes(GetCurrentProcess(), &ftCreate, &ftExit, &ftKernel, &ftUser);
ltd.Set100ns(*reinterpret_cast<int64_t*>(&ftUser));
2008-01-29 12:16:54 -08:00
#endif // WINDOWS_PROCESSES
#if defined(UNIX_PROCESSES)
#if defined(HAVE_GETRUSAGE)
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
ltd.SetTimeValueStruct(&usage.ru_utime);
#else
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
ltd = ltaNow - mudstate.start_time;
#endif
#endif
return ltd;
}
// ---------------------------------------------------------------------------
// add_to: Adjust an object's queue or semaphore count.
//
static bool add_to(const dbref executor, const int am, int attrnum, int *pnum)
{
int aflags;
dbref aowner;
LBuf atr_gotten = LBuf_Adopt(atr_get("add_to.68", executor, attrnum, &aowner, &aflags));
// int64_t, not int (#1402): semaphore counts are attribute integers.
//
int64_t num = mux_atoi64(atr_gotten);
num += am;
UTF8 buff[I64BUF_SIZE];
size_t nlen = 0;
*buff = '\0';
if (num)
{
nlen = mux_i64toa(num, buff);
}
if (!atr_add_raw_LEN(executor, attrnum, buff, nlen))
{
STARTLOG(LOG_PROBLEMS, "QUE", "SEMAPH");
log_printf(T("add_to(#%d/%d): failed to persist semaphore count %lld."),
executor, attrnum, static_cast<long long>(num));
ENDLOG;
return false;
}
if (pnum)
{
// Caller still takes int; clamp after full-width arithmetic.
//
if (num > INT_MAX)
{
*pnum = INT_MAX;
}
else if (num < INT_MIN)
{
*pnum = INT_MIN;
}
else
{
*pnum = static_cast<int>(num);
}
}
return true;
}
// Max recursive depth for process_command_list_inline (@include,
// @dolist/now, and any other inline action-list expansion). Bounds
// stack growth from mutual @include recursion.
//
static const int MAX_INCLUDE_NEST = 50;
engine: @dolist/now/break switch; ;| piping in inline command lists (#788) Two follow-ups to the @dolist/now rewrite (37e18bef5): - New /break switch: an @break inside the body still stops the loop, but with /break it also propagates to the enclosing command list, aborting the commands after the @dolist. This restores the one ability the containment change removed (using a break inside the loop to abort the rest of a $-command), with sane loop-stop semantics, and mirrors @include (propagate by default, /nobreak to contain). Without /now the switch is inert. - ;| pipe segments now work in inline command lists. The queued runner has always special-cased '|' after ';' to capture the previous command's output as %|; the inline loops in @dolist/now and @include treated such a segment as a literal command starting with '|' ("Huh?"). The splitting/pipe/break loop is factored into process_command_list_inline() in cque.cpp, shared by both callers and kept next to its queued twin so the semantics cannot drift. The inline list is its own pipe domain (save/restore of the enclosing pipe context), matching how each queued entry starts with a clean pipe state -- an inline list running inside a piped segment cannot corrupt the outer capture. help @dolist documents /break and the ;| support. Verified: default containment, /break propagation, and /break with no inner break all behave as specified; pipes carry %| through inline dolist and include bodies; smoke 1115/0/0 with three new test cases; 2x200 jit_diff clean. Closes #788. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:00:21 -06:00
// process_command_list_inline: run a semicolon-separated command list
// synchronously, honoring @break/@assert (the loop stops when
// break_called fires) and ;| piping (%|), exactly like the queued
// runner below. Used by @dolist/now and @include (#788).
//
// The list is its own pipe domain, like a fresh queue entry: the
// enclosing pipe context is saved and restored, so an inline list can
// itself run inside a piped segment without corrupting the outer
// capture, and pipes inside the list behave as they would queued.
//
// parse_to() rewrites the buffer in place; callers pass a private copy.
//
void process_command_list_inline(dbref executor, dbref caller, dbref enactor,
int eval, UTF8 *clist,
const UTF8 *cargs[], int ncargs)
{
if (mudstate.include_nest_lev >= MAX_INCLUDE_NEST)
{
notify(executor, M_("Include nesting limit exceeded."));
return;
}
mudstate.include_nest_lev++;
engine: @dolist/now/break switch; ;| piping in inline command lists (#788) Two follow-ups to the @dolist/now rewrite (37e18bef5): - New /break switch: an @break inside the body still stops the loop, but with /break it also propagates to the enclosing command list, aborting the commands after the @dolist. This restores the one ability the containment change removed (using a break inside the loop to abort the rest of a $-command), with sane loop-stop semantics, and mirrors @include (propagate by default, /nobreak to contain). Without /now the switch is inert. - ;| pipe segments now work in inline command lists. The queued runner has always special-cased '|' after ';' to capture the previous command's output as %|; the inline loops in @dolist/now and @include treated such a segment as a literal command starting with '|' ("Huh?"). The splitting/pipe/break loop is factored into process_command_list_inline() in cque.cpp, shared by both callers and kept next to its queued twin so the semantics cannot drift. The inline list is its own pipe domain (save/restore of the enclosing pipe context), matching how each queued entry starts with a clean pipe state -- an inline list running inside a piped segment cannot corrupt the outer capture. help @dolist documents /break and the ;| support. Verified: default containment, /break propagation, and /break with no inner break all behave as specified; pipes carry %| through inline dolist and include bodies; smoke 1115/0/0 with three new test cases; 2x200 jit_diff clean. Closes #788. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:00:21 -06:00
UTF8 *save_pout = mudstate.pout;
UTF8 *save_poutnew = mudstate.poutnew;
UTF8 *save_poutbufc = mudstate.poutbufc;
dbref save_poutobj = mudstate.poutobj;
bool save_inpipe = mudstate.inpipe;
int save_nest = mudstate.pipe_nest_lev;
mudstate.pout = nullptr;
mudstate.poutnew = nullptr;
mudstate.poutbufc = nullptr;
mudstate.poutobj = NOTHING;
mudstate.inpipe = false;
mudstate.pipe_nest_lev = 0;
while ( clist
&& !break_called)
{
UTF8 *cp = parse_to(&clist, ';', EV_STRIP_AROUND);
if ( cp
&& *cp)
{
// Will this command be piped into the next?
//
if ( clist
&& *clist == '|'
&& mudstate.pipe_nest_lev < mudconf.ntfy_nest_lim)
{
clist++;
mudstate.pipe_nest_lev++;
mudstate.inpipe = true;
mudstate.poutnew = alloc_lbuf("inline_list.pipe");
mudstate.poutbufc = mudstate.poutnew;
mudstate.poutobj = executor;
}
else
{
mudstate.inpipe = false;
mudstate.poutobj = NOTHING;
}
process_command(executor, caller, enactor, eval, false, cp,
cargs, ncargs);
// Transition %| value.
//
if (mudstate.pout)
{
free_lbuf(mudstate.pout);
mudstate.pout = nullptr;
}
if (mudstate.poutnew)
{
*mudstate.poutbufc = '\0';
mudstate.pout = mudstate.poutnew;
mudstate.poutnew = nullptr;
mudstate.poutbufc = nullptr;
}
}
}
// Clean up this list's %| and restore the enclosing pipe context.
//
if (mudstate.pout)
{
free_lbuf(mudstate.pout);
mudstate.pout = nullptr;
}
if (mudstate.poutnew)
{
free_lbuf(mudstate.poutnew);
mudstate.poutnew = nullptr;
mudstate.poutbufc = nullptr;
}
mudstate.pout = save_pout;
mudstate.poutnew = save_poutnew;
mudstate.poutbufc = save_poutbufc;
mudstate.poutobj = save_poutobj;
mudstate.inpipe = save_inpipe;
mudstate.pipe_nest_lev = save_nest;
mudstate.include_nest_lev--;
engine: @dolist/now/break switch; ;| piping in inline command lists (#788) Two follow-ups to the @dolist/now rewrite (37e18bef5): - New /break switch: an @break inside the body still stops the loop, but with /break it also propagates to the enclosing command list, aborting the commands after the @dolist. This restores the one ability the containment change removed (using a break inside the loop to abort the rest of a $-command), with sane loop-stop semantics, and mirrors @include (propagate by default, /nobreak to contain). Without /now the switch is inert. - ;| pipe segments now work in inline command lists. The queued runner has always special-cased '|' after ';' to capture the previous command's output as %|; the inline loops in @dolist/now and @include treated such a segment as a literal command starting with '|' ("Huh?"). The splitting/pipe/break loop is factored into process_command_list_inline() in cque.cpp, shared by both callers and kept next to its queued twin so the semantics cannot drift. The inline list is its own pipe domain (save/restore of the enclosing pipe context), matching how each queued entry starts with a clean pipe state -- an inline list running inside a piped segment cannot corrupt the outer capture. help @dolist documents /break and the ;| support. Verified: default containment, /break propagation, and /break with no inner break all behave as specified; pipes carry %| through inline dolist and include bodies; smoke 1115/0/0 with three new test cases; 2x200 jit_diff clean. Closes #788. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:00:21 -06:00
}
// This Task assumes that pEntry is already unlinked from any lists it may
// have been related to.
//
2022-03-14 15:56:59 -06:00
static void Task_RunQueueEntry(void *pEntry, const int iUnused)
{
UNUSED_PARAMETER(iUnused);
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(pEntry);
const dbref executor = point->executor;
if ( Good_obj(executor)
&& !Going(executor))
{
giveto(executor, mudconf.waitcost);
mudstate.curr_enactor = point->enactor;
mudstate.curr_executor = executor;
a_Queue(Owner(executor), -1);
point->executor = NOTHING;
if (!Halted(executor))
{
// Load scratch args.
//
for (int i = 0; i < MAX_GLOBAL_REGS; i++)
{
if (mudstate.global_regs[i])
{
RegRelease(mudstate.global_regs[i]);
2018-10-03 17:54:51 +00:00
mudstate.global_regs[i] = nullptr;
}
mudstate.global_regs[i] = point->scr[i];
2018-10-03 17:54:51 +00:00
point->scr[i] = nullptr;
}
NamedRegsClear(mudstate.named_regs);
mudstate.named_regs = point->named_scr;
point->named_scr = nullptr;
#if defined(STUB_SLAVE)
2018-10-03 17:54:51 +00:00
if (nullptr != mudstate.pResultsSet)
{
2007-12-18 18:36:20 -08:00
mudstate.pResultsSet->Release();
2018-10-03 17:54:51 +00:00
mudstate.pResultsSet = nullptr;
}
2007-12-18 18:36:20 -08:00
mudstate.pResultsSet = point->pResultsSet;
2018-10-03 17:54:51 +00:00
point->pResultsSet = nullptr;
mudstate.iRow = point->iRow;
#endif // STUB_SLAVE
// Restore iter/switch context from queue entry.
//
bool bIterContext = (point->iter_token != nullptr);
bool bSwitchContext = (point->switch_token != nullptr);
const UTF8 *save_switch = nullptr;
if (bIterContext)
{
bool bLoopInBounds = ( 0 <= mudstate.in_loop
&& mudstate.in_loop < MAX_ITEXT);
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = point->iter_token;
mudstate.inum[mudstate.in_loop] = point->iter_number;
}
mudstate.in_loop++;
}
if (bSwitchContext)
{
save_switch = mudstate.switch_token;
mudstate.switch_token = point->switch_token;
}
UTF8 *command = point->comm;
mux_assert(!mudstate.inpipe);
mux_assert(mudstate.pipe_nest_lev == 0);
mux_assert(mudstate.poutobj == NOTHING);
mux_assert(!mudstate.pout);
break_called = false;
while ( command
&& !break_called)
{
mux_assert(!mudstate.poutnew);
mux_assert(!mudstate.poutbufc);
UTF8 *cp = parse_to(&command, ';', EV_STRIP_AROUND);
if ( cp
&& *cp)
{
// Will command be piped?
//
if ( command
&& *command == '|'
&& mudstate.pipe_nest_lev < mudconf.ntfy_nest_lim)
{
command++;
mudstate.pipe_nest_lev++;
mudstate.inpipe = true;
mudstate.poutnew = alloc_lbuf("process_command.pipe");
mudstate.poutbufc = mudstate.poutnew;
mudstate.poutobj = executor;
}
else
{
mudstate.inpipe = false;
mudstate.poutobj = NOTHING;
}
CLinearTimeAbsolute ltaBegin;
ltaBegin.GetUTC();
alarm_clock.set(mudconf.max_cmdsecs);
CLinearTimeDelta ltdUsageBegin = GetProcessorUsage();
2022-03-14 15:56:59 -06:00
const UTF8 *log_cmdbuf = process_command(executor, point->caller,
point->enactor, point->eval, false, cp, const_cast<const UTF8**>(point->env),
point->nargs);
CLinearTimeAbsolute ltaEnd;
ltaEnd.GetUTC();
if (alarm_clock.alarmed)
{
notify(executor, M_("GAME: Expensive activity abbreviated."));
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
// The command has already returned by this point --
// polling loops abbreviated it mid-run -- so the HALT
// only quarantines future work. That is right for a
// machine object, whose future work is more of the
// same runaway, but wrong for a wizard player, whose
// future work is the next typed command -- often the
// one that fixes the problem. Don't dark the admin.
//
// halt_que's first arg is the owner key (matched
// against Owner(entry->executor)), not the running
// object. For a non-player object, halt only that
// object's entries; for a player, halt all of theirs.
// Good_obj before touching flags (enactor/executor
// may be gone by the time the alarm fires).
//
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
bool bExemptEnactor = isPlayer(point->enactor)
&& Wizard(point->enactor);
bool bExemptExecutor = isPlayer(executor)
&& Wizard(executor);
if ( !bExemptEnactor
&& Good_obj(point->enactor))
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
{
s_Halted(point->enactor);
if (isPlayer(point->enactor))
{
halt_que(point->enactor, NOTHING);
}
else
{
halt_que(Owner(point->enactor), point->enactor);
}
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
}
if ( !bExemptExecutor
&& Good_obj(executor))
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
{
s_Halted(executor);
if (isPlayer(executor))
{
halt_que(executor, NOTHING);
}
else
{
halt_que(Owner(executor), executor);
}
fix: exempt wizard players from the CPU guard's collateral HALT When a command exceeds max_cmdsecs, the runaway guard abbreviates it (via the polled alarm flag) and then HALTs both the enactor and the executor. The HALT fires after process_command has already returned, so it never protects the current timeslice -- it only quarantines future work. That is the right response for a machine object, whose future work is more of the same runaway loop, but wrong for a wizard player, whose future work is the next typed command -- often the one that fixes the problem. Classic casualty: an admin running @dbclean as God left #1 HALTED, with its queue flushed and wait_que refusing new entries, until the flag was cleared by hand. Both guard sites now skip the HALT flag and the halt_que flush, per-object, when the target is a wizard player (God included): the queue-side guard in Task_RunQueueEntry and the interactive guard in the network command path. Machine objects -- even wizard-owned ones -- and mortal players quarantine exactly as before, and abbreviation itself is untouched for everyone. A LOG_PROBLEMS line records each suppressed HALT, closing the visibility gap where the default rpt_cmdsecs (120s) exceeds max_cmdsecs (60s). This also fixes a latent no-op: the executor-side s_Flags call used point->executor, which the runner sets to NOTHING before executing, so it stamped the db[-1] sentinel slot (and issued a junk SQLite UpdateFlags(-1)) instead of the real executor. The enactor line was what actually darked #1. The guard now uses the correct local. No smoke case: exercising the guard needs a multi-second burn against a lowered lag_limit, which is timing-dependent and wrong for the harness. Verified by direct muxscript probes across all three lanes (wizard player exempt and live; machine object HALTed; mortal player HALTed) plus a clean full smoke run. Closes #896. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:11:32 -06:00
}
if ( bExemptEnactor
|| bExemptExecutor)
{
STARTLOG(LOG_PROBLEMS, "CMD", "CPU");
log_name_and_loc(executor);
log_text(T(" expensive activity abbreviated; wizard player exempted from HALT"));
ENDLOG;
}
}
alarm_clock.clear();
CLinearTimeDelta ltdUsageEnd = GetProcessorUsage();
CLinearTimeDelta ltd = ltdUsageEnd - ltdUsageBegin;
db[executor].cpu_time_used += ltd;
ltd = ltaEnd - ltaBegin;
if (mudconf.rpt_cmdsecs < ltd)
{
STARTLOG(LOG_PROBLEMS, "CMD", "CPU");
log_name_and_loc(executor);
LBuf logbuf = LBuf_Src("do_top.LOG.cpu");
mux_sprintf(logbuf, LBUF_SIZE, T(" queued command taking %s secs (enactor #%d): "),
ltd.ReturnSecondsString(4), point->enactor);
log_text(logbuf);
log_text(log_cmdbuf);
ENDLOG;
}
}
// Transition %| value.
//
if (mudstate.pout)
{
free_lbuf(mudstate.pout);
2018-10-03 17:54:51 +00:00
mudstate.pout = nullptr;
}
if (mudstate.poutnew)
{
*mudstate.poutbufc = '\0';
mudstate.pout = mudstate.poutnew;
2018-10-03 17:54:51 +00:00
mudstate.poutnew = nullptr;
mudstate.poutbufc = nullptr;
}
}
// Clean up %| value.
//
if (mudstate.pout)
{
free_lbuf(mudstate.pout);
2018-10-03 17:54:51 +00:00
mudstate.pout = nullptr;
}
mudstate.pipe_nest_lev = 0;
mudstate.inpipe = false;
mudstate.poutobj = NOTHING;
// Restore iter/switch context.
//
if (bIterContext)
{
mudstate.in_loop--;
bool bLoopInBounds = ( 0 <= mudstate.in_loop
&& mudstate.in_loop < MAX_ITEXT);
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = nullptr;
mudstate.inum[mudstate.in_loop] = 0;
}
}
if (bSwitchContext)
{
mudstate.switch_token = save_switch;
}
}
}
2022-03-14 15:56:59 -06:00
for (auto& i : point->scr)
{
2022-03-14 15:56:59 -06:00
if (i)
{
2022-03-14 15:56:59 -06:00
RegRelease(i);
i = nullptr;
}
2022-03-14 15:56:59 -06:00
}
NamedRegsClear(point->named_scr);
2022-03-14 15:56:59 -06:00
for (auto& global_reg : mudstate.global_regs)
{
if (global_reg)
{
2022-03-14 15:56:59 -06:00
RegRelease(global_reg);
global_reg = nullptr;
}
}
NamedRegsClear(mudstate.named_regs);
#if defined(STUB_SLAVE)
mudstate.iRow = RS_TOP;
2018-10-03 17:54:51 +00:00
if (nullptr != mudstate.pResultsSet)
{
2007-12-18 18:36:20 -08:00
mudstate.pResultsSet->Release();
2018-10-03 17:54:51 +00:00
mudstate.pResultsSet = nullptr;
}
#endif // STUB_SLAVE
if (point->switch_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->switch_token);
point->switch_token = nullptr;
}
if (point->iter_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->iter_token);
point->iter_token = nullptr;
}
MEMFREE(point->text);
2018-10-03 17:54:51 +00:00
point->text = nullptr;
free_qentry(point);
}
// ---------------------------------------------------------------------------
// que_want: Do we want this queue entry?
//
2022-03-14 15:56:59 -06:00
static bool que_want(const BQUE *entry, const dbref ptarg, dbref otarg)
{
if ( ptarg != NOTHING
&& ptarg != Owner(entry->executor))
{
return false;
}
return ( otarg == NOTHING
|| otarg == entry->executor);
}
2022-03-14 15:56:59 -06:00
static void Task_SemaphoreTimeout(void *pExpired, const int iUnused)
{
UNUSED_PARAMETER(iUnused);
// A semaphore has timed out.
//
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(pExpired);
(void)add_to(point->u.s.sem, -1, point->u.s.attr, nullptr);
point->u.s.sem = NOTHING;
Task_RunQueueEntry(point, 0);
}
2022-03-14 15:56:59 -06:00
void Task_SQLTimeout(void *pExpired, const int iUnused)
{
UNUSED_PARAMETER(iUnused);
// A SQL Query has timed out. Actually, this isn't supported.
//
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(pExpired);
Task_RunQueueEntry(point, 0);
}
static dbref Halt_Player_Target;
static dbref Halt_Object_Target;
static int Halt_Entries;
static dbref Halt_Player_Run;
static dbref Halt_Entries_Run;
2022-03-14 15:56:59 -06:00
static int CallBack_HaltQueue(const PTASK_RECORD p)
{
if ( p->fpTask == Task_RunQueueEntry
|| p->fpTask == Task_SQLTimeout
|| p->fpTask == Task_SemaphoreTimeout)
{
// This is a @wait, timed Semaphore Task, or timed SQL Query.
//
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(p->arg_voidptr);
if (que_want(point, Halt_Player_Target, Halt_Object_Target))
{
// Accounting for pennies and queue quota.
//
dbref dbOwner = point->executor;
if (!isPlayer(dbOwner))
{
dbOwner = Owner(dbOwner);
}
if (dbOwner != Halt_Player_Run)
{
if (Halt_Player_Run != NOTHING)
{
giveto(Halt_Player_Run, mudconf.waitcost * Halt_Entries_Run);
a_Queue(Halt_Player_Run, -Halt_Entries_Run);
}
Halt_Player_Run = dbOwner;
Halt_Entries_Run = 0;
}
Halt_Entries++;
Halt_Entries_Run++;
if (p->fpTask == Task_SemaphoreTimeout)
{
(void)add_to(point->u.s.sem, -1, point->u.s.attr, nullptr);
}
2022-03-14 15:56:59 -06:00
for (auto& i : point->scr)
{
2022-03-14 15:56:59 -06:00
if (i)
{
2022-03-14 15:56:59 -06:00
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(point->named_scr);
if (point->switch_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->switch_token);
point->switch_token = nullptr;
}
if (point->iter_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->iter_token);
point->iter_token = nullptr;
}
MEMFREE(point->text);
2018-10-03 17:54:51 +00:00
point->text = nullptr;
free_qentry(point);
return IU_REMOVE_TASK;
}
}
return IU_NEXT_TASK;
}
// ------------------------------------------------------------------
//
// halt_que: Remove all queued commands that match (executor, object).
//
// (NOTHING, NOTHING) matches all queue entries.
// (NOTHING, <object>) matches only queue entries run from <object>.
// (<executor>, NOTHING) matches only queue entries owned by <executor>.
// (<executor>, <object>) matches only queue entries run from <objects>
// and owned by <executor>.
//
2022-03-14 15:56:59 -06:00
int halt_que(const dbref executor, const dbref object)
{
Halt_Player_Target = executor;
Halt_Object_Target = object;
Halt_Entries = 0;
Halt_Player_Run = NOTHING;
Halt_Entries_Run = 0;
// Process @wait, timed semaphores, and untimed semaphores.
//
scheduler.TraverseUnordered(CallBack_HaltQueue);
if (Halt_Player_Run != NOTHING)
{
giveto(Halt_Player_Run, mudconf.waitcost * Halt_Entries_Run);
a_Queue(Halt_Player_Run, -Halt_Entries_Run);
Halt_Player_Run = NOTHING;
}
return Halt_Entries;
}
static uint64_t Halt_Pid_Target;
static dbref Halt_Pid_Executor;
static int Halt_Pid_Entries;
static dbref Halt_Pid_Player_Run;
static int Halt_Pid_Entries_Run;
static int CallBack_HaltQueueByPid(const PTASK_RECORD p)
{
if ( p->fpTask == Task_RunQueueEntry
|| p->fpTask == Task_SQLTimeout
|| p->fpTask == Task_SemaphoreTimeout)
{
if (p->m_Ticket == Halt_Pid_Target)
{
const auto point = static_cast<BQUE*>(p->arg_voidptr);
// Ownership check: non-wizards can only halt their own entries.
//
if ( !Can_Halt(Halt_Pid_Executor)
&& Owner(point->executor) != Owner(Halt_Pid_Executor))
{
return IU_NEXT_TASK;
}
// Accounting for pennies and queue quota.
//
dbref dbOwner = point->executor;
if (!isPlayer(dbOwner))
{
dbOwner = Owner(dbOwner);
}
if (dbOwner != Halt_Pid_Player_Run)
{
if (Halt_Pid_Player_Run != NOTHING)
{
giveto(Halt_Pid_Player_Run, mudconf.waitcost * Halt_Pid_Entries_Run);
a_Queue(Halt_Pid_Player_Run, -Halt_Pid_Entries_Run);
}
Halt_Pid_Player_Run = dbOwner;
Halt_Pid_Entries_Run = 0;
}
Halt_Pid_Entries++;
Halt_Pid_Entries_Run++;
if (p->fpTask == Task_SemaphoreTimeout)
{
(void)add_to(point->u.s.sem, -1, point->u.s.attr, nullptr);
}
for (auto& i : point->scr)
{
if (i)
{
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(point->named_scr);
if (point->switch_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->switch_token);
point->switch_token = nullptr;
}
if (point->iter_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->iter_token);
point->iter_token = nullptr;
}
MEMFREE(point->text);
point->text = nullptr;
free_qentry(point);
return IU_REMOVE_TASK;
}
}
return IU_NEXT_TASK;
}
static int halt_que_pid(const dbref executor, const uint64_t pid)
{
Halt_Pid_Target = pid;
Halt_Pid_Executor = executor;
Halt_Pid_Entries = 0;
Halt_Pid_Player_Run = NOTHING;
Halt_Pid_Entries_Run = 0;
scheduler.TraverseUnordered(CallBack_HaltQueueByPid);
if (Halt_Pid_Player_Run != NOTHING)
{
giveto(Halt_Pid_Player_Run, mudconf.waitcost * Halt_Pid_Entries_Run);
a_Queue(Halt_Pid_Player_Run, -Halt_Pid_Entries_Run);
Halt_Pid_Player_Run = NOTHING;
}
return Halt_Pid_Entries;
}
// ---------------------------------------------------------------------------
// do_halt: Command interface to halt_que.
//
2022-03-14 15:56:59 -06:00
void do_halt(const dbref executor, const dbref caller, dbref enactor, const int eval, int key, UTF8 *target, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref executor_targ, obj_targ;
if ((key & HALT_ALL) && !Can_Halt(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
// Halt by PID.
//
if (key & HALT_PID)
{
if (key & HALT_ALL)
{
notify(executor, M_("Cant specify /pid and /all"));
return;
}
if (!target || !*target)
{
notify(executor, M_("You must specify a PID."));
return;
}
const UTF8 *p = target;
while (mux_isspace(*p))
{
p++;
}
if ('\0' == *p || '-' == *p)
{
notify(executor, M_("PID must be a non-negative integer."));
return;
}
if ('+' == *p)
{
p++;
}
uint64_t pid = 0;
bool bHaveDigits = false;
while (mux_isdigit(*p))
{
bHaveDigits = true;
const uint64_t digit = static_cast<uint64_t>(*p - '0');
if (pid > (UINT64_MAX - digit) / 10U)
{
notify(executor, M_("PID is out of range."));
return;
}
pid = 10U * pid + digit;
p++;
}
while (mux_isspace(*p))
{
p++;
}
if ( !bHaveDigits
|| '\0' != *p)
{
notify(executor, M_("PID must be a non-negative integer."));
return;
}
const int numhalted = halt_que_pid(executor, pid);
if (!Quiet(executor))
{
if (0 == numhalted)
{
notify(Owner(executor), M_("No queue entry with that PID was found."));
}
else
{
// #1661 / #1622: count goes to the catalogue, not English y/ies.
//
notify(Owner(executor), tprintf(MN_("%d queue entry removed.",
"%d queue entries removed.", numhalted), numhalted));
}
}
return;
}
// Figure out what to halt.
//
if (!target || !*target)
{
obj_targ = NOTHING;
if (key & HALT_ALL)
{
executor_targ = NOTHING;
}
else
{
executor_targ = Owner(executor);
if (!isPlayer(executor))
{
obj_targ = executor;
}
}
}
else
{
if (Can_Halt(executor))
{
obj_targ = match_thing(executor, target);
}
else
{
obj_targ = match_controlled(executor, target);
}
if (!Good_obj(obj_targ))
{
return;
}
if (key & HALT_ALL)
{
notify(executor, M_("Cant specify a target and /all"));
return;
}
if (isPlayer(obj_targ))
{
executor_targ = obj_targ;
obj_targ = NOTHING;
}
else
{
executor_targ = NOTHING;
}
}
2022-03-14 15:56:59 -06:00
const int numhalted = halt_que(executor_targ, obj_targ);
if (Quiet(executor))
{
return;
}
// #1661 / #1622: count goes to the catalogue, not English y/ies.
//
notify(Owner(executor), tprintf(MN_("%d queue entry removed.",
"%d queue entries removed.", numhalted), numhalted));
}
static int Notify_Key;
static int Notify_Num_Done;
static int Notify_Num_Max;
static int Notify_Sem;
static int Notify_Attr;
// NFY_DRAIN or NFY_NFYALL
//
static int CallBack_NotifySemaphoreDrainOrAll(PTASK_RECORD p)
{
if (p->fpTask == Task_SemaphoreTimeout)
{
// This represents a semaphore.
//
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(p->arg_voidptr);
if ( point->u.s.sem == Notify_Sem
&& ( point->u.s.attr == Notify_Attr
|| !Notify_Attr))
{
Notify_Num_Done++;
2007-11-26 22:23:34 -08:00
if (NFY_DRAIN == (Notify_Key & NFY_MASK))
{
// Discard the command
//
giveto(point->executor, mudconf.waitcost);
a_Queue(Owner(point->executor), -1);
2022-03-14 15:56:59 -06:00
for (auto& i : point->scr)
{
2022-03-14 15:56:59 -06:00
if (i)
{
2022-03-14 15:56:59 -06:00
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(point->named_scr);
if (point->switch_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->switch_token);
point->switch_token = nullptr;
}
if (point->iter_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
MEMFREE(point->iter_token);
point->iter_token = nullptr;
}
MEMFREE(point->text);
2018-10-03 17:54:51 +00:00
point->text = nullptr;
free_qentry(point);
return IU_REMOVE_TASK;
}
else
{
// Allow the command to run. The priority may have been
// PRIORITY_SUSPEND, so we need to change it.
//
if (isPlayer(point->enactor))
{
p->iPriority = PRIORITY_PLAYER;
}
else
{
p->iPriority = PRIORITY_OBJECT;
}
p->ltaWhen.GetUTC();
p->fpTask = Task_RunQueueEntry;
return IU_UPDATE_TASK;
}
}
}
return IU_NEXT_TASK;
}
2007-11-26 22:23:34 -08:00
// NFY_NFY
//
2007-11-26 22:23:34 -08:00
static int CallBack_NotifySemaphoreFirst(PTASK_RECORD p)
{
// If we've notified enough, exit.
//
2007-11-26 22:23:34 -08:00
if ( NFY_NFY == (Notify_Key & NFY_MASK)
&& Notify_Num_Done >= Notify_Num_Max)
{
return IU_DONE;
}
if (p->fpTask == Task_SemaphoreTimeout)
{
// This represents a semaphore.
//
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(p->arg_voidptr);
if ( point->u.s.sem == Notify_Sem
&& ( point->u.s.attr == Notify_Attr
|| !Notify_Attr))
{
Notify_Num_Done++;
// Allow the command to run. The priority may have been
// PRIORITY_SUSPEND, so we need to change it.
//
if (isPlayer(point->enactor))
{
p->iPriority = PRIORITY_PLAYER;
}
else
{
p->iPriority = PRIORITY_OBJECT;
}
p->ltaWhen.GetUTC();
p->fpTask = Task_RunQueueEntry;
return IU_UPDATE_TASK;
}
}
return IU_NEXT_TASK;
}
// ---------------------------------------------------------------------------
// nfy_que: Notify commands from the queue and perform or discard them.
int nfy_que(dbref sem, int attr, int key, int count)
{
int cSemaphore = 1;
if (attr)
{
int aflags;
dbref aowner;
LBuf str = LBuf_Adopt(atr_get("nfy_que.562", sem, attr, &aowner, &aflags));
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373) Completes the sweep the issue called for. mux_atol returns long, which is 32-bit on LLP64, so every caller silently truncated on Windows. Two of those were real defects (the truthiness family and cf_size, fixed in the preceding commits); the rest were latent, waiting for a value large enough to matter. Rather than audit 290 sites for whether each can reach 2^31 today, use the 64-bit parser everywhere and remove the class. A dbref cannot overflow now, but nothing stops a later caller passing that same site a timestamp or a byte count. Pure 1:1 substitution: 285 lines changed, and every removed line contained mux_atol while every added line contains mux_atoi64. No control flow, no types, no behaviour beyond the wider parse. This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so the generated code there is unchanged. It only widens the parse on Windows. Narrowing destinations are unaffected either way: `int x = mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both models. Left alone: mux_atol itself in mathutil, its declaration, and three comments that name it. Callers that genuinely want 32-bit semantics can still ask for them; none appear to. Verified on Windows: full solution builds clean with no new warnings, smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched -- identical to before the sweep, with the same 16 build-configuration failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST). Spot checks after the change: the boolean family returns 1 for multiples of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited, and arithmetic, string and list functions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
cSemaphore = mux_atoi64(str);
}
Notify_Num_Done = 0;
if (0 < cSemaphore)
{
Notify_Key = key;
Notify_Sem = sem;
Notify_Attr = attr;
Notify_Num_Max = count;
2007-11-26 22:23:34 -08:00
if (NFY_NFY == (key & NFY_MASK))
{
2007-11-26 22:23:34 -08:00
scheduler.TraverseOrdered(CallBack_NotifySemaphoreFirst);
}
else
{
scheduler.TraverseUnordered(CallBack_NotifySemaphoreDrainOrAll);
}
}
// Update the sem waiters count.
//
2007-11-26 22:23:34 -08:00
if (NFY_NFY == (key & NFY_MASK))
{
(void)add_to(sem, -count, attr, nullptr);
}
else
{
if (!atr_clr(sem, attr))
{
STARTLOG(LOG_PROBLEMS, "QUE", "SEMAPH");
log_printf(T("nfy_que(#%d/%d): failed to clear semaphore count."), sem, attr);
ENDLOG;
}
}
return Notify_Num_Done;
}
// ---------------------------------------------------------------------------
// do_notify: Command interface to nfy_que
void do_notify
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *what,
UTF8 *count,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
2022-03-14 15:56:59 -06:00
const UTF8 *obj = parse_to(&what, '/', 0);
init_match(executor, obj, NOTYPE);
match_everything(0);
2022-03-14 15:56:59 -06:00
const dbref thing = noisy_match_result();
if (!Good_obj(thing))
{
return;
}
if (!Controls(executor, thing) && !Link_ok(thing))
{
notify(executor, NOPERM_MESSAGE);
}
else
{
int atr = A_SEMAPHORE;
if ( what
&& what[0] != '\0')
{
2022-03-14 15:56:59 -06:00
const UTF8 *AttributeName = what;
const int i = mkattr(executor, AttributeName);
if (0 < i)
{
atr = i;
if (atr != A_SEMAPHORE)
{
// Do they have permission to set this attribute?
//
ATTR *ap = static_cast<ATTR *>(anum_get(atr));
if (!bCanSetAttr(executor, thing, ap))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
}
}
}
int loccount;
if ( count
&& count[0] != '\0')
{
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373) Completes the sweep the issue called for. mux_atol returns long, which is 32-bit on LLP64, so every caller silently truncated on Windows. Two of those were real defects (the truthiness family and cf_size, fixed in the preceding commits); the rest were latent, waiting for a value large enough to matter. Rather than audit 290 sites for whether each can reach 2^31 today, use the 64-bit parser everywhere and remove the class. A dbref cannot overflow now, but nothing stops a later caller passing that same site a timestamp or a byte count. Pure 1:1 substitution: 285 lines changed, and every removed line contained mux_atol while every added line contains mux_atoi64. No control flow, no types, no behaviour beyond the wider parse. This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so the generated code there is unchanged. It only widens the parse on Windows. Narrowing destinations are unaffected either way: `int x = mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both models. Left alone: mux_atol itself in mathutil, its declaration, and three comments that name it. Callers that genuinely want 32-bit semantics can still ask for them; none appear to. Verified on Windows: full solution builds clean with no new warnings, smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched -- identical to before the sweep, with the same 16 build-configuration failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST). Spot checks after the change: the boolean family returns 1 for multiples of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited, and arithmetic, string and list functions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
loccount = mux_atoi64(count);
}
else
{
loccount = 1;
}
if (0 < loccount)
{
nfy_que(thing, atr, key, loccount);
2007-11-26 22:23:34 -08:00
if ( !Quiet(executor)
&& !Quiet(thing)
&& !(key & NFY_QUIET))
{
2007-11-26 22:23:34 -08:00
if (NFY_DRAIN == (key & NFY_MASK))
{
notify_quiet(executor, M_("Drained."));
}
else
{
notify_quiet(executor, M_("Notified."));
}
}
}
}
}
// ---------------------------------------------------------------------------
// setup_que: Set up a queue entry.
//
static BQUE *setup_que
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
UTF8 *command,
int nargs,
const UTF8 *args[],
reg_ref *sargs[],
NamedRegsMap *named_sargs = nullptr
)
{
// Can we run commands at all?
//
if (Halted(executor))
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Make sure executor can afford to do it. Keep the charged amount so
// later OOM can refund exactly what payfor took (waitcost, plus at most
// one machinecost penny).
//
int cost = mudconf.waitcost;
if (mudconf.machinecost && RandomINT32(0, mudconf.machinecost-1) == 0)
{
cost++;
}
if (!payfor(executor, cost))
{
notify(Owner(executor), M_("Not enough money to queue command."));
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Wizards and their objs may queue up to db_top+1 cmds. Players are
// limited to QUEUE_QUOTA. -mnp
//
int a = QueueMax(Owner(executor));
if (a < a_Queue(Owner(executor), 1))
{
a_Queue(Owner(executor), -1);
// #1080: refund the payfor charge — OOM paths already do this.
//
giveto(executor, cost);
notify(Owner(executor),
T("Run away objects: too many commands queued. Halted."));
halt_que(Owner(executor), NOTHING);
// Halt also means no command execution allowed.
//
s_Halted(executor);
2018-10-03 17:54:51 +00:00
return nullptr;
}
// We passed all the tests.
//
// Calculate the length of the save string.
//
size_t tlen = 0;
static size_t nCommand;
static size_t nLenEnv[NUM_ENV_VARS];
if (command)
{
nCommand = strlen(reinterpret_cast<char *>(command)) + 1;
tlen = nCommand;
}
if (NUM_ENV_VARS < nargs)
{
nargs = NUM_ENV_VARS;
}
for (a = 0; a < nargs; a++)
{
if (args[a])
{
nLenEnv[a] = strlen(reinterpret_cast<const char *>(args[a])) + 1;
tlen += nLenEnv[a];
}
}
// Create the queue entry and load the save string.
//
2022-03-14 15:56:59 -06:00
const auto tmp = alloc_qentry("setup_que.qblock");
if (nullptr == tmp)
{
STARTLOG(LOG_PROBLEMS, "QUE", "MEM");
log_printf(T("setup_que: out of memory allocating queue entry."));
ENDLOG;
giveto(executor, cost);
a_Queue(Owner(executor), -1);
return nullptr;
}
2018-10-03 17:54:51 +00:00
tmp->comm = nullptr;
tmp->text = nullptr;
2022-03-14 15:56:59 -06:00
UTF8 *tptr = tmp->text = static_cast<UTF8*>(MEMALLOC(tlen));
if (nullptr == tptr)
{
STARTLOG(LOG_PROBLEMS, "QUE", "MEM");
log_printf(T("setup_que: out of memory allocating %zu bytes for queue entry."), tlen);
ENDLOG;
free_qentry(tmp);
giveto(executor, cost);
a_Queue(Owner(executor), -1);
return nullptr;
}
if (command)
{
memcpy(tptr, command, nCommand);
tmp->comm = tptr;
tptr += nCommand;
}
for (a = 0; a < nargs; a++)
{
if (args[a])
{
memcpy(tptr, args[a], nLenEnv[a]);
tmp->env[a] = tptr;
tptr += nLenEnv[a];
}
else
{
2018-10-03 17:54:51 +00:00
tmp->env[a] = nullptr;
}
}
for ( ; a < NUM_ENV_VARS; a++)
{
2018-10-03 17:54:51 +00:00
tmp->env[a] = nullptr;
}
if (sargs)
{
for (a = 0; a < MAX_GLOBAL_REGS; a++)
{
tmp->scr[a] = sargs[a];
if (sargs[a])
{
RegAddRef(sargs[a]);
}
}
tmp->named_scr = NamedRegsCopy(named_sargs ? named_sargs : mudstate.named_regs);
}
else
{
for (a = 0; a < MAX_GLOBAL_REGS; a++)
{
2018-10-03 17:54:51 +00:00
tmp->scr[a] = nullptr;
}
tmp->named_scr = nullptr;
}
#if defined(STUB_SLAVE)
2007-12-19 09:59:35 -08:00
tmp->iRow = mudstate.iRow;
2007-12-18 18:36:20 -08:00
tmp->pResultsSet = mudstate.pResultsSet;
2018-10-03 17:54:51 +00:00
if (nullptr != mudstate.pResultsSet)
{
2007-12-18 18:36:20 -08:00
mudstate.pResultsSet->AddRef();
}
#endif // STUB_SLAVE
// Load the rest of the queue block.
//
tmp->executor = executor;
tmp->IsTimed = false;
tmp->u.s.sem = NOTHING;
tmp->u.s.attr = 0;
tmp->enactor = enactor;
tmp->caller = caller;
tmp->eval = eval;
tmp->nargs = nargs;
tmp->switch_token = nullptr;
tmp->iter_token = nullptr;
tmp->iter_number = 0;
return tmp;
}
// ---------------------------------------------------------------------------
// wait_que: Add commands to the wait or semaphore queues.
// Returns true if the command was queued, false if not (CF_INTERP off,
// setup_que failed, etc.). Callers that mutate state before calling
// (e.g. do_wait's semaphore add_to) must reverse on false.
//
bool wait_que
(
2022-03-14 15:56:59 -06:00
const dbref executor,
const dbref caller,
dbref enactor,
2022-03-14 15:56:59 -06:00
const int eval,
const bool bTimed,
const CLinearTimeAbsolute &ltaWhen,
const dbref sem,
const int attr,
UTF8 *command,
2022-03-14 15:56:59 -06:00
const int nargs,
const UTF8 *args[],
reg_ref *sargs[],
NamedRegsMap *named_sargs,
const UTF8 *iter_token,
int iter_number,
const UTF8 *switch_token
)
{
if (!(mudconf.control_flags & CF_INTERP))
{
return false;
}
BQUE *tmp = setup_que(executor, caller, enactor, eval,
command,
nargs, args,
sargs, named_sargs);
if (!tmp)
{
return false;
}
// Copy iter/switch context if provided.
//
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
// Exact-size clones, not LBUFs: tokens are typically a few bytes
// (a list element, a switch match), but a queued fan-out holds one
// per entry until dispatch — 5000 queued @dolist entries held
// 5000 x 32KB = 164MB of pool LBUFs for <=5-byte strings (queue
// review, 2026-07-21).
if (iter_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
tmp->iter_token = StringClone(iter_token);
tmp->iter_number = iter_number;
}
if (switch_token)
{
perf(queue): exact-size queue-entry tokens; survey the queue's context shipping Queue review (docs/survey-queue.md): the design holds up — two heaps with a monotone-ticket tie-break making FIFO-within-cycle a hard guarantee, priorities as a dequeue floor, and the scheduler deadline feeding the network poll timeout so an idle server does nothing. Context shipped per BQUE is reference-based everywhere semantics permit: the command+env snapshot is ONE exact-sized allocation, %q registers are refcounted (RegAddRef), the named-register map is null-when-empty and shares refcounted values, and dispatch is pure move semantics. Exactly one copy problem existed: iter_token/switch_token (the ##/#$ context of queued @dolist/@switch) each held a full 32KB pool LBUF per entry from enqueue to dispatch, for strings that are typically a few bytes. Measured before/after with @list allocations at 5,000 queued entries: 5,002 -> 2 Lbufs in use; max RSS of a 20k-entry @dolist: 244MB -> 21MB. Fix is StringClone (exact-size) at the single allocation site, MEMFREE at the four free sites; ##/#$ content is byte-identical through queued dispatch. Also recorded in the survey, no action: safer_iter gates iter_token shipping entirely; muxscript's EOF drain is paced and time-capped (~15s) so queue THROUGHPUT needs a live-netmux stress harness (the natural next hardening step); setup_que's static scratch locals are single-threaded-only. Tests: smoke 1319/1319 both toggles; oracle 9/9; jit_diff 400/0 LOGIC; ##/#$ queued-dispatch probes byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:38 -06:00
tmp->switch_token = StringClone(switch_token);
}
int iPriority;
if (isPlayer(tmp->enactor))
{
iPriority = PRIORITY_PLAYER;
}
else
{
iPriority = PRIORITY_OBJECT;
}
tmp->IsTimed = bTimed;
tmp->waittime = ltaWhen;
tmp->u.s.sem = sem;
tmp->u.s.attr = attr;
bool bDeferred = false;
if (sem == NOTHING)
{
// Not a semaphore, so let it run it immediately or put it on
// the wait queue.
//
if (tmp->IsTimed)
{
bDeferred = scheduler.DeferTask(tmp->waittime, iPriority,
Task_RunQueueEntry, tmp, 0);
}
else
{
bDeferred = scheduler.DeferImmediateTask(iPriority,
Task_RunQueueEntry, tmp, 0);
}
}
else
{
if (!tmp->IsTimed)
{
// In this case, the timeout task below will never run,
// but it allows us to manage all semaphores together in
// the same data structure.
//
iPriority = PRIORITY_SUSPEND;
}
bDeferred = scheduler.DeferTask(tmp->waittime, iPriority,
Task_SemaphoreTimeout, tmp, 0);
}
// #1871: if the scheduler could not hold a TASK_RECORD, the command is
// not queued — free the BQUE and reverse payfor/quota so a silent OOM
// does not strand accounting until restart.
//
if (!bDeferred)
{
STARTLOG(LOG_PROBLEMS, "QUE", "MEM");
log_printf(T("wait_que: scheduler refused task; dropping queue entry."));
ENDLOG;
giveto(executor, mudconf.waitcost);
a_Queue(Owner(executor), -1);
for (auto& i : tmp->scr)
{
if (i)
{
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(tmp->named_scr);
#if defined(STUB_SLAVE)
if (nullptr != tmp->pResultsSet)
{
tmp->pResultsSet->Release();
tmp->pResultsSet = nullptr;
}
#endif // STUB_SLAVE
if (tmp->switch_token)
{
MEMFREE(tmp->switch_token);
tmp->switch_token = nullptr;
}
if (tmp->iter_token)
{
MEMFREE(tmp->iter_token);
tmp->iter_token = nullptr;
}
MEMFREE(tmp->text);
tmp->text = nullptr;
free_qentry(tmp);
return false;
}
return true;
}
#if defined(STUB_SLAVE)
2007-12-18 18:36:20 -08:00
bool QueryComplete_bDone = false;
uint32_t QueryComplete_hQuery = 0;
2018-10-03 17:54:51 +00:00
CResultsSet *QueryComplete_prsResultsSet = nullptr;
static int CallBack_QueryComplete(PTASK_RECORD p)
{
if (QueryComplete_bDone)
{
return IU_DONE;
}
if (Task_SQLTimeout == p->fpTask)
{
// This represents a query.
//
BQUE *point = static_cast<BQUE *>(p->arg_voidptr);
if (point->u.hQuery == QueryComplete_hQuery)
{
p->iPriority = PRIORITY_OBJECT;
p->ltaWhen.GetUTC();
p->fpTask = Task_RunQueueEntry;
2007-12-18 18:36:20 -08:00
point->u.s.sem = NOTHING;
point->u.s.attr = 0;
QueryComplete_prsResultsSet->AddRef();
point->pResultsSet = QueryComplete_prsResultsSet;
point->iRow = RS_TOP;
QueryComplete_bDone = true;
return IU_UPDATE_TASK;
}
}
return IU_NEXT_TASK;
}
// This can be called as a side-effect of talking with the stubslave.
// Therefore, we only want to raise the priority of the corresponding take
// from SUSPENDED to OBJECT.
//
void query_complete(uint32_t hQuery, uint32_t iError, CResultsSet *prsResultsSet)
{
2018-10-03 17:54:51 +00:00
if (nullptr != prsResultsSet)
{
prsResultsSet->SetError(iError);
}
QueryComplete_bDone = false;
QueryComplete_hQuery = hQuery;
2007-12-18 18:36:20 -08:00
QueryComplete_prsResultsSet = prsResultsSet;
scheduler.TraverseUnordered(CallBack_QueryComplete);
2018-10-03 17:54:51 +00:00
QueryComplete_prsResultsSet = nullptr;
}
#endif // STUB_SLAVE
// ---------------------------------------------------------------------------
// sql_que: Add commands to the sql queue.
//
void sql_que
(
2022-03-14 15:56:59 -06:00
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
const dbref thing,
const int attr,
const UTF8 *dbname,
const UTF8 *query,
int nargs,
2007-09-24 20:13:48 -07:00
const UTF8 *args[],
reg_ref *sargs[]
)
{
static uint32_t next_handle = 0;
2007-10-11 12:05:29 -07:00
if ( !(mudconf.control_flags & CF_INTERP)
2018-10-03 17:54:51 +00:00
|| nullptr == mudstate.pIQueryControl)
{
return;
}
ATTR *pattr = atr_num(attr);
2018-10-03 17:54:51 +00:00
if (nullptr == pattr)
{
return;
}
UTF8 mbuf[MBUF_SIZE];
mux_sprintf(mbuf, MBUF_SIZE, T("@trigger #%d/%s"), thing, pattr->name);
BQUE *tmp = setup_que(executor, caller, enactor, eval,
mbuf,
nargs, args,
sargs);
if (!tmp)
{
return;
}
const uint32_t hQuery = next_handle++;
tmp->u.hQuery = hQuery;
// Hold the entry until Query completes (or fails). waittime is not
// used as a real SQL timeout (Task_SQLTimeout just runs the entry),
// but must be initialized before DeferTask.
//
tmp->waittime.GetUTC();
if (!scheduler.DeferTask(tmp->waittime, PRIORITY_SUSPEND, Task_SQLTimeout, tmp, 0))
{
// #1871: same rollback as wait_que / Query-reject path.
//
STARTLOG(LOG_PROBLEMS, "QUE", "MEM");
log_printf(T("sql_que: scheduler refused task; dropping queue entry."));
ENDLOG;
giveto(executor, mudconf.waitcost);
a_Queue(Owner(executor), -1);
for (auto& i : tmp->scr)
{
if (i)
{
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(tmp->named_scr);
#if defined(STUB_SLAVE)
if (nullptr != tmp->pResultsSet)
{
tmp->pResultsSet->Release();
tmp->pResultsSet = nullptr;
}
#endif // STUB_SLAVE
if (tmp->switch_token)
{
MEMFREE(tmp->switch_token);
tmp->switch_token = nullptr;
}
if (tmp->iter_token)
{
MEMFREE(tmp->iter_token);
tmp->iter_token = nullptr;
}
MEMFREE(tmp->text);
tmp->text = nullptr;
free_qentry(tmp);
return;
}
2022-03-14 15:56:59 -06:00
const MUX_RESULT mr = mudstate.pIQueryControl->Query(hQuery, dbname, query);
if (MUX_FAILED(mr))
{
// Query rejected: cancel the hold, free the BQUE, and refund
// money/quota exactly as a successful dequeue would.
//
scheduler.CancelTask(Task_SQLTimeout, tmp, 0);
giveto(executor, mudconf.waitcost);
a_Queue(Owner(executor), -1);
for (auto& i : tmp->scr)
{
if (i)
{
RegRelease(i);
i = nullptr;
}
}
NamedRegsClear(tmp->named_scr);
#if defined(STUB_SLAVE)
if (nullptr != tmp->pResultsSet)
{
tmp->pResultsSet->Release();
tmp->pResultsSet = nullptr;
}
#endif // STUB_SLAVE
if (tmp->switch_token)
{
MEMFREE(tmp->switch_token);
tmp->switch_token = nullptr;
}
if (tmp->iter_token)
{
MEMFREE(tmp->iter_token);
tmp->iter_token = nullptr;
}
MEMFREE(tmp->text);
tmp->text = nullptr;
free_qentry(tmp);
}
}
// ---------------------------------------------------------------------------
// do_wait: Command interface to wait_que
//
void do_wait
(
2022-03-14 15:56:59 -06:00
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
int key,
int nargs,
UTF8 *event,
UTF8 *cmd,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(nargs);
CLinearTimeAbsolute ltaWhen;
CLinearTimeDelta ltd;
// If arg1 is all numeric, do simple (non-sem) timed wait.
//
if (is_rational(event))
{
if (key & WAIT_UNTIL)
{
ltaWhen.SetSecondsString(event);
}
else
{
ltaWhen.GetUTC();
ltd.SetSecondsString(event);
ltaWhen += ltd;
}
wait_que(executor, caller, enactor, eval, true, ltaWhen, NOTHING, 0,
cmd,
ncargs, cargs,
mudstate.global_regs);
return;
}
// Semaphore wait with optional timeout.
//
2022-03-14 15:56:59 -06:00
const UTF8 *what = parse_to(&event, '/', 0);
init_match(executor, what, NOTYPE);
match_everything(0);
dbref thing = noisy_match_result();
if (!Good_obj(thing))
{
return;
}
else if (!Controls(executor, thing) && !Link_ok(thing))
{
notify(executor, NOPERM_MESSAGE);
}
else
{
// Get timeout, default 0.
//
int atr = A_SEMAPHORE;
bool bTimed = false;
if (event && *event)
{
if (is_rational(event))
{
if (key & WAIT_UNTIL)
{
ltaWhen.SetSecondsString(event);
}
else
{
ltaWhen.GetUTC();
ltd.SetSecondsString(event);
ltaWhen += ltd;
}
bTimed = true;
}
else
{
const UTF8 *EventAttributeName = reinterpret_cast<UTF8 *>(event);
ATTR *ap = atr_str(EventAttributeName);
if (!ap)
{
atr = mkattr(executor, EventAttributeName);
if (atr <= 0)
{
notify_quiet(executor, M_("Invalid attribute."));
return;
}
ap = atr_num(atr);
}
else
{
atr = ap->number;
}
if (!bCanSetAttr(executor, thing, ap))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
}
}
// Don't raise the semaphore if the queue is disabled — wait_que
// would silently no-op and leave the count elevated.
//
if (!(mudconf.control_flags & CF_INTERP))
{
return;
}
const dbref sem = thing;
int num = 0;
if (!add_to(thing, 1, atr, &num))
{
notify_quiet(executor, M_("Semaphore update failed."));
return;
}
if (num <= 0)
{
// Thing over-notified, run the command immediately.
//
thing = NOTHING;
bTimed = false;
}
if (!wait_que(executor, caller, enactor, eval, bTimed, ltaWhen, thing, atr,
cmd,
ncargs, cargs,
mudstate.global_regs))
{
// setup_que failed after we claimed the semaphore; reverse.
//
(void)add_to(sem, -1, atr, nullptr);
}
}
}
// ---------------------------------------------------------------------------
// do_query: Command interface to sql_que
//
void do_query
(
2022-03-14 15:56:59 -06:00
const dbref executor,
const dbref caller,
dbref enactor,
2022-03-14 15:56:59 -06:00
const int eval,
int key,
2022-03-14 15:56:59 -06:00
const int nargs,
UTF8 *dbref_attr,
UTF8 *dbname_query,
2007-09-24 20:13:48 -07:00
const UTF8 *cargs[],
int ncargs
)
{
2007-09-24 20:13:48 -07:00
UNUSED_PARAMETER(nargs);
2018-10-03 17:54:51 +00:00
if (nullptr == mudstate.pIQueryControl)
{
notify_quiet(executor, M_("Query server is not available."));
return;
}
if (key & QUERY_SQL)
{
// SQL Query.
//
dbref thing;
ATTR *pattr;
if (!( parse_attrib(executor, dbref_attr, &thing, &pattr)
2018-10-03 17:54:51 +00:00
&& nullptr != pattr))
{
notify_quiet(executor, M_("No match."));
return;
}
if (!Controls(executor, thing))
{
notify_quiet(executor, T(NOPERM_MESSAGE));
return;
}
UTF8 *pQuery = dbname_query;
const UTF8 *pDBName = parse_to(&pQuery, '/', 0);
2018-10-03 17:54:51 +00:00
if (nullptr == pQuery)
{
notify(executor, M_("QUERY: No Query."));
return;
}
sql_que(executor, caller, enactor, eval, thing, pattr->number,
pDBName, pQuery, ncargs, cargs, mudstate.global_regs);
}
else
{
notify_quiet(executor, M_("At least one query option is required."));
}
}
static CLinearTimeAbsolute Show_lsaNow;
static int Total_SystemTasks;
static int Total_RunQueueEntry;
static int Shown_RunQueueEntry;
static int Total_SemaphoreTimeout;
static int Shown_SemaphoreTimeout;
static dbref Show_Player_Target;
static dbref Show_Object_Target;
static int Show_Key;
static dbref Show_Player;
static int Show_bFirstLine;
int Total_SQLTimeout;
int Shown_SQLTimeout;
static int CallBack_ShowDispatches(const PTASK_RECORD p)
{
Total_SystemTasks++;
CLinearTimeDelta ltd = p->ltaWhen - Show_lsaNow;
if (p->fpTask == dispatch_DatabaseDump)
{
notify(Show_Player, tprintf(M_("[%d]auto-@dump"), ltd.ReturnSeconds()));
}
else if (p->fpTask == dispatch_FreeListReconstruction)
{
notify(Show_Player, tprintf(M_("[%d]auto-@dbck"), ltd.ReturnSeconds()));
}
else if (p->fpTask == dispatch_IdleCheck)
{
notify(Show_Player, tprintf(M_("[%d]Check for idle players"), ltd.ReturnSeconds()));
}
else if (p->fpTask == dispatch_CheckEvents)
{
notify(Show_Player, tprintf(M_("[%d]Test for @daily time"), ltd.ReturnSeconds()));
}
else if (p->fpTask == dispatch_KeepAlive)
{
notify(Show_Player, tprintf(M_("[%d]Keep Alive"), ltd.ReturnSeconds()));
}
else if (p->fpTask == dispatch_CacheTick)
{
notify(Show_Player, tprintf(M_("[%d]Database cache tick"), ltd.ReturnSeconds()));
}
else if (p->fpTask == Task_ProcessCommand)
{
notify(Show_Player, tprintf(M_("[%d]Further command quota"), ltd.ReturnSeconds()));
}
else
{
Total_SystemTasks--;
}
return IU_NEXT_TASK;
}
static void ShowPsLine(const BQUE *tmp, const uint64_t pid)
{
UTF8 *bufp = unparse_object(Show_Player, tmp->executor, false);
if (tmp->IsTimed && Good_obj(tmp->u.s.sem))
{
CLinearTimeDelta ltd = tmp->waittime - Show_lsaNow;
notify(Show_Player, tprintf(M_("[PID %llu][#%d/%d]%s:%s"),
static_cast<unsigned long long>(pid), tmp->u.s.sem,
ltd.ReturnSeconds(), bufp, tmp->comm));
}
else if (tmp->IsTimed)
{
CLinearTimeDelta ltd = tmp->waittime - Show_lsaNow;
notify(Show_Player, tprintf(M_("[PID %llu][%d]%s:%s"),
static_cast<unsigned long long>(pid), ltd.ReturnSeconds(), bufp,
tmp->comm));
}
else if (Good_obj(tmp->u.s.sem))
{
notify(Show_Player, tprintf(M_("[PID %llu][#%d]%s:%s"),
static_cast<unsigned long long>(pid), tmp->u.s.sem, bufp,
tmp->comm));
}
else
{
notify(Show_Player, tprintf(M_("[PID %llu]%s:%s"),
static_cast<unsigned long long>(pid), bufp, tmp->comm));
}
UTF8 *bp = bufp;
if (Show_Key == PS_LONG)
{
for (int i = 0; i < tmp->nargs; i++)
{
2018-10-03 17:54:51 +00:00
if (tmp->env[i] != nullptr)
{
safe_str(T("; Arg"), bufp, &bp);
safe_chr(static_cast<UTF8>(i + '0'), bufp, &bp);
safe_str(T("="), bufp, &bp);
safe_str(tmp->env[i], bufp, &bp);
safe_str(T(""), bufp, &bp);
}
}
*bp = '\0';
bp = unparse_object(Show_Player, tmp->enactor, false);
notify(Show_Player, tprintf(M_(" Enactor: %s%s"), bp, bufp));
free_lbuf(bp);
}
free_lbuf(bufp);
}
static int CallBack_ShowWait(PTASK_RECORD p)
{
if (p->fpTask != Task_RunQueueEntry)
{
return IU_NEXT_TASK;
}
Total_RunQueueEntry++;
2022-03-14 15:56:59 -06:00
const auto tmp = static_cast<BQUE*>(p->arg_voidptr);
if (que_want(tmp, Show_Player_Target, Show_Object_Target))
{
Shown_RunQueueEntry++;
if (Show_Key == PS_SUMM)
{
return IU_NEXT_TASK;
}
if (Show_bFirstLine)
{
notify(Show_Player, M_("----- Wait Queue -----"));
Show_bFirstLine = false;
}
ShowPsLine(tmp, p->m_Ticket);
}
return IU_NEXT_TASK;
}
static int CallBack_ShowSemaphore(PTASK_RECORD p)
{
if (p->fpTask != Task_SemaphoreTimeout)
{
return IU_NEXT_TASK;
}
Total_SemaphoreTimeout++;
2022-03-14 15:56:59 -06:00
const auto tmp = static_cast<BQUE*>(p->arg_voidptr);
if (que_want(tmp, Show_Player_Target, Show_Object_Target))
{
Shown_SemaphoreTimeout++;
if (Show_Key == PS_SUMM)
{
return IU_NEXT_TASK;
}
if (Show_bFirstLine)
{
notify(Show_Player, M_("----- Semaphore Queue -----"));
Show_bFirstLine = false;
}
ShowPsLine(tmp, p->m_Ticket);
}
return IU_NEXT_TASK;
}
int CallBack_ShowSQLQueries(PTASK_RECORD p)
{
if (p->fpTask != Task_SQLTimeout)
{
return IU_NEXT_TASK;
}
Total_SQLTimeout++;
2022-03-14 15:56:59 -06:00
const auto tmp = static_cast<BQUE*>(p->arg_voidptr);
if (que_want(tmp, Show_Player_Target, Show_Object_Target))
{
Shown_SQLTimeout++;
if (Show_Key == PS_SUMM)
{
return IU_NEXT_TASK;
}
if (Show_bFirstLine)
{
notify(Show_Player, M_("----- SQL Queries -----"));
Show_bFirstLine = false;
}
ShowPsLine(tmp, p->m_Ticket);
}
return IU_NEXT_TASK;
}
// ---------------------------------------------------------------------------
// do_ps: tell executor what commands they have pending in the queue
//
2022-03-14 15:56:59 -06:00
void do_ps(const dbref executor, const dbref caller, const dbref enactor, const int eval, int key, UTF8 *target, const UTF8 *cargs[], const int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref executor_targ, obj_targ;
// Figure out what to list the queue for.
//
if ((key & PS_ALL) && !See_Queue(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
if (!target || !*target)
{
obj_targ = NOTHING;
if (key & PS_ALL)
{
executor_targ = NOTHING;
}
else
{
executor_targ = Owner(executor);
if (!isPlayer(executor))
{
obj_targ = executor;
}
}
}
else
{
executor_targ = Owner(executor);
obj_targ = match_controlled(executor, target);
if (obj_targ == NOTHING)
{
return;
}
if (key & PS_ALL)
{
notify(executor, M_("Cant specify a target and /all"));
return;
}
if (isPlayer(obj_targ))
{
executor_targ = obj_targ;
obj_targ = NOTHING;
}
Fix command-side verb correctness bugs (@clone, @ps, whisper, @flag, @mark) Correctness sweep of the command-side verb handlers. Five confirmed bugs plus a help-text correction, verified by dual-lens review and code reading (the read-only smoke harness can't exercise these verbs directly): - @clone/cost on an exit bypassed the "must control current location" check (it lived only on the non-/cost path), letting a builder splice a cloned exit into a room they do not control. (#855) - The @mark/@mark_all/@apply_marked DB-cleaning refusal cited @unmark_all, which does not exist (produces "Huh?"); corrected to @mark_all/clear. (#856) - @ps <object> reported nothing for a controlled object owned by another player; do_ps was missing the non-player-target else clause that the sibling @halt has (clear the owner filter). (#857) - whisper "<quoted name>" skipped the locality/connected gate the unquoted form applies, giving a success confirmation plus a delivery error and polluting A_LASTWHISPER; also fixed an adjacent quoted-name continue that did not advance the parser. (#858) - @flag/remove of an unknown/empty flag name was silent; now reports an error like the other flag-name failure paths. (#859) - report help said 8-hour segments but the code uses 4 (deliberately, per 4a845139f); corrected the help. (#860) Also restores the "## JIT / DBT Engine" CHANGES heading dropped during an earlier 2.14.0.8 edit. Build clean, all 1264 smoke tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:29:11 -06:00
else
{
// Scope the listing to the object alone (any owner), matching
// @halt; match_controlled already verified permission, so a
// controller can inspect an object owned by another player.
//
executor_targ = NOTHING;
}
}
key = key & ~PS_ALL;
switch (key)
{
case PS_BRIEF:
case PS_SUMM:
case PS_LONG:
break;
default:
notify(executor, M_("Illegal combination of switches."));
return;
}
Show_lsaNow.GetUTC();
Total_SystemTasks = 0;
Total_RunQueueEntry = 0;
Shown_RunQueueEntry = 0;
Total_SemaphoreTimeout = 0;
Shown_SemaphoreTimeout = 0;
2007-12-17 11:44:57 -08:00
Total_SQLTimeout = 0;
Shown_SQLTimeout = 0;
Show_Player_Target = executor_targ;
Show_Object_Target = obj_targ;
Show_Key = key;
Show_Player = executor;
Show_bFirstLine = true;
scheduler.TraverseOrdered(CallBack_ShowWait);
Show_bFirstLine = true;
scheduler.TraverseOrdered(CallBack_ShowSemaphore);
Show_bFirstLine = true;
scheduler.TraverseOrdered(CallBack_ShowSQLQueries);
if (Wizard(executor))
{
notify(executor, M_("----- System Queue -----"));
scheduler.TraverseOrdered(CallBack_ShowDispatches);
}
// Display stats.
//
2022-03-14 15:56:59 -06:00
UTF8* bufp = alloc_mbuf("do_ps");
mux_sprintf(bufp, MBUF_SIZE, T("Totals: Wait Queue...%d/%d Semaphores...%d/%d SQL %d/%d"),
Shown_RunQueueEntry, Total_RunQueueEntry,
Shown_SemaphoreTimeout, Total_SemaphoreTimeout,
Shown_SQLTimeout, Total_SQLTimeout);
notify(executor, bufp);
if (Wizard(executor))
{
mux_sprintf(bufp, MBUF_SIZE, T(" System Tasks.....%d"), Total_SystemTasks);
notify(executor, bufp);
}
free_mbuf(bufp);
}
static CLinearTimeDelta ltdWarp;
static int CallBack_Warp(PTASK_RECORD p)
{
if ( p->fpTask == Task_RunQueueEntry
|| p->fpTask == Task_SQLTimeout
|| p->fpTask == Task_SemaphoreTimeout)
{
2022-03-14 15:56:59 -06:00
const auto point = static_cast<BQUE*>(p->arg_voidptr);
if (point->IsTimed)
{
point->waittime -= ltdWarp;
p->ltaWhen -= ltdWarp;
return IU_UPDATE_TASK;
}
}
return IU_NEXT_TASK;
}
// ---------------------------------------------------------------------------
// do_queue: Queue management
//
2022-03-14 15:56:59 -06:00
void do_queue(const dbref executor, const dbref caller, const dbref enactor, const int eval, const int key, UTF8 *arg, const UTF8 *cargs[], const int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (key == QUEUE_KICK)
{
// Parse full width, then clamp for RunTasks(int) (#1402).
//
int64_t wanted = mux_atoi64(arg);
int i;
if (wanted > INT_MAX)
{
i = INT_MAX;
}
else if (wanted < 0)
{
i = 0;
}
else
{
i = static_cast<int>(wanted);
}
2022-03-14 15:56:59 -06:00
const int save_minPriority = scheduler.GetMinPriority();
if (save_minPriority <= PRIORITY_CF_DEQUEUE_DISABLED)
{
notify(executor, M_("Warning: automatic dequeueing is disabled."));
scheduler.SetMinPriority(PRIORITY_CF_DEQUEUE_ENABLED);
}
CLinearTimeAbsolute lsaNow;
lsaNow.GetUTC();
scheduler.ReadyTasks(lsaNow);
2022-03-14 15:56:59 -06:00
const int ncmds = scheduler.RunTasks(i);
scheduler.SetMinPriority(save_minPriority);
if (!Quiet(executor))
{
notify(executor, tprintf(M_("%d commands processed."), ncmds));
}
}
else if (key == QUEUE_WARP)
{
// SetSeconds takes int64_t (UnderlyingTickType) (#1402).
//
const int64_t iWarp = mux_atoi64(arg);
ltdWarp.SetSeconds(iWarp);
if (scheduler.GetMinPriority() <= PRIORITY_CF_DEQUEUE_DISABLED)
{
notify(executor, M_("Warning: automatic dequeueing is disabled."));
}
scheduler.TraverseUnordered(CallBack_Warp);
if (Quiet(executor))
{
return;
}
if (0 < iWarp)
{
notify(executor, tprintf(M_("WaitQ timer advanced %d seconds."), iWarp));
}
else if (iWarp < 0)
{
notify(executor, tprintf(M_("WaitQ timer set back %d seconds."), iWarp));
}
else
{
notify(executor, M_("Object queue appended to player queue."));
}
}
}