Skip to content
Snippets Groups Projects
Select Git revision
  • dailybuild_linux-x64
  • dailybuild_win32
  • master default protected
  • sqlite
  • rip_abstraction
  • dailybuild_macos-armv8
  • dd_file_lister_filanem_in_desc_color
  • mode7
  • dd_msg_reader_are_you_there_warning_improvement
  • c23-playing
  • syncterm-1.3
  • syncterm-1.2
  • test-build
  • hide_remote_connection_with_telgate
  • 638-can-t-control-c-during-a-file-search
  • add_body_to_pager_email
  • mingw32-build
  • cryptlib-3.4.7
  • ree/mastermind
  • new_user_dat
  • sbbs320d
  • syncterm-1.6
  • syncterm-1.5
  • syncterm-1.4
  • sbbs320b
  • syncterm-1.3
  • syncterm-1.2
  • syncterm-1.2rc6
  • syncterm-1.2rc5
  • push
  • syncterm-1.2rc4
  • syncterm-1.2rc2
  • syncterm-1.2rc1
  • sbbs319b
  • sbbs318b
  • goodbuild_linux-x64_Sep-01-2020
  • goodbuild_win32_Sep-01-2020
  • goodbuild_linux-x64_Aug-31-2020
  • goodbuild_win32_Aug-31-2020
  • goodbuild_win32_Aug-30-2020
40 results

xtrn.cpp

Blame
  • xtrn.cpp 64.83 KiB
    /* Synchronet external program support routines */
    
    /****************************************************************************
     * @format.tab-size 4		(Plain Text/Source Code File Header)			*
     * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
     *																			*
     * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
     *																			*
     * This program is free software; you can redistribute it and/or			*
     * modify it under the terms of the GNU General Public License				*
     * as published by the Free Software Foundation; either version 2			*
     * of the License, or (at your option) any later version.					*
     * See the GNU General Public License for more details: gpl.txt or			*
     * http://www.fsf.org/copyleft/gpl.html										*
     *																			*
     * For Synchronet coding style and modification guidelines, see				*
     * http://www.synchro.net/source.html										*
     *																			*
     * Note: If this box doesn't appear square, then you need to fix your tabs.	*
     ****************************************************************************/
    
    #include "sbbs.h"
    #include "cmdshell.h"
    #include "telnet.h"
    
    #include <signal.h>			// kill()
    
    #ifdef __unix__
    	#include <sys/wait.h>	// WEXITSTATUS
    
    	#define TTYDEFCHARS		// needed for ttydefchars definition
    	#include <sys/ttydefaults.h>	// Linux - it's motherfucked.
    #if defined(__FreeBSD__)
    	#include <libutil.h>	// forkpty()
    #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DARWIN__)
    	#include <util.h>
    #elif defined(__linux__)
    	#include <pty.h>
    #elif defined(__QNX__)
    #if 0
    	#include <unix.h>
    #else
    	#define NEEDS_FORKPTY
    #endif
    #endif
    
    	#ifdef NEEDS_FORKPTY
    	#include <grp.h>
    	#endif
    
    	#include <termios.h>
    
    /*
     * Control Character Defaults
     */
    #ifndef CTRL
    	#define CTRL(x)	(x&037)
    #endif
    #ifndef CEOF
    	#define	CEOF		CTRL('d')
    #endif
    #ifndef CEOL
    	#define	CEOL		0xff		/* XXX avoid _POSIX_VDISABLE */
    #endif
    #ifndef CERASE
    	#define	CERASE		0177
    #endif
    #ifndef CERASE2
    	#define	CERASE2		CTRL('h')
    #endif
    #ifndef CINTR
    	#define	CINTR		CTRL('c')
    #endif
    #ifndef CSTATUS
    	#define	CSTATUS		CTRL('t')
    #endif
    #ifndef CKILL
    	#define	CKILL		CTRL('u')
    #endif
    #ifndef CMIN
    	#define	CMIN		1
    #endif
    #ifndef CQUIT
    	#define	CQUIT		034		/* FS, ^\ */
    #endif
    #ifndef CSUSP
    	#define	CSUSP		CTRL('z')
    #endif
    #ifndef CTIME
    	#define	CTIME		0
    #endif
    #ifndef CDSUSP
    	#define	CDSUSP		CTRL('y')
    #endif
    #ifndef CSTART
    	#define	CSTART		CTRL('q')
    #endif
    #ifndef CSTOP
    	#define	CSTOP		CTRL('s')
    #endif
    #ifndef CLNEXT
    	#define	CLNEXT		CTRL('v')
    #endif
    #ifndef CDISCARD
    	#define	CDISCARD 	CTRL('o')
    #endif
    #ifndef CWERASE
    	#define	CWERASE 	CTRL('w')
    #endif
    #ifndef CREPRINT
    	#define	CREPRINT 	CTRL('r')
    #endif
    #ifndef CEOT
    	#define	CEOT		CEOF
    #endif
    /* compat */
    #ifndef CBRK
    	#define	CBRK		CEOL
    #endif
    #ifndef CRPRNT
    	#define CRPRNT		CREPRINT
    #endif
    #ifndef CFLUSH
    	#define	CFLUSH		CDISCARD
    #endif
    
    #ifndef TTYDEF_IFLAG
    	#define TTYDEF_IFLAG    (BRKINT | ICRNL | IMAXBEL | IXON | IXANY)
    #endif
    #ifndef TTYDEF_OFLAG
    	#define TTYDEF_OFLAG    (OPOST | ONLCR)
    #endif
    #ifndef TTYDEF_LFLAG
    	#define TTYDEF_LFLAG    (ECHO | ICANON | ISIG | IEXTEN | ECHOE|ECHOKE|ECHOCTL)
    #endif
    #ifndef TTYDEF_CFLAG
    	#define TTYDEF_CFLAG    (CREAD | CS8 | HUPCL)
    #endif
    #if defined(__QNX__) || defined(__solaris__) || defined(__NetBSD__)
    	static cc_t     ttydefchars[NCCS] = {
            CEOF,   CEOL,   CEOL,   CERASE, CWERASE, CKILL, CREPRINT,
            CERASE2, CINTR, CQUIT,  CSUSP,  CDSUSP, CSTART, CSTOP,  CLNEXT,
            CDISCARD, CMIN, CTIME,  CSTATUS
    #ifndef __solaris__
    	, _POSIX_VDISABLE
    #endif
    	};
    #endif
    
    #endif	/* __unix__ */
    
    #define XTRN_IO_BUF_LEN 10000	/* 50% of IO_THREAD_BUF_SIZE */
    
    /*****************************************************************************/
    /* Interrupt routine to expand WWIV Ctrl-C# codes into ANSI escape sequences */
    /*****************************************************************************/
    BYTE* wwiv_expand(BYTE* buf, ulong buflen, BYTE* outbuf, ulong& newlen
    	,ulong user_misc, bool& ctrl_c)
    {
        char	ansi_seq[32];
    	ulong 	i,j,k;
    
        for(i=j=0;i<buflen;i++) {
            if(buf[i]==CTRL_C) {	/* WWIV color escape char */
                ctrl_c=true;
                continue;
            }
            if(!ctrl_c) {
                outbuf[j++]=buf[i];
                continue;
            }
            ctrl_c=false;
            if(user_misc&ANSI) {
                switch(buf[i]) {
                    default:
                        strcpy(ansi_seq,"\x1b[0m");          /* low grey */
                        break;
                    case '1':
                        strcpy(ansi_seq,"\x1b[0;1;36m");     /* high cyan */
                        break;
                    case '2':
                        strcpy(ansi_seq,"\x1b[0;1;33m");     /* high yellow */
                        break;
                    case '3':
                        strcpy(ansi_seq,"\x1b[0;35m");       /* low magenta */
                        break;
                    case '4':
                        strcpy(ansi_seq,"\x1b[0;1;44m");     /* white on blue */
                        break;
                    case '5':
                        strcpy(ansi_seq,"\x1b[0;32m");       /* low green */
                        break;
                    case '6':
                        strcpy(ansi_seq,"\x1b[0;1;5;31m");   /* high blinking red */
                        break;
                    case '7':
                        strcpy(ansi_seq,"\x1b[0;1;34m");     /* high blue */
                        break;
                    case '8':
                        strcpy(ansi_seq,"\x1b[0;34m");       /* low blue */
                        break;
                    case '9':
                        strcpy(ansi_seq,"\x1b[0;36m");       /* low cyan */
                        break;
                }
                for(k=0;ansi_seq[k];k++)
                    outbuf[j++]=ansi_seq[k];
            }
        }
        newlen=j;
        return(outbuf);
    }
    
    static void petscii_convert(BYTE* buf, ulong len)
    {
        for(ulong i=0; i<len; i++) {
    		buf[i] = cp437_to_petscii(buf[i]);
    	}
    }
    
    static bool native_executable(scfg_t* cfg, const char* cmdline, long mode)
    {
    	char*	p;
    	char	str[MAX_PATH+1];
    	char	name[64];
    	char	base[64];
    	unsigned i;
    
    	if(mode&EX_NATIVE)
    		return true;
    
    	if(*cmdline == '?' || *cmdline == '*')
    		return true;
    
        SAFECOPY(str,cmdline);				/* Set str to program name only */
    	truncstr(str," ");
        SAFECOPY(name,getfname(str));
    	SAFECOPY(base,name);
    	if((p=getfext(base))!=NULL)
    		*p=0;
    
        for(i=0;i<cfg->total_natvpgms;i++)
            if(stricmp(name,cfg->natvpgm[i]->name)==0
    		|| stricmp(base,cfg->natvpgm[i]->name)==0)
                break;
        return(i<cfg->total_natvpgms);
    }
    
    #define XTRN_LOADABLE_MODULE(cmdline,startup_dir)			\
    	if(cmdline[0]=='*')		/* Baja module or JavaScript */	\
    		return(exec_bin(cmdline+1,&main_csi,startup_dir));
    #ifdef JAVASCRIPT
    	#define XTRN_LOADABLE_JS_MODULE(cmdline,mode,startup_dir)	\
    	if(cmdline[0]=='?' && (mode&EX_SH))						\
    		return(js_execxtrn(cmdline+1, startup_dir));		\
    	if(cmdline[0]=='?')										\
    		return(js_execfile(cmdline+1,startup_dir));
    #else
    	#define XTRN_LOADABLE_JS_MODULE
    #endif
    
    #ifdef _WIN32
    
    #include "execvxd.h"	/* DOSXTRN.EXE API */
    
    extern SOCKET node_socket[];
    
    /*****************************************************************************/
    // Expands Single CR to CRLF
    /*****************************************************************************/
    BYTE* cr_expand(BYTE* inbuf, ulong inlen, BYTE* outbuf, ulong& newlen)
    {
    	ulong	i,j;
    
    	for(i=j=0;i<inlen;i++) {
    		outbuf[j++]=inbuf[i];
    		if(inbuf[i]=='\r')
    			outbuf[j++]='\n';
    	}
    	newlen=j;
        return(outbuf);
    }
    
    static void add_env_var(str_list_t* list, const char* var, const char* val)
    {
    	char	str[MAX_PATH*2];
    	SetEnvironmentVariable(var,NULL);	/* Delete in current process env */
    	SAFEPRINTF2(str,"%s=%s",var,val);
    	strListPush(list,str);
    }
    
    /* Clean-up resources while preserving current LastError value */
    #define XTRN_CLEANUP												\
    	last_error=GetLastError();										\
        if(vxd!=INVALID_HANDLE_VALUE)		CloseHandle(vxd);			\
    	if(rdslot!=INVALID_HANDLE_VALUE)	CloseHandle(rdslot);		\
    	if(wrslot!=INVALID_HANDLE_VALUE)	CloseHandle(wrslot);		\
    	if(start_event!=NULL)				CloseHandle(start_event);	\
    	if(hungup_event!=NULL)				CloseHandle(hungup_event);	\
    	if(hangup_event!=NULL)				CloseHandle(hangup_event);	\
    	SetLastError(last_error)
    
    /****************************************************************************/
    /* Runs an external program 												*/
    /****************************************************************************/
    int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
    {
    	char	str[MAX_PATH+1];
    	char*	env_block=NULL;
    	char*	env_strings;
    	const char* p_startup_dir;
    	char	path[MAX_PATH+1];
        char	fullcmdline[MAX_PATH+1];
    	char	realcmdline[MAX_PATH+1];
    	char	comspec_str[MAX_PATH+1];
    	char	title[MAX_PATH+1];
    	BYTE	buf[XTRN_IO_BUF_LEN],*bp;
        BYTE 	telnet_buf[XTRN_IO_BUF_LEN*2];
        BYTE 	output_buf[XTRN_IO_BUF_LEN*2];
        BYTE 	wwiv_buf[XTRN_IO_BUF_LEN*2];
        bool	wwiv_flag=false;
        bool	native=false;			// DOS program by default
        bool	was_online=true;
    	bool	rio_abortable_save=rio_abortable;
    	bool	use_pipes=false;	// NT-compatible console redirection
    	BOOL	success;
    	BOOL	processTerminated=false;
    	uint	i;
        time_t	hungup=0;
    	HANDLE	vxd=INVALID_HANDLE_VALUE;
    	HANDLE	rdslot=INVALID_HANDLE_VALUE;
    	HANDLE	wrslot=INVALID_HANDLE_VALUE;
    	HANDLE  start_event=NULL;
    	HANDLE	hungup_event=NULL;
    	HANDLE	hangup_event=NULL;
    	HANDLE	rdoutpipe;
    	HANDLE	wrinpipe;
        PROCESS_INFORMATION process_info;
    	unsigned long	rd;
        unsigned long	wr;
        unsigned long	len;
        DWORD	avail;
    	unsigned long	msglen;
    	unsigned long	retval;
    	DWORD	last_error;
    	DWORD	loop_since_io=0;
    	struct	tm tm;
    	str_list_t	env_list;
    
    	xtrn_mode = mode;
    	lprintf(LOG_DEBUG,"Executing external: %s",cmdline);
    
    	if(startup_dir!=NULL && startup_dir[0] && !isdir(startup_dir)) {
    		errormsg(WHERE, ERR_CHK, startup_dir, 0);
    		return -1;
    	}
    
    	XTRN_LOADABLE_MODULE(cmdline,startup_dir);
    	XTRN_LOADABLE_JS_MODULE(cmdline,mode,startup_dir);
    
    	attr(cfg.color[clr_external]);		/* setup default attributes */
    
    	native = native_executable(&cfg, cmdline, mode);
    
    	if(!native && (startup->options&BBS_OPT_NO_DOS)) {
    		lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOS programs not supported: %s", cmdline);
    		bprintf("Sorry, DOS programs are not supported on this node.\r\n");
    		return -1;
    	}
    
    	if(mode&EX_SH || strcspn(cmdline,"<>|")!=strlen(cmdline))
    		sprintf(comspec_str,"%s /C ", comspec);
    	else
    		comspec_str[0]=0;
    
        if(startup_dir && cmdline[1]!=':' && cmdline[0]!='/'
        	&& cmdline[0]!='\\' && cmdline[0]!='.')
           	SAFEPRINTF3(fullcmdline, "%s%s%s", comspec_str, startup_dir, cmdline);
        else
        	SAFEPRINTF2(fullcmdline, "%s%s", comspec_str, cmdline);
    
    	SAFECOPY(realcmdline, fullcmdline);	// for errormsg if failed to execute
    
    	now=time(NULL);
    	if(localtime_r(&now,&tm)==NULL)
    		memset(&tm,0,sizeof(tm));
    
    	if(native && mode&EX_STDOUT && !(mode&EX_OFFLINE))
    		use_pipes=true;
    
     	if(native) { // Native (not MS-DOS) external
    
    		if((env_list=strListInit())==NULL) {
    			XTRN_CLEANUP;
            	errormsg(WHERE, ERR_CREATE, "env_list", 0);
                return(errno);
    		}
    
    		// Current environment passed to child process
    		sprintf(str,"%sprotocol.log",cfg.node_dir);
    		add_env_var(&env_list,"DSZLOG",str);
    		add_env_var(&env_list,"SBBSNODE",cfg.node_dir);
    		add_env_var(&env_list,"SBBSCTRL",cfg.ctrl_dir);
    		add_env_var(&env_list,"SBBSDATA",cfg.data_dir);
    		add_env_var(&env_list,"SBBSEXEC",cfg.exec_dir);
    		sprintf(str,"%d",cfg.node_num);
    		add_env_var(&env_list,"SBBSNNUM",str);
    		/* date/time env vars */
    		sprintf(str,"%02u",tm.tm_mday);
    		add_env_var(&env_list,"DAY",str);
    		add_env_var(&env_list,"WEEKDAY",wday[tm.tm_wday]);
    		add_env_var(&env_list,"MONTHNAME",mon[tm.tm_mon]);
    		sprintf(str,"%02u",tm.tm_mon+1);
    		add_env_var(&env_list,"MONTH",str);
    		sprintf(str,"%u",1900+tm.tm_year);
    		add_env_var(&env_list,"YEAR",str);
    
    		env_strings=GetEnvironmentStrings();
    		env_block=strListCopyBlock(env_strings);
    		if(env_strings!=NULL)
    			FreeEnvironmentStrings(env_strings);
    		env_block=strListAppendBlock(env_block,env_list);
    		strListFree(&env_list);
    		if(env_block==NULL) {
    			XTRN_CLEANUP;
            	errormsg(WHERE, ERR_CREATE, "env_block", 0);
                return(errno);
    		}
    
        } else { // DOS external
    
    		// DOS-compatible (short) paths
    		char node_dir[MAX_PATH+1];
    		char ctrl_dir[MAX_PATH+1];
    		char data_dir[MAX_PATH+1];
    		char exec_dir[MAX_PATH+1];
    
    		// in case GetShortPathName fails
    		SAFECOPY(node_dir,cfg.node_dir);
    		SAFECOPY(ctrl_dir,cfg.ctrl_dir);
    		SAFECOPY(data_dir,cfg.data_dir);
    		SAFECOPY(exec_dir,cfg.exec_dir);
    
    		GetShortPathName(cfg.node_dir,node_dir,sizeof(node_dir));
    		GetShortPathName(cfg.ctrl_dir,ctrl_dir,sizeof(node_dir));
    		GetShortPathName(cfg.data_dir,data_dir,sizeof(data_dir));
    		GetShortPathName(cfg.exec_dir,exec_dir,sizeof(exec_dir));
    
    		SAFEPRINTF(path,"%sDOSXTRN.RET", cfg.node_dir);
    		(void)remove(path);
    
        	// Create temporary environment file
        	SAFEPRINTF(path,"%sDOSXTRN.ENV", node_dir);
            FILE* fp=fopen(path,"w");
            if(fp==NULL) {
    			XTRN_CLEANUP;
            	errormsg(WHERE, ERR_CREATE, path, 0);
                return(errno);
            }
            fprintf(fp, "%s\n", fullcmdline);
    		fprintf(fp, "DSZLOG=%sPROTOCOL.LOG\n", node_dir);
            fprintf(fp, "SBBSNODE=%s\n", node_dir);
            fprintf(fp, "SBBSCTRL=%s\n", ctrl_dir);
    		fprintf(fp, "SBBSDATA=%s\n", data_dir);
    		fprintf(fp, "SBBSEXEC=%s\n", exec_dir);
            fprintf(fp, "SBBSNNUM=%d\n", cfg.node_num);
    		fprintf(fp, "PCBNODE=%d\n", cfg.node_num);
    		fprintf(fp, "PCBDRIVE=%.2s\n", node_dir);
    		fprintf(fp, "PCBDIR=%s\n", node_dir + 2);
    		/* date/time env vars */
    		fprintf(fp, "DAY=%02u\n", tm.tm_mday);
    		fprintf(fp, "WEEKDAY=%s\n",wday[tm.tm_wday]);
    		fprintf(fp, "MONTHNAME=%s\n",mon[tm.tm_mon]);
    		fprintf(fp, "MONTH=%02u\n",tm.tm_mon+1);
    		fprintf(fp, "YEAR=%u\n",1900+tm.tm_year);
            fclose(fp);
    
            SAFEPRINTF2(fullcmdline, "%sDOSXTRN.EXE %s", cfg.exec_dir, path);
    
    		if(!(mode&EX_OFFLINE)) {
    			i=SBBSEXEC_MODE_FOSSIL;
    			if(mode&EX_STDIN)
               		i|=SBBSEXEC_MODE_DOS_IN;
    			if(mode&EX_STDOUT)
            		i|=SBBSEXEC_MODE_DOS_OUT;
    			sprintf(str," NT %u %u"
    				,cfg.node_num,i);
    			strcat(fullcmdline,str);
    
    			sprintf(str,"sbbsexec_hungup%d",cfg.node_num);
    			if((hungup_event=CreateEvent(
    				 NULL	// pointer to security attributes
    				,TRUE	// flag for manual-reset event
    				,FALSE  // flag for initial state
    				,str	// pointer to event-object name
    				))==NULL) {
    				XTRN_CLEANUP;
    				errormsg(WHERE, ERR_CREATE, str, 0);
    				return(GetLastError());
    			}
    
    			sprintf(str,"sbbsexec_hangup%d",cfg.node_num);
    			if((hangup_event=CreateEvent(
    				 NULL	// pointer to security attributes
    				,TRUE	// flag for manual-reset event
    				,FALSE  // flag for initial state
    				,str	// pointer to event-object name
    				))==NULL) {
    				XTRN_CLEANUP;
    				errormsg(WHERE, ERR_CREATE, str, 0);
    				return(GetLastError());
    			}
    
    			sprintf(str,"\\\\.\\mailslot\\sbbsexec\\rd%d"
    				,cfg.node_num);
    			rdslot=CreateMailslot(str
    				,sizeof(buf)/2			// Maximum message size (0=unlimited)
    				,0						// Read time-out
    				,NULL);                 // Security
    			if(rdslot==INVALID_HANDLE_VALUE) {
    				XTRN_CLEANUP;
    				errormsg(WHERE, ERR_CREATE, str, 0);
    				return(GetLastError());
    			}
    		}
        }
    
    	if(startup_dir!=NULL && startup_dir[0])
    		p_startup_dir=startup_dir;
    	else
    		p_startup_dir=NULL;
        STARTUPINFO startup_info={0};
        startup_info.cb=sizeof(startup_info);
    	if(mode&EX_OFFLINE)
    		startup_info.lpTitle=NULL;
    	else {
    		SAFEPRINTF3(title,"%s running %s on node %d"
    			,useron.number ? useron.alias : "Event"
    			,realcmdline
    			,cfg.node_num);
    		startup_info.lpTitle=title;
    	}
        if(startup->options&BBS_OPT_XTRN_MINIMIZED) {
        	startup_info.wShowWindow=SW_SHOWMINNOACTIVE;
            startup_info.dwFlags|=STARTF_USESHOWWINDOW;
        }
    	if(use_pipes) {
    		// Set up the security attributes struct.
    		SECURITY_ATTRIBUTES sa;
    		memset(&sa,0,sizeof(sa));
    		sa.nLength= sizeof(SECURITY_ATTRIBUTES);
    		sa.lpSecurityDescriptor = NULL;
    		sa.bInheritHandle = TRUE;
    
    		// Create the child output pipe (override default 4K buffer size)
    		if(!CreatePipe(&rdoutpipe,&startup_info.hStdOutput,&sa,sizeof(buf))) {
    			errormsg(WHERE,ERR_CREATE,"stdout pipe",0);
    			strListFreeBlock(env_block);
    			return(GetLastError());
    		}
    		startup_info.hStdError=startup_info.hStdOutput;
    
    		// Create the child input pipe.
    		if(!CreatePipe(&startup_info.hStdInput,&wrinpipe,&sa,sizeof(buf))) {
    			errormsg(WHERE,ERR_CREATE,"stdin pipe",0);
    			CloseHandle(rdoutpipe);
    			strListFreeBlock(env_block);
    			return(GetLastError());
    		}
    
    		DuplicateHandle(
    			GetCurrentProcess(), rdoutpipe,
    			GetCurrentProcess(), &rdslot, 0, FALSE, DUPLICATE_SAME_ACCESS);
    
    		DuplicateHandle(
    			GetCurrentProcess(), wrinpipe,
    			GetCurrentProcess(), &wrslot, 0, FALSE, DUPLICATE_SAME_ACCESS);
    
    		CloseHandle(rdoutpipe);
    		CloseHandle(wrinpipe);
    
    		startup_info.dwFlags|=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
        	startup_info.wShowWindow=SW_HIDE;
    	}
    	if(native && !(mode&EX_OFFLINE)) {
    
    		if(!(mode&EX_STDIN)) {
    			if(passthru_thread_running)
    				passthru_socket_activate(true);
    			else
    				pthread_mutex_lock(&input_thread_mutex);
    		}
    	}
    
        success=CreateProcess(
    		NULL,			// pointer to name of executable module
    		fullcmdline,  	// pointer to command line string
    		NULL,  			// process security attributes
    		NULL,   		// thread security attributes
    		native && !(mode&EX_OFFLINE),	 			// handle inheritance flag
    		CREATE_NEW_CONSOLE/*|CREATE_SEPARATE_WOW_VDM*/, // creation flags
            env_block, 		// pointer to new environment block
    		p_startup_dir,	// pointer to current directory name
    		&startup_info,  // pointer to STARTUPINFO
    		&process_info  	// pointer to PROCESS_INFORMATION
    		);
    
    	strListFreeBlock(env_block);
    
    	if(!success) {
    		XTRN_CLEANUP;
    		if(!(mode&EX_STDIN)) {
    			if(passthru_thread_running)
    				passthru_socket_activate(false);
    			else
    				pthread_mutex_unlock(&input_thread_mutex);
    		}
    		SetLastError(last_error);	/* Restore LastError */
            errormsg(WHERE, ERR_EXEC, realcmdline, mode);
    		SetLastError(last_error);	/* Restore LastError */
            return(GetLastError());
        }
    
    #if 0
    	char dbgstr[256];
    	sprintf(dbgstr,"Node %d created: hProcess %X hThread %X processID %X threadID %X\n"
    		,cfg.node_num
    		,process_info.hProcess
    		,process_info.hThread
    		,process_info.dwProcessId
    		,process_info.dwThreadId);
    	OutputDebugString(dbgstr);
    #endif
    
    	CloseHandle(process_info.hThread);
    
    	/* Disable Ctrl-C checking */
    	if(!(mode&EX_OFFLINE))
    		rio_abortable=false;
    
        // Executing app in foreground?, monitor
        retval=STILL_ACTIVE;
        while(!(mode&EX_BG)) {
    		if(mode&EX_CHKTIME)
    			gettimeleft();
            if(!online && !(mode&EX_OFFLINE)) { // Tell VXD/VDD and external that user hung-up
            	if(was_online) {
    				logline(LOG_NOTICE,"X!","hung-up in external program");
                	hungup=time(NULL);
    				if(!native) {
    					SetEvent(hungup_event);
    				}
    	            was_online=false;
                }
                if(hungup && time(NULL)-hungup>5 && !processTerminated) {
    				lprintf(LOG_INFO,"Terminating process from line %d", __LINE__);
    				processTerminated=TerminateProcess(process_info.hProcess, 2112);
    			}
            }
    		if((native && !use_pipes) || mode&EX_OFFLINE) {
    			/* Monitor for process termination only */
    			if(WaitForSingleObject(process_info.hProcess,1000)==WAIT_OBJECT_0)
    				break;
    		} else {
    
    			/* Write to VDD */
    
    			wr=RingBufPeek(&inbuf,buf,sizeof(buf));
    			if(wr) {
    				if(!use_pipes && wrslot==INVALID_HANDLE_VALUE) {
    					sprintf(str,"\\\\.\\mailslot\\sbbsexec\\wr%d"
    						,cfg.node_num);
    					wrslot=CreateFile(str
    						,GENERIC_WRITE
    						,FILE_SHARE_READ
    						,NULL
    						,OPEN_EXISTING
    						,FILE_ATTRIBUTE_NORMAL
    						,(HANDLE) NULL);
    					if(wrslot==INVALID_HANDLE_VALUE)
    						lprintf(LOG_DEBUG,"!ERROR %u (%s) opening %s", GetLastError(), strerror(errno), str);
    					else
    						lprintf(LOG_DEBUG,"CreateFile(%s)=0x%x", str, wrslot);
    				}
    
    				/* CR expansion */
    				if(use_pipes)
    					bp=cr_expand(buf,wr,output_buf,wr);
    				else
    					bp=buf;
    
    				len=0;
    				if(wrslot==INVALID_HANDLE_VALUE)
    					lprintf(LOG_WARNING,"VDD Open failed (not loaded yet?)");
    				else if(!WriteFile(wrslot,bp,wr,&len,NULL)) {
    					lprintf(LOG_ERR,"!VDD WriteFile(0x%x, %u) FAILURE (Error=%u)", wrslot, wr, GetLastError());
    					if(GetMailslotInfo(wrslot,&wr,NULL,NULL,NULL))
    						lprintf(LOG_DEBUG,"!VDD MailSlot max_msg_size=%u", wr);
    					else
    						lprintf(LOG_DEBUG,"!GetMailslotInfo(0x%x)=%u", wrslot, GetLastError());
    				} else {
    					if(len!=wr)
    						lprintf(LOG_WARNING,"VDD short write (%u instead of %u)", len,wr);
    					RingBufRead(&inbuf, NULL, len);
    					if(use_pipes && !(mode&EX_NOECHO)) {
    						/* echo */
    						RingBufWrite(&outbuf, bp, len);
    					}
    				}
    				wr=len;
    			}
    
    			/* Read from VDD */
    
    			rd=0;
    			len=sizeof(buf);
    			avail=RingBufFree(&outbuf)/2;	// leave room for wwiv/telnet expansion
    #if 0
    			if(avail==0)
    				lprintf("Node %d !output buffer full (%u bytes)"
    					,cfg.node_num,RingBufFull(&outbuf));
    #endif
    			if(len>avail)
                	len=avail;
    
    			while(rd<len) {
    				unsigned long waiting=0;
    
    				if(use_pipes)
    					PeekNamedPipe(
    						rdslot,             // handle to pipe to copy from
    						NULL,               // pointer to data buffer
    						0,					// size, in bytes, of data buffer
    						NULL,				// pointer to number of bytes read
    						&waiting,			// pointer to total number of bytes available
    						NULL				// pointer to unread bytes in this message
    						);
    				else
    					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(mode&EX_WWIV) {
                    	bp=wwiv_expand(buf, rd, wwiv_buf, rd, useron.misc, wwiv_flag);
    					if(rd>sizeof(wwiv_buf))
    						lprintf(LOG_ERR,"WWIV_BUF OVERRUN");
    				} else if(telnet_mode&TELNET_MODE_OFF) {
    					bp=buf;
    				} else {
                    	rd = telnet_expand(buf, rd, telnet_buf, sizeof(telnet_buf), /* expand_cr: */false, &bp);
    				}
    				if(rd>RingBufFree(&outbuf)) {
    					lprintf(LOG_ERR,"output buffer overflow");
    					rd=RingBufFree(&outbuf);
    				}
    				if(mode&EX_BIN)
    					RingBufWrite(&outbuf, bp, rd);
    				else
    					rputs((char*)bp, rd);
    			}
    #if defined(_DEBUG) && 0
    			if(rd>1) {
    				sprintf(str,"Node %d read %5d bytes from xtrn", cfg.node_num, rd);
    				OutputDebugString(str);
    			}
    #endif
                if((!rd && !wr) || hungup) {
    
    				loop_since_io++;	/* number of loop iterations with no I/O */
    
    				/* only check process termination after 300 milliseconds of no I/O */
    				/* to allow for last minute reception of output from DOS programs */
    				if(loop_since_io>=3) {
    
    					if(online && hangup_event!=NULL
    						&& WaitForSingleObject(hangup_event,0)==WAIT_OBJECT_0) {
    						lprintf(LOG_NOTICE,"Node %d External program requested hangup (dropped DTR)"
    							,cfg.node_num);
    						hangup();
    					}
    
    					if(WaitForSingleObject(process_info.hProcess,0)==WAIT_OBJECT_0)
    						break;	/* Process Terminated */
    				}
    
    				/* only check node for interrupt flag every 3 seconds of no I/O */
    				if((loop_since_io%30)==0) {
    					// Check if the node has been interrupted
    					getnodedat(cfg.node_num,&thisnode,0);
    					if(thisnode.misc&NODE_INTR)
    						break;
    				}
    
    				/* only send telnet GA every 30 seconds of no I/O */
    				if((loop_since_io%300)==0) {
    #if defined(_DEBUG)
    					sprintf(str,"Node %d xtrn idle\n",cfg.node_num);
    					OutputDebugString(str);
    #endif
    					// Let's make sure the socket is up
    					// Sending will trigger a socket d/c detection
    					if(!(startup->options&BBS_OPT_NO_TELNET_GA))
    						send_telnet_cmd(TELNET_GA,0);
    				}
    				sem_trywait_block(&inbuf.sem,100);
                } else
    				loop_since_io=0;
            }
    	}
    
        if(!(mode&EX_BG)) {			/* !background execution */
    
            if(GetExitCodeProcess(process_info.hProcess, &retval)==FALSE)
                errormsg(WHERE, ERR_CHK, "ExitCodeProcess",(DWORD)process_info.hProcess);
    
    		if(retval==STILL_ACTIVE) {
    			lprintf(LOG_INFO,"Node %d Terminating process from line %d",cfg.node_num,__LINE__);
    			TerminateProcess(process_info.hProcess, GetLastError());
    		}
    
    	 	// Get return value
    		if(!native) {
        		sprintf(str,"%sDOSXTRN.RET", cfg.node_dir);
    			FILE* fp=fopen(str,"r");
    			if(fp!=NULL) {
    				if(fscanf(fp,"%d",&retval) != 1) {
    					lprintf(LOG_ERR, "Node %d Error reading return value from %s", cfg.node_num, str);
    					retval = -1;
    				}
    				fclose(fp);
    			}
    		}
    	}
    
    	XTRN_CLEANUP;
    	CloseHandle(process_info.hProcess);
    
    	if(!(mode&EX_OFFLINE)) {	/* !off-line execution */
    
    		if(native) {
    			if(!(mode&EX_STDIN)) {
    				if(passthru_thread_running)
    					passthru_socket_activate(false);
    				else
    					pthread_mutex_unlock(&input_thread_mutex);
    			}
    		}
    
    		curatr=~0;			// Can't guarantee current attributes
    		attr(LIGHTGRAY);	// Force to "normal"
    
    		rio_abortable=rio_abortable_save;	// Restore abortable state
    
    		/* Got back to Text/NVT mode */
    		request_telnet_opt(TELNET_DONT,TELNET_BINARY_TX);
    	}
    
    //	lprintf("%s returned %d",realcmdline, retval);
    
    	errorlevel = retval; // Baja or JS retrievable error value
    
    	return(retval);
    }
    
    #else	/* !WIN32 */
    
    /*****************************************************************************/
    // Expands Unix LF to CRLF
    /*****************************************************************************/
    BYTE* lf_expand(BYTE* inbuf, ulong inlen, BYTE* outbuf, ulong& newlen)
    {
    	ulong	i,j;
    
    	for(i=j=0;i<inlen;i++) {
    		if(inbuf[i]=='\n' && (!i || inbuf[i-1]!='\r'))
    			outbuf[j++]='\r';
    		outbuf[j++]=inbuf[i];
    	}
    	newlen=j;
        return(outbuf);
    }
    
    #define MAX_ARGS 1000
    
    #ifdef NEEDS_SETENV
    static int setenv(const char *name, const char *value, int overwrite)
    {
    	char *envstr;
    	char *oldenv;
    	if(overwrite || getenv(name)==NULL) {
    		envstr=(char *)malloc(strlen(name)+strlen(value)+2);
    		if(envstr==NULL) {
    			errno=ENOMEM;
    			return(-1);
    		}
    		/* Note, on some platforms, this can be free()d... */
    		sprintf(envstr,"%s=%s",name,value);
    		putenv(envstr);
    	}
    	return(0);
    }
    #endif
    
    #ifdef NEEDS_CFMAKERAW
    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
    
    #ifdef NEEDS_FORKPTY
    static int login_tty(int fd)
    {
    	(void) setsid();
    	if (!isatty(fd))
    		return (-1);
    	(void) dup2(fd, 0);
    	(void) dup2(fd, 1);
    	(void) dup2(fd, 2);
    	if (fd > 2)
    		(void) close(fd);
    	return (0);
    }
    
    #ifdef NEEDS_DAEMON
    /****************************************************************************/
    /* Daemonizes the process                                                   */
    /****************************************************************************/
    int
    daemon(int nochdir, int noclose)
    {
        int fd;
    
        switch (fork()) {
        case -1:
            return (-1);
        case 0:
            break;
        default:
            _exit(0);
        }
    
        if (setsid() == -1)
            return (-1);
    
        if (!nochdir)
            (void)chdir("/");
    
        if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
            (void)dup2(fd, STDIN_FILENO);
            (void)dup2(fd, STDOUT_FILENO);
            (void)dup2(fd, STDERR_FILENO);
            if (fd > 2)
                (void)close(fd);
        }
        return (0);
    }
    #endif
    
    static int openpty(int *amaster, int *aslave, char *name, struct termios *termp, winsize *winp)
    {
    	char line[] = "/dev/ptyXX";
    	const char *cp1, *cp2;
    	int master, slave, ttygid;
    	struct group *gr;
    
    	if ((gr = getgrnam("tty")) != NULL)
    		ttygid = gr->gr_gid;
    	else
    		ttygid = -1;
    
    	for (cp1 = "pqrsPQRS"; *cp1; cp1++) {
    		line[8] = *cp1;
    		for (cp2 = "0123456789abcdefghijklmnopqrstuv"; *cp2; cp2++) {
    			line[5] = 'p';
    			line[9] = *cp2;
    			if ((master = open(line, O_RDWR, 0)) == -1) {
    				if (errno == ENOENT)
    					break; /* try the next pty group */
    			} else {
    				line[5] = 't';
    				(void) chown(line, getuid(), ttygid);
    				(void) chmod(line, S_IRUSR|S_IWUSR|S_IWGRP);
    				/* Hrm... SunOS doesn't seem to have revoke
    				(void) revoke(line); */
    				if ((slave = open(line, O_RDWR, 0)) != -1) {
    					*amaster = master;
    					*aslave = slave;
    					if (name)
    						strcpy(name, line);
    					if (termp)
    						(void) tcsetattr(slave,
    							TCSAFLUSH, termp);
    					if (winp)
    						(void) ioctl(slave, TIOCSWINSZ,
    							(char *)winp);
    					return (0);
    				}
    				(void) close(master);
    			}
    		}
    	}
    	errno = ENOENT;	/* out of ptys */
    	return (-1);
    }
    
    static int forkpty(int *amaster, char *name, termios *termp, winsize *winp)
    {
    	int master, slave, pid;
    
    	if (openpty(&master, &slave, name, termp, winp) == -1)
    		return (-1);
    	switch (pid = FORK()) {
    	case -1:
    		return (-1);
    	case 0:
    		/*
    		 * child
    		 */
    		(void) close(master);
    		login_tty(slave);
    		return (0);
    	}
    	/*
    	 * parent
    	 */
    	*amaster = master;
    	(void) close(slave);
    	return (pid);
    }
    #endif /* NEED_FORKPTY */
    
    int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
    {
    	char	str[MAX_PATH+1];
    	char	fname[MAX_PATH+1];
    	char	fullpath[MAX_PATH+1];
    	char	fullcmdline[MAX_PATH+1];
    	char*	argv[MAX_ARGS + 1];
    	BYTE*	bp;
    	BYTE	buf[XTRN_IO_BUF_LEN];
        BYTE 	output_buf[XTRN_IO_BUF_LEN*2];
    	ulong	avail;
        ulong	output_len;
    	bool	native=false;			// DOS program by default
    	bool	rio_abortable_save=rio_abortable;
    	int		i;
    	bool	data_waiting;
    	int		rd;
    	int		wr;
    	int		argc;
    	pid_t	pid;
    	int		in_pipe[2];
    	int		out_pipe[2];
    	int		err_pipe[2];
    	fd_set ibits;
    	int	high_fd;
    	struct timeval timeout;
        BYTE 	wwiv_buf[XTRN_IO_BUF_LEN*2];
        bool	wwiv_flag=false;
     	char* p;
    
    	xtrn_mode = mode;
    	lprintf(LOG_DEBUG, "Executing external: %s", cmdline);
    
    	if(startup_dir!=NULL && startup_dir[0] && !isdir(startup_dir)) {
    		errormsg(WHERE, ERR_CHK, startup_dir, 0);
    		return -1;
    	}
    
    	if(startup_dir==NULL)
    		startup_dir=nulstr;
    
    	XTRN_LOADABLE_MODULE(cmdline,startup_dir);
    	XTRN_LOADABLE_JS_MODULE(cmdline,mode,startup_dir);
    
    	attr(cfg.color[clr_external]);  /* setup default attributes */
    
    	native = native_executable(&cfg, cmdline, mode);
    
        SAFECOPY(str,cmdline);			/* Set fname to program name only */
    	truncstr(str," ");
        SAFECOPY(fname,getfname(str));
    
    	sprintf(fullpath,"%s%s",startup_dir,fname);
    	if(startup_dir!=NULL && cmdline[0]!='/' && cmdline[0]!='.' && fexist(fullpath))
    		sprintf(fullcmdline,"%s%s",startup_dir,cmdline);
    	else
    		SAFECOPY(fullcmdline,cmdline);
    
     	if(native) { // Native (not MS-DOS) external
    
    		// Current environment passed to child process
    		sprintf(dszlog,"%sPROTOCOL.LOG",cfg.node_dir);
    		setenv("DSZLOG",dszlog,1); 		/* Makes the DSZ LOG active */
    		setenv("SBBSNODE",cfg.node_dir,1);
    		setenv("SBBSCTRL",cfg.ctrl_dir,1);
    		setenv("SBBSDATA",cfg.data_dir,1);
    		setenv("SBBSEXEC",cfg.exec_dir,1);
    		sprintf(str,"%u",cfg.node_num);
    		setenv("SBBSNNUM",str,1);
    
    		/* date/time env vars */
    		now = time(NULL);
    		struct	tm tm;
    		if(localtime_r(&now, &tm) == NULL)
    			memset(&tm, 0, sizeof(tm));
    		sprintf(str," %02u", tm.tm_mday);
    		setenv("DAY", str, /* overwrite */TRUE);
    		setenv("WEEKDAY", wday[tm.tm_wday], /* overwrite */TRUE);
    		setenv("MONTHNAME", mon[tm.tm_mon], /* overwrite */TRUE);
    		sprintf(str, "%02u", tm.tm_mon + 1);
    		setenv("MONTH", str, /* overwrite */TRUE);
    		sprintf(str,"%u", 1900 + tm.tm_year);
    		if(setenv("YEAR", str, /* overwrite */TRUE) != 0)
    			errormsg(WHERE,ERR_WRITE,"environment",0);
    
    	} else {
    		if(startup->options&BBS_OPT_NO_DOS) {
    			lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOS programs not supported: %s", cmdline);
    			bprintf("Sorry, DOS programs are not supported on this node.\r\n");
    			return -1;
    		}
    #if defined(__FreeBSD__)
    		/* ToDo: This seems to work for every door except Iron Ox
    		   ToDo: Iron Ox is unique in that it runs perfectly from
    		   ToDo: tcsh but not at all from anywhere else, complaining
    		   ToDo: about corrupt files.  I've ruled out the possibilty
    		   ToDo: of it being a terminal mode issue... no other ideas
    		   ToDo: come to mind. */
    
    		FILE * doscmdrc;
    
    		sprintf(str,"%s.doscmdrc",cfg.node_dir);
    		if((doscmdrc=fopen(str,"w+"))==NULL)  {
    			errormsg(WHERE,ERR_CREATE,str,0);
    			return(-1);
    		}
    		if(startup_dir!=NULL && startup_dir[0])
    			fprintf(doscmdrc,"assign C: %s\n",startup_dir);
    		else
    			fprintf(doscmdrc,"assign C: .\n");
    
    		fprintf(doscmdrc,"assign D: %s\n",cfg.node_dir);
    		SAFECOPY(str,cfg.exec_dir);
    		if((p=strrchr(str,'/'))!=NULL)
    			*p=0;
    		if((p=strrchr(str,'/'))!=NULL)
    			*p=0;
    		fprintf(doscmdrc,"assign E: %s\n",str);
    
    		/* setup doscmd env here */
    		/* ToDo Note, this assumes that the BBS uses standard dir names */
    		fprintf(doscmdrc,"DSZLOG=E:\\node%d\\PROTOCOL.LOG\n",cfg.node_num);
    		fprintf(doscmdrc,"SBBSNODE=D:\\\n");
    		fprintf(doscmdrc,"SBBSCTRL=E:\\ctrl\\\n");
    		fprintf(doscmdrc,"SBBSDATA=E:\\data\\\n");
    		fprintf(doscmdrc,"SBBSEXEC=E:\\exec\\\n");
    		fprintf(doscmdrc,"SBBSNNUM=%d\n",cfg.node_num);
    		fprintf(doscmdrc,"PCBNODE=%d\n",cfg.node_num);
    		fprintf(doscmdrc,"PCBDRIVE=D:\n");
    		fprintf(doscmdrc,"PCBDIR=\\\n");
    
    		fclose(doscmdrc);
    		SAFECOPY(str,fullcmdline);
    		sprintf(fullcmdline,"%s -F %s",startup->dosemu_path,str);
    
    #elif defined(__linux__)
    
    		/* dosemu integration  --  originally by Ryan Underwood, <nemesis @ icequake.net> */
    
    		FILE *dosemubatfp;
    		FILE *externalbatfp;
    		FILE *de_launch_inifp;
    		char tok[MAX_PATH+1];
    		char buf[1024];
    		char bufout[1024];
    
    		char cmdlinebatch[MAX_PATH+1];
    		char externalbatsrc[MAX_PATH+1];
    		char externalbat[MAX_PATH+1];
    		char dosemuconf[MAX_PATH+1];
    		char de_launch_cmd[INI_MAX_VALUE_LEN];
    		char dosemubinloc[MAX_PATH+1];
    		char virtualconf[75];
    		char dosterm[15];
    		char log_external[MAX_PATH+1];
    		const char* runtype;
    		str_list_t de_launch_ini;
    		
    		/*  on the Unix side. xtrndir is the parent of the door's startup dir. */
    		char xtrndir[MAX_PATH+1];
    
    		/*  on the DOS side.  */
    		char xtrndir_dos[MAX_PATH+1];
    		char ctrldir_dos[MAX_PATH+1];
    		char datadir_dos[MAX_PATH+1];
    		char execdir_dos[MAX_PATH+1];
    		char nodedir_dos[MAX_PATH+1];
    
    		/* Default locations that can be overridden by
    		 * the sysop in emusetup.bat */
    
    		const char ctrldrive[] = DOSEMU_CTRL_DRIVE;
    		const char datadrive[] = DOSEMU_DATA_DRIVE;
    		const char execdrive[] = DOSEMU_EXEC_DRIVE;
    		const char nodedrive[] = DOSEMU_NODE_DRIVE;
    		
    		const char external_bat_fn[] = "external.bat";
    		const char dosemu_cnf_fn[] = "dosemu.conf";
    
    		SAFECOPY(str,startup_dir);
    		if(*(p=lastchar(str))=='/')		/* kill trailing slash */
    			*p=0;
    		if((p=strrchr(str,'/'))!=NULL)  /* kill the last element of the path */
    			*p=0;
    
    		SAFECOPY(xtrndir,str);
    
    		SAFECOPY(xtrndir_dos,xtrndir);
    		REPLACE_CHARS(xtrndir_dos,'/','\\',p);
    		
    		SAFECOPY(ctrldir_dos,cfg.ctrl_dir);
    		REPLACE_CHARS(ctrldir_dos,'/','\\',p);
    
    		p=lastchar(ctrldir_dos);
    		if (*p=='\\') *p=0;
    
    		SAFECOPY(datadir_dos,cfg.data_dir);
    		REPLACE_CHARS(datadir_dos,'/','\\',p);
    
    		p=lastchar(datadir_dos);
    		if (*p=='\\') *p=0;
    
    		SAFECOPY(execdir_dos,cfg.exec_dir);
    		REPLACE_CHARS(execdir_dos,'/','\\',p);
    
    		p=lastchar(execdir_dos);
    		if (*p=='\\') *p=0;
    
    		SAFECOPY(nodedir_dos,cfg.node_dir);
    		REPLACE_CHARS(nodedir_dos,'/','\\',p);
     
    		p=lastchar(nodedir_dos);
    		if (*p=='\\') *p=0;
    
    		/* must have sbbs.ini bbs useDOSemu=1 (or empty), cannot be =0 */
    		if (!startup->usedosemu) {
    			lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOSEMU disabled, program not run");
    			bprintf("Sorry, DOSEMU is not supported on this node.\r\n");
    			return -1;
    		}
    
    		/* must have sbbs.ini bbs DOSemuPath set to valid path */
    		SAFECOPY(dosemubinloc,(cmdstr(startup->dosemu_path,nulstr,nulstr,tok)));
    		if (dosemubinloc[0] == '\0') {
    			lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOSEMU invalid DOSEmuPath, program not run");
    			bprintf("Sorry, DOSEMU is not supported on this node.\r\n");
    			return -1;
    		}
    
    		if (!fexist(dosemubinloc)) {
    			lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOSEMU not found: %s", dosemubinloc);
    			bprintf("Sorry, DOSEMU is not supported on this node.\r\n");
    			return -1;
    		}
    
    		/* check for existence of a dosemu.conf in the door directory.
    		 * It is a good idea to be able to use separate configs for each
    		 * door. 
    		 *
    		 * First check startup_dir, then check cfg.ctrl_dir
    		 */
    		SAFEPRINTF2(str,"%s%s",startup_dir, dosemu_cnf_fn);
    		if (!fexist(str)) {
    			/* If we can't find it in the door dir, look for the configured one */
    			SAFECOPY(str,startup->dosemuconf_path);
    			if (!isabspath(str)) {
    				SAFEPRINTF2(str,"%s%s", cfg.ctrl_dir, startup->dosemuconf_path);
    			}
    			if (!fexist(str)) {
    				/* If we couldn't find either, try for the system one, then
    				 * error out. */
    				SAFEPRINTF(str,"/etc/dosemu/%s", dosemu_cnf_fn);
    				if (!fexist(str)) {
    					SAFEPRINTF(str,"/etc/%s", dosemu_cnf_fn);
    					if (!fexist(str)) {
    						errormsg(WHERE,ERR_READ,str,0);
    						return(-1);
    					}
    					else SAFECOPY(dosemuconf,str);  /* using system conf */
    				}
    				else SAFECOPY(dosemuconf,str);  /* using system conf */
    			}
    			else SAFECOPY(dosemuconf,str);   /* using global conf */
    		}
    		else SAFECOPY(dosemuconf,str);  /* using door-specific conf */
    
    		/* Create the external bat here to be placed in the node dir. */
    		SAFEPRINTF2(str,"%s%s",cfg.node_dir,external_bat_fn);
    		if(!(dosemubatfp=fopen(str,"w+"))) {
    			errormsg(WHERE,ERR_CREATE,str,0);
    			return(-1);
    		}
    
    		fprintf(dosemubatfp,"@ECHO OFF\r\n");
    		fprintf(dosemubatfp,"SET DSZLOG=%s\\PROTOCOL.LOG\r\n",nodedrive);
    		fprintf(dosemubatfp,"SET SBBSNODE=%s\r\n",nodedrive);
    		fprintf(dosemubatfp,"SET SBBSNNUM=%d\r\n",cfg.node_num);
    		fprintf(dosemubatfp,"SET SBBSCTRL=%s\r\n",ctrldrive);
    		fprintf(dosemubatfp,"SET SBBSDATA=%s\r\n",datadrive);
    		fprintf(dosemubatfp,"SET SBBSEXEC=%s\r\n",execdrive);
    		fprintf(dosemubatfp,"SET PCBNODE=%d\r\n",cfg.node_num);
    		fprintf(dosemubatfp,"SET PCBDRIVE=%s\r\n",nodedrive);
    		fprintf(dosemubatfp,"SET PCBDIR=\\\r\n");
    
    		char gamedir[MAX_PATH+1];
    		if(startup_dir!=NULL && startup_dir[0]) {
    			SAFECOPY(str, startup_dir);
    			*lastchar(str) = 0;
    			SAFECOPY(gamedir, getfname(str));
    		}
     
    		if(*gamedir == 0) {
    			lprintf(LOG_ERR, "No startup directory configured for DOS command-line: %s", cmdline);
    			fclose(dosemubatfp);
    			return -1;
    		}
    
    		/* external editors use node dir so unset this */
    		if (startup_dir == cfg.node_dir) {
    			*gamedir = '\0';
    		}
    		
    		fprintf(dosemubatfp,"SET STARTDIR=%s\r\n",gamedir);
    
    		/* Check the "Stdio Interception" flag from scfg for this door.  If it's
    		 * enabled, we enable doorway mode.  Else, it's vmodem for us, unless
    		 * it's a timed event.
    		 */
    
    		if (!(mode&(EX_STDIO)) && online!=ON_LOCAL) {
    			SAFECOPY(virtualconf,"-I\"serial { virtual com 1 }\"");
    			runtype = "FOSSIL";
    		} else {
    			virtualconf[0] = '\0';
    			runtype = "STDIO";
    		}
    
    		/* now append exec/external.bat (which is editable) to this 
    		 generated file */
    		SAFEPRINTF2(str,"%s%s",startup_dir,external_bat_fn);
    
    		if ((startup_dir == cfg.node_dir) || !fexist(str)) {
    			SAFEPRINTF2(str,"%s%s",cfg.exec_dir, external_bat_fn);
    			if (!fexist(str)) {
    				errormsg(WHERE,ERR_READ,str,0);
    				fclose(dosemubatfp);
    				return(-1);
    			} 
    		}
    
            SAFECOPY(externalbatsrc, str); 
    
    		if (!(externalbatfp=fopen(externalbatsrc,"r"))) {
    			errormsg(WHERE,ERR_OPEN,externalbatsrc,0);
    			fclose(dosemubatfp);
    			return(-1);
    		} 
    
    		/* append the command line to the batch file */
    		SAFECOPY(tok,cmdline);
    		truncstr(tok," ");
    		p = getfext(tok);  
    		/*  check if it's a bat file  */
    		if (p != NULL && stricmp(p, ".bat") == 0) {
    			SAFEPRINTF(cmdlinebatch, "CALL %s", cmdline);
    		} else {
    			SAFECOPY(cmdlinebatch, cmdline);
    		}
    
    		named_string_t externalbat_replacements[] = {
    			{(char*)"CMDLINE", cmdlinebatch},
    			{(char*)"DSZLOG", (char*)nodedrive},
    			{(char*)"SBBSNODE", (char*)nodedrive},
    			{(char*)"SBBSCTRL", (char*)ctrldrive},
    			{(char*)"SBBSDATA", (char*)datadrive},
    			{(char*)"SBBSEXEC", (char*)execdrive},
    			{(char*)"XTRNDIR", xtrndir_dos},
    			{(char*)"CTRLDIR", ctrldir_dos},
    			{(char*)"DATADIR", datadir_dos},
    			{(char*)"EXECDIR", execdir_dos},
    			{(char*)"NODEDIR", nodedir_dos},
    			{(char*)"STARTDIR", (char*)gamedir},
    			{(char*)"RUNTYPE", (char *)runtype},
    			{NULL, NULL}
    		};
    
    		named_int_t externalbat_int_replacements[] = {
    		    {(char*)"SBBSNNUM", cfg.node_num },
    		};
    
    		while(!feof(externalbatfp)) {
    			if (fgets(buf, sizeof(buf), externalbatfp)!=NULL) {
    				replace_named_values(buf, bufout, sizeof(bufout), "$", externalbat_replacements, 
    					externalbat_int_replacements, FALSE);
    				fprintf(dosemubatfp,"%s",bufout);
    			}
    		}
    
    		fclose(externalbatfp);
    
    		/* Set the interception bits, since we are always going to want Synchronet
    		 * to intercept dos programs under Unix.
    		 */
    
    		mode |= EX_STDIO;
    
    		/* Attempt to keep dosemu from prompting for a disclaimer. */
    
    		sprintf(str, "%s/.dosemu", cfg.ctrl_dir);
    		if (!isdir(str)) {
    			mkdir(str, 0755);
    		}
    		strcat(str, "/disclaimer");
    		ftouch(str);
    
    		/* Set up the command for dosemu to execute with 'unix -e'. */
    		SAFEPRINTF2(externalbat,"%s%s",nodedrive, external_bat_fn);
    
    		/* need TERM=linux for maintenance programs to work
    		 * (dosemu won't start with no controlling terminal)
    		 * Also, redirect stdout to a log if it's a timed event.
    		 */
    
    		if (online==ON_LOCAL) {
    			SAFECOPY(dosterm,"TERM=linux");
    			safe_snprintf(log_external, sizeof(log_external), ">> %sdosevent_%s.log",cfg.logs_dir,fname);
    		}
    		else {
    			dosterm[0]='\0';
    			log_external[0] = '\0';
    		}
    
    		/*
    		 * Get the global emu launch command
    		 */
    		 /* look for file in startup dir */
    		SAFEPRINTF(str,"%sdosemu.ini",startup_dir);
    		if (!fexist(str)) {
    			/* look for file in exec dir */        
        		SAFEPRINTF(str,"%sdosemu.ini",cfg.exec_dir);
    			if (!fexist(str)) {
    				errormsg(WHERE,ERR_OPEN,"dosemu.ini", 0);
    				fclose(dosemubatfp);
    				return(-1);
    			}
    		}
    
    		/* if file found, then open and process it */
    		if ((de_launch_inifp=iniOpenFile(str, false))==NULL) {
    			errormsg(WHERE,ERR_OPEN,str, 0);
    			fclose(dosemubatfp);
    			return(-1);
    		}         
    		de_launch_ini = iniReadFile(de_launch_inifp);
    		iniCloseFile(de_launch_inifp);
    		SAFECOPY(de_launch_cmd, "");
    		iniGetString(de_launch_ini, ROOT_SECTION, "cmd", nulstr, de_launch_cmd);
    		if (virtualconf[0] == '\0') {
    			iniGetString(de_launch_ini, "stdio", "cmd", de_launch_cmd, de_launch_cmd);
    		}
    		iniFreeStringList(de_launch_ini);
    
    		named_string_t de_ini_replacements[] = 
    		{
    			{(char*)"TERM", dosterm}, 
    			{(char*)"CTRLDIR", cfg.ctrl_dir},
    			{(char*)"NODEDIR", cfg.node_dir},
    			{(char*)"DOSEMUBIN", dosemubinloc},
    			{(char*)"VIRTUALCONF", virtualconf},
    			{(char*)"DOSEMUCONF", dosemuconf},
    			{(char*)"EXTBAT", externalbat},
    			{(char*)"EXTLOG", log_external},
    			{(char*)"RUNTYPE", (char *)runtype},
    			{NULL, NULL}
    		};
    		named_int_t de_ini_int_replacements[] = {
                {(char*)"NNUM", cfg.node_num },
            };
    		replace_named_values(de_launch_cmd, fullcmdline, sizeof(fullcmdline), (char*)"$", 
    			de_ini_replacements, de_ini_int_replacements, FALSE);
    
    		/* Drum roll. */      
    		fprintf(dosemubatfp,"REM For debugging: %s\r\n",fullcmdline);
    		fclose(dosemubatfp);
    
    #else
    		lprintf((mode&EX_OFFLINE) ? LOG_ERR : LOG_WARNING, "DOS programs not supported: %s", cmdline);
    		bprintf("Sorry, DOS programs are not supported on this node.\r\n");
    
    		return(-1);
    #endif
    	}
    
    	if(!(mode&EX_STDIN)) {
    		if(!(mode&EX_STDIN)) {
    			if(passthru_thread_running)
    				passthru_socket_activate(true);
    			else
    				pthread_mutex_lock(&input_thread_mutex);
    		}
    	}
    
    	if(!(mode&EX_NOLOG) && pipe(err_pipe)!=0) {
    		errormsg(WHERE,ERR_CREATE,"err_pipe",0);
    		return(-1);
    	}
    
    	if((mode&EX_STDIO)==EX_STDIO)  {
    		struct winsize winsize;
    		struct termios term;
    		memset(&term,0,sizeof(term));
    		cfsetispeed(&term,B19200);
    		cfsetospeed(&term,B19200);
    		if(mode&EX_BIN)
    			cfmakeraw(&term);
    		else {
    			term.c_iflag = TTYDEF_IFLAG;
    			term.c_oflag = TTYDEF_OFLAG;
    			term.c_lflag = TTYDEF_LFLAG;
    			term.c_cflag = TTYDEF_CFLAG;
    			memcpy(term.c_cc,ttydefchars,sizeof(term.c_cc));
    		}
    		winsize.ws_row=rows;
    		winsize.ws_col=cols;
    		if((pid=forkpty(&in_pipe[1],NULL,&term,&winsize))==-1) {
    			if(!(mode&EX_STDIN)) {
    				if(passthru_thread_running)
    					passthru_socket_activate(false);
    				else
    					pthread_mutex_unlock(&input_thread_mutex);
    			}
    			errormsg(WHERE,ERR_EXEC,fullcmdline,0);
    			return(-1);
    		}
    		out_pipe[0]=in_pipe[1];
    	}
    	else  {
    		if(mode&EX_STDIN)
    			if(pipe(in_pipe)!=0) {
    				errormsg(WHERE,ERR_CREATE,"in_pipe",0);
    				return(-1);
    			}
    		if(mode&EX_STDOUT)
    			if(pipe(out_pipe)!=0) {
    				errormsg(WHERE,ERR_CREATE,"out_pipe",0);
    				return(-1);
    			}
    
    
    		if((pid=FORK())==-1) {
    			if(!(mode&EX_STDIN)) {
    				if(passthru_thread_running)
    					passthru_socket_activate(false);
    				else
    					pthread_mutex_unlock(&input_thread_mutex);
    			}
    			errormsg(WHERE,ERR_EXEC,fullcmdline,0);
    			return(-1);
    		}
    	}
    	if(pid==0) {	/* child process */
    		/* Give away all privs for good now */
    		if(startup->setuid!=NULL)
    			startup->setuid(TRUE);
    
    		sigset_t        sigs;
    		sigfillset(&sigs);
    		sigprocmask(SIG_UNBLOCK,&sigs,NULL);
    		if(!(mode&EX_BIN))  {
    			if(term_supports(ANSI))
    				SAFEPRINTF(term_env,"TERM=%s",startup->xtrn_term_ansi);
    			else
    				SAFEPRINTF(term_env,"TERM=%s",startup->xtrn_term_dumb);
    			putenv(term_env);
    		}
    #ifdef __FreeBSD__
    		if(!native)
    			chdir(cfg.node_dir);
    		else
    #endif
    		if(startup_dir!=NULL && startup_dir[0])
    			if(chdir(startup_dir)!=0) {
    				errormsg(WHERE,ERR_CHDIR,startup_dir,0);
    				return(-1);
    			}
    
    		if(mode&EX_SH || strcspn(fullcmdline,"<>|;\"")!=strlen(fullcmdline)) {
    			argv[0]=comspec;
    			argv[1]=(char*)"-c";
    			argv[2]=fullcmdline;
    			argv[3]=NULL;
    		} else {
    			argv[0]=fullcmdline;	/* point to the beginning of the string */
    			argc=1;
    			for(i=0;fullcmdline[i] && argc<MAX_ARGS;i++)	/* Break up command line */
    				if(fullcmdline[i]==' ') {
    					fullcmdline[i]=0;			/* insert nulls */
    					argv[argc++]=fullcmdline+i+1; /* point to the beginning of the next arg */
    				}
    			argv[argc]=NULL;
    		}
    
    		if(mode&EX_STDIN && !(mode&EX_STDOUT))  {
    			close(in_pipe[1]);		/* close write-end of pipe */
    			dup2(in_pipe[0],0);		/* redirect stdin */
    			close(in_pipe[0]);		/* close excess file descriptor */
    		}
    
    		if(mode&EX_STDOUT && !(mode&EX_STDIN)) {
    			close(out_pipe[0]);		/* close read-end of pipe */
    			dup2(out_pipe[1],1);	/* stdout */
    			if(!(mode&EX_NOLOG))
    				dup2(out_pipe[1],2);	/* stderr */
    			close(out_pipe[1]);		/* close excess file descriptor */
    		}
    
    		if(!(mode & EX_STDIO)) {
    			int fd;
    
    			/* Redirect stdio to /dev/null */
    			if ((fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
    				dup2(fd, STDIN_FILENO);
    				dup2(fd, STDOUT_FILENO);
    				if(!(mode&EX_NOLOG))
    					dup2(fd, STDERR_FILENO);
    				if (fd > 2)
    					close(fd);
    			}
    		}
    
    		if(mode&EX_BG)	/* background execution, detach child */
    		{
    			lprintf(LOG_INFO,"Detaching external process");
    			daemon(TRUE,FALSE);
       	    }
    
    		if(!(mode&EX_NOLOG)) {
    			close(err_pipe[0]);		/* close read-end of pipe */
    			dup2(err_pipe[1],2);	/* stderr */
    		}
    
    		execvp(argv[0],argv);
    		lprintf(LOG_ERR,"!ERROR %d (%s) executing: %s", errno, strerror(errno), argv[0]);
    		_exit(-1);	/* should never get here */
    	}
    
    	if(strcmp(cmdline, fullcmdline) != 0)
    		lprintf(LOG_DEBUG,"Executing cmd-line: %s", fullcmdline);
    
    	/* Disable Ctrl-C checking */
    	if(!(mode&EX_OFFLINE))
    		rio_abortable=false;
    
    	if(!(mode&EX_NOLOG))
    		close(err_pipe[1]);	/* close write-end of pipe */
    
    	if(mode&EX_STDOUT) {
    		if(!(mode&EX_STDIN))
    			close(out_pipe[1]);	/* close write-end of pipe */
    		while(!terminated) {
    			if(waitpid(pid, &i, WNOHANG)!=0)	/* child exited */
    				break;
    
    			if(mode&EX_CHKTIME)
    				gettimeleft();
    
    			if(!online && !(mode&EX_OFFLINE)) {
    				logline(LOG_NOTICE,"X!","hung-up in external program");
    				break;
    			}
    
    			/* Input */
    			if(mode&EX_STDIN && RingBufFull(&inbuf)) {
    				if((wr=RingBufRead(&inbuf,buf,sizeof(buf)))!=0)
    					write(in_pipe[1],buf,wr);
    			}
    
    			/* Error Output */
    			FD_ZERO(&ibits);
    			if(!(mode&EX_NOLOG)) {
    				FD_SET(err_pipe[0],&ibits);
    				high_fd=err_pipe[0];
    			}
    			FD_SET(out_pipe[0],&ibits);
    			if(!(mode&EX_NOLOG)) {
    				if(out_pipe[0]>err_pipe[0])
    					high_fd=out_pipe[0];
    			} else
    				high_fd=out_pipe[0];
    			timeout.tv_sec=0;
    			timeout.tv_usec=1000;
    			bp=buf;
    			i=0;
    			if(mode&EX_NOLOG)
    				select(high_fd+1,&ibits,NULL,NULL,&timeout);
    			else {
    				while ((select(high_fd+1,&ibits,NULL,NULL,&timeout)>0) && FD_ISSET(err_pipe[0],&ibits) && (i<(int)sizeof(buf)-1))  {
    					if((rd=read(err_pipe[0],bp,1))>0)  {
    						i+=rd;
    						bp++;
    						if(*(bp-1)=='\n')
    							break;
    					}
    					else
    						break;
    					FD_ZERO(&ibits);
    					FD_SET(err_pipe[0],&ibits);
    					FD_SET(out_pipe[0],&ibits);
    					timeout.tv_sec=0;
    					timeout.tv_usec=1000;
    				}
    				if(i > 0) {
    					buf[i] = '\0';
    					p = (char*)buf;
    					truncsp(p);
    					SKIP_WHITESPACE(p);
    					if(*p)
    						lprintf(LOG_NOTICE, "%s: %s", fname, p);
    				}
    
    				/* Eat stderr if mode is EX_BIN */
    				if(mode&EX_BIN)  {
    					bp=buf;
    					i=0;
    				}
    			}
    
    			data_waiting=FD_ISSET(out_pipe[0],&ibits);
    			if(i==0 && data_waiting==0)
    				continue;
    
    			avail=(RingBufFree(&outbuf)-i)/2;	// Leave room for wwiv/telnet expansion
    			if(avail==0) {
    #if 0
    				lprintf("Node %d !output buffer full (%u bytes)"
    						,cfg.node_num,RingBufFull(&outbuf));
    #endif
    				YIELD();
    				continue;
    			}
    
    			rd=avail;
    
    			if(rd>((int)sizeof(buf)-i))
    				rd=sizeof(buf)-i;
    
    			if(data_waiting)  {
    				rd=read(out_pipe[0],bp,rd);
    				if(rd<1 && i==0)
    					continue;
    				if(rd<0)
    					rd=0;
    			}
    			else
    				rd=0;
    
    			rd += i;
    
    			if(mode&EX_BIN) {
    				if(telnet_mode&TELNET_MODE_OFF) {
    					bp=buf;
    					output_len=rd;
    				}
    				else
       	       			output_len = telnet_expand(buf, rd, output_buf, sizeof(output_buf), /* expand_cr: */false, &bp);
    			} else {
    				if ((mode & EX_STDIO) != EX_STDIO) {
    					/* LF to CRLF expansion */
    					bp=lf_expand(buf, rd, output_buf, output_len);
    				} else if(mode&EX_WWIV) {
    					bp=wwiv_expand(buf, rd, wwiv_buf, output_len, useron.misc, wwiv_flag);
    					if(output_len > sizeof(wwiv_buf))
    						lprintf(LOG_ERR, "WWIV_BUF OVERRUN");
    				} else {
    					bp=buf;
    					output_len=rd;
    				}
    				if (term_supports(PETSCII))
    					petscii_convert(bp, output_len);
    			}
    			/* Did expansion overrun the output buffer? */
    			if(output_len>sizeof(output_buf)) {
    				lprintf(LOG_ERR,"OUTPUT_BUF OVERRUN");
    				output_len=sizeof(output_buf);
    			}
    
    			/* Does expanded size fit in the ring buffer? */
    			if(output_len>RingBufFree(&outbuf)) {
    				lprintf(LOG_ERR,"output buffer overflow");
    				output_len=RingBufFree(&outbuf);
    			}
    
    			RingBufWrite(&outbuf, bp, output_len);
    
    		}
    
    		if(waitpid(pid, &i, WNOHANG)==0)  {		// Child still running?
    			kill(pid, SIGHUP);					// Tell child user has hung up
    			time_t start=time(NULL);			// Wait up to 10 seconds
    			while(time(NULL)-start<10) {		// for child to terminate
    				if(waitpid(pid, &i, WNOHANG)!=0)
    					break;
    				mswait(500);
    			}
    			if(waitpid(pid, &i, WNOHANG)==0)	// Child still running?
    				kill(pid, SIGKILL);				// terminate child process
    		}
    		/* close unneeded descriptors */
    		if(mode&EX_STDIN)
    			close(in_pipe[1]);
    		close(out_pipe[0]);
    	}
    	if(mode&EX_NOLOG)
    		waitpid(pid, &i, 0);
    	else {
    		while(waitpid(pid, &i, WNOHANG)==0)  {
    			FD_ZERO(&ibits);
    			FD_SET(err_pipe[0],&ibits);
    			timeout.tv_sec=1;
    			timeout.tv_usec=0;
    			bp=buf;
    			i=0;
    			while ((select(err_pipe[0]+1,&ibits,NULL,NULL,&timeout)>0) && (i<XTRN_IO_BUF_LEN-1))  {
    				if((rd=read(err_pipe[0],bp,1))>0)  {
    					i+=rd;
    					if(*bp=='\n') {
    						buf[i] = '\0';
    						p = (char*)buf;
    						truncsp(p);
    						SKIP_WHITESPACE(p);
    						if(*p)
    							lprintf(LOG_NOTICE, "%s: %s", fname, p);
    						i=0;
    						bp=buf;
    					}
    					else
    						bp++;
    				}
    				else
    					break;
    			}
    			if(i > 0) {
    				buf[i] = '\0';
    				p = (char*)buf;
    				truncsp(p);
    				SKIP_WHITESPACE(p);
    				if(*p)
    					lprintf(LOG_NOTICE, "%s: %s", fname, p);
    			}
    		}
    	}
    	if(!(mode&EX_OFFLINE)) {	/* !off-line execution */
    
    		curatr=~0;			// Can't guarantee current attributes
    		attr(LIGHTGRAY);	// Force to "normal"
    
    		rio_abortable=rio_abortable_save;	// Restore abortable state
    
    		/* Got back to Text/NVT mode */
    		request_telnet_opt(TELNET_DONT,TELNET_BINARY_TX);
    	}
    
    	if(!(mode&EX_NOLOG))
    		close(err_pipe[0]);
    
    	if(!(mode&EX_STDIN)) {
    		if(passthru_thread_running)
    			passthru_socket_activate(false);
    		else
    			pthread_mutex_unlock(&input_thread_mutex);
    	}
    
    	return(errorlevel = WEXITSTATUS(i));
    }
    
    #endif	/* !WIN32 */
    
    const char* quoted_string(const char* str, char* buf, size_t maxlen)
    {
    	if(strchr(str,' ')==NULL)
    		return(str);
    	safe_snprintf(buf,maxlen,"\"%s\"",str);
    	return(buf);
    }
    
    #define QUOTED_STRING(ch, str, buf, maxlen) \
    	((IS_ALPHA(ch) && IS_UPPERCASE(ch)) ? str : quoted_string(str,buf,maxlen))
    
    /*****************************************************************************/
    /* Returns command line generated from instr with %c replacements            */
    /*****************************************************************************/
    char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, char *outstr, long mode)
    {
    	char	str[MAX_PATH+1],*cmd;
        int		i,j,len;
    	bool	native = (mode == EX_UNSPECIFIED) || native_executable(&cfg, instr, mode);
    	(void) native;
    
        if(outstr==NULL)
            cmd=cmdstr_output;
        else
            cmd=outstr;
        len=strlen(instr);
    	int maxlen = (int)sizeof(cmdstr_output) - 1;
        for(i=j=0; i<len && j < maxlen; i++) {
            if(instr[i]=='%') {
                i++;
                cmd[j]=0;
    			int avail = maxlen - j;
    			char ch=instr[i];
    			if(IS_ALPHA(ch))
    				ch=toupper(ch);
                switch(ch) {
                    case 'A':   /* User alias */
                        strncat(cmd,QUOTED_STRING(instr[i],useron.alias,str,sizeof(str)), avail);
                        break;
                    case 'B':   /* Baud (DTE) Rate */
                        strncat(cmd,ultoa(dte_rate,str,10), avail);
                        break;
                    case 'C':   /* Connect Description */
                        strncat(cmd,connection, avail);
                        break;
                    case 'D':   /* Connect (DCE) Rate */
                        strncat(cmd,ultoa((ulong)cur_rate,str,10), avail);
                        break;
                    case 'E':   /* Estimated Rate */
                        strncat(cmd,ultoa((ulong)cur_cps*10,str,10), avail);
                        break;
                    case 'F':   /* File path */
    #if defined(__linux__)
    					if(!native && strncmp(fpath, cfg.node_dir, strlen(cfg.node_dir)) == 0) {
    						strncat(cmd, DOSEMU_NODE_DIR, avail);
    						strncat(cmd, fpath + strlen(cfg.node_dir), avail);
    					}
    					else
    #endif
    						strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
                        break;
                    case 'G':   /* Temp directory */
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_TEMP_DIR, avail);
    					else
    #endif
    	                    strncat(cmd,cfg.temp_dir, avail);
                        break;
                    case 'H':   /* Socket Handle */
                        strncat(cmd,ultoa(client_socket_dup,str,10), avail);
                        break;
                    case 'I':   /* IP address */
                        strncat(cmd,cid, avail);
                        break;
                    case 'J':
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_DATA_DIR, avail);
    					else
    #endif
    						strncat(cmd,cfg.data_dir, avail);
                        break;
                    case 'K':
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_CTRL_DIR, avail);
    					else
    #endif
    	                    strncat(cmd,cfg.ctrl_dir, avail);
                        break;
                    case 'L':   /* Lines per message */
                        strncat(cmd,ultoa(cfg.level_linespermsg[useron.level],str,10), avail);
                        break;
                    case 'M':   /* Minutes (credits) for user */
                        strncat(cmd,ultoa(useron.min,str,10), avail);
                        break;
                    case 'N':   /* Node Directory (same as SBBSNODE environment var) */
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_NODE_DIR, avail);
    					else
    #endif
    	                    strncat(cmd,cfg.node_dir, avail);
                        break;
                    case 'O':   /* SysOp */
                        strncat(cmd,QUOTED_STRING(instr[i],cfg.sys_op,str,sizeof(str)), avail);
                        break;
                    case 'P':   /* Client protocol */
                        strncat(cmd, passthru_thread_running ? "raw" : client.protocol, avail);
                        break;
                    case 'Q':   /* QWK ID */
                        strncat(cmd,cfg.sys_id, avail);
                        break;
                    case 'R':   /* Rows */
                        strncat(cmd,ultoa(rows,str,10), avail);
                        break;
                    case 'S':   /* File Spec (or Baja command str) or startup-directory */
                        strncat(cmd, fspec, avail);
                        break;
                    case 'T':   /* Time left in seconds */
                        gettimeleft();
                        strncat(cmd,ultoa(timeleft,str,10), avail);
                        break;
                    case 'U':   /* UART I/O Address (in hex) */
                        strncat(cmd,ultoa(cfg.com_base,str,16), avail);
                        break;
                    case 'V':   /* Synchronet Version */
                        sprintf(str,"%s%c",VERSION,REVISION);
    					strncat(cmd,str, avail);
                        break;
                    case 'W':   /* Columns (width) */
                        strncat(cmd,ultoa(cols,str,10), avail);
                        break;
                    case 'X':
                        strncat(cmd,cfg.shell[useron.shell]->code, avail);
                        break;
                    case '&':   /* Address of msr */
                        break;
                    case 'Y':
                        strncat(cmd,comspec, avail);
                        break;
                    case 'Z':
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_TEXT_DIR, avail);
    					else
    #endif
    	                    strncat(cmd,cfg.text_dir, avail);
                        break;
    				case '~':	/* DOS-compatible (8.3) filename */
    #ifdef _WIN32
    					char sfpath[MAX_PATH+1];
    					SAFECOPY(sfpath,fpath);
    					GetShortPathName(fpath,sfpath,sizeof(sfpath));
    					strncat(cmd,sfpath, avail);
    #else
                        strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
    #endif
    					break;
                    case '!':   /* EXEC Directory */
    #if defined(__linux__)
    					if(!native)
    						strncat(cmd, DOSEMU_EXEC_DIR, avail);
    					else
    #endif
    	                    strncat(cmd,cfg.exec_dir, avail);
                        break;
                    case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
    #ifndef __unix__
                        strncat(cmd,cfg.exec_dir, avail);
    #endif
                        break;
    
                    case '#':   /* Node number (same as SBBSNNUM environment var) */
                        sprintf(str,"%d",cfg.node_num);
                        strncat(cmd,str, avail);
                        break;
                    case '*':
                        sprintf(str,"%03d",cfg.node_num);
                        strncat(cmd,str, avail);
                        break;
                    case '$':   /* Credits */
                        strncat(cmd,ultoa(useron.cdt+useron.freecdt,str,10), avail);
                        break;
                    case '%':   /* %% for percent sign */
                        strncat(cmd,"%", avail);
                        break;
    				case '.':	/* .exe for DOS/OS2/Win32, blank for Unix */
    #ifndef __unix__
    					strncat(cmd,".exe", avail);
    #endif
    					break;
    				case '?':	/* Platform */
    #ifdef __OS2__
    					strcpy(str,"OS2");
    #else
    					strcpy(str,PLATFORM_DESC);
    #endif
    					strlwr(str);
    					strncat(cmd,str, avail);
    					break;
    				case '^':	/* Architecture */
    					strncat(cmd, ARCHITECTURE_DESC, avail);
    					break;
                    default:    /* unknown specification */
                        if(IS_DIGIT(instr[i])) {
                            sprintf(str,"%0*d",instr[i]&0xf,useron.number);
                            strncat(cmd,str, avail); }
                        break; }
                j=strlen(cmd); }
            else
                cmd[j++]=instr[i]; }
        cmd[j]=0;
    
        return(cmd);
    }
    
    /****************************************************************************/
    /* Returns command line generated from instr with %c replacments            */
    /* This is the C-exported version											*/
    /****************************************************************************/
    extern "C"
    char* DLLCALL cmdstr(scfg_t* cfg, user_t* user, const char* instr, const char* fpath
    						,const char* fspec, char* cmd)
    {
    	char	str[MAX_PATH+1];
        int		i,j,len;
    	static char	buf[512];
    
    	if(cmd==NULL)	cmd=buf;
        len=strlen(instr);
    	int maxlen = (int)sizeof(buf) - 1;
        for(i=j=0; i<len && j < maxlen; i++) {
            if(instr[i]=='%') {
                i++;
                cmd[j]=0;
    			int avail = maxlen - j;
    			char ch=instr[i];
    			if(IS_ALPHA(ch))
    				ch=toupper(ch);
                switch(ch) {
                    case 'A':   /* User alias */
    					if(user!=NULL)
    						strncat(cmd,QUOTED_STRING(instr[i],user->alias,str,sizeof(str)), avail);
                        break;
                    case 'B':   /* Baud (DTE) Rate */
                        break;
                    case 'C':   /* Connect Description */
    					if(user!=NULL)
    						strncat(cmd,user->modem, avail);
                        break;
                    case 'D':   /* Connect (DCE) Rate */
                        break;
                    case 'E':   /* Estimated Rate */
                        break;
                    case 'F':   /* File path */
                        strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
                        break;
                    case 'G':   /* Temp directory */
                        strncat(cmd,cfg->temp_dir, avail);
                        break;
                    case 'H':   /* Port Handle or Hardware Flow Control */
                        break;
                    case 'I':   /* IP address */
    					if(user!=NULL)
    						strncat(cmd,user->note, avail);
                        break;
                    case 'J':
                        strncat(cmd,cfg->data_dir, avail);
                        break;
                    case 'K':
                        strncat(cmd,cfg->ctrl_dir, avail);
                        break;
                    case 'L':   /* Lines per message */
    					if(user!=NULL)
    						strncat(cmd,ultoa(cfg->level_linespermsg[user->level],str,10), avail);
                        break;
                    case 'M':   /* Minutes (credits) for user */
    					if(user!=NULL)
    						strncat(cmd,ultoa(user->min,str,10), avail);
                        break;
                    case 'N':   /* Node Directory (same as SBBSNODE environment var) */
                        strncat(cmd,cfg->node_dir, avail);
                        break;
                    case 'O':   /* SysOp */
                        strncat(cmd,QUOTED_STRING(instr[i],cfg->sys_op,str,sizeof(str)), avail);
                        break;
                    case 'P':   /* Client protocol */
                        break;
                    case 'Q':   /* QWK ID */
                        strncat(cmd,cfg->sys_id, avail);
                        break;
                    case 'R':   /* Rows */
    					if(user!=NULL)
    						strncat(cmd,ultoa(user->rows,str,10), avail);
                        break;
                    case 'S':   /* File Spec */
                        strncat(cmd, fspec, avail);
                        break;
                    case 'T':   /* Time left in seconds */
                        break;
                    case 'U':   /* UART I/O Address (in hex) */
                        strncat(cmd,ultoa(cfg->com_base,str,16), avail);
                        break;
                    case 'V':   /* Synchronet Version */
                        sprintf(str,"%s%c",VERSION,REVISION);
    					strncat(cmd,str, avail);
                        break;
                    case 'W':   /* Columns/width */
                        break;
                    case 'X':
    					if(user!=NULL)
    						strncat(cmd,cfg->shell[user->shell]->code, avail);
                        break;
                    case '&':   /* Address of msr */
                        break;
                    case 'Y':
                        break;
                    case 'Z':
                        strncat(cmd,cfg->text_dir, avail);
                        break;
    				case '~':	/* DOS-compatible (8.3) filename */
    #ifdef _WIN32
    					char sfpath[MAX_PATH+1];
    					SAFECOPY(sfpath,fpath);
    					GetShortPathName(fpath,sfpath,sizeof(sfpath));
    					strncat(cmd,sfpath, avail);
    #else
                        strncat(cmd,QUOTED_STRING(instr[i],fpath,str,sizeof(str)), avail);
    #endif
    					break;
                    case '!':   /* EXEC Directory */
                        strncat(cmd,cfg->exec_dir, avail);
                        break;
                    case '@':   /* EXEC Directory for DOS/OS2/Win32, blank for Unix */
    #ifndef __unix__
                        strncat(cmd,cfg->exec_dir, avail);
    #endif
                        break;
    
                    case '#':   /* Node number (same as SBBSNNUM environment var) */
                        sprintf(str,"%d",cfg->node_num);
                        strncat(cmd,str, avail);
                        break;
                    case '*':
                        sprintf(str,"%03d",cfg->node_num);
                        strncat(cmd,str, avail);
                        break;
                    case '$':   /* Credits */
    					if(user!=NULL)
    						strncat(cmd,ultoa(user->cdt+user->freecdt,str,10), avail);
                        break;
                    case '%':   /* %% for percent sign */
                        strncat(cmd,"%", avail);
                        break;
    				case '.':	/* .exe for DOS/OS2/Win32, blank for Unix */
    #ifndef __unix__
    					strncat(cmd,".exe", avail);
    #endif
    					break;
    				case '?':	/* Platform */
    #ifdef __OS2__
    					strcpy(str,"OS2");
    #else
    					strcpy(str,PLATFORM_DESC);
    #endif
    					strlwr(str);
    					strncat(cmd,str, avail);
    					break;
    				case '^':	/* Architecture */
    					strncat(cmd, ARCHITECTURE_DESC, avail);
    					break;
                    default:    /* unknown specification */
                        if(IS_DIGIT(instr[i]) && user!=NULL) {
                            sprintf(str,"%0*d",instr[i]&0xf,user->number);
                            strncat(cmd,str, avail);
    					}
                        break;
    			}
                j=strlen(cmd);
    		}
            else
                cmd[j++]=instr[i];
    	}
        cmd[j]=0;
    
        return(cmd);
    }