/* Synchronet terminal server thread and related functions */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This program is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU General Public License				*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "sbbs.h"
#include "ident.h"
#include "telnet.h"
#include "netwrap.h"
#include "petdefs.h"
#include "filedat.h"
#include "js_rtpool.h"
#include "js_request.h"
#include "os_info.h"
#include "ssl.h"
#include "ver.h"
#include "eventwrap.h"
#include "filterfile.hpp"
#include <multisock.h>
#include <limits.h>     // HOST_NAME_MAX

#ifdef __unix__
	#include <sys/un.h>
#endif

//#define SBBS_TELNET_ENVIRON_SUPPORT 1
//---------------------------------------------------------------------------

#define TELNET_SERVER "Synchronet Terminal Server"
static const char* server_abbrev = "term";

static const char* eventIniSection = "Events";
static ini_style_t eventIniStyle = {
	/* key_len: */ LEN_CODE
};
static ini_style_t qhubIniStyle = {
	/* key_len: */ LEN_QWKID
};
static const char* qhubIniSection = "QWK_NetworkHubs";

#define TIMEOUT_THREAD_WAIT     60          // Seconds (was 15)
#define IO_THREAD_BUF_SIZE      20000       // Bytes
#define TIMEOUT_MUTEX_FILE      12 * 60 * 60

#ifdef USE_CRYPTLIB

static protected_uint32_t ssh_sessions;

void ssh_session_destroy(SOCKET sock, CRYPT_SESSION session, int line)
{
	int result = cryptDestroySession(session);

	if (result != 0)
		lprintf(LOG_ERR, "%04d SSH ERROR %d destroying Cryptlib Session %d from line %d"
		        , sock, result, session, line);
	else {
		uint32_t remain = protected_uint32_adjust_fetch(&ssh_sessions, -1);
		lprintf(LOG_DEBUG, "%04d SSH Cryptlib Session: %d destroyed from line %d (%u remain)"
		        , sock, session, line, remain);
	}
}

	#define SSH_END(sock) do {                                      \
				if (ssh) {                                                   \
					pthread_mutex_lock(&sbbs->ssh_mutex);                   \
					ssh_session_destroy(sock, sbbs->ssh_session, __LINE__); \
					sbbs->ssh_mode = false;                                 \
					pthread_mutex_unlock(&sbbs->ssh_mutex);                 \
				}                                                           \
} while (0)
#else
	#define SSH_END(x)
#endif

time_t                     uptime = 0;
volatile uint              served = 0;

static std::atomic<time_t> event_thread_tick;
static bool                event_thread_blocked;
static protected_uint32_t  node_threads_running;
static volatile uint32_t   client_highwater = 0;

time_t                     laston_time;
char                       lastuseron[LEN_ALIAS + 1]; /* Name of user last online */
RingBuf*                   node_inbuf[MAX_NODES];
SOCKET                     spy_socket[MAX_NODES];
#ifdef __unix__
SOCKET                     uspy_socket[MAX_NODES]; /* UNIX domain spy sockets */
#endif
SOCKET                     node_socket[MAX_NODES];
struct xpms_set *          ts_set;
static scfg_t              scfg;
static struct mqtt         mqtt;
static char *              text[TOTAL_TEXT];
static scfg_t              node_scfg[MAX_NODES];
static char *              node_text[MAX_NODES][TOTAL_TEXT];
static WORD                first_node;
static WORD                last_node;
static bool                terminate_server = false;
static str_list_t          pause_semfiles;
static str_list_t          recycle_semfiles;
static str_list_t          shutdown_semfiles;
static str_list_t          clear_attempts_semfiles;
static link_list_t         current_logins;
static link_list_t         current_connections;
static link_list_t         max_concurrent_attempts;
static void                clearMaxConcurrentAttempt(const char* host_ip);

static trashCan            ip_can;
static trashCan            ip_silent_can;
static trashCan            host_can;
static filterFile          host_exempt;

#ifdef _THREAD_SUID_BROKEN
int                        thread_suid_broken = true; /* NPTL is no longer broken */
#endif

/* convenient space-saving global variables */
extern "C" {
const char* crlf = "\r\n";
const char* nulstr = "";
};

#define GCES(status, node, sess, action) do {                          \
			char *GCES_estr;                                                    \
			int   GCES_level;                                                      \
			get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level); \
			if (GCES_estr) {                                                       \
				if (GCES_level < startup->ssh_error_level)                          \
				GCES_level = startup->ssh_error_level;                           \
				lprintf(GCES_level, "Node %d SSH %s from %s", node, GCES_estr, __FUNCTION__);             \
				free_crypt_attrstr(GCES_estr);                                       \
			}                                                                         \
} while (0)

#define GCESNN(status, sess, action) do {                              \
			char *GCES_estr;                                                    \
			int   GCES_level;                                                      \
			get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level); \
			if (GCES_estr) {                                                       \
				if (GCES_level < startup->ssh_error_level)                          \
				GCES_level = startup->ssh_error_level;                           \
				lprintf(GCES_level, "SSH %s from %s", GCES_estr, __FUNCTION__);     \
				free_crypt_attrstr(GCES_estr);                                       \
			}                                                                         \
} while (0)

#define GCESS(status, sock, sess, action) do {                         \
			char *GCES_estr;                                                    \
			int   GCES_level;                                                      \
			get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level); \
			if (GCES_estr) {                                                       \
				if (GCES_level < startup->ssh_error_level)                          \
				GCES_level = startup->ssh_error_level;                           \
				lprintf(GCES_level, "%04d SSH %s from %s", sock, GCES_estr, __FUNCTION__);                \
				free_crypt_attrstr(GCES_estr);                                       \
			}                                                                         \
} while (0)

#define GCESSTR(status, str, sess, action) do {                         \
			char *GCES_estr;                                                    \
			int   GCES_level;                                                      \
			get_crypt_error_string(status, sess, &GCES_estr, action, &GCES_level); \
			if (GCES_estr) {                                                       \
				if (GCES_level < startup->ssh_error_level)                          \
				GCES_level = startup->ssh_error_level;                           \
				lprintf(GCES_level, "%s SSH %s from %s (session %d)", str, GCES_estr, __FUNCTION__, sess);                \
				free_crypt_attrstr(GCES_estr);                                       \
			}                                                                         \
} while (0)


extern "C" {

static bbs_startup_t* startup = NULL;

static void set_state(enum server_state state)
{
	static int curr_state;

	if (state == curr_state)
		return;

	if (startup != NULL) {
		if (startup->set_state != NULL)
			startup->set_state(startup->cbdata, state);
		mqtt_server_state(&mqtt, state);
	}
	curr_state = state;
}

static void update_clients()
{
	if (startup != NULL) {
		if (startup->clients != NULL)
			startup->clients(startup->cbdata, protected_uint32_value(node_threads_running));
	}
}

void client_on(SOCKET sock, client_t* client, BOOL update)
{
	if (!update)
		listAddNodeData(&current_connections, client->addr, strlen(client->addr) + 1, sock, LAST_NODE);
	if (startup != NULL && startup->client_on != NULL)
		startup->client_on(startup->cbdata, true, sock, client, update);
	mqtt_client_on(&mqtt, true, sock, client, update);
}

static void client_off(SOCKET sock)
{
	listRemoveTaggedNode(&current_connections, sock, /* free_data */ true);
	if (startup != NULL && startup->client_on != NULL)
		startup->client_on(startup->cbdata, false, sock, NULL, false);
	mqtt_client_on(&mqtt, false, sock, NULL, false);
}

static void thread_up(BOOL setuid)
{
	if (startup != NULL && startup->thread_up != NULL)
		startup->thread_up(startup->cbdata, true, setuid);
}

static void thread_down()
{
	if (startup != NULL && startup->thread_up != NULL)
		startup->thread_up(startup->cbdata, false, false);
}

int lputs(int level, const char* str)
{
	mqtt_lputs(&mqtt, TOPIC_SERVER, level, str);
	if (level <= LOG_ERR) {
		char errmsg[1024];
		SAFEPRINTF2(errmsg, "%-4s %s", server_abbrev, str);
		errorlog(&scfg, &mqtt, level, startup == NULL ? NULL:startup->host_name, errmsg);
		if (startup != NULL && startup->errormsg != NULL)
			startup->errormsg(startup->cbdata, level, errmsg);
	}

	if (startup == NULL || startup->lputs == NULL || str == NULL || level > startup->log_level)
		return 0;

#if defined(_WIN32)
	if (IsBadCodePtr((FARPROC)startup->lputs))
		return 0;
#endif

	return startup->lputs(startup->cbdata, level, str);
}

int eputs(int level, const char *str)
{
	if (*str == 0)
		return 0;

	mqtt_lputs(&mqtt, TOPIC_HOST_EVENT, level, str);

	if (level <= LOG_ERR) {
		char errmsg[1024];
		SAFEPRINTF(errmsg, "evnt %s", str);
		errorlog(&scfg, &mqtt, level, startup == NULL ? NULL:startup->host_name, errmsg);
		if (startup != NULL && startup->errormsg != NULL)
			startup->errormsg(startup->cbdata, level, errmsg);
	}

	if (startup == NULL || startup->event_lputs == NULL || level > startup->event_log_level)
		return 0;

	return startup->event_lputs(startup->event_cbdata, level, str);
}

int lprintf(int level, const char *fmt, ...)
{
	va_list argptr;
	char    sbuf[1024];

	va_start(argptr, fmt);
	vsnprintf(sbuf, sizeof(sbuf), fmt, argptr);
	sbuf[sizeof(sbuf) - 1] = 0;
	va_end(argptr);
	return lputs(level, sbuf);
}

DLLEXPORT uint repeated_error(int line, const char* function)
{
	static int         lastline;
	static const char* lastfunc;
	static uint        repeat_count;

	if (line == lastline && function == lastfunc)
		++repeat_count;
	else
		repeat_count = 0;
	lastline = line;
	lastfunc = function;
	return repeat_count;
}

int errprintf(int level, int line, const char* function, const char* file, const char *fmt, ...)
{
	va_list argptr;
	char    sbuf[1024];

	va_start(argptr, fmt);
	vsnprintf(sbuf, sizeof(sbuf), fmt, argptr);
	sbuf[sizeof(sbuf) - 1] = 0;
	va_end(argptr);
	if (repeated_error(line, function)) {
		if (level < LOG_WARNING)
			level = LOG_WARNING;
	}
	return lputs(level, sbuf);
}

void
sbbs_t::log_crypt_error_status_sock(int status, const char *action)
{
	char *estr;
	int   level;
	if (cryptStatusError(status)) {
		get_crypt_error_string(status, ssh_session, &estr, action, &level);
		if (estr) {
			if (level < startup->ssh_error_level)
				level = startup->ssh_error_level;
			lprintf(level, "%04d SSH %s", client_socket.load(), estr);
			free_crypt_attrstr(estr);
		}
	}
}

/* Picks the right log callback function (event or term) based on the sbbs->cfg.node_num value */
/* Prepends the current node number and user alias (if applicable) */
int sbbs_t::lputs(int level, const char* str)
{
	char msg[2048];
	char prefix[32] = "";
	char user_str[64] = "";

	if (is_event_thread && event_code != NULL && *event_code)
		SAFEPRINTF(prefix, "%s ", event_code);
	else if (cfg.node_num && !is_event_thread)
		SAFEPRINTF(prefix, "Node %d ", cfg.node_num);
	else if (client_name[0])
		SAFEPRINTF(prefix, "%s ", client_name);
	if (useron.number)
		SAFEPRINTF(user_str, "<%s> ", useron.alias);
	SAFEPRINTF3(msg, "%s%s%s", prefix, user_str, str);
	strip_ctrl(msg, msg);
	if (is_event_thread) {
		if (startup != nullptr && level <= startup->event_log_level) {
			if (logfile_fp == nullptr) {
				char str[128];
				if(startup->first_node > 1)
					snprintf(str, sizeof str, "%u", startup->first_node);
				else
					str[0] = '\0';
				char path[MAX_PATH + 1];
				snprintf(path, sizeof path, "%sevents%s.log", cfg.logs_dir, str);
				logfile_fp = fopenlog(&cfg, path, /* shareable: */true);
			}
			if (logfile_fp != nullptr)
				fprintlog(&cfg, &logfile_fp, msg);
		}
		return ::eputs(level, msg);
	}
	return ::lputs(level, msg);
}

int sbbs_t::lprintf(int level, const char *fmt, ...)
{
	va_list argptr;
	char    sbuf[1024];

	va_start(argptr, fmt);
	vsnprintf(sbuf, sizeof(sbuf), fmt, argptr);
	sbuf[sizeof(sbuf) - 1] = 0;
	va_end(argptr);
	return lputs(level, sbuf);
}

int sbbs_t::errprintf(int level, int line, const char* function, const char* file, const char* fmt, ...)
{
	va_list argptr;
	char    sbuf[1024];

	va_start(argptr, fmt);
	vsnprintf(sbuf, sizeof(sbuf), fmt, argptr);
	sbuf[sizeof(sbuf) - 1] = 0;
	va_end(argptr);
	if (repeated_error(line, function)) {
		if (level < LOG_WARNING)
			level = LOG_WARNING;
	}
	return lputs(level, sbuf);
}

struct main_sock_cb_data {
	bbs_startup_t *startup;
	const char *protocol;
};

void sock_cb(SOCKET sock, void *cb_data)
{
	char                      error_str[256];
	struct main_sock_cb_data *cb = (struct main_sock_cb_data *)cb_data;

	if (cb->startup && cb->startup->socket_open)
		cb->startup->socket_open(cb->startup->cbdata, true);
	if (set_socket_options(&scfg, sock, cb->protocol, error_str, sizeof(error_str)))
		lprintf(LOG_ERR, "%04d !ERROR %s", sock, error_str);
}

void sock_close_cb(SOCKET sock, void *cb_data)
{
	bbs_startup_t *su = (bbs_startup_t *)cb_data;

	if (su && su->socket_open)
		su->socket_open(su->cbdata, false);
}

void call_socket_open_callback(bool open)
{
	if (startup != NULL && startup->socket_open != NULL)
		startup->socket_open(startup->cbdata, open);
}

SOCKET open_socket(int domain, int type, const char* protocol)
{
	SOCKET sock;
	char   error[256];

	sock = socket(domain, type, IPPROTO_IP);
	if (sock != INVALID_SOCKET)
		call_socket_open_callback(true);
	if (sock != INVALID_SOCKET && set_socket_options(&scfg, sock, protocol, error, sizeof(error)))
		lprintf(LOG_ERR, "%04d !ERROR %s", sock, error);

	return sock;
}

// Used by sbbs_t::ftp_put() and js_accept()
SOCKET accept_socket(SOCKET s, union xp_sockaddr* addr, socklen_t* addrlen)
{
	SOCKET sock;

	sock = xp_accept(s, addr, addrlen);
	if (sock != INVALID_SOCKET)
		call_socket_open_callback(true);

	return sock;
}

int close_socket(SOCKET sock)
{
	int result;

	if (sock == INVALID_SOCKET || sock == 0)
		return 0;

	shutdown(sock, SHUT_RDWR);   /* required on Unix */
	result = closesocket(sock);
	call_socket_open_callback(false);
	if (result != 0 && SOCKET_ERRNO != ENOTSOCK)
		lprintf(LOG_WARNING, "!ERROR %d closing socket %d", SOCKET_ERRNO, sock);
	return result;
}

in_addr_t resolve_ipv4(const char *addr)
{
	const char* p;
	struct addrinfo* res;
	struct addrinfo hints = {0};
	in_addr_t ipa = INADDR_NONE;

	if (*addr == 0)
		return INADDR_NONE;

	for (p = addr; *p; p++)
		if (*p != '.' && !IS_DIGIT(*p))
			break;
	if (!(*p))
		return parseIPv4Address(addr);

	hints.ai_family = AF_INET;
	if (getaddrinfo(addr, NULL, &hints, &res) != 0)
		return INADDR_NONE;

	if (res->ai_family == AF_INET)
		ipa = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr;
	freeaddrinfo(res);
	return ipa;
}

} /* extern "C" */

#ifdef _WINSOCKAPI_

WSADATA     WSAData;
#define SOCKLIB_DESC WSAData.szDescription
static BOOL WSAInitialized = false;

static BOOL winsock_startup(void)
{
	int status;                 /* Status Code */

	if ((status = WSAStartup(MAKEWORD(1, 1), &WSAData)) == 0) {
		lprintf(LOG_DEBUG, "%s %s", WSAData.szDescription, WSAData.szSystemStatus);
		WSAInitialized = true;
		return true;
	}

	lprintf(LOG_CRIT, "!WinSock startup ERROR %d", status);
	return false;
}

#else /* No WINSOCK */

#define winsock_startup()   (true)
#define SOCKLIB_DESC NULL

#endif

DLLEXPORT void sbbs_srand()
{
	DWORD seed;

	xp_randomize();
#if defined(HAS_DEV_RANDOM) && defined(RANDOM_DEV)
	int   rf, rd = 0;

	if ((rf = open(RANDOM_DEV, O_RDONLY | O_NONBLOCK)) != -1) {
		rd = read(rf, &seed, sizeof(seed));
		close(rf);
	}
	if (rd != sizeof(seed))
#endif
	seed = time32(NULL) ^ (uintmax_t)GetCurrentThreadId();

	srand(seed);
	sbbs_random(10);    /* Throw away first number */
}

int sbbs_random(int n)
{
	return xp_random(n);
}

#ifdef JAVASCRIPT

static js_server_props_t js_server_props;

void* js_GetClassPrivate(JSContext *cx, JSObject *obj, JSClass* cls)
{
	void *ret = JS_GetInstancePrivate(cx, obj, cls, NULL);

	/*
	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
	 *       (ie: anything not Synchronet specific).
	 */
	if (ret == NULL)
		JS_ReportError(cx, "'%s' instance: No Private Data or Class Mismatch"
		               , cls == NULL ? "???" : cls->name);
	return ret;
}

/* Convert from Synchronet-specific jsSyncMethodSpec to JSAPI's JSFunctionSpec */

JSBool
js_DescribeSyncObject(JSContext* cx, JSObject* obj, const char* str, int ver)
{
	JSString* js_str = JS_NewStringCopyZ(cx, str);

	if (js_str == NULL)
		return JS_FALSE;

	if (ver < 10000)     /* auto convert 313 to 31300 */
		ver *= 100;

	return JS_DefineProperty(cx, obj, "_description"
	                         , STRING_TO_JSVAL(js_str), NULL, NULL, JSPROP_READONLY) != JS_FALSE
	       && JS_DefineProperty(cx, obj, "_ver"
	                            , INT_TO_JSVAL(ver), NULL, NULL, JSPROP_READONLY) != JS_FALSE;
}

JSBool
js_DescribeSyncConstructor(JSContext* cx, JSObject* obj, const char* str)
{
	JSString* js_str = JS_NewStringCopyZ(cx, str);

	if (js_str == NULL)
		return JS_FALSE;

	return JS_DefineProperty(cx, obj, "_constructor"
	                         , STRING_TO_JSVAL(js_str), NULL, NULL, JSPROP_READONLY);
}

#ifdef BUILD_JSDOCS

static const char* method_array_name = "_method_list";
static const char* property_array_name = "_property_list";

/*
 * from jsatom.c:
 * Keep this in sync with jspubtd.h -- an assertion below will insist that
 * its length match the JSType enum's JSTYPE_LIMIT limit value.
 */
static const char *js_type_str[] = {
	"void",         // changed from "undefined"
	"object",
	"function",
	"string",
	"number",
	"boolean",
	"null",
	"xml",
	"array",
	"alias",
	"undefined"
};

JSBool
js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
{
	uint      i;
	int       ver;
	jsval     val;
	JSString* js_str;
	JSObject* array;
	JSObject* prop;

	if ((array = JS_NewObject(cx, NULL, NULL, obj)) == NULL)
		return JS_FALSE;

	for (i = 0; props[i].name != NULL; ++i) {
		if (props[i].tinyid < 256 && props[i].tinyid > -129) {
			if (!JS_DefinePropertyWithTinyId(cx, obj, /* Never reserve any "slots" for properties */
			                                 props[i].name, props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
				return JS_FALSE;
		}
		else {
			if (!JS_DefineProperty(cx, obj, props[i].name, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
				return JS_FALSE;
		}
		if (!(props[i].flags & JSPROP_ENUMERATE))   /* No need to document invisible props */
			continue;

		prop = JS_NewObject(cx, NULL, NULL, array);
		if (prop == NULL)
			return JS_FALSE;

		if ((ver = props[i].ver) < 10000)      /* auto convert 313 to 31300 */
			ver *= 100;
		val = INT_TO_JSVAL(ver);
		JS_SetProperty(cx, prop, "ver", &val);

		if (props[i].desc != NULL) {
			if ((js_str = JS_NewStringCopyZ(cx, props[i].desc)) == NULL)
				return JS_FALSE;
			val = STRING_TO_JSVAL(js_str);
			JS_SetProperty(cx, prop, "desc", &val);
		}
		if (!JS_DefineProperty(cx, array, props[i].name, OBJECT_TO_JSVAL(prop), NULL, NULL, JSPROP_READONLY | JSPROP_ENUMERATE))
			return JS_FALSE;
	}

	return JS_DefineProperty(cx, obj, property_array_name, OBJECT_TO_JSVAL(array), NULL, NULL, JSPROP_READONLY);
}

JSBool
js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
{
	int       i;
	jsuint    len = 0;
	int       ver;
	jsval     val;
	JSObject* method;
	JSObject* method_array;
	JSString* js_str;
	size_t    str_len = 0;
	char *    str = NULL;

	/* Return existing method_list array if it's already been created */
	if (JS_GetProperty(cx, obj, method_array_name, &val) && val != JSVAL_VOID) {
		method_array = JSVAL_TO_OBJECT(val);
		// If the first item is already in the list, don't do anything.
		if (method_array == NULL || !JS_GetArrayLength(cx, method_array, &len))
			return JS_FALSE;
		for (i = 0; i < (int)len; i++) {
			if (JS_GetElement(cx, method_array, i, &val) != JS_TRUE || val == JSVAL_VOID)
				continue;
			JS_GetProperty(cx, JSVAL_TO_OBJECT(val), "name", &val);
			JSVALUE_TO_RASTRING(cx, val, str, &str_len, NULL);
			if (str == NULL)
				continue;
			if (strcmp(str, funcs[0].name) == 0)
				return JS_TRUE;
		}
	}
	else {
		if ((method_array = JS_NewArrayObject(cx, 0, NULL)) == NULL)
			return JS_FALSE;
		if (!JS_DefineProperty(cx, obj, method_array_name, OBJECT_TO_JSVAL(method_array)
		                       , NULL, NULL, 0))
			return JS_FALSE;
	}

	for (i = 0; funcs[i].name != NULL; ++i) {

		if (!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
			return JS_FALSE;

		if (funcs[i].type == JSTYPE_ALIAS)
			continue;

		method = JS_NewObject(cx, NULL, NULL, method_array);
		if (method == NULL)
			return JS_FALSE;

		if (funcs[i].name != NULL) {
			if ((js_str = JS_NewStringCopyZ(cx, funcs[i].name)) == NULL)
				return JS_FALSE;
			val = STRING_TO_JSVAL(js_str);
			JS_SetProperty(cx, method, "name", &val);
		}

		val = INT_TO_JSVAL(funcs[i].nargs);
		if (!JS_SetProperty(cx, method, "nargs", &val))
			return JS_FALSE;

		if ((js_str = JS_NewStringCopyZ(cx, js_type_str[funcs[i].type])) == NULL)
			return JS_FALSE;
		val = STRING_TO_JSVAL(js_str);
		JS_SetProperty(cx, method, "type", &val);

		if (funcs[i].args != NULL) {
			if ((js_str = JS_NewStringCopyZ(cx, funcs[i].args)) == NULL)
				return JS_FALSE;
			val = STRING_TO_JSVAL(js_str);
			JS_SetProperty(cx, method, "args", &val);
		}

		if (funcs[i].desc != NULL) {
			if ((js_str = JS_NewStringCopyZ(cx, funcs[i].desc)) == NULL)
				return JS_FALSE;
			val = STRING_TO_JSVAL(js_str);
			JS_SetProperty(cx, method, "desc", &val);
		}

		if (funcs[i].ver) {
			if ((ver = funcs[i].ver) < 10000)      /* auto convert 313 to 31300 */
				ver *= 100;
			val = INT_TO_JSVAL(ver);
			JS_SetProperty(cx, method, "ver", &val);
		}

		val = OBJECT_TO_JSVAL(method);
		if (!JS_SetElement(cx, method_array, len + i, &val))
			return JS_FALSE;
	}

	return JS_TRUE;
}

/*
 * Always resolve all here since
 * 1) We'll always be enumerating anyways
 * 2) The speed penalty won't be seen in production code anyways
 */
JSBool
js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertySpec* props, jsSyncMethodSpec* funcs, jsConstIntSpec* consts, int flags)
{
	JSBool ret = JS_TRUE;

	if (props) {
		if (!js_DefineSyncProperties(cx, obj, props))
			ret = JS_FALSE;
	}

	if (funcs) {
		if (!js_DefineSyncMethods(cx, obj, funcs))
			ret = JS_FALSE;
	}

	if (consts) {
		if (!js_DefineConstIntegers(cx, obj, consts, flags))
			ret = JS_FALSE;
	}

	return ret;
}

#else // NON-JSDOCS

JSBool
js_DefineSyncProperties(JSContext *cx, JSObject *obj, jsSyncPropertySpec* props)
{
	uint i;

	/*
	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
	 *       (ie: anything not Synchronet specific).
	 */
	for (i = 0; props[i].name; i++) {
		if (props[i].tinyid < 256 && props[i].tinyid > -129) {
			if (!JS_DefinePropertyWithTinyId(cx, obj,
			                                 props[i].name, props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
				return JS_FALSE;
		}
		else {
			if (!JS_DefineProperty(cx, obj, props[i].name, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
				return JS_FALSE;
		}
	}

	return JS_TRUE;
}


JSBool
js_DefineSyncMethods(JSContext* cx, JSObject* obj, jsSyncMethodSpec *funcs)
{
	uint i;

	/*
	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
	 *       (ie: anything not Synchronet specific).
	 */
	for (i = 0; funcs[i].name; i++)
		if (!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
			return JS_FALSE;
	return JS_TRUE;
}

JSBool
js_SyncResolve(JSContext* cx, JSObject* obj, char *name, jsSyncPropertySpec* props, jsSyncMethodSpec* funcs, jsConstIntSpec* consts, int flags)
{
	uint  i;
	jsval val;

	/*
	 * NOTE: Any changes here should also be added to the same function in jsdoor.c
	 *       (ie: anything not Synchronet specific).
	 */
	if (props) {
		for (i = 0; props[i].name; i++) {
			if (name == NULL || strcmp(name, props[i].name) == 0) {
				if (props[i].tinyid < 256 && props[i].tinyid > -129) {
					if (!JS_DefinePropertyWithTinyId(cx, obj,
					                                 props[i].name, props[i].tinyid, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
						return JS_FALSE;
				}
				else {
					if (!JS_DefineProperty(cx, obj, props[i].name, JSVAL_VOID, NULL, NULL, props[i].flags | JSPROP_SHARED))
						return JS_FALSE;
				}
				if (name)
					return JS_TRUE;
			}
		}
	}
	if (funcs) {
		for (i = 0; funcs[i].name; i++) {
			if (name == NULL || strcmp(name, funcs[i].name) == 0) {
				if (!JS_DefineFunction(cx, obj, funcs[i].name, funcs[i].call, funcs[i].nargs, 0))
					return JS_FALSE;
				if (name)
					return JS_TRUE;
			}
		}
	}
	if (consts) {
		for (i = 0; consts[i].name; i++) {
			if (name == NULL || strcmp(name, consts[i].name) == 0) {
				val = INT_TO_JSVAL(consts[i].val);

				if (!JS_DefineProperty(cx, obj, consts[i].name, val, NULL, NULL, flags))
					return JS_FALSE;

				if (name)
					return JS_TRUE;
			}
		}
	}

	return JS_TRUE;
}

#endif

/* This is a stream-lined version of JS_DefineConstDoubles */
JSBool
js_DefineConstIntegers(JSContext* cx, JSObject* obj, jsConstIntSpec* ints, int flags)
{
	uint  i;
	jsval val;

	for (i = 0; ints[i].name; i++) {
		val = INT_TO_JSVAL(ints[i].val);

		if (!JS_DefineProperty(cx, obj, ints[i].name, val, NULL, NULL, flags))
			return JS_FALSE;
	}

	return JS_TRUE;
}

static JSBool
js_log(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	uintN      i = 0;
	int32      level = LOG_INFO;
	JSString*  str = NULL;
	sbbs_t*    sbbs;
	jsrefcount rc;
	char *     line = NULL;
	size_t     line_sz = 0;

	if (js_argcIsInsufficient(cx, argc, 1))
		return JS_FALSE;

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	if (argc > 1 && JSVAL_IS_NUMBER(argv[i])) {
		if (!JS_ValueToInt32(cx, argv[i++], &level))
			return JS_FALSE;
	}

	for (; i < argc; i++) {
		if ((str = JS_ValueToString(cx, argv[i])) == NULL) {
			FREE_AND_NULL(line);
			return JS_FALSE;
		}
		JSSTRING_TO_RASTRING(cx, str, line, &line_sz, NULL);
		if (line == NULL)
			return JS_FALSE;
		rc = JS_SUSPENDREQUEST(cx);
		sbbs->lputs(level, line);
		JS_RESUMEREQUEST(cx, rc);
	}
	if (line != NULL)
		free(line);

	if (str == NULL)
		JS_SET_RVAL(cx, arglist, JSVAL_VOID);
	else
		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
	return JS_TRUE;
}

static JSBool
js_read(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	uchar*     buf;
	int32      len = 128;
	sbbs_t*    sbbs;
	jsrefcount rc;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	if (argc) {
		if (js_argvIsNullOrVoid(cx, argv, 0))
			return JS_FALSE;
		if (!JS_ValueToInt32(cx, argv[0], &len))
			return JS_FALSE;
	}

	if ((buf = (uchar*)malloc(len)) == NULL)
		return JS_TRUE;

	rc = JS_SUSPENDREQUEST(cx);
	len = RingBufRead(&sbbs->inbuf, buf, len);
	JS_RESUMEREQUEST(cx, rc);

	if (len > 0)
		JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyN(cx, (char*)buf, len)));

	free(buf);
	return JS_TRUE;
}

static JSBool
js_readln(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	char*      buf;
	int32      len = 128;
	sbbs_t*    sbbs;
	jsrefcount rc;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	if (argc) {
		if (js_argvIsNullOrVoid(cx, argv, 0))
			return JS_FALSE;
		if (!JS_ValueToInt32(cx, argv[0], &len))
			return JS_FALSE;
	}

	if (len > 0) {
		if ((buf = (char*)malloc(len)) == NULL)
			return JS_TRUE;

		rc = JS_SUSPENDREQUEST(cx);
		len = sbbs->getstr(buf, len, K_NONE);
		JS_RESUMEREQUEST(cx, rc);

		if (len > 0)
			JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyZ(cx, buf)));

		free(buf);
	}
	return JS_TRUE;
}

static JSBool
js_write(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	uintN      i;
	JSString*  str = NULL;
	sbbs_t*    sbbs;
	jsrefcount rc;
	char *     cstr = NULL;
	size_t     cstr_sz = 0;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	for (i = 0; i < argc; i++) {
		if ((str = JS_ValueToString(cx, argv[i])) == NULL) {
			FREE_AND_NULL(cstr);
			return JS_FALSE;
		}
		JSSTRING_TO_RASTRING(cx, str, cstr, &cstr_sz, NULL);
		if (cstr == NULL)
			return JS_FALSE;
		rc = JS_SUSPENDREQUEST(cx);
		if (sbbs->online != ON_REMOTE)
			sbbs->lputs(LOG_INFO, cstr);
		else
			sbbs->bputs(cstr);
		JS_RESUMEREQUEST(cx, rc);
	}
	FREE_AND_NULL(cstr);

	return JS_TRUE;
}

static JSBool
js_write_raw(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	uintN      i;
	char*      str = NULL;
	size_t     str_sz = 0;
	size_t     len;
	sbbs_t*    sbbs;
	jsrefcount rc;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	for (i = 0; i < argc; i++) {
		JSVALUE_TO_RASTRING(cx, argv[i], str, &str_sz, &len);
		if (str == NULL)
			return JS_FALSE;
		if (len < 1)
			continue;
		rc = JS_SUSPENDREQUEST(cx);
		sbbs->putcom(str, len);
		JS_RESUMEREQUEST(cx, rc);
	}
	if (str != NULL)
		free(str);

	return JS_TRUE;
}

static JSBool
js_writeln(JSContext *cx, uintN argc, jsval *arglist)
{
	sbbs_t*    sbbs;
	jsrefcount rc;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	js_write(cx, argc, arglist);
	rc = JS_SUSPENDREQUEST(cx);
	if (sbbs->online == ON_REMOTE)
		sbbs->bputs(crlf);
	JS_RESUMEREQUEST(cx, rc);

	return JS_TRUE;
}

static JSBool
js_printf(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	char*      p;
	sbbs_t*    sbbs;
	jsrefcount rc;

	if (argc < 1) {
		JS_SET_RVAL(cx, arglist, JS_GetEmptyStringValue(cx));
		return JS_TRUE;
	}

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	if ((p = js_sprintf(cx, 0, argc, argv)) == NULL) {
		JS_ReportError(cx, "js_sprintf failed");
		return JS_FALSE;
	}

	rc = JS_SUSPENDREQUEST(cx);
	if (sbbs->online != ON_REMOTE)
		sbbs->lputs(LOG_INFO, p);
	else
		sbbs->bputs(p);
	JS_RESUMEREQUEST(cx, rc);

	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(JS_NewStringCopyZ(cx, p)));

	js_sprintf_free(p);

	return JS_TRUE;
}

static JSBool
js_alert(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	sbbs_t*    sbbs;
	jsrefcount rc;
	char *     cstr;

	if (js_argcIsInsufficient(cx, argc, 1))
		return JS_FALSE;

	JS_SET_RVAL(cx, arglist, JSVAL_VOID);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
	if (cstr == NULL)
		return JS_FALSE;

	rc = JS_SUSPENDREQUEST(cx);
	if (sbbs->online != ON_REMOTE)
		sbbs->lputs(LOG_WARNING, cstr);
	else {
		sbbs->term->cond_newline();
		sbbs->attr(sbbs->cfg.color[clr_err]);
		sbbs->bputs(cstr);
		sbbs->attr(LIGHTGRAY);
		sbbs->bputs(crlf);
	}
	free(cstr);
	JS_RESUMEREQUEST(cx, rc);

	return JS_TRUE;
}

static JSBool
js_confirm(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	sbbs_t*    sbbs;
	jsrefcount rc;
	char *     cstr;

	if (js_argcIsInsufficient(cx, argc, 1))
		return JS_FALSE;
	if (js_argvIsNullOrVoid(cx, argv, 0))
		return JS_FALSE;

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
	if (cstr == NULL)
		return JS_FALSE;

	rc = JS_SUSPENDREQUEST(cx);
	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->yesno(cstr)));
	free(cstr);
	JS_RESUMEREQUEST(cx, rc);
	return JS_TRUE;
}

static JSBool
js_deny(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	sbbs_t*    sbbs;
	jsrefcount rc;
	char *     cstr;

	if (js_argcIsInsufficient(cx, argc, 1))
		return JS_FALSE;
	if (js_argvIsNullOrVoid(cx, argv, 0))
		return JS_FALSE;

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	JSVALUE_TO_MSTRING(cx, argv[0], cstr, NULL);
	if (cstr == NULL)
		return JS_FALSE;

	rc = JS_SUSPENDREQUEST(cx);
	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->noyes(cstr)));
	free(cstr);
	JS_RESUMEREQUEST(cx, rc);
	return JS_TRUE;
}


static JSBool
js_prompt(JSContext *cx, uintN argc, jsval *arglist)
{
	jsval *    argv = JS_ARGV(cx, arglist);
	char       instr[128] = "";
	JSString * str;
	sbbs_t*    sbbs;
	jsrefcount rc;
	char*      prompt = NULL;
	int32      mode = K_EDIT;
	size_t     result;

	JS_SET_RVAL(cx, arglist, JSVAL_NULL);

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return JS_FALSE;

	uintN argn = 0;
	if (argc > argn && JSVAL_IS_STRING(argv[argn])) {
		JSVALUE_TO_MSTRING(cx, argv[argn], prompt, NULL);
		if (prompt == NULL)
			return JS_FALSE;
		argn++;
	}
	if (argc > argn && JSVAL_IS_STRING(argv[argn])) {
		JSVALUE_TO_STRBUF(cx, argv[argn], instr, sizeof(instr), NULL);
		argn++;
	}
	if (argc > argn && JSVAL_IS_NUMBER(argv[argn])) {
		if (!JS_ValueToInt32(cx, argv[argn], &mode)) {
			free(prompt);
			return JS_FALSE;
		}
		argn++;
	}

	rc = JS_SUSPENDREQUEST(cx);
	if (prompt != NULL) {
		sbbs->bprintf("\1n\1y\1h%s\1w: ", prompt);
		free(prompt);
	}

	result = sbbs->getstr(instr, sizeof(instr) - 1, mode);
	sbbs->attr(LIGHTGRAY);
	if (!result) {
		JS_RESUMEREQUEST(cx, rc);
		return JS_TRUE;
	}
	JS_RESUMEREQUEST(cx, rc);

	if ((str = JS_NewStringCopyZ(cx, instr)) == NULL)
		return JS_FALSE;

	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(str));
	return JS_TRUE;
}

static jsSyncMethodSpec js_global_functions[] = {
	{"log",             js_log,             1,  JSTYPE_STRING,  JSDOCSTR("[<i>number</i> level=LOG_INFO,] value [,value]")
	 , JSDOCSTR("Add a line of text to the server and/or system log.<br>"
		        "<i>values</i> are typically string constants or variables (each logged as a separate log message),<br>"
		        "<i>level</i> is the severity of the message to be logged, one of the globally-defined values, in decreasing severity:<br>"
		        "<tt>LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, and LOG_DEBUG</tt> (default: <tt>LOG_INFO</tt>)")
	 , 311
	},
	{"read",            js_read,            0,  JSTYPE_STRING,  JSDOCSTR("[count=128]")
	 , JSDOCSTR("Read up to <tt>count</tt> characters from input stream and return as a string or <tt>undefined</tt> upon error")
	 , 311
	},
	{"readln",          js_readln,          0,  JSTYPE_STRING,  JSDOCSTR("[count=128]")
	 , JSDOCSTR("Read a single line, up to <tt>count</tt> characters, from input stream and return as a string or <tt>undefined</tt> upon error")
	 , 311
	},
	{"write",           js_write,           0,  JSTYPE_VOID,    JSDOCSTR("value [,value]")
	 , JSDOCSTR("Send one or more values (typically strings) to the output stream")
	 , 311
	},
	{"write_raw",       js_write_raw,           0,  JSTYPE_VOID,    JSDOCSTR("value [,value]")
	 , JSDOCSTR("Send a stream of bytes (possibly containing NUL or special control code sequences) to the output stream")
	 , 314
	},
	{"print",           js_writeln,         0,  JSTYPE_ALIAS },
	{"writeln",         js_writeln,         0,  JSTYPE_VOID,    JSDOCSTR("value [,value]")
	 , JSDOCSTR("Send a line of text to the output stream with automatic line termination (CRLF), "
		        "<i>values</i> are typically string constants or variables (AKA print)")
	 , 311
	},
	{"printf",          js_printf,          1,  JSTYPE_STRING,  JSDOCSTR("<i>string</i> format [,value][,value]")
	 , JSDOCSTR("Send a C-style formatted string of text to the output stream.  See also the <tt>format()</tt> function.")
	 , 310
	},
	{"alert",           js_alert,           1,  JSTYPE_VOID,    JSDOCSTR("value")
	 , JSDOCSTR("Send an alert message (ala client-side JS) to the output stream")
	 , 310
	},
	{"prompt",          js_prompt,          1,  JSTYPE_STRING,  JSDOCSTR("[<i>string</i> text] [,<i>string</i> value] [,<i>number</i> k_mode=K_EDIT]")
	 , JSDOCSTR("Display a prompt (<tt>text</tt>) and return a string of user input (ala client-side JS) or <tt>null</tt> upon no-input<br>"
		        "<tt>value</tt> is an optional default string to be edited (used with the <tt>k_mode K_EDIT</tt> flag)<br>"
		        "See <tt>sbbsdefs.js</tt> for all valid <tt>K_</tt> (keyboard-input) mode flags.")
	 , 310
	},
	{"confirm",         js_confirm,         1,  JSTYPE_BOOLEAN, JSDOCSTR("value")
	 , JSDOCSTR("Display a Yes/No prompt and return <tt>true</tt> or <tt>false</tt> "
		        "based on user's confirmation (ala client-side JS, <tt>true</tt> = yes)<br>"
		        "see also <tt>console.yesno()<tt>")
	 , 310
	},
	{"deny",            js_deny,            1,  JSTYPE_BOOLEAN, JSDOCSTR("value")
	 , JSDOCSTR("Display a No/Yes prompt and returns <tt>true</tt> or <tt>false</tt> "
		        "based on user's denial (<tt>true</tt> = no)<br>"
		        "see also <tt>console.noyes()<tt>")
	 , 31501
	},
	{0}
};

static void
js_ErrorReporter(JSContext *cx, const char *message, JSErrorReport *report)
{
	char        line[64];
	char        file[MAX_PATH + 1];
	sbbs_t*     sbbs;
	const char* warning;
	jsrefcount  rc;
	int         log_level;

	if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL)
		return;

	if (report == NULL) {
		sbbs->lprintf(LOG_ERR, "!JavaScript: %s", message);
		return;
	}

	if (report->filename)
		SAFEPRINTF(file, " %s", report->filename);
	else
		file[0] = 0;

	if (report->lineno)
		SAFEPRINTF(line, " line %d", report->lineno);
	else
		line[0] = 0;

	if (JSREPORT_IS_WARNING(report->flags)) {
		if (JSREPORT_IS_STRICT(report->flags))
			warning = "strict warning ";
		else
			warning = "warning ";
		log_level = LOG_WARNING;
	} else {
		warning = nulstr;
		log_level = LOG_ERR;
	}

	rc = JS_SUSPENDREQUEST(cx);
	sbbs->lprintf(log_level, "!JavaScript %s%s%s: %s", warning, file, line, message);
	if (sbbs->online == ON_REMOTE)
		sbbs->bprintf("!JavaScript %s%s%s: %s\r\n", warning, getfname(file), line, message);
	JS_RESUMEREQUEST(cx, rc);
}

JSContext* sbbs_t::js_init(JSRuntime** runtime, JSObject** glob, const char* desc)
{
	JSContext* js_cx;

	if (startup->js.max_bytes == 0)
		startup->js.max_bytes = JAVASCRIPT_MAX_BYTES;

	lprintf(LOG_DEBUG, "JavaScript: Creating %s runtime: %u bytes"
	        , desc, startup->js.max_bytes);
	if ((*runtime = jsrt_GetNew(startup->js.max_bytes, 1000, __FILE__, __LINE__)) == NULL) {
		lprintf(LOG_NOTICE, "Failed to created new JavaScript runtime");
		return NULL;
	}

	if ((js_cx = JS_NewContext(*runtime, JAVASCRIPT_CONTEXT_STACK)) == NULL) {
		lprintf(LOG_NOTICE, "Failed to create new JavaScript context");
		return NULL;
	}
	JS_SetOptions(js_cx, startup->js.options);
	JS_BEGINREQUEST(js_cx);

	memset(&js_callback, 0, sizeof(js_callback));
	js_callback.limit = startup->js.time_limit;
	js_callback.gc_interval = startup->js.gc_interval;
	js_callback.yield_interval = startup->js.yield_interval;
	js_callback.terminated = &terminated;
	js_callback.auto_terminate = true;
	js_callback.terminate_on_disconnect = true;  // abort terminal scripts when the user disconnects (peer of auto_terminate)
	js_callback.events_supported = true;

	bool success = false;
	bool rooted = false;

	do {

		JS_SetErrorReporter(js_cx, js_ErrorReporter);

		JS_SetContextPrivate(js_cx, this);  /* Store a pointer to sbbs_t instance */

		/* Global Objects (including system, js, client, Socket, MsgBase, File, User, etc. */
		if (!js_CreateCommonObjects(js_cx, &scfg, &cfg, js_global_functions
		                            , uptime, server_host_name(), SOCKLIB_DESC /* system */
		                            , &js_callback              /* js */
		                            , &startup->js
			                        , &useron
		                            , client_socket == INVALID_SOCKET ? NULL : &client, client_socket.load(), -1 /* client */
		                            , &js_server_props          /* server */
		                            , glob
		                            , mqtt
		                            ))
			break;
		rooted = true;

#ifdef BUILD_JSDOCS
		js_CreateUifcObject(js_cx, *glob);
		js_CreateConioObject(js_cx, *glob);
#endif

		/* BBS Object */
		if (js_CreateBbsObject(js_cx, *glob) == NULL)
			break;

		/* Console Object */
		if (js_CreateConsoleObject(js_cx, *glob) == NULL)
			break;

		success = true;

	} while (0);

	if (!success) {
		if (rooted)
			JS_RemoveObjectRoot(js_cx, glob);
		JS_ENDREQUEST(js_cx);
		JS_DestroyContext(js_cx);
		js_cx = NULL;
		return NULL;
	}
	else
		JS_ENDREQUEST(js_cx);

	return js_cx;
}

void sbbs_t::js_cleanup(void)
{
	/* Free Context */
	if (js_cx != NULL) {
		lprintf(LOG_DEBUG, "JavaScript: Destroying context");
		JS_BEGINREQUEST(js_cx);
		JS_RemoveObjectRoot(js_cx, &js_glob);
		JS_ENDREQUEST(js_cx);
		JS_DestroyContext(js_cx);
		js_cx = NULL;
	}

	if (js_runtime != NULL) {
		lprintf(LOG_DEBUG, "JavaScript: Destroying runtime");
		jsrt_Release(js_runtime);
		js_runtime = NULL;
	}

	if (js_hotkey_cx != NULL) {
		lprintf(LOG_DEBUG, "JavaScript: Destroying HotKey context");
		JS_BEGINREQUEST(js_hotkey_cx);
		JS_RemoveObjectRoot(js_hotkey_cx, &js_hotkey_glob);
		JS_ENDREQUEST(js_hotkey_cx);
		JS_DestroyContext(js_hotkey_cx);
		js_hotkey_cx = NULL;
	}

	if (js_hotkey_runtime != NULL) {
		lprintf(LOG_DEBUG, "JavaScript: Destroying HotKey runtime");
		jsrt_Release(js_hotkey_runtime);
		js_hotkey_runtime = NULL;
	}

}

bool sbbs_t::js_create_user_objects(JSContext* cx, JSObject* glob)
{
	bool result = false;
	if (cx != NULL) {
		JS_BEGINREQUEST(cx);
		if (!js_CreateUserObjects(cx, glob, &cfg, &useron, &client, startup == NULL ? NULL :startup->web_file_vpath_prefix, subscan, mqtt))
			errprintf(LOG_ERR, WHERE, "!JavaScript ERROR creating user objects");
		else
			result = true;
		JS_ENDREQUEST(cx);
	}
	return result;
}

bool js_CreateCommonObjects(JSContext* js_cx
                                       , scfg_t* cfg                /* common */
                                       , scfg_t* node_cfg           /* node-specific */
                                       , jsSyncMethodSpec* methods  /* global */
                                       , time_t uptime              /* system */
                                       , const char* host_name      /* system */
                                       , const char* socklib_desc   /* system */
                                       , js_callback_t* cb          /* js */
                                       , js_startup_t* js_startup   /* js */
                                       , user_t* user               /* user */
                                       , client_t* client           /* client */
                                       , SOCKET client_socket       /* client */
                                       , CRYPT_CONTEXT session      /* client */
                                       , js_server_props_t* props   /* server */
                                       , JSObject** glob
                                       , struct mqtt* mqtt
                                       )
{
	bool success = false;

	if (node_cfg == NULL)
		node_cfg = cfg;

	/* Global Object */
	if (!js_CreateGlobalObject(js_cx, node_cfg, methods, js_startup, glob))
		return false;

	do {
		/*
		 * NOTE: Where applicable, anything added here should also be added to
		 *       the same function in jsdoor.c (ie: anything not Synchronet
		 *       specific).
		 */

		/* System Object */
		if (js_CreateSystemObject(js_cx, *glob, node_cfg, uptime, host_name, socklib_desc, mqtt) == NULL)
			break;

		/* Internal JS Object */
		if (cb != NULL
		    && js_CreateInternalJsObject(js_cx, *glob, cb, js_startup) == NULL)
			break;

		/* Client Object */
		if (client != NULL
		    && js_CreateClientObject(js_cx, *glob, "client", client, client_socket, session) == NULL)
			break;

		/* Server */
		if (props != NULL
		    && js_CreateServerObject(js_cx, *glob, props) == NULL)
			break;

		/* Socket Class */
		if (js_CreateSocketClass(js_cx, *glob) == NULL)
			break;

		/* Queue Class */
		if (js_CreateQueueClass(js_cx, *glob) == NULL)
			break;

		/* MsgBase Class */
		if (js_CreateMsgBaseClass(js_cx, *glob) == NULL)
			break;

		/* FileBase Class */
		if (js_CreateFileBaseClass(js_cx, *glob) == NULL)
			break;

		/* File Class */
		if (js_CreateFileClass(js_cx, *glob) == NULL)
			break;

#ifdef USE_SQLITE
		/* SQLite Class */
		if (js_CreateSQLiteClass(js_cx, *glob) == NULL)
			break;
#endif

		/* Archive Class */
		if (js_CreateArchiveClass(js_cx, *glob, cfg->supported_archive_formats) == NULL)
			break;

		/* User class */
		if (js_CreateUserClass(js_cx, *glob) == NULL)
			break;

		/* COM Class */
		if (js_CreateCOMClass(js_cx, *glob) == NULL)
			break;

		/* CryptContext Class */
		if (js_CreateCryptContextClass(js_cx, *glob) == NULL)
			break;

		/* CryptKeyset Class */
		if (js_CreateCryptKeysetClass(js_cx, *glob) == NULL)
			break;

		/* CryptCert Class */
		if (js_CreateCryptCertClass(js_cx, *glob) == NULL)
			break;

		if (js_CreateMQTTClass(js_cx, *glob) == NULL)
			break;
		/* Area Objects */
		if (!js_CreateUserObjects(js_cx, *glob, cfg, user, client, startup == NULL ? NULL :startup->web_file_vpath_prefix, /* subscan: */ NULL, mqtt))
			break;

		success = true;
	} while (0);

	if (!success)
		JS_RemoveObjectRoot(js_cx, glob);

	return success;
}

#endif  /* JAVASCRIPT */

static BYTE* telnet_interpret(sbbs_t* sbbs, BYTE* inbuf, int inlen,
                              BYTE* outbuf, int& outlen)
{
	BYTE* first_iac = NULL;
	BYTE* first_cr = NULL;
	int   i;

	outlen = 0;

	if (inlen < 1) {
		return inbuf;  // no length? No interpretation
	}

	first_iac = (BYTE*)memchr(inbuf, TELNET_IAC, inlen);

	if (!(sbbs->telnet_mode & TELNET_MODE_GATE)
	    && sbbs->telnet_remote_option[TELNET_BINARY_TX] != TELNET_WILL
	    && !(sbbs->console & CON_RAW_IN)) {
		if (sbbs->telnet_last_rxch == CR)
			first_cr = inbuf;
		else
			first_cr = (BYTE*)memchr(inbuf, CR, inlen);
	}

	if (!sbbs->telnet_cmdlen) {
		if (first_iac == NULL && first_cr == NULL) {
			outlen = inlen;
			return inbuf;  // no interpretation needed
		}

		if (first_iac != NULL || first_cr != NULL) {
			if (first_iac != NULL && (first_cr == NULL || first_iac < first_cr))
				outlen = first_iac - inbuf;
			else
				outlen = first_cr - inbuf;
			memcpy(outbuf, inbuf, outlen);
		}
	}

	for (i = outlen; i < inlen; i++) {
		if (!(sbbs->telnet_mode & TELNET_MODE_GATE)
		    && sbbs->telnet_remote_option[TELNET_BINARY_TX] != TELNET_WILL
		    && !(sbbs->console & CON_RAW_IN)) {
			if (sbbs->telnet_last_rxch == CR
			    && (inbuf[i] == LF || inbuf[i] == 0)) { // CR/LF or CR/NUL, ignore 2nd char
#if 0 /* Debug CR/LF problems */
				lprintf(LOG_INFO, "Node %d CR/%02Xh detected and ignored"
				        , sbbs->cfg.node_num, inbuf[i]);
#endif
				sbbs->telnet_last_rxch = inbuf[i];
				continue;
			}
			sbbs->telnet_last_rxch = inbuf[i];
		}

		if (inbuf[i] == TELNET_IAC && sbbs->telnet_cmdlen == 1) { /* escaped 255 */
			sbbs->telnet_cmdlen = 0;
			outbuf[outlen++] = TELNET_IAC;
			continue;
		}
		if (inbuf[i] == TELNET_IAC || sbbs->telnet_cmdlen) {

			if (sbbs->telnet_cmdlen < sizeof(sbbs->telnet_cmd))
				sbbs->telnet_cmd[sbbs->telnet_cmdlen++] = inbuf[i];
			else {
				lprintf(LOG_WARNING, "Node %d telnet command (%d, %d) buffer limit reached (%u bytes)"
				        , sbbs->cfg.node_num, sbbs->telnet_cmd[1], sbbs->telnet_cmd[2], sbbs->telnet_cmdlen);
				sbbs->telnet_cmdlen = 0;
			}

			uchar command   = sbbs->telnet_cmd[1];
			uchar option    = sbbs->telnet_cmd[2];

			if (sbbs->telnet_cmdlen == 2 && command == TELNET_SE) {
				lprintf(LOG_WARNING, "Node %d unexpected telnet sub-negotiation END command"
				        , sbbs->cfg.node_num);
				sbbs->telnet_cmdlen = 0;
			}
			else if (sbbs->telnet_cmdlen >= 2 && command == TELNET_SB) {
				if (inbuf[i] == TELNET_SE
				    && sbbs->telnet_cmd[sbbs->telnet_cmdlen - 2] == TELNET_IAC) {
					sbbs->telnet_cmds_received++;
					if (startup->options & BBS_OPT_DEBUG_TELNET)
						lprintf(LOG_DEBUG, "Node %d %s telnet sub-negotiation command: %s (%u bytes)"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , telnet_opt_desc(option)
						        , sbbs->telnet_cmdlen);

					/* sub-option terminated */
					if (option == TELNET_TERM_TYPE
					    && sbbs->telnet_cmd[3] == TELNET_TERM_IS) {
						safe_snprintf(sbbs->telnet_terminal, sizeof(sbbs->telnet_terminal), "%.*s"
						              , (int)sbbs->telnet_cmdlen - 6, sbbs->telnet_cmd + 4);
						lprintf(LOG_DEBUG, "Node %d %s telnet terminal type: %s"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , sbbs->telnet_terminal);

					} else if (option == TELNET_TERM_SPEED
					           && sbbs->telnet_cmd[3] == TELNET_TERM_IS) {
						char speed[128];
						safe_snprintf(speed, sizeof(speed), "%.*s", (int)sbbs->telnet_cmdlen - 6, sbbs->telnet_cmd + 4);
						lprintf(LOG_DEBUG, "Node %d %s telnet terminal speed: %s"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , speed);
						sbbs->telnet_speed = atoi(speed);
#ifdef SBBS_TELNET_ENVIRON_SUPPORT
					} else if (option == TELNET_NEW_ENVIRON
					           && sbbs->telnet_cmd[3] == TELNET_ENVIRON_IS) {
						BYTE* p;
						BYTE* end = sbbs->telnet_cmd + (sbbs->telnet_cmdlen - 2);
						for (p = sbbs->telnet_cmd + 4; p < end; ) {
							if (*p == TELNET_ENVIRON_VAR || *p == TELNET_ENVIRON_USERVAR) {
								BYTE  type = *p++;
								char* name = (char*)p;
								/* RFC 1572: The characters following a "type" up to the next "type" or VALUE specify the variable name. */
								while (p < end) {
									if (*p == TELNET_ENVIRON_VAR || *p == TELNET_ENVIRON_USERVAR || *p == TELNET_ENVIRON_VALUE)
										break;
									p++;
								}
								if (p < end) {
									char* value = (char*)p + 1;
									*(p++) = 0;
									while (p < end) {
										if (*p == TELNET_ENVIRON_VAR || *p == TELNET_ENVIRON_USERVAR || *p == TELNET_ENVIRON_VALUE)
											break;
										p++;
									}
									*p = 0;
									lprintf(LOG_DEBUG, "Node %d telnet %s %s environment variable '%s' = '%s'"
									        , sbbs->cfg.node_num
									        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
									        , type == TELNET_ENVIRON_VAR ? "well-known" : "user-defined"
									        , name
									        , value);
									if (strcmp(name, "USER") == 0) {
										SAFECOPY(sbbs->rlogin_name, value);
									}
								}
							} else
								p++;
						}
#endif
					} else if (option == TELNET_SEND_LOCATION) {
						safe_snprintf(sbbs->telnet_location
						              , sizeof(sbbs->telnet_location)
						              , "%.*s", (int)sbbs->telnet_cmdlen - 5, sbbs->telnet_cmd + 3);
						lprintf(LOG_DEBUG, "Node %d %s telnet location: %s"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , sbbs->telnet_location);
					} else if (option == TELNET_TERM_LOCATION_NUMBER && sbbs->telnet_cmd[3] == 0) {
						SAFEPRINTF4(sbbs->telnet_location, "%u.%u.%u.%u"
						            , sbbs->telnet_cmd[4]
						            , sbbs->telnet_cmd[5]
						            , sbbs->telnet_cmd[6]
						            , sbbs->telnet_cmd[7]
						            );
						lprintf(LOG_DEBUG, "Node %d %s telnet location number (IP address): %s"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , sbbs->telnet_location);
					} else if (option == TELNET_NEGOTIATE_WINDOW_SIZE) {
						int cols = (sbbs->telnet_cmd[3] << 8) | sbbs->telnet_cmd[4];
						int rows = (sbbs->telnet_cmd[5] << 8) | sbbs->telnet_cmd[6];
						lprintf(LOG_DEBUG, "Node %d %s telnet window size: %dx%d"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , cols
						        , rows);
						sbbs->telnet_cols = cols;
						sbbs->telnet_rows = rows;
					} else if (startup->options & BBS_OPT_DEBUG_TELNET)
						lprintf(LOG_DEBUG, "Node %d %s unsupported telnet sub-negotiation cmd: %s, 0x%02X"
						        , sbbs->cfg.node_num
						        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
						        , telnet_opt_desc(option)
						        , sbbs->telnet_cmd[3]);
					sbbs->telnet_cmdlen = 0;
				}
			} // Sub-negotiation command
			else if (sbbs->telnet_cmdlen == 2 && inbuf[i] < TELNET_WILL) {
				sbbs->telnet_cmds_received++;
				if (startup->options & BBS_OPT_DEBUG_TELNET)
					lprintf(LOG_DEBUG, "Node %d %s telnet cmd: %s"
					        , sbbs->cfg.node_num
					        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
					        , telnet_cmd_desc(option));
				sbbs->telnet_cmdlen = 0;
			}
			else if (sbbs->telnet_cmdlen >= 3) {   /* telnet option negotiation */
				sbbs->telnet_cmds_received++;
				if (startup->options & BBS_OPT_DEBUG_TELNET)
					lprintf(LOG_DEBUG, "Node %d %s telnet cmd: %s %s"
					        , sbbs->cfg.node_num
					        , sbbs->telnet_mode & TELNET_MODE_GATE ? "passed-through" : "received"
					        , telnet_cmd_desc(command)
					        , telnet_opt_desc(option));

				if (!(sbbs->telnet_mode & TELNET_MODE_GATE)) {
					if (command == TELNET_DO || command == TELNET_DONT) {    /* local options */
						if (sbbs->telnet_local_option[option] == command)
							SetEvent(sbbs->telnet_ack_event);
						else {
							sbbs->telnet_local_option[option] = command;
							sbbs->send_telnet_cmd(telnet_opt_ack(command), option);
						}
					} else { /* WILL/WONT (remote options) */
						if (sbbs->telnet_remote_option[option] == command)
							SetEvent(sbbs->telnet_ack_event);
						else {
							switch (option) {
								case TELNET_BINARY_TX:
								case TELNET_ECHO:
								case TELNET_TERM_TYPE:
								case TELNET_TERM_SPEED:
								case TELNET_SUP_GA:
								case TELNET_NEGOTIATE_WINDOW_SIZE:
								case TELNET_SEND_LOCATION:
								case TELNET_TERM_LOCATION_NUMBER:
#ifdef SBBS_TELNET_ENVIRON_SUPPORT
								case TELNET_NEW_ENVIRON:
#endif
									sbbs->telnet_remote_option[option] = command;
									sbbs->send_telnet_cmd(telnet_opt_ack(command), option);
									break;
								default: /* unsupported remote options */
									if (command == TELNET_WILL) /* NAK */
										sbbs->send_telnet_cmd(telnet_opt_nak(command), option);
									break;
							}
						}

						if (command == TELNET_WILL && option == TELNET_TERM_TYPE) {
							if (startup->options & BBS_OPT_DEBUG_TELNET)
								lprintf(LOG_DEBUG, "Node %d requesting telnet terminal type"
								        , sbbs->cfg.node_num);

							char buf[64];
							snprintf(buf, sizeof buf, "%c%c%c%c%c%c"
							         , TELNET_IAC, TELNET_SB
							         , TELNET_TERM_TYPE, TELNET_TERM_SEND
							         , TELNET_IAC, TELNET_SE);
							sbbs->putcom(buf, 6);
						}
						else if (command == TELNET_WILL && option == TELNET_TERM_SPEED) {
							if (startup->options & BBS_OPT_DEBUG_TELNET)
								lprintf(LOG_DEBUG, "Node %d requesting telnet terminal speed"
								        , sbbs->cfg.node_num);

							char buf[64];
							snprintf(buf, sizeof buf, "%c%c%c%c%c%c"
							         , TELNET_IAC, TELNET_SB
							         , TELNET_TERM_SPEED, TELNET_TERM_SEND
							         , TELNET_IAC, TELNET_SE);
							sbbs->putcom(buf, 6);
						}
#ifdef SBBS_TELNET_ENVIRON_SUPPORT
						else if (command == TELNET_WILL && option == TELNET_NEW_ENVIRON) {
							if (startup->options & BBS_OPT_DEBUG_TELNET)
								lprintf(LOG_DEBUG, "Node %d requesting USER environment variable value"
								        , sbbs->cfg.node_num);

							char buf[64];
							int  len = snprintf(buf, sizeof buf, "%c%c%c%c%c%c"
							                    , TELNET_IAC, TELNET_SB
							                    , TELNET_NEW_ENVIRON, TELNET_ENVIRON_SEND //,TELNET_ENVIRON_VAR
							                    , TELNET_IAC, TELNET_SE);
							sbbs->putcom(buf, len);
						}
#endif
					}
				}
				sbbs->telnet_cmdlen = 0;
			}
			if (sbbs->telnet_mode & TELNET_MODE_GATE)  // Pass-through commands
				outbuf[outlen++] = inbuf[i];
		} else
			outbuf[outlen++] = inbuf[i];
	}
	return outbuf;
}

void sbbs_t::send_telnet_cmd(uchar cmd, uchar opt)
{
	char buf[16];
	int  sz;
	int  result;

	if (telnet_mode & TELNET_MODE_OFF)
		return;

	if (cmd < TELNET_WILL) {
		if (startup->options & BBS_OPT_DEBUG_TELNET)
			lprintf(LOG_DEBUG, "sending telnet cmd: %s"
			        , telnet_cmd_desc(cmd));
		snprintf(buf, sizeof buf, "%c%c", TELNET_IAC, cmd);
		result = sendsocket(client_socket, buf, sz = 2);
	} else {
		if (startup->options & BBS_OPT_DEBUG_TELNET)
			lprintf(LOG_DEBUG, "sending telnet cmd: %s %s"
			        , telnet_cmd_desc(cmd)
			        , telnet_opt_desc(opt));
		snprintf(buf, sizeof buf, "%c%c%c", TELNET_IAC, cmd, opt);
		result = sendsocket(client_socket, buf, sz = 3);
	}
	if (result != sz)
		lprintf(LOG_DEBUG, "ERROR sending telnet command (%s): send returned %d instead of %d"
		        , telnet_cmd_desc(cmd)
		        , result
		        , sz);
}

bool sbbs_t::request_telnet_opt(uchar cmd, uchar opt, unsigned waitforack)
{
	if (telnet_mode & TELNET_MODE_OFF)
		return false;

	switch (cmd) {
		case TELNET_DO: /* remote option */
		case TELNET_DONT:
			if (telnet_remote_option[opt] == telnet_opt_ack(cmd))
				return true;    /* already set in this mode, do nothing */
			telnet_remote_option[opt] = telnet_opt_ack(cmd);
			break;
		case TELNET_WILL: /* local option */
		case TELNET_WONT:
			if (telnet_local_option[opt] == telnet_opt_ack(cmd))
				return true;    /* already set in this mode, do nothing */
			telnet_local_option[opt] = telnet_opt_ack(cmd);
			break;
		default:
			return false;
	}
	if (waitforack)
		ResetEvent(telnet_ack_event);
	send_telnet_cmd(cmd, opt);
	if (waitforack)
		return WaitForEvent(telnet_ack_event, waitforack) == WAIT_OBJECT_0;
	return true;
}

static bool channel_open(sbbs_t *sbbs, int channel)
{
	int rval;

	if (cryptStatusError(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))
		return false;
	if (cryptStatusError(cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_OPEN, &rval)))
		return false;
	return rval;
}

static int crypt_pop_channel_data(sbbs_t *sbbs, char *inbuf, int want, int *got)
{
	int   status;
	int   cid;
	char *cname = nullptr;
	char *ssname = nullptr;
	int   ret;
	int   closing_channel = -1;
	int   tgot;

	*got = 0;
	while (sbbs->online && sbbs->client_socket != INVALID_SOCKET
	       && node_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET) {
		ret = cryptPopData(sbbs->ssh_session, inbuf, want, &tgot);
		if (ret == CRYPT_OK) {
			status = cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, &cid);
			if (status == CRYPT_OK) {
				if (cid == closing_channel)
					continue;
				if (cid == sbbs->sftp_channel) {
					pthread_mutex_unlock(&sbbs->ssh_mutex);
					SFTPS_OUTCOME_DECL(out, 256);
					if (!sftps_recv(sbbs->sftp_state, reinterpret_cast<uint8_t *>(inbuf), tgot, out)) {
						lprintf(LOG_NOTICE, "sftp recv: %s", out->estr);
						sbbs->sftp_end();
					}
					pthread_mutex_lock(&sbbs->ssh_mutex);
				}
				else if (cid == sbbs->session_channel) {
					*got = tgot;
				}
				else {
					if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, cid))) {
						sbbs->log_crypt_error_status_sock(status, "setting channel");
						return status;
					}
					cname = get_crypt_attribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TYPE);
					if (cname && strcmp(cname, "subsystem") == 0) {
						ssname = get_crypt_attribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ARG1);
					}
					if (((startup->options & (BBS_OPT_ALLOW_SFTP | BBS_OPT_SSH_ANYAUTH)) == BBS_OPT_ALLOW_SFTP) && ssname && cname && sbbs->sftp_channel == -1 && strcmp(ssname, "sftp") == 0) {
						if (sbbs->init_sftp(cid)) {
							if (tgot > 0) {
								pthread_mutex_unlock(&sbbs->ssh_mutex);
								SFTPS_OUTCOME_DECL(out2, 256);
								if (!sftps_recv(sbbs->sftp_state, reinterpret_cast<uint8_t *>(inbuf), tgot, out2)) {
									lprintf(LOG_NOTICE, "sftp recv: %s", out2->estr);
									sbbs->sftp_end();
								}
								pthread_mutex_lock(&sbbs->ssh_mutex);
							}
							sbbs->sftp_channel = cid;
						}
					}
					if (cname && sbbs->session_channel == -1 && strcmp(cname, "shell") == 0) {
						sbbs->session_channel = cid;
					}

					if (cid != sbbs->sftp_channel && cid != sbbs->session_channel) {
						if (cname == nullptr || strcmp(cname, "session") != 0) {
							lprintf(LOG_WARNING, "Node %d SSH WARNING: attempt to use channel '%s' (%d != %d or %d)"
							        , sbbs->cfg.node_num, cname ? cname : "<unknown>", cid, sbbs->session_channel, sbbs->sftp_channel);
							if (cname) {
								free_crypt_attrstr(cname);
								cname = nullptr;
							}
							if (ssname) {
								free_crypt_attrstr(ssname);
								ssname = nullptr;
							}
							closing_channel = cid;
							if (cryptStatusError(status = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0))) {
								sbbs->log_crypt_error_status_sock(status, "closing channel");
								return status;
							}
						}
					}
					free_crypt_attrstr(cname);
					cname = nullptr;
					free_crypt_attrstr(ssname);
					ssname = nullptr;

					continue;
				}
				if (cid != -1) {
					if (cryptStatusOK(cryptGetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_OPEN, &ret))) {
						if (!ret) {
							closing_channel = -1;
							ret = CRYPT_ERROR_NOTFOUND;
						}
					}
				}
			}
			else {
				/*
				 * CRYPT_ERROR_NOTFOUND indicates this is the last data on the channel (whatever it was)
				 * and it was destroyed, so it's no longer possible to get the channel id.
				 */
				bool closed {false};
				if (status != CRYPT_ERROR_NOTFOUND)
					sbbs->log_crypt_error_status_sock(status, "getting channel id");
				closing_channel = -1;
				if (sbbs->sftp_channel != -1) {
					if (!channel_open(sbbs, sbbs->sftp_channel)) {
						if (cryptStatusOK(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->sftp_channel)))
							(void)cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0);
						sbbs->sftp_channel = -1;
						closed = true;
					}
				}
				if (sbbs->session_channel != -1) {
					if (!channel_open(sbbs, sbbs->session_channel)) {
						if (cryptStatusOK(cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))
							(void)cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_ACTIVE, 0);
						sbbs->session_channel = -1;
						closed = true;
					}
				}
				// All channels are now closed.
				if (closed && sbbs->sftp_channel == -1 && sbbs->session_channel == -1)
					return CRYPT_ERROR_COMPLETE;
			}
		}
		if (ret == CRYPT_ENVELOPE_RESOURCE)
			return CRYPT_ERROR_TIMEOUT;
		/* sftp_state->mtx is acquired+released entirely inside sftps_recv;
		 * crypt_pop_channel_data never holds it across return. The caller
		 * owns ssh_mutex and re-takes it after each sftps_recv call. */
		// coverity[LOCK:SUPPRESS]
		return ret;
	}
	return CRYPT_ERROR_TIMEOUT;
}

void input_thread(void *arg)
{
	BYTE          inbuf[4000];
	BYTE          telbuf[sizeof(inbuf)];
	BYTE *        wrbuf;
	int           i, wr, avail;
	uint64_t      total_recv = 0;
	uint          total_pkts = 0;
	sbbs_t*       sbbs = (sbbs_t*) arg;
	SOCKET        sock;
#ifdef USE_CRYPTLIB
	// cryptPopData() returns after one SSH packet even if more plaintext is buffered.
	bool          drain_ssh = false;
#endif
#ifdef PREFER_POLL
	struct pollfd fds[2];
	int           nfds;
#endif

	SetThreadName("sbbs/termInput");
	thread_up(true /* setuid */);

#ifdef _DEBUG
	lprintf(LOG_DEBUG, "Node %d input thread started", sbbs->cfg.node_num);
#endif

	while (sbbs->online && sbbs->client_socket != INVALID_SOCKET
	       && node_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET) {

		if (sbbs->max_socket_inactivity && !(sbbs->console & CON_NO_INACT)) {
			if (sbbs->cfg.inactivity_warn
			    && sbbs->socket_inactive >= sbbs->max_socket_inactivity * (sbbs->cfg.inactivity_warn / 100.0)
			    && !sbbs->socket_inactivity_warning_sent) {
				sbbs->bputs(text[InactivityAlert]);
				sbbs->socket_inactivity_warning_sent = true;
			}
			if (sbbs->socket_inactive > sbbs->max_socket_inactivity) {
				lprintf(LOG_NOTICE, "Node %d maximum socket inactivity exceeded: %u seconds"
				        , sbbs->cfg.node_num, sbbs->max_socket_inactivity.load());
				sbbs->bputs(text[CallBackWhenYoureThere]);
				break;
			}
		}

#ifdef _WIN32   // No spy sockets
#ifdef USE_CRYPTLIB
		if (!drain_ssh)
#endif
		{
			if (!socket_readable(sbbs->client_socket, 1000)) {
				++sbbs->socket_inactive;
				continue;
			}
		}
#else
#ifdef PREFER_POLL
#ifdef USE_CRYPTLIB
		if (!drain_ssh)
#endif
		{
			fds[0].fd = sbbs->client_socket;
			fds[0].events = POLLIN;
			nfds = 1;
			if (uspy_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET) {
				fds[1].fd = uspy_socket[sbbs->cfg.node_num - 1];
				fds[1].events = POLLIN;
				nfds++;
			}

			if (poll(fds, nfds, 1000) < 1) {
				++sbbs->socket_inactive;
				continue;
			}
		}
#else
#error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
#endif
#endif

		if (sbbs->client_socket == INVALID_SOCKET)
			break;

		sbbs->socket_inactive = 0;
		sbbs->socket_inactivity_warning_sent = false;

/*         ^          ^
 *      \______    ______/
 *       \  * \   / *   /
 *        -----   ------           /----\
 *              ||               -< Boo! |
 *             /__\                \----/
 *       \______________/
 *        \/\/\/\/\/\/\/
 *         ------------
 */

		// Remember which socket was selected here: don't re-compare 'sock'
		// against the atomic client_socket later, as another thread (e.g.
		// sbbs_t::hangup) may have stored INVALID_SOCKET by then, misrouting
		// an ordinary client disconnect into spy-socket handling (issue #1184).
		bool spy_sock = false;
#ifdef USE_CRYPTLIB
		if (drain_ssh)
			sock = sbbs->client_socket;
		else
#endif
		{
#ifdef _WIN32   // No spy sockets
			sock = sbbs->client_socket;
#else
#ifdef PREFER_POLL
			if (fds[0].revents & POLLIN)
				sock = sbbs->client_socket;
			else if (uspy_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET && fds[1].revents & POLLIN) {
				if (socket_recvdone(uspy_socket[sbbs->cfg.node_num - 1], 0)) {
					close_socket(uspy_socket[sbbs->cfg.node_num - 1]);
					lprintf(LOG_NOTICE, "Node %d Closing local spy socket: %d", sbbs->cfg.node_num, uspy_socket[sbbs->cfg.node_num - 1]);
					uspy_socket[sbbs->cfg.node_num - 1] = INVALID_SOCKET;
					continue;
				}
				sock = uspy_socket[sbbs->cfg.node_num - 1];
				spy_sock = true;
			}
			else {
				continue;
			}
#else
#error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3
#endif
#endif
		}
		avail = RingBufFree(&sbbs->inbuf);

		if (avail < 1) { // input buffer full
			lprintf(LOG_WARNING, "Node %d !WARNING input buffer full after receiving %" PRIu64 " bytes ", sbbs->cfg.node_num, total_recv);
			// wait up to 5 seconds to empty (1 byte min)
			time_t start = time(NULL);
			while ((avail = RingBufFree(&sbbs->inbuf)) < 1 && time(NULL) - start < 5) {
				YIELD();
			}
			if (avail < 1)   /* input buffer still full */
				continue;
		}

		if (avail > (int)sizeof inbuf)
			avail = sizeof inbuf;

		if (pthread_mutex_lock(&sbbs->input_thread_mutex) != 0)
			sbbs->errormsg(WHERE, ERR_LOCK, "input_thread_mutex", 0);

		int rd; // number of bytes read
#ifdef USE_CRYPTLIB
		if (sbbs->ssh_mode && !spy_sock) {
			int err;
			drain_ssh = false;
			if (WaitForEvent(sbbs->ssh_active, 1000) == WAIT_TIMEOUT) {
				pthread_mutex_unlock(&sbbs->input_thread_mutex);
				continue;
			}
			pthread_mutex_lock(&sbbs->ssh_mutex);
			if (cryptStatusError((err = crypt_pop_channel_data(sbbs, (char*)inbuf, avail, &i)))) {
				pthread_mutex_unlock(&sbbs->ssh_mutex);
				if (pthread_mutex_unlock(&sbbs->input_thread_mutex) != 0)
					sbbs->errormsg(WHERE, ERR_UNLOCK, "input_thread_mutex", 0);
				if (err == CRYPT_ERROR_COMPLETE) {
					lprintf(LOG_WARNING, "Node %d SSH Operation complete", sbbs->cfg.node_num);
					break;
				}
				if (err == CRYPT_ERROR_TIMEOUT) {
					lprintf(LOG_NOTICE, "Node %d SSH Timeout", sbbs->cfg.node_num);
					continue;
				}
				/* hangup() destroys the session between this thread's ssh_mode
				 * test and its pop, so an error here is a teardown artifact (#1225) */
				if (!sbbs->ssh_mode || !sbbs->online) {
					lprintf(LOG_DEBUG, "Node %d SSH session closed while popping data (%d)"
					    , sbbs->cfg.node_num, err);
					break;
				}
				/* Handle the SSH error here... */
				GCES(err, sbbs->cfg.node_num, sbbs->ssh_session, "popping data");
				break;
			}
			else {
				pthread_mutex_unlock(&sbbs->ssh_mutex);
				if (i < 1) {
					if (pthread_mutex_unlock(&sbbs->input_thread_mutex) != 0)
						sbbs->errormsg(WHERE, ERR_UNLOCK, "input_thread_mutex", 0);
					continue;
				}
				drain_ssh = true;
				rd = i;
			}
		}
		else
#endif
			rd = recv(sock, (char*)inbuf, avail, 0);

		if (pthread_mutex_unlock(&sbbs->input_thread_mutex) != 0)
			sbbs->errormsg(WHERE, ERR_UNLOCK, "input_thread_mutex", 0);

		if (rd == 0 && !socket_recvdone(sock, 0))
			continue;

		if (rd == SOCKET_ERROR)
		{
#ifdef __unix__
			if (!spy_sock)  {
#endif
			if (!sbbs->online)      // sbbs_t::hangup() called?
				break;
			if (SOCKET_ERRNO == EAGAIN)
				continue;
			if (SOCKET_ERRNO == ENOTSOCK)
				lprintf(LOG_NOTICE, "Node %d socket closed by peer on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ECONNRESET)
				lprintf(LOG_NOTICE, "Node %d connection reset by peer on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ESHUTDOWN)
				lprintf(LOG_NOTICE, "Node %d socket shutdown on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ECONNABORTED)
				lprintf(LOG_NOTICE, "Node %d connection aborted by peer on receive", sbbs->cfg.node_num);
			else
				lprintf(LOG_WARNING, "Node %d !ERROR %d receiving from socket %d"
				        , sbbs->cfg.node_num, SOCKET_ERRNO, sock);
			break;
#ifdef __unix__
		} else  {
			if (SOCKET_ERRNO != EAGAIN)  {
				errprintf(LOG_ERR, WHERE, "Node %d !ERROR %d (%s) on local spy socket %d receive"
				          , sbbs->cfg.node_num, errno, strerror(errno), sock);
				close_socket(uspy_socket[sbbs->cfg.node_num - 1]);
				uspy_socket[sbbs->cfg.node_num - 1] = INVALID_SOCKET;
			}
			continue;
		}
#endif
		}

		if (rd == 0 && !spy_sock)
		{
			lprintf(LOG_NOTICE, "Node %d disconnected", sbbs->cfg.node_num);
			break;
		}

		total_recv += rd;
		total_pkts++;

		// telbuf and wr are modified to reflect telnet escaped data
		wr = rd;
#ifdef __unix__
		if (spy_sock)
			wrbuf = inbuf;
		else
#endif
		if (sbbs->telnet_mode & TELNET_MODE_OFF)
			wrbuf = inbuf;
		else
			wrbuf = telnet_interpret(sbbs, inbuf, rd, telbuf, wr);
		if (wr > (int)sizeof(telbuf)) {
			errprintf(LOG_ERR, WHERE, "Node %d !TELBUF OVERFLOW (%d>%d)", sbbs->cfg.node_num, wr, (int)sizeof(telbuf));
			wr = sizeof(telbuf);
		}

		if (!(sbbs->console & CON_RAW_IN))
			sbbs->translate_input(wrbuf, wr);

		if (sbbs->passthru_socket_active == true) {
			bool writable = false;
			if (socket_check(sbbs->passthru_socket, NULL, &writable, 1000) && writable) {
				if (sendsocket(sbbs->passthru_socket, (char*)wrbuf, wr) != wr)
					errprintf(LOG_ERR, WHERE, "Node %d ERROR %d writing to passthru socket"
					          , sbbs->cfg.node_num, SOCKET_ERRNO);
			} else
				lprintf(LOG_WARNING, "Node %d could not write to passthru socket (writable=%d)"
				        , sbbs->cfg.node_num, (int)writable);
			continue;
		}

		/* First level Ctrl-C checking */
		if (!(sbbs->cfg.ctrlkey_passthru & (1 << CTRL_C))
		    && sbbs->rio_abortable
		    && !(sbbs->telnet_mode & TELNET_MODE_GATE)
		    && !(sbbs->sys_status & SS_FILEXFER)
		    && memchr(wrbuf, CTRL_C, wr)) {
			if (RingBufFull(&sbbs->inbuf))
				lprintf(LOG_DEBUG, "Node %d Ctrl-C hit with %u bytes in input buffer"
				        , sbbs->cfg.node_num, RingBufFull(&sbbs->inbuf));
			if (RingBufFull(&sbbs->outbuf))
				lprintf(LOG_DEBUG, "Node %d Ctrl-C hit with %u bytes in output buffer"
				        , sbbs->cfg.node_num, RingBufFull(&sbbs->outbuf));
			sbbs->sys_status |= SS_ABORT;
			RingBufReInit(&sbbs->inbuf);    /* Purge input buffer */
			RingBufReInit(&sbbs->outbuf);   /* Purge output buffer */
			SetEvent(sbbs->inbuf.data_event);
			SetEvent(sbbs->inbuf.highwater_event);
			continue;   // Ignore the entire buffer
		}

		avail = RingBufFree(&sbbs->inbuf);

		if (avail < wr)
			errprintf(LOG_ERR, WHERE, "Node %d !INPUT BUFFER FULL (%d free, %d bytes discarded)", sbbs->cfg.node_num, avail, wr);
		else
			RingBufWrite(&sbbs->inbuf, wrbuf, wr);
//		if(wr>100)
//			mswait(500);	// Throttle sender
	}
	sbbs->sftp_end();
	sbbs->online = false;
	sbbs->sys_status |= SS_ABORT; /* as though Ctrl-C were hit */
	SetEvent(sbbs->inbuf.data_event); // terminate incom() wait

	sbbs->input_thread_running = false;
	sbbs->terminate_output_thread = true;
	if (node_socket[sbbs->cfg.node_num - 1] == INVALID_SOCKET)   // Shutdown locally
		sbbs->terminated = true;    // Signal JS to stop execution

	lprintf(LOG_DEBUG, "Node %d input thread terminated (received %" PRIu64 " bytes in %u blocks)"
	        , sbbs->cfg.node_num, total_recv, total_pkts);
	thread_down();
}

// Flush the duplicate client_socket when activating the passthru socket
// to eliminate any stale data from the previous passthru session
void sbbs_t::passthru_socket_activate(bool activate)
{
	time_t timeout = time(NULL) + 60;

	if (activate) {
		bool rd = false;
		while (socket_check(client_socket_dup, &rd, /* wr_p */ NULL, /* timeout */ 0) && rd && time(NULL) < timeout) {
			char ch;
			if (recv(client_socket_dup, &ch, sizeof(ch), /* flags: */ 0) != sizeof(ch))
				break;
		}
	} else {
		/* Re-enable blocking (in case disabled by external program) */
		u_long l = 0;
		ioctlsocket(client_socket_dup, FIONBIO, &l);

		/* Re-set socket options */
		char err[512];
		if (set_socket_options(&cfg, client_socket_dup, "passthru", err, sizeof(err)))
			errprintf(LOG_ERR, WHERE, "%04d !ERROR %s setting passthru socket options", client_socket.load(), err);

		do { // Allow time for the passthru_thread to move any pending socket data to the outbuf
			SLEEP(100); // Before the node_thread starts sending its own data to the outbuf
		} while (RingBufFull(&outbuf) && time(NULL) < timeout);
	}
	passthru_socket_active = activate;
}

/*
 * This thread simply copies anything it manages to read from the
 * passthru_socket into the output ringbuffer.
 */
void passthru_thread(void* arg)
{
	sbbs_t  *sbbs = (sbbs_t*) arg;
	int rd;
	char inbuf[IO_THREAD_BUF_SIZE / 2];

	SetThreadName("sbbs/passthru");
	thread_up(false /* setuid */);
	lprintf(LOG_DEBUG, "Node %d passthru thread started", sbbs->cfg.node_num);

	while (sbbs->online && sbbs->passthru_socket != INVALID_SOCKET && !terminate_server) {
		if (!socket_readable(sbbs->passthru_socket, 1000))
			continue;

		if (sbbs->passthru_socket == INVALID_SOCKET)
			break;

		rd = RingBufFree(&sbbs->outbuf) / 2;
		if (rd > (int)sizeof(inbuf))
			rd = sizeof(inbuf);

		rd = recv(sbbs->passthru_socket, inbuf, rd, 0);

		if (rd == SOCKET_ERROR)
		{
			if (SOCKET_ERRNO == ENOTSOCK)
				lprintf(LOG_NOTICE, "Node %d passthru socket closed by peer on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ECONNRESET)
				lprintf(LOG_NOTICE, "Node %d passthru connection reset by peer on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ESHUTDOWN)
				lprintf(LOG_NOTICE, "Node %d passthru socket shutdown on receive", sbbs->cfg.node_num);
			else if (SOCKET_ERRNO == ECONNABORTED)
				lprintf(LOG_NOTICE, "Node %d passthru connection aborted by peer on receive", sbbs->cfg.node_num);
			else
				lprintf(LOG_WARNING, "Node %d !ERROR %d receiving from passthru socket %d"
				        , sbbs->cfg.node_num, SOCKET_ERRNO, sbbs->passthru_socket.load());
			break;
		}

		if (rd == 0)
		{
			char ch;
			if (recv(sbbs->passthru_socket, &ch, sizeof(ch), MSG_PEEK) == 0) {
				lprintf(sbbs->online ? LOG_WARNING : LOG_DEBUG
				        , "Node %d passthru socket disconnected", sbbs->cfg.node_num);
				break;
			}
			YIELD();
			continue;
		}

		if (sbbs->xtrn_mode & EX_BIN) {
			BYTE telnet_buf[sizeof(inbuf) * 2];
			BYTE*   bp = (BYTE*)inbuf;

			if (!(sbbs->telnet_mode & TELNET_MODE_OFF))
				rd = telnet_expand((BYTE*)inbuf, rd, telnet_buf, sizeof(telnet_buf), /* expand_cr */ false, &bp);

			int wr = RingBufWrite(&sbbs->outbuf, bp, rd);
			if (wr != rd) {
				errprintf(LOG_ERR, WHERE, "Node %d Short-write (%d of %d bytes) from passthru socket to outbuf"
				          , sbbs->cfg.node_num, wr, rd);
				break;
			}
		} else {
			sbbs->rputs(inbuf, rd);
		}
	}
	if (sbbs->passthru_socket != INVALID_SOCKET) {
		close_socket(sbbs->passthru_socket);
		sbbs->passthru_socket = INVALID_SOCKET;
	}
	sbbs->passthru_thread_running = false;
	sbbs->passthru_socket_active = false;
	lprintf(LOG_DEBUG, "Node %d passthru thread terminated", sbbs->cfg.node_num);
	thread_down();
}

// Wait for the output ring buffer to empty *and* for the output_thread to
// transmit its linear buffer.  WaitForOutbufEmpty() alone only confirms the
// ring buffer was drained into output_thread's linear buffer (that's when
// empty_event fires), not that those bytes have actually been sent; closing
// the socket in that window loses the unsent output (GitLab #1157).
bool sbbs_t::WaitForOutbufDrained(int timeout)
{
	if (!WaitForOutbufEmpty(timeout))
		return false;
	msclock_t start = msclock();
	while (output_thread_busy && output_thread_running) {
		if ((int)(msclock() - start) >= timeout)
			return false;
		SLEEP(1);
	}
	return true;
}

void output_thread(void* arg)
{
	char node[128];
	char errmsg[SOCKET_STRERROR_BUFLEN];
	char stats[128];
	char spy_topic[128];
	BYTE buf[IO_THREAD_BUF_SIZE];
	int i = 0;          // Assignment to silence Valgrind
	uint avail;
	uint total_sent = 0;
	uint total_pkts = 0;
	uint short_sends = 0;
	uint bufbot = 0;
	uint buftop = 0;
	sbbs_t*     sbbs = (sbbs_t*) arg;
	uint mss = IO_THREAD_BUF_SIZE;

	SetThreadName("sbbs/termOutput");
	thread_up(true /* setuid */);

	if (sbbs->cfg.node_num) {
		SAFEPRINTF(node, "Node %d", sbbs->cfg.node_num);
		SAFEPRINTF(spy_topic, "node/%d/output", sbbs->cfg.node_num);
	} else
		SAFECOPY(node, sbbs->client_name);
#ifdef _DEBUG
	lprintf(LOG_DEBUG, "%s output thread started", node);
#endif

#ifdef TCP_MAXSEG
	/*
	 * Auto-tune the highwater mark to be the negotiated MSS for the
	 * socket (when possible)
	 */
	socklen_t sl;
	sl = sizeof(i);
	if (!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_MAXSEG,
#ifdef _WIN32
	                (char *)
#endif
	                &i, &sl)) {
		/* Check for sanity... */
		if (i > 100) {
#ifdef _WIN32
#ifdef TCP_TIMESTAMPS
			DWORD ts;
			sl = sizeof(ts);
			if (!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_TIMESTAMPS, (char *)&ts, &sl)) {
				if (ts)
					i -= 12;
			}
#endif
#else
#if (defined(TCP_INFO) && defined(TCPI_OPT_TIMESTAMPS))
			struct tcp_info tcpi;

			sl = sizeof(tcpi);
			if (!getsockopt(sbbs->client_socket, IPPROTO_TCP, TCP_INFO, &tcpi, &sl)) {
				if (tcpi.tcpi_options & TCPI_OPT_TIMESTAMPS)
					i -= 12;
			}
#endif
#endif
			if (i > IO_THREAD_BUF_SIZE)
				i = IO_THREAD_BUF_SIZE;
			sbbs->outbuf.highwater_mark = i;
			mss = sbbs->outbuf.highwater_mark;
			lprintf(LOG_DEBUG, "%s outbuf highwater mark tuned to %d based on MSS", node, sbbs->outbuf.highwater_mark);
		}
	}
#endif
	sbbs->terminate_output_thread = false;

	/* Note: do not terminate when online==false, that is expected for the terminal server output_thread */
	while (sbbs->client_socket != INVALID_SOCKET && !terminate_server && !sbbs->terminate_output_thread) {
		/*
		 * I'd like to check the linear buffer against the highwater
		 * at this point, but it would get too clumsy imho - Deuce
		 *
		 * Actually, another option would just be to have the size
		 * of the linear buffer equal to the MSS... any larger and
		 * you could have small sends off the end.  this would
		 * probably be even clumsier
		 */
		if (bufbot == buftop) {
			/* Wait for something to output in the RingBuffer */
			if (WaitForEvent(sbbs->outbuf.data_event, 1000) != WAIT_OBJECT_0)
				continue;

			/* Wait for full buffer or drain timeout */
			if (sbbs->outbuf.highwater_mark)
				WaitForEvent(sbbs->outbuf.highwater_event, startup->outbuf_drain_timeout);

			/* Get fill level */
			avail = RingBufFull(&sbbs->outbuf);

			// If flushing or terminating, there will be nothing available
			if (avail == 0) {
				// Reset data/highwater events
				RingBufRead(&sbbs->outbuf, NULL, 0);
				continue;
			}

			/* Mark busy *before* draining the ring buffer (which fires
			 * empty_event), so WaitForOutbufDrained() can't mistake the
			 * ring->linear hand-off for "everything sent" (GitLab #1157). */
			sbbs->output_thread_busy = true;

			/*
			 * At this point, there's something to send and,
			 * if the highwater mark is set, the timeout has
			 * passed or we've hit highwater.  Read ring buffer
			 * into linear buffer.
			 */
			if (avail > sizeof(buf)) {
				lprintf(LOG_WARNING, "%s !Insufficient linear output buffer (%u > %d)"
				        , node, avail, (int)sizeof(buf));
				avail = sizeof(buf);
			}
			/* If we know the MSS, use it as the max linear buffer size. */
			if (avail > mss)
				avail = mss;
			buftop = RingBufRead(&sbbs->outbuf, buf, avail);
			bufbot = 0;
			if (buftop == 0) {
				sbbs->output_thread_busy = false;
				continue;
			}
		}

		/* Check socket for writability */
		if (!socket_writable(sbbs->client_socket, 1)) {
			continue;
		}

		uint sendbytes = buftop - bufbot;   // Bytes offered to the transport this iteration
#ifdef USE_CRYPTLIB
		if (sbbs->ssh_mode) {
			int err;
			pthread_mutex_lock(&sbbs->ssh_mutex);
			if (sbbs->terminate_output_thread) {
				pthread_mutex_unlock(&sbbs->ssh_mutex);
				break;
			}
			if (!sbbs->ssh_mode) {
				pthread_mutex_unlock(&sbbs->ssh_mutex);
				continue;
			}
			if (sbbs->session_channel == -1) {
				i = sendbytes;    // Pretend we sent it all
			}
			else {
				if (cryptStatusError((err = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_CHANNEL, sbbs->session_channel)))) {
					GCESSTR(err, node, sbbs->ssh_session, "setting channel");
					sbbs->online = false;
					i = sendbytes;    // Pretend we sent it all
				}
				else {
					/*
					 * Limit as per js_socket.c.
					 * Sure, this is TLS, not SSH, but we see weird stuff here in sz file transfers.
					 */
					if (sendbytes > 0x2000)
						sendbytes = 0x2000;
					if (cryptStatusError((err = cryptPushData(sbbs->ssh_session, (char*)buf + bufbot, sendbytes, &i)))) {
						/* Handle the SSH error here. The lprintf inside GCESSTR
						 * runs while ssh_mutex is held; releasing+reacquiring it
						 * across the error report would make the error-handling
						 * sequence racy (ssh_errors++, online=FALSE) and is the
						 * wrong tradeoff for a fast log write. */
						// coverity[SLEEP:SUPPRESS]
						GCESSTR(err, node, sbbs->ssh_session, "pushing data");
						sbbs->online = false;
						sendbytes = buftop - bufbot;
						i = sendbytes;    // Pretend we sent it all
					}
					else {
						// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
						/* This sets the write timeout for the flush, then sets it to zero
						 * afterward... presumably because the read timeout gets set to
						 * what the current write timeout is.
						 */
						if (cryptStatusError(err = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 5)))
							GCESSTR(err, node, sbbs->ssh_session, "setting write timeout");
						if (cryptStatusError((err = cryptFlushData(sbbs->ssh_session)))) {
							GCESSTR(err, node, sbbs->ssh_session, "flushing data");
							if (err != CRYPT_ERROR_TIMEOUT) {
								sbbs->online = false;
								sendbytes = buftop - bufbot;
								i = sendbytes;    // Pretend we sent it all
							}
						}
						// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
						if (cryptStatusError(err = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 0)))
							GCESSTR(err, node, sbbs->ssh_session, "setting write timeout");
					}
				}
			}
			pthread_mutex_unlock(&sbbs->ssh_mutex);
		}
		else
#endif
		// coverity[INTEGER_OVERFLOW:SUPPRESS] bufbot < buftop and 0 <= i <= sendbytes by loop invariant
		i = sendsocket(sbbs->client_socket, (char*)buf + bufbot, sendbytes);
		if (i == SOCKET_ERROR) {
			SOCKET sock = sbbs->client_socket.load();
			if (SOCKET_ERRNO == ENOTSOCK)
				lprintf(LOG_NOTICE, "%04d %s [%s] client socket closed on send", sock, sbbs->client.protocol, sbbs->client_ipaddr);
			else if (SOCKET_ERRNO == ECONNRESET)
				lprintf(LOG_NOTICE, "%04d %s [%s] connection reset by peer on send", sock, sbbs->client.protocol, sbbs->client_ipaddr);
			else if (SOCKET_ERRNO == ECONNABORTED)
				lprintf(LOG_NOTICE, "%04d %s [%s] connection aborted by peer on send", sock, sbbs->client.protocol, sbbs->client_ipaddr);
			else if (SOCKET_ERRNO == ESHUTDOWN)
				lprintf(LOG_NOTICE, "%04d %s [%s] socket shutdown on send", sock, sbbs->client.protocol, sbbs->client_ipaddr);
			else if (SOCKET_ERRNO == EINVAL)
				lprintf(LOG_NOTICE, "%04d %s [%s] socket invalid on send", sock, sbbs->client.protocol, sbbs->client_ipaddr);
			else
				lprintf(LOG_WARNING, "%04d %s [%s] !ERROR %d (%s) on send"
				        , sock, sbbs->client.protocol, sbbs->client_ipaddr, SOCKET_ERRNO, SOCKET_STRERROR(errmsg, sizeof errmsg));
			sbbs->online = false;
			/* was break; on 4/7/00 */
			i = sendbytes;    // Pretend we sent it all
		}

		if (sbbs->cfg.node_num > 0 && !(sbbs->sys_status & SS_FILEXFER)) {
			/* Spy on the user locally */
			if (startup->node_spybuf != NULL
			    && startup->node_spybuf[sbbs->cfg.node_num - 1] != NULL) {
				int result = RingBufWrite(startup->node_spybuf[sbbs->cfg.node_num - 1], buf + bufbot, i);
				if (result != i)
					lprintf(LOG_WARNING, "%s SHORT WRITE to spy ring buffer (%u < %u)"
					        , node, result, i);
			}
			/* Spy on the user remotely */
			if (mqtt.connected) {
				int result = mqtt_pub_message(&mqtt, TOPIC_BBS, spy_topic, buf + bufbot, i, /* retain: */ false);
				if (result != MQTT_SUCCESS)
					lprintf(LOG_WARNING, "%s ERROR %d (%d) publishing node output (%u bytes): %s"
					        , node, result, errno, i, spy_topic);
			}
			if (spy_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET)
				// coverity[INTEGER_OVERFLOW:SUPPRESS] bufbot < buftop and 0 <= i <= sendbytes by loop invariant
				if (sendsocket(spy_socket[sbbs->cfg.node_num - 1], (char*)buf + bufbot, i) != i && SOCKET_ERRNO != EPIPE)
					errprintf(LOG_ERR, WHERE, "%s ERROR %d writing to spy socket", node, SOCKET_ERRNO);
#ifdef __unix__
			if (uspy_socket[sbbs->cfg.node_num - 1] != INVALID_SOCKET)
				if (sendsocket(uspy_socket[sbbs->cfg.node_num - 1], (char*)buf + bufbot, i) != i)
					errprintf(LOG_ERR, WHERE, "%s ERROR %d writing to UNIX spy socket", node, SOCKET_ERRNO);
#endif
		}

		if (i != (int)sendbytes) {
			lprintf(LOG_WARNING, "%s !Short socket send (%u instead of %u)"
			        , node, i, sendbytes);
			short_sends++;
		}
		bufbot += i;
		total_sent += i;
		total_pkts++;
		if (bufbot == buftop)   // linear buffer fully transmitted
			sbbs->output_thread_busy = false;
	}

	sbbs->spymsg("Disconnected");

	sbbs->output_thread_running = false;

	if (total_sent)
		safe_snprintf(stats, sizeof(stats), "(sent %u bytes in %u blocks, %u average, %u short)"
		              , total_sent, total_pkts, total_sent / total_pkts, short_sends);
	else
		stats[0] = 0;

	lprintf(LOG_DEBUG, "%s output thread terminated %s", node, stats);
	thread_down();
}

static bool is_day_to_run(const struct tm* now, uint8_t days, uint16_t months = 0, uint32_t mdays = 0)
{
	if (!(days & (1 << now->tm_wday)))
		return false;

	if ((mdays > 1) && !(mdays & (1 << now->tm_mday)))
		return false;

	if ((months != 0) && !(months & (1 << now->tm_mon)))
		return false;

	return true;
}

static bool is_time_to_run(time_t now, const struct tm* now_tm, time_t last, uint16_t freq, uint16_t time)
{
	if (freq != 0) {
		if ((now - last) / 60 < freq)
			return false;
	} else {
		struct tm last_tm = {};
		localtime_r(&last, &last_tm);
		if (now_tm->tm_mday == last_tm.tm_mday && now_tm->tm_mon == last_tm.tm_mon) // Already ran today
			return false;
		if (((now_tm->tm_hour * 60) + now_tm->tm_min) < time)
			return false;
	}
	return true;
}

static bool is_time_to_run(time_t now, const event_t* event)
{
	if (event->last == -1)   // Always Run after init/re-init or signaled via semaphore
		return true;

	struct tm now_tm = {};
	localtime_r(&now, &now_tm);

	if (!is_day_to_run(&now_tm, event->days, event->months, event->mdays))
		return false;

	if (!is_time_to_run(now, &now_tm, event->last, event->freq, event->time))
		return false;

	if (xtrn_is_running(&scfg, getxtrnnum(&scfg, event->xtrn)))
		return false;

	return true;
}

static bool is_time_to_run(time_t now, const qhub_t* hub)
{
	if (hub->last == -1) // Always Run when signaled via semaphore
		return true;

	struct tm now_tm = {};
	localtime_r(&now, &now_tm);

	if (!is_day_to_run(&now_tm, hub->days))
		return false;

	if (!is_time_to_run(now, &now_tm, hub->last, hub->freq, hub->time))
		return false;

	return true;
}

void event_thread(void* arg)
{
	char str[MAX_PATH + 1];
	int i, j;
	int file;
	int offset;
	bool check_semaphores;
	bool packed_rep;
	uint l;
	/* TODO: This is a silly hack... */
	uint32_t l32;
	time_t now;
	time_t start;
	time_t lastsemchk = 0;
	time_t lastnodechk = 0;
	node_t node;
	glob_t g;
	sbbs_t*     sbbs = (sbbs_t*) arg;
	char event_code[LEN_CODE + 1];

	if (terminate_server)
		return;

	sbbs->lprintf(LOG_INFO, "BBS Events thread started");

	sbbs_srand();   /* Seed random number generator */

	SetThreadName("sbbs/events");
	thread_up(true /* setuid */);

	mqtt_pub_strval(&mqtt, TOPIC_HOST, "event", "thread started");

#ifdef JAVASCRIPT
	if (!(startup->options & BBS_OPT_NO_JAVASCRIPT)) {
		if ((sbbs->js_cx = sbbs->js_init(&sbbs->js_runtime, &sbbs->js_glob, "event")) == NULL) /* This must be done in the context of the events thread */
			sbbs->errprintf(LOG_CRIT, WHERE, "!JavaScript Initialization FAILURE");
	}
#endif

	// Read event "last ran" times from ctrl/time.ini
	char ini_file[MAX_PATH + 1];
	SAFEPRINTF(ini_file, "%stime.ini", sbbs->cfg.ctrl_dir);
	FILE* fp = iniOpenFile(ini_file, /* for modify: */ false);
	if (fp != nullptr) {
		for (i = 0; i < sbbs->cfg.total_events; ++i)
			sbbs->cfg.event[i]->last = (time32_t)iniReadDateTime(fp, eventIniSection, sbbs->cfg.event[i]->code, 0);
		for (i = 0; i < sbbs->cfg.total_qhubs; ++i)
			sbbs->cfg.qhub[i]->last = (time32_t)iniReadDateTime(fp, qhubIniSection, sbbs->cfg.qhub[i]->id, 0);
		iniCloseFile(fp);
	} else {
		// Read time.dab (legacy), and auto-upgrade to time.ini
		SAFEPRINTF(str, "%stime.dab", sbbs->cfg.ctrl_dir);
		if ((file = sbbs->nopen(str, O_RDONLY)) != -1) {
			for (i = 0; i < sbbs->cfg.total_events; i++) {
				if (read(file, &sbbs->cfg.event[i]->last, sizeof(sbbs->cfg.event[i]->last)) != sizeof(sbbs->cfg.event[i]->last))
					sbbs->cfg.event[i]->last = 0;
			}
			close(file);
		}
		// Read qnet.dab (legacy), and auto-upgrade to time.ini
		SAFEPRINTF(str, "%sqnet.dab", sbbs->cfg.ctrl_dir);
		if ((file = sbbs->nopen(str, O_RDONLY)) != -1) {
			for (i = 0; i < sbbs->cfg.total_qhubs; i++) {
				if (read(file, &sbbs->cfg.qhub[i]->last, sizeof(sbbs->cfg.qhub[i]->last)) != sizeof(sbbs->cfg.qhub[i]->last))
					sbbs->cfg.qhub[i]->last = 0;
			}
			close(file);
		}

		fp = iniOpenFile(ini_file, /* for modify: */ true);
		if (fp == nullptr)
			sbbs->errormsg(WHERE, ERR_CREATE, ini_file, 0);
		else {
			str_list_t ini = strListInit();
			if (ini != NULL) {
				for (i = 0; i < sbbs->cfg.total_events; ++i)
					iniSetDateTime(&ini, eventIniSection, sbbs->cfg.event[i]->code, /* include time */ true, sbbs->cfg.event[i]->last, &eventIniStyle);
				for (i = 0; i < sbbs->cfg.total_qhubs; ++i)
					iniSetDateTime(&ini, qhubIniSection, sbbs->cfg.qhub[i]->id, /* include time */ true, sbbs->cfg.qhub[i]->last, &qhubIniStyle);
				iniWriteFile(fp, ini);
				iniCloseFile(fp);
				iniFreeStringList(ini);
			}
		}
	}
	for (i = 0; i < sbbs->cfg.total_events; ++i) {
		/* Event always runs after initialization? */
		if (sbbs->cfg.event[i]->misc & EVENT_INIT)
			sbbs->cfg.event[i]->last = -1;
		if (sbbs->cfg.event[i]->node == NODE_ANY
		    || (sbbs->cfg.event[i]->node >= first_node && sbbs->cfg.event[i]->node <= last_node))
			sbbs->fremove(WHERE, sbbs->event_running_filename(str, sizeof str, i));
	}

	while (!sbbs->terminated && !terminate_server) {

		sbbs->event_thread_running = true;
		event_thread_tick = time(NULL);

		if (startup->options & BBS_OPT_NO_EVENTS) {
			SLEEP(1000);
			continue;
		}

		now = time(NULL);

		if (now - lastsemchk >= startup->sem_chk_freq) {
			check_semaphores = true;
			lastsemchk = now;
		} else
			check_semaphores = false;

		sbbs->online = false; /* reset this from ON_LOCAL */

		/* QWK events */
		if (check_semaphores && !(startup->options & BBS_OPT_NO_QWK_EVENTS)) {
			sbbs->event_code = "unpackREP";
			/* Import any REP files that have magically appeared (via FTP perhaps) */
			SAFEPRINTF(str, "%sfile/", sbbs->cfg.data_dir);
			offset = strlen(str);
			strcat(str, "*.rep");
			glob(str, 0, NULL, &g);
			for (i = 0; i < (int)g.gl_pathc && !sbbs->terminated; i++) {
				char* fname = g.gl_pathv[i];
				if (flength(fname) < 1)
					continue;
				sbbs->useron.number = 0;
				sbbs->lprintf(LOG_DEBUG, "Inbound QWK Reply Packet detected: %s", fname);
				if (sbbs->getuseron(WHERE, atoi(fname + offset)) && !(sbbs->useron.misc & (DELETED | INACTIVE))) {
					fmutex_t lockfile;
					SAFEPRINTF(lockfile.name, "%s.lock", fname);
					if (!fmutex_open(&lockfile, startup->host_name, TIMEOUT_MUTEX_FILE)) {
						if (difftime(time(NULL), lockfile.time) > 60)
							sbbs->lprintf(LOG_INFO, " %s exists (unpack in progress?) since %s", lockfile.name, time_as_hhmm(&sbbs->cfg, lockfile.time, str));
						continue;
					}
					sbbs->lprintf(LOG_DEBUG, "Opened %s", lockfile.name);
					if (!fexist(fname)) {
						sbbs->lprintf(LOG_DEBUG, "%s already gone", fname);
						if (!fmutex_close(&lockfile))
							sbbs->errormsg(WHERE, ERR_CLOSE, lockfile.name);
						continue;
					}
					sbbs->online = ON_LOCAL;
					sbbs->getusrsubs();
					bool success = sbbs->unpack_rep(fname);
					sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);        /* clean-up temp_dir after unpacking */
					sbbs->online = false;

					/* putuserdat? */
					if (success) {
						sbbs->fremove(WHERE, fname, /* log-all-errors: */ true);
					} else if (flength(fname) > 0) {
						char badpkt[MAX_PATH + 1];
						SAFEPRINTF2(badpkt, "%s.%" PRIx64 ".bad", fname, (uint64_t)time(NULL));
						sbbs->fremove(WHERE, badpkt);
						if (flength(fname) > 0) {
							if (rename(fname, badpkt) == 0)
								sbbs->lprintf(LOG_NOTICE, "%s renamed to %s", fname, badpkt);
							else
								sbbs->lprintf(LOG_ERR, "!ERROR %d (%s) renaming %s (%ld bytes) to %s"
								              , errno, strerror(errno), fname, (long)flength(fname), badpkt);
							SAFEPRINTF(badpkt, "%u.rep.*.bad", sbbs->useron.number);
							SAFEPRINTF(str, "%sfile/", sbbs->cfg.data_dir);
							sbbs->delfiles(str, badpkt, /* keep: */ 10);
						}
					}
					if (!fmutex_close(&lockfile))
						sbbs->errormsg(WHERE, ERR_CLOSE, lockfile.name);
				}
				else {
					sbbs->lprintf(LOG_INFO, "Removing: %s", fname);
					sbbs->fremove(WHERE, fname, /* log-all-errors: */ true);
				}
			}
			globfree(&g);
			sbbs->useron.number = 0;

			/* Create any QWK files that have magically appeared (via FTP perhaps) */
			sbbs->event_code = "packQWK";
			SAFEPRINTF(str, "%spack*.now", sbbs->cfg.data_dir);
			offset = strlen(sbbs->cfg.data_dir) + 4;
			glob(str, 0, NULL, &g);
			for (i = 0; i < (int)g.gl_pathc && !sbbs->terminated; i++) {
				char* fname = g.gl_pathv[i];
				if (!fexist(fname))
					continue;
				sbbs->useron.number = 0;
				sbbs->lprintf(LOG_INFO, "QWK pack semaphore signaled: %s", fname);
				if (!sbbs->getuseron(WHERE, atoi(fname + offset))) {
					sbbs->fremove(WHERE, fname, /* log-all-errors: */ true);
					continue;
				}
				fmutex_t lockfile;
				SAFEPRINTF2(lockfile.name, "%spack%04u.lock", sbbs->cfg.data_dir, sbbs->useron.number);
				if (!fmutex_open(&lockfile, startup->host_name, TIMEOUT_MUTEX_FILE)) {
					if (difftime(time(NULL), lockfile.time) > 60)
						sbbs->lprintf(LOG_INFO, "%s exists (pack in progress?) since %s", lockfile.name, time_as_hhmm(&sbbs->cfg, lockfile.time, str));
					continue;
				}
				sbbs->lprintf(LOG_DEBUG, "Opened %s", lockfile.name);
				if (!fexist(fname)) {
					sbbs->lprintf(LOG_DEBUG, "%s already gone", fname);
					if (!fmutex_close(&lockfile))
						sbbs->errormsg(WHERE, ERR_CLOSE, lockfile.name);
					continue;
				}
				if (!(sbbs->useron.misc & (DELETED | INACTIVE))) {
					sbbs->lprintf(LOG_INFO, "Packing QWK Message Packet");
					sbbs->online = ON_LOCAL;
					sbbs->getmsgptrs();
					sbbs->getusrsubs();

					sbbs->last_ns_time = sbbs->ns_time = sbbs->useron.ns_time;

					SAFEPRINTF3(str, "%sfile%c%04u.qwk"
					            , sbbs->cfg.data_dir, PATH_DELIM, sbbs->useron.number);
					if (sbbs->pack_qwk(str, &l, true /* pre-pack/off-line */)) {
						sbbs->lprintf(LOG_INFO, "Packing completed: %s", str);
						sbbs->qwk_success(l, 0, 1);
						sbbs->putmsgptrs();
						batch_list_clear(&sbbs->cfg, sbbs->useron.number, XFER_BATCH_DOWNLOAD);
					} else
						sbbs->lputs(LOG_INFO, "No packet created (no new messages)");
					sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);
					sbbs->online = false;
				}
				sbbs->fremove(WHERE, fname);
				if (!fmutex_close(&lockfile))
					sbbs->errormsg(WHERE, ERR_CLOSE, lockfile.name);
			}
			globfree(&g);
			sbbs->useron.number = 0;
		}

		if (check_semaphores) {

			/* Run daily maintenance? */
			sbbs->cfg.node_num = 0;
			if (!(startup->options & BBS_OPT_NO_NEWDAY_EVENTS)) {
				sbbs->event_code = "";
				sbbs->online = ON_LOCAL;
				sbbs->logonstats();
				sbbs->online = false;
				if (sbbs->sys_status & SS_DAILY)
					sbbs->daily_maint();
			}
			/* Node Daily Events */
			sbbs->event_code = "DAILY";
			for (i = first_node; i <= last_node; i++) {
				// Node Daily Event
				node.status = NODE_INVALID_STATUS;
				if (!sbbs->getnodedat(i, &node))
					continue;
				if (node.misc & NODE_EVENT && node.status == NODE_WFC) {
					if (sbbs->getnodedat(i, &node, true)) {
						node.status = NODE_EVENT_RUNNING;
						sbbs->putnodedat(i, &node);
					}
					if (sbbs->cfg.node_daily_cmd[0] && !(sbbs->cfg.node_daily_misc & EVENT_DISABLED)) {
						sbbs->cfg.node_num = i;
						SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[i - 1]);

						sbbs->lprintf(LOG_INFO, "Running node %d daily event", i);
						sbbs->online = ON_LOCAL;
						sbbs->logentry("!:", "Run node daily event");
						const char* cmd = sbbs->cmdstr(sbbs->cfg.node_daily_cmd, nulstr, nulstr, NULL, sbbs->cfg.node_daily_misc);
						int result = sbbs->external(cmd, EX_OFFLINE | sbbs->cfg.node_daily_misc);
						sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Node daily event: '%s' returned %d", cmd, result);
						sbbs->online = false;
					}
					if (sbbs->getnodedat(i, &node, true)) {
						node.misc &= ~NODE_EVENT;
						node.status = NODE_WFC;
						node.useron = 0;
						sbbs->putnodedat(i, &node);
					}
				}
			}
			sbbs->event_code = nulstr;

			/* QWK Networking Call-out semaphores */
			for (i = 0; i < sbbs->cfg.total_qhubs; i++) {
				if (!sbbs->cfg.qhub[i]->enabled)
					continue;
				if (sbbs->cfg.qhub[i]->node != NODE_ANY
				    && (sbbs->cfg.qhub[i]->node<first_node || sbbs->cfg.qhub[i]->node>last_node))
					continue;
				if (sbbs->cfg.qhub[i]->last == -1) // already signaled
					continue;
				SAFEPRINTF2(str, "%sqnet/%s.now", sbbs->cfg.data_dir, sbbs->cfg.qhub[i]->id);
				if (fexistcase(str)) {
					SAFECOPY(str, sbbs->cfg.qhub[i]->id);
					sbbs->lprintf(LOG_INFO, "Semaphore signaled for QWK Network Hub: %s", strupr(str));
					sbbs->cfg.qhub[i]->last = -1;
				}
			}

			/* Timed Event semaphores */
			for (i = 0; i < sbbs->cfg.total_events; i++) {
				if (sbbs->cfg.event[i]->node != NODE_ANY
				    && (sbbs->cfg.event[i]->node<first_node || sbbs->cfg.event[i]->node>last_node)
				    && !(sbbs->cfg.event[i]->misc & EVENT_EXCL))
					continue;   // ignore non-exclusive events for other instances
				if (sbbs->cfg.event[i]->misc & EVENT_DISABLED)
					continue;
				if (sbbs->cfg.event[i]->last == -1) // already signaled
					continue;
				SAFEPRINTF2(str, "%s%s.now", sbbs->cfg.data_dir, sbbs->cfg.event[i]->code);
				if (fexistcase(str)) {
					SAFECOPY(str, sbbs->cfg.event[i]->code);
					sbbs->lprintf(LOG_INFO, "Semaphore signaled for Timed Event: %s", strupr(str));
					sbbs->cfg.event[i]->last = -1;
				}
			}
		}

		/* QWK Networking Call-out Events */
		sbbs->event_code = "QNET";
		for (i = 0; i < sbbs->cfg.total_qhubs && !sbbs->terminated; i++) {
			if (!sbbs->cfg.qhub[i]->enabled)
				continue;
			if (sbbs->cfg.qhub[i]->node != NODE_ANY
			    && (sbbs->cfg.qhub[i]->node<first_node || sbbs->cfg.qhub[i]->node>last_node))
				continue;

			if (check_semaphores) {
				// See if any packets have come in
				SAFEPRINTF2(str, "%s%s.q??", sbbs->cfg.data_dir, sbbs->cfg.qhub[i]->id);
				glob(str, GLOB_NOSORT, NULL, &g);
				for (j = 0; j < (int)g.gl_pathc; j++) {
					SAFECOPY(str, g.gl_pathv[j]);
					if (flength(str) > 0) {    /* silently ignore 0-byte QWK packets */
						sbbs->lprintf(LOG_DEBUG, "Inbound QWK Packet detected: %s", str);
						sbbs->online = ON_LOCAL;
						if (sbbs->unpack_qwk(str, i) == false) {
							char newname[MAX_PATH + 1];
							SAFEPRINTF2(newname, "%s.%x.bad", str, (int)now);
							sbbs->fremove(WHERE, newname);
							if (rename(str, newname) == 0)
								sbbs->lprintf(LOG_NOTICE, "%s renamed to %s", str, newname);
							else
								sbbs->lprintf(LOG_ERR, "!ERROR %d (%s) renaming %s to %s"
								              , errno, strerror(errno), str, newname);
							SAFEPRINTF(newname, "%s.q??.*.bad", sbbs->cfg.qhub[i]->id);
							sbbs->delfiles(sbbs->cfg.data_dir, newname, /* keep: */ 10);
						}
						sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);
						sbbs->online = false;
						if (fexist(str))
							sbbs->fremove(WHERE, str, /* log-all-errors: */ true);
					}
				}
				globfree(&g);
			}

			/* Qnet call out based on time */
			if (is_time_to_run(now, sbbs->cfg.qhub[i])) {
				SAFEPRINTF2(str, "%sqnet/%s.now"
				            , sbbs->cfg.data_dir, sbbs->cfg.qhub[i]->id);
				if (fexistcase(str))
					sbbs->fremove(WHERE, str);                  /* Remove semaphore file */
				SAFEPRINTF2(str, "%sqnet/%s.ptr"
				            , sbbs->cfg.data_dir, sbbs->cfg.qhub[i]->id);
				file = sbbs->nopen(str, O_RDONLY);
				for (j = 0; j < sbbs->cfg.qhub[i]->subs; j++) {
					sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr = 0;
					if (file != -1) {
						lseek(file, sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]->subnum]->ptridx * sizeof(int32_t), SEEK_SET);
						if (read(file, &sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr
						         , sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)) != sizeof(uint32_t))
							sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr = 0;
					}
				}
				if (file != -1)
					close(file);
				packed_rep = sbbs->pack_rep(i);
				if (packed_rep) {
					if ((file = sbbs->nopen(str, O_WRONLY | O_CREAT)) == -1)
						sbbs->errormsg(WHERE, ERR_OPEN, str, O_WRONLY | O_CREAT);
					else {
						for (j = l = 0; j < sbbs->cfg.qhub[i]->subs; j++) {
							while (filelength(file) <
							       sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]->subnum]->ptridx * 4L) {
								l32 = l;
								if (write(file, &l32, 4) != 4)     /* initialize ptrs to null */
									sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
							}
							lseek(file
							      , sbbs->cfg.sub[sbbs->cfg.qhub[i]->sub[j]->subnum]->ptridx * sizeof(int32_t)
							      , SEEK_SET);
							if (write(file, &sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr, sizeof(sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)) != sizeof sbbs->subscan[sbbs->cfg.qhub[i]->sub[j]->subnum].ptr)
								sbbs->errormsg(WHERE, ERR_WRITE, str, 4);
						}
						close(file);
					}
				}
				sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);

				sbbs->cfg.qhub[i]->last = time32(NULL);
				SAFEPRINTF(str, "%stime.ini", sbbs->cfg.ctrl_dir);
				FILE* fp = iniOpenFile(str, /* for_modify: */ true);
				if (fp == NULL) {
					sbbs->errormsg(WHERE, ERR_OPEN, str, O_WRONLY);
					break;
				}
				str_list_t ini = iniReadFile(fp);
				if (ini != NULL) {
					iniSetDateTime(&ini, qhubIniSection, sbbs->cfg.qhub[i]->id, /* include time: */ true, sbbs->cfg.qhub[i]->last, &qhubIniStyle);
					iniWriteFile(fp, ini);
					iniFreeStringList(ini);
				}
				iniCloseFile(fp);

				if (sbbs->cfg.qhub[i]->call[0]) {
					if (sbbs->cfg.qhub[i]->node == NODE_ANY)
						sbbs->cfg.node_num = startup->first_node;
					else
						sbbs->cfg.node_num = sbbs->cfg.qhub[i]->node;
					if (sbbs->cfg.node_num < 1)
						sbbs->cfg.node_num = 1;
					SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num - 1]);
					sbbs->lprintf(LOG_INFO, "Call-out: %s", sbbs->cfg.qhub[i]->id);
					sbbs->online = ON_LOCAL;
					int ex_mode = EX_OFFLINE | EX_SH; /* sh for Unix perl scripts */
					if (sbbs->cfg.qhub[i]->misc & QHUB_NATIVE)
						ex_mode |= EX_NATIVE;
					const char* cmd = sbbs->cmdstr(sbbs->cfg.qhub[i]->call, sbbs->cfg.qhub[i]->id, sbbs->cfg.qhub[i]->id, NULL, ex_mode);
					int result = sbbs->external(cmd, ex_mode);
					sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Call-out to: %s (%s) returned %d", sbbs->cfg.qhub[i]->id, cmd, result);
					sbbs->online = false;
				}
			}
		}

		/* Timed Events */
		for (i = 0; i < sbbs->cfg.total_events && !sbbs->terminated; i++) {
			if (sbbs->cfg.event[i]->node > sbbs->cfg.sys_nodes)
				continue;   // ignore events for invalid nodes

			if (sbbs->cfg.event[i]->misc & EVENT_DISABLED)
				continue;

			if (sbbs->cfg.event[i]->node != NODE_ANY
			    && (sbbs->cfg.event[i]->node<first_node || sbbs->cfg.event[i]->node>last_node)
			    && !(sbbs->cfg.event[i]->misc & EVENT_EXCL))
				continue;   // ignore non-exclusive events for other instances

			SAFECOPY(event_code, sbbs->cfg.event[i]->code);
			strupr(event_code);
			sbbs->event_code = event_code;

			if (is_time_to_run(now, sbbs->cfg.event[i]))
			{
				if (sbbs->cfg.event[i]->node != NODE_ANY && (sbbs->cfg.event[i]->misc & EVENT_EXCL)) { /* exclusive event */

					if (sbbs->cfg.event[i]->node<first_node
					                             || sbbs->cfg.event[i]->node>last_node) {
						sbbs->lprintf(LOG_INFO, "Waiting for node %d to run timed event: %s"
						              , sbbs->cfg.event[i]->node, event_code);
						sbbs->lprintf(LOG_DEBUG, "event last run: %s (0x%08x)"
						              , sbbs->timestr(sbbs->cfg.event[i]->last)
						              , sbbs->cfg.event[i]->last);
						lastnodechk = 0;   /* really last event time check */
						start = time(NULL);
						while (!sbbs->terminated) {
							mswait(1000);
							now = time(NULL);
							if (now - start > 10 && now - lastnodechk < 10)
								continue;
							for (j = first_node; j <= last_node; j++) {
								if (!sbbs->getnodedat(j, &node, true))
									continue;
								if (node.status == NODE_WFC)
									node.status = NODE_EVENT_LIMBO;
								node.aux = sbbs->cfg.event[i]->node;
								sbbs->putnodedat(j, &node);
							}

							lastnodechk = now;
							SAFEPRINTF(str, "%stime.ini", sbbs->cfg.ctrl_dir);
							if ((fp = iniOpenFile(str, /* for modify: */ false)) == NULL) {
								sbbs->errormsg(WHERE, ERR_OPEN, str, O_RDONLY);
								sbbs->cfg.event[i]->last = (time32_t)now;
								continue;
							}
							sbbs->cfg.event[i]->last = (time32_t)iniReadDateTime(fp, eventIniSection, sbbs->cfg.event[i]->code, 0);
							iniCloseFile(fp);
							if (now - sbbs->cfg.event[i]->last < (60 * 60))    /* event is done */
								break;
							if (now - start > (90 * 60)) {
								sbbs->lprintf(LOG_WARNING, "!TIMEOUT waiting for event to complete");
								break;
							}
						}
						SAFEPRINTF2(str, "%s%s.now", sbbs->cfg.data_dir, sbbs->cfg.event[i]->code);
						if (fexistcase(str))
							sbbs->fremove(WHERE, str);
						sbbs->cfg.event[i]->last = (time32_t)now;
					} else {    // Exclusive event to run on a node under our control
						sbbs->lprintf(LOG_INFO, "Waiting for all nodes to become inactive before running timed event");
						lastnodechk = 0;
						start = time(NULL);
						while (!sbbs->terminated) {
							mswait(1000);
							now = time(NULL);
							if (now - start > 10 && now - lastnodechk < 10)
								continue;
							lastnodechk = now;
							// Check/change the status of the nodes that we're in control of
							for (j = first_node; j <= last_node; j++) {
								if (!sbbs->getnodedat(j, &node, true))
									continue;
								if (node.status == NODE_WFC) {
									if (j == sbbs->cfg.event[i]->node)
										node.status = NODE_EVENT_WAITING;
									else
										node.status = NODE_EVENT_LIMBO;
									node.aux = sbbs->cfg.event[i]->node;
								}
								sbbs->putnodedat(j, &node);
							}

							for (j = 1; j <= sbbs->cfg.sys_nodes; j++) {
								if (!sbbs->getnodedat(j, &node))
									continue;
								if (j == sbbs->cfg.event[i]->node) {
									if (node.status != NODE_EVENT_WAITING)
										break;
								} else {
									if (node.status != NODE_OFFLINE
									    && node.status != NODE_EVENT_LIMBO)
										break;
								}
							}
							if (j > sbbs->cfg.sys_nodes) /* all nodes either offline or in limbo */
								break;
							char node_status[128];
							snprintf(node_status, sizeof node_status, "status=%d, misc=0x%X, action=%d, useron=%d, aux=%d"
								, node.status, node.misc, node.action, node.useron, node.aux);
							sbbs->lprintf(LOG_DEBUG, "Waiting for node %d (%s)", j, node_status);
							if (now - start > (30 * 60) && !(node.misc & NODE_INTR)) {
								sbbs->lprintf(LOG_NOTICE, "!Interrupting node %d (%s)", j, node_status);
								if (!set_node_interrupt(&sbbs->cfg, j, true))
									sbbs->lprintf(LOG_ERR, "!ERROR interrupting node %d (%s)", j, node_status);
							}
							if (now - start > (60 * 60) && node_socket[j - 1] != INVALID_SOCKET) {
								sbbs->lprintf(LOG_WARNING, "!TIRED of waiting for node %d to become inactive (%s), shutting down socket %d"
								              , j, node_status, node_socket[j - 1]);
								// Shutdown (don't close) to wake the node's blocked reads: the
								// node thread owns and closes this descriptor in its own teardown
								shutdown(node_socket[j - 1], SHUT_RDWR);
								node_socket[j - 1] = INVALID_SOCKET;
							}
							if (now - start > (90 * 60)) {
								sbbs->lprintf(LOG_WARNING, "!TIMEOUT waiting for node %d to become inactive (%s), aborting wait"
								              , j, node_status);
								break;
							}
						}
					}
				}
#if 0 // removed Jun-23-2002
				else {  /* non-exclusive */
					sbbs->getnodedat(sbbs->cfg.event[i]->node, &node, 0);
					if (node.status != NODE_WFC)
						continue;
				}
#endif
				if (sbbs->cfg.event[i]->node != NODE_ANY
				    && (sbbs->cfg.event[i]->node<first_node || sbbs->cfg.event[i]->node>last_node)) {
					sbbs->lprintf(LOG_NOTICE, "Changing node status for nodes %d through %d to WFC"
					              , first_node, last_node);
					sbbs->cfg.event[i]->last = (time32_t)now;
					for (j = first_node; j <= last_node; j++) {
						node.status = NODE_INVALID_STATUS;
						if (!sbbs->getnodedat(j, &node, true))
							continue;
						node.status = NODE_WFC;
						sbbs->putnodedat(j, &node);
					}
				}
				else {
					if (sbbs->cfg.event[i]->node == NODE_ANY)
						sbbs->cfg.node_num = startup->first_node;
					else
						sbbs->cfg.node_num = sbbs->cfg.event[i]->node;
					if (sbbs->cfg.node_num < 1)
						sbbs->cfg.node_num = 1;
					SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num - 1]);

					SAFEPRINTF2(str, "%s%s.now", sbbs->cfg.data_dir, sbbs->cfg.event[i]->code);
					if (fexistcase(str))
						sbbs->fremove(WHERE, str);
					if (sbbs->cfg.event[i]->node != NODE_ANY && (sbbs->cfg.event[i]->misc & EVENT_EXCL)) {
						if (sbbs->getnodedat(sbbs->cfg.event[i]->node, &node, true)) {
							node.status = NODE_EVENT_RUNNING;
							sbbs->putnodedat(sbbs->cfg.event[i]->node, &node);
						}
					}
					const char* cmd = sbbs->cfg.event[i]->cmd;
					int ex_mode = EX_OFFLINE;
					if (!(sbbs->cfg.event[i]->misc & EVENT_EXCL)
					    && sbbs->cfg.event[i]->misc & EX_BG)
						ex_mode |= EX_BG;
					if (sbbs->cfg.event[i]->misc & XTRN_SH)
						ex_mode |= EX_SH;
					ex_mode |= (sbbs->cfg.event[i]->misc & EX_NATIVE);
					sbbs->online = ON_LOCAL;
					cmd = sbbs->cmdstr(cmd, nulstr, sbbs->cfg.event[i]->dir, NULL, ex_mode);
					sbbs->lprintf(LOG_INFO, "Running %s%stimed event: %s"
					              , native_executable(&sbbs->cfg, cmd, ex_mode) ? "native ":"16-bit DOS "
					              , (ex_mode & EX_BG)        ? "background ":""
					              , cmd);
					{
						char path[MAX_PATH + 1];
						if (!(ex_mode & EX_BG))
							ftouch(sbbs->event_running_filename(path, sizeof path, i));
						int result = sbbs->external(cmd, ex_mode, sbbs->cfg.event[i]->dir);
						if (!(ex_mode & EX_BG)) {
							sbbs->lprintf(result ? sbbs->cfg.event[i]->errlevel : LOG_INFO, "Timed event: '%s' returned %d", cmd, result);
							sbbs->fremove(WHERE, path);
						}
						else
							sbbs->lprintf(LOG_DEBUG, "Background timed event spawned: %s", cmd);
					}
					sbbs->online = false;
					sbbs->cfg.event[i]->last = time32(NULL);
					SAFEPRINTF(str, "%stime.ini", sbbs->cfg.ctrl_dir);
					if ((fp = iniOpenFile(str, /* for modify: */ true)) == NULL) {
						sbbs->errormsg(WHERE, ERR_OPEN, str, O_WRONLY);
						break;
					}
					str_list_t ini = iniReadFile(fp);
					if (ini != NULL) {
						iniSetDateTime(&ini, eventIniSection, sbbs->cfg.event[i]->code, /* include time */ true, sbbs->cfg.event[i]->last, &eventIniStyle);
						iniWriteFile(fp, ini);
						iniFreeStringList(ini);
					}
					iniCloseFile(fp);

					if (sbbs->cfg.event[i]->node != NODE_ANY
					    && sbbs->cfg.event[i]->misc & EVENT_EXCL) { /* exclusive event */
						// Check/change the status of the nodes that we're in control of
						for (j = first_node; j <= last_node; j++) {
							node.status = NODE_INVALID_STATUS;
							if (!sbbs->getnodedat(j, &node, true))
								continue;
							node.status = NODE_WFC;
							sbbs->putnodedat(j, &node);
						}
					}
				}
			}
		}
		sbbs->event_code = nulstr;
		mswait(1000);
	}
	sbbs->cfg.node_num = 0;
	sbbs->useron.number = 0;
	sbbs->js_cleanup();

	sbbs->event_thread_running = false;

	mqtt_pub_strval(&mqtt, TOPIC_HOST, "event", "thread stopped");

	sbbs->lprintf(LOG_INFO, "BBS Events thread terminated");
	FCLOSE_OPEN_FILE(sbbs->logfile_fp);
	thread_down();
}

//****************************************************************************
sbbs_t::sbbs_t(ushort node_num, union xp_sockaddr *addr, size_t addr_len, const char* name, SOCKET sd,
               scfg_t* global_cfg, char* global_text[], client_t* client_info, bool is_event_thread)
{
	char path[MAX_PATH + 1];
	uint i;

	// Initialize some member variables used by lputs:
	this->is_event_thread = is_event_thread;
	cfg.node_num = node_num;
	SAFECOPY(client_name, name);

	startup = ::startup;    // Convert from global to class member
	mqtt = &::mqtt;

	memcpy(&cfg, global_cfg, sizeof(cfg));
	cfg.node_num = node_num;    // Restore the node_num passed to the constructor
	lprintf(LOG_DEBUG, "constructor using socket %d (settings=%x)"
	        , sd, global_cfg->node_misc);

	if (node_num > 0) {
		SAFECOPY(cfg.node_dir, cfg.node_path[node_num - 1]);
		prep_dir(cfg.node_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
		SAFEPRINTF2(syspage_semfile, "%ssyspage.%u", cfg.ctrl_dir, node_num);
		fremove(WHERE, syspage_semfile);
	} else {    /* event thread needs exclusive-use temp_dir */
		if (startup->temp_dir[0])
			SAFECOPY(cfg.temp_dir, startup->temp_dir);
		else
			SAFECOPY(cfg.temp_dir, "../temp");
		prep_dir(cfg.ctrl_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
		if ((i = md(cfg.temp_dir)) != 0)
			errprintf(LOG_CRIT, WHERE, "!ERROR %d (%s) creating directory: %s", i, strerror(i), cfg.temp_dir);
		if (sd == INVALID_SOCKET) {    /* events thread */
			if (startup->first_node == 1)
				SAFEPRINTF(path, "%sevent", cfg.temp_dir);
			else
				SAFEPRINTF2(path, "%sevent%u", cfg.temp_dir, startup->first_node);
			backslash(path);
			SAFECOPY(cfg.temp_dir, path);
		}
		syspage_semfile[0] = 0;
	}
	lprintf(LOG_DEBUG, "temporary file directory: %s", cfg.temp_dir);

	if (client_info != nullptr)
		memcpy(&client, client_info, sizeof(client));
	client_addr.store = addr->store;
	client_socket = sd;
	client_socket_dup = INVALID_SOCKET;

	/* Init some important variables */

	SAFECOPY(connection, "Telnet");
	telnet_ack_event = CreateEvent(NULL, /* Manual Reset: */ false, /* InitialState */ false, NULL);

	listInit(&smb_list, /* flags: */ 0);
	pthread_mutex_init(&nodefile_mutex, NULL);

	for (i = 0; i < TOTAL_TEXT; i++)
		text[i] = text_sav[i] = global_text[i];

	memcpy(rainbow, cfg.rainbow, sizeof rainbow);
}

//****************************************************************************
bool sbbs_t::init()
{
	char str[MAX_PATH + 1];
	int result;
	int i, j, k, l;
	node_t node;
	socklen_t addr_len;
	union xp_sockaddr addr;

	if (cfg.total_shells < 1) {
		errormsg(WHERE, ERR_CHK, "At least one command shell must be configured (e.g. in SCFG->Command Shells)"
		         , cfg.total_shells);
		return false;
	}

	RingBufInit(&inbuf, IO_THREAD_BUF_SIZE);
	if (cfg.node_num > 0)
		node_inbuf[cfg.node_num - 1] = &inbuf;

	RingBufInit(&outbuf, IO_THREAD_BUF_SIZE);
	outbuf.highwater_mark = startup->outbuf_highwater_mark;

	if (cfg.node_num && client_socket != INVALID_SOCKET) {

		addr_len = sizeof(addr);
		if ((result = getsockname(client_socket, &addr.addr, &addr_len)) != 0) {
			errprintf(LOG_CRIT, WHERE, "%04d %s !ERROR %d (%d) getting local address/port of socket"
			          , client_socket.load(), client.protocol, result, SOCKET_ERRNO);
			return false;
		}
		inet_addrtop(&addr, local_addr, sizeof(local_addr));
		inet_addrtop(&client_addr, client_ipaddr, sizeof(client_ipaddr));
		SAFEPRINTF(str, "%sclient.ini", cfg.node_dir);
		FILE* fp = fopen(str, "wt");
		if (fp != NULL) {
			fprintf(fp, "sock=%d\n", client_socket.load());
			fprintf(fp, "addr=%s\n", client.addr);
			fprintf(fp, "host=%s\n", client.host);
			fprintf(fp, "port=%u\n", (uint)client.port);
			fprintf(fp, "time=%u\n", (uint)client.time);
			fprintf(fp, "prot=%s\n", client.protocol);
			fprintf(fp, "local_addr=%s\n", local_addr);
			fprintf(fp, "local_port=%u\n", (uint)inet_addrport(&addr));
			fclose(fp);
		}
		spymsg("Connected");
	}

	if ((comspec = os_cmdshell()) == NULL) {
		errormsg(WHERE, ERR_CHK, OS_CMD_SHELL_ENV_VAR " environment variable", 0);
		return false;
	}

	if ((i = md(cfg.temp_dir)) != 0) {
		errprintf(LOG_CRIT, WHERE, "!ERROR %d (%s) creating directory: %s", i, strerror(i), cfg.temp_dir);
		return false;
	}

	/* Shared NODE files */
	pthread_mutex_lock(&nodefile_mutex);
	if ((nodefile = opennodedat(&cfg)) == -1) {
		pthread_mutex_unlock(&nodefile_mutex);
		errormsg(WHERE, ERR_OPEN, "nodefile", cfg.node_num);
		return false;
	}
	memset(&node, 0, sizeof(node_t));  /* write NULL to node struct */
	node.status = NODE_OFFLINE;
	while (filelength(nodefile) < (int)(cfg.sys_nodes * sizeof(node_t))) {
		lseek(nodefile, 0L, SEEK_END);
		if (write(nodefile, &node, sizeof(node_t)) != sizeof(node_t)) {
			pthread_mutex_unlock(&nodefile_mutex);
			errormsg(WHERE, ERR_WRITE, "nodefile", sizeof(node_t));
			break;
		}
	}
	if (chsize(nodefile, (off_t)(cfg.sys_nodes * sizeof(node_t))) != 0) {
		pthread_mutex_unlock(&nodefile_mutex);
		errormsg(WHERE, ERR_LEN, "ndoefile", cfg.sys_nodes * sizeof(node_t));
	}
	for (i = 0; cfg.node_num > 0 && i < LOOP_NODEDAB; i++) {
		if (lock(nodefile, (cfg.node_num - 1) * sizeof(node_t), sizeof(node_t)) == 0) {
			unlock(nodefile, (cfg.node_num - 1) * sizeof(node_t), sizeof(node_t));
			break;
		}
		FILE_RETRY_DELAY(i + 1, LOCK_RETRY_DELAY);
	}
	if (cfg.node_misc & NM_CLOSENODEDAB) {
		CLOSE_OPEN_FILE(nodefile);
	}
	pthread_mutex_unlock(&nodefile_mutex);

	if (i >= LOOP_NODEDAB) {
		errormsg(WHERE, ERR_LOCK, "nodefile", cfg.node_num);
		return false;
	}

	if (cfg.node_num) {
		SAFEPRINTF(str, "%snode.log", cfg.node_dir);
		if ((logfile_fp = fopen(str, "a+b")) == NULL) {
			errormsg(WHERE, ERR_OPEN, str, 0);
			lprintf(LOG_NOTICE, "Perhaps this node is already running");
			return false;
		}

		if (filelength(fileno(logfile_fp)) > 0) {
			log(crlf);
			time_t ftime = fdate(str);
			llprintf(LOG_NOTICE, "L!",
			              "End of preexisting log entry (possible crash on %s)"
			              , timestr(ftime));
			log(crlf);
			catsyslog(true);
		}

		if (getnodedat(cfg.node_num, &thisnode, true)) {
			/* thisnode.status=0; */
			thisnode.action = 0;
			thisnode.useron = 0;
			thisnode.aux = 0;
			thisnode.misc &= (NODE_EVENT | NODE_LOCK | NODE_RRUN);
			criterrs = thisnode.errors;
			putnodedat(cfg.node_num, &thisnode);
		}

		// remove any pending node messages
		safe_snprintf(str, sizeof(str), "%smsgs/n%3.3u.msg", cfg.data_dir, cfg.node_num);
		fremove(WHERE, str);
		// Delete any stale temporary files (with potentially sensitive content)
		delfiles(cfg.temp_dir, ALLFILES);
		safe_snprintf(str, sizeof(str), "%sMSGTMP", cfg.node_dir);
		removecase(str);
		safe_snprintf(str, sizeof(str), "%sQUOTES.TXT", cfg.node_dir);
		removecase(str);
	}

	/* Reset COMMAND SHELL */

	main_csi.str = (char *)malloc(1024);
	if (main_csi.str == NULL) {
		errormsg(WHERE, ERR_ALLOC, "main_csi.str", 1024);
		return false;
	}
	memset(main_csi.str, 0, 1024);
/***/

	if (cfg.total_grps) {

		usrgrp_total = cfg.total_grps;

		if ((cursub = (int *)malloc(sizeof(int) * usrgrp_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "cursub", sizeof(int) * usrgrp_total);
			return false;
		}

		if ((usrgrp = (int *)malloc(sizeof(int) * usrgrp_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrgrp", sizeof(int) * usrgrp_total);
			return false;
		}

		if ((usrsubs = (int *)malloc(sizeof(int) * usrgrp_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrsubs", sizeof(int) * usrgrp_total);
			return false;
		}

		if ((usrsub = (int **)calloc(usrgrp_total, sizeof(int *))) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrsub", sizeof(int) * usrgrp_total);
			return false;
		}
	}
	if (cfg.total_subs) {
		if ((subscan = (subscan_t *)calloc(cfg.total_subs, sizeof(subscan_t))) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "subscan", sizeof(subscan_t) * cfg.total_subs);
			return false;
		}
	}

	for (i = l = 0; i < cfg.total_grps; i++) {
		for (j = k = 0; j < cfg.total_subs; j++)
			if (cfg.sub[j]->grp == i)
				k++;    /* k = number of subs per grp[i] */
		if (k > l)
			l = k;        /* l = the largest number of subs per grp */
	}
	if (l)
		for (i = 0; i < cfg.total_grps; i++)
			if ((usrsub[i] = (int *)malloc(sizeof(int) * l)) == NULL) {
				errormsg(WHERE, ERR_ALLOC, "usrsub[x]", sizeof(int) * l);
				return false;
			}

	if (cfg.total_libs) {

		usrlib_total = cfg.total_libs;

		if ((curdir = (int *)malloc(sizeof(int) * usrlib_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "curdir", sizeof(int) * usrlib_total);
			return false;
		}

		if ((usrlib = (int *)malloc(sizeof(int) * usrlib_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrlib", sizeof(int) * usrlib_total);
			return false;
		}

		if ((usrdirs = (int *)malloc(sizeof(uint) * usrlib_total)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrdirs", sizeof(int) * usrlib_total);
			return false;
		}

		if ((usrdir = (int **)calloc(usrlib_total, sizeof(int *))) == NULL) {
			errormsg(WHERE, ERR_ALLOC, "usrdir", sizeof(int) * usrlib_total);
			return false;
		}
	}

	for (i = l = 0; i < cfg.total_libs; i++) {
		for (j = k = 0; j < cfg.total_dirs; j++)
			if (cfg.dir[j]->lib == i)
				k++;
		if (k > l)
			l = k;        /* l = largest number of dirs in a lib */
	}
	if (l) {
		l++;    /* for temp dir */
		for (i = 0; i < cfg.total_libs; i++)
			if ((usrdir[i] = (int *)malloc(sizeof(int) * l)) == NULL) {
				errormsg(WHERE, ERR_ALLOC, "usrdir[x]", sizeof(int) * l);
				return false;
			}
	}

#ifdef USE_CRYPTLIB
	pthread_mutex_init(&ssh_mutex, NULL);
	ssh_mutex_created = true;
	ssh_active = CreateEvent(nullptr, true, false, nullptr);
#endif
	pthread_mutex_init(&input_thread_mutex, NULL);
	input_thread_mutex_created = true;

	update_terminal(this);
	reset_logon_vars();

	online = ON_REMOTE;

	return true;
}

//****************************************************************************
sbbs_t::~sbbs_t()
{
	int i;

	useron.number = 0;

#ifdef _DEBUG
	lprintf(LOG_DEBUG, "destructor begin");
#endif

//	if(!cfg.node_num)
//		rmdir(cfg.temp_dir);

	if (client_socket_dup != INVALID_SOCKET && client_socket_dup != client_socket)
		closesocket(client_socket_dup); /* close duplicate handle */

	if (cfg.node_num > 0)
		node_inbuf[cfg.node_num - 1] = NULL;
	if (!input_thread_running)
		RingBufDispose(&inbuf);
	if (!output_thread_running && !passthru_thread_running)
		RingBufDispose(&outbuf);

	if (telnet_ack_event != NULL)
		CloseEvent(telnet_ack_event);

	/* Close all open files */
	pthread_mutex_lock(&nodefile_mutex);
	CLOSE_OPEN_FILE(nodefile);
	pthread_mutex_unlock(&nodefile_mutex);
	pthread_mutex_destroy(&nodefile_mutex);
	CLOSE_OPEN_FILE(node_ext);
	FCLOSE_OPEN_FILE(logfile_fp);

	if (syspage_semfile[0])
		fremove(WHERE, syspage_semfile);

	/********************************/
	/* Free allocated class members */
	/********************************/

	/* nodefile_mutex is confined to getnodedat/putnodedat — not held here. */
	// coverity[SLEEP:SUPPRESS]
	js_cleanup();

	/* Reset text.dat */

	for (i = 0; i < TOTAL_TEXT; i++)
		if (text[i] != text_sav[i]) {
			if (text[i] != nulstr)
				free(text[i]);
		}

	/* Global command shell vars */

	freevars(&main_csi);
	clearvars(&main_csi);
	FREE_AND_NULL(main_csi.str);
	FREE_AND_NULL(main_csi.cs);

	for (i = 0; i < global_str_vars && global_str_var != NULL; i++)
		FREE_AND_NULL(global_str_var[i]);

	FREE_AND_NULL(global_str_var);
	FREE_AND_NULL(global_str_var_name);
	global_str_vars = 0;

	FREE_AND_NULL(global_int_var);
	FREE_AND_NULL(global_int_var_name);
	global_int_vars = 0;

	/* Sub-board variables */
	for (i = 0; i < usrgrp_total && usrsub != NULL; i++)
		FREE_AND_NULL(usrsub[i]);

	FREE_AND_NULL(cursub);
	FREE_AND_NULL(usrgrp);
	FREE_AND_NULL(usrsubs);
	FREE_AND_NULL(usrsub);
	FREE_AND_NULL(subscan);

	/* File Directory variables */
	for (i = 0; i < usrlib_total && usrdir != NULL; i++)
		FREE_AND_NULL(usrdir[i]);

	FREE_AND_NULL(curdir);
	FREE_AND_NULL(usrlib);
	FREE_AND_NULL(usrdirs);
	FREE_AND_NULL(usrdir);

	FREE_AND_NULL(qwknode);
	total_qwknodes = 0;

	listFree(&smb_list);

	free(mod_callstack); // Don't need to free the strings themselves since they're all const ptrs

#ifdef USE_CRYPTLIB
	if (ssh_mutex_created && pthread_mutex_destroy(&ssh_mutex))
		lprintf(LOG_CRIT, "!!!! Failed to destroy ssh_mutex, system is unstable");
	if (ssh_active) {
		SetEvent(ssh_active);
		while ((!CloseEvent(ssh_active)) && errno == EBUSY)
			mswait(1);
		ssh_active = nullptr;
	}
#endif
	if (input_thread_mutex_created && pthread_mutex_destroy(&input_thread_mutex))
		lprintf(LOG_CRIT, "!!!! Failed to destroy input_thread_mutex, system is unstable");

#if 0 && defined(_WIN32) && defined(_DEBUG) && defined(_MSC_VER)
	if (!_CrtCheckMemory())
		lprintf(LOG_ERR, "!MEMORY ERRORS REPORTED IN DATA/DEBUG.LOG!");
#endif

	if (term) {
		delete term;
		term = nullptr;
	}

#ifdef _DEBUG
	lprintf(LOG_DEBUG, "destructor end");
#endif
}

int sbbs_t::smb_stack(smb_t* smb, bool push)
{
	if (push) {
		if (smb == NULL || !SMB_IS_OPEN(smb))
			return SMB_SUCCESS;   /* Msg base not open, do nothing */
		if (listPushNodeData(&smb_list, smb, sizeof(*smb)) == NULL)
			return SMB_FAILURE;
		return SMB_SUCCESS;
	}
	/* pop */
	smb_t* data = (smb_t*)listPopNode(&smb_list);
	if (data == NULL)    /* Nothing on the stack, so do nothing */
		return SMB_SUCCESS;

	*smb = *data;
	free(data);
	return SMB_SUCCESS;
}

/****************************************************************************/
/* Network open function. Opens all files DENYALL and retries LOOP_NOPEN    */
/* number of times if the attempted file is already open or denying access  */
/* for some other reason.													*/
/* All files are opened in BINARY mode, unless O_TEXT access bit is set.	*/
/****************************************************************************/
int sbbs_t::nopen(char *str, int access)
{
	int file, share, count = 0;

	if (access & O_DENYNONE) {
		share = SH_DENYNO;
		access &= ~O_DENYNONE;
	}
	else if (access == O_RDONLY)
		share = SH_DENYWR;
	else
		share = SH_DENYRW;
	if (!(access & O_TEXT))
		access |= O_BINARY;
	while (((file = sopen(str, access, share, DEFFILEMODE)) == -1)
	       && FILE_RETRY_ERRNO(errno) && count++ < LOOP_NOPEN)
		FILE_RETRY_DELAY(count, LOCK_RETRY_DELAY);
	if (count > (LOOP_NOPEN / 2) && count <= LOOP_NOPEN) {
		llprintf(LOG_WARNING, "!!", "NOPEN COLLISION - File: \"%s\" Count: %d"
		            , str, count);
	}
	if (file == -1 && (errno == EACCES || errno == EAGAIN || errno == EDEADLOCK)) {
		llprintf(LOG_WARNING, "!!", "NOPEN ACCESS DENIED - File: \"%s\" errno: %d"
		            , str, errno);
		bputs("\7\r\nNOPEN: ACCESS DENIED\r\n\7");
	}
	return file;
}

void sbbs_t::spymsg(const char* msg)
{
	char str[512];

	if (cfg.node_num < 1)
		return;

	SAFEPRINTF4(str, "\r\n\r\n*** Spy Message ***\r\nNode %d: %s [%s]\r\n*** %s ***\r\n\r\n"
	            , cfg.node_num, client_name, client_ipaddr, msg);
	if (startup->node_spybuf != NULL
	    && startup->node_spybuf[cfg.node_num - 1] != NULL) {
		RingBufWrite(startup->node_spybuf[cfg.node_num - 1], (uchar*)str, strlen(str));
	}

	if (cfg.node_num && spy_socket[cfg.node_num - 1] != INVALID_SOCKET)
		if (sendsocket(spy_socket[cfg.node_num - 1], str, strlen(str)) < 1)
			errprintf(LOG_ERR, WHERE, "Node %d ERROR %d writing to spy socket", cfg.node_num, SOCKET_ERRNO);
#ifdef __unix__
	if (cfg.node_num && uspy_socket[cfg.node_num - 1] != INVALID_SOCKET)
		if (sendsocket(uspy_socket[cfg.node_num - 1], str, strlen(str)) < 1)
			errprintf(LOG_ERR, WHERE, "Node %d ERROR %d writing to spy socket", cfg.node_num, SOCKET_ERRNO);
#endif
}

/****************************************************************************/
/* Moves or copies a file from one dir to another                           */
/* both 'src' and 'dest' must contain full path and filename                */
/* returns 0 if successful, -1 if error                                     */
/****************************************************************************/
int sbbs_t::mv(const char* path, const char* dest, bool copy)
{
	char src[MAX_PATH + 1];

	if (!stricmp(path, dest))     /* source and destination are the same! */
		return 0;

	SAFECOPY(src, path);
	if (!fexistcase(src)) {
		bprintf("\r\n\7MV ERROR: Source doesn't exist\r\n'%s'\r\n"
		        , src);
		return -1;
	}
	if (!copy && fexist(dest)) {
		bprintf("\r\n\7MV ERROR: Destination already exists\r\n'%s'\r\n"
		        , dest);
		return -1;
	}
	if (!copy && rename(src, dest) == 0)
		return 0;
	if (!CopyFile(src, dest, /* fail if exists: */ !copy)) {
		errormsg(WHERE, "CopyFile", src, 0, dest);
		return -1;
	}
	if (!copy && remove(src) != 0) {
		errormsg(WHERE, ERR_REMOVE, src, 0);
		return -1;
	}
	return 0;
}

void sbbs_t::hangup(void)
{
	if (online) {
		term->clear_hotspots();
		lprintf(LOG_DEBUG, "disconnecting client");
		// Wait for the last of the output -- e.g. the echoed command key of a
		// fast log-off (/O), which is the only output that command produces --
		// to be *transmitted* before tearing the connection down.  Do this
		// while still online: flush_output() short-circuits on !online.
		// WaitForOutbufEmpty() alone would not be enough either; it returns in
		// the gap between output_thread's ring->linear read and its send
		// (GitLab #1157), and on SSH the ssh_session_destroy() below then
		// discards those still-untransmitted bytes.
		if (client_socket != INVALID_SOCKET)
			WaitForOutbufDrained(1000);
		online = false;   // moved from the bottom of this function on Jan-25-2009
	}
	if (client_socket_dup != INVALID_SOCKET && client_socket_dup != client_socket)
		closesocket(client_socket_dup);
	client_socket_dup = INVALID_SOCKET;

	if (client_socket != INVALID_SOCKET) {
		client_off(client_socket);
		if (ssh_mode) {
			pthread_mutex_lock(&ssh_mutex);
			ssh_session_destroy(client_socket, ssh_session, __LINE__);
			ssh_mode = false;
			pthread_mutex_unlock(&ssh_mutex);
		}
		close_socket(client_socket);
		client_socket = INVALID_SOCKET;
	}
	SetEvent(outbuf.data_event);
	SetEvent(outbuf.highwater_event);
}

int sbbs_t::incom(unsigned int timeout)
{
	uchar ch;

	if (!online)
		return NOINP;
	if (term_output_disabled)
		return NOINP;

	// If we think we may have some input, send all our output
	if (RingBufFull(&outbuf) != 0) {
		SetEvent(outbuf.highwater_event);
		SetEvent(outbuf.data_event);
	}
#if 0   /* looping version */
	while (!RingBufRead(&inbuf, &ch, 1))
		if (WaitForEvent(inbuf.data_event, timeout) != WAIT_OBJECT_0 || sys_status & SS_ABORT)
			return NOINP;
#else
	if (!RingBufRead(&inbuf, &ch, 1)) {
		if (WaitForEvent(inbuf.data_event, timeout) != WAIT_OBJECT_0)
			return NOINP;
		if (!RingBufRead(&inbuf, &ch, 1))
			return NOINP;
	}
#endif
	return ch;
}

// Steve's original implementation (in RCIOL) did not incorporate a retry
// ... so this function does not either. :-P
int sbbs_t::_outcom(uchar ch)
{
	if (!RingBufFree(&outbuf))
		return TXBOF;
	if (!RingBufWrite(&outbuf, &ch, 1))
		return TXBOF;
	return 0;
}

// This outcom version retries - copied loop from sbbs_t::outchar()
int sbbs_t::outcom(uchar ch)
{
	int i = 0;
	if (term_output_disabled)
		return 0; // "Success"
	while (_outcom(ch) != 0) {
		if (!online)
			break;
		i++;
		if (i >= outcom_max_attempts) {          /* timeout - beep flush outbuf */
			lprintf(LOG_NOTICE, "%04d %s TIMEOUT after %d attempts with %d bytes in transmit buffer (purging)"
			        , client_socket.load(), __FUNCTION__, i, RingBufFull(&outbuf));
			RingBufReInit(&outbuf);
			_outcom(BEL);
			return TXBOF;
		}
		if (sys_status & SS_SYSPAGE)
			sbbs_beep(i, OUTCOM_RETRY_DELAY);
		else
			flush_output(OUTCOM_RETRY_DELAY);
	}
	return 0;   // Success
}

int sbbs_t::putcom(const char *str, size_t len)
{
	size_t i;

	if (!len)
		len = strlen(str);
	for (i = 0; i < len && online; i++)
		if (outcom(str[i]) != 0)
			break;
	return i;
}

/* Legacy Remote I/O Control Interface:
 * This function mimics the RCIOL MS-DOS library written in 8086 assembler by Steven B. Deppe (1958-2014).
 * This function prototype shall remain the same in tribute to Steve (Ille Homine Albe).
 */
int sbbs_t::rioctl(ushort action)
{
	int mode;
	int state;

	switch (action) {
		case GVERS:     /* Get version */
			return 0x200;
		case GUART:     /* Get UART I/O address, not available */
			return 0xffff;
		case GIRQN:     /* Get IRQ number, not available */
			return (int)client_socket;
		case GBAUD:     /* Get current bit rate */
			return 0xffff;
		case RXBC:      /* Get receive buffer count */
			// ulong	cnt;
			// ioctlsocket (client_socket,FIONREAD,&cnt);
			return /* cnt+ */ RingBufFull(&inbuf);
		case RXBS:      /* Get receive buffer size */
			return inbuf.size;
		case TXBC:      /* Get transmit buffer count */
			return RingBufFull(&outbuf);
		case TXBS:      /* Get transmit buffer size */
			return outbuf.size;
		case TXBF:      /* Get transmit buffer free space */
			return RingBufFree(&outbuf);
		case IOMODE:
			mode = 0;
			if (rio_abortable)
				mode |= ABORT;
			return mode;
		case IOSTATE:
			state = 0;
			if (sys_status & SS_ABORT)
				state |= ABORT;
			return state;
		case IOFI:      /* Flush input buffer */
			RingBufReInit(&inbuf);
			break;
		case IOFO:      /* Flush output buffer */
			RingBufReInit(&outbuf);
			break;
		case IOFB:      /* Flush both buffers */
			RingBufReInit(&inbuf);
			RingBufReInit(&outbuf);
			break;
		case LFN81:
		case LFE71:
		case FIFOCTL:
			return 0;
	}

	if ((action & 0xff) == IOSM) {   /* Get/Set/Clear mode */
		if (action & ABORT)
			rio_abortable = true;
		return 0;
	}

	if ((action & 0xff) == IOCM) {   /* Get/Set/Clear mode */
		if (action & ABORT)
			rio_abortable = false;
		return 0;
	}

	if ((action & 0xff) == IOSS) {   /* Set state */
		if (action & ABORT)
			sys_status |= SS_ABORT;
		return 0;
	}

	if ((action & 0xff) == IOCS) {   /* Clear state */
		if (action & ABORT)
			clearabort();
		return 0;
	}

	return 0;
}

void sbbs_t::reset_logon_vars(void)
{
	int i;

	sys_status &= ~(SS_USERON | SS_TMPSYSOP | SS_LCHAT | SS_ABORT
	                | SS_PAUSEON | SS_PAUSEOFF | SS_EVENT | SS_NEWUSER | SS_DATE_CHANGED | SS_QWKLOGON | SS_FASTLOGON);
	cid[0] = 0;
	wordwrap[0] = 0;
	question[0] = 0;
	if (term) {
		term->row = 0;
		term->rows = startup->default_term_height;
		term->cols = startup->default_term_width;
		term->lncntr = 0;
		term->cterm_version = 0;
		term->lbuflen = 0;
		term->cur_output_rate = output_rate_unlimited;
	}
	autoterm = 0;
	timeleft_warn = 0;
	keybufbot = keybuftop = 0;
	usrgrps = usrlibs = 0;
	curgrp = curlib = 0;
	for (i = 0; i < cfg.total_libs; i++)
		curdir[i] = 0;
	for (i = 0; i < cfg.total_grps; i++)
		cursub[i] = 0;
	cur_rate = 30000;
	dte_rate = 38400;
	main_cmds = xfer_cmds = posts_read = 0;
	lastnodemsg = 0;
	lastnodemsguser[0] = 0;
}

/****************************************************************************/
/* Writes NODE.LOG at end of SYSTEM.LOG										*/
/****************************************************************************/
void sbbs_t::catsyslog(int crash)
{
	char str[MAX_PATH + 1] = "node.log";
	char *buf;
	FILE* fp;
	int i, file;
	int length;
	struct tm tm;

	if (logfile_fp == NULL) {
		SAFEPRINTF(str, "%snode.log", cfg.node_dir);
		if ((logfile_fp = fopen(str, "rb")) == NULL) {
			errormsg(WHERE, ERR_OPEN, str, O_RDONLY);
			return;
		}
	}
	length = ftell(logfile_fp);
	if (length > 0) {
		if ((buf = (char *)malloc(length)) == NULL) {
			errormsg(WHERE, ERR_ALLOC, str, length);
			return;
		}
		rewind(logfile_fp);
		if (fread(buf, 1, length, logfile_fp) != (size_t)length) {
			errormsg(WHERE, ERR_READ, "log file", length);
			free((char *)buf);
			return;
		}
		now = time(NULL);
		localtime_r(&now, &tm);
		SAFEPRINTF4(str, "%slogs/%2.2d%2.2d%2.2d.log", cfg.logs_dir, tm.tm_mon + 1, tm.tm_mday
		            , TM_YEAR(tm.tm_year));
		if ((file = nopen(str, O_WRONLY | O_APPEND | O_CREAT)) == -1) {
			errormsg(WHERE, ERR_OPEN, str, O_WRONLY | O_APPEND | O_CREAT);
			free((char *)buf);
			return;
		}
		if (write(file, buf, length) != length) {
			close(file);
			errormsg(WHERE, ERR_WRITE, str, length);
			free((char *)buf);
			return;
		}
		close(file);
		if (crash) {
			for (i = 0; i < 2; i++) {
				SAFEPRINTF(str, "%scrash.log", i ? cfg.logs_dir : cfg.node_dir);
				if ((fp = fopenlog(&cfg, str, /* shareable: */false)) == NULL) {
					errormsg(WHERE, ERR_OPEN, str, O_WRONLY | O_APPEND | O_CREAT);
					free((char *)buf);
					return;
				}
				if (fwrite(buf, sizeof(uint8_t), length, fp) != (size_t)length) {
					fcloselog(fp);
					errormsg(WHERE, ERR_WRITE, str, length);
					free((char *)buf);
					return;
				}
				fcloselog(fp);
			}
		}
		free((char *)buf);
	}

	fclose(logfile_fp);

	SAFEPRINTF(str, "%snode.log", cfg.node_dir);
	if ((logfile_fp = fopen(str, "w+b")) == NULL) /* Truncate NODE.LOG */
		errormsg(WHERE, ERR_OPEN, str, O_WRONLY | O_TRUNC);
}


void sbbs_t::logoffstats()
{
	int i;
	stats_t stats;

	if (user_is_sysop(&useron) && !(cfg.sys_misc & SM_SYSSTAT))
		return;

	if (useron.rest & UREST_QWK_NODE)   /* Don't count QWKnet nodes */
		return;

	now = time(NULL);
	if (now <= logontime) {
		lprintf(LOG_WARNING, "Logoff time (%u) <= logon time (%u): %s"
		        , (uint)now, (uint)logontime, timestr(logontime));
		return;
	}
	uint minutes_used = (uint)(now - logontime) / 60;
	if (minutes_used < 1)
		return;

	for (i = 0; i < 2; i++) {
		FILE* fp = fopen_dstats(&cfg, i ? 0 : cfg.node_num, /* for_write: */ true);
		if (fp == NULL)
			continue;
		if (!fread_dstats(fp, &stats)) {
			/* nodefile_mutex is confined to getnodedat/putnodedat (which
			 * lock+unlock atomically). No nodefile_mutex is held here. */
			// coverity[LOCK:SUPPRESS]
			errormsg(WHERE, ERR_READ, "dsts.ini", i);
		} else {
			stats.total.timeon += minutes_used;
			stats.today.timeon += minutes_used;
			if (!fwrite_dstats(fp, &stats, __FUNCTION__))
				errormsg(WHERE, ERR_WRITE, "dsts.ini", i);
		}
		fclose_dstats(fp);
	}
}


void sbbs_t::register_login()
{
	if (user_login_state >= user_logged_in)
		return;
	if (useron.pass[0]) {
		loginSuccess(startup->login_attempt_list, &client_addr);
		clearMaxConcurrentAttempt(client.addr);
		mqtt_pub_login_attempt_clear(mqtt, client.addr);
		mqtt_pub_max_concurrent_clear(mqtt, client.addr);
		listAddNodeData(&current_logins, client.addr, strlen(client.addr) + 1, cfg.node_num, LAST_NODE);
	}
#ifdef _WIN32
	if (startup->sound.login[0] && !sound_muted(&cfg))
		PlaySound(startup->sound.login, NULL, SND_ASYNC | SND_FILENAME);
#endif
	user_login_state = user_logged_in;
}

void node_thread(void* arg)
{
	char str[MAX_PATH + 1];
	int file;
	int curshell = -1;
	node_t node;
	long login_attempts;
	sbbs_t*         sbbs = (sbbs_t*) arg;

	update_clients();
	SetThreadName("sbbs/termNode");
	thread_up(true /* setuid */);

#ifdef _DEBUG
	lprintf(LOG_DEBUG, "Node %d thread started", sbbs->cfg.node_num);
#endif

	sbbs_srand();       /* Seed random number generator */

#ifdef JAVASCRIPT
	if (!(startup->options & BBS_OPT_NO_JAVASCRIPT)) {
		if ((sbbs->js_cx = sbbs->js_init(&sbbs->js_runtime, &sbbs->js_glob, "node")) == NULL) /* This must be done in the context of the node thread */
			errprintf(LOG_CRIT, WHERE, "Node %d !JavaScript Initialization FAILURE", sbbs->cfg.node_num);
	}
#endif

	if (startup->login_attempt.throttle
	    && (login_attempts = loginAttempts(startup->login_attempt_list, &sbbs->client_addr)) > 1) {
		lprintf(LOG_DEBUG, "Node %d Throttling suspicious connection from: %s (%ld login attempts)"
		        , sbbs->cfg.node_num, sbbs->client_ipaddr, login_attempts);
		for (long i = 0; i < login_attempts; ++i) {
			mswait(startup->login_attempt.throttle);
			sbbs->socket_inactive = 0;
		}
	}

	if (sbbs->answer()) {

		sbbs->register_login();

		if (sbbs->term_output_disabled) { // e.g. SFTP
			while (sbbs->online) {
				SLEEP(1000);
				sbbs->getnodedat(sbbs->cfg.node_num, &sbbs->thisnode, /* lock: */ false);
				if ((sbbs->thisnode.misc & NODE_UDAT) && !sbbs->useron_is_guest()) {
					if (sbbs->getuseron(WHERE) && sbbs->getnodedat(sbbs->cfg.node_num, &sbbs->thisnode, /* lock: */ true)) {
						sbbs->thisnode.misc &= ~NODE_UDAT;
						sbbs->putnodedat(sbbs->cfg.node_num, &sbbs->thisnode);
					}
				}
				if (sbbs->thisnode.misc & NODE_INTR) {
					sbbs->logline(LOG_NOTICE, nulstr, "Interrupted");
					sbbs->hangup();
				}
			}
		}
		else if (sbbs->sys_status & SS_QWKLOGON) {
			sbbs->getsmsg(sbbs->useron.number);
			sbbs->qwk_sec();
		} else while (sbbs->useron.number
			          && (sbbs->main_csi.misc & CS_OFFLINE_EXEC || sbbs->online)) {

				if (curshell != sbbs->useron.shell) {
					if (sbbs->useron.shell >= sbbs->cfg.total_shells)
						sbbs->useron.shell = 0;
					sbbs->menu_dir[0] = 0;
					sbbs->menu_file[0] = 0;

					if (sbbs->cfg.mods_dir[0]) {
						SAFEPRINTF2(str, "%s%s.js", sbbs->cfg.mods_dir
						            , sbbs->cfg.shell[sbbs->useron.shell]->code);
						if (fexistcase(str)) {
							if (sbbs->js_execfile(str) != 0)
								break;
							continue;
						}
					}
					SAFEPRINTF2(str, "%s%s.js", sbbs->cfg.exec_dir
					            , sbbs->cfg.shell[sbbs->useron.shell]->code);
					if (fexistcase(str)) {
						if (sbbs->js_execfile(str) != 0)
							break;
						continue;
					}
					SAFEPRINTF2(str, "%s%s.bin", sbbs->cfg.mods_dir
					            , sbbs->cfg.shell[sbbs->useron.shell]->code);
					if (sbbs->cfg.mods_dir[0] == 0 || !fexistcase(str)) {
						SAFEPRINTF2(str, "%s%s.bin", sbbs->cfg.exec_dir
						            , sbbs->cfg.shell[sbbs->useron.shell]->code);
						(void)fexistcase(str);
					}
					if ((file = sbbs->nopen(str, O_RDONLY)) == -1) {
						sbbs->errormsg(WHERE, ERR_OPEN, str, O_RDONLY);
						sbbs->hangup();
						break;
					}
					FREE_AND_NULL(sbbs->main_csi.cs);
					sbbs->freevars(&sbbs->main_csi);
					sbbs->clearvars(&sbbs->main_csi);

					sbbs->main_csi.length = (int)filelength(file);
					if (sbbs->main_csi.length < 1) {
						close(file);
						sbbs->errormsg(WHERE, ERR_LEN, str, sbbs->main_csi.length);
						sbbs->hangup();
						break;
					}

					if ((sbbs->main_csi.cs = (uchar *)malloc(sbbs->main_csi.length)) == NULL) {
						close(file);
						sbbs->errormsg(WHERE, ERR_ALLOC, str, sbbs->main_csi.length);
						sbbs->hangup();
						break;
					}

					if (read(file, sbbs->main_csi.cs, sbbs->main_csi.length)
					    != (int)sbbs->main_csi.length) {
						sbbs->errormsg(WHERE, ERR_READ, str, sbbs->main_csi.length);
						close(file);
						free(sbbs->main_csi.cs);
						sbbs->main_csi.cs = NULL;
						sbbs->hangup();
						break;
					}
					close(file);

					curshell = sbbs->useron.shell;
					sbbs->main_csi.ip = sbbs->main_csi.cs;
				}
				if (sbbs->exec(&sbbs->main_csi))
					break;
			}
		listRemoveTaggedNode(&current_logins, sbbs->cfg.node_num, /* free_data */ true);
		sbbs->logoffstats();    /* Updates both system and node dsts.ini (daily statistics) files */
	}

#ifdef _WIN32
	if (startup->sound.hangup[0] && !sound_muted(&scfg))
		PlaySound(startup->sound.hangup, NULL, SND_ASYNC | SND_FILENAME);
#endif

	sbbs->hangup(); /* closes sockets, calls client_off, and shuts down the output_thread */

	sbbs->logout();

	time_t now = time(NULL);
	SAFEPRINTF(str, "%sclient.ini", sbbs->cfg.node_dir);
	FILE* fp = fopen(str, "at");
	if (fp != NULL) {
		fprintf(fp, "user=%u\n", sbbs->useron.number);
		fprintf(fp, "name=%s\n", sbbs->useron.alias);
		fprintf(fp, "done=%u\n", (uint)now);
		fclose(fp);
	}

	if (sbbs->sys_status & SS_DAILY) { // New day, run daily events/maintenance
		sbbs->daily_maint();
	}

#if 0   /* this is handled in the event_thread now */
	// Node Daily Event
	sbbs->getnodedat(sbbs->cfg.node_num, &node, 0);
	if (node.misc & NODE_EVENT) {
		sbbs->getnodedat(sbbs->cfg.node_num, &node, 1);
		node.status = NODE_EVENT_RUNNING;
		sbbs->putnodedat(sbbs->cfg.node_num, &node);
		if (sbbs->cfg.node_daily[0]) {
			sbbs->logentry("!:", "Run node daily event");
			sbbs->external(
				sbbs->cmdstr(sbbs->cfg.node_daily, nulstr, nulstr, NULL)
				, EX_OFFLINE);
		}
		sbbs->getnodedat(sbbs->cfg.node_num, &node, 1);
		node.misc &= ~NODE_EVENT;
		sbbs->putnodedat(sbbs->cfg.node_num, &node);
	}
#endif

	// Wait for all node threads to terminate
	if (sbbs->input_thread_running || sbbs->output_thread_running
	    || sbbs->passthru_thread_running
	    ) {
		lprintf(LOG_INFO, "Node %d Waiting for %s to terminate..."
		        , sbbs->cfg.node_num
		        , (sbbs->input_thread_running && sbbs->output_thread_running) ?
		        "I/O threads" : sbbs->input_thread_running
		        ? "input thread" : "output thread");
		time_t start = time(NULL);
		while (sbbs->input_thread_running
		       || sbbs->output_thread_running
		       || sbbs->passthru_thread_running
		       ) {
			if (time(NULL) - start > TIMEOUT_THREAD_WAIT) {
				lprintf(LOG_NOTICE, "Node %d !TIMEOUT waiting for %s to terminate"
				        , sbbs->cfg.node_num
				        , (sbbs->input_thread_running && sbbs->output_thread_running) ?
				        "I/O threads"
				    : sbbs->input_thread_running
				        ? "input thread" : "output thread");
				break;
			}
			mswait(100);
		}
	}

	if (sbbs->user_login_state > sbbs->user_not_logged_in)
		sbbs->catsyslog(/* Crash: */ false);
	else {
		rewind(sbbs->logfile_fp);
		if (chsize(fileno(sbbs->logfile_fp), 0) != 0)
			sbbs->errormsg(WHERE, "truncating", "logfile", 0);
	}

	// Retry the node status reset: giving up here strands the node record
	// in-use (e.g. when node.dab is briefly locked or inaccessible during
	// an exclusive event), later "corrected" on connect - see issue #1178
	bool node_status_reset = false;
	for (int attempt = 1; attempt <= 3 && !node_status_reset; attempt++) {
		if (attempt > 1)
			SLEEP(500);
		if (!sbbs->getnodedat(sbbs->cfg.node_num, &node, true))
			continue;
		node_socket[sbbs->cfg.node_num - 1] = INVALID_SOCKET;
		if (node.misc & NODE_DOWN)
			node.status = NODE_OFFLINE;
		else
			node.status = NODE_WFC;
		node.misc &= ~(NODE_DOWN | NODE_INTR | NODE_MSGW | NODE_NMSG
		               | NODE_UDAT | NODE_POFF | NODE_AOFF | NODE_EXT);
		/*	node.useron=0; needed for hang-ups while in multinode chat */
		sbbs->putnodedat(sbbs->cfg.node_num, &node);
		node_status_reset = true;
	}
	if (!node_status_reset) {
		node_socket[sbbs->cfg.node_num - 1] = INVALID_SOCKET;
		lprintf(LOG_ERR, "Node %d thread terminated without resetting node status"
		        , sbbs->cfg.node_num);
	}

	{
		uint32_t remain = protected_uint32_adjust_fetch(&node_threads_running, -1);
		lprintf(LOG_INFO, "Node %d thread terminated (%u node threads remain, %u clients served)"
		        , sbbs->cfg.node_num, remain, served);
	}
	if (!sbbs->input_thread_running && !sbbs->output_thread_running)
		delete sbbs;
	else
		lprintf(LOG_WARNING, "Node %d !ORPHANED I/O THREAD(s)", sbbs->cfg.node_num);

	update_clients();
	thread_down();
}

// This version of backup() returns the size of the file backed-up
int64_t sbbs_t::backup(const char* fname, int backup_level, bool rename)
{
	int64_t bytes = flength(fname);
	if (bytes < 0)
		return 0;

	lprintf(LOG_DEBUG, "Backing-up %s (%" PRId64 " bytes)", fname, bytes);
	if (!::backup(fname, backup_level, rename))
		return false;
	return bytes;
}

void sbbs_t::daily_maint(void)
{
	char str[128];
	char uname[LEN_ALIAS + 1];
	uint i;
	uint usernum;
	uint lastusernum;
	user_t user;

	lputs(LOG_INFO, "DAILY: System maintenance begun");
	now = time(NULL);

	if (cfg.node_num) {
		if (getnodedat(cfg.node_num, &thisnode, true)) {
			thisnode.status = NODE_EVENT_RUNNING;
			putnodedat(cfg.node_num, &thisnode);
		}
	}

	if (cfg.user_backup_level && (lastusernum = lastuser(&cfg)) > 0) {
		lputs(LOG_DEBUG, "DAILY: Backing up user data...");
		int64_t bytes = backup(userdat_filename(&cfg, str, sizeof str), cfg.user_backup_level, false);
		bytes += backup(useridx_filename(&cfg, str, sizeof str), cfg.user_backup_level, false);
		lprintf(LOG_INFO, "DAILY: Backed up %s bytes of user data (%u users)"
			, byte_estimate_to_str(bytes, str, sizeof str, 1024 * 1024, 1)
			, lastusernum);
	}

	if (cfg.mail_backup_level && getmail(&cfg, 0, false, /* attr: */ 0) > 0) {
		lputs(LOG_DEBUG, "DAILY: Backing up mail data...");
		smb_t mail;
		/* nodefile_mutex is confined to getnodedat/putnodedat — not held here. */
		// coverity[SLEEP:SUPPRESS]
		int result = smb_open_sub(&cfg, &mail, INVALID_SUB);
		if (result != SMB_SUCCESS)
			errprintf(LOG_ERR, WHERE, "ERROR %d (%s) opening mail base", result, mail.last_error);
		else {
			result = smb_lock(&mail);
			if (result != SMB_SUCCESS)
				errprintf(LOG_ERR, WHERE, "ERROR %d (%s) locking mail base", result, mail.last_error);
			else {
				SAFEPRINTF(str, "%smail.shd", cfg.data_dir);
				int64_t bytes = backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.sha", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.sdt", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.sda", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.sid", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.sch", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.hash", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				SAFEPRINTF(str, "%smail.ini", cfg.data_dir);
				bytes += backup(str, cfg.mail_backup_level, false);
				lprintf(LOG_INFO, "DAILY: Backed up %s bytes of mail data (%u messages)"
					, byte_estimate_to_str(bytes, str, sizeof str, 1024 * 1024, 1)
					, mail.status.total_msgs);
				result = smb_unlock(&mail);
				if (result != SMB_SUCCESS)
					errprintf(LOG_ERR, WHERE, "ERROR %d (%s) unlocking mail base", result, mail.last_error);
			}
			smb_close(&mail);
		}
	}

	lputs(LOG_INFO, "DAILY: Checking for inactive/expired user records...");
	lastusernum = lastuser(&cfg);
	int userfile = openuserdat(&cfg, /* for_modify: */ false);
	for (usernum = 1; usernum <= lastusernum; usernum++) {
		user.number = usernum;
		if ((i = fgetuserdat(&cfg, &user, userfile)) != 0) {
			SAFEPRINTF(str, "user record %u", usernum);
			errormsg(WHERE, ERR_READ, str, i);
			continue;
		}

		/***********************************************/
		/* Fix name (name.dat and user.tab) mismatches */
		/***********************************************/
		username(&cfg, user.number, uname);
		if (user.misc & DELETED) {
			if (strcmp(uname, "DELETED USER"))
				putusername(&cfg, user.number, nulstr);
			continue;
		}

		if (strcmp(user.alias, uname))
			putusername(&cfg, user.number, user.alias);

		if (user.number == 1)
			continue;   /* skip expiration/inactivity checks for user #1 */

		if (!(user.misc & (DELETED | INACTIVE))
		    && user.expire && (uint)user.expire <= (uint)now) {
			putsmsg(user.number, text[AccountHasExpired]);
			SAFEPRINTF2(str, "DAILY: %s #%u Expired", user.alias, user.number);
			lputs(LOG_NOTICE, str);
			if (cfg.level_misc[user.level] & LEVEL_EXPTOVAL
			    && cfg.level_expireto[user.level] < 10) {
				user.flags1 = cfg.val_flags1[cfg.level_expireto[user.level]];
				user.flags2 = cfg.val_flags2[cfg.level_expireto[user.level]];
				user.flags3 = cfg.val_flags3[cfg.level_expireto[user.level]];
				user.flags4 = cfg.val_flags4[cfg.level_expireto[user.level]];
				user.exempt = cfg.val_exempt[cfg.level_expireto[user.level]];
				user.rest = cfg.val_rest[cfg.level_expireto[user.level]];
				if (cfg.val_expire[cfg.level_expireto[user.level]])
					user.expire = (time32_t)now
					              + (cfg.val_expire[cfg.level_expireto[user.level]] * 24 * 60 * 60);
				else
					user.expire = 0;
				user.level = cfg.val_level[cfg.level_expireto[user.level]];
			}
			else {
				if (cfg.level_misc[user.level] & LEVEL_EXPTOLVL)
					user.level = cfg.level_expireto[user.level];
				else
					user.level = cfg.expired_level;
				user.flags1 &= ~cfg.expired_flags1; /* expired status */
				user.flags2 &= ~cfg.expired_flags2; /* expired status */
				user.flags3 &= ~cfg.expired_flags3; /* expired status */
				user.flags4 &= ~cfg.expired_flags4; /* expired status */
				user.exempt &= ~cfg.expired_exempt;
				user.rest |= cfg.expired_rest;
				user.expire = 0;
			}
			putuserdec32(user.number, USER_LEVEL, user.level);
			putuserflags(user.number, USER_FLAGS1, user.flags1);
			putuserflags(user.number, USER_FLAGS2, user.flags2);
			putuserflags(user.number, USER_FLAGS3, user.flags3);
			putuserflags(user.number, USER_FLAGS4, user.flags4);
			putuserdatetime(user.number, USER_EXPIRE, user.expire);
			putuserflags(user.number, USER_EXEMPT, user.exempt);
			putuserflags(user.number, USER_REST, user.rest);
			useron = user;
			online = ON_LOCAL;
			exec_mod("expired user", cfg.expire_mod);
			online = false;
		}

		/***********************************************************/
		/* Auto deletion based on expiration date or days inactive */
		/***********************************************************/
		if (!(user.exempt & UEXEMPT_PERMANENT)
		    && !(user.misc & (DELETED | INACTIVE))   /* alive */
		    && (cfg.sys_autodel && (now - user.laston) / (int)(24L * 60L * 60L)
		        > cfg.sys_autodel)) {       /* Inactive too long */
			lprintf(LOG_NOTICE, "DAILY: Auto-Deleting user: %s (%s) #%u due to inactivity > %u days (pending e-mail: %d)"
			        , user.alias, user.name, user.number, cfg.sys_autodel, delallmail(user.number, MAIL_ANY));
			del_user(&cfg, &user);
		}
	}
	closeuserdat(userfile);

	lputs(LOG_INFO, "DAILY: Purging deleted/expired e-mail");
	SAFEPRINTF(smb.file, "%smail", cfg.data_dir);
	smb.retry_time = cfg.smb_retry_time;
	smb.subnum = INVALID_SUB;
	if ((i = smb_open(&smb)) != 0)
		errormsg(WHERE, ERR_OPEN, smb.file, i, smb.last_error);
	else {
		if (filelength(fileno(smb.shd_fp)) > 0) {
			if ((i = smb_lock(&smb)) != SMB_SUCCESS)
				errormsg(WHERE, ERR_LOCK, smb.file, i, smb.last_error);
			else {
				lprintf(LOG_INFO, "DAILY: Removed %d e-mail messages", delmail(0, MAIL_ALL));
				smb_unlock(&smb);
			}
		}
		smb_close(&smb);
	}

	online = ON_LOCAL;
	for (i = 0; cfg.sys_daily.cmd != nullptr && cfg.sys_daily.cmd[i] != nullptr; ++i) {
		if (cfg.sys_daily.misc[i] & EVENT_DISABLED)
			continue;
		const char* cmd = cmdstr(cfg.sys_daily.cmd[i], nulstr, nulstr, NULL, cfg.sys_daily.misc[i]);
		lprintf(LOG_INFO, "DAILY: Running system event: %s", cmd);
		int result = external(cmd, EX_OFFLINE | cfg.sys_daily.misc[i]);
		lprintf(result ? LOG_ERR : LOG_INFO, "Daily event: '%s' returned %d", cmd, result);
	}
	if (sys_status & SS_NEW_WEEK) {
		for (i = 0; cfg.sys_weekly.cmd != nullptr && cfg.sys_weekly.cmd[i] != nullptr; ++i) {
			if (cfg.sys_weekly.misc[i] & EVENT_DISABLED)
				continue;
			const char* cmd = cmdstr(cfg.sys_weekly.cmd[i], nulstr, nulstr, NULL, cfg.sys_weekly.misc[i]);
			lprintf(LOG_INFO, "DAILY: Running weekly event: %s", cmd);
			int result = external(cmd, EX_OFFLINE | cfg.sys_weekly.misc[i]);
			lprintf(result ? LOG_ERR : LOG_INFO, "Weekly event: '%s' returned %d", cmd, result);
		}
	}
	if (sys_status & SS_NEW_MONTH) {
		for (i = 0; cfg.sys_monthly.cmd != nullptr && cfg.sys_monthly.cmd[i] != nullptr; ++i) {
			if (cfg.sys_monthly.misc[i] & EVENT_DISABLED)
				continue;
			const char* cmd = cmdstr(cfg.sys_monthly.cmd[i], nulstr, nulstr, NULL, cfg.sys_monthly.misc[i]);
			lprintf(LOG_INFO, "DAILY: Running monthly event: %s", cmd);
			int result = external(cmd, EX_OFFLINE | cfg.sys_monthly.misc[i]);
			lprintf(result ? LOG_ERR : LOG_INFO, "Monthly event: '%s' returned %d", cmd, result);
		}
	}
	online = false;
	lputs(LOG_INFO, "DAILY: System maintenance ended");
	sys_status &= ~SS_DAILY;
}

const char* js_ver(void)
{
#ifdef JAVASCRIPT
	return JS_GetImplementationVersion();
#else
	return "";
#endif
}

/* Returns char string of version and revision */
const char* bbs_ver(void)
{
	static char ver[256];
	char compiler[32];

	if (ver[0] == 0) { /* uninitialized */
		DESCRIBE_COMPILER(compiler);

		safe_snprintf(ver, sizeof(ver), "%s %s%c%s  Compiled %s/%s %s with %s"
		              , TELNET_SERVER
		              , VERSION, REVISION
#ifdef _DEBUG
		              , " Debug"
#else
		              , ""
#endif
		              , git_branch, git_hash
		              , git_date, compiler
		              );
	}
	return ver;
}

/* Returns binary-coded version and revision (e.g. 0x31000 == 3.10a) */
int bbs_ver_num(void)
{
	return VERSION_HEX;
}

void bbs_terminate(void)
{
	lprintf(LOG_INFO, "BBS Server terminate");
	terminate_server = true;
}

extern bbs_startup_t bbs_startup;
static void cleanup(int code)
{
	lputs(LOG_INFO, "Terminal Server thread terminating");

	xpms_destroy(ts_set, sock_close_cb, startup);
	ts_set = NULL;

#ifdef _WINSOCKAPI_
	if (WSAInitialized && WSACleanup() != 0)
		lprintf(LOG_ERR, "!WSACleanup ERROR %d", SOCKET_ERRNO);
#endif

	free_cfg(&scfg);
	free_text(text);

	for (int i = 0; i < MAX_NODES; i++) {
		free_text(node_text[i]);
		free_cfg(&node_scfg[i]);
		memset(&node_scfg[i], 0, sizeof(node_scfg[i]));
	}

	semfile_list_free(&pause_semfiles);
	semfile_list_free(&recycle_semfiles);
	semfile_list_free(&shutdown_semfiles);
	semfile_list_free(&clear_attempts_semfiles);

	listFree(&current_logins);
	listFree(&current_connections);
	listFree(&max_concurrent_attempts);

	protected_uint32_destroy(node_threads_running);
	protected_uint32_destroy(ssh_sessions);

	thread_down();
	if (terminate_server || code) {
		lprintf(LOG_INFO, "Terminal Server thread terminated (%u clients served)", served);
		set_state(SERVER_STOPPED);
		if (startup->terminated != NULL)
			startup->terminated(startup->cbdata, code);
	}

	ip_can.reset();
	ip_silent_can.reset();
	host_can.reset();
	host_exempt.reset();

	mqtt_shutdown(&mqtt);
}

/* Per-IP tracker for "max concurrent connections" hits.
   Each node's data is a max_concurrent_attempt_t. */
typedef struct {
	char     ip[INET6_ADDRSTRLEN];
	uint32_t count;
} max_concurrent_attempt_t;

/* Increment the strike counter for host_ip. Returns the new count, or 0 on error. */
static uint32_t bumpMaxConcurrentAttempt(const char* host_ip)
{
	list_node_t*              node;
	max_concurrent_attempt_t* att;
	uint32_t                  count = 0;

	if (!listLock(&max_concurrent_attempts))
		return 0;
	for (node = max_concurrent_attempts.first; node != NULL; node = node->next) {
		att = (max_concurrent_attempt_t*)node->data;
		if (att != NULL && strcmp(att->ip, host_ip) == 0) {
			att->count++;
			count = att->count;
			break;
		}
	}
	if (node == NULL) {
		max_concurrent_attempt_t entry;
		memset(&entry, 0, sizeof(entry));
		SAFECOPY(entry.ip, host_ip);
		entry.count = 1;
		if (listPushNodeData(&max_concurrent_attempts, &entry, sizeof(entry)) != NULL)
			count = 1;
	}
	listUnlock(&max_concurrent_attempts);
	return count;
}

/* Remove the strike-counter entry for host_ip (called after the IP has been filtered). */
static void clearMaxConcurrentAttempt(const char* host_ip)
{
	list_node_t* node;

	if (!listLock(&max_concurrent_attempts))
		return;
	for (node = max_concurrent_attempts.first; node != NULL; node = node->next) {
		max_concurrent_attempt_t* att = (max_concurrent_attempt_t*)node->data;
		if (att != NULL && strcmp(att->ip, host_ip) == 0) {
			listRemoveNode(&max_concurrent_attempts, node, /* free_data: */ true);
			break;
		}
	}
	listUnlock(&max_concurrent_attempts);
}

void bbs_thread(void* arg)
{
	char host_name[256];
	char*           identity;
	char*           p;
	char str[MAX_PATH + 1];
	char logstr[256];
	union xp_sockaddr server_addr = {{0}};
	union xp_sockaddr client_addr;
	socklen_t client_addr_len;
	SOCKET client_socket = INVALID_SOCKET;
	int node_num;
	int result;
	time_t t;
	time_t start;
	time_t initialized = 0;
	node_t node;
	sbbs_t*         events = NULL;
	client_t client;
	startup = (bbs_startup_t*)arg;
	BOOL is_client = false;
#ifdef __unix__
	struct main_sock_cb_data uspy_cb[MAX_NODES] = {};
	union xp_sockaddr uspy_addr;
#endif
#ifdef USE_CRYPTLIB
	CRYPT_CONTEXT ssh_context;
#endif
	struct main_sock_cb_data telnet_cb;
	struct main_sock_cb_data ssh_cb;
	struct main_sock_cb_data rlogin_cb;
	void                        *ts_cb;

	if (startup == NULL) {
		sbbs_beep(100, 500);
		fprintf(stderr, "No startup structure passed!\n");
		return;
	}

	if (startup->size != sizeof(bbs_startup_t)) {  // verify size
		sbbs_beep(100, 500);
		sbbs_beep(300, 500);
		sbbs_beep(100, 500);
		fprintf(stderr, "Invalid startup structure!\n");
		return;
	}

	set_state(SERVER_INIT);

#ifdef _THREAD_SUID_BROKEN
	if (thread_suid_broken)
		startup->seteuid(true);
#endif

	ZERO_VAR(js_server_props);
	SAFEPRINTF3(js_server_props.version, "%s %s%c", TELNET_SERVER, VERSION, REVISION);
	js_server_props.version_detail = bbs_ver();
	js_server_props.clients = &node_threads_running;
	js_server_props.options = &startup->options;
	js_server_props.interfaces = &startup->telnet_interfaces;

	uptime = 0;
	served = 0;

	startup->recycle_now = false;
	startup->shutdown_now = false;
	terminate_server = false;

	SetThreadName("sbbs/termServer");

	do {
		/* Setup intelligent defaults */
		if (startup->telnet_port == 0)
			startup->telnet_port = IPPORT_TELNET;
		if (startup->rlogin_port == 0)
			startup->rlogin_port = 513;
#ifdef USE_CRYPTLIB
		if (startup->ssh_port == 0)
			startup->ssh_port = 22;
#endif
		if (startup->outbuf_drain_timeout == 0)
			startup->outbuf_drain_timeout = 10;
		if (startup->sem_chk_freq == 0)
			startup->sem_chk_freq = DEFAULT_SEM_CHK_FREQ;
		if (startup->temp_dir[0])
			backslash(startup->temp_dir);

		protected_uint32_init(&node_threads_running, 0);
		protected_uint32_init(&ssh_sessions, 0);

		thread_up(false /* setuid */);
		if (startup->seteuid != NULL)
			startup->seteuid(true);

		memset(text, 0, sizeof(text));
		memset(&scfg, 0, sizeof(scfg));

		lastuseron[0] = 0;

		char compiler[32];
		DESCRIBE_COMPILER(compiler);

		lprintf(LOG_INFO, "%s Version %s%c%s"
		        , TELNET_SERVER
		        , VERSION
		        , REVISION
#ifdef _DEBUG
		        , " Debug"
#else
		        , ""
#endif
		        );
		lprintf(LOG_INFO, "Compiled %s/%s %s with %s", git_branch, git_hash, git_date, compiler);

#ifdef _DEBUG
		lprintf(LOG_DEBUG, "sizeof: int=%d, long=%d, off_t=%d, time_t=%d"
		        , (int)sizeof(int), (int)sizeof(long), (int)sizeof(off_t), (int)sizeof(time_t));

		memset(str, 0xff, sizeof(str));
		snprintf(str, 2, "\x01\x02");
		if (str[1] != '\0') {
			lprintf(LOG_CRIT, "Non-terminating (unsafe) snprintf function detected (0x%02X instead of 0x00)", (uchar)str[1]);
			cleanup(1);
			return;
		}
#endif

		if (startup->first_node < 1 || startup->first_node > startup->last_node) {
			lprintf(LOG_CRIT, "!ILLEGAL node configuration (first: %d, last: %d)"
			        , startup->first_node, startup->last_node);
			cleanup(1);
			return;
		}

#ifdef __BORLANDC__
	#pragma warn -8008  /* Disable "Condition always false" warning */
	#pragma warn -8066  /* Disable "Unreachable code" warning */
#endif
		if (sizeof(node_t) != SIZEOF_NODE_T) {
			lprintf(LOG_CRIT, "!COMPILER ERROR: sizeof(node_t)=%d instead of %d"
			        , (int)sizeof(node_t), SIZEOF_NODE_T);
			cleanup(1);
			return;
		}

		if (!winsock_startup()) {
			cleanup(1);
			return;
		}

		t = time(NULL);
		lprintf(LOG_INFO, "Initializing on %.24s with options: %x"
		        , ctime_r(&t, str), startup->options);

		if (chdir(startup->ctrl_dir) != 0)
			lprintf(LOG_ERR, "!ERROR %d (%s) changing directory to: %s", errno, strerror(errno), startup->ctrl_dir);

		/* Initial configuration and load from CNF files */
		SAFECOPY(scfg.ctrl_dir, startup->ctrl_dir);
		lprintf(LOG_INFO, "Loading configuration files from %s", scfg.ctrl_dir);
		scfg.size = sizeof(scfg);
		scfg.node_num = startup->first_node;
		SAFECOPY(logstr, UNKNOWN_LOAD_ERROR);
		if (!load_cfg(&scfg, text, TOTAL_TEXT, /* prep: */ true, /* node_req: */ true, logstr, sizeof(logstr))) {
			lprintf(LOG_CRIT, "!ERROR loading configuration files: %s", logstr);
			cleanup(1);
			return;
		}
		if (logstr[0] != '\0')
			lprintf(LOG_WARNING, "!WARNING loading configuration files: %s", logstr);

		mqtt_startup(&mqtt, &scfg, (struct startup*)startup, bbs_ver(), lputs);

		// Validate the userbase file sizes
		char userdat[MAX_PATH + 1];
		userdat_filename(&scfg, userdat, sizeof userdat);
		if (fexist(userdat)) {
			lprintf(LOG_DEBUG, "Validating userbase: %s", userdat);
			off_t userdat_len = flength(userdat);
			if ((userdat_len % USER_RECORD_LINE_LEN) != 0) {
				lprintf(LOG_CRIT, "User data (%s) length (%" PRIdOFF ") not evenly-divisible by record length (%u), corruption suspected"
					, userdat, userdat_len, USER_RECORD_LINE_LEN);
				cleanup(1);
				return;
			}
			int user_count = (int)(userdat_len / USER_RECORD_LINE_LEN);
			if (user_count > USER_MAX_NUM) {
				lprintf(LOG_CRIT, "User data (%s) has %d users, corruption suspected", userdat, user_count);
				cleanup(1);
				return;
			}
			char useridx[MAX_PATH + 1];
			off_t useridx_len = flength(useridx_filename(&scfg, useridx, sizeof useridx));
			if ((useridx_len % USER_INDEX_RECORD_LEN) != 0) {
				lprintf(LOG_CRIT, "User index (%s) length (%" PRIdOFF ") not evenly-divisible by record length (%u), corruption suspected"
					, useridx, useridx_len, USER_INDEX_RECORD_LEN);
				cleanup(1);
				return;
			}
			if ((useridx_len / USER_INDEX_RECORD_LEN) != user_count) {
				lprintf(LOG_CRIT, "User index (%s) length (%" PRIdOFF ") does not match expected length (%u), corruption suspected"
					, useridx, useridx_len, user_count * USER_INDEX_RECORD_LEN);
				cleanup(1);
				return;
			}
			lprintf(LOG_INFO, "Userbase has %d total users, %d active", user_count, total_users(&scfg));
		}

		if (scfg.total_shells < 1) {
			lprintf(LOG_CRIT, "At least one command shell must be configured (e.g. in SCFG->Command Shells)");
			cleanup(1);
			return;
		}

		if ((t = checktime()) != 0) { /* Check binary time */
			lprintf(LOG_ERR, "!TIME PROBLEM (%" PRId64 ")", (int64_t)t);
		}

		if (smb_tzutc(sys_timezone(&scfg)) != xpTimeZone_local()) {
			lprintf(LOG_WARNING, "Configured time zone (%s, 0x%04hX, offset: %d) does not match system-local time zone offset: %d"
			        , smb_zonestr(scfg.sys_timezone, str), scfg.sys_timezone, smb_tzutc(scfg.sys_timezone), xpTimeZone_local());
		}
		if (startup->max_login_inactivity > 0 && startup->max_login_inactivity <= scfg.max_getkey_inactivity) {
			lprintf(LOG_WARNING, "Configured max_login_inactivity (%u) is less than or equal to max_getkey_inactivity (%u)"
			        , startup->max_login_inactivity, scfg.max_getkey_inactivity);
		}
		if (startup->max_newuser_inactivity > 0 && startup->max_newuser_inactivity <= scfg.max_getkey_inactivity) {
			lprintf(LOG_WARNING, "Configured max_newuser_inactivity (%u) is less than or equal to max_getkey_inactivity (%u)"
			        , startup->max_newuser_inactivity, scfg.max_getkey_inactivity);
		}
		if (startup->max_session_inactivity > 0 && startup->max_session_inactivity <= scfg.max_getkey_inactivity) {
			lprintf(LOG_WARNING, "Configured max_session_inactivity (%u) is less than or equal to max_getkey_inactivity (%u)"
			        , startup->max_session_inactivity, scfg.max_getkey_inactivity);
		}
		if (uptime == 0)
			uptime = time(NULL);

		if (startup->last_node > scfg.sys_nodes) {
			lprintf(LOG_NOTICE, "Specified last_node (%d) > sys_nodes (%d), auto-corrected"
			        , startup->last_node, scfg.sys_nodes);
			startup->last_node = scfg.sys_nodes;
		}

		SAFEPRINTF(str, "%u total", scfg.sys_nodes);
		mqtt_pub_strval(&mqtt, TOPIC_BBS, "node", str);

		/* Create missing directories */
		lprintf(LOG_INFO, "Verifying/creating data directories");
		make_data_dirs(&scfg);

		/* Create missing node directories */
		lprintf(LOG_INFO, "Verifying/creating node directories");
		for (int i = 0; i <= scfg.sys_nodes; i++) {
			char newfn[MAX_PATH + 1];
			char oldfn[MAX_PATH + 1];
			if (i) {
				int err;
				if ((err = md(scfg.node_path[i - 1])) != 0) {
					lprintf(LOG_CRIT, "!ERROR %d (%s) creating directory: %s", err, strerror(err), scfg.node_path[i - 1]);
					cleanup(1);
					return;
				}
			}
			// Convert old dsts.dab -> dsts.ini
			SAFEPRINTF(oldfn, "%sdsts.dab", i ? scfg.node_path[i - 1] : scfg.ctrl_dir);
			if (fexist(oldfn) && !fexist(dstats_fname(&scfg, i, newfn, sizeof(newfn)))) {
				lprintf(LOG_NOTICE, "Auto-upgrading daily statistics data file: %s", oldfn);
				stats_t stats;
				getstats(&scfg, i, &stats);
				putstats(&scfg, i, &stats);
			}
			SAFEPRINTF(oldfn, "%scsts.dab", i ? scfg.node_path[i - 1] : scfg.ctrl_dir);
			if (fexist(oldfn) && !fexist(cstats_fname(&scfg, i, newfn, sizeof(newfn)))) {
				uint record = 0;
				lprintf(LOG_NOTICE, "Auto-upgrading cumulative statistics log file: %s", oldfn);
				stats_t stats;
				FILE* out = fopen_cstats(&scfg, i, /* write: */ true);
				if (out == NULL) {
					lprintf(LOG_ERR, "!ERROR %d (%s) creating: %s", errno, strerror(errno), newfn);
					continue;
				}
				FILE* in = fopen(oldfn, "rb");
				if (in == NULL)
					lprintf(LOG_ERR, "!ERROR %d (%s) opening: %s", errno, strerror(errno), oldfn);
				else {
					ZERO_VAR(stats);
					while (!feof(in)) {
						struct {                                /* System/Node Statistics */
							uint32_t date,                      /* When last rolled-over */
							         logons,                    /* Total Logons on System */
							         timeon,                    /* Total Time on System */
							         uls,                       /* Total Uploads Today */
							         ulb,                       /* Total Upload Bytes Today */
							         dls,                       /* Total Downloads Today */
							         dlb,                       /* Total Download Bytes Today */
							         ptoday,                    /* Total Posts Today */
							         etoday,                    /* Total Emails Today */
							         ftoday;                    /* Total Feedbacks Today */
						} legacy_stats;
						if (fread(&legacy_stats, sizeof(legacy_stats), 1, in) != 1)
							break;
						record++;
						if (legacy_stats.logons > 1000
						    || legacy_stats.timeon > 10000) {
							lprintf(LOG_WARNING, "Skipped corrupted record #%u in %s", record, oldfn);
							continue;
						}
						ZERO_VAR(stats);
						stats.date         = legacy_stats.date;
						stats.today.logons = legacy_stats.logons;
						stats.today.timeon = legacy_stats.timeon;
						stats.today.uls    = legacy_stats.uls;
						stats.today.ulb    = legacy_stats.ulb;
						stats.today.dls    = legacy_stats.dls;
						stats.today.dlb    = legacy_stats.dlb;
						stats.today.posts  = legacy_stats.ptoday;
						stats.today.email  = legacy_stats.etoday;
						stats.today.fbacks = legacy_stats.ftoday;
						if (!fwrite_cstats(out, &stats)) {
							lprintf(LOG_ERR, "!WRITE ERROR, %s line %d", __FILE__, __LINE__);
							break;
						}
					}
					fclose(in);
				}
				fclose_cstats(out);
				lprintf(LOG_INFO, "Done (%u daily-statistics records converted)", record);
			}
		}

		/* Initial global node variables */
		for (int i = 0; i < MAX_NODES; i++) {
			node_inbuf[i] = NULL;
			node_socket[i] = INVALID_SOCKET;
			spy_socket[i] = INVALID_SOCKET;
#ifdef __unix__
			uspy_socket[i] = INVALID_SOCKET;
#endif
		}

		startup->node_inbuf = node_inbuf;

		/* open a socket and wait for a client */
		ts_set = xpms_create(startup->bind_retry_count, startup->bind_retry_delay, lprintf);
		if (ts_set == NULL) {
			lprintf(LOG_CRIT, "!ERROR %d creating Terminal Server socket set", SOCKET_ERRNO);
			cleanup(1);
			return;
		}
		if (!(startup->options & BBS_OPT_NO_TELNET)) {
			telnet_cb.protocol = "telnet";
			telnet_cb.startup = startup;

			/*
			 * Add interfaces
			 */
			xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->telnet_interfaces, startup->telnet_port, "Telnet Server", &terminate_server, sock_cb, startup->seteuid, &telnet_cb);
		}

		if (startup->options & BBS_OPT_ALLOW_RLOGIN) {
			/* open a socket and wait for a client */
			rlogin_cb.protocol = "rlogin";
			rlogin_cb.startup = startup;
			xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->rlogin_interfaces, startup->rlogin_port, "RLogin Server", &terminate_server, sock_cb, startup->seteuid, &rlogin_cb);
		}

#ifdef USE_CRYPTLIB
#if CRYPTLIB_VERSION < 3300 && CRYPTLIB_VERSION > 999
	#warning This version of Cryptlib is known to crash Synchronet.  Upgrade to at least version 3.3 or do not build with Cryptlib support.
#endif
		if (startup->options & BBS_OPT_ALLOW_SSH) {
			CRYPT_KEYSET ssh_keyset;

			if (!do_cryptInit(lprintf)) {
				lprintf(LOG_ERR, "SSH cryptInit failure");
				goto NO_SSH;
			}
			int i;
			/* Get the private key... first try loading it from a file... */
			SAFEPRINTF2(str, "%s%s", scfg.ctrl_dir, "cryptlib.key");
			if (cryptStatusOK(cryptKeysetOpen(&ssh_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_READONLY))) {
				if (cryptStatusError(i = cryptGetPrivateKey(ssh_keyset, &ssh_context, CRYPT_KEYID_NAME, "ssh_server", scfg.sys_pass))) {
					GCESNN(i, ssh_keyset, "getting private key");
					goto NO_SSH;
				}
			}
			else {
				/* Couldn't do that... create a new context and use the key from there... */

				if (cryptStatusError(i = cryptCreateContext(&ssh_context, CRYPT_UNUSED, CRYPT_ALGO_RSA))) {
					GCESNN(i, CRYPT_UNUSED, "creating context");
					goto NO_SSH;
				}
				if (cryptStatusError(i = cryptSetAttributeString(ssh_context, CRYPT_CTXINFO_LABEL, "ssh_server", 10))) {
					GCESNN(i, ssh_context, "setting label");
					goto NO_SSH;
				}
				if (cryptStatusError(i = cryptGenerateKey(ssh_context))) {
					GCESNN(i, ssh_context, "generating key");
					goto NO_SSH;
				}

				/* Ok, now try saving this one... use the syspass to encrypt it. */
				if (cryptStatusOK(i = cryptKeysetOpen(&ssh_keyset, CRYPT_UNUSED, CRYPT_KEYSET_FILE, str, CRYPT_KEYOPT_CREATE))) {
					if (cryptStatusError(i = cryptAddPrivateKey(ssh_keyset, ssh_context, scfg.sys_pass)))
						GCESNN(i, ssh_keyset, "adding private key");
					if (cryptStatusError(i = cryptKeysetClose(ssh_keyset)))
						GCESNN(i, ssh_keyset, "closing keyset");
				}
				else
					GCESNN(i, CRYPT_UNUSED, "creating keyset");
			}

			/* open a socket and wait for a client */
			ssh_cb.protocol = "ssh";
			ssh_cb.startup = startup;
			xpms_add_list(ts_set, PF_UNSPEC, SOCK_STREAM, 0, startup->ssh_interfaces, startup->ssh_port, "SSH Server", &terminate_server, sock_cb, startup->seteuid, &ssh_cb);
		}
NO_SSH:
#endif

		ip_can.init(&scfg, "ip");
		ip_silent_can.init(&scfg, "ip-silent");
		host_can.init(&scfg, "host");
		host_exempt.init(&scfg, strIpFilterExemptConfigFile);

		sbbs_t* sbbs = new sbbs_t(0, &server_addr, sizeof(server_addr)
		                          , "Terminal Server", ts_set->socks[0].sock, &scfg, text, NULL);
		if (sbbs->init() == false) {
			lputs(LOG_CRIT, "!BBS initialization failed");
			cleanup(1);
			return;
		}
		sbbs->outcom_max_attempts = 10;
		sbbs->output_thread_running = true;
		_beginthread(output_thread, 0, sbbs);

		if (!(startup->options & BBS_OPT_NO_EVENTS)) {
			events = new sbbs_t(0, &server_addr, sizeof(server_addr)
			                    , "BBS Events", INVALID_SOCKET, &scfg, text, NULL, true);
			if (events->init() == false) {
				lputs(LOG_CRIT, "!Events initialization failed");
				cleanup(1);
				return;
			}
			_beginthread(event_thread, 0, events);
		}

		/* Save these values in case they're changed dynamically */
		first_node = startup->first_node;
		last_node = startup->last_node;

		for (int i = first_node; i <= last_node; i++) {
			if (sbbs->getnodedat(i, &node, true)) {
				node.status = NODE_WFC;
				node.misc &= NODE_EVENT; /* Note: Turns-off NODE_RRUN flag (and others) */
				node.action = 0;
				sbbs->putnodedat(i, &node);
			}
		}

		/* Setup recycle/shutdown semaphore file lists */
		shutdown_semfiles = semfile_list_init(scfg.ctrl_dir, "shutdown", server_abbrev);
		recycle_semfiles = semfile_list_init(scfg.ctrl_dir, "recycle", server_abbrev);
		pause_semfiles = semfile_list_init(scfg.ctrl_dir, "pause", server_abbrev);
		clear_attempts_semfiles = semfile_list_init(scfg.ctrl_dir, "clear", server_abbrev);
		semfile_list_add(&recycle_semfiles, startup->ini_fname);
		strListAppendFormat(&recycle_semfiles, "%stext.dat", scfg.ctrl_dir);
		strListAppendFormat(&recycle_semfiles, "%stext.ini", scfg.ctrl_dir);
		strListAppendFormat(&recycle_semfiles, "%sattr.ini", scfg.ctrl_dir);
		if (!initialized)
			semfile_list_check(&initialized, shutdown_semfiles);
		semfile_list_check(&initialized, recycle_semfiles);
		semfile_list_check(&initialized, clear_attempts_semfiles);

		listInit(&current_logins, LINK_LIST_MUTEX);
		listInit(&current_connections, LINK_LIST_MUTEX);
		listInit(&max_concurrent_attempts, LINK_LIST_MUTEX);

#ifdef __unix__ //	unix-domain spy sockets
		for (int i = first_node; i <= last_node && !(startup->options & BBS_OPT_NO_SPY_SOCKETS); i++)  {
			if ((unsigned int)snprintf(str, sizeof(uspy_addr.un.sun_path),
			                           "%slocalspy%d.sock", startup->temp_dir, i)
			    >= sizeof(uspy_addr.un.sun_path)) {
				lprintf(LOG_ERR, "Node %d !ERROR local spy socket path \"%slocalspy%d.sock\" too long."
				        , i, startup->temp_dir, i);
				continue;
			}
			else  {
				if (xpms_add(ts_set, PF_UNIX, SOCK_STREAM, 0, str, 0, "Spy Socket", &terminate_server, sock_cb, NULL, &uspy_cb[i - 1]))
					lprintf(LOG_INFO, "Node %d local spy using socket %s", i, str);
				else
					lprintf(LOG_ERR, "Node %d !ERROR %d (%s) creating local spy socket %s"
					        , i, errno, strerror(errno), str);
			}
		}
#endif // __unix__ (unix-domain spy sockets)

		lprintf(LOG_INFO, "Terminal Server thread started for nodes %d through %d", first_node, last_node);
		mqtt_client_max(&mqtt, (last_node - first_node) + 1);

		bool recycle_pending = false;
		while (!terminate_server) {
			YIELD();
			/* check for re-run flags and recycle/shutdown sem files */
			if (!(startup->options & BBS_OPT_NO_RECYCLE)) {
				bool recycle = false;
				if ((p = semfile_list_check(&initialized, recycle_semfiles)) != NULL) {
					lprintf(LOG_INFO, "Recycle semaphore file (%s) detected"
					        , p);
					recycle = true;
				}
				if (startup->recycle_now == true) {
					lprintf(LOG_INFO, "Recycle semaphore signaled");
					startup->recycle_now = false;
					recycle = true;
				}
				if (recycle) {
					recycle_pending = true;
					/* Flag this server's nodes to re-read their configuration
					   on their next connection, so configuration changes reach
					   new logins without waiting for the terminal server to
					   drain to idle (the full server recycle below still waits
					   until all nodes are idle). */
					for (int i = first_node; i <= last_node; i++)
						set_node_rerun(&scfg, i, /* set: */ true);
				}
				if (recycle_pending
				    && protected_uint32_value(node_threads_running) == 0)
					break;
			}
			if (((p = semfile_list_check(&initialized, shutdown_semfiles)) != NULL
			     && lprintf(LOG_INFO, "Shutdown semaphore file (%s) detected"
			                , p))
			    || (startup->shutdown_now == true
			        && lprintf(LOG_INFO, "Shutdown semaphore signaled"))) {
				startup->shutdown_now = false;
				terminate_server = true;
				break;
			}
			if (((p = semfile_list_check(NULL, pause_semfiles)) != NULL
			     && lprintf(LOG_INFO, "Pause semaphore file (%s) detected"
			                , p))
			    || (startup->paused
			        && lprintf(LOG_INFO, "Pause semaphore signaled"))) {
				set_state(SERVER_PAUSED);
				SLEEP(startup->sem_chk_freq * 1000);
				continue;
			}
			/* signal caller that we've started up successfully */
			set_state(SERVER_READY);

			sbbs->online = false;
#ifdef USE_CRYPTLIB
			sbbs->ssh_mode = false;
#endif

			if (events != nullptr && events->event_thread_running) {
				time_t now = time(NULL);
				t = event_thread_tick;
				if (now > t && now - t > 60 * 60 * 2) {
					if (!event_thread_blocked) {
						if (*events->event_code != '\0')
							snprintf(str, sizeof str, " running %s", events->event_code);
						else
							*str = '\0';
						lprintf(LOG_ERR, "Event thread appears to be blocked%s since %s", str, events->timestr(t));
						event_thread_blocked = true;
					}
				} else {
					if (event_thread_blocked) {
						lprintf(LOG_NOTICE, "Event thread unblocked");
						event_thread_blocked = false;
					}
				}
			}

			/* now wait for connection */
			client_addr_len = sizeof(client_addr);
			client_socket = xpms_accept(ts_set, &client_addr
			                            , &client_addr_len, startup->sem_chk_freq * 1000, (startup->options & BBS_OPT_HAPROXY_PROTO) ? XPMS_ACCEPT_FLAG_HAPROXY : XPMS_FLAGS_NONE, &ts_cb);

			if (terminate_server) { /* terminated */
				if (client_socket != INVALID_SOCKET)
					close_socket(client_socket);
				break;
			}

			{
				char  clear_ip[INET6_ADDRSTRLEN] = {0};
				bool  do_clear = false;
				if ((p = semfile_list_check(&initialized, clear_attempts_semfiles)) != NULL) {
					semfile_first_line(p, clear_ip, sizeof(clear_ip));
					lprintf(LOG_INFO, "Clear Failed Login Attempts semaphore file (%s) detected%s%s"
					        , p, clear_ip[0] ? " for IP " : "", clear_ip);
					do_clear = true;
				}
				if (startup->clear_attempts_now) {
					if (clear_ip[0] == '\0' && mqtt.clear_attempts_ip[0] != '\0')
						SAFECOPY(clear_ip, mqtt.clear_attempts_ip);
					lprintf(LOG_INFO, "Clear Failed Login Attempts signaled%s%s"
					        , clear_ip[0] ? " for IP " : "", clear_ip);
					startup->clear_attempts_now = false;
					mqtt.clear_attempts_ip[0] = '\0';
					do_clear = true;
				}
				if (do_clear) {
					if (clear_ip[0] != '\0') {
						long removed = loginAttemptListClearAddr(startup->login_attempt_list, clear_ip);
						if (removed < 0)
							lprintf(LOG_WARNING, "Failed to clear login attempts for IP %s (invalid address?)", clear_ip);
						else
							lprintf(removed == 0 ? LOG_DEBUG : LOG_INFO
							        , "Cleared %ld login attempt(s) for IP %s", removed, clear_ip);
						clearMaxConcurrentAttempt(clear_ip);
						mqtt_pub_login_attempt_clear(&mqtt, clear_ip);
						mqtt_pub_max_concurrent_clear(&mqtt, clear_ip);
					} else {
						mqtt_clear_login_attempt_list(&mqtt, startup->login_attempt_list);
						/* max_concurrent_attempts is term-server-only, so walk it here.    */
						/* Skip the per-IP walk when MQTT isn't publishing.                 */
						listLock(&max_concurrent_attempts);
						if (mqtt.connected) {
							for (list_node_t* n = max_concurrent_attempts.first; n != NULL; n = n->next) {
								const max_concurrent_attempt_t* a = (const max_concurrent_attempt_t*)n->data;
								if (a != NULL && a->ip[0] != '\0')
									mqtt_pub_max_concurrent_clear(&mqtt, a->ip);
							}
						}
						listFreeNodes(&max_concurrent_attempts);
						listUnlock(&max_concurrent_attempts);
					}
				}
			}

			if (client_socket == INVALID_SOCKET)
				continue;

			bool rlogin = false;
#ifdef USE_CRYPTLIB
			bool ssh = false;
#endif

			is_client = false;
			if (ts_cb == &telnet_cb) {
				is_client = true;
			} else if (ts_cb == &rlogin_cb) {
				rlogin = true;
				is_client = true;
#ifdef USE_CRYPTLIB
			} else if (ts_cb == &ssh_cb) {
				ssh = true;
				is_client = true;
#endif
			} else {
#ifdef __unix__
				for (int i = first_node; i <= last_node; i++)  {
					if (&uspy_cb[i - 1] == ts_cb) {
						if (node_socket[i - 1] == INVALID_SOCKET)
							if (read(uspy_socket[i - 1], str, sizeof(str)) < 1)
								*str = '\0';
						if (!socket_check(uspy_socket[i - 1], NULL, NULL, 0)) {
							lprintf(LOG_NOTICE, "Spy socket for node %d disconnected", i);
							close_socket(uspy_socket[i - 1]);
							uspy_socket[i - 1] = INVALID_SOCKET;
						}
					}
					if (&uspy_cb[i - 1] == ts_cb) {
						BOOL already_connected = (uspy_socket[i - 1] != INVALID_SOCKET);
						SOCKET new_socket = client_socket;
						fcntl(new_socket, F_SETFL, fcntl(new_socket, F_GETFL) | O_NONBLOCK);
						if (already_connected)  {
							lprintf(LOG_ERR, "!ERROR Spy socket %s already in use", uspy_addr.un.sun_path);
							send(new_socket, "Spy socket already in use.\r\n", 27, 0);
							close_socket(new_socket);
						}
						else  {
							lprintf(LOG_INFO, "Spy socket %s (%d) connected", uspy_addr.un.sun_path, new_socket);
							uspy_socket[i - 1] = new_socket;
							SAFEPRINTF(str, "Spy connection established to node %d\r\n", i);
							send(uspy_socket[i - 1], str, strlen(str), 0);
						}
					}
				}
#else
				lprintf(LOG_ERR, "!NO SOCKETS set by select");
#endif
			}

			if (!is_client) {
				/* Do not need to close_socket(client_socket) here */
				continue;
			}

			// Count the socket:
			call_socket_open_callback(true);

			char host_ip[INET6_ADDRSTRLEN];

			inet_addrtop(&client_addr, host_ip, sizeof(host_ip));

			if (!host_exempt.listed(host_ip, nullptr) && ip_silent_can.listed(host_ip)) {
				close_socket(client_socket);
				continue;
			}


#ifdef USE_CRYPTLIB
			SAFECOPY(client.protocol, rlogin ? "RLogin":(ssh ? "SSH" : "Telnet"));
#else
			SAFECOPY(client.protocol, rlogin ? "RLogin":"Telnet");
#endif
			union xp_sockaddr local_addr;
			memset(&local_addr, 0, sizeof(local_addr));
			socklen_t addr_len = sizeof(local_addr);
			if (getsockname(client_socket, (struct sockaddr *)&local_addr, &addr_len) != 0) {
				errprintf(LOG_CRIT, WHERE, "%04d %s [%s] !ERROR %d getting local address/port of socket"
				          , client_socket, client.protocol, host_ip, SOCKET_ERRNO);
				close_socket(client_socket);
				continue;
			}
			char local_ip[INET6_ADDRSTRLEN];
			inet_addrtop(&local_addr, local_ip, sizeof local_ip);

			lprintf(LOG_INFO, "%04d %s [%s] Connection accepted on %s port %u from port %u"
			        , client_socket, client.protocol, host_ip, local_ip, inet_addrport(&local_addr), inet_addrport(&client_addr));

			if (!host_exempt.listed(host_ip, nullptr) && startup->max_concurrent_connections > 0) {
				int ip_len = strlen(host_ip) + 1;
				int connections = listCountMatches(&current_connections, host_ip, ip_len);
				int logins = listCountMatches(&current_logins, host_ip, ip_len);

				if (connections - logins >= (int)startup->max_concurrent_connections) {
					lprintf(LOG_NOTICE, "%04d %s !Maximum concurrent connections without login (%u) reached from host: %s"
					        , client_socket, client.protocol, startup->max_concurrent_connections, host_ip);
					if (startup->max_concurrent.filter_threshold > 0) {
						uint32_t strikes = bumpMaxConcurrentAttempt(host_ip);
						mqtt_pub_max_concurrent(&mqtt, host_ip, strikes);
						if (strikes >= startup->max_concurrent.filter_threshold) {
							char reason[128];
							SAFEPRINTF2(reason, "exceeding max concurrent connection limit (%u) %u times"
							            , startup->max_concurrent_connections, strikes);
							const char* can_fname = startup->max_concurrent.filter_silent
							                          ? ip_silent_can.fname : ip_can.fname;
							if (filter_ip(&scfg, client.protocol, reason, /* host: */ NULL, host_ip
							              , /* user: */ NULL, can_fname
							              , startup->max_concurrent.filter_duration)) {
								lprintf(LOG_NOTICE, "%04d %s !BLOCKING IP ADDRESS: %s in %s"
								        , client_socket, client.protocol, host_ip, can_fname);
								clearMaxConcurrentAttempt(host_ip);
								mqtt_pub_max_concurrent_clear(&mqtt, host_ip);
							}
						}
					}
					close_socket(client_socket);
					continue;
				}
			}

			// Initialize term state
			sbbs->autoterm = 0;
			if (sbbs->term)
				delete sbbs->term;
			sbbs->term = new Terminal(sbbs);
			sbbs->term->cols = startup->default_term_width;

			sbbs->client_socket = client_socket; // required for output to the user
			SAFECOPY(sbbs->client.protocol, client.protocol);
			SAFECOPY(sbbs->client_ipaddr, host_ip);
			if (!ssh)
				sbbs->online = ON_REMOTE;

			if (!host_exempt.listed(host_ip, nullptr)) {
				login_attempt_t attempted;
				uint banned = loginBanned(&scfg, startup->login_attempt_list, client_socket, /* host_name: */ NULL, startup->login_attempt, &attempted);
				if (banned) {
					char ban_duration[128];
					lprintf(LOG_NOTICE, "%04d %s [%s] !TEMPORARY BAN (%lu login attempts%s%s) - remaining: %s"
							, client_socket, client.protocol, host_ip, attempted.count - attempted.dupes
							, attempted.user[0] ? ", last: " : "", attempted.user, duration_estimate_to_vstr(banned, ban_duration, sizeof ban_duration, 1, 1));
					close_socket(client_socket);
					continue;
				}
			}
			if (inet_addrport(&local_addr) == startup->pet40_port || inet_addrport(&local_addr) == startup->pet80_port) {
				sbbs->autoterm = PETSCII;
				sbbs->term->cols = inet_addrport(&local_addr) == startup->pet40_port ? 40 : 80;
				sbbs->term_out(PETSCII_UPPERLOWER);
			}
			update_terminal(sbbs);
			// TODO: Plain text output in SSH socket
			struct trash trash;
			if (!host_exempt.listed(host_ip, host_name) && ip_can.listed(host_ip, nullptr, &trash)) {
				if (!trash.quiet) {
					char details[128];
					lprintf(LOG_NOTICE, "%04d %s [%s] !CLIENT BLOCKED in %s %s", client_socket, client.protocol, host_ip, ip_can.fname, trash_details(&trash, details, sizeof details));
					sbbs->trashcan_msg("ip");
				}
				// Drop any unsent badip.msg residue so output_thread doesn't try to send it on the closed FD
				sbbs->rioctl(IOFB);
				close_socket(client_socket);
				continue;
			}

#ifdef _WIN32
			if (startup->sound.answer[0] && !sound_muted(&scfg))
				PlaySound(startup->sound.answer, NULL, SND_ASYNC | SND_FILENAME);
#endif

			/* Purge (flush) any pending input or output data */
			sbbs->rioctl(IOFB);

			/* Do SSH stuff here */
#ifdef USE_CRYPTLIB
			if (ssh) {
				BOOL nodelay = true;
				ulong nb = 0;
				int i;

				if (cryptStatusError(i = cryptCreateSession(&sbbs->ssh_session, CRYPT_UNUSED, CRYPT_SESSION_SSH_SERVER))) {
					GCESS(i, client_socket, CRYPT_UNUSED, "creating SSH session");
					close_socket(client_socket);
					continue;
				}
				lprintf(LOG_DEBUG, "%04d SSH Cryptlib Session: %d created", client_socket, sbbs->ssh_session);
				protected_uint32_adjust(&ssh_sessions, 1);
				sbbs->ssh_mode = true;

				if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_CONNECTTIMEOUT, startup->ssh_connect_timeout)))
					sbbs->log_crypt_error_status_sock(i, "setting connect timeout");

				if (startup->options & BBS_OPT_SSH_ANYAUTH) {
					if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_SSH_OPTIONS, CRYPT_SSHOPTION_NONE_AUTH)))
						sbbs->log_crypt_error_status_sock(i, "setting none auth");
				}

				if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_PRIVATEKEY, ssh_context))) {
					sbbs->log_crypt_error_status_sock(i, "setting private key");
					SSH_END(client_socket);
					close_socket(client_socket);
					continue;
				}
				setsockopt(client_socket, IPPROTO_TCP, TCP_NODELAY, (char*)&nodelay, sizeof(nodelay));
				ioctlsocket(client_socket, FIONBIO, &nb);
				if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_SESSINFO_NETWORKSOCKET, client_socket))) {
					sbbs->log_crypt_error_status_sock(i, "setting network socket");
					SSH_END(client_socket);
					close_socket(client_socket);
					continue;
				}
				if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_READTIMEOUT, 0)))
					sbbs->log_crypt_error_status_sock(i, "setting read timeout");
				// READ = WRITE TIMEOUT HACK... REMOVE WHEN FIXED
				if (cryptStatusError(i = cryptSetAttribute(sbbs->ssh_session, CRYPT_OPTION_NET_WRITETIMEOUT, 0)))
					sbbs->log_crypt_error_status_sock(i, "setting write timeout");
#if 0
				if (cryptStatusError(err = crypt_pop_channel_data(sbbs, str, sizeof(str), &i))) {
					GCES(i, sbbs->cfg.node_num, sbbs->ssh_session, "popping data");
					i = 0;
				}
#endif
			}
#endif

			if (rlogin)
				sbbs->outcom(0); /* acknowledge RLogin per RFC 1282 */

			sbbs->bprintf("\r\n%s\r\n", VERSION_NOTICE);
			sbbs->bprintf("%s connection from: %s\r\n", client.protocol, host_ip);

			SAFECOPY(host_name, STR_NO_HOSTNAME);
			if (!(startup->options & BBS_OPT_NO_HOST_LOOKUP)) {
				sbbs->bprintf("Resolving hostname...");
				getnameinfo(&client_addr.addr, client_addr_len, host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD);
				sbbs->cp437_out(crlf);
				lprintf(LOG_INFO, "%04d %s [%s] Hostname: %s", client_socket, client.protocol, host_ip, host_name);
			}

			if (!host_exempt.listed(host_ip, host_name) && host_can.listed(host_name, nullptr, &trash)) {
				if (!trash.quiet) {
					char details[128];
					lprintf(LOG_NOTICE, "%04d %s [%s] !CLIENT BLOCKED in %s: %s %s"
							, client_socket, client.protocol, host_ip, host_name, host_can.fname, trash_details(&trash, details, sizeof details));
					sbbs->trashcan_msg("host");
				}
				// Drop any unsent badhost.msg residue so output_thread doesn't try to send it on the closed FD
				sbbs->rioctl(IOFB);
				SSH_END(client_socket);
				close_socket(client_socket);
				continue;
			}

			identity = NULL;
			if (startup->options & BBS_OPT_GET_IDENT) {
				sbbs->bprintf("Resolving identity...");
				/* ToDo: Make ident timeout configurable */
				if (identify(&client_addr, inet_addrport(&client_addr), str, sizeof(str) - 1, /* timeout: */ 1)) {
					lprintf(LOG_DEBUG, "%04d %s [%s] Ident Response: %s", client_socket, client.protocol, host_ip, str);
					identity = strrchr(str, ':');
					if (identity != NULL) {
						identity++; /* skip colon */
						SKIP_WHITESPACE(identity);
						if (*identity)
							lprintf(LOG_INFO, "%04d %s [%s] Identity: %s", client_socket, client.protocol, host_ip, identity);
					}
				}
				sbbs->cp437_out(crlf);
			}
			/* Initialize client display */
			client.size = sizeof(client);
			client.time = time32(NULL);
			SAFECOPY(client.addr, host_ip);
			SAFECOPY(client.host, host_name);
			client.port = inet_addrport(&client_addr);
			SAFECOPY(client.user, STR_UNKNOWN_USER);
			client.usernum = 0;
			client_on(client_socket, &client, false /* update */);

			for (node_num = first_node; node_num <= last_node; node_num++) {
				if (node_socket[node_num - 1] != INVALID_SOCKET)
					continue;
				/* paranoia: make sure node.status!=NODE_WFC by default */
				node.status = NODE_INVALID_STATUS;
				if (!sbbs->getnodedat(node_num, &node, true))
					continue;
				int corrected = 0;
				switch (node.status) {
					case NODE_LOGON:
					case NODE_LOGOUT:
					case NODE_NEWUSER:
					case NODE_INUSE:
					case NODE_QUIET:
						corrected = node.status;
						node.status = NODE_WFC;
						break;
				}
				if (node.status == NODE_WFC) {
					node.status = NODE_LOGON;
#ifdef USE_CRYPTLIB
					if (ssh)
						node.connection = NODE_CONNECTION_SSH;
					else
#endif
					if (rlogin)
						node.connection = NODE_CONNECTION_RLOGIN;
					else
						node.connection = NODE_CONNECTION_TELNET;

					node_socket[node_num - 1] = client_socket;
					sbbs->putnodedat(node_num, &node);
					if (corrected) // lprintf/lputs only after unlocking the node.dab
						lprintf(LOG_ERR, "%04d !Node %d status with invalid socket corrected (was %d)"
						        , client_socket, node_num, corrected);
					break;
				}
				else
					sbbs->unlocknodedat(node_num);
			}

			if (node_num > last_node) {
				lprintf(LOG_WARNING, "%04d %s [%s] !No nodes available for login.", client_socket, client.protocol, host_ip);
				SAFEPRINTF(str, "%snonodes.txt", scfg.text_dir);
				if (fexist(str))
					sbbs->printfile(str, P_NOABORT | P_MODS);
				else {
					sbbs->cp437_out("\r\nSorry, all terminal nodes are in use or otherwise unavailable.\r\n");
					sbbs->cp437_out("Please try again later.\r\n");
				}
				// Wait for nonodes.txt to be fully transmitted (not just moved
				// out of the ring buffer) before closing the socket (#1157)
				sbbs->WaitForOutbufDrained(3000);
				// Drop any unsent goodbye output so output_thread doesn't try to send it on the closed FD
				sbbs->rioctl(IOFB);
				client_off(client_socket);
				SSH_END(client_socket);
				close_socket(client_socket);
				continue;
			}

			lprintf(LOG_INFO, "%04d %s [%s] Attaching to Node %d", client_socket, client.protocol, host_ip, node_num);

			// Load the configuration files for this node, only if/when needed/updated
			scfg_t* cfg = &node_scfg[node_num - 1];
			if (cfg->size != sizeof(*cfg) || (node.misc & NODE_RRUN)) {
				sbbs->bprintf("Loading configuration...");
				cfg->size = sizeof(*cfg);
				cfg->node_num = node_num;
				SAFECOPY(cfg->ctrl_dir, startup->ctrl_dir);
				lprintf(LOG_INFO, "Node %d Loading configuration files from %s", cfg->node_num, cfg->ctrl_dir);
				SAFECOPY(logstr, UNKNOWN_LOAD_ERROR);
				if (!load_cfg(cfg, node_text[node_num - 1], TOTAL_TEXT, /* prep: */ true, /* node_req: */ true, logstr, sizeof(logstr))) {
					lprintf(LOG_WARNING, "Node %d LOAD ERROR: %s, falling back to Node %d", cfg->node_num, logstr, first_node);
					cfg->node_num = first_node;
					if (!load_cfg(cfg, node_text[node_num - 1], TOTAL_TEXT, /* prep: */ true, /* node: */ true, logstr, sizeof(logstr))) {
						lprintf(LOG_CRIT, "!ERROR loading configuration files: %s", logstr);
						sbbs->bprintf("\r\nFAILED: %s", logstr);
						client_off(client_socket);
						SSH_END(client_socket);
						close_socket(client_socket);
						if (sbbs->getnodedat(cfg->node_num, &node, true)) {
							node.status = NODE_WFC;
							node_socket[node_num - 1] = INVALID_SOCKET;
							sbbs->putnodedat(cfg->node_num, &node);
						} else
							node_socket[node_num - 1] = INVALID_SOCKET;
						continue;
					}
				}
				cfg->node_num = node_num; // correct the node number
				if (node.misc & NODE_RRUN) {
					if (sbbs->getnodedat(cfg->node_num, &node, true)) {
						node.misc &= ~NODE_RRUN;
						sbbs->putnodedat(cfg->node_num, &node);
					}
				}
				sbbs->bputs(crlf);
			}
			// Copy event last-run info from global config
			for (int e = 0; e < cfg->total_events && e < scfg.total_events; e++)
				cfg->event[e]->last = scfg.event[e]->last;

			sbbs_t* new_node = new sbbs_t(node_num, &client_addr, client_addr_len, host_name
			                              , client_socket
			                              , cfg, node_text[node_num - 1], &client);

			new_node->client = client;
#ifdef USE_CRYPTLIB
			if (ssh) {
				new_node->ssh_session = sbbs->ssh_session; // This is done again later, after NO_PASSTHRU: Why?
				new_node->ssh_mode = true;
				new_node->session_channel = sbbs->session_channel;
			}
#endif

			/* copy the IDENT response, if any */
			if (identity != NULL)
				SAFECOPY(new_node->client_ident, identity);

			if (new_node->init() == false) {
				lprintf(LOG_INFO, "%04d %s Node %d !Initialization failure"
				        , client_socket, client.protocol, new_node->cfg.node_num);
				SAFEPRINTF(str, "%snonodes.txt", scfg.text_dir);
				if (fexist(str))
					sbbs->printfile(str, P_NOABORT | P_MODS);
				else
					sbbs->cp437_out("\r\nSorry, initialization failed. Try again later.\r\n");
				// Wait for the message to be fully transmitted (not just moved
				// out of the ring buffer) before closing the socket (#1157)
				sbbs->WaitForOutbufDrained(3000);
				// Drop any unsent goodbye output so output_thread doesn't try to send it on the closed FD
				sbbs->rioctl(IOFB);
				if (sbbs->getnodedat(new_node->cfg.node_num, &node, true)) {
					node.status = NODE_WFC;
					sbbs->putnodedat(new_node->cfg.node_num, &node);
				}
				delete new_node;
				node_socket[node_num - 1] = INVALID_SOCKET;
				client_off(client_socket);
				SSH_END(client_socket);
				close_socket(client_socket);
				continue;
			}

			if (rlogin == true) {
				SAFECOPY(new_node->connection, "RLogin");
				new_node->node_connection = NODE_CONNECTION_RLOGIN;
				new_node->sys_status |= SS_RLOGIN;
				new_node->telnet_mode |= TELNET_MODE_OFF; // RLogin does not use Telnet commands
			}

			// Passthru socket creation/connection
			if (true) {
				/* TODO: IPv6? */
				SOCKET tmp_sock;
				SOCKADDR_IN tmp_addr = {0};
				socklen_t tmp_addr_len;

				/* open a socket and connect to yourself */

				tmp_sock = open_socket(PF_INET, SOCK_STREAM, "passthru");

				if (tmp_sock == INVALID_SOCKET) {
					lprintf(LOG_ERR, "Node %d !ERROR %d creating passthru listen socket"
					        , new_node->cfg.node_num, SOCKET_ERRNO);
					goto NO_PASSTHRU;
				}

				lprintf(LOG_DEBUG, "Node %d passthru listen socket %d opened"
				        , new_node->cfg.node_num, tmp_sock);

				/*****************************/
				/* Listen for incoming calls */
				/*****************************/
				memset(&tmp_addr, 0, sizeof(tmp_addr));

				tmp_addr.sin_addr.s_addr = htonl(IPv4_LOCALHOST);
				tmp_addr.sin_family = AF_INET;
				tmp_addr.sin_port   = 0;

				result = bind(tmp_sock, (struct sockaddr *)&tmp_addr, sizeof(tmp_addr));
				if (result != 0) {
					lprintf(LOG_NOTICE, "%s", BIND_FAILURE_HELP);
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}

				result = listen(tmp_sock, 1);

				if (result != 0) {
					lprintf(LOG_ERR, "Node %d !ERROR %d (%d) listening on passthru socket"
					        , new_node->cfg.node_num, result, SOCKET_ERRNO);
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}

				tmp_addr_len = sizeof(tmp_addr);
				if (getsockname(tmp_sock, (struct sockaddr *)&tmp_addr, &tmp_addr_len)) {
					lprintf(LOG_CRIT, "Node %d !ERROR %d getting passthru listener address/port of socket"
					        , new_node->cfg.node_num, SOCKET_ERRNO);
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}
				lprintf(LOG_DEBUG, "Node %d passthru socket listening on port %u"
				        , new_node->cfg.node_num, htons(tmp_addr.sin_port));

				new_node->passthru_socket = open_socket(PF_INET, SOCK_STREAM, "passthru");

				if (new_node->passthru_socket == INVALID_SOCKET) {
					lprintf(LOG_ERR, "Node %d !ERROR %d creating passthru connecting socket"
					        , new_node->cfg.node_num, SOCKET_ERRNO);
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}

				lprintf(LOG_DEBUG, "Node %d passthru connect socket %d opened"
				        , new_node->cfg.node_num, new_node->passthru_socket.load());

				result = connect(new_node->passthru_socket, (struct sockaddr *)&tmp_addr, tmp_addr_len);

				if (result != 0) {
					char tmp[16];
					lprintf(LOG_ERR, "Node %d !ERROR %d (%d) connecting to passthru socket: %s port %u"
					        , new_node->cfg.node_num, result, SOCKET_ERRNO
					        , inet_ntop(AF_INET, &tmp_addr.sin_addr.s_addr, tmp, sizeof tmp), htons(tmp_addr.sin_port));
					close_socket(new_node->passthru_socket);
					new_node->passthru_socket = INVALID_SOCKET;
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}

				/* Deliberately not xp_accept(): a native socket-door is handed this
				   descriptor by number (the %H command-line specifier), so it is the
				   one socket that must survive exec.  GitLab #1174. */
				new_node->client_socket_dup = accept(tmp_sock, (struct sockaddr *)&tmp_addr, &tmp_addr_len);

				if (new_node->client_socket_dup == INVALID_SOCKET) {
					lprintf(LOG_ERR, "Node %d !ERROR (%d) accepting on passthru socket"
					        , new_node->cfg.node_num, SOCKET_ERRNO);
					lprintf(LOG_WARNING, "Node %d !WARNING native doors which use sockets will not function"
					        , new_node->cfg.node_num);
					close_socket(new_node->passthru_socket);
					new_node->passthru_socket = INVALID_SOCKET;
					close_socket(tmp_sock);
					goto NO_PASSTHRU;
				}
				close_socket(tmp_sock);
				new_node->passthru_thread_running = true;
				_beginthread(passthru_thread, 0, new_node);

NO_PASSTHRU:
				if (ssh) {
					SAFECOPY(new_node->connection, "SSH");
					new_node->node_connection = NODE_CONNECTION_SSH;
					new_node->sys_status |= SS_SSH;
					new_node->telnet_mode |= TELNET_MODE_OFF; // SSH does not use Telnet commands
					new_node->ssh_session = sbbs->ssh_session;
					new_node->online = ON_REMOTE;
				}
				/* Wait for pending data to be sent then turn off ssh_mode for uber-output */
				// ...but not forever: a dead SSH peer never drains the outbuf, which would
				// wedge this node thread in a SLEEP(1) spin.  Bail on server shutdown or
				// after a bounded wait.  (Cf. websrvr close_session_socket.)
				{
					time_t ssh_drain_timeout = time(NULL) + 60;
					while (sbbs->output_thread_running && RingBufFull(&sbbs->outbuf)) {
						if (terminate_server || time(NULL) >= ssh_drain_timeout)
							break;
						SLEEP(1);
					}
				}
				pthread_mutex_lock(&sbbs->ssh_mutex);
				sbbs->ssh_mode = false;
				sbbs->ssh_session = 0; // Don't allow subsequent SSH connections to affect this one (!)
				pthread_mutex_unlock(&sbbs->ssh_mutex);
			}

			uint32_t client_count = protected_uint32_adjust_fetch(&node_threads_running, 1);
			new_node->input_thread_running = true;
			new_node->autoterm = sbbs->autoterm;
			update_terminal(new_node, sbbs->term);
			new_node->term->cols = sbbs->term->cols;
			new_node->input_thread = (HANDLE)_beginthread(input_thread, 0, new_node);
			new_node->output_thread_running = true;
			_beginthread(output_thread, 0, new_node);
			_beginthread(node_thread, 0, new_node);
			served++;
			if (client_count > client_highwater) {
				client_highwater = client_count;
				if (client_highwater > 1)
					lprintf(LOG_NOTICE, "Node %d New active client highwater mark: %u"
					        , node_num, client_highwater);
				mqtt_pub_uintval(&mqtt, TOPIC_SERVER, "highwater", mqtt.highwater = client_highwater);
			}
		}

		set_state(terminate_server ? SERVER_STOPPING : SERVER_RELOADING);

		// Shutdown (don't close) all open client sockets to wake and disconnect
		// the node threads: each node's own teardown (sbbs_t::hangup) owns and
		// closes its client socket descriptor.  Closing it here too would
		// double-close (EBADF log noise on *nix) and, worse, allow the fd
		// number to be reused and then closed out from under its new owner.
		for (int i = 0; i < MAX_NODES; i++)  {
			if (node_socket[i] != INVALID_SOCKET) {
				lprintf(LOG_INFO, "Shutting down node %d socket %d", i + 1, node_socket[i]);
				shutdown(node_socket[i], SHUT_RDWR);
				node_socket[i] = INVALID_SOCKET;
			}
#ifdef __unix__
			if (uspy_socket[i] != INVALID_SOCKET) {
				close_socket(uspy_socket[i]);
				uspy_socket[i] = INVALID_SOCKET;
			}
			snprintf(str, sizeof(uspy_addr.un.sun_path), "%slocalspy%d.sock", startup->temp_dir, i + 1);
			if (fexist(str))
				unlink(str);
#endif
		}

		sbbs->client_socket = INVALID_SOCKET;
		if (events != NULL)
			events->terminated = true;
		// Wake-up BBS output thread so it can terminate
		SetEvent(sbbs->outbuf.data_event);
		SetEvent(sbbs->outbuf.highwater_event);

		// Wait for all node threads to terminate
		if (protected_uint32_value(node_threads_running)) {
			lprintf(LOG_INFO, "Waiting for %d node threads to terminate...", protected_uint32_value(node_threads_running));
			start = time(NULL);
			while (protected_uint32_value(node_threads_running)) {
				if (time(NULL) - start > TIMEOUT_THREAD_WAIT) {
					lprintf(LOG_ERR, "!TIMEOUT waiting for %d node thread(s) to "
					        "terminate", protected_uint32_value(node_threads_running));
					break;
				}
				mswait(100);
			}
			lprintf(LOG_INFO, "Done waiting for node threads to terminate");
		}

		// Wait for Events thread to terminate
		if (events != NULL && events->event_thread_running) {
			lprintf(LOG_INFO, "Waiting for events thread to terminate...");
			start = time(NULL);
			while (events->event_thread_running) {
#if 0 /* the events thread can/will segfault if it continues to run and dereference sbbs->cfg */
				if (time(NULL) - start > TIMEOUT_THREAD_WAIT) {
					lprintf(LOG_ERR, "!TIMEOUT waiting for BBS events thread to "
					        "terminate");
					break;
				}
#endif
				mswait(100);
			}
			lprintf(LOG_INFO, "Done waiting for events thread to terminate");
		}

		// Wait for BBS output thread to terminate
		if (sbbs->output_thread_running) {
			lprintf(LOG_INFO, "Waiting for system output thread to terminate...");
			start = time(NULL);
			while (sbbs->output_thread_running) {
				if (time(NULL) - start > TIMEOUT_THREAD_WAIT) {
					lprintf(LOG_ERR, "!TIMEOUT waiting for BBS output thread to "
					        "terminate");
					break;
				}
				mswait(100);
			}
			lprintf(LOG_INFO, "Done waiting for system output thread to terminate");
		}

		// Wait for BBS passthru thread to terminate
		if (sbbs->passthru_thread_running) {
			lprintf(LOG_INFO, "Waiting for passthru thread to terminate...");
			start = time(NULL);
			while (sbbs->passthru_thread_running) {
				if (time(NULL) - start > TIMEOUT_THREAD_WAIT) {
					lprintf(LOG_ERR, "!TIMEOUT waiting for passthru thread to terminate");
					break;
				}
				mswait(100);
			}
			lprintf(LOG_INFO, "Done waiting for passthru threads to terminate");
		}

		// Set all nodes' status to OFFLINE
		for (int i = first_node; i <= last_node; i++) {
			if (sbbs->getnodedat(i, &node, true)) {
				node.status = NODE_OFFLINE;
				sbbs->putnodedat(i, &node);
			}
		}

		if (events != NULL) {
			if (events->event_thread_running)
				lprintf(LOG_ERR, "!Events thread still running, can't delete");
			else
				delete events;
		}

		if (sbbs->passthru_thread_running || sbbs->output_thread_running)
			lprintf(LOG_ERR, "!System I/O thread still running, can't delete");
		else
			delete sbbs;

		if (!terminate_server) {
			lprintf(LOG_INFO, "Recycling server...");
			if (startup->recycle != NULL)
				startup->recycle(startup->cbdata);
		}
		cleanup(0);

	} while (!terminate_server);

}
