/* Synchronet Virtual DOS Modem for Windows */

/****************************************************************************
 * @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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#define WIN32_LEAN_AND_MEAN
#define NOGDI
#include <windows.h>
#include <process.h>
#include <mmsystem.h>   /* SND_ASYNC */

#include "genwrap.h"
#include "findstr.h"
#include "dirwrap.h"
#include "gen_defs.h"
#include "sockwrap.h"
#include "telnet.h"
#include "ini_file.h"
#include "vdd_func.h"
#include "git_branch.h"
#include "git_hash.h"

#define TITLE "Synchronet Virtual DOS Modem for Windows"
#define VERSION "0.6"

bool              external_socket;
union xp_sockaddr addr;
SOCKET            sock = INVALID_SOCKET;
SOCKET            listening_sock = INVALID_SOCKET;
HANDLE            hangup_event = INVALID_HANDLE_VALUE; // e.g. program drops DTR
HANDLE            hungup_event = INVALID_HANDLE_VALUE; // e.g. ATH0
HANDLE            carrier_event = INVALID_HANDLE_VALUE;
HANDLE            rdslot = INVALID_HANDLE_VALUE;
HANDLE            wrslot = INVALID_HANDLE_VALUE;
str_list_t        ini;
char              ini_fname[MAX_PATH + 1];
union xp_sockaddr listening_interface;
enum {
	RAW
	, TELNET
} mode;
char* modeNames[] = {"raw", "telnet", NULL};
struct {
	uint8_t local_option[0x100];
	uint8_t remote_option[0x100];
	uint cmdlen;
	uchar cmd[64];
} telnet;
unsigned int sbbsexec_mode = SBBSEXEC_MODE_UNSPECIFIED;
#define XTRN_IO_BUF_LEN 10000
#define RING_DELAY 6000 /* US standard is 6 seconds */

struct {
	int node_num;
	uint16_t port;
	bool listen;
	bool debug;
	bool terminate_on_disconnect;
	ulong data_rate;
	bool server_echo;
	bool server_binary;
	DWORD main_loop_delay;
	DWORD socket_select_timeout;
	char client_file[MAX_PATH + 1];
	char ip_filter_file[MAX_PATH + 1];
	char ring_sound[MAX_PATH + 1];
	char connect_sound[MAX_PATH + 1];
	char disconnect_sound[MAX_PATH + 1];
	char busy_notice[INI_MAX_VALUE_LEN];
	char answer_banner[INI_MAX_VALUE_LEN];
	char connect_msg[INI_MAX_VALUE_LEN];
	enum {
		ADDRESS_FAMILY_UNSPEC
		, ADDRESS_FAMILY_INET
		, ADDRESS_FAMILY_INET6
	} address_family;
} cfg;
char* addrFamilyNames[] = {"unspec", "ipv4", "ipv6", NULL};

static void dprintf(const char *fmt, ...)
{
	char    buf[1024] = "SBBSVDM: ";
	va_list argptr;

	va_start(argptr, fmt);
	size_t  offset = strlen(buf);
	_vsnprintf(buf + offset, sizeof(buf) - offset, fmt, argptr);
	TERMINATE(buf);
	va_end(argptr);
	OutputDebugString(buf);
}

void usage(const char* progname)
{
	fprintf(stderr, "usage: %s [-opts] <program> [options]\n"
	        "opts:\n"
	        "\t-telnet   Use Telnet protocol by default\n"
	        "\t-raw      Use Raw TCP by default\n"
	        "\t-4        Use IPv4 address family by default\n"
	        "\t-6        Use IPv6 address family by default\n"
	        "\t-l[addr]  Listen for incoming TCP connections\n"
	        "\t          [on optionally-specified network interface]\n"
	        "\t-p<port>  Specify default TCP port number (decimal)\n"
	        "\t-n<node>  Specify node number\n"
	        "\t-d        Enable debug output\n"
	        "\t-h<sock>  Specify socket descriptor/handle to use (decimal)\n"
	        "\t-r<cps>   Specify maximum receive data rate (chars/second)\n"
	        "\t-c<fname> Specify alternate configuration (.ini) path/filename\n"
	        "\t-V        Display detailed version information and exit\n"
	        , progname
	        );
	exit(EXIT_SUCCESS);
}

const char* string_cmds = "D";
#define MAX_SAVES 20
struct modem {
	enum {
		INIT
		, A
		, AT
	} cmdstate;
	char cr;
	char lf;
	char bs;
	char esc;
	char save[MAX_SAVES][INI_MAX_VALUE_LEN];
	char last[INI_MAX_VALUE_LEN];
	bool echo_off;
	bool numeric_mode;
	bool offhook;
	bool online; // false means "command mode"
	bool caller_id;
	bool ringing;
	bool speaker;
	ulong ringcount;
	ulong auto_answer;
	ulong dial_wait;
	ulong guard_time;
	ulong esc_count;
	ulong ext_results;
	ulong quiet;
	uint8_t buf[128];
	size_t buflen;
};

void newcmd(struct modem* modem)
{
	modem->cmdstate = INIT;
	modem->buflen = 0;
}

ulong guard_time(struct modem* modem)
{
	return modem->guard_time * 20;
}

ulong count_esc(struct modem* modem, uint8_t* buf, size_t rd)
{
	if (modem->esc < 0 || modem->esc > 127)
		return 0;

	ulong count = 0;
	for (size_t i = 0; i < rd; i++) {
		if (buf[i] == modem->esc)
			count++;
		else
			return 0;
	}
	return count;
}

// Basic Hayes modem responses
enum modem_response {
	OK              = 0,
	CONNECT         = 1,
	RING            = 2,
	NO_CARRIER      = 3,
	ERROR           = 4,
	CONNECT_1200    = 5,
	NO_DIAL_TONE    = 6,
	BUSY            = 7,
	NO_ANSWER       = 8,
	RESERVED1       = 9,
	CONNECT_2400    = 10,
	RESERVED2       = 11,
	RESERVED3       = 12,
	CONNECT_9600    = 13
};

const char* response_str[] = {
	"OK",
	"CONNECT",
	"RING",
	"NO CARRIER",
	"ERROR",
	"CONNECT 1200",
	"NO DIAL TONE",
	"BUSY",
	"NO ANSWER",
	"RESERVED1",
	"CONNECT 2400",
	"RESERVED2",
	"RESERVED3",
	"CONNECT 9600"
};

char* verbal_response(struct modem* modem, const char* response)
{
	static char str[128];
	safe_snprintf(str, sizeof(str), "%c%c%s%c%c", modem->cr, modem->lf, response, modem->cr, modem->lf);
	return str;
}

char* numeric_response(struct modem* modem, int code)
{
	static char str[128];
	safe_snprintf(str, sizeof(str), "%u%c", code, modem->cr);
	return str;
}

char* response(struct modem* modem, enum modem_response code)
{
	if (modem->quiet)
		return "";
	if (modem->numeric_mode)
		return numeric_response(modem, code);
	return verbal_response(modem, response_str[code]);
}

char* ok(struct modem* modem)
{
	return response(modem, OK);
}

char* error(struct modem* modem)
{
	return response(modem, ERROR);
}

char* text_response(struct modem* modem, const char* text)
{
	static char str[512];

	SAFECOPY(str, verbal_response(modem, text));
	SAFECAT(str, response(modem, OK));
	return str;
}

char* connect_result(struct modem* modem)
{
	if (modem->quiet)
		return "";
	// When extended result codes are enabled and the sysop has configured a
	// custom connect message (e.g. "115200/ARQ"), report "CONNECT <message>"
	// in verbal mode. This lets DOS software that paces its output to the
	// reported modem speed (or keys on an error-correction tag) see a faster,
	// error-corrected connection. Numeric mode has no result code for an
	// arbitrary message, so it falls through to the standard CONNECT code.
	if (modem->ext_results && cfg.connect_msg[0] && !modem->numeric_mode) {
		char str[INI_MAX_VALUE_LEN + 16];
		safe_snprintf(str, sizeof(str), "CONNECT %s", cfg.connect_msg);
		return verbal_response(modem, str);
	}
	return response(modem, modem->ext_results ? CONNECT_9600 : CONNECT);
}

char* connected(struct modem* modem)
{
	if (modem->speaker && cfg.connect_sound[0])
		PlaySound(cfg.connect_sound, NULL, SND_ASYNC | SND_FILENAME);
	modem->online = true;
	modem->ringing = false;
	ResetEvent(hangup_event);
	ResetEvent(hungup_event);
	SetEvent(carrier_event);
	return connect_result(modem);
}

void disconnect(struct modem* modem)
{
	if (modem->speaker && cfg.disconnect_sound[0])
		PlaySound(cfg.disconnect_sound, NULL, SND_ASYNC | SND_FILENAME);
	modem->online = false;
	if (!external_socket) {
		shutdown(sock, SD_SEND);
		closesocket(sock);
	}
	sock = INVALID_SOCKET;
	SetEvent(hungup_event);
	SetEvent(carrier_event);
}

bool kbhit()
{
	unsigned long waiting = 0;
	if (!GetMailslotInfo(
			rdslot,         // mailslot handle
			NULL,           // address of maximum message size
			NULL,           // address of size of next message
			&waiting,       // address of number of messages
			NULL            // address of read time-out
			))
		return false;
	return waiting != 0;
}

const char* iniKeyAutoAnswer = "AutoAnswer";
const char* iniKeyEcho = "Echo";
const char* iniKeyQuiet = "Quiet";
const char* iniKeyNumeric = "Numeric";
const char* iniKeySpeaker = "Speaker";
const char* iniKeyCR = "CR";
const char* iniKeyLF = "LF";
const char* iniKeyBS = "BS";
const char* iniKeyESC = "ESC";
const char* iniKeyExtResults = "ExtResults";
const char* iniKeyDialWait = "DialWait";
const char* iniKeyGuardTime = "GuardTime";
const char* iniKeyCallerID = "CallerID";

void init(struct modem* modem)
{
	const char* section = "modem";
	memset(modem, 0, sizeof(*modem));
	modem->auto_answer = iniGetBool(ini, section, iniKeyAutoAnswer, FALSE);
	modem->echo_off = !iniGetBool(ini, section, iniKeyEcho, TRUE);
	modem->quiet = iniGetBool(ini, section, iniKeyQuiet, FALSE);
	modem->speaker = iniGetBool(ini, section, iniKeySpeaker, FALSE);
	modem->numeric_mode = iniGetBool(ini, section, iniKeyNumeric, FALSE);
	modem->caller_id = iniGetBool(ini, section, iniKeyCallerID, FALSE);
	modem->cr = (char)iniGetInteger(ini, section,  iniKeyCR, '\r');
	modem->lf = (char)iniGetInteger(ini, section, iniKeyLF, '\n');
	modem->bs = (char)iniGetInteger(ini, section, iniKeyBS, '\b');
	modem->esc = (char)iniGetInteger(ini, section, iniKeyESC, '+');
	modem->ext_results = iniGetInteger(ini, section, iniKeyExtResults, 4);
	modem->dial_wait = iniGetInteger(ini, section, iniKeyDialWait, 60);
	modem->guard_time = iniGetInteger(ini, section, iniKeyGuardTime, 50);
}

bool write_cfg(struct modem* modem)
{
	dprintf(__FUNCTION__);
	ini_style_t* style = NULL;
	const char*  section = "modem";
	iniSetBool(&ini, section, iniKeyAutoAnswer, modem->auto_answer, style);
	iniSetBool(&ini, section, iniKeyEcho, !modem->echo_off, style);
	iniSetBool(&ini, section, iniKeyQuiet, modem->quiet, style);
	iniSetBool(&ini, section, iniKeySpeaker, modem->speaker, style);
	iniSetBool(&ini, section, iniKeyNumeric, modem->numeric_mode, style);
	iniSetBool(&ini, section, iniKeyCallerID, modem->caller_id, style);
	iniSetInteger(&ini, section, iniKeyCR, modem->cr, style);
	iniSetInteger(&ini, section, iniKeyLF, modem->lf, style);
	iniSetInteger(&ini, section, iniKeyBS, modem->bs, style);
	iniSetInteger(&ini, section, iniKeyESC, modem->esc, style);
	iniSetInteger(&ini, section, iniKeyExtResults, modem->ext_results, style);
	iniSetInteger(&ini, section, iniKeyDialWait, modem->dial_wait, style);
	iniSetInteger(&ini, section, iniKeyGuardTime, modem->guard_time, style);
	bool  result = false;
	FILE* fp = iniOpenFile(ini_fname, /* for_modify: */ TRUE);
	if (fp != NULL) {
		if (iniWriteFile(fp, ini)) {
			dprintf("Wrote config to: %s", ini_fname);
			result = true;
		}
		iniCloseFile(fp);
	}
	return result;
}

bool write_save(struct modem* modem, ulong savnum)
{
	char       key[128];
	str_list_t ini;

	if (savnum >= MAX_SAVES)
		return false;
	SAFEPRINTF(key, "Save%lu", savnum);
	FILE* fp = iniOpenFile(ini_fname, /* for_modify: */ TRUE);
	if (fp == NULL)
		return false;
	ini = iniReadFile(fp);
	iniSetString(&ini, "modem", key, modem->save[savnum], /* style: */ NULL);
	bool result = iniWriteFile(fp, ini);
	iniCloseFile(fp);
	iniFreeStringList(ini);
	return result;
}

const char* protocol(enum mode mode)
{
	switch (mode) {
		case TELNET: return "Telnet";
	}
	return "Raw";
}

int address_family(BOOL for_listen)
{
	switch (cfg.address_family) {
		case ADDRESS_FAMILY_INET:   return PF_INET;
		case ADDRESS_FAMILY_INET6:  return PF_INET6;
	}
	return for_listen ? PF_INET : PF_UNSPEC;
}

int listen_address_family()
{
	return address_family(true);
}

int putcom(char* buf, size_t len)
{
	return send(sock, buf, len, /* flags: */ 0);
}

static void send_telnet_cmd(uchar cmd, uchar opt)
{
	char buf[16];

	if (cmd < TELNET_WILL) {
		dprintf("TELNET TX: %s"
		        , telnet_cmd_desc(cmd));
		sprintf(buf, "%c%c", TELNET_IAC, cmd);
		putcom(buf, 2);
	} else {
		dprintf("TELNET TX: %s %s"
		        , telnet_cmd_desc(cmd), telnet_opt_desc(opt));
		sprintf(buf, "%c%c%c", TELNET_IAC, cmd, opt);
		putcom(buf, 3);
	}
}

void request_telnet_opt(uchar cmd, uchar opt)
{
	if (cmd == TELNET_DO || cmd == TELNET_DONT) {    /* remote option */
		if (telnet.remote_option[opt] == telnet_opt_ack(cmd))
			return; /* already set in this mode, do nothing */
		telnet.remote_option[opt] = telnet_opt_ack(cmd);
	} else {    /* local option */
		if (telnet.local_option[opt] == telnet_opt_ack(cmd))
			return; /* already set in this mode, do nothing */
		telnet.local_option[opt] = telnet_opt_ack(cmd);
	}
	send_telnet_cmd(cmd, opt);
}

BYTE* telnet_interpret(BYTE* inbuf, size_t inlen, BYTE* outbuf, size_t *outlen)
{
	BYTE   command;
	BYTE   option;
	BYTE*  first_cr = NULL;
	BYTE*  first_int = NULL;
	size_t i;

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

	first_int = (BYTE*)memchr(inbuf, TELNET_IAC, inlen);
	if (telnet.remote_option[TELNET_BINARY_TX] != TELNET_WILL) {
		first_cr = (BYTE*)memchr(inbuf, '\r', inlen);
		if (first_cr) {
			if (first_int == NULL || first_cr < first_int)
				first_int = first_cr;
		}
	}

	if (telnet.cmdlen == 0 && first_int == NULL) {
		*outlen = inlen;
		return inbuf;  /* no interpretation needed */
	}

	if (telnet.cmdlen == 0 /* If we haven't returned and telnet.cmdlen==0 then first_int is not NULL */) {
		*outlen = first_int - inbuf;
		memcpy(outbuf, inbuf, *outlen);
	} else
		*outlen = 0;

	for (i = *outlen; i < inlen; i++) {
		if (telnet.remote_option[TELNET_BINARY_TX] != TELNET_WILL) {
			if (telnet.cmdlen == 1 && telnet.cmd[0] == '\r') {
				outbuf[(*outlen)++] = '\r';
				if (inbuf[i] != 0 && inbuf[i] != TELNET_IAC)
					outbuf[(*outlen)++] = inbuf[i];
				telnet.cmdlen = 0;
				if (inbuf[i] != TELNET_IAC)
					continue;
			}
			if (inbuf[i] == '\r' && telnet.cmdlen == 0) {
				telnet.cmd[telnet.cmdlen++] = '\r';
				continue;
			}
		}

		if (inbuf[i] == TELNET_IAC && telnet.cmdlen == 1) { /* escaped 255 */
			telnet.cmdlen = 0;
			outbuf[(*outlen)++] = TELNET_IAC;
			continue;
		}
		if (inbuf[i] == TELNET_IAC || telnet.cmdlen) {

			if (telnet.cmdlen < sizeof(telnet.cmd))
				telnet.cmd[telnet.cmdlen++] = inbuf[i];

			command = telnet.cmd[1];
			option  = telnet.cmd[2];

			if (telnet.cmdlen >= 2 && command == TELNET_SB) {
				if (inbuf[i] == TELNET_SE
				    && telnet.cmd[telnet.cmdlen - 2] == TELNET_IAC) {
					telnet.cmdlen = 0;
				}
			}
			else if (telnet.cmdlen == 2 && inbuf[i] < TELNET_WILL) {
				telnet.cmdlen = 0;
			}
			else if (telnet.cmdlen >= 3) { /* telnet option negotiation */

				dprintf("TELNET RX: %s %s"
				        , telnet_cmd_desc(command), telnet_opt_desc(option));

				if (command == TELNET_DO || command == TELNET_DONT) {    /* local options */
					if (telnet.local_option[option] != command) {
						switch (option) {
							case TELNET_BINARY_TX:
							case TELNET_ECHO:
							case TELNET_TERM_TYPE:
							case TELNET_SUP_GA:
							case TELNET_NEGOTIATE_WINDOW_SIZE:
								telnet.local_option[option] = command;
								send_telnet_cmd(telnet_opt_ack(command), option);
								break;
							default: /* unsupported local options */
								if (command == TELNET_DO) /* NAK */
									send_telnet_cmd(telnet_opt_nak(command), option);
								break;
						}
					}
				} else { /* WILL/WONT (remote options) */
					if (telnet.remote_option[option] != command) {

						switch (option) {
							case TELNET_BINARY_TX:
							case TELNET_ECHO:
							case TELNET_TERM_TYPE:
							case TELNET_SUP_GA:
							case TELNET_NEGOTIATE_WINDOW_SIZE:
								telnet.remote_option[option] = command;
								send_telnet_cmd(telnet_opt_ack(command), option);
								break;
							default: /* unsupported remote options */
								if (command == TELNET_WILL) /* NAK */
									send_telnet_cmd(telnet_opt_nak(command), option);
								break;
						}
					}
				}

				telnet.cmdlen = 0;

			}
		} else
			outbuf[(*outlen)++] = inbuf[i];
	}
	return outbuf;
}

void setsockopts(SOCKET sock)
{
	char      error[256] = "";
	dprintf("Setting socket options");
	if (iniGetSocketOptions(ini, "sockopts", sock, error, sizeof(error)) != 0)
		dprintf("!ERROR %s", error);
	int       value = 0;
	socklen_t len = sizeof(value);
	if (getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&value, &len) == 0)
		dprintf("Socket send buffer length: %d bytes", value);
	value = 0;
	len = sizeof(value);
	if (getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&value, &len) == 0)
		dprintf("Socket receive buffer length: %d bytes", value);
	value = 0;
	len = sizeof(value);
	if (getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&value, &len) == 0)
		dprintf("Socket TCP_NODELAY: %d", value);
}

// Significant portions copies from syncterm/conn.c
char* dial(struct modem* modem, const char* number)
{
	struct addrinfo  hints;
	struct addrinfo *res = NULL;
	char             host[128];
	char             portnum[16];
	char             value[INI_MAX_VALUE_LEN];
	uint16_t         port = cfg.port;

	dprintf("dial(%s)", number);
	if (stricmp(number, "L") == 0)
		number = modem->last;
	else {
		const char* p = iniGetString(ini, "alias", number, NULL, value);
		if (p != NULL)
			number = p;
		else if (toupper(*number) == 'S' && IS_DIGIT(number[1])) {
			char* p;
			ulong val = strtoul(number + 1, &p, 10);
			if (val < MAX_SAVES && *p == '\0')
				number = modem->save[val];
		}
		SAFECOPY(modem->last, number);
	}
	if (strncmp(number, "raw:", 4) == 0) {
		mode = RAW;
		number += 4;
	}
	else if (strncmp(number, "telnet:", 7) == 0) {
		mode = TELNET;
		number += 7;
	}
	SKIP_WHITESPACE(number);
	SAFECOPY(host, number);
	char* p = strrchr(host, ':');
	char* b = strrchr(host, ']');
	if (p != NULL && p > b) {
		port = (uint16_t)strtol(p + 1, NULL, 10);
		*p = 0;
	}
	dprintf("Connecting to port %hu at host '%s' via %s", port, host, protocol(mode));
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = address_family(/* for listen: */ false);
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	dprintf("line %d: calling getaddrinfo", __LINE__);
	SAFEPRINTF(portnum, "%hu", port);
	int result = getaddrinfo(host, portnum, &hints, &res);
	if (result != 0) {
		dprintf("getaddrinfo(%s, %s) [family=%d] returned %d", host, portnum, hints.ai_family, result);
		return response(modem, NO_ANSWER);
	}

	int              nonblock;
	struct addrinfo *cur;
	for (cur = res; cur && sock == INVALID_SOCKET; cur = cur->ai_next) {
		if (sock == INVALID_SOCKET) {
			sock = socket(cur->ai_family, cur->ai_socktype, cur->ai_protocol);
			if (sock == INVALID_SOCKET) {
				dprintf("Error %ld creating socket", WSAGetLastError());
				return response(modem, NO_DIAL_TONE);
			}
			/* Set to non-blocking for the connect */
			nonblock = -1;
			ioctlsocket(sock, FIONBIO, &nonblock);
		}

		dprintf("line %d: calling connect", __LINE__);
		if (connect(sock, cur->ai_addr, cur->ai_addrlen)) {
			switch (ERROR_VALUE) {
				case EINPROGRESS:
				case EINTR:
				case EAGAIN:
				case EWOULDBLOCK:
					for (; sock != INVALID_SOCKET;) {
						if (socket_writable(sock, 1000)) {
							if (socket_recvdone(sock, 0)) {
								closesocket(sock);
								sock = INVALID_SOCKET;
								continue;
							}
							else {
								goto connected;
							}
						}
						else {
							if (kbhit()) {
								dprintf("line %d: kbhit", __LINE__);
								closesocket(sock);
								sock = INVALID_SOCKET;
								return response(modem, NO_CARRIER);
							}
						}
					}

connected:
					break;
				default:
					closesocket(sock);
					sock = INVALID_SOCKET;
					continue;
			}
		}
	}
	if (sock == INVALID_SOCKET) {
		dprintf("line %d: invalid hostname?", __LINE__);
		return response(modem, NO_ANSWER);
	}

	freeaddrinfo(res);
	res = NULL;
	nonblock = 0;
	ioctlsocket(sock, FIONBIO, &nonblock);
	setsockopts(sock);
	if (socket_recvdone(sock, 0)) {
		dprintf("line %d: socket_recvdone", __LINE__);
		closesocket(sock);
		sock = INVALID_SOCKET;
		return response(modem, NO_CARRIER);
	}

	dprintf("line %d: connected!", __LINE__);
	int keepalives = TRUE;
	setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepalives, sizeof(keepalives));
	ZERO_VAR(telnet);
	return connected(modem);
}

void init_telnet_options()
{
	if (mode == TELNET) {
		ZERO_VAR(telnet);
		if (cfg.server_echo) {
			/* Disable Telnet Terminal Echo */
			request_telnet_opt(TELNET_WILL, TELNET_ECHO);
		}
		if (cfg.server_binary) {
			request_telnet_opt(TELNET_DO, TELNET_BINARY_TX);
			request_telnet_opt(TELNET_WILL, TELNET_BINARY_TX);
		} else {
			request_telnet_opt(TELNET_DONT, TELNET_BINARY_TX);
			request_telnet_opt(TELNET_WONT, TELNET_BINARY_TX);
		}
		/* Will suppress Go Ahead */
		request_telnet_opt(TELNET_WILL, TELNET_SUP_GA);
	}
}

char* answer(struct modem* modem)
{
	if (listening_sock == INVALID_SOCKET)
		return response(modem, NO_DIAL_TONE);

	if (sock == INVALID_SOCKET)
		return response(modem, NO_CARRIER);

	setsockopts(sock);
	if (cfg.client_file[0]) {
		FILE* fp = fopen(cfg.client_file, "wt");
		if (fp == NULL)
			dprintf("!Error %d creating '%s'", errno, cfg.client_file);
		else {
			char tmp[256];
			fprintf(fp, "sock=%d\n", sock);
			fprintf(fp, "addr=%s\n", inet_addrtop(&addr, tmp, sizeof(tmp)));
			fprintf(fp, "port=%u\n", inet_addrport(&addr));
			fprintf(fp, "prot=%s\n", protocol(mode));
			fclose(fp);
		}
	}
	init_telnet_options();
	putcom(cfg.answer_banner, strlen(cfg.answer_banner));
	return connected(modem);
}

bool read_ini(const char* ini_fname);

char* atmodem_exec(struct modem* modem)
{
	static char respbuf[128];
	char*       resp = ok(modem);
	modem->buf[modem->buflen] = '\0';
	for (char* p = modem->buf; *p != '\0';) {
		char ch = toupper(*p);
		p++;
		if (ch == '&') {
			ch = toupper(*p);
			ulong val = strtoul(p + 1, &p, 10);
			switch (ch) {
				case 'W':
					resp = write_cfg(modem) ? ok(modem) : error(modem);
					break;
				case 'Z':
					if (val >= MAX_SAVES)
						return error(modem);
					if (*p == '=') {
						p++;
						if (stricmp(p, "L") == 0)
							p = modem->last;
						SAFECOPY(modem->save[val], p);
						return write_save(modem, val) ? ok(modem) : error(modem);
					}
					if (*p == '?' || stricmp(p, "L?") == 0) {
						if (stricmp(p, "L?") == 0)
							p = modem->last;
						else
							p = modem->save[val];
						safe_snprintf(respbuf, sizeof(respbuf), "%c%s%c%c%s"
						              , modem->lf, p, modem->cr, modem->lf, ok(modem));
						return respbuf;
					}
			}
			continue;
		}
		// Caller ID control
		if (ch == '#' || ch == '+') {
			if (ch == '+' && toupper(*p) == 'V')
				p++;
			if (stricmp(p, "CID?") == 0) {
				safe_snprintf(respbuf, sizeof(respbuf), "%c%u%c%c%s"
				              , modem->lf, modem->caller_id, modem->cr, modem->lf, ok(modem));
				return respbuf;
			}
			if (stricmp(p, "CID=?") == 0) {
				safe_snprintf(respbuf, sizeof(respbuf), "%c0,1%c%c%s"
				              , modem->lf, modem->cr, modem->lf, ok(modem));
				return respbuf;
			}
			if (strnicmp(p, "CID=", 4) == 0) {
				modem->caller_id = strtoul(p + 4, &p, 10);
				continue;
			}
			return error(modem);
		}
		// Numeric argument commands
		ulong val = 0;
		if (strchr(string_cmds, ch) == NULL)
			val = strtoul(p, &p, 10);
		switch (ch) {
			case 'A':
				return answer(modem);
			case 'D':
				if (sock != INVALID_SOCKET) {
					dprintf("Can't dial: Already connected");
					return error(modem);
				}
				if (*p == 'T' /* tone */ || *p == 'P' /* pulse */)
					p++;
				return dial(modem, p);
			case 'E':
				modem->echo_off = !val;
				break;
			case 'H':
				modem->offhook = val;
				modem->ringing = false;
				if (!modem->offhook) {
					if (sock != INVALID_SOCKET) {
						disconnect(modem);
					}
				}
				break;
			case 'I':
				switch (val) {
					case 0:
						safe_snprintf(respbuf, sizeof(respbuf)
						              , TITLE " v" VERSION " Copyright %s Rob Swindell\r\n%s/%s"
						              , &__DATE__[7]
						              , GIT_BRANCH
						              , GIT_HASH
						              );
						return text_response(modem, respbuf);
					case 1:
						return text_response(modem, ini_fname);
					default:
						return error(modem);
				}
				return respbuf;
			case 'L': /* Speaker volume */
				break;
			case 'M':
				modem->speaker = val;
				break;
			case 'O':
				if (sock == INVALID_SOCKET)
					return error(modem);
				modem->online = true;
				return connect_result(modem);
				break;
			case 'V':
				modem->numeric_mode = !val;
				resp = ok(modem); // Use the new verbal/numeric mode in response (ala USRobotics)
				break;
			case 'Q':
				modem->quiet = val;
				resp = ok(modem); // Use the new quiet/verbose mode in response (ala USRobotics)
				break;
			case 'S':
				if (*p == '=') {
					ulong sreg = val;
					ulong val = strtoul(p + 1, &p, 10);
					dprintf("S%lu = %lu", sreg, val);
					switch (sreg) {
						case 0:
							if (val && listening_sock == INVALID_SOCKET) {
								dprintf("Can't enable auto-answer when not in listening mode");
								return error(modem);
							}
							modem->auto_answer = val;
							break;
						case 1:
							modem->ringcount = val;
							break;
						case 2:
							modem->esc = (char)val;
							break;
						case 3:
							modem->cr = (char)val;
							break;
						case 4:
							modem->lf = (char)val;
							break;
						case 5:
							modem->bs = (char)val;
							break;
						case 7:
							modem->dial_wait = val;
							break;
						case 12:
							modem->guard_time = val;
							break;
					}
				} else if (*p == '?') {
					switch (val) {
						case 0:
							val = modem->auto_answer;
							break;
						case 1:
							val = modem->ringcount;
							break;
						case 2:
							val = modem->esc;
							break;
						case 3:
							val = modem->cr;
							break;
						case 4:
							val = modem->lf;
							break;
						case 5:
							val = modem->bs;
							break;
						case 7:
							val = modem->dial_wait;
							break;
						case 12:
							val = modem->guard_time;
							break;
						default:
							val = 0;
							break;
					}
					safe_snprintf(respbuf, sizeof(respbuf), "%c%03lu%c%c%s"
					              , modem->lf, val, modem->cr, modem->lf, ok(modem));
					return respbuf;
				} else
					return error(modem);
				break;
			case 'P': /* Pulse */
				break;
			case 'T': /* Tone */
				break;
			case 'X':
				modem->ext_results = val;
				break;
			case 'Z':
				if (fexist(ini_fname)) {
					if (!read_ini(ini_fname))
						return error(modem);
				}
				init(modem);
				break;
			default:
				return error(modem);
		}
	}
	return resp;
}

char* atmodem_parsech(struct modem* modem, uint8_t ch)
{
	switch (modem->cmdstate) {
		case INIT:
			if (toupper(ch) == 'A')
				modem->cmdstate = A;
			break;
		case A:
			if (toupper(ch) == 'T')
				modem->cmdstate = AT;
			else
				newcmd(modem);
			break;
		case AT:
			if (ch == modem->cr) {
				char* retval = atmodem_exec(modem);
				newcmd(modem);
				return retval;
			} else if (ch == modem->bs) {
				if (modem->buflen)
					modem->buflen--;
			} else {
				if (modem->buflen >= sizeof(modem->buf))
					return error(modem);
				if (ch != ' ')
					modem->buf[modem->buflen++] = ch;
			}
			break;
	}
	return NULL;
}

char* atmodem_parse(struct modem* modem, uint8_t* buf, size_t len)
{
	for (size_t i = 0; i < len; i++) {
		char* resp = atmodem_parsech(modem, buf[i]);
		if (resp != NULL)
			return resp;
	}
	return NULL;
}

BOOL vdd_write(HANDLE* slot, uint8_t* buf, size_t buflen)
{
	if (*slot == INVALID_HANDLE_VALUE) {
		char path[MAX_PATH + 1];
		SAFEPRINTF(path, "\\\\.\\mailslot\\sbbsexec\\wr%d", cfg.node_num);
		*slot = CreateFile(path
		                   , GENERIC_WRITE
		                   , FILE_SHARE_READ
		                   , NULL
		                   , OPEN_EXISTING
		                   , FILE_ATTRIBUTE_NORMAL
		                   , (HANDLE) NULL);
		if (*slot == INVALID_HANDLE_VALUE) {
			dprintf("!ERROR %u (%s) opening '%s'", GetLastError(), strerror(GetLastError()), path);
			return FALSE;
		}
	}
	DWORD wr = 0;
	BOOL  result = WriteFile(*slot, buf, buflen, &wr, /* LPOVERLAPPED */ NULL);
	if (wr != buflen)
		dprintf("WriteFile wrote %ld instead of %ld", wr, buflen);
	return result;
}

BOOL vdd_writestr(HANDLE* slot, char* str)
{
	return vdd_write(slot, str, strlen(str));
}

void listen_thread(void* arg)
{
	struct modem* modem = (struct modem*)arg;

	for (;;) {
		fd_set         fds = {0};
		FD_SET(listening_sock, &fds);
		struct timeval tv = { 1, 0 };
		if (select(/* ignored: */ 0, &fds, NULL, NULL, &tv) == 1) {
			union xp_sockaddr newaddr;
			socklen_t         addrlen = sizeof(newaddr);
			SOCKET            newsock = accept(listening_sock, (SOCKADDR*)&newaddr, &addrlen);
			if (newsock == INVALID_SOCKET)
				continue;
			char              ipaddr[256];
			inet_addrtop(&newaddr, ipaddr, sizeof(ipaddr));
			dprintf("Connection accepted from TCP port %hu at %s"
			        , inet_addrport(&newaddr), ipaddr);
			if (findstr(ipaddr, cfg.ip_filter_file)) {
				dprintf("IP address blocked: %s", ipaddr);
				shutdown(newsock, SD_SEND);
				closesocket(newsock);
				continue;
			}
			if (sock != INVALID_SOCKET) {    // In-use
				send(newsock, cfg.busy_notice, strlen(cfg.busy_notice), /* flags: */ 0);
				shutdown(newsock, SD_SEND);
				closesocket(newsock);
				continue;
			}
			sock = newsock;
			addr = newaddr;
			if (!modem->offhook && !modem->online) {
				modem->ringing = true;
				modem->ringcount = 0;
			}
		}
	}
}

bool read_ini(const char* ini_fname)
{
	FILE* fp = iniOpenFile(ini_fname, /* for_modify: */ false);
	if (fp == NULL)
		return false;

	iniFreeStringList(ini);
	ini = iniReadFile(fp);
	iniCloseFile(fp);
	mode = iniGetEnum(ini, ROOT_SECTION, "Mode", modeNames, mode);
	cfg.port = iniGetShortInt(ini, ROOT_SECTION, "Port", cfg.port);
	cfg.node_num = iniGetInteger(ini, ROOT_SECTION, "Node", cfg.node_num);
	cfg.listen = iniGetBool(ini, ROOT_SECTION, "Listen", cfg.listen);
	cfg.debug = iniGetBool(ini, ROOT_SECTION, "Debug", cfg.debug);
	cfg.server_echo = iniGetBool(ini, ROOT_SECTION, "ServerEcho", cfg.server_echo);
	cfg.server_binary = iniGetBool(ini, ROOT_SECTION, "ServerBinary", cfg.server_binary);
	cfg.data_rate = iniGetLongInt(ini, ROOT_SECTION, "Rate", cfg.data_rate);
	cfg.main_loop_delay = iniGetUInteger(ini, ROOT_SECTION, "MainLoopDelay", cfg.main_loop_delay);
	cfg.socket_select_timeout = iniGetUInteger(ini, ROOT_SECTION, "SocketSelectTimeout", cfg.socket_select_timeout);
	cfg.address_family = iniGetEnum(ini, ROOT_SECTION, "AddressFamily", addrFamilyNames, cfg.address_family);
	char        value[INI_MAX_VALUE_LEN];
	const char* p = iniGetString(ini, ROOT_SECTION, "BusyNotice", NULL, value);
	if (p != NULL)
		SAFECOPY(cfg.busy_notice, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "AnswerBanner", NULL, value)) != NULL)
		SAFECOPY(cfg.answer_banner, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "ConnectMsg", NULL, value)) != NULL)
		SAFECOPY(cfg.connect_msg, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "ClientFile", NULL, value)) != NULL)
		SAFECOPY(cfg.client_file, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "IpFilterFile", NULL, value)) != NULL)
		SAFECOPY(cfg.ip_filter_file, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "RingSound", NULL, value)) != NULL)
		SAFECOPY(cfg.ring_sound, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "ConnectSound", NULL, value)) != NULL)
		SAFECOPY(cfg.connect_sound, p);
	if ((p = iniGetString(ini, ROOT_SECTION, "DisconnectSound", NULL, value)) != NULL)
		SAFECOPY(cfg.disconnect_sound, p);
	return true;
}

int main(int argc, char** argv)
{
	int       argn = 1;
	char      tmp[256];
	char      path[MAX_PATH + 1];
	char      fullcmdline[MAX_PATH + 1];
	uint8_t   buf[XTRN_IO_BUF_LEN];
	uint8_t   telnet_buf[sizeof(buf) * 2];
	size_t    rx_buflen = sizeof(buf);
	ULONGLONG rx_delay = 0;
	WSADATA   WSAData;
	int       result;

	fprintf(stdout, TITLE " v" VERSION " Copyright %s Rob Swindell\n", &__DATE__[7]);
	if ((result = WSAStartup(MAKEWORD(1, 1), &WSAData)) == 0)
		dprintf("%s %s", WSAData.szDescription, WSAData.szSystemStatus);
	else {
		fprintf(stderr, "!WinSock startup ERROR %d", result);
		return EXIT_FAILURE;
	}

	// Default configuration values
	mode = TELNET;
	cfg.server_echo = TRUE;
	cfg.server_binary = TRUE;
	cfg.port = IPPORT_TELNET;
	cfg.address_family = ADDRESS_FAMILY_UNSPEC;
	SAFECOPY(cfg.client_file, "client.ini");
	SAFECOPY(cfg.busy_notice, "\r\nSorry, not available right now\r\n");
	SAFEPRINTF(cfg.answer_banner, "\r\n" TITLE " v" VERSION " Copyright %s Rob Swindell\r\n", &__DATE__[7]);

	ini = strListInit();
	GetModuleFileName(NULL, ini_fname, sizeof(ini_fname) - 1);
	char* ext = getfext(ini_fname);
	if (ext != NULL)
		*ext = 0;
	SAFECAT(ini_fname, ".ini");
	if (fexist(ini_fname))
		read_ini(ini_fname);

	for (; argn < argc; argn++) {
		char* arg = argv[argn];
		if (*arg != '-')
			break;
		while (*arg == '-')
			arg++;
		if (stricmp(arg, "telnet") == 0) {
			mode = TELNET;
			continue;
		}
		if (stricmp(arg, "raw") == 0) {
			mode = RAW;
			continue;
		}
		switch (*arg) {
			case '4':
				cfg.address_family = ADDRESS_FAMILY_INET;
				break;
			case '6':
				cfg.address_family = ADDRESS_FAMILY_INET6;
				break;
			case 'l':
				cfg.listen = true;
				arg++;
				if (*arg != '\0') {
					listening_interface.addr.sa_family = listen_address_family();
					if (inet_ptoaddr(arg, &listening_interface, sizeof(listening_interface)) == NULL) {
						fprintf(stderr, "!Error parsing network address: %s", arg);
						return EXIT_FAILURE;
					}
				}
				break;
			case 'n':
				cfg.node_num = atoi(arg + 1);
				break;
			case 'p':
				cfg.port = atoi(arg + 1);
				break;
			case 'c':
				SAFECOPY(ini_fname, arg + 1);
				if (!read_ini(ini_fname)) {
					fprintf(stderr, "!Error %d reading: %s\n", errno, ini_fname);
					return EXIT_FAILURE;
				}
				break;
			case 'd':
				cfg.debug = true;
				break;
			case 'h':
				sock = strtoul(arg + 1, NULL, 10);
				external_socket = true;
				break;
			case 'r':
				cfg.data_rate = strtoul(arg + 1, NULL, 10);
				break;
			case 'B':
				rx_buflen = min(strtoul(arg + 1, NULL, 10), sizeof(buf));
				break;
			case 'R':
				rx_delay = strtoul(arg + 1, NULL, 10);
				break;
			case 'I':
				sbbsexec_mode |= SBBSEXEC_MODE_DOS_IN;
				break;
			case 'O':
				sbbsexec_mode |= SBBSEXEC_MODE_DOS_OUT;
				break;
			case 'M':
				sbbsexec_mode = strtoul(arg + 1, NULL, 0);
				break;
			case 'V':
			{
				char compiler[128];
				DESCRIBE_COMPILER(compiler);
				fprintf(stdout, "%s/%s built %s %s using %s\n", GIT_BRANCH, GIT_HASH, __DATE__, __TIME__, compiler);
				return EXIT_SUCCESS;
			}
			default:
				usage(argv[0]);
				break;
		}
	}
	if (argn >= argc) {
		usage(argv[0]);
	}

	struct modem modem = {0};
	init(&modem);

	if (cfg.listen) {
		if (sock != INVALID_SOCKET)
			listening_sock = sock;
		else {
			listening_sock = socket(listen_address_family(), SOCK_STREAM, IPPROTO_IP);
			if (listening_sock == INVALID_SOCKET) {
				fprintf(stderr, "Error %ld creating socket\n", WSAGetLastError());
				return EXIT_FAILURE;
			}
		}
		listening_interface.addr.sa_family = listen_address_family();
		inet_setaddrport(&listening_interface, cfg.port);
		result = bind(listening_sock, &listening_interface.addr, xp_sockaddr_len(&listening_interface));
		if (result != 0) {
			fprintf(stderr, "Error %d binding socket\n", WSAGetLastError());
			return EXIT_FAILURE;
		}
		if (listen(listening_sock, /* backlog: */ 1) != 0) {
			fprintf(stderr, "Error %d listening on socket\n", WSAGetLastError());
			return EXIT_FAILURE;
		}
		fprintf(stderr, "Listening on TCP port %u at %s\n"
		        , inet_addrport(&listening_interface), inet_addrtop(&listening_interface, tmp, sizeof(tmp)));

		_beginthread(listen_thread, /* stack_size: */ 0, &modem);
	} else {
		if (sock != INVALID_SOCKET) { // -h option specified without -l
			setsockopts(sock);
			connected(&modem);
			init_telnet_options();
		}
	}

	const char* dropfile = "dosxtrn.env";
	FILE*       fp = fopen(dropfile, "w");
	if (fp == NULL) {
		perror(dropfile);
		return EXIT_FAILURE;
	}
	char* dospgm = argv[argn];
	for (; argn < argc; argn++) {
		fprintf(fp, "%s ", argv[argn]);
	}
	fputc('\n', fp);
	fclose(fp);

	while (1) {
		SAFEPRINTF(path, "\\\\.\\mailslot\\sbbsexec\\rd%d", cfg.node_num);
		rdslot = CreateMailslot(path
		                        , sizeof(buf) / 2 // Maximum message size (0=unlimited)
		                        , 0 // Read time-out
		                        , NULL); // Security
		if (rdslot != INVALID_HANDLE_VALUE)
			break;
		if (cfg.node_num == 0xff) {
			fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
			return EXIT_FAILURE;
		}
		++cfg.node_num;
	}

	SAFEPRINTF(path, "sbbsexec_carrier%d", cfg.node_num);
	carrier_event = CreateEvent(
		NULL    // pointer to security attributes
		, FALSE  // flag for manual-reset event
		, FALSE  // flag for initial state
		, path   // pointer to event-object name
		);
	if (carrier_event == NULL) {
		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
		return EXIT_FAILURE;
	}

	SAFEPRINTF(path, "sbbsexec_hangup%d", cfg.node_num);
	hangup_event = CreateEvent(
		NULL    // pointer to security attributes
		, FALSE  // flag for manual-reset event
		, FALSE  // flag for initial state (DTR = high)
		, path   // pointer to event-object name
		);
	if (hangup_event == NULL) {
		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
		return EXIT_FAILURE;
	}

	SAFEPRINTF(path, "sbbsexec_hungup%d", cfg.node_num);
	hungup_event = CreateEvent(
		NULL    // pointer to security attributes
		, TRUE   // flag for manual-reset event
		, !modem.online   // flag for initial state
		, path   // pointer to event-object name
		);
	if (hungup_event == NULL) {
		fprintf(stderr, "Error %ld creating '%s'\n", GetLastError(), path);
		return EXIT_FAILURE;
	}

	STARTUPINFO startup_info = {0};
	startup_info.cb = sizeof(startup_info);

	BOOL        x64 = FALSE;
	IsWow64Process(GetCurrentProcess(), &x64);
	safe_snprintf(fullcmdline, sizeof(fullcmdline), "dosxtrn.exe %s %s %u %u %s"
	              , dropfile, x64 ? "x64" : "NT", cfg.node_num, sbbsexec_mode, ini_fname);

	PROCESS_INFORMATION process_info;
	if (!CreateProcess(
			NULL,       // pointer to name of executable module
			fullcmdline, // pointer to command line string
			NULL,       // process security attributes
			NULL,       // thread security attributes
			FALSE,      // handle inheritance flag
			0,          // creation flags
			NULL,       // pointer to new environment block
			NULL,       // pointer to current directory name
			&startup_info, // pointer to STARTUPINFO
			&process_info // pointer to PROCESS_INFORMATION
			)) {
		fprintf(stderr, "Error %ld executing '%s'", GetLastError(), fullcmdline);
		return EXIT_FAILURE;
	}
	printf("Executed '%s' successfully\n", fullcmdline);

	CloseHandle(process_info.hThread);

	if (cfg.data_rate > 0) {
		rx_buflen = max(cfg.data_rate / 100, 1);
		rx_delay = (ULONGLONG) (1000 * ((double)rx_buflen / cfg.data_rate));
	}

	ULONGLONG lastring = 0;
	ULONGLONG lasttx = 0;
	ULONGLONG lastrx = 0;
	int       largest_recv = 0;

	while (WaitForSingleObject(process_info.hProcess, cfg.main_loop_delay) != WAIT_OBJECT_0) {
		ULONGLONG now = xp_timer64();
		if (modem.online) {
			fd_set         fds = {0};
			FD_SET(sock, &fds);
			struct timeval tv = { 0, cfg.socket_select_timeout * 1000 };
			result = select(/* ignored: */ 0, &fds, NULL, NULL, &tv);
			if (result != 0) {
				if (result == SOCKET_ERROR)
					dprintf("select returned SOCKET_ERROR (%d) at %llu", WSAGetLastError(), now);
				else
					dprintf("select returned %d at %llu", result, now);
			}
			if (now - lastrx >= rx_delay &&  result == 1) {
				int rd = recv(sock, buf, rx_buflen, /* flags: */ 0);
				dprintf("recv returned %d", rd);
				if (rd <= 0) {
					int error = WSAGetLastError();
					if (rd == 0 || error == WSAECONNRESET || error == WSAECONNABORTED) {
						dprintf("Connection reset (error %d) detected on socket %ld", error, sock);
						disconnect(&modem);
						vdd_writestr(&wrslot, response(&modem, NO_CARRIER));
						continue;
					}
					dprintf("Socket error %ld on recv", error);
					continue;
				}
				if (rd > largest_recv)
					largest_recv = rd;
				uint8_t* p = buf;
				size_t   len = rd;
				if (mode == TELNET) {
					p = telnet_interpret(buf, rd, telnet_buf, &len);
				}
				vdd_write(&wrslot, p, len);
				lastrx = now;
			}
			if (WaitForSingleObject(hangup_event, 0) == WAIT_OBJECT_0) {
				dprintf("hangup_event signaled at %llu", now);
				disconnect(&modem);
				vdd_writestr(&wrslot, response(&modem, NO_CARRIER));
			}
		} else {
			if (modem.ringing) {
				if (modem.ringcount < 1)
					dprintf("Incoming connection");
				if (now - lastring > RING_DELAY) {
					dprintf("RING");
					if (modem.speaker && cfg.ring_sound[0])
						PlaySound(cfg.ring_sound, NULL, SND_ASYNC | SND_FILENAME);
					vdd_writestr(&wrslot, response(&modem, RING));
					lastring = now;
					modem.ringcount++;
					if (modem.ringcount == 1 && modem.caller_id) {
						char str[256];
						SAFEPRINTF(str, "NMBR = %s", inet_addrtop(&addr, tmp, sizeof(tmp)));
						vdd_writestr(&wrslot, verbal_response(&modem, str));
					}
					if (modem.auto_answer > 0 && modem.ringcount >= modem.auto_answer) {
						vdd_writestr(&wrslot, answer(&modem));
					}
				}
			}
			if (cfg.terminate_on_disconnect) {
				dprintf("Terminating process on disconnect");
				TerminateProcess(process_info.hProcess, 2112);
			}
		}

		size_t rd = 0;
		size_t len = sizeof(buf);

		while (rd < len) {
			unsigned long waiting = 0;
			unsigned long msglen = 0;

			GetMailslotInfo(
				rdslot,             // mailslot handle
				NULL,               // address of maximum message size
				NULL,               // address of size of next message
				&waiting,           // address of number of messages
				NULL                // address of read time-out
				);
			if (!waiting)
				break;
			if (ReadFile(rdslot, buf + rd, len - rd, &msglen, NULL) == FALSE || msglen < 1)
				break;
			rd += msglen;
		}
		if (rd) {
			if (modem.online) {
				if (modem.esc_count) {
					dprintf("Esc count = %d", modem.esc_count);
					if (modem.esc_count >= 3)
						modem.esc_count = 0;
					else  {
						if (now - lasttx < guard_time(&modem))
							if (*buf == modem.esc)
								modem.esc_count += count_esc(&modem, buf, rd);
							else
								modem.esc_count = 0;
					}
				} else {
					if (now - lasttx > guard_time(&modem)) {
						modem.esc_count = count_esc(&modem, buf, rd);
						dprintf("New esc count = %d at %llu", modem.esc_count, now);
					}
				}
				size_t   len = rd;
				uint8_t* p = buf;
				if (mode == TELNET) {
					len = telnet_expand(buf, rd, telnet_buf, sizeof(telnet_buf)
					                    , !telnet_opt_enabled(telnet.local_option[TELNET_BINARY_TX]) // expand_cr
					                    , &p);
					if (len != rd)
						dprintf("Telnet expanded %d bytes to %d", rd, len);
				}
				int wr = send(sock, p, len, /* flags: */ 0);
				if (wr != len)
					dprintf("Sent %d instead of %d", wr, len);
				else if (cfg.debug)
					dprintf("TX: %d bytes", wr);
			} else { // Command mode
				dprintf("RX command: '%.*s' at %llu\n", rd, buf, now);
				if (!modem.echo_off)
					vdd_write(&wrslot, buf, rd);
				char* response = atmodem_parse(&modem, buf, rd);
				if (response != NULL) {
					vdd_writestr(&wrslot, response);
					SKIP_WHITESPACE(response);
					dprintf("Modem response: %s", response);
				}
			}
			lasttx = now;
		} else {
			if (modem.online && modem.esc_count == 3 && now - lasttx >= guard_time(&modem)) {
				dprintf("Entering command mode");
				modem.online = false;
				modem.esc_count = 0;
				vdd_writestr(&wrslot, ok(&modem));
			}
		}
	}

	int retval = EXIT_SUCCESS;
	fp = fopen("DOSXTRN.RET", "r");
	if (fp == NULL) {
		perror("DOSXTRN.RET");
	} else {
		if (fscanf(fp, "%d", &retval) != 1) {
			fprintf(stderr, "Error reading return value from DOSXTRN.REG");
			retval = EXIT_FAILURE;
		}
		fclose(fp);
		if (retval == -1) {
			fprintf(stderr, "DOSXTRN failed to execute '%s': ", dospgm);
			fp = fopen("DOSXTRN.ERR", "r");
			if (fp == NULL) {
				perror("DOSXTRN.ERR");
			} else {
				char errstr[256] = "";
				int  errval = 0;
				if (fscanf(fp, "%d\n", &errval) == 1) {
					fgets(errstr, sizeof(errstr), fp);
					truncsp(errstr);
					fprintf(stderr, "Error %d (%s)\n", errval, errstr);
				} else
					fprintf(stderr, "Failed to parse DOSXTRN.ERR\n");
				fclose(fp);
			}
		}
	}

	if (cfg.debug) {
		printf("rx_delay: %lld\n", rx_delay);
		printf("rx_buflen: %ld\n", rx_buflen);
		printf("largest recv: %d\n", largest_recv);
		printf("lasttx: %llu\n", lasttx);
		printf("lastrx: %llu\n", lastrx);
		printf("lastring: %llu\n", lastring);
		printf("timer: %llu\n", xp_timer64());
	} else {
		if (cfg.client_file[0])
			remove(cfg.client_file);
		remove("DOSXTRN.ENV");
		remove("DOSXTRN.RET");
		remove("DOSXTRN.ERR");
	}
	return retval;
}
