tinymux/mux/script/mux_main.cpp
Stephen Dennis c502af6192 fix(restart): preserve the -p and -e command-line values across @restart (#2199)
do_restart() rebuilds its exec argv from mudconf.pid_file and
mudconf.log_dir.  Both were read in three places and written in NONE:

    predicates.cpp   the execl                                  <- read
    engine_com.cpp   GetConfig -> DRIVER_CONFIG.pid_file         <- read
    engine_com.cpp   GetConfig -> DRIVER_CONFIG.log_dir          <- read

There is no `pid_file` or `log_dir` config directive either, so nothing in
netmux.conf could populate them.  Both are driver-owned CLI values --
driver.cpp says so outright ("pid_file is driver-owned -- set from CLI or
default") -- and the engine had no way to learn them.  So every @restart on
every site exec'd with an empty -p and -e.

Impact is quiet, which is why it lasted: a site running

    netmux -p /var/run/mux/netmux.pid

has its pidfile revert to the default netmux.pid in the working directory
after the first restart, and stops maintaining the configured path.  The
pid inside the stale file stays correct, because execve preserves it, so
nothing breaks until something else moves.  Same for -e.

The fingerprint is visible in ps on any restarted 2.14: `-p` with nothing
after it.

Fix follows #817's precedent exactly -- driver-owned strings the engine
needs, handed over at bridge init:

  - g_driver_pid_file / g_driver_log_dir replace two file-static CLI
    variables in driver.cpp (pErrorBasename was also a poor name for
    something that is a log directory).  Globals for the same reason
    g_version is one: the CDriverControl bridge is a different TU.
  - mux_IDriverControl gains GetInvocationPaths.  IID bumped D4D6 -> D4D7;
    the comment now records both bumps and why a vtable change needs one.
  - conn_bridge_init caches them into mudconf.  Safe there because
    Startup() runs after LoadGame(), so cf_init() cannot wipe them
    afterwards.  Pointers are stored, not copied: they are driver-owned
    storage that lives for the process, and nothing in the engine frees or
    reassigns either field.
  - CScriptDriverControl implements it too, returning empty strings.
    muxscript writes no pidfile and has no @restart; an invented default
    would only be wrong somewhere later.

The argv is now built dynamically and omits an option it has no value for.
-c, -p and -e are all CLI_REQUIRED, so a bare flag makes the successor log
"Option 'x' requires an argument, but none was found."  Populating the
fields alone would have left that warning for any site that supplies only
one of the two -- which is what test-scenario showed, since run.sh passes
-p and not -e.

Verified on a live restart, reading the successor's argv (execve replaces
argv and preserves the pid, so this is direct evidence):

    boot:     ./bin/netmux -c netmux.conf -p custom-name.pid -e logs
    restart:  netmux -c netmux.conf -p custom-name.pid -e logs
    ok - successor kept -p custom-name.pid
    ok - successor kept -e logs
    ok - no missing-argument warning in the log

Catch-verified by removing only the bridge caching:

    restart:  netmux -c netmux.conf -p
    not ok - successor lost the -p value
    not ok - successor lost the -e value
    not ok - successor logged a missing-argument warning:
          15:Warning: Option '-p' requires an argument, but none was found.

tests/scenario/restart_helpers.py gains the assertion; it already pays for
a restart, so this costs no extra cycle.  It skips if the server under test
was not started with -p.

Clean rebuild, since the IDriverControl vtable changed.

make test: 36 targets, 34 passed, 2 skipped (NLS), 0 failed
config: jit=yes stubslave=yes nls=no realitylvls=yes wodrealms=yes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:39:34 -06:00

2211 lines
62 KiB
C++

/*! \file mux_main.cpp
* \brief Script-mode driver for TinyMUX.
*
* Minimal binary that loads engine.so via COM and runs softcode from
* stdin or a file. No networking, no telnet, no SSL, no descriptors.
* Same engine, different front-end.
*
* Usage:
* muxscript -g /path/to/game -c netmux.conf < script.mux
* muxscript -e 'think add(2,3)'
* echo 'think sha1(hello)' | muxscript
*
* Game directory resolution (in priority order):
* 1. -g <dir> command-line flag
* 2. MUX_HOME environment variable
* 3. Current working directory
*
* engine.so is loaded from <gamedir>/bin/engine.so.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "libmux.h"
#include "modules.h"
#include "timeutil.h"
#include "alloc.h"
#include "svdrand.h"
#include "_build.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#if defined(WIN32)
#include <io.h>
#include <direct.h>
#include <windows.h>
#define chdir _chdir
#else
#include <unistd.h>
#include <poll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#endif
#include <string>
// Script mode doesn't include externs.h or db.h, so we define the
// constants we need directly.
//
#ifndef GOD
#define GOD (static_cast<dbref>(1))
#endif
#ifndef NOTHING
#define NOTHING (-1)
#endif
// ---------------------------------------------------------------------------
// Globals.
// ---------------------------------------------------------------------------
static mux_IGameEngine *g_pEngine = nullptr;
static char g_gamedir[4096];
static bool g_script_shutdown = false;
static dbref g_player = GOD; // Player identity for commands
// ---------------------------------------------------------------------------
// CScriptConnectionManager — stub mux_IConnectionManager for script mode.
// Output goes to stdout. No descriptors exist.
// ---------------------------------------------------------------------------
class CScriptConnectionManager : public mux_IConnectionManager
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
// Output
virtual MUX_RESULT SendText(dbref target, const UTF8 *text);
virtual MUX_RESULT SendRaw(dbref target, const UTF8 *data, size_t len);
virtual MUX_RESULT BroadcastAndFlush(int inflags, const UTF8 *text);
virtual MUX_RESULT SendProgPrompt(dbref target);
virtual MUX_RESULT SendKeepaliveNops(void);
virtual MUX_RESULT SendGmcp(dbref target, const UTF8 *pkg, const UTF8 *json);
// Queries by dbref
virtual MUX_RESULT GetTotalConnections(int *pCount);
virtual MUX_RESULT CountPlayerDescs(dbref target, int *pCount);
virtual MUX_RESULT SumPlayerCommandCount(dbref target, int *pCount);
virtual MUX_RESULT FetchHeight(dbref target, int *pHeight);
virtual MUX_RESULT FetchWidth(dbref target, int *pWidth);
virtual MUX_RESULT FetchIdle(dbref target, int *pIdle);
virtual MUX_RESULT FetchConnect(dbref target, int *pConnect);
// Queries by opaque DESC handle
virtual MUX_RESULT FindDescBySocket(SOCKET s, DESC **ppDesc);
virtual MUX_RESULT FindDescByPlayer(dbref target, DESC **ppDesc);
virtual MUX_RESULT DescPlayer(const DESC *d, dbref *pPlayer);
virtual MUX_RESULT DescHeight(const DESC *d, int *pHeight);
virtual MUX_RESULT DescWidth(const DESC *d, int *pWidth);
virtual MUX_RESULT DescEncoding(const DESC *d, int *pEncoding);
virtual MUX_RESULT DescCommandCount(const DESC *d, int *pCount);
virtual MUX_RESULT DescTtype(const DESC *d, const UTF8 **ppTtype);
virtual MUX_RESULT DescLastTime(const DESC *d, CLinearTimeAbsolute *pTime);
virtual MUX_RESULT DescConnectedAt(const DESC *d, CLinearTimeAbsolute *pTime);
virtual MUX_RESULT DescNvtHimState(const DESC *d, unsigned char chOption, int *pState);
virtual MUX_RESULT DescSocketState(const DESC *d, SocketState *pState);
// Iteration
virtual MUX_RESULT ForEachConnectedPlayer(
void (*callback)(dbref player, void *context), void *context);
virtual MUX_RESULT ForEachConnectedDesc(
void (*callback)(dbref player, SOCKET sock, void *context), void *context);
// @program state
virtual MUX_RESULT PlayerHasProgram(dbref target, bool *pbHas);
virtual MUX_RESULT DetachPlayerProgram(dbref target, program_data **ppProgram);
virtual MUX_RESULT SetPlayerProgram(dbref target, program_data *program);
// Encoding / Display
virtual MUX_RESULT SetPlayerEncoding(dbref target, int encoding);
virtual MUX_RESULT ResetPlayerEncoding(dbref target);
virtual MUX_RESULT SetDoingAll(dbref target, const UTF8 *doing, size_t len);
virtual MUX_RESULT SetDoingLeastIdle(dbref target, const UTF8 *doing, size_t len, bool *pbFound);
// Quota
virtual MUX_RESULT UpdateAllDescQuotas(int nExtra, int nMax);
// Connection lifecycle
virtual MUX_RESULT BootOff(dbref target, const UTF8 *message, int *pCount);
virtual MUX_RESULT BootByPort(SOCKET port, bool bGod, const UTF8 *message, int *pCount);
// Idle check
virtual MUX_RESULT CheckIdle(void);
virtual MUX_RESULT EmergencyShutdown(void);
virtual MUX_RESULT DescQueueWrite(DESC *d, const UTF8 *data, size_t len);
virtual MUX_RESULT DescQueueString(DESC *d, const UTF8 *text);
virtual MUX_RESULT DescReload(dbref player);
virtual MUX_RESULT TrimmedName(dbref player, UTF8 *cbuff,
size_t cbuffSize, unsigned short nMin, unsigned short nMax,
unsigned short nPad, unsigned short *pResult);
virtual MUX_RESULT MakePortlist(dbref player, dbref target,
UTF8 *buff, UTF8 **bufc);
virtual MUX_RESULT ForEachPlayerDesc(dbref target,
void (*callback)(DESC *d, void *context), void *context);
virtual MUX_RESULT FunHost(dbref executor, dbref caller, dbref enactor,
int eval, const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc);
virtual MUX_RESULT FunDoing(dbref executor, dbref caller, dbref enactor,
int eval, const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc);
virtual MUX_RESULT FunSiteinfo(dbref executor, dbref caller,
dbref enactor, int eval, const UTF8 * const fargs[], int nfargs,
UTF8 *buff, UTF8 **bufc);
CScriptConnectionManager(void);
virtual ~CScriptConnectionManager();
private:
uint32_t m_cRef;
};
CScriptConnectionManager::CScriptConnectionManager(void) : m_cRef(1)
{
}
CScriptConnectionManager::~CScriptConnectionManager()
{
}
MUX_RESULT CScriptConnectionManager::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IConnectionManager *>(this);
}
else if (IID_IConnectionManager == iid)
{
*ppv = static_cast<mux_IConnectionManager *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CScriptConnectionManager::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CScriptConnectionManager::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
// --- Output ---
MUX_RESULT CScriptConnectionManager::SendText(dbref target, const UTF8 *text)
{
UNUSED_PARAMETER(target);
if (text)
{
fputs(reinterpret_cast<const char *>(text), stdout);
}
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SendRaw(dbref target, const UTF8 *data, size_t len)
{
UNUSED_PARAMETER(target);
if (data && len > 0)
{
fwrite(data, 1, len, stdout);
}
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::BroadcastAndFlush(int inflags, const UTF8 *text)
{
UNUSED_PARAMETER(inflags);
if (text)
{
fputs(reinterpret_cast<const char *>(text), stdout);
fflush(stdout);
}
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SendProgPrompt(dbref target)
{
UNUSED_PARAMETER(target);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SendKeepaliveNops(void)
{
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SendGmcp(dbref target, const UTF8 *pkg, const UTF8 *json)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(pkg);
UNUSED_PARAMETER(json);
return MUX_S_OK;
}
// --- Queries by dbref ---
MUX_RESULT CScriptConnectionManager::GetTotalConnections(int *pCount)
{
if (nullptr == pCount) return MUX_E_INVALIDARG;
*pCount = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::CountPlayerDescs(dbref target, int *pCount)
{
UNUSED_PARAMETER(target);
if (nullptr == pCount) return MUX_E_INVALIDARG;
*pCount = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SumPlayerCommandCount(dbref target, int *pCount)
{
UNUSED_PARAMETER(target);
if (nullptr == pCount) return MUX_E_INVALIDARG;
*pCount = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FetchHeight(dbref target, int *pHeight)
{
UNUSED_PARAMETER(target);
if (nullptr == pHeight) return MUX_E_INVALIDARG;
*pHeight = 24;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FetchWidth(dbref target, int *pWidth)
{
UNUSED_PARAMETER(target);
if (nullptr == pWidth) return MUX_E_INVALIDARG;
*pWidth = 78;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FetchIdle(dbref target, int *pIdle)
{
UNUSED_PARAMETER(target);
if (nullptr == pIdle) return MUX_E_INVALIDARG;
*pIdle = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FetchConnect(dbref target, int *pConnect)
{
UNUSED_PARAMETER(target);
if (nullptr == pConnect) return MUX_E_INVALIDARG;
*pConnect = 0;
return MUX_S_OK;
}
// --- Queries by opaque DESC handle ---
MUX_RESULT CScriptConnectionManager::FindDescBySocket(SOCKET s, DESC **ppDesc)
{
UNUSED_PARAMETER(s);
if (nullptr == ppDesc) return MUX_E_INVALIDARG;
*ppDesc = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FindDescByPlayer(dbref target, DESC **ppDesc)
{
UNUSED_PARAMETER(target);
if (nullptr == ppDesc) return MUX_E_INVALIDARG;
*ppDesc = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescPlayer(const DESC *d, dbref *pPlayer)
{
UNUSED_PARAMETER(d);
if (nullptr == pPlayer) return MUX_E_INVALIDARG;
*pPlayer = NOTHING;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescHeight(const DESC *d, int *pHeight)
{
UNUSED_PARAMETER(d);
if (nullptr == pHeight) return MUX_E_INVALIDARG;
*pHeight = 24;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescWidth(const DESC *d, int *pWidth)
{
UNUSED_PARAMETER(d);
if (nullptr == pWidth) return MUX_E_INVALIDARG;
*pWidth = 78;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescEncoding(const DESC *d, int *pEncoding)
{
UNUSED_PARAMETER(d);
if (nullptr == pEncoding) return MUX_E_INVALIDARG;
*pEncoding = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescCommandCount(const DESC *d, int *pCount)
{
UNUSED_PARAMETER(d);
if (nullptr == pCount) return MUX_E_INVALIDARG;
*pCount = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescTtype(const DESC *d, const UTF8 **ppTtype)
{
UNUSED_PARAMETER(d);
if (nullptr == ppTtype) return MUX_E_INVALIDARG;
*ppTtype = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescLastTime(const DESC *d, CLinearTimeAbsolute *pTime)
{
UNUSED_PARAMETER(d);
UNUSED_PARAMETER(pTime);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescConnectedAt(const DESC *d, CLinearTimeAbsolute *pTime)
{
UNUSED_PARAMETER(d);
UNUSED_PARAMETER(pTime);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescNvtHimState(const DESC *d, unsigned char chOption, int *pState)
{
UNUSED_PARAMETER(d);
UNUSED_PARAMETER(chOption);
if (nullptr == pState) return MUX_E_INVALIDARG;
*pState = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescSocketState(const DESC *d, SocketState *pState)
{
UNUSED_PARAMETER(d);
if (nullptr == pState) return MUX_E_INVALIDARG;
// SocketState::Accepted == 1, but the enum is only forward-declared
// in modules.h. Use a cast to avoid needing the full definition.
*pState = static_cast<SocketState>(1);
return MUX_S_OK;
}
// --- Iteration ---
MUX_RESULT CScriptConnectionManager::ForEachConnectedPlayer(
void (*callback)(dbref player, void *context), void *context)
{
UNUSED_PARAMETER(callback);
UNUSED_PARAMETER(context);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::ForEachConnectedDesc(
void (*callback)(dbref player, SOCKET sock, void *context), void *context)
{
UNUSED_PARAMETER(callback);
UNUSED_PARAMETER(context);
return MUX_S_OK;
}
// --- @program state ---
MUX_RESULT CScriptConnectionManager::PlayerHasProgram(dbref target, bool *pbHas)
{
UNUSED_PARAMETER(target);
if (nullptr == pbHas) return MUX_E_INVALIDARG;
*pbHas = false;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DetachPlayerProgram(dbref target, program_data **ppProgram)
{
UNUSED_PARAMETER(target);
if (nullptr == ppProgram) return MUX_E_INVALIDARG;
*ppProgram = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SetPlayerProgram(dbref target, program_data *program)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(program);
return MUX_S_OK;
}
// --- Encoding / Display ---
MUX_RESULT CScriptConnectionManager::SetPlayerEncoding(dbref target, int encoding)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(encoding);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::ResetPlayerEncoding(dbref target)
{
UNUSED_PARAMETER(target);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SetDoingAll(dbref target, const UTF8 *doing, size_t len)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(doing);
UNUSED_PARAMETER(len);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::SetDoingLeastIdle(dbref target, const UTF8 *doing, size_t len, bool *pbFound)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(doing);
UNUSED_PARAMETER(len);
if (nullptr == pbFound) return MUX_E_INVALIDARG;
*pbFound = false;
return MUX_S_OK;
}
// --- Quota ---
MUX_RESULT CScriptConnectionManager::UpdateAllDescQuotas(int nExtra, int nMax)
{
UNUSED_PARAMETER(nExtra);
UNUSED_PARAMETER(nMax);
return MUX_S_OK;
}
// --- Connection lifecycle ---
MUX_RESULT CScriptConnectionManager::BootOff(dbref target, const UTF8 *message, int *pCount)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(message);
if (pCount) *pCount = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::BootByPort(SOCKET port, bool bGod, const UTF8 *message, int *pCount)
{
UNUSED_PARAMETER(port);
UNUSED_PARAMETER(bGod);
UNUSED_PARAMETER(message);
if (pCount) *pCount = 0;
return MUX_S_OK;
}
// --- Idle check ---
MUX_RESULT CScriptConnectionManager::CheckIdle(void)
{
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::EmergencyShutdown(void)
{
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescQueueWrite(DESC *d, const UTF8 *data, size_t len)
{
UNUSED_PARAMETER(d);
if (data && len > 0)
{
fwrite(data, 1, len, stdout);
}
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescQueueString(DESC *d, const UTF8 *text)
{
UNUSED_PARAMETER(d);
if (text)
{
fputs(reinterpret_cast<const char *>(text), stdout);
}
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::DescReload(dbref player)
{
UNUSED_PARAMETER(player);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::TrimmedName(dbref player, UTF8 *cbuff,
size_t cbuffSize, unsigned short nMin, unsigned short nMax,
unsigned short nPad, unsigned short *pResult)
{
UNUSED_PARAMETER(player);
UNUSED_PARAMETER(cbuff);
UNUSED_PARAMETER(cbuffSize);
UNUSED_PARAMETER(nMin);
UNUSED_PARAMETER(nMax);
UNUSED_PARAMETER(nPad);
if (nullptr == pResult) return MUX_E_INVALIDARG;
*pResult = 0;
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::MakePortlist(dbref player, dbref target,
UTF8 *buff, UTF8 **bufc)
{
UNUSED_PARAMETER(player);
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(buff);
UNUSED_PARAMETER(bufc);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::ForEachPlayerDesc(dbref target,
void (*callback)(DESC *d, void *context), void *context)
{
UNUSED_PARAMETER(target);
UNUSED_PARAMETER(callback);
UNUSED_PARAMETER(context);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FunHost(dbref executor, dbref caller,
dbref enactor, int eval, const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(buff);
UNUSED_PARAMETER(bufc);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FunDoing(dbref executor, dbref caller,
dbref enactor, int eval, const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(buff);
UNUSED_PARAMETER(bufc);
return MUX_S_OK;
}
MUX_RESULT CScriptConnectionManager::FunSiteinfo(dbref executor, dbref caller,
dbref enactor, int eval, const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(buff);
UNUSED_PARAMETER(bufc);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CScriptConnectionManagerFactory
// ---------------------------------------------------------------------------
DEFINE_FACTORY(CScriptConnectionManagerFactory)
CScriptConnectionManagerFactory::CScriptConnectionManagerFactory(void) : m_cRef(1)
{
}
CScriptConnectionManagerFactory::~CScriptConnectionManagerFactory()
{
}
MUX_RESULT CScriptConnectionManagerFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CScriptConnectionManagerFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CScriptConnectionManagerFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CScriptConnectionManagerFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CScriptConnectionManager *pConnMgr = nullptr;
try
{
pConnMgr = new CScriptConnectionManager;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pConnMgr)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pConnMgr->QueryInterface(iid, ppv);
pConnMgr->Release();
return mr;
}
MUX_RESULT CScriptConnectionManagerFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CScriptDriverControl — stub mux_IDriverControl for script mode.
// ---------------------------------------------------------------------------
class CScriptDriverControl : public mux_IDriverControl
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT ShutdownRequest(void);
virtual MUX_RESULT GetRestarting(bool *pbRestarting);
virtual MUX_RESULT SiteUpdate(const UTF8 *subnetStr,
dbref player, UTF8 *cmd, int operation);
virtual MUX_RESULT GetPid(int *pPid);
virtual MUX_RESULT GetCharsetNametab(NAMETAB **ppTable);
virtual MUX_RESULT GetSigactionsNametab(NAMETAB **ppTable);
virtual MUX_RESULT GetLogoutCmdtable(NAMETAB **ppTable);
virtual MUX_RESULT LoggedOut0(dbref executor, dbref caller,
dbref enactor, int eval, int key);
virtual MUX_RESULT LoggedOut1(dbref executor, dbref caller,
dbref enactor, int eval, int key, UTF8 *arg,
const UTF8 *cargs[], int ncargs);
virtual MUX_RESULT DoVersion(dbref executor, dbref caller,
dbref enactor, int eval, int key);
virtual MUX_RESULT DoStartSlave(dbref executor, dbref caller,
dbref enactor, int eval, int key);
virtual MUX_RESULT GetTaskProcessCommand(
void (**ppfTask)(void *, int));
virtual MUX_RESULT DumpRestartDb(void);
virtual MUX_RESULT PrepareNetworkForRestart(void);
virtual MUX_RESULT StartEmailSend(dbref executor, const UTF8 *recipient,
const UTF8 *subject, const UTF8 *body, bool *pResult);
virtual MUX_RESULT ListSiteInfo(dbref player);
virtual MUX_RESULT GetVersionStrings(const UTF8 **ppVersion,
const UTF8 **ppShortVer);
virtual MUX_RESULT GetInvocationPaths(const UTF8 **ppPidFile,
const UTF8 **ppLogDir);
CScriptDriverControl(void);
virtual ~CScriptDriverControl();
private:
uint32_t m_cRef;
};
CScriptDriverControl::CScriptDriverControl(void) : m_cRef(1)
{
}
CScriptDriverControl::~CScriptDriverControl()
{
}
MUX_RESULT CScriptDriverControl::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IDriverControl *>(this);
}
else if (IID_IDriverControl == iid)
{
*ppv = static_cast<mux_IDriverControl *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CScriptDriverControl::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CScriptDriverControl::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CScriptDriverControl::ShutdownRequest(void)
{
g_script_shutdown = true;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetRestarting(bool *pbRestarting)
{
if (nullptr == pbRestarting) return MUX_E_INVALIDARG;
*pbRestarting = false;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::SiteUpdate(const UTF8 *subnetStr,
dbref player, UTF8 *cmd, int operation)
{
UNUSED_PARAMETER(subnetStr);
UNUSED_PARAMETER(player);
UNUSED_PARAMETER(cmd);
UNUSED_PARAMETER(operation);
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetPid(int *pPid)
{
if (nullptr == pPid) return MUX_E_INVALIDARG;
*pPid = static_cast<int>(getpid());
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetCharsetNametab(NAMETAB **ppTable)
{
if (nullptr == ppTable) return MUX_E_INVALIDARG;
*ppTable = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetSigactionsNametab(NAMETAB **ppTable)
{
if (nullptr == ppTable) return MUX_E_INVALIDARG;
*ppTable = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetLogoutCmdtable(NAMETAB **ppTable)
{
if (nullptr == ppTable) return MUX_E_INVALIDARG;
*ppTable = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::LoggedOut0(dbref executor, dbref caller,
dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::LoggedOut1(dbref executor, dbref caller,
dbref enactor, int eval, int key, UTF8 *arg,
const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(arg);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::DoVersion(dbref executor, dbref caller,
dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
#if defined(ALPHA)
fprintf(stdout, "MUX %s [ALPHA]\r\n", MUX_VERSION);
#elif defined(BETA)
fprintf(stdout, "MUX %s [BETA]\r\n", MUX_VERSION);
#else
fprintf(stdout, "MUX %s [%s]\r\n", MUX_VERSION, MUX_RELEASE_DATE);
#endif
UNUSED_PARAMETER(executor);
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::DoStartSlave(dbref executor, dbref caller,
dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetTaskProcessCommand(
void (**ppfTask)(void *, int))
{
if (nullptr == ppfTask) return MUX_E_INVALIDARG;
*ppfTask = nullptr;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::DumpRestartDb(void)
{
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::PrepareNetworkForRestart(void)
{
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::StartEmailSend(dbref executor,
const UTF8 *recipient, const UTF8 *subject, const UTF8 *body,
bool *pResult)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(recipient);
UNUSED_PARAMETER(subject);
UNUSED_PARAMETER(body);
if (nullptr == pResult) return MUX_E_INVALIDARG;
*pResult = false;
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::ListSiteInfo(dbref player)
{
UNUSED_PARAMETER(player);
return MUX_E_NOTIMPLEMENTED;
}
MUX_RESULT CScriptDriverControl::GetInvocationPaths(const UTF8 **ppPidFile,
const UTF8 **ppLogDir)
{
if (nullptr == ppPidFile || nullptr == ppLogDir)
{
return MUX_E_INVALIDARG;
}
// Script mode writes no pidfile and has no @restart, so there is nothing
// meaningful to report (#2199). Empty rather than a plausible-looking
// default: muxscript never execs itself, and a made-up path here would
// only be wrong somewhere later.
//
*ppPidFile = T("");
*ppLogDir = T("");
return MUX_S_OK;
}
MUX_RESULT CScriptDriverControl::GetVersionStrings(const UTF8 **ppVersion,
const UTF8 **ppShortVer)
{
if (nullptr == ppVersion || nullptr == ppShortVer)
{
return MUX_E_INVALIDARG;
}
// Script mode does not link the driver's version.cpp (it pulls in
// g_pILog and notify), so build the strings here from the version
// macros, mirroring build_version()'s Unix form, so that version()
// resolves under muxscript too (#817).
//
static UTF8 s_version[128];
static UTF8 s_short_ver[64];
if ('\0' == s_version[0])
{
char *pv = reinterpret_cast<char *>(s_version);
char *ps = reinterpret_cast<char *>(s_short_ver);
#if defined(ALPHA)
snprintf(pv, sizeof(s_version), "MUX %s [ALPHA]", MUX_VERSION);
snprintf(ps, sizeof(s_short_ver), "MUX %s Alpha", MUX_VERSION);
#elif defined(BETA)
snprintf(pv, sizeof(s_version), "MUX %s [BETA]", MUX_VERSION);
snprintf(ps, sizeof(s_short_ver), "MUX %s Beta", MUX_VERSION);
#else
snprintf(pv, sizeof(s_version), "MUX %s [%s]", MUX_VERSION,
MUX_RELEASE_DATE);
snprintf(ps, sizeof(s_short_ver), "MUX %s", MUX_VERSION);
#endif
}
*ppVersion = s_version;
*ppShortVer = s_short_ver;
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CScriptDriverControlFactory
// ---------------------------------------------------------------------------
DEFINE_FACTORY(CScriptDriverControlFactory)
CScriptDriverControlFactory::CScriptDriverControlFactory(void) : m_cRef(1)
{
}
CScriptDriverControlFactory::~CScriptDriverControlFactory()
{
}
MUX_RESULT CScriptDriverControlFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CScriptDriverControlFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CScriptDriverControlFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CScriptDriverControlFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CScriptDriverControl *pDC = nullptr;
try
{
pDC = new CScriptDriverControl;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pDC)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pDC->QueryInterface(iid, ppv);
pDC->Release();
return mr;
}
MUX_RESULT CScriptDriverControlFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// COM registration — register script-mode factory classes before loading
// engine.so, so the engine can acquire them during its initialization.
// ---------------------------------------------------------------------------
static MUX_CLASS_INFO script_classes[] =
{
{ CID_ConnectionManager },
{ CID_DriverControl }
};
#define NUM_SCRIPT_CLASSES (sizeof(script_classes)/sizeof(script_classes[0]))
extern "C" MUX_RESULT DCL_API script_GetClassObject(MUX_CID cid, MUX_IID iid, void **ppv)
{
MUX_RESULT mr = MUX_E_CLASSNOTAVAILABLE;
if (CID_ConnectionManager == cid)
{
CScriptConnectionManagerFactory *pFactory = nullptr;
try
{
pFactory = new CScriptConnectionManagerFactory;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pFactory)
{
return MUX_E_OUTOFMEMORY;
}
mr = pFactory->QueryInterface(iid, ppv);
pFactory->Release();
}
else if (CID_DriverControl == cid)
{
CScriptDriverControlFactory *pFactory = nullptr;
try
{
pFactory = new CScriptDriverControlFactory;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pFactory)
{
return MUX_E_OUTOFMEMORY;
}
mr = pFactory->QueryInterface(iid, ppv);
pFactory->Release();
}
return mr;
}
// ---------------------------------------------------------------------------
// Game directory resolution and engine loading.
// ---------------------------------------------------------------------------
static bool resolve_gamedir(const char *flag_dir)
{
const char *dir = flag_dir;
if (!dir)
{
dir = getenv("MUX_HOME");
}
if (!dir)
{
dir = ".";
}
#if defined(WIN32)
if (!_fullpath(g_gamedir, dir, sizeof(g_gamedir)))
#else
if (!realpath(dir, g_gamedir))
#endif
{
fprintf(stderr, "muxscript: cannot resolve game directory '%s': %s\n",
dir, strerror(errno));
return false;
}
char engine_path[4096 + 64];
#if defined(WIN32)
snprintf(engine_path, sizeof(engine_path), "%s\\bin\\engine.dll", g_gamedir);
if (_access(engine_path, 4) != 0) // 4 = read permission
#else
snprintf(engine_path, sizeof(engine_path), "%s/bin/engine.so", g_gamedir);
if (access(engine_path, R_OK) != 0)
#endif
{
fprintf(stderr, "muxscript: engine not found at '%s'\n", engine_path);
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// Stubslave parent path for release (--enable-stubslave) builds.
//
// netmux boots the helper via CPlatform::BootHelperProcess + GanlAdapter
// pump. muxscript has neither, so without a local boot any "module X local"
// config fails (or, historically, SEGV'd looking for a channel that never
// existed — #1293). Smoke modules load in-proc, but booting the slave here
// means the shipping configuration is actually exercised by make test.
// ---------------------------------------------------------------------------
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
static QUEUE_INFO g_stub_queue_in;
static QUEUE_INFO g_stub_queue_out;
static int g_stub_fd = -1;
static pid_t g_stub_pid = 0;
static std::string g_stub_write_remainder;
static mux_ISlaveControl *g_pISlaveControl = nullptr;
static void reap_stubslave_bounded(pid_t pid)
{
if (pid <= 0)
{
return;
}
for (int i = 0; i < 50; i++)
{
pid_t r = waitpid(pid, nullptr, WNOHANG);
if (r == pid || (r == -1 && errno == ECHILD))
{
return;
}
usleep(100000);
}
kill(pid, SIGKILL);
while (waitpid(pid, nullptr, 0) == -1 && errno == EINTR)
{
}
}
static void shutdown_stubslave_parent(void)
{
// Re-entrancy guard (#1939). ShutdownSlave() below drives a call packet
// through Pipe_SendReceive, which spins the pump (script_pipepump). When
// the slave is already gone, that pump write/read fails and script_pipepump
// calls back into this function to tear the transport down -- while a
// ShutdownSlave() round-trip is still on the stack and g_pISlaveControl is
// still non-null. Without this guard each pump failure launches a fresh
// ShutdownSlave(), and the mutual recursion overflows the stack (SIGSEGV in
// script_pipepump's prologue). The pump's own MUX_E_FAIL return already
// unwinds the in-flight send; the outermost call finishes teardown once.
static bool bInShutdown = false;
if (bInShutdown)
{
return;
}
bInShutdown = true;
if (nullptr != g_pISlaveControl)
{
(void)g_pISlaveControl->ShutdownSlave();
g_pISlaveControl->Release();
g_pISlaveControl = nullptr;
}
if (g_stub_fd >= 0)
{
::shutdown(g_stub_fd, SHUT_RDWR);
mux_close(g_stub_fd);
g_stub_fd = -1;
}
g_stub_write_remainder.clear();
if (g_stub_pid > 0)
{
reap_stubslave_bounded(g_stub_pid);
g_stub_pid = 0;
}
bInShutdown = false;
}
extern "C" MUX_RESULT DCL_API script_pipepump(void)
{
if (g_stub_fd < 0)
{
return MUX_E_FAIL;
}
struct pollfd pfd;
pfd.fd = g_stub_fd;
pfd.events = POLLIN;
if ( 0 < Pipe_QueueLength(&g_stub_queue_out)
|| !g_stub_write_remainder.empty())
{
pfd.events |= POLLOUT;
}
pfd.revents = 0;
int found = poll(&pfd, 1, -1);
if (found < 0)
{
if (EINTR == errno)
{
return MUX_S_OK;
}
return MUX_E_FAIL;
}
if (pfd.revents & (POLLIN | POLLHUP | POLLERR))
{
char buf[LBUF_SIZE];
for (;;)
{
int len = mux_read(g_stub_fd, buf, sizeof(buf));
if (len < 0)
{
if (EAGAIN == errno || EWOULDBLOCK == errno)
{
break;
}
shutdown_stubslave_parent();
return MUX_E_FAIL;
}
if (0 == len)
{
shutdown_stubslave_parent();
return MUX_E_FAIL;
}
Pipe_AppendBytes(&g_stub_queue_in, static_cast<size_t>(len), buf);
}
}
// Drain Queue_Out. Pipe_GetBytes dequeues immediately, so short writes
// and EAGAIN must keep the unwritten tail (same fix as #1276).
//
if ( g_stub_fd >= 0
&& ( !g_stub_write_remainder.empty()
|| (pfd.revents & POLLOUT)
|| 0 < Pipe_QueueLength(&g_stub_queue_out)))
{
if (g_stub_write_remainder.empty())
{
char buf[LBUF_SIZE];
size_t nWanted = sizeof(buf);
if (Pipe_GetBytes(&g_stub_queue_out, &nWanted, buf) && 0 < nWanted)
{
g_stub_write_remainder.assign(buf, nWanted);
}
}
while (!g_stub_write_remainder.empty())
{
const char *p = g_stub_write_remainder.data();
size_t n = g_stub_write_remainder.size();
int len = mux_write(g_stub_fd, p, n);
if (len > 0)
{
g_stub_write_remainder.erase(0, static_cast<size_t>(len));
continue;
}
if (len < 0)
{
if (EAGAIN == errno || EWOULDBLOCK == errno)
{
break;
}
shutdown_stubslave_parent();
return MUX_E_FAIL;
}
break;
}
}
return MUX_S_OK;
}
// Fork/exec bin/stubslave with a socketpair. Mirrors CPlatform::BootHelperProcess
// without pulling platform.cpp (and its netmux-only deps) into muxscript.
//
static bool boot_stubslave_helper(void)
{
int sv[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0)
{
fprintf(stderr, "muxscript: stubslave socketpair failed: %s\n",
strerror(errno));
return false;
}
{
int flags = fcntl(sv[0], F_GETFL, 0);
if (flags < 0 || fcntl(sv[0], F_SETFL, flags | O_NONBLOCK) < 0)
{
fprintf(stderr, "muxscript: stubslave fcntl(O_NONBLOCK) failed: %s\n",
strerror(errno));
mux_close(sv[0]);
mux_close(sv[1]);
return false;
}
}
pid_t childPid = fork();
if (childPid < 0)
{
fprintf(stderr, "muxscript: stubslave fork failed: %s\n", strerror(errno));
mux_close(sv[0]);
mux_close(sv[1]);
return false;
}
if (0 == childPid)
{
// Child: reset lock-free alarm flag only — never clear() (mutex may
// be locked across fork). Then exec stubslave on the socketpair.
//
alarm_clock.alarmed.store(false);
mux_close(sv[0]);
if (sv[1] != 0)
{
mux_close(0);
if (dup2(sv[1], 0) == -1)
{
_exit(1);
}
}
if (sv[1] != 1)
{
mux_close(1);
if (dup2(sv[1], 1) == -1)
{
_exit(1);
}
}
if (sv[1] > 1)
{
mux_close(sv[1]);
}
// Nested glibc version test — see platform.cpp / #1303.
//
#if defined(__linux__) && defined(__GLIBC_PREREQ)
# if __GLIBC_PREREQ(2, 34)
# define MUXSCRIPT_USE_CLOSE_RANGE 1
# endif
#endif
#if defined(MUXSCRIPT_USE_CLOSE_RANGE)
(void)close_range(3, ~0U, 0);
#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
|| defined(__DragonFly__) || defined(__sun)
closefrom(3);
#else
{
int maxfds = 256;
#ifdef HAVE_GETDTABLESIZE
maxfds = getdtablesize();
#else
maxfds = static_cast<int>(sysconf(_SC_OPEN_MAX));
#endif
if (maxfds < 3)
{
maxfds = 256;
}
for (int i = 3; i < maxfds; i++)
{
mux_close(i);
}
}
#endif
execlp("bin/stubslave", "stubslave", static_cast<char *>(nullptr));
_exit(1);
}
mux_close(sv[1]);
g_stub_fd = sv[0];
g_stub_pid = childPid;
return true;
}
// Attach pipe pump and open the StubSlave management interface. Call after
// mux_InitModuleLibrary and before LoadGame so conf "module X local" works.
// Soft-fails (returns false, continues in-proc-only) if the helper is missing
// — same policy as netmux when BootHelperProcess fails.
//
static bool init_stubslave_for_script(void)
{
if (!boot_stubslave_helper())
{
return false;
}
Pipe_InitializeQueueInfo(&g_stub_queue_in);
Pipe_InitializeQueueInfo(&g_stub_queue_out);
MUX_RESULT mr = mux_InitModuleLibraryPump(script_pipepump,
&g_stub_queue_in,
&g_stub_queue_out);
if (MUX_FAILED(mr))
{
fprintf(stderr, "muxscript: mux_InitModuleLibraryPump failed (%d)\n", mr);
shutdown_stubslave_parent();
return false;
}
mr = mux_CreateInstance(CID_StubSlave, nullptr, UseSlaveProcess,
IID_ISlaveControl,
reinterpret_cast<void **>(&g_pISlaveControl));
if (MUX_FAILED(mr) || nullptr == g_pISlaveControl)
{
fprintf(stderr,
"muxscript: StubSlave management interface unavailable (%d)\n",
mr);
shutdown_stubslave_parent();
return false;
}
fprintf(stderr, "muxscript: stubslave attached (pid %d)\n",
static_cast<int>(g_stub_pid));
return true;
}
#endif // STUB_SLAVE && HAVE_WORKING_FORK
// Unwind whatever init_com() established, in reverse (#1598).
//
// These failure paths used to return straight to main, which returns 2
// without touching the module library -- unlike the LoadGame failure path
// just below, which revokes the class objects and finalizes the library
// first. So a failed engine load left the library initialized and the
// script classes registered. Nothing is known to break as a result; the
// asymmetry was simply unintended, and an exit path that leaves a loaded
// module library behind is not a thing to rely on.
//
// This is NOT the fix for the hang reported in #1598, which did not
// reproduce on aarch64 or arm64 -- muxscript exits 2 in ~20ms with a
// deliberately broken engine.so, before and after this change. The
// alarm_clock join cannot explain a muxscript hang either: the worker is
// lazy since #1035 and muxscript never arms it (#1597).
//
static void init_com_unwind(bool bClassesRegistered)
{
if (bClassesRegistered)
{
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
}
mux_FinalizeModuleLibrary();
}
static MUX_RESULT init_com(void)
{
MUX_RESULT mr = mux_InitModuleLibrary(IsMainProcess);
if (MUX_FAILED(mr))
{
fprintf(stderr, "muxscript: mux_InitModuleLibrary failed (%d)\n", mr);
return mr;
}
// Register script-mode driver classes BEFORE loading engine.so.
mr = mux_RegisterClassObjects(NUM_SCRIPT_CLASSES, script_classes,
script_GetClassObject);
if (MUX_FAILED(mr))
{
fprintf(stderr, "muxscript: mux_RegisterClassObjects failed (%d)\n", mr);
init_com_unwind(false);
return mr;
}
// Build engine path relative to game directory.
#if defined(WIN32)
// Add game/bin to DLL search path so engine.dll can find libmux.dll.
{
wchar_t bindir[4096 + 64];
swprintf(bindir, sizeof(bindir)/sizeof(wchar_t), L"%hs\\bin", g_gamedir);
SetDllDirectoryW(bindir);
}
wchar_t engine_pathw[4096 + 64];
swprintf(engine_pathw, sizeof(engine_pathw)/sizeof(wchar_t),
L"%hs\\bin\\engine.dll", g_gamedir);
mr = mux_AddModule(T("engine"), engine_pathw);
#else
char engine_path[4096 + 64];
snprintf(engine_path, sizeof(engine_path), "%s/bin/engine.so", g_gamedir);
mr = mux_AddModule(T("engine"),
reinterpret_cast<const UTF8 *>(engine_path));
#endif
if (MUX_FAILED(mr))
{
#if defined(WIN32)
fprintf(stderr, "muxscript: cannot load engine (%d) path=%ls\n", mr, engine_pathw);
DWORD err = GetLastError();
if (err) fprintf(stderr, "muxscript: GetLastError=%lu\n", err);
#else
fprintf(stderr, "muxscript: cannot load engine (%d) path=%s\n", mr, engine_path);
#endif
init_com_unwind(true);
return mr;
}
// Turn logging on. Until #1633 nothing here ever did, and CLogFile's
// constructor leaves bEnabled false, so CLogFile::WriteBuffer dropped
// every byte -- meaning every STARTLOG in the server was a no-op under
// muxscript. Not just module diagnostics: the whole channel. netmux
// does this at driver.cpp:404 and has all along, which is why the same
// config reports "Module bogusnothere not found" there and nothing here.
//
// Consequences that were live before this: cf_module HAS diagnosed an
// unloadable module since forever (cf_log_notfound, conf.cpp), and that
// line had never once reached a human -- #1594 was diagnosed from
// silence that was not real silence. And #1620/#1629/#1630 all deliver
// "log the refused write", which no harness could assert.
//
// stderr rather than a log file: tools/Smoke redirects muxscript's
// stderr into netmux.log and greps it, so that is where a driver can
// already see it. A <basename>-<timestamp>.log file would land where
// nothing looks and would need Makesmoke taught to clean it up.
//
// The interface is released immediately on purpose. The log state lives
// in the engine's global CLogFile, not in this pointer, so holding a
// reference for the process lifetime would buy nothing but teardown
// paths to get wrong.
//
mux_ILog *pILog = nullptr;
if ( MUX_SUCCEEDED(mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
IID_ILog,
reinterpret_cast<void **>(&pILog)))
&& nullptr != pILog)
{
pILog->SetBasename(T("-"));
pILog->StartLogging();
pILog->Release();
}
else
{
fprintf(stderr, "muxscript: logging unavailable; server diagnostics "
"will be silent (#1633)\n");
}
// Create IGameEngine via COM.
mr = mux_CreateInstance(CID_GameEngine, nullptr, UseSameProcess,
IID_IGameEngine,
reinterpret_cast<void **>(&g_pEngine));
if (MUX_FAILED(mr) || !g_pEngine)
{
fprintf(stderr, "muxscript: cannot create IGameEngine (%d)\n", mr);
init_com_unwind(true);
return mr;
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// Command execution helpers.
// ---------------------------------------------------------------------------
static void execute_command(const UTF8 *line)
{
UTF8 *pLogBuf = nullptr;
g_pEngine->PrepareForCommand(g_player);
g_pEngine->ProcessCommand(g_player, g_player, g_player, 0, true,
const_cast<UTF8 *>(line), nullptr, 0, &pLogBuf);
g_pEngine->FinishCommand();
fflush(stdout);
}
static void run_tasks_now(void)
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetUTC();
g_pEngine->RunTasks(ltaNow);
fflush(stdout);
}
// ---------------------------------------------------------------------------
// Event loop: interleave stdin input with queue processing.
//
// Uses poll() on stdin so the engine can process queued commands
// between input lines. After EOF on stdin, continues draining the
// queue until no tasks remain.
// ---------------------------------------------------------------------------
// Helper: process complete lines in linebuf, returning new linepos.
//
static size_t process_lines(UTF8 *linebuf, size_t linepos)
{
size_t start = 0;
for (size_t i = 0; i < linepos; i++)
{
if (linebuf[i] == '\n')
{
linebuf[i] = '\0';
size_t len = i - start;
if (len > 0 && linebuf[start + len - 1] == '\r')
{
linebuf[start + len - 1] = '\0';
len--;
}
UTF8 *line = linebuf + start;
if (len > 0
&& !(line[0] == '#'
&& (len < 2 || line[1] < '0' || line[1] > '9')))
{
execute_command(line);
}
start = i + 1;
}
}
if (start > 0)
{
linepos -= start;
if (linepos > 0)
{
memmove(linebuf, linebuf + start, linepos);
}
}
return linepos;
}
// Helper: compute scheduler timeout in milliseconds.
// Returns -1 for infinite wait, 0 for immediate, or positive ms.
//
static int scheduler_timeout_ms(bool eof_seen)
{
CLinearTimeAbsolute ltaNext;
if (MUX_SUCCEEDED(g_pEngine->WhenNext(&ltaNext)))
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetUTC();
if (ltaNext <= ltaNow)
{
return 0;
}
long ms = (ltaNext - ltaNow).ReturnMilliseconds();
if (ms > 1000) ms = 1000;
return (int)ms;
}
return eof_seen ? -2 : -1; // -2 = no tasks + eof = exit
}
// Helper: once stdin is exhausted, the script loop should finish as soon as
// no real user work remains. Recurring system tasks (dumps, idle checks,
// keepalives) would otherwise keep the scheduler non-empty forever; delayed
// @wait tasks still count as user work, so they are honored before exit.
//
static bool script_should_exit_on_eof(bool eof_seen)
{
if (!eof_seen)
{
return false;
}
bool bPending = false;
return MUX_FAILED(g_pEngine->HasPendingUserTasks(&bPending)) || !bPending;
}
#if defined(WIN32)
static void script_loop(FILE *input)
{
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
int fd = _fileno(input);
bool eof_seen = false;
UTF8 linebuf[8192];
size_t linepos = 0;
while (!g_script_shutdown)
{
int timeout_ms = scheduler_timeout_ms(eof_seen);
if (timeout_ms == -2) break; // no tasks + eof
if (!eof_seen)
{
DWORD waitResult = WaitForSingleObject(hStdin,
timeout_ms < 0 ? INFINITE : (DWORD)timeout_ms);
if (waitResult == WAIT_OBJECT_0)
{
int n = _read(fd, linebuf + linepos,
(unsigned)(sizeof(linebuf) - linepos - 1));
if (n > 0)
{
linepos += (size_t)n;
linepos = process_lines(linebuf, linepos);
if (linepos >= sizeof(linebuf) - 1)
{
linebuf[linepos] = '\0';
execute_command(linebuf);
linepos = 0;
}
}
else
{
eof_seen = true;
if (linepos > 0)
{
linebuf[linepos] = '\0';
size_t len = linepos;
if (len > 0 && linebuf[len - 1] == '\r')
{
linebuf[--len] = '\0';
}
if (len > 0
&& !(linebuf[0] == '#'
&& (len < 2 || linebuf[1] < '0' || linebuf[1] > '9')))
{
execute_command(linebuf);
}
linepos = 0;
}
}
}
}
else
{
if (timeout_ms > 0)
{
Sleep((DWORD)timeout_ms);
}
}
for (int i = 0; i < 100; i++)
{
run_tasks_now();
CLinearTimeAbsolute ltaCheck;
if (MUX_FAILED(g_pEngine->WhenNext(&ltaCheck)))
{
break;
}
CLinearTimeAbsolute ltaNow;
ltaNow.GetUTC();
if (ltaCheck > ltaNow)
{
break;
}
}
if (script_should_exit_on_eof(eof_seen))
{
break;
}
}
}
#else // Unix
static void script_loop(FILE *input)
{
int fd = fileno(input);
// Set stdin to non-blocking so we can interleave input with
// queue processing.
//
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0)
{
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
bool eof_seen = false;
UTF8 linebuf[8192];
size_t linepos = 0;
while (!g_script_shutdown)
{
int timeout_ms = scheduler_timeout_ms(eof_seen);
if (timeout_ms == -2) break; // no tasks + eof
// Poll for stdin readability.
//
if (!eof_seen)
{
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
int found = poll(&pfd, 1, timeout_ms < 0 ? -1 : timeout_ms);
if (found < 0 && EINTR != errno)
{
break;
}
// Read available bytes and process complete lines. We attempt
// the read regardless of what poll() reported: on macOS poll()
// does not flag /dev/null (or some regular files) as readable, so
// gating the read on poll's verdict would miss input and never
// detect EOF. The descriptor is non-blocking, so a read with no
// data simply returns EAGAIN and we fall through.
//
{
for (;;)
{
int n = read(fd, linebuf + linepos,
sizeof(linebuf) - linepos - 1);
if (n > 0)
{
linepos += (size_t)n;
linepos = process_lines(linebuf, linepos);
// Protect against line overflow.
//
if (linepos >= sizeof(linebuf) - 1)
{
linebuf[linepos] = '\0';
execute_command(linebuf);
linepos = 0;
}
}
else if (n == 0)
{
// EOF on stdin.
//
eof_seen = true;
// Process any remaining partial line.
//
if (linepos > 0)
{
linebuf[linepos] = '\0';
size_t len = linepos;
if (len > 0 && linebuf[len - 1] == '\r')
{
linebuf[--len] = '\0';
}
if (len > 0
&& !(linebuf[0] == '#'
&& (len < 2 || linebuf[1] < '0' || linebuf[1] > '9')))
{
execute_command(linebuf);
}
linepos = 0;
}
break;
}
else
{
// EAGAIN — no more data right now.
//
break;
}
}
}
}
else
{
// stdin is EOF — just sleep until next task is due.
//
if (timeout_ms > 0)
{
poll(nullptr, 0, timeout_ms);
}
}
// Run ready tasks. Loop until no more are immediately ready,
// since processing a task may queue additional tasks.
//
for (int i = 0; i < 100; i++)
{
run_tasks_now();
// Check if more tasks are immediately ready.
//
CLinearTimeAbsolute ltaCheck;
if (MUX_FAILED(g_pEngine->WhenNext(&ltaCheck)))
{
break; // No more tasks.
}
CLinearTimeAbsolute ltaNow2;
ltaNow2.GetUTC();
if (ltaCheck > ltaNow2)
{
break; // Next task is in the future.
}
}
if (script_should_exit_on_eof(eof_seen))
{
break;
}
}
}
#endif // WIN32 / Unix
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
static void usage(void)
{
fprintf(stderr,
"Usage: muxscript [options]\n"
" -g <dir> Game directory (default: $MUX_HOME or cwd)\n"
" -c <config> Configuration file (default: netmux.conf)\n"
" -e <expr> Evaluate single expression\n"
" -p <player> Run as player (dbref like #4 or number, default: #1)\n"
" --readonly Don't save database on exit\n"
" --help Show this help\n"
"\n"
"Reads softcode commands from stdin (or -e) and writes output to stdout.\n"
"Loads engine.so from <gamedir>/bin/engine.so.\n"
"\n"
"Game directory resolution:\n"
" 1. -g <dir> flag\n"
" 2. MUX_HOME environment variable\n"
" 3. Current working directory\n"
);
}
int main(int argc, char *argv[])
{
const char *conffile = "netmux.conf";
const char *gamedir_flag = nullptr;
const char *eval_expr = nullptr;
const char *player_arg = nullptr;
bool readonly = false;
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "-g") == 0 && i + 1 < argc)
{
gamedir_flag = argv[++i];
}
else if (strcmp(argv[i], "-c") == 0 && i + 1 < argc)
{
conffile = argv[++i];
}
else if (strcmp(argv[i], "-e") == 0 && i + 1 < argc)
{
eval_expr = argv[++i];
}
else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc)
{
player_arg = argv[++i];
}
else if (strcmp(argv[i], "--readonly") == 0)
{
readonly = true;
}
else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
{
usage();
return 0;
}
else
{
fprintf(stderr, "muxscript: unknown option '%s'\n", argv[i]);
usage();
return 1;
}
}
// Resolve game directory.
if (!resolve_gamedir(gamedir_flag))
{
return 1;
}
// chdir to game directory so config file paths resolve correctly.
if (chdir(g_gamedir) != 0)
{
fprintf(stderr, "muxscript: cannot chdir to '%s': %s\n",
g_gamedir, strerror(errno));
return 1;
}
// Runtime initialization.
SeedRandomNumberGenerator();
pool_init(POOL_LBUF, LBUF_SIZE);
pool_init(POOL_MBUF, MBUF_SIZE);
pool_init(POOL_SBUF, SBUF_SIZE);
// Initialize COM and load engine.so.
MUX_RESULT mr = init_com();
if (MUX_FAILED(mr))
{
return 2;
}
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
// Boot the release-config helper before LoadGame so conf can load
// modules as "local"/"slave". Soft-fail leaves in-proc modules working
// (same as netmux when BootHelperProcess fails). #1293
//
if (!init_stubslave_for_script())
{
fprintf(stderr,
"muxscript: continuing without stubslave "
"(in-proc modules only)\n");
}
#endif
// Load game database.
mr = g_pEngine->LoadGame(reinterpret_cast<const UTF8 *>(conffile),
nullptr, false);
if (MUX_FAILED(mr))
{
// A harness driving muxscript needs to know that the config never
// loaded, not just that "LoadGame failed" (#1601). Before this was
// fatal, an unreadable config produced a green run against the
// compiled-in defaults -- indistinguishable from a real one.
//
if (MUX_E_NOTFOUND == mr)
{
fprintf(stderr, "muxscript: cannot read configuration file '%s'\n",
conffile);
}
fprintf(stderr, "muxscript: LoadGame failed (%d)\n", mr);
g_pEngine->Release();
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
shutdown_stubslave_parent();
#endif
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
mux_FinalizeModuleLibrary();
return 2;
}
// Set timing state.
CLinearTimeAbsolute ltaNow;
ltaNow.GetUTC();
g_pEngine->SetStartTime(ltaNow);
g_pEngine->SetRestartTime(ltaNow);
g_pEngine->SetRestartCount(0);
g_pEngine->SetCpuCountFrom(ltaNow);
// Post-load initialization.
mr = g_pEngine->Startup();
if (MUX_FAILED(mr))
{
fprintf(stderr, "muxscript: Startup failed (%d)\n", mr);
g_pEngine->Release();
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
shutdown_stubslave_parent();
#endif
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
mux_FinalizeModuleLibrary();
return 2;
}
// Resolve -p player argument.
//
if (player_arg)
{
const char *p = player_arg;
if (*p == '#') p++;
char *end = nullptr;
long val = strtol(p, &end, 10);
if (end == p || *end != '\0' || val < 0)
{
fprintf(stderr, "muxscript: invalid player '%s' (use dbref like #4 or 4)\n",
player_arg);
g_pEngine->Release();
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
shutdown_stubslave_parent();
#endif
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
mux_FinalizeModuleLibrary();
return 1;
}
g_player = static_cast<dbref>(val);
}
fprintf(stderr, "muxscript: loaded game from %s (player #%d)\n",
g_gamedir, g_player);
// Mark the player as connected so raw_notify() delivers output.
// Reject invalid -p early rather than ProcessCommand'ing a garbage dbref.
//
if (MUX_FAILED(g_pEngine->MarkConnected(g_player)))
{
fprintf(stderr,
"muxscript: player #%d is not a valid player object\n",
g_player);
g_pEngine->Shutdown();
g_pEngine->Release();
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
shutdown_stubslave_parent();
#endif
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
mux_FinalizeModuleLibrary();
return 1;
}
// Run script.
if (eval_expr)
{
execute_command(reinterpret_cast<const UTF8 *>(eval_expr));
run_tasks_now();
}
else
{
script_loop(stdin);
}
// Save and shutdown.
if (!readonly)
{
g_pEngine->DumpDatabase();
}
g_pEngine->Shutdown();
g_pEngine->Release();
#if defined(STUB_SLAVE) && defined(HAVE_WORKING_FORK)
shutdown_stubslave_parent();
#endif
// Revoke script-mode classes and finalize.
mux_RevokeClassObjects(NUM_SCRIPT_CLASSES, script_classes);
mux_FinalizeModuleLibrary();
return 0;
}