/* Synchronet External X/Y/ZMODEM Transfer Protocols */

/****************************************************************************
 * @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.	*
 ****************************************************************************/
/*
 * ZMODEM code based on zmtx/zmrx v1.02 (C) Mattheij Computer Service 1994
 * by Jacques Mattheij
 *
 *	Date: Thu, 19 Nov 2015 10:10:02 +0100
 *	From: Jacques Mattheij
 *	Subject: Re: zmodem license
 *	To: Stephen Hurd, Fernando Toledo
 *	CC: Rob Swindell
 *
 *	Hello there to all of you,
 *
 *	So, this email will then signify as the transfer of any and all rights I
 *	held up to this point with relation to the copyright of the zmodem
 *	package as released by me many years ago and all associated files to
 *	Stephen Hurd. Fernando Toledo and Rob Swindell are named as
 *	witnesses to this transfer.
 *
 *	...
 *
 *	best regards,
 *
 *	Jacques Mattheij
 */

#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>      /* isdigit */
#include <sys/stat.h>

#ifdef __unix__
	#include <termios.h>
	#include <signal.h>
#endif

/* xpdev */
#include "conwrap.h"
#include "dirwrap.h"
#include "eventwrap.h"
#include "filewrap.h"
#include "genwrap.h"
#include "ini_file.h"
#include "os_info.h"
#include "semwrap.h"
#include "sockwrap.h"
#include "str_list.h"
#include "threadwrap.h"

/* sbbs */
#include "ringbuf.h"
#include "telnet.h"
#include "nopen.h"
#include "date_str.h"
#include "git_branch.h"
#include "git_hash.h"

/* sexyz */
#include "sexyz.h"

#define SINGLE_THREADED     FALSE
#define TXBUF_SIZE          4096
#define MIN_OUTBUF_SIZE     1024
#define MAX_OUTBUF_SIZE     (64 * 1024)
#define INBUF_SIZE          (64 * 1024)
#define MAX_FILE_SIZE       0           /* Default value for max recv file size, 0 = unlimited*/

/***************/
/* Global Vars */
/***************/
long mode = 0;                          /* Program mode 					*/
long        zmode = 0L;                 /* ZMODEM mode						*/
uchar       block[XMODEM_MAX_BLOCK_SIZE]; /* Block buffer 					*/
ulong       block_num;                  /* Block number 					*/
char*       dszlog;
BOOL        dszlog_path = TRUE;         /* Log complete path to filename	*/
BOOL        dszlog_short = FALSE;       /* Log Micros~1 short filename		*/
BOOL        dszlog_quotes = FALSE;      /* Quote filenames in DSZLOG		*/
int         log_level = LOG_INFO;
BOOL        use_syslog = FALSE;
BOOL        lc_filenames = FALSE;
int64_t     max_file_size = MAX_FILE_SIZE;

xmodem_t    xm;
zmodem_t    zm;

FILE*       errfp;
FILE*       statfp;
FILE*       logfp = NULL;

const char* revision = "3.6";

SOCKET      sock = INVALID_SOCKET;

BOOL        telnet = TRUE;
#ifdef __unix__
BOOL        stdio = FALSE;
#endif
BOOL        terminate = FALSE;
BOOL        debug_tx = FALSE;
BOOL        debug_rx = FALSE;
BOOL        debug_telnet = FALSE;
BOOL        pause_on_exit = FALSE;
BOOL        pause_on_abend = FALSE;
BOOL        newline = TRUE;
BOOL        connected = TRUE;

time_t      progress_interval;

RingBuf     outbuf;
#if defined(RINGBUF_EVENT)
	#define     outbuf_empty outbuf.empty_event
#else
xpevent_t outbuf_empty;
#endif
unsigned outbuf_drain_timeout;
long     outbuf_size;

uchar    inbuf[INBUF_SIZE];
unsigned inbuf_pos = 0;
unsigned inbuf_len = 0;

unsigned flows = 0;
unsigned select_errors = 0;

#ifdef _WINSOCKAPI_

/* Note: Don't call WSACleanup() or TCP session will close! */
WSADATA WSAData;

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

	if ((status = WSAStartup(MAKEWORD(1, 1), &WSAData)) == 0) {
		fprintf(statfp, "%s %s\n", WSAData.szDescription, WSAData.szSystemStatus);
		return TRUE;
	}

	fprintf(errfp, "!WinSock startup ERROR %d\n", status);
	return FALSE;
}

#else /* No WINSOCK */

#define winsock_startup()   (TRUE)

#endif

static int lputs(void* unused, int level, const char* str)
{
	FILE* fp = statfp;
	int   ret;

#if defined(_WIN32) && defined(_DEBUG)
	if (log_level == LOG_DEBUG) {
		char dbgstr[1024];
		SAFEPRINTF(dbgstr, "SEXYZ: %s\n", str);
		OutputDebugString(dbgstr);
	}
#endif

	if (level > log_level)
		return 0;

	if (level < LOG_NOTICE)
		fp = errfp;

	if (!newline) {
		fprintf(fp, "\n");
		newline = TRUE;
	}
	if (level < LOG_NOTICE)
		ret = fprintf(fp, "!%s\n", str);
	else
		ret = fprintf(fp, "%s\n", str);

#if defined(__unix__)
	if (use_syslog) {
		char* msg;
		char* p;
		if ((msg = strdup(str)) != NULL) {
			REPLACE_CHARS(msg, '\r', ' ', p);
			REPLACE_CHARS(msg, '\n', ' ', p);
			syslog(level, "%s", msg);
			free(msg);
		}
	}
#endif

	return ret;
}

#if defined(__GNUC__)   // Catch printf-format errors with lprintf
static int lprintf(int level, const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
#endif
static int lprintf(int level, const char *fmt, ...)
{
	char    sbuf[1024];
	va_list argptr;

	if (level > log_level)
		return 0;

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

void break_handler(int type)
{
	lprintf(LOG_NOTICE, "-> Aborted Locally (signal: %d)", type);

	/* Flag to indicate local (as opposed to remote) abort */
	zm.local_abort = TRUE;

	/* Stop any transfers in progress immediately */
	xm.cancelled = TRUE;
	zm.cancelled = TRUE;
}

#if defined(_WIN32)
BOOL WINAPI ControlHandler(unsigned long CtrlType)
{
	break_handler((int)CtrlType);
	return TRUE;
}
#endif

char* dszlog_filename(char* str)
{
	char*       p = str;
	static char path[MAX_PATH + 1];

#ifdef _WIN32
	static char sfpath[MAX_PATH + 1];
	if (dszlog_short) {
		SAFECOPY(sfpath, str);
		GetShortPathName(str, sfpath, sizeof(sfpath));
		p = sfpath;
	}
#endif

	if (!dszlog_path)
		p = getfname(p);

	if (!dszlog_quotes)
		return p;

	SAFEPRINTF(path, "\"%s\"", p);
	return path;
}

static char *chr(uchar ch)
{
	static char str[25];

	if (mode & ZMODEM) {
		switch (ch) {
			case ZRQINIT:   return "ZRQINIT";
			case ZRINIT:    return "ZRINIT";
			case ZSINIT:    return "ZSINIT";
			case ZACK:      return "ZACK";
			case ZFILE:     return "ZFILE";
			case ZSKIP:     return "ZSKIP";
			case ZNAK:      return "ZNAK";
			case ZABORT:    return "ZABORT";
			case ZFIN:      return "ZFIN";
			case ZRPOS:     return "ZRPOS";
			case ZDATA:     return "ZDATA";
			case ZEOF:      return "ZEOF";
			case ZPAD:      return "ZPAD";
			case ZDLE:      return "ZDLE";
			case ZDLEE:     return "ZDLEE";
			case ZBIN:      return "ZBIN";
			case ZHEX:      return "ZHEX";
			case ZBIN32:    return "ZBIN32";
			case ZRESC:     return "ZRESC";
			case ZCRCE:     return "ZCRCE";
			case ZCRCG:     return "ZCRCG";
			case ZCRCQ:     return "ZCRCQ";
			case ZCRCW:     return "ZCRCW";
		}
	} else {
		switch (ch) {
			case SOH:   return "SOH";
			case STX:   return "STX";
			case ETX:   return "ETX";
			case EOT:   return "EOT";
			case ACK:   return "ACK";
			case NAK:   return "NAK";
			case CAN:   return "CAN";
		}
	}
	if (ch >= ' ' && ch <= '~')
		sprintf(str, "'%c' (%02Xh)", ch, ch);
	else
		sprintf(str, "%u (%02Xh)", ch, ch);
	return str;
}

void dump(BYTE* buf, int len)
{
	char   str[128];
	int    i, j;
	size_t slen = 0;

	slen = sprintf(str, "TX: ");
	for (i = 0; i < len; i += j) {
		for (j = 0; i + j < len && j < 32; j++)
			slen += sprintf(str + slen, "%02X ", buf[i + j]);
		lprintf(LOG_DEBUG, "%s", str);
		slen = sprintf(str, "TX: ");
	}
}

int sendbuf(SOCKET s, void *buf, size_t buflen)
{
	size_t sent = 0;
	int    ret;
#ifdef PREFER_POLL
	struct pollfd fds = {0};
#else
	fd_set socket_set;
#endif

#ifdef __unix__
	if (stdio)
		s = STDOUT_FILENO;
#endif
	for (;;) {
#ifdef __unix__
		if (stdio)
			ret = write(s, (char *)buf + sent, buflen - sent);
		else
#endif
		ret = sendsocket(s, (char *)buf + sent, buflen - sent);
		if (ret == SOCKET_ERROR) {
			switch (SOCKET_ERRNO) {
				case EAGAIN:
				case ENOBUFS:
#if (EAGAIN != EWOULDBLOCK)
				case EWOULDBLOCK:
#endif
#ifdef PREFER_POLL
					fds.fd = s;
					fds.events = POLLOUT;
					if ((ret = poll(&fds, 1, /* timeout (infinite): */-1)) < 1)
						goto disconnect;
					break;
#else
					/* Block until we can send */
					FD_ZERO(&socket_set);
					FD_SET(s, &socket_set);

					if ((ret = select(s + 1, NULL, &socket_set, NULL, NULL)) < 1) {
						if (ret == SOCKET_ERROR && SOCKET_ERRNO != EINTR) {
							lprintf(LOG_ERR, "ERROR %d selecting socket", SOCKET_ERRNO);
							goto disconnect;
						}
					}
#endif
					break;
				default:
					goto disconnect;
			}
		}
		else {
			sent += ret;
			if (sent >= buflen)
				return sent;
		}
	}

disconnect:
	lprintf(LOG_DEBUG, "DISCONNECTED line %u", __LINE__);
	connected = FALSE;
	if (sent)
		return sent;
	return SOCKET_ERROR;
}

void send_telnet_cmd(SOCKET sock, uchar cmd, uchar opt)
{
	uchar buf[3];

	buf[0] = TELNET_IAC;
	buf[1] = cmd;
	buf[2] = opt;

	if (debug_telnet)
		lprintf(LOG_DEBUG, "Sending telnet command: %s %s"
		        , telnet_cmd_desc(buf[1]), telnet_opt_desc(buf[2]));

	if (sendbuf(sock, buf, sizeof(buf)) != sizeof(buf) && debug_telnet)
		lprintf(LOG_ERR, "FAILED");
}

#define DEBUG_TELNET FALSE

/*
 * Returns -1 on disconnect, 0 on timeout, or the number of bytes read.
 * Does not muck around with SOCKET_ERRNO (hopefully)
 */
#if !SINGLE_THREADED
int tx_flush(unsigned timeout);
#endif

static int recv_buffer(int timeout /* seconds */)
{
	int            i;
#ifdef PREFER_POLL
	struct pollfd fds = {0};
#else
	fd_set         socket_set;
	struct timeval tv;
#endif
	int            magic_errno;

#if !SINGLE_THREADED
	/* Don't wait on the back-channel with buffered output still unsent. */
	tx_flush(10);
#endif
	for (;;) {
		if (inbuf_len > inbuf_pos)
			return inbuf_len - inbuf_pos;
#ifdef __unix__
		if (stdio) {
			i = read(STDIN_FILENO, inbuf, sizeof(inbuf));
			/* Look like a socket using MAGIC! */
			switch (i) {
				case 0:
					i = -1;
					magic_errno = EAGAIN;
					break;
				case -1:
					magic_errno = errno;
					break;
			}
		}
		else
#endif
		{
			i = recv(sock, inbuf, sizeof(inbuf), 0);
			if (i == SOCKET_ERROR)
				magic_errno = SOCKET_ERRNO;
		}
		if (i == SOCKET_ERROR) {
			switch (magic_errno) {
				case EAGAIN:
				case EINTR:
#if (EAGAIN != EWOULDBLOCK)
				case EWOULDBLOCK:
#endif
					if (timeout) {
#ifdef PREFER_POLL
						if (stdio)
							fds.fd = STDIN_FILENO;
						else
							fds.fd = sock;
						fds.events = POLLIN;
						if ((i = poll(&fds, 1, timeout * 1000)) < 1) {
							if (fds.revents & POLLHUP)
								connected = FALSE;
							if (i == 0)
								lprintf(LOG_WARNING, "Receive timeout (%u seconds)", timeout);
							else
								lprintf(LOG_ERR, "Receive ERROR %d", magic_errno);
						}
						if (i == 1 && fds.revents & POLLIN) {
							timeout = 0;
							continue;
						}
#else
						// Call select()
						FD_ZERO(&socket_set);
#ifdef __unix__
						if (stdio)
							FD_SET(STDIN_FILENO, &socket_set);
						else
#endif
						FD_SET(sock, &socket_set);
						tv.tv_sec = timeout;
						tv.tv_usec = 0;
						if ((i = select(sock + 1, &socket_set, NULL, NULL, &tv)) < 1) {
							if (i == SOCKET_ERROR) {
								lprintf(LOG_ERR, "Receive ERROR %d", magic_errno);
								connected = FALSE;
							}
							else
								lprintf(LOG_WARNING, "Receive timeout (%u seconds)", timeout);
						}
						else {
							timeout = 0;
							continue;
						}
#endif
					}
					return 0;
				default:
					lprintf(LOG_DEBUG, "DISCONNECTED line %u, error=%u", __LINE__, magic_errno);
					connected = FALSE;
					return -1;
			}
			return -1; // Impossible.
		}
		else if (i == 0) {
			lprintf(LOG_DEBUG, "DISCONNECTED line %u", __LINE__);
			connected = FALSE;
			return -1;
		}
		else {
			inbuf_len = i;
			return i;
		}
	};
}

/****************************************************************************/
/* ZMODEM file-management option (ZFILE ZF1), sent to the receiver to say	*/
/* what it should do when the file already exists at its end.  The engine	*/
/* treats these as mutually exclusive, so set exactly one.					*/
/****************************************************************************/
static char* zmodem_management_list[] = { "crc", "clobber", "protect", "newer", NULL };
enum { ZMGMT_CRC, ZMGMT_CLOBBER, ZMGMT_PROTECT, ZMGMT_NEWER };

static void set_zmodem_management(unsigned opt)
{
	zm.management_clobber = (opt == ZMGMT_CLOBBER);
	zm.management_protect = (opt == ZMGMT_PROTECT);
	zm.management_newer   = (opt == ZMGMT_NEWER);
}

/****************************************************************************/
/* Bulk receive: hand the engine a run of bytes it can take verbatim.		*/
/* Never blocks and never refills -- recv_byte() owns the waiting, so a		*/
/* return of 0 simply means "nothing buffered right now, go per-byte".		*/
/* Disabled in the two cases where recv_byte() has to see every byte: telnet	*/
/* mode, whose IAC state machine needs them, and DebugRx, whose per-byte		*/
/* trace would otherwise lose every byte the span path carries.				*/
/****************************************************************************/
size_t recv_span(void* unused, uint8_t* buf, size_t maxlen, const uint8_t* plain_tab)
{
	size_t n = 0;

	if (telnet || debug_rx || inbuf_pos >= inbuf_len)
		return 0;

	while (n < maxlen && inbuf_pos < inbuf_len && plain_tab[inbuf[inbuf_pos]])
		buf[n++] = inbuf[inbuf_pos++];

	if (inbuf_pos >= inbuf_len)
		inbuf_pos = inbuf_len = 0;

	return n;
}

/****************************************************************************/
/* Receive a byte from remote (single-threaded version)						*/
/****************************************************************************/
int recv_byte(void* unused, unsigned timeout /* seconds */)
{
	int          i;
	uchar        ch;
	static uchar telnet_cmd;
	static int   telnet_cmdlen;

	while ((inbuf_len || connected) && !terminate) {
		if (inbuf_len) {
			ch = inbuf[inbuf_pos++];
			i = 1;
			if (inbuf_pos >= inbuf_len)
				inbuf_pos = inbuf_len = 0;
		}
		else {
			i = recv_buffer(timeout);
			switch (i) {
				case -1:
					i = 0;
					break;
				case 0:
					return NOINP;
				default:
					i = 1;
					continue;
			}
		}

		if (i != sizeof(ch)) {
			if (i == 0) {
				lprintf(LOG_WARNING, "Socket Disconnected");
			} else
				lprintf(LOG_ERR, "recv error %d (%d)", i, SOCKET_ERRNO);
			return NOINP;
		}

		if (telnet) {
			if (ch == TELNET_IAC) {
#if DEBUG_TELNET
				lprintf(LOG_DEBUG, "T<%s> ", telnet_cmd_desc(ch));
#endif
				if (telnet_cmdlen == 0) {
					telnet_cmdlen = 1;
					continue;
				}
				if (telnet_cmdlen == 1) {
					telnet_cmdlen = 0;
					if (debug_rx)
						lprintf(LOG_DEBUG, "RX: %s", chr(TELNET_IAC));
					return TELNET_IAC;
				}
			}
			if (telnet_cmdlen) {
				telnet_cmdlen++;
#if DEBUG_TELNET
				if (telnet_cmdlen == 2)
					lprintf(LOG_DEBUG, "T<%s> ", telnet_cmd_desc(ch));
				else
					lprintf(LOG_DEBUG, "T<%s> ", telnet_opt_desc(ch));
#endif
				if (debug_telnet && telnet_cmdlen == 3)
					lprintf(LOG_DEBUG, "Received telnet command: %s %s"
					        , telnet_cmd_desc(telnet_cmd), telnet_opt_desc(ch));
				if (telnet_cmdlen == 3 && telnet_cmd == TELNET_DO)
					send_telnet_cmd(sock, TELNET_WILL, ch);
				/*
				            else if(telnet_cmdlen==3 && telnet_cmd==TELNET_WILL)
				            	send_telnet_cmd(sock, TELNET_DO,ch);
				*/
				telnet_cmd = ch;
				if ((telnet_cmdlen == 2 && ch < TELNET_WILL) || telnet_cmdlen > 2) {
					telnet_cmdlen = 0;
					/* Code disabled.  Why?  ToDo */
					/* break; */
				}
				continue;
			}
		}
		if (debug_rx)
			lprintf(LOG_DEBUG, "RX: %s", chr(ch));
		return ch;
	}

	return NOINP;
}

#if !SINGLE_THREADED

static uchar  txbuf[TXBUF_SIZE];
static size_t txbuf_len;
static BOOL   send_buffered = FALSE;  /* accumulate into txbuf (streaming) vs. per-byte (windowed) */

/* Write as much of the span into the ring as it will take, reporting how much
   went in.  A short write here is transient: the caller keeps the remainder
   buffered and retries; nothing is ever dropped. */
static int tx_write_span(const uchar* p, size_t len, unsigned timeout, size_t* written)
{
	DWORD  result;
	size_t avail;

	*written = 0;
	while (len) {
		size_t chunk = len;

		if ((avail = RingBufFree(&outbuf)) == 0) {
			fprintf(statfp, "FLOW");
			flows++;
			result = WaitForEvent(outbuf_empty, timeout * 1000);
			fprintf(statfp, "\b\b\b\b    \b\b\b\b");
			if ((avail = RingBufFree(&outbuf)) == 0) {
				lprintf(LOG_WARNING
				        , "TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)"
				        , result, timeout, RingBufFull(&outbuf));
				return -1;
			}
		}
		if (chunk > avail)
			chunk = avail;
		if (RingBufWrite(&outbuf, (uchar*)p, chunk) != chunk) {
			lprintf(LOG_ERR, "RingBufWrite() short write");
			return -1;
		}
		p += chunk;
		len -= chunk;
		*written += chunk;
	}
	return 0;
}

/* Push everything send_byte() has accumulated into the ring. */
int tx_flush(unsigned timeout)
{
	size_t written;
	int    result;

	if (txbuf_len == 0)
		return 0;
	result = tx_write_span(txbuf, txbuf_len, timeout, &written);
	if (written != 0 && written < txbuf_len)
		memmove(txbuf, txbuf + written, txbuf_len - written);
	txbuf_len -= written;
	return result;
}

/*************************/
/* Send a byte to remote */
/*************************/
int send_byte(void* unused, uchar ch, unsigned timeout)
{
	/* Streaming: accumulate and hand whole spans to the ring, so the producer
	   is not the bottleneck.  A transmit window or segmented mode instead falls
	   through to the per-byte path below, which keeps acks flowing back as data
	   is sent (buffering would burst the output and stall the window). */
	if (send_buffered) {
		if (txbuf_len + 2 > sizeof(txbuf)) {
			tx_flush(timeout);
			if (txbuf_len + 2 > sizeof(txbuf))
				return -1;
		}
		if (telnet && ch == TELNET_IAC)    /* escape IAC char */
			txbuf[txbuf_len++] = TELNET_IAC;
		txbuf[txbuf_len++] = ch;
		return 0;
	}

	uchar    buf[2] = { TELNET_IAC, TELNET_IAC };
	unsigned len = 1;
	DWORD    result;

	if (telnet && ch == TELNET_IAC)    /* escape IAC char */
		len = 2;
	else
		buf[0] = ch;

	if (RingBufFree(&outbuf) < len) {
#if !defined(RINGBUF_EVENT)
		ResetEvent(outbuf_empty);
#endif
		fprintf(statfp, "FLOW");
		flows++;
		result = WaitForEvent(outbuf_empty, timeout * 1000);
		fprintf(statfp, "\b\b\b\b    \b\b\b\b");
		if (result != WAIT_OBJECT_0) {
			lprintf(LOG_WARNING
			        , "TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)"
			        , result, timeout, RingBufFull(&outbuf));
			fprintf(statfp
			        , "\n!TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)\n"
			        , result, timeout, RingBufFull(&outbuf));
			newline = TRUE;
			if ((result = RingBufFree(&outbuf)) < len) {
				lprintf(LOG_ERR, "Still not enough space in ring buffer (need %d, avail=%d)", len, result);
				return -1;
			}
		}
		if ((result = RingBufFree(&outbuf)) < len) {
			lprintf(LOG_ERR, "Not enough space in ring buffer (need %d, avail=%d) although empty event is set!", len, result);
			return -1;
		}
	}

	if ((result = RingBufWrite(&outbuf, buf, len)) != len) {
		lprintf(LOG_ERR, "RingBufWrite() returned %d, expected %d", result, len);
	}

#if 0
	if (debug_tx)
		lprintf(LOG_DEBUG, "TX: %s", chr(ch));
#endif
	return 0;
}

#else

/*************************/
/* Send a byte to remote */
/*************************/
int send_byte(void* unused, uchar ch, unsigned timeout)
{
	uchar buf[2] = { TELNET_IAC, TELNET_IAC };
	int   len = 1;
	int   i;

	if (telnet && ch == TELNET_IAC)    /* escape IAC char */
		len = 2;
	else
		buf[0] = ch;

	i = sendbuf(sock, buf, len);

	if (i == len) {
		if (debug_tx)
			lprintf(LOG_DEBUG, "TX: %s", chr(ch));
		return 0;
	}

	return -1;
}
#endif

static void output_thread(void* arg)
{
	char     stats[128];
	BYTE     buf[MAX_OUTBUF_SIZE];
	int      i;
	ulong    avail;
	uint64_t total_sent = 0;
	uint64_t total_pkts = 0;
	ulong    short_sends = 0;
	ulong    bufbot = 0;
	ulong    buftop = 0;

#if 0 /* def _DEBUG */
	fprintf(statfp, "output thread started\n");
#endif

	while (sock != INVALID_SOCKET && !terminate) {

		if (bufbot == buftop)
			avail = RingBufFull(&outbuf);
		else
			avail = buftop - bufbot;

		if (!avail) {
			WaitForEvent(outbuf.data_event, INFINITE);
			if (outbuf.highwater_mark)
				WaitForEvent(outbuf.highwater_event, outbuf_drain_timeout);
			continue;
		}

		if (bufbot == buftop) { /* linear buf empty, read from ring buf */
			if (avail > sizeof(buf)) {
				lprintf(LOG_ERR, "Insufficient linear output buffer (%lu > %lu)"
				        , avail, sizeof(buf));
				avail = sizeof(buf);
			}
			buftop = RingBufRead(&outbuf, buf, avail);
			bufbot = 0;
		}
		i = sendbuf(sock, (char*)buf + bufbot, buftop - bufbot);
		if (i == SOCKET_ERROR) {
			if (SOCKET_ERRNO == ENOTSOCK)
				lprintf(LOG_ERR, "client socket closed on send");
			else if (SOCKET_ERRNO == ECONNRESET)
				lprintf(LOG_ERR, "connection reset by peer on send");
			else if (SOCKET_ERRNO == ECONNABORTED)
				lprintf(LOG_ERR, "connection aborted by peer on send");
			else
				lprintf(LOG_ERR, "ERROR %d sending on socket %d"
				        , SOCKET_ERRNO, sock);
			break;
		}

		if (debug_tx)
			dump(buf + bufbot, i);

		if (i != (int)(buftop - bufbot)) {
			lprintf(LOG_ERR, "Short socket send (%u instead of %lu)"
			        , i, buftop - bufbot);
			short_sends++;
		}
		bufbot += i;
		total_sent += i;
		total_pkts++;
	}

	if (total_sent)
		sprintf(stats, "(sent %" PRIu64 " bytes in %" PRIu64 " blocks, %" PRIu64 " average, %lu short, %u errors)"
		        , total_sent, total_pkts, total_sent / total_pkts, short_sends, select_errors);
	else
		stats[0] = 0;

	lprintf(LOG_DEBUG, "output thread terminated\n%s", stats);
}

/* Flush output buffer */
void flush(void* unused)
{
#if !SINGLE_THREADED
	tx_flush(10);
#endif
#ifdef __unix__
	if (stdio)
		fflush(stdout);
#endif
}

BOOL is_connected(void* unused)
{
	if (inbuf_len > inbuf_pos)
		return TRUE;
	return connected;
}

BOOL data_waiting(void* unused, unsigned timeout /* seconds */)
{
	if (recv_buffer(timeout) > 0)
		return TRUE;
	return FALSE;
}

/****************************************************************************/
/* Returns the total number of blocks required to send the file				*/
/****************************************************************************/
uint64_t num_blocks(unsigned block_num, uint64_t offset, uint64_t len, unsigned block_size)
{
	uint64_t blocks;
	uint64_t remain = len - offset;

	blocks = block_num + (remain / block_size);
	if (remain % block_size)
		blocks++;
	return blocks;
}

/************************************************/
/* Dump the current block contents - for debug  */
/************************************************/
void dump_block(long block_size)
{
	long l;

	for (l = 0; l < block_size; l++)
		fprintf(statfp, "%02X  ", block[l]);
	fprintf(statfp, "\n");
}

void xmodem_progress(void* unused, unsigned block_num, int64_t offset, int64_t fsize, time_t start)
{
	char          tmp1[64];
	char          tmp2[64];
	unsigned      cps;
	uint64_t      total_blocks;
	long          l;
	long          t;
	time_t        now;
	static time_t last_progress;

	now = time(NULL);
	if (now - last_progress >= progress_interval || offset >= fsize || newline) {
		t = (long)(now - start);
		if (t <= 0)
			t = 1;
		if ((cps = (unsigned)(offset / t)) == 0)
			cps = 1;            /* cps so far */
		l = (long)(fsize / cps);    /* total transfer est time */
		l -= t;                   /* now, it's est time left */
		if (l < 0)
			l = 0;
		if (mode & SEND) {
			total_blocks = num_blocks(block_num, offset, fsize, xm.block_size);
			fprintf(statfp, "\rBlock (%lu%s): %u/%" PRId64 "  Byte: %" PRId64 "  "
			        "Time: %s/%s  %u cps  %lu%% "
			        , xm.block_size % 1024L ? xm.block_size: xm.block_size / 1024L
			        , xm.block_size % 1024L ? "" : "K"
			        , block_num
			        , total_blocks
			        , offset
			        , seconds_to_str(t, tmp1)
			        , seconds_to_str(l, tmp2)
			        , cps
			        , fsize?(long)(((float)offset / (float)fsize) * 100.0):100
			        );
		} else if (mode & YMODEM) {
			fprintf(statfp, "\rBlock (%lu%s): %u  Byte: %" PRId64 "  "
			        "Time: %s/%s  %u cps  %lu%% "
			        , xm.block_size % 1024L ? xm.block_size: xm.block_size / 1024L
			        , xm.block_size % 1024L ? "" : "K"
			        , block_num
			        , offset
			        , seconds_to_str(t, tmp1)
			        , seconds_to_str(l, tmp2)
			        , cps
			        , fsize?(long)(((float)offset / (float)fsize) * 100.0):100
			        );
		} else { /* XModem receive */
			fprintf(statfp, "\rBlock (%lu%s): %u  Byte: %" PRId64 "  "
			        "Time: %s  %u cps "
			        , xm.block_size % 1024L ? xm.block_size: xm.block_size / 1024L
			        , xm.block_size % 1024L ? "" : "K"
			        , block_num
			        , offset
			        , seconds_to_str(t, tmp1)
			        , cps
			        );
		}
		newline = FALSE;
		last_progress = now;
	}
}

/*
 * show the progress of the transfer like this:
 * zmtx: sending file "garbage" 4096 bytes ( 20%)
 */
void zmodem_progress(void* cbdata, int64_t current_pos)
{
	char          tmp1[64];
	char          tmp2[64];
	unsigned      cps;
	long          l;
	long          t;
	time_t        now;
	static time_t last_progress;

	now = time(NULL);
	if (now - last_progress >= progress_interval || current_pos >= zm.current_file_size || newline) {
		t = (long)(now - zm.transfer_start_time);
		if (t <= 0)
			t = 1;
		if (zm.transfer_start_pos > current_pos)
			zm.transfer_start_pos = 0;
		if ((cps = (unsigned)((current_pos - zm.transfer_start_pos) / t)) == 0)
			cps = 1;                        /* cps so far */
		l = (long)(zm.current_file_size / cps); /* total transfer est time */
		l -= t;                               /* now, it's est time left */
		if (l < 0)
			l = 0;
		fprintf(statfp, "\rKByte: %" PRId64 "/%" PRId64 "  %u/CRC-%u  "
		        "Time: %s/%s  %u cps  %lu%% "
		        , current_pos / 1024
		        , zm.current_file_size / 1024
		        , zm.block_size
		        , mode & RECV ? (zm.receive_32bit_data ? 32:16) :
		        (zm.can_fcs_32 && !zm.want_fcs_16) ? 32:16
		        , seconds_to_str(t, tmp1)
		        , seconds_to_str(l, tmp2)
		        , cps
		        , zm.current_file_size?(long)(((float)current_pos / (float)zm.current_file_size) * 100.0):100
		        );
		newline = FALSE;
		last_progress = now;
	}
}

static int send_files(char** fname, uint fnames)
{
	char     tmp[64];
	char     path[MAX_PATH + 1];
	uint     fnum;
	uint     cps;
	BOOL     success = FALSE;
	uint64_t fsize;
	uint64_t sent_bytes;
	uint64_t total_bytes = 0;
	time_t   t, startfile;
	time_t   startall;
	FILE*    fp;

	startall = time(NULL);

	/****************************************************/
	/* Search through all to find total files and bytes */
	/****************************************************/
	for (fnum = 0; fnum < fnames; fnum++) {
		if (!fexist(fname[fnum]) || isdir(fname[fnum]))
			continue;
		xm.total_files++;
		xm.total_bytes += flength(fname[fnum]);
	}

	if (xm.total_files < 1) {
		lprintf(LOG_ERR, "No files to send");
		return -1;
	}
	if (xm.total_files > 1)
		lprintf(LOG_INFO, "Sending %lu files (%" PRId64 " KB total)"
		        , xm.total_files, xm.total_bytes / 1024);

	zm.files_remaining = xm.total_files;
	zm.bytes_remaining = xm.total_bytes;

	/***********************************************/
	/* Send every file matching names or filespecs */
	/***********************************************/
	for (fnum = 0; fnum < fnames; fnum++) {
		SAFECOPY(path, fname[fnum]);
		if (isdir(path))
			continue;

		if ((fp = fnopen(NULL, path, O_RDONLY | O_BINARY)) == NULL
			&& (fp = fopen(path, "rb")) == NULL) {
			lprintf(LOG_ERR, "Error %d opening %s for read", errno, path);
			continue;
		}
		setvbuf(fp, NULL, _IOFBF, 0x10000);

		fsize = filelength(fileno(fp));

		startfile = time(NULL);

		lprintf(LOG_INFO, "Sending %s (%" PRId64 " KB) via %cMODEM"
			    , path, fsize / 1024
			    , mode & XMODEM ? 'X' : mode & YMODEM ? 'Y' : 'Z');

		if (mode & ZMODEM)
			success = zmodem_send_file(&zm, path, fp, /* ZRQINIT? */ xm.sent_files == 0, &startfile, &sent_bytes);
		else    /* X/YMODEM */
			success = xmodem_send_file(&xm, path, fp, &startfile, &sent_bytes);

		fclose(fp);

		if ((t = time(NULL) - startfile) <= 0)
			t = 1;
		if ((cps = (unsigned)(sent_bytes / t)) == 0)
			cps = 1;
		if (success) {
			xm.sent_files++;
			xm.sent_bytes += fsize;
			if (zm.file_skipped)
				lprintf(LOG_WARNING, "File Skipped");
			else
				lprintf(LOG_INFO, "Successful - Time: %s  CPS: %u"
					    , seconds_to_str((uint)t, tmp)
						, cps);

			if (xm.total_files - xm.sent_files)
				lprintf(LOG_INFO, "Remaining - Time: %s  Files: %lu  KBytes: %" PRId64
					    , seconds_to_str((uint)((xm.total_bytes - xm.sent_bytes) / cps), tmp)
					    , xm.total_files - xm.sent_files
					    , (xm.total_bytes - xm.sent_bytes) / 1024
					    );
		} else
			lprintf(LOG_WARNING, "File Transfer %s", zm.local_abort ? "Aborted" : "Failure");

		/* DSZLOG entry */
		if (logfp) {
			lprintf(LOG_DEBUG, "Updating DSZLOG: %s", dszlog);
			fprintf(logfp, "%c %7" PRId64 " %5u bps %6u cps %3u errors %5u %4u "
				    "%s -1\n"
				    , (mode & ZMODEM && zm.file_skipped) ? 's'
				    : success ? (mode & ZMODEM ? 'z':'S')
				    : 'E'
				    , sent_bytes
				    , 115200 /* baud */
				    , cps
				    , mode & ZMODEM ? zm.errors : xm.errors
				    , flows
				    , mode & ZMODEM ? zm.block_size : xm.block_size
				    , dszlog_filename(path));
			fflush(logfp);
		}
		total_bytes += sent_bytes;

		if (zm.local_abort) {
			xm.cancelled = FALSE;
			xmodem_cancel(&xm);
			break;
		}

		if (xm.cancelled || zm.cancelled || !success)
			break;
	}

	if (mode & ZMODEM && !zm.cancelled && is_connected(NULL) && (success || total_bytes))
		zmodem_get_zfin(&zm);

	if (fnum < fnames) /* error occurred */
		return -1;

	if (!success)
		return -1;

	if (mode & XMODEM)
		return 0;
	if (mode & YMODEM) {

		if (xmodem_get_mode(&xm)) {

			lprintf(LOG_INFO, "Sending YMODEM termination block");

			memset(block, 0, XMODEM_MIN_BLOCK_SIZE);  /* send short block for terminator */
			xmodem_put_block(&xm, block, XMODEM_MIN_BLOCK_SIZE /* block_size */, 0 /* block_num */);
			if (xmodem_get_ack(&xm, /* tries: */ 6, /* block_num: */ 0) != ACK) {
				lprintf(LOG_WARNING, "Failed to receive ACK after terminating block");
			}
		}
	}
	if (xm.total_files > 1) {
		t = time(NULL) - startall;
		if (!t)
			t = 1;
		lprintf(LOG_INFO, "Overall - Time %s  KBytes: %" PRId64 "  CPS: %lu"
		        , seconds_to_str((uint)t, tmp)
			    , total_bytes / 1024, total_bytes / t);
	}
	return 0;  /* success */
}

static int receive_files(char** fname_list, int fnames)
{
	char      tmp[64];
	char      str[MAX_PATH + 1];
	char      fname[MAX_PATH + 1];
	int       i;
	int       fnum = 0;
	uint      errors;
	uint      total_files = 0;
	uint      cps;
	uint      wr;
	BOOL      success = FALSE;
	long      fmode;
	long      serial_num = -1;
	int64_t   file_bytes = 0, file_bytes_left = 0;
	int64_t   total_bytes = 0;
	FILE*     fp;
	time_t    t, startfile;
	uintmax_t ftime;

	if (fnames > 1)
		lprintf(LOG_INFO, "Receiving %u files", fnames);

	outbuf.highwater_mark = 0;    /* don't delay ACK/NAK transmits */

	/* Purge input buffer */
	while (is_connected(NULL) && (i = recv_byte(NULL, 0)) != NOINP && !zm.local_abort)
		lprintf(LOG_WARNING, "Throwing out received: %s", chr((uchar)i));

	while (is_connected(NULL)) {
		file_bytes = 0x7fffffff;
		if (mode & XMODEM) {
			SAFECOPY(str, fname_list[0]);    /* we'll have at least one fname */
			file_bytes_left = file_bytes;
		}
		else {
			if (mode & YMODEM) {
				lprintf(LOG_INFO, "Fetching YMODEM header block");
				for (errors = 0; errors <= xm.max_errors && !xm.cancelled; errors++) {
					xmodem_put_nak(&xm, /* expected_block: */ 0);
					if (xmodem_get_block(&xm, block, /* expected_block: */ 0) == SUCCESS) {
						if (!(mode & GMODE))
							send_byte(NULL, ACK, 10);
						break;
					}
					if (errors + 1 > xm.max_errors / 3 && mode & CRC && !(mode & GMODE)) {
						lprintf(LOG_NOTICE, "Falling back to 8-bit Checksum mode");
						mode &= ~CRC;
					}
				}
				if (errors > xm.max_errors || xm.cancelled) {
					lprintf(LOG_ERR, "Error fetching YMODEM header block");
					xmodem_cancel(&xm);
					return 1;
				}
				if (!block[0]) {
					lprintf(LOG_INFO, "Received YMODEM termination block");
					return 0;
				}
				ftime = total_files = 0;
				total_bytes = 0;
				i = sscanf((char*)block + strlen((char*)block) + 1, "%" SCNd64 " %" SCNoMAX " %lo %lo %u %" SCNd64
				           , &file_bytes    /* file size (decimal) */
				           , &ftime         /* file time (octal unix format) */
				           , &fmode         /* file mode (not used) */
				           , &serial_num    /* program serial number */
				           , &total_files   /* remaining files to be sent */
				           , &total_bytes   /* remaining bytes to be sent */
				           );
				lprintf(LOG_DEBUG, "YMODEM header (%u fields): %s", i, block + strlen((char*)block) + 1);
				SAFECOPY(fname, (char*)block);

			} else {    /* ZMODEM */
				lprintf(LOG_INFO, "Waiting for ZMODEM sender...");

				i = zmodem_recv_init(&zm);

				if (zm.cancelled)
					return 1;
				if (i < 0)
					return -1;
				switch (i) {
					case ZFILE:
						SAFECOPY(fname, zm.current_file_name);
						file_bytes = zm.current_file_size;
						ftime = zm.current_file_time;
						total_files = zm.files_remaining;
						total_bytes = zm.bytes_remaining;
						break;
					case ZFIN:
					case ZCOMPL:
						return !success;
					default:
						return -1;
				}
			}
			file_bytes_left = file_bytes;
			if (!total_files)
				total_files = fnames - fnum;
			if (!total_files)
				total_files = 1;
			if (total_bytes < file_bytes)
				total_bytes = file_bytes;

			lprintf(LOG_DEBUG, "Incoming filename: %.64s ", fname);

			if (mode & RECVDIR)
				SAFEPRINTF2(str, "%s%s", fname_list[0], getfname(fname));
			else {
				SAFECOPY(str, getfname(fname));
				for (i = 0; i < fnames; i++) {
					if (!fname_list[i][0])   /* name blank or already used */
						continue;
					if (!stricmp(getfname(fname_list[i]), str)) {
						SAFECOPY(str, fname_list[i]);
						fname_list[i][0] = 0;
						break;
					}
				}
				if (i == fnames) {                 /* Not found in list */
					if (fnames)
						fprintf(statfp, " - Not in receive list!");
					if (!fnames || fnum >= fnames || !fname_list[fnum][0])
						SAFECOPY(str, getfname(fname)); /* worst case */
					else {
						SAFECOPY(str, fname_list[fnum]);
						fname_list[fnum][0] = 0;
					}
				}
			}
			fprintf(statfp, "File size: %" PRId64 " bytes\n", file_bytes);
			if (total_files > 1)
				fprintf(statfp, "Remaining: %" PRId64 " bytes in %u files\n", total_bytes, total_files);
		}

		lprintf(LOG_DEBUG, "Receiving: %.64s ", str);

		fnum++;

		if (!(mode & RECVDIR) && fnames && fnum > fnames) {
			lprintf(LOG_WARNING, "Attempt to send more files than specified");
			xmodem_cancel(&xm);
			break;
		}

		if (fexistcase(str) && !(mode & OVERWRITE)) {
			lprintf(LOG_WARNING, "%s already exists", str);
			if (mode & ZMODEM) {
				zmodem_send_zskip(&zm);
				continue;
			}
			xmodem_cancel(&xm);
			return 1;
		}

		if (!(mode & XMODEM) && max_file_size != 0 && file_bytes > max_file_size) {
			lprintf(LOG_WARNING, "%s file size (%" PRId64 ") exceeds specified maximum: %" PRId64 " bytes", str, file_bytes, max_file_size);
			if (mode & ZMODEM) {
				zmodem_send_zskip(&zm);
				continue;
			}
			xmodem_cancel(&xm);
			return 1;
		}

		if (lc_filenames) {
			strlwr(str);
		}

		if ((fp = fnopen(NULL, str, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY)) == NULL
		    && (fp = fopen(str, "wb")) == NULL) {
			lprintf(LOG_ERR, "Error %d creating %s", errno, str);
			if (mode & ZMODEM) {
				zmodem_send_zskip(&zm);
				continue;
			}
			xmodem_cancel(&xm);
			return 1;
		}

		if (mode & XMODEM)
			lprintf(LOG_INFO, "Receiving %s via XMODEM%s %s"
			        , str
			        , mode & GMODE ? "-G" : ""
			        , mode & CRC ? "CRC-16":"Checksum");
		else
			lprintf(LOG_INFO, "Receiving %s (%" PRId64 " KB) via %s %s"
			        , str
			        , file_bytes / 1024
			        , mode & YMODEM ? mode & GMODE ? "YMODEM-G" : "YMODEM" :"ZMODEM"
			        , mode & ZMODEM ? "" : (mode & CRC ? "CRC-16" : "Checksum"));

		startfile = time(NULL);
		success = FALSE;
		if (mode & ZMODEM) {

			errors = zmodem_recv_file_data(&zm, fp, 0);

			if (errors <= zm.max_errors && !zm.cancelled)
				success = TRUE;

		} else {
			errors = 0;
			block_num = 1;
			xmodem_put_nak(&xm, block_num);
			while (is_connected(NULL)) {
				off_t pos = ftello(fp);
				if (max_file_size != 0 && pos >= max_file_size) {
					lprintf(LOG_WARNING, "Specified maximum file size (%" PRId64 " bytes) reached at offset %" PRId64
					        , max_file_size, pos);
					break;
				}
				xmodem_progress(NULL, block_num, pos, file_bytes, startfile);
				i = xmodem_get_block(&xm, block, block_num);

				if (i != SUCCESS) {
					if (i == EOT)  {       /* end of transfer */
						success = TRUE;
						xmodem_put_ack(&xm);
						break;
					}
					if (i == CAN) {        /* Cancel */
						xm.cancelled = TRUE;
						break;
					}

					if (++errors > xm.max_errors || (mode & GMODE)) {
						lprintf(LOG_ERR, "Too many errors (%u)", errors);
						xmodem_cancel(&xm);
						break;
					}
					if (i != NOT_XMODEM
					    && block_num == 1 && errors > xm.max_errors / 3 && mode & CRC && !(mode & GMODE)) {
						lprintf(LOG_NOTICE, "Falling back to 8-bit Checksum mode (error=%d)", i);
						mode &= ~CRC;
					}
					xmodem_put_nak(&xm, block_num);
					continue;
				}
				if (!(mode & GMODE))
					send_byte(NULL, ACK, 10);
				if (file_bytes_left <= 0L)  { /* No more bytes to receive */
					lprintf(LOG_WARNING, "Sender attempted to send more bytes than were specified in header");
					break;
				}
				wr = xm.block_size;
				if (wr > (uint)file_bytes_left)
					wr = (uint)file_bytes_left;
				if (fwrite(block, 1, wr, fp) != wr) {
					lprintf(LOG_ERR, "ERROR %d writing %u bytes at file offset %" PRIu64
					        , errno, wr, (uint64_t)ftello(fp));
					fclose(fp);
					xmodem_cancel(&xm);
					return 1;
				}
				file_bytes_left -= wr;
				block_num++;
			}
		}

		/* Use correct file size */
		if (mode & ZMODEM)
			file_bytes = zm.current_file_size;  /* file can grow in transit */
		else {
			fflush(fp);
			if (file_bytes < filelength(fileno(fp))) {
				lprintf(LOG_INFO, "Truncating file to %" PRIu64 " bytes", file_bytes);
				if (chsize(fileno(fp), (off_t)file_bytes) != 0)
					lprintf(LOG_ERR, "ERROR %d setting file size", errno);
			} else
				file_bytes = filelength(fileno(fp));
		}
		fclose(fp);

		t = time(NULL) - startfile;
		if (!t)
			t = 1;
		if (zm.file_skipped)
			lprintf(LOG_WARNING, "File Skipped");
		else if (success)
			lprintf(LOG_INFO, "Successful - Time: %s  CPS: %lu"
			        , seconds_to_str((uint)t, tmp), (ulong)(file_bytes / t));
		else
			lprintf(LOG_ERR, "File Transfer %s"
			        , zm.local_abort ? "Aborted": zm.cancelled ? "Cancelled":"Failure");

		if (!(mode & XMODEM) && ftime)
			setfdate(str, ftime);

		if (logfp) {
			lprintf(LOG_DEBUG, "Updating DSZLOG: %s", dszlog);
			fprintf(logfp, "%c %6" PRId64 " %5u bps %4" PRId64 " cps %3u errors %5u %4u "
			        "%s %ld\n"
			        , success ? (mode & ZMODEM ? 'Z' : 'R') : 'E'
			        , file_bytes
			        , 115200 /* baud */
			        , file_bytes / t
			        , errors
			        , flows
			        , mode & ZMODEM ? zm.block_size : xm.block_size
			        , dszlog_filename(str)
			        , serial_num);
			fflush(logfp);
		}

		if (zm.local_abort) {
			lprintf(LOG_DEBUG, "Locally aborted, sending cancel to remote");
			if (mode & ZMODEM)
				zmodem_send_zabort(&zm);
			xm.cancelled = FALSE;
			xmodem_cancel(&xm);
			break;
		}

		if (mode & XMODEM) /* maximum of one file */
			break;
		if ((cps = (unsigned)(file_bytes / t)) == 0)
			cps = 1;
		total_files--;
		total_bytes -= file_bytes;
		if (total_files > 1 && total_bytes)
			lprintf(LOG_INFO, "Remaining - Time: %s  Files: %u  KBytes: %" PRIu64
			        , seconds_to_str((uint)(total_bytes / cps), tmp)
			        , total_files
			        , total_bytes / 1024
			        );
	}
	return !success;   /* 0=success */
}

void bail(int code)
{
	lprintf(LOG_DEBUG, "Exiting with error level %d", code);
	if (pause_on_exit || (pause_on_abend && code != 0)) {
		printf("Hit enter to continue...");
		getchar();
	}
	exit(code);
}

static const char* usage =
	"usage: sexyz <socket> [-opts] <cmd> [file | path | @list]\n"
	"\n"
#ifdef __unix__
	"socket = TCP socket descriptor (leave blank for stdio mode)\n"
#else
	"socket = TCP socket descriptor\n"
#endif
	"\n"
	"opts   = -y  overwrite an existing destination file\n"
	"             (when receiving: here; when sending: at the receiver)\n"
	"         -p  when sending, protect an existing file at the receiver\n"
	"         -n  when sending, overwrite at the receiver only if ours is newer\n"
	"         -e  request ZMODEM control-character escaping (ZF0_ESCCTL)\n"
	"             (when receiving; when sending, the receiver's request governs)\n"
	"         -k  enable X/YMODEM-1K send mode\n"
	"         -c  enable XMODEM-CRC receive mode\n"
	"         -g  enable X/YMODEM-G receive mode (no error recovery)\n"
	"         -o  disable ZMODEM CRC-32 mode (use CRC-16)\n"
	"         -s  use segmented ZMODEM (disable streaming, both directions)\n"
	"         -2  set maximum ZMODEM block size to 2K\n"
	"         -4  set maximum ZMODEM block size to 4K\n"
	"         -8  set maximum ZMODEM block size to 8K (ZedZap)\n"
	"         -w# set maximum ZMODEM transmit window size (default=0, unlimited)\n"
	"             (applies when sending only)\n"
	"         -m# set maximum receive file size to # bytes (default=0, unlimited)\n"
	"         -l  lowercase received filenames\n"
	"         -!  to pause after abnormal exit (error)\n"
#ifdef __unix__
	"         -telnet to enable Telnet mode (the default except in stdio mode)\n"
#else
	"         -telnet to enable Telnet mode (the default)\n"
#endif
	"         -rlogin or -ssh or -raw to disable Telnet mode\n"
	"\n"
	"cmd    = v  to display detailed version information\n"
	"         sx to send XMODEM     rx to receive XMODEM\n"
	"         sX to send XMODEM-1K  rc to receive XMODEM-CRC\n"
	"         sy to send YMODEM     ry to receive YMODEM\n"
	"         sY to send YMODEM-1K  rg to receive YMODEM-G\n"
	"         sz to send ZMODEM     rz to receive ZMODEM\n"
	"\n"
	"file   = filename to send or receive\n"
	"path   = directory to receive files into\n"
	"list   = name of text file with list of filenames to send or receive\n";

#ifdef __unix__

struct termios tio_default;             /* Initial term settings */

#ifdef NEEDS_CFMAKERAW
static void
cfmakeraw(struct termios *t)
{
	t->c_iflag &= ~(IMAXBEL | IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
	t->c_oflag &= ~OPOST;
	t->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
	t->c_cflag &= ~(CSIZE | PARENB);
	t->c_cflag |= CS8;
}
#endif

static void fixterm(void)
{
	tcsetattr(STDIN_FILENO, TCSANOW, &tio_default);
}

static void init_stdio(void)
{
	struct termios tio_raw;

	if (isatty(STDERR_FILENO))
		fclose(stderr);

	if (isatty(STDIN_FILENO))  {
		tcgetattr(STDIN_FILENO, &tio_default);
		tio_raw = tio_default;
		/* cfmakeraw(&tio_raw); */
		tio_raw.c_iflag &= ~(IMAXBEL | IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
		tio_raw.c_oflag &= ~OPOST;
		tio_raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
		tio_raw.c_cflag &= ~(CSIZE | PARENB);
		tio_raw.c_cflag |= CS8;
		tcsetattr(STDIN_FILENO, TCSANOW, &tio_raw);
		setvbuf(stdout, NULL, _IOFBF, 0);
		atexit(fixterm);
	}
}

BOOL    RingBufIsEmpty(void *buf)
{
	return RingBufFull(buf) == 0;
}

#endif

/***************/
/* Entry Point */
/***************/
int main(int argc, char **argv)
{
	char       str[MAX_PATH + 1];
	char       fname[MAX_PATH + 1];
	char       ini_fname[MAX_PATH + 1];
	char*      p;
	char*      arg;
	int        i;
	int        retval;
	uint       fnames = 0;
	FILE*      fp;
	char       compiler[32];
	BOOL       telnet_requested = FALSE;
	str_list_t ini = strListInit();
	str_list_t fname_list;

	fname_list = strListInit();

	DESCRIBE_COMPILER(compiler);

	errfp = stderr;
#ifdef __unix__
	statfp = stderr;
#else
	statfp = stdout;
#endif

	fprintf(statfp, "\nSynchronet External X/Y/ZMODEM  v%s  %s/%s"
	        "  Copyright Rob Swindell\n\n"
	        , revision
	        , GIT_BRANCH, GIT_HASH
	        );

	xmodem_init(&xm, NULL, &mode, lputs, xmodem_progress, send_byte, recv_byte, is_connected, NULL, flush);
	zmodem_init(&zm, NULL, lputs, zmodem_progress, send_byte, recv_byte, is_connected, NULL, data_waiting, flush);
	zm.recv_span = recv_span;
	xm.log_level = &log_level;
	zm.log_level = &log_level;

	/* Generate path/sexyz[.host].ini from path/sexyz[.exe] */
	SAFECOPY(str, argv[0]);
	p = getfname(str);
	SAFECOPY(fname, p);
	*p = 0;
	if ((p = getfext(fname)) != NULL)
		*p = 0;
	SAFECAT(fname, ".ini");

	iniFileName(ini_fname, sizeof(ini_fname), str, fname);
	if ((fp = fopen(ini_fname, "r")) != NULL) {
		fprintf(statfp, "Reading %s\n", ini_fname);
		ini = iniReadFile(fp);
		fclose(fp);
	}

	telnet                  = iniGetBool(ini, ROOT_SECTION, "Telnet", TRUE);
	debug_tx                = iniGetBool(ini, ROOT_SECTION, "DebugTx", FALSE);
	debug_rx                = iniGetBool(ini, ROOT_SECTION, "DebugRx", FALSE);
	debug_telnet            = iniGetBool(ini, ROOT_SECTION, "DebugTelnet", FALSE);

	pause_on_exit           = iniGetBool(ini, ROOT_SECTION, "PauseOnExit", FALSE);
	pause_on_abend          = iniGetBool(ini, ROOT_SECTION, "PauseOnAbend", FALSE);

	log_level               = iniGetLogLevel(ini, ROOT_SECTION, "LogLevel", log_level);
	use_syslog              = iniGetBool(ini, ROOT_SECTION, "SysLog", use_syslog);

	outbuf.highwater_mark   = (ulong)iniGetBytes(ini, ROOT_SECTION, "OutbufHighwaterMark", 1, 1100);
	outbuf_drain_timeout    = iniGetInteger(ini, ROOT_SECTION, "OutbufDrainTimeout", 10);
	outbuf_size             = (ulong)iniGetBytes(ini, ROOT_SECTION, "OutbufSize", 1, 16 * 1024);

	progress_interval       = iniGetInteger(ini, ROOT_SECTION, "ProgressInterval", 1);
	max_file_size           = iniGetBytes(ini, ROOT_SECTION, "MaxFileSize", /* unit: */ 1, MAX_FILE_SIZE);

	if (iniGetBool(ini, ROOT_SECTION, "Debug", FALSE))
		log_level = LOG_DEBUG;

	const char* section = "XMODEM";
	xm.send_timeout         = iniGetInteger(ini, section, "SendTimeout", xm.send_timeout); /* seconds */
	xm.recv_timeout         = iniGetInteger(ini, section, "RecvTimeout", xm.recv_timeout); /* seconds */
	xm.byte_timeout         = iniGetInteger(ini, section, "ByteTimeout", xm.byte_timeout); /* seconds */
	xm.ack_timeout          = iniGetInteger(ini, section, "AckTimeout", xm.ack_timeout);   /* seconds */
	xm.block_size           = (ulong)iniGetBytes(ini, section, "BlockSize", 1, xm.block_size);          /* 128 or 1024 */
	xm.max_block_size       = (ulong)iniGetBytes(ini, section, "MaxBlockSize", 1, xm.max_block_size);   /* 128 or 1024 */
	xm.max_errors           = iniGetInteger(ini, section, "MaxErrors", xm.max_errors);
	xm.g_delay              = iniGetInteger(ini, section, "G_Delay", xm.g_delay);
	xm.crc_mode_supported   = iniGetBool(ini, section, "SendCRC", xm.crc_mode_supported);
	xm.g_mode_supported     = iniGetBool(ini, section, "SendG", xm.g_mode_supported);

	xm.fallback_to_xmodem   = iniGetInteger(ini, "YMODEM", "FallbackToXmodem", xm.fallback_to_xmodem);

	section = "ZMODEM";
	zm.init_timeout         = iniGetInteger(ini, section, "InitTimeout", zm.init_timeout); /* seconds */
	zm.send_timeout         = iniGetInteger(ini, section, "SendTimeout", zm.send_timeout); /* seconds */
	zm.recv_timeout         = iniGetInteger(ini, section, "RecvTimeout", zm.recv_timeout); /* seconds */
	zm.crc_timeout          = iniGetInteger(ini, section, "CrcTimeout", zm.crc_timeout);   /* seconds */
	zm.block_size           = (ulong)iniGetBytes(ini, section, "BlockSize", 1, zm.block_size);  /* 1024  */
	zm.max_block_size       = (ulong)iniGetBytes(ini, section, "MaxBlockSize", 1, zm.max_block_size); /* 1024 or 8192 */
	zm.max_errors           = iniGetInteger(ini, section, "MaxErrors", zm.max_errors);
	zm.recv_bufsize         = (ulong)iniGetBytes(ini, section, "RecvBufSize", 1, 0);
	zm.no_streaming         = !iniGetBool(ini, section, "Streaming", TRUE);
	zm.want_fcs_16          = !iniGetBool(ini, section, "CRC32", TRUE);
	zm.can_full_duplex      = iniGetBool(ini, section, "FullDuplex", TRUE);
	zm.escape_telnet_iac    = iniGetBool(ini, section, "EscapeTelnetIAC", TRUE);
	zm.escape_ctrl_chars    = iniGetBool(ini, section, "EscapeCtrlChars", FALSE);
	set_zmodem_management(iniGetEnum(ini, section, "SendManagement", zmodem_management_list, ZMGMT_CRC));
	zm.max_window_size      = (uint32_t)iniGetBytes(ini, section, "MaxWindowSize", 1, 0);
	zm.target_window_size   = (unsigned)iniGetDuration(ini, section, "TargetWindowSize", 0);

	section = "DSZLOG";
	dszlog_path             = iniGetBool(ini, section, "Path", TRUE);
	dszlog_short            = iniGetBool(ini, section, "Short", FALSE);
	dszlog_quotes           = iniGetBool(ini, section, "Quotes", FALSE);

	if (zm.recv_bufsize > 0xffff)
		zm.recv_bufsize = 0xffff;

	if (outbuf_size < MIN_OUTBUF_SIZE)
		outbuf_size = MIN_OUTBUF_SIZE;
	else if (outbuf_size > MAX_OUTBUF_SIZE)
		outbuf_size = MAX_OUTBUF_SIZE;

	lprintf(LOG_DEBUG, "Output buffer size: %lu", outbuf_size);
	RingBufInit(&outbuf, outbuf_size);

#if !defined(RINGBUF_EVENT)
	outbuf_empty = CreateEvent(NULL, /* ManualReset */ TRUE, /*InitialState */ TRUE, NULL);
#ifdef __unix__
	outbuf_empty->cbdata = &outbuf;
	outbuf_empty->verify = RingBufIsEmpty;
#endif
#endif

#if 0
	if (argc > 1) {
		fprintf(statfp, "Command line: ");
		for (i = 1; i < argc; i++)
			fprintf(statfp, "%s ", argv[i]);
		fprintf(statfp, "\n", statfp);
	}
#endif


	for (i = 1; i < argc; i++) {

		if (sock == INVALID_SOCKET && IS_DIGIT(argv[i][0])) {
			sock = atoi(argv[i]);
			continue;
		}

		if (!(mode & (SEND | RECV))) {
			if (toupper(argv[i][0]) == 'S' || toupper(argv[i][0]) == 'R') { /* cmd */
				if (toupper(argv[i][0]) == 'R')
					mode |= RECV;
				else
					mode |= SEND;

				switch (argv[i][1]) {
					case 'c':
					case 'C':
						mode |= XMODEM | CRC;
						break;
					case 'x':
						xm.block_size = XMODEM_MIN_BLOCK_SIZE;
					case 'X':
						mode |= XMODEM;
						break;
					case 'b':   /* sz/rz compatible */
					case 'B':
					case 'y':
						xm.block_size = XMODEM_MIN_BLOCK_SIZE;
					case 'Y':
						mode |= (YMODEM | CRC);
						break;
					case 'k':   /* YMODEM-Checksum for debug/test purposes only */
						mode |= YMODEM;
						break;
					case 'g':
					case 'G':
						mode |= (YMODEM | CRC | GMODE);
						break;
					case 'z':
					case 'Z':
						mode |= (ZMODEM | CRC);
						break;
					default:
						fprintf(statfp, "Unrecognized command '%s'\n\n", argv[i]);
						fprintf(statfp, "%s", usage);
						bail(1);
						return -1;
				}
				continue;
			}

			if (toupper(argv[i][0]) == 'V') {

				fprintf(statfp, "%-8s %s\n", getfname(__FILE__), revision);
				fprintf(statfp, "%-8s %s\n", getfname(xmodem_source()), xmodem_ver(str));
				fprintf(statfp, "%-8s %s\n", getfname(zmodem_source()), zmodem_ver(str));
#ifdef _DEBUG
				fprintf(statfp, "Debug\n");
#endif
				fprintf(statfp, "Compiled %s/%s %s with %s\n", GIT_BRANCH, GIT_HASH, GIT_DATE, compiler);
				fprintf(statfp, "%s\n", os_version(str, sizeof(str)));
				bail(0);
				return 0;
			}

			arg = argv[i];
			if (*arg == '-') {
				while (*arg == '-')
					arg++;
				if (stricmp(arg, "telnet") == 0) {
					telnet_requested = TRUE;
					telnet = TRUE;
					continue;
				}
				if (stricmp(arg, "rlogin") == 0 || stricmp(arg, "ssh") == 0 || stricmp(arg, "raw") == 0) {
					telnet_requested = FALSE;
					telnet = FALSE;
					continue;
				}
				if (stricmp(arg, "debug") == 0) {
					log_level = LOG_DEBUG;
					continue;
				}
				if (stricmp(arg, "syslog") == 0) {
					use_syslog = TRUE;
					continue;
				}
				if (stricmp(arg, "quotes") == 0) {
					dszlog_quotes = TRUE;
					continue;
				}
				switch (*arg) {
					case 'k':   /* sz/rz compatible */
						xm.block_size = XMODEM_MAX_BLOCK_SIZE;
						break;
					case 'c':   /* sz/rz compatible */
						mode |= CRC;
						break;
					case '2':
						zm.max_block_size = 2048;
						break;
					case '4':
						zm.max_block_size = 4096;
						break;
					case '8':   /* ZedZap */
						zm.max_block_size = 8192;
						break;
					case 'o':   /* disable ZMODEM CRC-32 */
						zm.want_fcs_16 = TRUE;
						break;
					case 's':   /* disable ZMODEM streaming */
						zm.no_streaming = TRUE;
						break;
					case 'w':   /* Max ZMODEM Transmit Window Size */
						zm.max_window_size = parse_byte_count(arg + 1, /* units: */ 1);
						break;
					case 'g':   /* YMODEM-G or XMODEM-G (a.k.a. Qmodem-G) */
						mode |= (GMODE | CRC);
						break;
					case 'y':
						/* Receiving: may we overwrite a file already here.
						   Sending: tell the receiver to overwrite its own. */
						mode |= OVERWRITE;
						set_zmodem_management(ZMGMT_CLOBBER);
						break;
					case 'p':   /* sz compatible */
						set_zmodem_management(ZMGMT_PROTECT);
						break;
					case 'n':   /* sz compatible */
						set_zmodem_management(ZMGMT_NEWER);
						break;
					case 'e':   /* sz/rz compatible */
						zm.escape_ctrl_chars = TRUE;
						break;
					case '!':
						pause_on_abend = TRUE;
						break;
					case 'm':   /* MaxFileSize */
						max_file_size = parse_byte_count(arg + 1, /* units: */ 1);
						break;
					case 'l':   /* Lowercase received filenames */
						lc_filenames = TRUE;
						break;
				}
			}
		}

		else if ((argv[i][0] == '+' || argv[i][0] == '@') && fexist(argv[i] + 1)) {
			if (mode & RECVDIR) {
				fprintf(statfp, "!Cannot specify both directory and filename\n");
				bail(1);
				return -1;
			}
			SAFEPRINTF(str, "%s", argv[i] + 1);
			if ((fp = fopen(str, "r")) == NULL) {
				fprintf(statfp, "!Error %d opening filelist: %s\n", errno, str);
				bail(1);
				return -1;
			}
			while (!feof(fp) && !ferror(fp)) {
				if (!fgets(str, sizeof(str), fp))
					break;
				truncsp(str);
				strListAppend(&fname_list, strdup(str), fnames++);
			}
			fclose(fp);
		}

		else if (mode & (SEND | RECV)) {
			if (isdir(argv[i])) { /* is a directory */
				if (mode & RECVDIR) {
					fprintf(statfp, "!Only one directory can be specified\n");
					bail(1);
					return -1;
				}
				if (fnames) {
					fprintf(statfp, "!Cannot specify both directory and filename\n");
					bail(1);
					return -1;
				}
				if (mode & SEND) {
					fprintf(statfp, "!Cannot send directory '%s'\n", argv[i]);
					bail(1);
					return -1;
				}
				mode |= RECVDIR;
			}
			strListAppend(&fname_list, argv[i], fnames++);
		}
	}

	/* Keep the transmit window at least 4x the block size, the way lsz/sz do
	   (they force blklen down to window/4): the ZMODEM ack-every-quarter-window
	   scheme needs several blocks in flight, and window <= block leaves only one
	   block unacked at a time, which stalls (GitLab #1197). */
	if (zm.max_window_size > 0) {
		if (zm.max_window_size < 256)
			zm.max_window_size = 256;
		zm.max_window_size = (zm.max_window_size / 64) * 64;
		unsigned window_block = (unsigned)(zm.max_window_size / 4);
		if (zm.max_block_size > window_block) {
			fprintf(statfp, "Reducing ZMODEM block size from %u to %u for a %" PRId64 "-byte window\n"
			        , zm.max_block_size, window_block, zm.max_window_size);
			zm.max_block_size = window_block;
		}
		if (zm.block_size > zm.max_block_size)
			zm.block_size = zm.max_block_size;
	}

#if !SINGLE_THREADED
	/* Buffer the producer only for full ZMODEM streaming.  A transmit window or
	   segmented mode depends on acks flowing back as data is sent (GitLab
	   #1195). */
	send_buffered = (zm.max_window_size == 0) && !zm.no_streaming;
#endif

	if (max_file_size)
		fprintf(statfp, "Maximum receive file size: %" PRIi64 "\n", max_file_size);

	if (!(mode & (SEND | RECV))) {
		fprintf(statfp, "!No command specified\n\n");
		fprintf(statfp, "%s", usage);
		bail(1);
		return -1;
	}

	if (mode & (SEND | XMODEM) && !fnames) { /* Sending with any or recv w/XMODEM */
		fprintf(statfp, "!Must specify filename or filelist\n\n");
		fprintf(statfp, "%s", usage);
		bail(1);
		return -1;
	}

	if (sock == INVALID_SOCKET || sock < 1) {
#ifdef __unix__
		if (STDOUT_FILENO > STDIN_FILENO)
			sock = STDOUT_FILENO;
		else
			sock = STDIN_FILENO;
		stdio = TRUE;

		fprintf(statfp, "No socket descriptor specified, using STDIO\n");
		if (!telnet_requested)
			telnet = FALSE;
		init_stdio();
#else
		fprintf(statfp, "!No socket descriptor specified\n\n");
		fprintf(errfp, usage);
		bail(1);
		return -1;
#endif
	}
#ifdef __unix__
	else
		statfp = stdout;
#endif

	if (!telnet)
		zm.escape_telnet_iac = FALSE;

	zm.max_file_size = max_file_size;

	/* Code disabled.  Why?  ToDo */
/*	if(mode&RECVDIR)
		backslash(fname[0]); */

	if (!winsock_startup())
		bail(-1);

	/* Enable the Nagle Algorithm */
#ifdef __unix__
	if (!stdio) {
#endif
	char error[256] = "";
	lprintf(LOG_DEBUG, "Setting socket options");
	if (iniGetSocketOptions(ini, "sockopts", sock, error, sizeof(error)) != 0)
		lprintf(LOG_ERR, "ERROR %s", error);
	int value = 0;
	socklen_t len = sizeof(value);
	if (getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&value, &len) == 0)
		lprintf(LOG_DEBUG, "Socket send buffer length: %d bytes", value);
	value = 0;
	len = sizeof(value);
	if (getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&value, &len) == 0)
		lprintf(LOG_DEBUG, "Socket receive buffer length: %d bytes", value);
	value = 0;
	len = sizeof(value);
	if (getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&value, &len) == 0)
		lprintf(LOG_DEBUG, "Socket TCP_NODELAY: %d", value);

#ifdef __unix__
}
#endif

	/* With the producer buffered, a fast streaming send fills the socket's
	   auto-tuned (multi-megabyte) send buffer, so a single line error costs a
	   ZRPOS retransmission of everything in flight and breaks error recovery.
	   Bound the in-flight backlog to the ring size.  A transmit window already
	   bounds in-flight data, and a socket buffer smaller than the window fights
	   it, so only when streaming.  Applies in stdio mode too (harmless on a
	   non-socket); [sockopts] can override for a high bandwidth-delay link. */
	if (send_buffered) {
		int sndbuf = (int)outbuf_size;
		if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndbuf, sizeof(sndbuf)) != 0)
			lprintf(LOG_DEBUG, "Could not bound socket send buffer (error %d)", SOCKET_ERRNO);
	}

	/* Set non-blocking mode */
#ifdef __unix__
	if (stdio) {
		(void)fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
	}
	else
#endif
	{
#ifdef _WIN32
		// TODO: Is there a type for this?
		u_long nonblock = 1;
#else
		int    nonblock = 1;
#endif
		ioctlsocket(sock, FIONBIO, &nonblock);
	}

	if (!socket_check(sock, NULL, NULL, 0)) {
		lprintf(LOG_WARNING, "No socket connection");
		bail(-1);
		return -1;
	}

	if ((dszlog = getenv("DSZLOG")) != NULL) {
		lprintf(LOG_DEBUG, "Logging to %s", dszlog);
		if ((logfp = fopen(dszlog, "w")) == NULL) {
			lprintf(LOG_WARNING, "Error %d opening DSZLOG file: %s", errno, dszlog);
			bail(-1);
			return -1;
		}
	}

	/* Install Ctrl-C/Break signal handler here */
#if defined(_WIN32)
	SetConsoleCtrlHandler(ControlHandler, TRUE /* Add */);
#elif defined(__unix__)
	signal(SIGQUIT, break_handler);
	signal(SIGINT, break_handler);
	signal(SIGTERM, break_handler);

	signal(SIGHUP, SIG_IGN);

	/* Don't die on SIGPIPE  */
	signal(SIGPIPE, SIG_IGN);
#endif

#if !SINGLE_THREADED
	_beginthread(output_thread, 0, NULL);
#endif

	if (mode & RECV)
		retval = receive_files(fname_list, fnames);
	else
		retval = send_files(fname_list, fnames);

#if !SINGLE_THREADED
	lprintf(LOG_DEBUG, "Waiting for output buffer to empty... ");
	if (RingBufFull(&outbuf)) {
#if !defined(RINGBUF_EVENT)
		ResetEvent(outbuf_empty);
#endif
		if (WaitForEvent(outbuf_empty, 5000) != WAIT_OBJECT_0)
			lprintf(LOG_DEBUG, "FAILURE");
	}
#endif

	terminate = TRUE; /* stop output thread */

	lprintf(LOG_INFO, "Exiting - Error level: %d, flows: %u, select_errors=%u"
	        , retval, flows, select_errors);

	bail(retval);
	return retval;
}

