/*
 * Copyright (c) 1993-2010, Paul Mattes.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the names of Paul Mattes nor the names of his contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY PAUL MATTES "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL PAUL MATTES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 *      macros.c
 *              This module handles string, macro and script (sms) processing.
 */

#include "globals.h"

#if defined(X3270_MENUS) /*[*/
#include <X11/StringDefs.h>
#include <X11/Xaw/Dialog.h>
#endif /*]*/

#if !defined(_WIN32) /*[*/
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#endif /*]*/
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include "3270ds.h"
#include "appres.h"
#include "ctlr.h"
#include "screen.h"
#include "resources.h"

#include "actionsc.h"
#include "childc.h"
#include "ctlrc.h"
#include "ftc.h"
#include "hostc.h"
#include "idlec.h"
#include "kybdc.h"
#include "macrosc.h"
#include "menubarc.h"
#include "popupsc.h"
#if defined(X3270_PRINTER) /*[*/
#include "printerc.h"
#endif /*]*/
#include "screenc.h"
#include "seec.h"
#include "statusc.h"
#include "tablesc.h"
#include "telnetc.h"
#include "trace_dsc.h"
#include "unicodec.h"
#include "utf8c.h"
#include "utilc.h"
#include "xioc.h"

#if defined(_WIN32) /*[*/
#include "windows.h"
#include <ws2tcpip.h>
#include "w3miscc.h"
#endif /*]*/

#if defined(_MSC_VER) /*[*/
#include "Msc/deprecated.h"
#endif /*]*/

#define ANSI_SAVE_SIZE	4096

#if defined(_WIN32) /*[*/
#define SOCK_CLOSE(s)	closesocket(s)
#else /*][*/
#define SOCK_CLOSE(s)	close(s)
#endif /*[*/

/* Externals */
extern int      linemode;

/* Globals */
struct macro_def *macro_defs = (struct macro_def *)NULL;
Boolean		macro_output = False;

/* Statics */
typedef struct sms {
	struct sms *next;	/* next sms on the stack */
	char	msc[1024];	/* input buffer */
	int	msc_len;	/* length of input buffer */
	char   *dptr;		/* data pointer (macros only) */
	enum sms_state {
		SS_IDLE,	/* no command active (scripts only) */
		SS_INCOMPLETE,	/* command(s) buffered and ready to run */
		SS_RUNNING,	/* command executing */
		SS_KBWAIT,	/* command awaiting keyboard unlock */
		SS_CONNECT_WAIT,/* command awaiting connection to complete */
#if defined(X3270_FT) /*[*/
		SS_FT_WAIT,	/* command awaiting file transfer to complete */
#endif /*]*/
		SS_TIME_WAIT,   /* command awaiting simple timeout */
		SS_PAUSED,	/* stopped in PauseScript action */
		SS_WAIT_NVT,	/* awaiting completion of Wait(NVTMode) */
		SS_WAIT_3270,	/* awaiting completion of Wait(3270Mode) */
		SS_WAIT_OUTPUT,	/* awaiting completion of Wait(Output) */
		SS_SWAIT_OUTPUT,/* awaiting completion of Snap(Wait) */
		SS_WAIT_DISC,	/* awaiting completion of Wait(Disconnect) */
		SS_WAIT_IFIELD,	/* awaiting completion of Wait(InputField) */
		SS_WAIT_UNLOCK,	/* awaiting completion of Wait(Unlock) */
		SS_EXPECTING,	/* awaiting completion of Expect() */
		SS_CLOSING	/* awaiting completion of Close() */
	} state;
	enum sms_type {
		ST_STRING,	/* string */
		ST_MACRO,	/* macro */
		ST_COMMAND,	/* interactive command */
		ST_KEYMAP,	/* keyboard map */
		ST_IDLE,	/* idle command */
		ST_CHILD,	/* child process */
		ST_PEER,	/* peer (external) process */
		ST_FILE		/* read commands from file */
	} type;
	Boolean	success;
	Boolean	need_prompt;
	Boolean	is_login;
	Boolean	is_hex;		/* flag for ST_STRING only */
	Boolean output_wait_needed;
	Boolean executing;	/* recursion avoidance */
	Boolean accumulated;	/* accumulated time flag */
	Boolean idle_error;	/* idle command caused an error */
	Boolean is_socket;	/* I/O is via a socket */
	Boolean is_transient;	/* I/O is via a transient socket */
	Boolean is_external;	/* I/O is via a transient socket to -socket */
	unsigned long msec;	/* total accumulated time */
	FILE   *outfile;
	int	infd;
#if defined(_WIN32) /*[*/
	HANDLE	inhandle;
	HANDLE	child_handle;
	unsigned long exit_id;
	unsigned long listen_id;
#endif /*]*/
	int	pid;
	unsigned long expect_id;
	unsigned long wait_id;
} sms_t;
#define SN	((sms_t *)NULL)
static sms_t *sms = SN;
static int sms_depth = 0;
#if defined(X3270_SCRIPT) /*[*/
static int socketfd = -1;
static unsigned long socket_id = 0L;
# if defined(_WIN32) /*[*/
static HANDLE socket_event = NULL;
# endif /*]*/
#endif /*]*/

#if defined(X3270_TRACE) /*[*/
static const char *sms_state_name[] = {
	"IDLE",
	"INCOMPLETE",
	"RUNNING",
	"KBWAIT",
	"CONNECT_WAIT",
#if defined(X3270_FT) /*[*/
	"FT_WAIT",
#endif /*]*/
	"TIME_WAIT",
	"PAUSED",
	"WAIT_NVT",
	"WAIT_3270",
	"WAIT_OUTPUT",
	"SWAIT_OUTPUT",
	"WAIT_DISC",
	"WAIT_IFIELD",
	"WAIT_UNLOCK",
	"EXPECTING",
	"CLOSING"
};
#endif /*]*/

#if defined(X3270_MENUS) /*[*/
static struct macro_def *macro_last = (struct macro_def *) NULL;
#endif /*]*/
static unsigned long stdin_id = 0L;
static unsigned char *ansi_save_buf;
static int      ansi_save_cnt = 0;
static int      ansi_save_ix = 0;
static char    *expect_text = CN;
static int	expect_len = 0;
static const char *st_name[] = { "String", "Macro", "Command", "KeymapAction",
				 "IdleCommand", "ChildScript", "PeerScript",
				 "File" };
static enum iaction st_cause[] = { IA_MACRO, IA_MACRO, IA_COMMAND, IA_KEYMAP,
				 IA_IDLE, IA_MACRO, IA_MACRO };
#define ST_sNAME(s)	st_name[(int)(s)->type]
#define ST_NAME		ST_sNAME(sms)

#if defined(X3270_SCRIPT) /*[*/
static void cleanup_socket(Boolean b);
#endif /*]*/
static void script_prompt(Boolean success);
static void script_input(void);
static void sms_pop(Boolean can_exit);
#if defined(X3270_SCRIPT) /*[*/
static void socket_connection(void);
# if defined(_WIN32) /*[*/
static void child_socket_connection(void);
static void child_exited(void);
# endif /*]*/
#endif /*]*/
static void wait_timed_out(void);
static void read_from_file(void);
static sms_t *sms_redirect_to(void);

/* Macro that defines that the keyboard is locked due to user input. */
#define KBWAIT	(kybdlock & (KL_OIA_LOCKED|KL_OIA_TWAIT|KL_DEFERRED_UNLOCK))
#if defined(X3270_SCRIPT) /*[*/
#define CKBWAIT	(appres.toggle[AID_WAIT].value && KBWAIT)
#else /*][*/
#define CKBWAIT	(KBWAIT)
#endif /*]*/

/* Macro that defines when it's safe to continue a Wait()ing sms. */
#define CAN_PROCEED ( \
    IN_SSCP || \
    (IN_3270 && (no_login_host || (formatted && cursor_addr)) && !CKBWAIT) || \
    (IN_ANSI && !(kybdlock & KL_AWAITING_FIRST)) \
)

#if defined(X3270_SCRIPT) && defined(X3270_PLUGIN) /*[*/
static int plugin_pid = 0;		/* process ID if running, or 0 */
static int plugin_outpipe = -1;		/* from emulator, to plugin */
static int plugin_inpipe = -1;		/* from plugin, to emulator */
static Bool plugin_started = False;	/* True after INIT ack'ed */
static unsigned long plugin_input_id = 0L;	/* input event */
static unsigned long plugin_timeout_id = 0L;	/* timeout event */
#define PRB_MAX	1024			/* maximum reply size */
static char plugin_buf[PRB_MAX];	/* reply buffer */
static int prb_cnt = 0;			/* pending reply size */
#define PLUGINSTART_SECS	5	/* timeout for init reply */
#define PLUGINWAIT_SECS		2	/* timeout for cmd or AID reply */
#define MAX_START_ARGS		10	/* max args for plugin command */
#define PLUGIN_QMAX		256	/* max pending INITs/AIDs/cmds */
static char plugin_tq[PLUGIN_QMAX];	/* FIFO of pending INITs/AIDs/cmds */
static int ptq_first = 0;		/* FIFO index of first pending cmd */
static int ptq_last = 0;		/* FIFO index of last pending cmd */
#define PLUGIN_BACKLOG	(((ptq_last + PLUGIN_QMAX) - ptq_first) % PLUGIN_QMAX)
#define PLUGIN_INIT	0		/* commands: initialize */
#define PLUGIN_AID	1		/*           process AID */
#define PLUGIN_CMD	2		/*           process command */
static Boolean plugin_complain = False;
static Boolean plugin_start_failed = False;
static char plugin_start_error[PRB_MAX];
static void plugin_start(char *command, char *argv[], Boolean complain);
static void no_plugin(void);
#endif /*]*/

#if defined(X3270_SCRIPT) && defined(X3270_PLUGIN) /*[*/
static void
plugin_start_appres(Boolean complain)
{
	int i = 0;
	char *args[MAX_START_ARGS];
	char *ccopy = NewString(appres.plugin_command);
	char *command;
	char *s;

	command = strtok(ccopy, " \t");
	if (command == NULL) {
		Free(ccopy);
		return;
	}

	args[i++] = command;
	while (i < MAX_START_ARGS - 1 && (s = strtok(NULL, " \t")) != NULL) {
		args[i++] = s;
	}
	args[i] = NULL;
	plugin_start(command, args, complain);
	Free(ccopy);
}
#endif /*]*/

/* Callbacks for state changes. */
static void
sms_connect(Boolean connected)
{
#if defined(X3270_SCRIPT) && defined(X3270_PLUGIN) /*[*/
	if (connected) {
		if (appres.plugin_command && !plugin_pid) {
			plugin_start_appres(False);
		}
	} else
		no_plugin();
#endif /*]*/

	/* Hack to ensure that disconnects don't cause infinite recursion. */
	if (sms != SN && sms->executing)
		return;

	if (!connected) {
		while (sms != SN && sms->is_login) {
#if !defined(_WIN32) /*[*/
			if (sms->type == ST_CHILD && sms->pid > 0)
				(void) kill(sms->pid, SIGTERM);
#endif /*]*/
			sms_pop(False);
		}
	}
	sms_continue();
}

static void
sms_in3270(Boolean in3270)
{
	if (in3270 || IN_SSCP)
		sms_continue();
}

/* One-time initialization. */
void
sms_init(void)
{
	register_schange(ST_CONNECT, sms_connect);
	register_schange(ST_3270_MODE, sms_in3270);
}

#if defined(X3270_MENUS) /*[*/
/* Parse the macros resource into the macro list */
void
macros_init(void)
{
	char *s = CN;
	char *name, *action;
	struct macro_def *m;
	int ns;
	int ix = 1;
	static char *last_s = CN;

	/* Free the previous macro definitions. */
	while (macro_defs) {
		m = macro_defs->next;
		Free(macro_defs);
		macro_defs = m;
	}
	macro_defs = (struct macro_def *)NULL;
	macro_last = (struct macro_def *)NULL;
	if (last_s) {
		Free(last_s);
		last_s = CN;
	}

	/* Search for new ones. */
	if (PCONNECTED) {
		char *rname;
		char *space;

		rname = NewString(current_host);
		if ((space = strchr(rname, ' ')))
			*space = '\0';
		s = get_fresource("%s.%s", ResMacros, rname);
		Free(rname);
	}
	if (s == CN) {
		if (appres.macros == CN)
			return;
		s = NewString(appres.macros);
	} else
		s = NewString(s);
	last_s = s;

	while ((ns = split_dresource(&s, &name, &action)) == 1) {
		m = (struct macro_def *)Malloc(sizeof(*m));
		if (!split_hier(name, &m->name, &m->parents)) {
			Free(m);
			continue;
		}
		m->action = action;
		if (macro_last)
			macro_last->next = m;
		else
			macro_defs = m;
		m->next = (struct macro_def *)NULL;
		macro_last = m;
		ix++;
	}
	if (ns < 0) {
		char buf[256];

		(void) sprintf(buf, "Error in macro %d", ix);
		Warning(buf);
	}
}
#endif /*]*/

/*
 * Enable input from a script.
 */
static void
script_enable(void)
{
#if defined(_WIN32) /*[*/
	/* Windows child scripts are listening sockets. */
	if (sms->type == ST_CHILD && sms->inhandle != INVALID_HANDLE_VALUE) {
	    	sms->listen_id = AddInput((int)sms->inhandle,
			child_socket_connection);
		return;
	}
#endif /*]*/

	if (sms->infd >= 0 && stdin_id == 0) {
		trace_dsn("Enabling input for %s[%d]\n", ST_NAME, sms_depth);
#if defined(_WIN32) /*[*/
		stdin_id = AddInput((int)sms->inhandle, script_input);
#else /*][*/
		stdin_id = AddInput(sms->infd, script_input);
#endif /*]*/
	}

}

/*
 * Disable input from a script.
 */
static void
script_disable(void)
{
	if (stdin_id != 0) {
		trace_dsn("Disabling input for %s[%d]\n", ST_NAME, sms_depth);
		RemoveInput(stdin_id);
		stdin_id = 0L;
	}
}

/* Allocate a new sms. */
static sms_t *
new_sms(enum sms_type type)
{
	sms_t *s;

	s = (sms_t *)Calloc(1, sizeof(sms_t));

	s->state = SS_IDLE;
	s->type = type;
	s->dptr = s->msc;
	s->success = True;
	s->need_prompt = False;
	s->is_login = False;
	s->outfile = (FILE *)NULL;
	s->infd = -1;
#if defined(_WIN32) /*[*/
	s->inhandle = INVALID_HANDLE_VALUE;
	s->child_handle = INVALID_HANDLE_VALUE;
#endif /*]*/
	s->pid = -1;
	s->expect_id = 0L;
	s->wait_id = 0L;
	s->output_wait_needed = False;
	s->executing = False;
	s->accumulated = False;
	s->idle_error = False;
	s->msec = 0L;

	return s;
}

/*
 * Push an sms definition on the stack.
 * Returns whether or not that is legal.
 */
static Boolean
sms_push(enum sms_type type)
{
	sms_t *s;

	/* Preempt any running sms. */
	if (sms != SN) {
		/* Remove the running sms's input. */
		script_disable();
	}

	s = new_sms(type);
	if (sms != SN)
		s->is_login = sms->is_login;	/* propagate from parent */
	s->next = sms;
	sms = s;

	/* Enable the abort button on the menu and the status indication. */
	if (++sms_depth == 1) {
		menubar_as_set(True);
		status_script(True);
	}

	if (ansi_save_buf == (unsigned char *)NULL)
		ansi_save_buf = (unsigned char *)Malloc(ANSI_SAVE_SIZE);
	return True;
}

/*
 * Add an sms definition to the _bottom_ of the stack.
 */
static sms_t *
sms_enqueue(enum sms_type type)
{
	sms_t *s, *t, *t_prev = SN;

	/* Allocate and initialize a new structure. */
	s = new_sms(type);

	/* Find the bottom of the stack. */
	for (t = sms; t != SN; t = t->next)
		t_prev = t;

	if (t_prev == SN) {	/* Empty stack. */
		s->next = sms;
		sms = s;

		/*
		 * Enable the abort button on the menu and the status
		 * line indication.
		 */
		menubar_as_set(True);
		status_script(True);
	} else {			/* Add to bottom. */
		s->next = SN;
		t_prev->next = s;
	}

	sms_depth++;

	if (ansi_save_buf == (unsigned char *)NULL)
		ansi_save_buf = (unsigned char *)Malloc(ANSI_SAVE_SIZE);

	return s;
}

/* Pop an sms definition off the stack. */
static void
sms_pop(Boolean can_exit)
{
	sms_t *s;

	trace_dsn("%s[%d] complete\n", ST_NAME, sms_depth);

	/* When you pop the peer script, that's the end of x3270. */
	if (sms->type == ST_PEER && !sms->is_transient && can_exit)
		x3270_exit(0);

	/* Remove the input event. */
	script_disable();

	/* Close the files. */
	if (sms->outfile != NULL)
	    	fclose(sms->outfile);
	if (sms->infd >= 0) {
		if (sms->is_socket)
			SOCK_CLOSE(sms->infd);
		else
		    	close(sms->infd);
	}

	/* Cancel any pending timeouts. */
	if (sms->expect_id != 0L)
		RemoveTimeOut(sms->expect_id);
	if (sms->wait_id != 0L)
		RemoveTimeOut(sms->wait_id);

	/*
	 * If this was an idle command that generated an error, now is the
	 * time to announce that.  (If we announced it when the error first
	 * occurred, we might be telling the wrong party, such as a script.)
	 */
	if (sms->idle_error)
		popup_an_error("Idle command disabled due to error");

#if defined(X3270_SCRIPT) /*[*/
	/* If this was a -socket peer, get ready for another connection. */
	if (sms->type == ST_PEER && sms->is_external) {
#if defined(_WIN32) /*[*/
		socket_id = AddInput((int)socket_event, socket_connection);
#else /*][*/
		socket_id = AddInput(socketfd, socket_connection);
#endif /*]*/
	}
#endif /*]*/

	/* Release the memory. */
	s = sms;
	sms = s->next;
	Free(s);
	sms_depth--;

	if (sms == SN) {
		/* Turn off the menu option. */
		menubar_as_set(False);
		status_script(False);
	} else if (CKBWAIT && (int)sms->state < (int)SS_KBWAIT) {
		/* The child implicitly blocked the parent. */
		sms->state = SS_KBWAIT;
		trace_dsn("%s[%d] implicitly paused %s\n",
			    ST_NAME, sms_depth, sms_state_name[sms->state]);
	} else if (sms->state == SS_IDLE && sms->type != ST_FILE) {
		/* The parent needs to be restarted. */
		script_enable();
	} else if (sms->type == ST_FILE) {
	    	read_from_file();
	}

#if defined(_WIN32) /*[*/
	/* If the new top sms is an exited script, pop it, too. */
	if (sms != SN &&
	    sms->type == ST_CHILD &&
	    sms->child_handle == INVALID_HANDLE_VALUE)
	    	sms_pop(False);
#endif /*]*/
}

/*
 * Peer script initialization.
 *
 * Must be called after the initial call to connect to the host from the
 * command line, so that the initial state can be set properly.
 */
void
peer_script_init(void)
{
	sms_t *s;
	Boolean on_top;

#if defined(X3270_SCRIPT) /*[*/
	if (appres.script_port) {
		struct sockaddr_in sin;
		int on = 1;

#if !defined(_WIN32) /*[*/
		if (appres.socket)
		    	xs_warning("-scriptport overrides -socket");
#endif /*]*/

		/* -scriptport overrides -script */
		appres.scripted = False;

		/* Create the listening socket. */
		socketfd = socket(AF_INET, SOCK_STREAM, 0);
		if (socketfd < 0) {
#if !defined(_WIN32) /*[*/
			popup_an_errno(errno, "socket()");
#else /*][*/
			popup_an_error("socket(): %s",
				win32_strerror(GetLastError()));
#endif /*]*/
			return;
		}
		if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on,
			    sizeof(on)) < 0) {
#if !defined(_WIN32) /*[*/
			popup_an_errno(errno, "setsockopt(SO_REUSEADDR)");
#else /*][*/
			popup_an_error("setsockopt(SO_REUSEADDR): %s",
				win32_strerror(GetLastError()));
#endif /*]*/
			return;
		}
		(void) memset(&sin, '\0', sizeof(sin));
		sin.sin_family = AF_INET;
		sin.sin_port = htons(appres.script_port);
		sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
		if (bind(socketfd, (struct sockaddr *)&sin, sizeof(sin))
				< 0) {
#if !defined(_WIN32) /*[*/
			popup_an_errno(errno, "socket bind");
#else /*][*/
			popup_an_error("socket bind: %s",
				win32_strerror(GetLastError()));
#endif /*]*/
			SOCK_CLOSE(socketfd);
			socketfd = -1;
			return;
		}
		if (listen(socketfd, 1) < 0) {
#if !defined(_WIN32) /*[*/
			popup_an_errno(errno, "socket listen");
#else /*][*/
			popup_an_error("socket listen: %s",
				win32_strerror(GetLastError()));
#endif /*]*/
			SOCK_CLOSE(socketfd);
			socketfd = -1;
			return;
		}
#if defined(_WIN32) /*[*/
		socket_event = CreateEvent(NULL, FALSE, FALSE, NULL);
		if (socket_event == NULL) {
			popup_an_error("CreateEvent: %s",
				win32_strerror(GetLastError()));
			SOCK_CLOSE(socketfd);
			socketfd = -1;
			return;
		}
		if (WSAEventSelect(socketfd, socket_event, FD_ACCEPT) != 0) {
			popup_an_error("WSAEventSelect: %s",
				win32_strerror(GetLastError()));
			SOCK_CLOSE(socketfd);
			socketfd = -1;
			return;
		}
		socket_id = AddInput((int)socket_event, socket_connection);
#else /*][*/
		socket_id = AddInput(socketfd, socket_connection);
#endif/*]*/
		register_schange(ST_EXITING, cleanup_socket);
		return;
	}
#endif /*]*/
#if defined(X3270_SCRIPT) && !defined(_WIN32) /*[*/
	if (appres.socket && !appres.script_port) {
		struct sockaddr_un ssun;

		/* -socket overrides -script */
		appres.scripted = False;

		/* Create the listening socket. */
		socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
		if (socketfd < 0) {
			popup_an_errno(errno, "Unix-domain socket");
			return;
		}
		(void) memset(&ssun, '\0', sizeof(ssun));
		ssun.sun_family = AF_UNIX;
		(void) sprintf(ssun.sun_path, "/tmp/x3sck.%u", getpid());
		(void) unlink(ssun.sun_path);
		if (bind(socketfd, (struct sockaddr *)&ssun, sizeof(ssun))
				< 0) {
			popup_an_errno(errno, "Unix-domain socket bind");
			close(socketfd);
			socketfd = -1;
			return;
		}
		if (listen(socketfd, 1) < 0) {
			popup_an_errno(errno, "Unix-domain socket listen");
			close(socketfd);
			socketfd = -1;
			(void) unlink(ssun.sun_path);
			return;
		}
		socket_id = AddInput(socketfd, socket_connection);
		register_schange(ST_EXITING, cleanup_socket);
		return;
	}
#endif /*]*/

	if (!appres.scripted)
		return;

	if (sms == SN) {
		/* No login script running, simply push a new sms. */
		(void) sms_push(ST_PEER);
		s = sms;
		on_top = True;
	} else {
		/* Login script already running, pretend we started it. */
		s = sms_enqueue(ST_PEER);
		s->state = SS_RUNNING;
		on_top = False;
	}

	s->infd = fileno(stdin);
#if defined(_WIN32) /*[*/
	s->inhandle = GetStdHandle(STD_INPUT_HANDLE);
#endif /*]*/
	s->outfile = stdout;
	(void) SETLINEBUF(s->outfile);	/* even if it's a pipe */

	if (on_top) {
		if (HALF_CONNECTED ||
		    (CONNECTED && (kybdlock & KL_AWAITING_FIRST)))
			s->state = SS_CONNECT_WAIT;
		else
			script_enable();
	}
}

#if defined(X3270_SCRIPT) /*[*/
/* Accept a new socket connection. */
static void
socket_connection(void)
{
	int fd;
	sms_t *s;

	/* Accept the connection. */
#if !defined(_WIN32) /*[*/
	if (appres.script_port)
#endif /*]*/
	{
		struct sockaddr_in sin;
		socklen_t len = sizeof(sin);

		(void) memset(&sin, '\0', sizeof(sin));
		sin.sin_family = AF_INET;
		fd = accept(socketfd, (struct sockaddr *)&sin, &len);
	}
#if !defined(_WIN32) /*[*/
	else {
		struct sockaddr_un ssun;
		socklen_t len = sizeof(ssun);

		(void) memset(&ssun, '\0', sizeof(ssun));
		ssun.sun_family = AF_UNIX;
		fd = accept(socketfd, (struct sockaddr *)&ssun, &len);
	}
#endif /*]*/

	if (fd < 0) {
		popup_an_errno(errno, "socket accept");
		return;
	}
	trace_dsn("New script socket connection\n");

	/* Push on a peer script. */
	(void) sms_push(ST_PEER);
	s = sms;
	s->is_transient = True;
	s->is_external = True;
	s->infd = fd;
#if !defined(_WIN32) /*[*/
	s->outfile = fdopen(dup(fd), "w");
#endif /*]*/
#if defined(_WIN32) /*[*/
	s->inhandle = CreateEvent(NULL, FALSE, FALSE, NULL);
	if (s->inhandle == NULL) {
	    	fprintf(stderr, "Can't create socket handle\n");
		exit(1);
	}
	if (WSAEventSelect(s->infd, s->inhandle, FD_READ | FD_CLOSE) != 0) {
	    	fprintf(stderr, "Can't set socket handle events\n");
		exit(1);
	}
#endif /*]*/
	s->is_socket = True;
	script_enable();

	/* Don't accept any more connections. */
	RemoveInput(socket_id);
	socket_id = 0L;
}

# if defined(_WIN32) /*[*/
/* Accept a new socket connection from a child process. */
static void
child_socket_connection(void)
{
	int fd;
	sms_t *old_sms;
	sms_t *s;
	struct sockaddr_in sin;
	socklen_t len = sizeof(sin);

	/* Accept the connection. */
	(void) memset(&sin, '\0', sizeof(sin));
	sin.sin_family = AF_INET;
	fd = accept(sms->infd, (struct sockaddr *)&sin, &len);

	if (fd < 0) {
		popup_an_error("socket accept: %s",
			win32_strerror(GetLastError()));
		return;
	}
	trace_dsn("New child script socket connection\n");

	/* Push on a peer script. */
	old_sms = sms;
	(void) sms_push(ST_PEER);
	s = sms;
	s->is_transient = True;
	s->infd = fd;
	s->inhandle = CreateEvent(NULL, FALSE, FALSE, NULL);
	if (s->inhandle == NULL) {
	    	fprintf(stderr, "Can't create socket handle\n");
		exit(1);
	}
	if (WSAEventSelect(s->infd, s->inhandle, FD_READ | FD_CLOSE) != 0) {
	    	fprintf(stderr, "Can't set socket handle events\n");
		exit(1);
	}
	s->is_socket = True;
	script_enable();

	/* Don't accept any more connections on the global listen socket. */
	RemoveInput(old_sms->listen_id);
	old_sms->listen_id = 0L;
}
#endif /*]*/

/* Clean up the Unix-domain socket. */
static void
cleanup_socket(Boolean b _is_unused)
{
#if !defined(_WIN32) /*[*/
	char buf[1024];

	(void) sprintf(buf, "/tmp/x3sck.%u", getpid());
	(void) unlink(buf);
#endif /*]*/
}
#endif /*]*/

#if defined(_WIN32) /*[*/
/* Process an event on a child script handle (presumably a process exit). */
static void
child_exited(void)
{
    	sms_t *s;
	DWORD status;

	for (s = sms; s != NULL; s = s->next) {
	    	if (s->type == ST_CHILD) {
		    	status = 0;
		    	if (GetExitCodeProcess(s->child_handle, &status) == 0) {
			    	popup_an_error("GetExitCodeProcess failed: %s",
					win32_strerror(GetLastError()));
			} else if (status != STILL_ACTIVE) {
				trace_dsn("Child script exited with status "
					"0x%x\n", (unsigned)status);
			    	CloseHandle(s->child_handle);
				s->child_handle = INVALID_HANDLE_VALUE;
				RemoveInput(s->exit_id);
				s->exit_id = 0;
				if (s == sms) {
				    	sms_pop(False);
					sms_continue();
				}
				break;
			}
		}
	}
}
#endif /*]*/

/*
 * Interpret and execute a script or macro command.
 */
enum em_stat { EM_CONTINUE, EM_PAUSE, EM_ERROR };
static enum em_stat
execute_command(enum iaction cause, char *s, char **np)
{
	enum {
		ME_GND,		/* before action name */
		ME_COMMENT,	/* within a comment */
		ME_FUNCTION,	/* within action name */
		ME_FUNCTIONx,	/* saw whitespace after action name */
		ME_LPAREN,	/* saw left paren */
		ME_P_PARM,	/* paren: within unquoted parameter */
		ME_P_QPARM,	/* paren: within quoted parameter */
		ME_P_BSL,	/* paren: after backslash in quoted parameter */
		ME_P_PARMx,	/* paren: saw whitespace after parameter */
		ME_S_PARM,	/* space: within unquoted parameter */
		ME_S_QPARM,	/* space: within quoted parameter */
		ME_S_BSL,	/* space: after backslash in quoted parameter */
		ME_S_PARMx	/* space: saw whitespace after parameter */
	} state = ME_GND;
	char c;
	char aname[64+1];
	char parm[1024+1];
	int nx = 0;
	Cardinal count = 0;
	String params[64];
	int i, any, exact;
	int failreason = 0;
	Boolean saw_paren = False;
	static const char *fail_text[] = {
		/*1*/ "Action name must begin with an alphanumeric character",
		/*2*/ "Syntax error in action name",
		/*3*/ "Syntax error: \")\" or \",\" expected",
		/*4*/ "Extra data after parameters",
		/*5*/ "Syntax error: \")\" expected"
	};
#define fail(n) { failreason = n; goto failure; }
#define free_params() { \
	if (cause == IA_MACRO ||   cause == IA_KEYMAP || \
	    cause == IA_COMMAND || cause == IA_IDLE) { \
		Cardinal j; \
		for (j = 0; j < count; j++) \
			Free(params[j]); \
	} \
}

	parm[0] = '\0';
	params[count] = parm;

	while ((c = *s++)) switch (state) {
	    case ME_GND:
		if (isspace(c))
			continue;
		else if (isalnum(c)) {
			state = ME_FUNCTION;
			nx = 0;
			aname[nx++] = c;
		} else if (c == '!' || c == '#')
		    	state = ME_COMMENT;
		else
			fail(1);
		break;
	    case ME_COMMENT:
		break;
	    case ME_FUNCTION:	/* within function name */
		if (c == '(' || isspace(c)) {
			aname[nx] = '\0';
			if (c == '(') {
				nx = 0;
				state = ME_LPAREN;
				saw_paren = True;
			} else
				state = ME_FUNCTIONx;
		} else if (isalnum(c) || c == '_' || c == '-') {
			if (nx < 64)
				aname[nx++] = c;
		} else {
			fail(2);
		}
		break;
	    case ME_FUNCTIONx:	/* space after function name */
		if (isspace(c))
			continue;
		else if (c == '(') {
			nx = 0;
			state = ME_LPAREN;
		} else if (c == '"') {
			nx = 0;
			state = ME_S_QPARM;
		}
		else { state = ME_S_PARM;
			nx = 0;
			parm[nx++] = c;
		}
		break;
	    case ME_LPAREN:
		if (isspace(c))
			continue;
		else if (c == '"')
			state = ME_P_QPARM;
		else if (c == ',') {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
		} else if (c == ')')
			goto success;
		else {
			state = ME_P_PARM;
			parm[nx++] = c;
		}
		break;
	    case ME_P_PARM:
		if (isspace(c)) {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
			state = ME_P_PARMx;
		} else if (c == ')') {
			parm[nx] = '\0';
			++count;
			goto success;
		} else if (c == ',') {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
			state = ME_LPAREN;
		} else {
			if (nx < 1024)
				parm[nx++] = c;
		}
		break;
	    case ME_P_BSL:
		if (c == 'n' && nx < 1024)
			parm[nx++] = '\n';
		else {
			if (c != '"' && nx < 1024)
				parm[nx++] = '\\';
			if (nx < 1024)
				parm[nx++] = c;
		}
		state = ME_P_QPARM;
		break;
	    case ME_P_QPARM:
		if (c == '"') {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
			state = ME_P_PARMx;
		} else if (c == '\\') {
			state = ME_P_BSL;
		} else if (nx < 1024)
			parm[nx++] = c;
		break;
	    case ME_P_PARMx:
		if (isspace(c))
			continue;
		else if (c == ',')
			state = ME_LPAREN;
		else if (c == ')')
			goto success;
		else
			fail(3);
		break;
	    case ME_S_PARM:
		if (isspace(c)) {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
			state = ME_S_PARMx;
		} else {
			if (nx < 1024)
				parm[nx++] = c;
		}
		break;
	    case ME_S_BSL:
		if (c == 'n' && nx < 1024)
			parm[nx++] = '\n';
		else {
			if (c != '"' && nx < 1024)
				parm[nx++] = '\\';
			if (nx < 1024)
				parm[nx++] = c;
		}
		state = ME_S_QPARM;
		break;
	    case ME_S_QPARM:
		if (c == '"') {
			parm[nx++] = '\0';
			params[++count] = &parm[nx];
			state = ME_S_PARMx;
		} else if (c == '\\') {
			state = ME_S_BSL;
		} else if (nx < 1024)
			parm[nx++] = c;
		break;
	    case ME_S_PARMx:
		if (isspace(c))
			continue;
		else if (c == '"')
			state = ME_S_QPARM;
		else {
			parm[nx++] = c;
			state = ME_S_PARM;
		}
		break;
	}

	/* Terminal state. */
	switch (state) {
	    case ME_FUNCTION:	/* mid-function-name */
		aname[nx] = '\0';
		break;
	    case ME_FUNCTIONx:	/* space after function */
		break;
	    case ME_GND:	/* nothing */
	    case ME_COMMENT:
		if (np)
			*np = s - 1;
		return EM_CONTINUE;
	    case ME_S_PARMx:	/* space after space-style parameter */
		break;
	    case ME_S_PARM:	/* mid space-style parameter */
		parm[nx++] = '\0';
		params[++count] = &parm[nx];
		break;
	    default:
		fail(5);
	}

    success:
	if (c) {
		while (*s && isspace(*s))
			s++;
		if (*s) {
			if (np)
				*np = s;
			else
				fail(4);
		} else if (np)
			*np = s;
	} else if (np)
		*np = s-1;

	/* If it's a macro, do variable substitutions. */
	if (cause == IA_MACRO || cause == IA_KEYMAP ||
	    cause == IA_COMMAND || cause == IA_IDLE) {
		Cardinal j;

		for (j = 0; j < count; j++)
			params[j] = do_subst(params[j], True, False);
	}

	/* Search the action list. */
	if (!strncasecmp(aname, PA_PFX, strlen(PA_PFX))) {
		popup_an_error("Invalid action: %s", aname);
		free_params();
		return EM_ERROR;
	}
	any = -1;
	exact = -1;
	for (i = 0; i < actioncount; i++) {
		if (!strcasecmp(aname, actions[i].string)) {
			exact = any = i;
			break;
		}
	}
	if (exact < 0) {
		for (i = 0; i < actioncount; i++) {
			if (!strncasecmp(aname, actions[i].string,
			    strlen(aname))) {
				if (any >= 0) {
					popup_an_error("Ambiguous action name: "
					    "%s", aname);
					free_params();
					return EM_ERROR;
				}
				any = i;
			}
		}
	}
	if (any >= 0) {
		sms->accumulated = False;
		sms->msec = 0L;
		ia_cause = cause;
		(*actions[any].proc)((Widget)NULL, (XEvent *)NULL,
			count? params: (String *)NULL, &count);
		free_params();
		screen_disp(False);
	} else {
		popup_an_error("Unknown action: %s", aname);
		free_params();
		return EM_ERROR;
	}

#if defined(X3270_FT) /*[*/
	if (ft_state != FT_NONE)
		sms->state = SS_FT_WAIT;
#endif /*]*/
	trace_rollover_check();
	if (CKBWAIT)
		return EM_PAUSE;
	else
		return EM_CONTINUE;

    failure:
	popup_an_error("%s", fail_text[failreason-1]);
	return EM_ERROR;
#undef fail
#undef free_params
}

/* Run the string at the top of the stack. */
static void
run_string(void)
{
	int len;
	int len_left;

	trace_dsn("%s[%d] running\n", ST_NAME, sms_depth);

	sms->state = SS_RUNNING;
	len = strlen(sms->dptr);
	trace_dsn("%sString[%d]: '%s'\n",
		    sms->is_hex ? "Hex" : "",
		    sms_depth, sms->dptr);

	if (sms->is_hex) {
		if (CKBWAIT) {
			sms->state = SS_KBWAIT;
			trace_dsn("%s[%d] paused %s\n",
				    ST_NAME, sms_depth,
				    sms_state_name[sms->state]);
		} else {
			hex_input(sms->dptr);
			sms_pop(False);
		}
	} else {
		if ((len_left = emulate_input(sms->dptr, len, False))) {
			sms->dptr += len - len_left;
			if (CKBWAIT) {
				sms->state = SS_KBWAIT;
				trace_dsn("%s[%d] paused %s\n",
					    ST_NAME, sms_depth,
					    sms_state_name[sms->state]);
			}
		} else {
			sms_pop(False);
		}
	}
}

/* Run the macro at the top of the stack. */

static void
run_macro(void)
{
	char *a = sms->dptr;
	char *nextm;
	enum em_stat es;
	sms_t *s;

	trace_dsn("%s[%d] running\n", ST_NAME, sms_depth);

	/*
	 * Keep executing commands off the line until one pauses or
	 * we run out of commands.
	 */
	while (*a) {
		/*
		 * Check for command failure.
		 */
		if (!sms->success) {
			trace_dsn("%s[%d] failed\n", ST_NAME, sms_depth);

			/* Propogate it. */
			if (sms->next != SN)
				sms->next->success = False;
			break;
		}

		sms->state = SS_RUNNING;
		trace_dsn("%s[%d]: '%s'\n", ST_NAME, sms_depth, a);
		s = sms;
		s->success = True;
		s->executing = True;
		es = execute_command(st_cause[s->type], a, &nextm);
		s->executing = False;
		s->dptr = nextm;

		/*
		 * If a new sms was started, we will be resumed
		 * when it completes.
		 */
		if (sms != s) {
			return;
		}

		/* Macro could not execute.  Abort it. */
		if (es == EM_ERROR) {
			trace_dsn("%s[%d] error\n", ST_NAME, sms_depth);

			/* Propogate it. */
			if (sms->next != SN)
				sms->next->success = False;

			/* If it was an idle command, cancel it. */
			cancel_if_idle_command();
			break;
		}

		/* Macro paused, implicitly or explicitly.  Suspend it. */
		if (es == EM_PAUSE || (int)sms->state >= (int)SS_KBWAIT) {
			if (sms->state == SS_RUNNING)
				sms->state = SS_KBWAIT;
			trace_dsn("%s[%d] paused %s\n", ST_NAME, sms_depth,
				    sms_state_name[sms->state]);
			sms->dptr = nextm;
			return;
		}

		/* Macro ran. */
		a = nextm;
	}

	/* Finished with this macro. */
	sms_pop(False);
}

/* Push a macro (macro, command or keymap action) on the stack. */
static void
push_xmacro(enum sms_type type, char *s, Boolean is_login)
{
	macro_output = False;
	if (!sms_push(type))
		return;
	(void) strncpy(sms->msc, s, 1023);
	sms->msc[1023] = '\0';
	sms->msc_len = strlen(s);
	if (sms->msc_len > 1023)
		sms->msc_len = 1023;
	if (is_login) {
		sms->state = SS_WAIT_IFIELD;
		sms->is_login = True;
	} else
		sms->state = SS_INCOMPLETE;
	sms_continue();
}

/* Push a macro on the stack. */
void
push_macro(char *s, Boolean is_login)
{
	push_xmacro(ST_MACRO, s, is_login);
}

/* Push an interactive command on the stack. */
void
push_command(char *s)
{
	push_xmacro(ST_COMMAND, s, False);
}

/* Push an keymap action on the stack. */
void
push_keymap_action(char *s)
{
	push_xmacro(ST_KEYMAP, s, False);
}

/* Push an keymap action on the stack. */
void
push_idle(char *s)
{
	push_xmacro(ST_IDLE, s, False);
}

/* Push a string on the stack. */
static void
push_string(char *s, Boolean is_login, Boolean is_hex)
{
	if (!sms_push(ST_STRING))
		return;
	(void) strncpy(sms->msc, s, 1023);
	sms->msc[1023] = '\0';
	sms->msc_len = strlen(s);
	if (sms->msc_len > 1023)
		sms->msc_len = 1023;
	if (is_login) {
		sms->state = SS_WAIT_IFIELD;
		sms->is_login = True;
	} else
		sms->state = SS_INCOMPLETE;
	sms->is_hex = is_hex;
	if (sms_depth == 1)
		sms_continue();
}

/* Push a Source'd file on the stack. */
static void
push_file(int fd)
{
    	if (!sms_push(ST_FILE))
	    	return;
	sms->infd = fd;
	read_from_file();
}

/* Set a pending string. */
void
ps_set(char *s, Boolean is_hex)
{
	push_string(s, False, is_hex);
}

#if defined(X3270_MENUS) /*[*/
/* Callback for macros menu. */
void
macro_command(struct macro_def *m)
{
	push_macro(m->action, False);
}
#endif /*]*/

/*
 * If the string looks like an action, e.g., starts with "Xxx(", run a login
 * macro.  Otherwise, set a simple pending login string.
 */
void
login_macro(char *s)
{
	char *t = s;
	Boolean looks_right = False;

	while (isspace(*t))
		t++;
	if (isalnum(*t)) {
		while (isalnum(*t))
			t++;
		while (isspace(*t))
			t++;
		if (*t == '(')
			looks_right = True;
	}

	if (looks_right)
		push_macro(s, True);
	else
		push_string(s, True, False);
}

/* Run the first command in the msc[] buffer. */
static void
run_script(void)
{
	trace_dsn("%s[%d] running\n", ST_NAME, sms_depth);

	for (;;) {
		char *ptr;
		int cmd_len;
		char *cmd;
		sms_t *s;
		enum em_stat es;

		/* If the script isn't idle, we're done. */
		if (sms->state != SS_IDLE)
			break;

		/* If a prompt is required, send one. */
		if (sms->need_prompt) {
			script_prompt(sms->success);
			sms->need_prompt = False;
		}

		/* If there isn't a pending command, we're done. */
		if (!sms->msc_len)
			break;

		/* Isolate the command. */
		ptr = memchr(sms->msc, '\n', sms->msc_len);
		if (!ptr)
			break;
		*ptr++ = '\0';
		cmd_len = ptr - sms->msc;
		cmd = sms->msc;

		/* Execute it. */
		sms->state = SS_RUNNING;
		sms->success = True;
		trace_dsn("%s[%d]: '%s'\n", ST_NAME, sms_depth, cmd);
		s = sms;
		s->executing = True;
		es = execute_command(IA_SCRIPT, cmd, (char **)NULL);
		s->executing = False;

		/* Move the rest of the buffer over. */
		if (cmd_len < s->msc_len) {
			s->msc_len -= cmd_len;
			(void) memmove(s->msc, ptr, s->msc_len);
		} else
			s->msc_len = 0;

		/*
		 * If a new sms was started, we will be resumed
		 * when it completes.
		 */
		if (sms != s) {
			s->need_prompt = True;
			return;
		}

		/* Handle what it did. */
		if (es == EM_PAUSE || (int)sms->state >= (int)SS_KBWAIT) {
			if (sms->state == SS_RUNNING)
				sms->state = SS_KBWAIT;
			script_disable();
			if (sms->state == SS_CLOSING) {
				sms_pop(False);
				return;
			}
			sms->need_prompt = True;
		} else if (es == EM_ERROR) {
			trace_dsn("%s[%d] error\n", ST_NAME, sms_depth);
			script_prompt(False);
			/* If it was an idle command, cancel it. */
			cancel_if_idle_command();
		} else
			script_prompt(sms->success);
		if (sms->state == SS_RUNNING)
			sms->state = SS_IDLE;
		else {
			trace_dsn("%s[%d] paused %s\n",
				    ST_NAME, sms_depth,
				    sms_state_name[sms->state]);
		}
	}
}

/* Read the next command from a file. */
static void
read_from_file(void)
{
	char *dptr;
	int len_left = sizeof(sms->msc);

	sms->msc_len = 0;
	dptr = sms->msc;

	while (len_left) {
		int nr;

		nr = read(sms->infd, dptr, 1);
		if (nr < 0) {
			sms_pop(False);
			return;
		}
		if (nr == 0) {
		    	if (sms->msc_len == 0) {
			    	sms_pop(False);
				return;
			} else {
			    	*++dptr = '\0';
			    	break;
			}
		}
		if (*dptr == '\n') {
		    	if (sms->msc_len) {
			    	*dptr = '\0';
				break;
			}
		}
		dptr++;
		sms->msc_len++;
		len_left--;
	}

	/* Run the command as a macro. */
	trace_dsn("%s[%d] read '%s'\n", ST_NAME, sms_depth, sms->msc);
	sms->state = SS_INCOMPLETE;
	push_macro(sms->dptr, False);
}

/* Handle an error generated during the execution of a script or macro. */
void
sms_error(const char *msg)
{
	sms_t *s;
	Boolean is_script = False;

	/* Print the error message. */
	s = sms_redirect_to();
	is_script = (s != NULL);
	if (is_script) {
	    	char *text = Malloc(strlen("data: ") + strlen(msg) + 2);
		char *newline;
		char *last_space;

		sprintf(text, "data: %s", msg);
		newline = text;
		while ((newline = strchr(newline, '\n')) != NULL)
		    	*newline++ = ' ';
		last_space = strrchr(text, ' ');
		if (last_space != NULL &&
			last_space == text + strlen(text) - 1)
		    	*last_space = '\n';
		else
		    	strcat(text, "\n");

		if (s->is_socket)
			send(s->infd, text, strlen(text), 0);
		else
			fprintf(s->outfile, "%s", text);
		Free(text);
	} else
		(void) fprintf(stderr, "%s\n", msg);

	/* Fail the current command. */
	sms->success = False;

	/* Cancel any login. */
	if (s != NULL && s->is_login)
		host_disconnect(True);
}

/*
 * Generate a response to a script command.
 * Makes sure that each line of output is prefixed with 'data:', if
 * appropriate, and makes sure that the output is newline terminated.
 *
 * If the parameter is an empty string, generates nothing, but if it is a
 * newline, generates an empty line.
 */
void
sms_info(const char *fmt, ...)
{
	char *nl;
	char msgbuf[4096];
	char *msg = msgbuf;
	va_list args;
	sms_t *s;

	va_start(args, fmt);
	vsprintf(msgbuf, fmt, args);
	va_end(args);

	do {
		int nc;

		nl = strchr(msg, '\n');
		if (nl != CN)
			nc = nl - msg;
		else
			nc = strlen(msg);
		if (nc || (nl != CN)) {
		    	if ((s = sms_redirect_to()) != NULL) {
			    	char *text = Malloc(strlen("data: ") + nc + 2);

				sprintf(text, "data: %.*s\n", nc, msg);
			    	if (s->is_socket)
				    	send(s->infd, text, strlen(text), 0);
				else
					(void) fprintf(s->outfile, "%s", text);
				Free(text);
			} else
				(void) printf("%.*s\n", nc, msg);
		}
		msg = nl + 1;
	} while (nl);

	macro_output = True;
}

/* Process available input from a script. */
static void
script_input(void)
{
	char buf[128];
	int nr;
	char *ptr;
	char c;

	trace_dsn("Input for %s[%d] %s reading %s %d\n", ST_NAME, sms_depth,
		sms_state_name[sms->state],
		sms->is_socket? "socket": "fd",
		sms->infd);

	/* Read in what you can. */
	if (sms->is_socket)
		nr = recv(sms->infd, buf, sizeof(buf), 0);
	else
		nr = read(sms->infd, buf, sizeof(buf));
	if (nr < 0) {
#if defined(_WIN32) /*[*/
	    	if (sms->is_socket)
		    	popup_an_error("%s[%d] recv: %s", ST_NAME, sms_depth,
				win32_strerror(GetLastError()));
		else
#endif
		popup_an_errno(errno, "%s[%d] read", ST_NAME, sms_depth);
		return;
	}
	trace_dsn("Input for %s[%d] %s complete nr=%d\n", ST_NAME, sms_depth,
		sms_state_name[sms->state], nr);
	if (nr == 0) {	/* end of file */
		trace_dsn("EOF %s[%d]\n", ST_NAME, sms_depth);
		sms_pop(True);
		sms_continue();
		return;
	}

	/* Append to the pending command, ignoring returns. */
	ptr = buf;
	while (nr--)
		if ((c = *ptr++) != '\r')
			sms->msc[sms->msc_len++] = c;

	/* Run the command(s). */
	sms->state = SS_INCOMPLETE;
	sms_continue();
}

/* Resume a paused sms, if conditions are now ripe. */
void
sms_continue(void)
{
	static Boolean continuing = False;

	if (continuing)
		return;
	continuing = True;

	while (True) {
		if (sms == SN) {
			continuing = False;
			return;
		}

		switch (sms->state) {

		    case SS_IDLE:
			continuing = False;
			return;		/* nothing to do */

		    case SS_INCOMPLETE:
		    case SS_RUNNING:
			break;		/* let it proceed */

		    case SS_KBWAIT:
			if (CKBWAIT) {
				continuing = False;
				return;
			}
			break;

		    case SS_WAIT_NVT:
			if (IN_ANSI) {
			    sms->state = SS_WAIT_IFIELD;
			    continue;
			}
			continuing = False;
			return;

		    case SS_WAIT_3270:
			if (IN_3270 | IN_SSCP) {
			    sms->state = SS_WAIT_IFIELD;
			    continue;
			}
			continuing = False;
			return;

		    case SS_WAIT_UNLOCK:
			if (KBWAIT) {
				continuing = False;
				return;
			}
			break;

		    case SS_WAIT_IFIELD:
			if (!CAN_PROCEED) {
				continuing = False;
				return;
			}
			/* fall through... */
		    case SS_CONNECT_WAIT:
			if (HALF_CONNECTED ||
			    (CONNECTED && (kybdlock & KL_AWAITING_FIRST))) {
				continuing = False;
				return;
			}
			if (!CONNECTED) {
				/* connection failed */
				if (sms->need_prompt) {
					script_prompt(False);
					sms->need_prompt = False;
				}
				break;
			}
			break;

#if defined(X3270_FT) /*[*/
		    case SS_FT_WAIT:
			if (ft_state == FT_NONE)
				break;
			else {
				continuing = False;
				return;
			}
#endif /*]*/

		    case SS_TIME_WAIT:
			continuing = False;
			return;

		    case SS_WAIT_OUTPUT:
		    case SS_SWAIT_OUTPUT:
			if (!CONNECTED) {
				popup_an_error("Host disconnected");
				break;
			}
			continuing = False;
			return;

		    case SS_WAIT_DISC:
			if (!CONNECTED)
				break;
			else {
				continuing = False;
				return;
			}

		    case SS_PAUSED:
			continuing = False;
			return;

		    case SS_EXPECTING:
			continuing = False;
			return;

		    case SS_CLOSING:
			continuing = False;
			return;	/* can't happen, I hope */

		}

		/* Restart the sms. */

		sms->state = SS_IDLE;

		if (sms->wait_id != 0L) {
			RemoveTimeOut(sms->wait_id);
			sms->wait_id = 0L;
		}

		switch (sms->type) {
		    case ST_STRING:
			run_string();
			break;
		    case ST_MACRO:
		    case ST_COMMAND:
		    case ST_KEYMAP:
		    case ST_IDLE:
			run_macro();
			break;
		    case ST_PEER:
		    case ST_CHILD:
			script_enable();
			run_script();
			break;
		    case ST_FILE:
			read_from_file();
			break;
		}
	}

	continuing = False;
}

/*
 * Return True if there is a pending macro.
 */
Boolean
sms_in_macro(void)
{
	sms_t *s;

	for (s = sms; s != NULL; s = s->next) {
		if (s->type == ST_MACRO || s->type == ST_STRING)
			return True;
	}
	return False;
}

/*
 * Macro- and script-specific actions.
 */

static void
dump_range(int first, int len, Boolean in_ascii, struct ea *buf,
    int rel_rows _is_unused, int rel_cols)
{
	register int i;
	Boolean any = False;
	Boolean is_zero = False;
	char *linebuf;
	char *s;

	linebuf = Malloc(maxCOLS * 4 + 1);
	s = linebuf;

	/*
	 * If the client has looked at the live screen, then if they later
	 * execute 'Wait(output)', they will need to wait for output from the
	 * host.  output_wait_needed is cleared by sms_host_output,
	 * which is called from the write logic in ctlr.c.
	 */     
	if (sms != SN && buf == ea_buf)
		sms->output_wait_needed = True;

	is_zero = FA_IS_ZERO(get_field_attribute(first));

	for (i = 0; i < len; i++) {
		if (i && !((first + i) % rel_cols)) {
			*s = '\0';
			action_output("%s", linebuf);
			s = linebuf;
			any = False;
		}
		if (in_ascii) {
			char mb[16];
			ucs4_t uc;
			int j;
			int xlen;

			if (buf[first + i].fa) {
				is_zero = FA_IS_ZERO(buf[first + i].fa);
				s += sprintf(s, " ");
			} else if (is_zero)
				s += sprintf(s, " ");
			else
#if defined(X3270_DBCS) /*[*/
			if (IS_LEFT(ctlr_dbcs_state(first + i))) {

				xlen = ebcdic_to_multibyte(
					(buf[first + i].cc << 8) |
					 buf[first + i + 1].cc,
					mb, sizeof(mb));
				for (j = 0; j < xlen - 1; j++) {
					s += sprintf(s, "%c", mb[j]);
				}
			} else if (IS_RIGHT(ctlr_dbcs_state(first + i))) {
				continue;
			} else
#endif /*]*/
			{
				xlen = ebcdic_to_multibyte_x(
						       buf[first + i].cc,
						       buf[first + i].cs,
						       mb, sizeof(mb),
						       EUO_BLANK_UNDEF,
						       &uc);
				for (j = 0; j < xlen - 1; j++) {
					s += sprintf(s, "%c", mb[j]);
				}
			}
		} else {
			s += sprintf(s, "%s%02x",
				i ? " " : "",
				buf[first + i].cc);
		}
		any = True;
	}
	if (any) {
		*s = '\0';
		action_output("%s", linebuf);
	}
	Free(linebuf);
}

static void
dump_fixed(String params[], Cardinal count, const char *name, Boolean in_ascii,
    struct ea *buf, int rel_rows, int rel_cols, int caddr)
{
	int row, col, len, rows = 0, cols = 0;

	switch (count) {
	    case 0:	/* everything */
		row = 0;
		col = 0;
		len = rel_rows*rel_cols;
		break;
	    case 1:	/* from cursor, for n */
		row = caddr / rel_cols;
		col = caddr % rel_cols;
		len = atoi(params[0]);
		break;
	    case 3:	/* from (row,col), for n */
		row = atoi(params[0]);
		col = atoi(params[1]);
		len = atoi(params[2]);
		break;
	    case 4:	/* from (row,col), for rows x cols */
		row = atoi(params[0]);
		col = atoi(params[1]);
		rows = atoi(params[2]);
		cols = atoi(params[3]);
		len = 0;
		break;
	    default:
		popup_an_error("%s requires 0, 1, 3 or 4 arguments", name);
		return;
	}

	if (
	    (row < 0 || row > rel_rows || col < 0 || col > rel_cols || len < 0) ||
	    ((count < 4)  && ((row * rel_cols) + col + len > rel_rows * rel_cols)) ||
	    ((count == 4) && (cols < 0 || rows < 0 ||
			      col + cols > rel_cols || row + rows > rel_rows))
	   ) {
		popup_an_error("%s: Invalid argument", name);
		return;
	}
	if (count < 4)
		dump_range((row * rel_cols) + col, len, in_ascii, buf,
		    rel_rows, rel_cols);
	else {
		int i;

		for (i = 0; i < rows; i++)
			dump_range(((row+i) * rel_cols) + col, cols, in_ascii,
			    buf, rel_rows, rel_cols);
	}
}

static void
dump_field(Cardinal count, const char *name, Boolean in_ascii)
{
	int faddr;
	unsigned char fa;
	int start, baddr;
	int len = 0;

	if (count != 0) {
		popup_an_error("%s requires 0 arguments", name);
		return;
	}
	if (!formatted) {
		popup_an_error("%s: Screen is not formatted", name);
		return;
	}
	faddr = find_field_attribute(cursor_addr);
	fa = get_field_attribute(cursor_addr);
	start = faddr;
	INC_BA(start);
	baddr = start;
	do {
		if (ea_buf[baddr].fa)
			break;
		len++;
		INC_BA(baddr);
	} while (baddr != start);
	dump_range(start, len, in_ascii, ea_buf, ROWS, COLS);
}

void
Ascii_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	dump_fixed(params, *num_params, action_name(Ascii_action), True,
		ea_buf, ROWS, COLS, cursor_addr);
}

void
AsciiField_action(Widget w _is_unused, XEvent *event _is_unused, String *params _is_unused,
    Cardinal *num_params)
{
	dump_field(*num_params, action_name(AsciiField_action), True);
}

void
Ebcdic_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	dump_fixed(params, *num_params, action_name(Ebcdic_action), False,
		ea_buf, ROWS, COLS, cursor_addr);
}

void
EbcdicField_action(Widget w _is_unused, XEvent *event _is_unused,
    String *params _is_unused, Cardinal *num_params)
{
	dump_field(*num_params, action_name(EbcdicField_action), False);
}

static unsigned char
calc_cs(unsigned char cs)
{
	switch (cs & CS_MASK) { 
	case CS_APL:
	    return 0xf1;
	case CS_LINEDRAW:
	    return 0xf2;
	case CS_DBCS:
	    return 0xf8;
	default:
	    return 0x00;
	}
}

/*
 * Internals of the ReadBuffer action.
 * Operates on the supplied 'buf' parameter, which might be the live
 * screen buffer 'ea_buf' or a copy saved with 'Snap'.
 */
static void
do_read_buffer(String *params, Cardinal num_params, struct ea *buf, int fd)
{
	register int	baddr;
	unsigned char	current_fg = 0x00;
	unsigned char	current_gr = 0x00;
	unsigned char	current_cs = 0x00;
	Boolean in_ebcdic = False;
	rpf_t r;

	if (num_params > 0) {
		if (num_params > 1) {
			popup_an_error("%s: extra agruments",
					action_name(ReadBuffer_action));
			return;
		}
		if (!strncasecmp(params[0], "Ascii", strlen(params[0])))
			in_ebcdic = False;
		else if (!strncasecmp(params[0], "Ebcdic", strlen(params[0])))
			in_ebcdic = True;
		else {
			popup_an_error("%s: first parameter must be "
					"Ascii or Ebcdic",
					action_name(ReadBuffer_action));
			return;
		}
					                                        
	}
	if (fd >= 0) {
		char *s;
		int nw;

		s = xs_buffer("rows %d cols %d cursor %d\n", ROWS, COLS,
				cursor_addr);
		nw = write(fd, s, strlen(s));
		Free(s);
		if (nw < 0)
			return;
	}

	rpf_init(&r);
	baddr = 0;
	do {
		if (!(baddr % COLS)) {
			if (baddr) {
				if (fd >= 0) {
					if (write(fd, r.buf + 1,
						     strlen(r.buf + 1)) < 0)
						goto done;
					if (write(fd, "\n", 1) < 0)
						goto done;
				} else
					action_output("%s", r.buf + 1);
			}
			rpf_reset(&r);
		}
		if (buf[baddr].fa) {

			rpf(&r, " SF(%02x=%02x", XA_3270, buf[baddr].fa);
			if (buf[baddr].fg)
				rpf(&r, ",%02x=%02x", XA_FOREGROUND,
						buf[baddr].fg);
			if (buf[baddr].gr)
				rpf(&r, ",%02x=%02x", XA_HIGHLIGHTING,
						buf[baddr].gr | 0xf0);
			if (buf[baddr].cs & CS_MASK)
				rpf(&r, ",%02x=%02x", XA_CHARSET,
						calc_cs(buf[baddr].cs));
			rpf(&r, ")");
		} else {
			if (buf[baddr].fg != current_fg) {
				rpf(&r, " SA(%02x=%02x)", XA_FOREGROUND,
						buf[baddr].fg);
				current_fg = buf[baddr].fg;
			}
			if (buf[baddr].gr != current_gr) {
				rpf(&r, " SA(%02x=%02x)", XA_HIGHLIGHTING,
						buf[baddr].gr | 0xf0);
				current_gr = buf[baddr].gr;
			}
			if ((buf[baddr].cs & ~CS_GE) !=
					(current_cs & ~CS_GE)) {
				rpf(&r, " SA(%02x=%02x)", XA_CHARSET,
						calc_cs(buf[baddr].cs));
				current_cs = buf[baddr].cs;
			}
			if (in_ebcdic) {
				if (buf[baddr].cs & CS_GE)
					rpf(&r, " GE(%02x)", buf[baddr].cc);
				else
					rpf(&r, " %02x", buf[baddr].cc);
			} else {
			    	Boolean done = False;
				char mb[16];
				int j;
				ucs4_t uc;
#if defined(X3270_DBCS) /*[*/
				int len;

				if (IS_LEFT(ctlr_dbcs_state(baddr))) {
				    	len = ebcdic_to_multibyte(
						(buf[baddr].cc << 8) |
						 buf[baddr + 1].cc,
						mb, sizeof(mb));
					rpf(&r, " ");
					for (j = 0; j < len-1; j++)
						rpf(&r, "%02x", mb[j] & 0xff);
					done = True;
				} else if (IS_RIGHT(ctlr_dbcs_state(baddr))) {
					rpf(&r, " -");
					done = True;
				}
#endif /*]*/

				switch (buf[baddr].cc) {
				case EBC_null:
					mb[0] = '\0';
					break;
				case EBC_so:
					mb[0] = 0x0e;
					mb[1] = '\0';
					break;
				case EBC_si:
					mb[0] = 0x0f;
					mb[1] = '\0';
					break;
				default:
					(void) ebcdic_to_multibyte_x(
							       buf[baddr].cc,
							       buf[baddr].cs,
							       mb, sizeof(mb),
							       EUO_NONE,
							       &uc);
				    break;
				}

				if (!done) {
					rpf(&r, " ");
					if (mb[0] == '\0')
						rpf(&r, "00");
					else {
					    	for (j = 0; mb[j]; j++) {
						    	rpf(&r, "%02x",
								mb[j] & 0xff);
						}
					}
				}
			}
		}
		INC_BA(baddr);
	} while (baddr != 0);
	if (fd >= 0) {
		if (write(fd, r.buf + 1, strlen(r.buf + 1)) < 0)
		    	goto done;
		if (write(fd, "\n", 1) < 0)
		    	goto done;
	} else
		action_output("%s", r.buf + 1);
done:
	rpf_free(&r);
}

/*
 * ReadBuffer action.
 */
void
ReadBuffer_action(Widget w _is_unused, XEvent *event _is_unused,
    String *params, Cardinal *num_params)
{
	do_read_buffer(params, *num_params, ea_buf, -1);
}

/*
 * The sms prompt is preceeded by a status line with 11 fields:
 *
 *  1 keyboard status
 *     U unlocked
 *     L locked, waiting for host response
 *     E locked, keying error
 *  2 formatting status of screen
 *     F formatted
 *     U unformatted
 *  3 protection status of current field
 *     U unprotected (modifiable)
 *     P protected
 *  4 connect status
 *     N not connected
 *     C(host) connected
 *  5 emulator mode
 *     N not connected
 *     C connected in ANSI character mode
 *     L connected in ANSI line mode
 *     P 3270 negotiation pending
 *     I connected in 3270 mode
 *  6 model number
 *  7 rows
 *  8 cols
 *  9 cursor row
 * 10 cursor col
 * 11 main window id
 */
static char *
status_string(void)
{
	char kb_stat;
	char fmt_stat;
	char prot_stat;
	char *connect_stat = CN;
	char em_mode;
	char s[1024];
	char *r;

	if (!kybdlock)
		kb_stat = 'U';
	else if (!CONNECTED || KBWAIT)
		kb_stat = 'L';
	else
		kb_stat = 'E';

	if (formatted)
		fmt_stat = 'F';
	else
		fmt_stat = 'U';

	if (!formatted)
		prot_stat = 'U';
	else {
		unsigned char fa;

		fa = get_field_attribute(cursor_addr);
		if (FA_IS_PROTECTED(fa))
			prot_stat = 'P';
		else
			prot_stat = 'U';
	}

	if (CONNECTED)
		connect_stat = xs_buffer("C(%s)", current_host);
	else
		connect_stat = NewString("N");

	if (CONNECTED) {
		if (IN_ANSI) {
			if (linemode)
				em_mode = 'L';
			else
				em_mode = 'C';
		} else if (IN_3270)
			em_mode = 'I';
		else
			em_mode = 'P';
	} else
		em_mode = 'N';

	(void) sprintf(s,
	    "%c %c %c %s %c %d %d %d %d %d 0x%lx",
	    kb_stat,
	    fmt_stat,
	    prot_stat,
	    connect_stat,
	    em_mode,
	    model_num,
	    ROWS, COLS,
	    cursor_addr / COLS, cursor_addr % COLS,
#if defined(X3270_DISPLAY) /*[*/
	    XtWindow(toplevel)
#else /*][*/
	    0L
#endif /*]*/
	    );

	r = NewString(s);
	Free(connect_stat);
	return r;
}

static void
script_prompt(Boolean success)
{
	char *s;
	char timing[64];
	char *t;

	s = status_string();

	if (sms != SN && sms->accumulated)
		(void) sprintf(timing, "%ld.%03ld", sms->msec / 1000L,
			sms->msec % 1000L);
	else
		(void) strcpy(timing, "-");

	t = Malloc(strlen(s) + 1 + strlen(timing) + 1 + 6 + 1);
	sprintf(t, "%s %s\n%s\n", s, timing, success ? "ok" : "error");
	Free(s);

	if (sms->is_socket) {
	    	send(sms->infd, t, strlen(t), 0);
	} else {
		(void) fprintf(sms->outfile, "%s", t);
		(void) fflush(sms->outfile);
	}
	free(t);
}

/* Save the state of the screen for Snap queries. */
static char *snap_status = NULL;
static struct ea *snap_buf = NULL;
static int snap_rows = 0;
static int snap_cols = 0;
static int snap_field_start = -1;
static int snap_field_length = -1;
static int snap_caddr = 0;

static void
snap_save(void)
{
	sms->output_wait_needed = True;
	Replace(snap_status, status_string());

	Replace(snap_buf, (struct ea *)Malloc(ROWS*COLS*sizeof(struct ea)));
	(void) memcpy(snap_buf, ea_buf, ROWS*COLS*sizeof(struct ea));

	snap_rows = ROWS;
	snap_cols = COLS;

	if (!formatted) {
		snap_field_start = -1;
		snap_field_length = -1;
	} else {
		int baddr;

		snap_field_length = 0;
		snap_field_start = find_field_attribute(cursor_addr);
		INC_BA(snap_field_start);
		baddr = snap_field_start;
		do {
			if (ea_buf[baddr].fa)
				break;
			snap_field_length++;
			INC_BA(baddr);
		} while (baddr != snap_field_start);
	}
	snap_caddr = cursor_addr;
}

/*
 * "Snap" action, maintains a snapshot for consistent multi-field comparisons:
 *
 *  Snap [Save]
 *	updates the saved image from the live image
 *  Snap Rows
 *	returns the number of rows
 *  Snap Cols
 *	returns the number of columns
 *  Snap Staus
 *  Snap Ascii ...
 *  Snap AsciiField (not yet)
 *  Snap Ebcdic ...
 *  Snap EbcdicField (not yet)
 *  Snap ReadBuffer
 *	runs the named command
 *  Snap Wait [tmo] Output
 *      wait for the screen to change, then do a Snap Save
 */
void
Snap_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	if (sms == SN || sms->state != SS_RUNNING) {
		popup_an_error("%s can only be called from scripts or macros",
		    action_name(Snap_action));
		return;
	}

	if (*num_params == 0) {
		snap_save();
		return;
	}

	/* Handle 'Snap Wait' separately. */
	if (!strcasecmp(params[0], action_name(Wait_action))) {
		long tmo = -1;
		char *ptr;
		Cardinal maxp = 0;

		if (*num_params > 1 &&
		    (tmo = strtol(params[1], &ptr, 10)) >= 0 &&
		    ptr != params[0] &&
		    *ptr == '\0') {
			maxp = 3;
		} else {
			tmo = -1;
			maxp = 2;
		}
		if (*num_params > maxp) {
			popup_an_error("Too many arguments to %s %s",
			    action_name(Snap_action),
			    action_name(Wait_action));
			    return;
		}
		if (*num_params < maxp) {
			popup_an_error("Too few arguments to %s %s",
			    action_name(Snap_action),
			    action_name(Wait_action));
			    return;
		}
		if (strcasecmp(params[*num_params - 1], "Output")) {
			popup_an_error("Unknown parameter to %s %s",
			    action_name(Snap_action),
			    action_name(Wait_action));
			    return;
		}

		/* Must be connected. */
		if (!(CONNECTED || HALF_CONNECTED)) {
			popup_an_error("%s: Not connected",
			    action_name(Snap_action));
			return;
		}

		/*
		 * Make sure we need to wait.
		 * If we don't, then Snap(Wait) is equivalent to Snap().
		 */
		if (!sms->output_wait_needed) {
			snap_save();
			return;
		}

		/* Set the new state. */
		sms->state = SS_SWAIT_OUTPUT;

		/* Set up a timeout, if they want one. */
		if (tmo >= 0)
			sms->wait_id = AddTimeOut(tmo? (tmo * 1000): 1,
					wait_timed_out);
		return;
	}

	if (!strcasecmp(params[0], "Save")) {
		if (*num_params != 1) {
			popup_an_error("Extra argument(s)");
			return;
		}
		snap_save();
	} else if (!strcasecmp(params[0], "Status")) {
		if (*num_params != 1) {
			popup_an_error("Extra argument(s)");
			return;
		}
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		action_output("%s", snap_status);
	} else if (!strcasecmp(params[0], "Rows")) {
		if (*num_params != 1) {
			popup_an_error("Extra argument(s)");
			return;
		}
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		action_output("%d", snap_rows);
	} else if (!strcasecmp(params[0], "Cols")) {
		if (*num_params != 1) {
			popup_an_error("Extra argument(s)");
			return;
		}
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		action_output("%d", snap_cols);
	} else if (!strcasecmp(params[0], action_name(Ascii_action))) {
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		dump_fixed(params + 1, *num_params - 1,
			action_name(Ascii_action), True, snap_buf,
			snap_rows, snap_cols, snap_caddr);
	} else if (!strcasecmp(params[0], action_name(Ebcdic_action))) {
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		dump_fixed(params + 1, *num_params - 1,
			action_name(Ebcdic_action), False, snap_buf,
			snap_rows, snap_cols, snap_caddr);
	} else if (!strcasecmp(params[0], action_name(ReadBuffer_action))) {
		if (snap_status == NULL) {
			popup_an_error("No saved state");
			return;
		}
		do_read_buffer(params + 1, *num_params - 1, snap_buf, -1);
	} else {
		popup_an_error("%s: Argument must be Save, Status, Rows, Cols, "
		    "%s, %s %s, or %s",
		    action_name(Snap_action),
		    action_name(Wait_action),
		    action_name(Ascii_action),
		    action_name(Ebcdic_action),
		    action_name(ReadBuffer_action));
	}
}

/*
 * Wait for various conditions.
 */
void
Wait_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	enum sms_state next_state = SS_WAIT_IFIELD;
	long tmo = -1;
	char *ptr;
	Cardinal np;
	String *pr;

	/* Pick off the timeout parameter first. */
	if (*num_params > 0 &&
	    (tmo = strtol(params[0], &ptr, 10)) >= 0 &&
	    ptr != params[0] &&
	    *ptr == '\0') {
		np = *num_params - 1;
		pr = params + 1;
	} else {
		tmo = -1;
		np = *num_params;
		pr = params;
	}

	if (np > 1) {
		popup_an_error("Too many arguments to %s or invalid timeout "
				"value", action_name(Wait_action));
		return;
	}
	if (sms == SN || sms->state != SS_RUNNING) {
		popup_an_error("%s can only be called from scripts or macros",
		    action_name(Wait_action));
		return;
	}
	if (np == 1) {
		if (!strcasecmp(pr[0], "NVTMode") ||
		    !strcasecmp(pr[0], "ansi")) {
			if (!IN_ANSI)
				next_state = SS_WAIT_NVT;
		} else if (!strcasecmp(pr[0], "3270Mode") ||
			   !strcasecmp(pr[0], "3270")) {
			if (!IN_3270)
			    next_state = SS_WAIT_3270;
		} else if (!strcasecmp(pr[0], "Output")) {
			if (sms->output_wait_needed)
				next_state = SS_WAIT_OUTPUT;
			else
				return;
		} else if (!strcasecmp(pr[0], "Disconnect")) {
			if (CONNECTED)
				next_state = SS_WAIT_DISC;
			else
				return;
		} else if (!strcasecmp(pr[0], "Unlock")) {
			if (KBWAIT)
				next_state = SS_WAIT_UNLOCK;
			else
				return;
		} else if (tmo > 0 && !strcasecmp(pr[0], "Seconds")) {
		    	next_state = SS_TIME_WAIT;
		} else if (strcasecmp(pr[0], "InputField")) {
			popup_an_error("%s argument must be InputField, "
			    "NVTmode, 3270Mode, Output, Seconds, Disconnect "
			    "or Unlock",
			action_name(Wait_action));
			return;
		}
	}
	if (!(CONNECTED || HALF_CONNECTED)) {
		popup_an_error("%s: Not connected", action_name(Wait_action));
		return;
	}

	/* Is it already okay? */
	if (next_state == SS_WAIT_IFIELD && CAN_PROCEED)
		return;

	/* No, wait for it to happen. */
	sms->state = next_state;

	/* Set up a timeout, if they want one. */
	if (tmo >= 0)
		sms->wait_id = AddTimeOut(tmo? (tmo * 1000): 1, wait_timed_out);
}

/*
 * Callback from Connect() and Reconnect() actions, to minimally pause a
 * running sms.
 */
void
sms_connect_wait(void)
{
	if (sms != SN &&
	    (int)sms->state >= (int)SS_RUNNING &&
	    sms->state != SS_WAIT_IFIELD) {
		if (HALF_CONNECTED ||
		    (CONNECTED && (kybdlock & KL_AWAITING_FIRST)))
			sms->state = SS_CONNECT_WAIT;
	}
}

/*
 * Callback from ctlr.c, to indicate that the host has changed the screen.
 */
void
sms_host_output(void)
{
	if (sms != SN) {
		sms->output_wait_needed = False;

		switch (sms->state) {
		case SS_SWAIT_OUTPUT:
		    snap_save();
		    /* fall through... */
		case SS_WAIT_OUTPUT:
		    sms->state = SS_RUNNING;
		    sms_continue();
		    break;
		default:
		    break;
		}
	}
}

/* Return whether error pop-ups and acition output should be short-circuited. */
static sms_t *
sms_redirect_to(void)
{
    	sms_t *s;

	for (s = sms; s != SN; s = s->next) {
	    	if ((s->type == ST_CHILD || s->type == ST_PEER) &&
		    (s->state == SS_RUNNING ||
		     s->state == SS_CONNECT_WAIT ||
		     s->state == SS_WAIT_OUTPUT ||
		     s->state == SS_SWAIT_OUTPUT ||
		     s->wait_id != 0L))
			return s;
	}
	return NULL;
}

/* Return whether error pop-ups and acition output should be short-circuited. */
Boolean
sms_redirect(void)
{
    	return sms_redirect_to() != NULL;
}

#if defined(X3270_MENUS) || defined(C3270) /*[*/
/* Return whether any scripts are active. */
Boolean
sms_active(void)
{
	return sms != SN;
}
#endif /*]*/

/* Translate an expect string (uses C escape syntax). */
static void
expand_expect(char *s)
{
	char *t = Malloc(strlen(s) + 1);
	char c;
	enum { XS_BASE, XS_BS, XS_O, XS_X } state = XS_BASE;
	int n = 0;
	int nd = 0;
	static char hexes[] = "0123456789abcdef";

	expect_text = t;

	while ((c = *s++)) {
		switch (state) {
		    case XS_BASE:
			if (c == '\\')
				state = XS_BS;
			else
				*t++ = c;
			break;
		    case XS_BS:
			switch (c) {
			    case 'x':
				nd = 0;
				n = 0;
				state = XS_X;
				break;
			    case 'r':
				*t++ = '\r';
				state = XS_BASE;
				break;
			    case 'n':
				*t++ = '\n';
				state = XS_BASE;
				break;
			    case 'b':
				*t++ = '\b';
				state = XS_BASE;
				break;
			    default:
				if (c >= '0' && c <= '7') {
					nd = 1;
					n = c - '0';
					state = XS_O;
				} else {
					*t++ = c;
					state = XS_BASE;
				}
				break;
			}
			break;
		    case XS_O:
			if (nd < 3 && c >= '0' && c <= '7') {
				n = (n * 8) + (c - '0');
				nd++;
			} else {
				*t++ = n;
				*t++ = c;
				state = XS_BASE;
			}
			break;
		    case XS_X:
			if (isxdigit(c)) {
				n = (n * 16) +
				    strchr(hexes, tolower(c)) - hexes;
				nd++;
			} else {
				if (nd) 
					*t++ = n;
				else
					*t++ = 'x';
				*t++ = c;
				state = XS_BASE;
			}
			break;
		}
	}
	expect_len = t - expect_text;
}

/* 'mem' version of strstr */
static char *
memstr(char *s1, char *s2, int n1, int n2)
{
	int i;

	for (i = 0; i <= n1 - n2; i++, s1++)
		if (*s1 == *s2 && !memcmp(s1, s2, n2))
			return s1;
	return CN;
}

/* Check for a match against an expect string. */
static Boolean
expect_matches(void)
{
	int ix, i;
	unsigned char buf[ANSI_SAVE_SIZE];
	char *t;

	ix = (ansi_save_ix + ANSI_SAVE_SIZE - ansi_save_cnt) % ANSI_SAVE_SIZE;
	for (i = 0; i < ansi_save_cnt; i++) {
		buf[i] = ansi_save_buf[(ix + i) % ANSI_SAVE_SIZE];
	}
	t = memstr((char *)buf, expect_text, ansi_save_cnt, expect_len);
	if (t != CN) {
		ansi_save_cnt -= ((unsigned char *)t - buf) + expect_len;
		Free(expect_text);
		expect_text = CN;
		return True;
	} else
		return False;
}

/* Store an ANSI character for use by the Ansi action. */
void
sms_store(unsigned char c)
{
	if (sms == SN)
		return;

	/* Save the character in the buffer. */
	ansi_save_buf[ansi_save_ix++] = c;
	ansi_save_ix %= ANSI_SAVE_SIZE;
	if (ansi_save_cnt < ANSI_SAVE_SIZE)
		ansi_save_cnt++;

	/* If a script or macro is waiting to match a string, check now. */
	if (sms->state == SS_EXPECTING && expect_matches()) {
		RemoveTimeOut(sms->expect_id);
		sms->expect_id = 0L;
		sms->state = SS_INCOMPLETE;
		sms_continue();
	}
}

/* Dump whatever ANSI data has been sent by the host since last called. */
void
AnsiText_action(Widget w _is_unused, XEvent *event _is_unused, String *params _is_unused,
    Cardinal *num_params _is_unused)
{
	register int i;
	int ix;
	unsigned char c;
	char linebuf[ANSI_SAVE_SIZE * 4 + 1];
	char *s = linebuf;

	if (!ansi_save_cnt)
		return;
	ix = (ansi_save_ix + ANSI_SAVE_SIZE - ansi_save_cnt) % ANSI_SAVE_SIZE;
	for (i = 0; i < ansi_save_cnt; i++) {
		c = ansi_save_buf[(ix + i) % ANSI_SAVE_SIZE];
		if (!(c & ~0x1f)) switch (c) {
		    case '\n':
			s += sprintf(s, "\\n");
			break;
		    case '\r':
			s += sprintf(s, "\\r");
			break;
		    case '\b':
			s += sprintf(s, "\\b");
			break;
		    default:
			s += sprintf(s, "\\%03o", c);
			break;
		} else if (c == '\\')
			s += sprintf(s, "\\\\");
		else
			*s++ = (char)c;
	}
	*s = '\0';
	action_output("%s", linebuf);
	ansi_save_cnt = 0;
	ansi_save_ix = 0;
}

/* Pause a script. */
void
PauseScript_action(Widget w _is_unused, XEvent *event _is_unused, String *params _is_unused,
    Cardinal *num_params _is_unused)
{
	if (sms == SN || (sms->type != ST_PEER && sms->type != ST_CHILD)) {
		popup_an_error("%s can only be called from a script",
		    action_name(PauseScript_action));
		return;
	}
	sms->state = SS_PAUSED;
}

/* Continue a script. */
void
ContinueScript_action(Widget w, XEvent *event _is_unused, String *params,
	Cardinal *num_params)
{
	if (check_usage(ContinueScript_action, *num_params, 1, 1) < 0)
		return;

	/*
	 * If this is a nested script, this action aborts the current script,
	 * then applies to the previous one.
	 */
	if (w == (Widget)NULL && sms_depth > 1)
		sms_pop(False);

	/* Continue the previous script. */
	if (sms == SN || sms->state != SS_PAUSED) {
		popup_an_error("%s: No script waiting",
		    action_name(ContinueScript_action));
		sms_continue();
		return;
	}
	action_output("%s", params[0]);
	sms->state = SS_RUNNING;
	sms_continue();
}

/* Stop listening to stdin. */
void
CloseScript_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	if (sms != SN &&
	    (sms->type == ST_PEER || sms->type == ST_CHILD)) {

		/* Close this script. */
		sms->state = SS_CLOSING;
		script_prompt(True);

		/* If nonzero status passed, fail the calling script. */
		if (*num_params > 0 &&
		    atoi(params[0]) != 0 &&
		    sms->next != SN) {
			sms->next->success = False;
			if (sms->is_login)
				host_disconnect(True);
		}
	} else
		popup_an_error("%s can only be called from a script",
		    action_name(CloseScript_action));
}

/* Execute an arbitrary shell command. */
void
Execute_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
    	int status;

	if (check_usage(Execute_action, *num_params, 1, 1) < 0)
		return;
	status = system(params[0]);
	if (status < 0) {
	    	popup_an_errno(errno, "system(\"%s\") failed", params[0]);
	} else if (status != 0) {
#if defined(_WIN32) /*[*/
		popup_an_error("system(\"%s\") exited with status %d\n",
			params[0], status);
#else /*][*/
	    	if (WIFEXITED(status)) {
		    	popup_an_error("system(\"%s\") exited with status %d\n",
				params[0], WEXITSTATUS(status));
		} else if (WIFSIGNALED(status)) {
		    	popup_an_error("system(\"%s\") killed by signal %d\n",
				params[0], WTERMSIG(status));
		} else if (WIFSTOPPED(status)) {
		    	popup_an_error("system(\"%s\") stopped by signal %d\n",
				params[0], WSTOPSIG(status));
		}
#endif /*]*/
	} else {
#if defined(_WIN32) && !defined(S3270) /*[*/
	    	/* Get back mouse events; system() cancels them. */
	    	screen_fixup();
#endif /*]*/
	}
}

/* Timeout for Expect action. */
static void
expect_timed_out(void)
{
	if (sms == SN || sms->state != SS_EXPECTING)
		return;

	Free(expect_text);
	expect_text = CN;
	popup_an_error("%s: Timed out", action_name(Expect_action));
	sms->expect_id = 0L;
	sms->state = SS_INCOMPLETE;
	sms->success = False;
	if (sms->is_login)
		host_disconnect(True);
	sms_continue();
}

/* Timeout for Wait action. */
static void
wait_timed_out(void)
{
    	/* If they just wanted a delay, succeed. */
    	if (sms->state == SS_TIME_WAIT) {
	    	sms->success = True;
		sms->state = SS_INCOMPLETE;
		sms_continue();
		return;
	}

	/* Pop up the error message. */
	popup_an_error("%s: Timed out", action_name(Wait_action));

	/* Forget the ID. */
	sms->wait_id = 0L;

	/* If this is a login macro, it has failed. */
	if (sms->is_login)
		host_disconnect(True);

	sms->success = False;
	sms->state = SS_INCOMPLETE;

	/* Let the script proceed. */
	sms_continue();
}

/* Wait for a string from the host (ANSI mode only). */
void
Expect_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	int tmo;

	/* Verify the environment and parameters. */
	if (sms == SN || sms->state != SS_RUNNING) {
		popup_an_error("%s can only be called from a script or macro",
		    action_name(Expect_action));
		return;
	}
	if (check_usage(Expect_action, *num_params, 1, 2) < 0)
		return;
	if (!IN_ANSI) {
		popup_an_error("%s is valid only when connected in ANSI mode",
		    action_name(Expect_action));
	}
	if (*num_params == 2) {
		tmo = atoi(params[1]);
		if (tmo < 1 || tmo > 600) {
			popup_an_error("%s: Invalid timeout: %s",
			    action_name(Expect_action), params[1]);
			return;
		}
	} else
		tmo = 30;

	/* See if the text is there already; if not, wait for it. */
	expand_expect(params[0]);
	if (!expect_matches()) {
		sms->expect_id = AddTimeOut(tmo * 1000, expect_timed_out);
		sms->state = SS_EXPECTING;
	}
	/* else allow sms to proceed */
}


#if defined(X3270_MENUS) /*[*/

/* "Execute an Action" menu option */

static Widget execute_action_shell = (Widget)NULL;

/* Callback for "OK" button on execute action popup */
static void
execute_action_callback(Widget w _is_unused, XtPointer client_data,
    XtPointer call_data _is_unused)
{
	char *text;

	text = XawDialogGetValueString((Widget)client_data);
	XtPopdown(execute_action_shell);
	if (!text)
		return;
	push_macro(text, False);
}

void
execute_action_option(Widget w _is_unused, XtPointer client_data _is_unused,
    XtPointer call_data _is_unused)
{
	if (execute_action_shell == NULL)
		execute_action_shell = create_form_popup("ExecuteAction",
		    execute_action_callback, (XtCallbackProc)NULL, FORM_NO_CC);

	popup_popup(execute_action_shell, XtGrabExclusive);
}

#endif /*]*/

#if defined(X3270_SCRIPT) /*[*/

# if defined(_WIN32) /*[*/
/* Let the system pick a TCP port to bind to, and listen on it. */
static unsigned short
pick_port(int *sp)
{
    	int s;
    	struct sockaddr_in sin;
	socklen_t len;

	s = socket(PF_INET, SOCK_STREAM, 0);
	if (s < 0) {
	    	popup_an_error("socket: %s\n", win32_strerror(GetLastError()));
		return 0;
	}
	(void) memset(&sin, '\0', sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
	    	popup_an_error("bind: %s\n", win32_strerror(GetLastError()));
		closesocket(s);
		return 0;
	}
	len = sizeof(sin);
	if (getsockname(s, (struct sockaddr *)&sin, &len) < 0) {
	    	popup_an_error("getsockaddr: %s\n",
			win32_strerror(GetLastError()));
		closesocket(s);
		return 0;
	}
	if (listen(s, 10) < 0) {
	    	popup_an_error("listen: %s\n", win32_strerror(GetLastError()));
		closesocket(s);
		return 0;
	}
	*sp = s;
	return ntohs(sin.sin_port);
}
# endif /*]*/

/* "Script" action, runs a script as a child process. */
# if !defined(_WIN32) /*[*/
void
Script_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	int inpipe[2];
	int outpipe[2];

	if (*num_params < 1) {
		popup_an_error("%s requires at least one argument",
		    action_name(Script_action));
		return;
	}

	/* Create a new script description. */
	if (!sms_push(ST_CHILD))
		return;

	/*
	 * Create pipes and stdout stream for the script process.
	 *  inpipe[] is read by x3270, written by the script
	 *  outpipe[] is written by x3270, read by the script
	 */
	if (pipe(inpipe) < 0) {
		sms_pop(False);
		popup_an_error("pipe() failed");
		return;
	}
	if (pipe(outpipe) < 0) {
		(void) close(inpipe[0]);
		(void) close(inpipe[1]);
		sms_pop(False);
		popup_an_error("pipe() failed");
		return;
	}
	if ((sms->outfile = fdopen(outpipe[1], "w")) == (FILE *)NULL) {
		(void) close(inpipe[0]);
		(void) close(inpipe[1]);
		(void) close(outpipe[0]);
		(void) close(outpipe[1]);
		sms_pop(False);
		popup_an_error("fdopen() failed");
		return;
	}
	(void) SETLINEBUF(sms->outfile);

	/* Fork and exec the script process. */
	if ((sms->pid = fork_child()) < 0) {
		(void) close(inpipe[0]);
		(void) close(inpipe[1]);
		(void) close(outpipe[0]);
		sms_pop(False);
		popup_an_error("fork() failed");
		return;
	}

	/* Child processing. */
	if (sms->pid == 0) {
		char **argv;
		Cardinal i;
		char env_buf[2][32];

		/* Clean up the pipes. */
		(void) close(outpipe[1]);
		(void) close(inpipe[0]);

		/* Export the names of the pipes into the environment. */
		(void) sprintf(env_buf[0], "X3270OUTPUT=%d", outpipe[0]);
		(void) putenv(env_buf[0]);
		(void) sprintf(env_buf[1], "X3270INPUT=%d", inpipe[1]);
		(void) putenv(env_buf[1]);

		/* Set up arguments. */
		argv = (char **)Malloc((*num_params + 1) * sizeof(char *));
		for (i = 0; i < *num_params; i++)
			argv[i] = params[i];
		argv[i] = CN;

		/* Exec. */
		(void) execvp(params[0], argv);
		(void) fprintf(stderr, "exec(%s) failed\n", params[0]);
		(void) _exit(1);
	}

	/* Clean up our ends of the pipes. */
	sms->infd = inpipe[0];
	(void) close(inpipe[1]);
	(void) close(outpipe[0]);

	/* Enable input. */
	script_enable();

	/* Set up to reap the child's exit status. */
	++children;
}
# endif /*]*/

# if defined(_WIN32) /*[*/
/* "Script" action, runs a script as a child process. */
void
Script_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	int s = -1;
	unsigned short port = 0;
	HANDLE hevent;
	char *pe;
	STARTUPINFO startupinfo;
	PROCESS_INFORMATION process_information;
	char *args;
	Cardinal i;

	if (*num_params < 1) {
		popup_an_error("%s requires at least one argument",
		    action_name(Script_action));
		return;
	}

	/* Set up X3270PORT for the child process. */
	port = pick_port(&s);
	if (port == 0)
	    	return;
	hevent = CreateEvent(NULL, FALSE, FALSE, NULL);
	if (hevent == NULL) {
		popup_an_error("CreateEvent: %s",
			win32_strerror(GetLastError()));
		closesocket(s);
		return;
	}
	if (WSAEventSelect(s, hevent, FD_ACCEPT) != 0) {
		popup_an_error("WSAEventSelect: %s",
			win32_strerror(GetLastError()));
		closesocket(s);
		return;
	}

	pe = xs_buffer("X3270PORT=%d", port);
	putenv(pe);
	Free(pe);

	/* Start the child process. */
	(void) memset(&startupinfo, '\0', sizeof(STARTUPINFO));
	startupinfo.cb = sizeof(STARTUPINFO);
	(void) memset(&process_information, '\0', sizeof(PROCESS_INFORMATION));
	args = NewString(params[0]);
	for (i = 1; i < *num_params; i++) {
	    	char *t;

		if (strchr(params[i], ' ') != NULL &&
		    params[i][0] != '"' &&
		    params[i][strlen(params[i]) - 1] != '"')
			t = xs_buffer("%s \"%s\"", args, params[i]);
		else
			t = xs_buffer("%s %s", args, params[i]);
		Free(args);
		args = t;
	}
	if (CreateProcess(
		    NULL,
		    args,
		    NULL,
		    NULL,
		    FALSE,
		    0,
		    NULL,
		    NULL,
		    &startupinfo,
		    &process_information) == 0) {
	    	popup_an_error("CreateProcess(%s) failed: %s",
			params[0], win32_strerror(GetLastError()));
		Free(args);
		return;
	} else {
	    	Free(args);
	    	CloseHandle(process_information.hThread);
	}

	/* Create a new script description. */
	if (!sms_push(ST_CHILD))
		return;
	sms->child_handle = process_information.hProcess;
	sms->inhandle = hevent;
	sms->infd = s;

	/*
	 * Wait for the child process to exit.
	 * Note that this is an asynchronous event -- exits for multiple
	 * children can happen in any order.
	 */
	sms->exit_id = AddInput((int)process_information.hProcess,
			    child_exited);

	/* Allow the child script to connect back to us. */
	sms->listen_id = AddInput((int)hevent, child_socket_connection);

	/* Enable input. */
	script_enable();

}
# endif /*]*/
#endif /*]*/

/* "Macro" action, explicitly invokes a named macro. */
void
Macro_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	struct macro_def *m;

	if (check_usage(Macro_action, *num_params, 1, 1) < 0)
		return;
	for (m = macro_defs; m != (struct macro_def *)NULL; m = m->next) {
		if (!strcmp(m->name, params[0])) {
			push_macro(m->action, False);
			return;
		}
	}
	popup_an_error("no such macro: '%s'", params[0]);
}

#if defined(X3270_SCRIPT) /*[*/
/*
 * Idle cancellation: cancels the idle command if the current sms or any sms
 * that called it caused an error.
 */
void
cancel_if_idle_command(void)
{
	sms_t *s;

	for (s = sms; s != NULL; s = s->next) {
		if (s->type == ST_IDLE) {
			cancel_idle_timer();
			s->idle_error = True;
			trace_dsn("Cancelling idle command");
			break;
		}
	}
}
#endif /*]*/

#if defined(X3270_PRINTER) /*[*/
/* "Printer" action, starts or stops a printer session. */
void
Printer_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	if (check_usage(Printer_action, *num_params, 1, 2) < 0)
		return;
	if (!strcasecmp(params[0], "Start")) {
		printer_start((*num_params > 1)? params[1] : CN);
	} else if (!strcasecmp(params[0], "Stop")) {
		if (*num_params != 1) {
			popup_an_error("%s: Extra argument(s)",
			    action_name(Printer_action));
			return;
		}
		printer_stop();
	} else {
		popup_an_error("%s: Argument must Start or Stop",
		    action_name(Printer_action));
	}
}
#endif /*]*/

#if defined(X3270_SCRIPT) /*[*/
/* Abort all running scripts. */
void
abort_script(void)
{
	while (sms != SN) {
#if !defined(_WIN32) /*[*/
		if (sms->type == ST_CHILD && sms->pid > 0)
			(void) kill(sms->pid, SIGTERM);
#endif /*]*/
		sms_pop(True);
	}
}

/* "Abort" action, stops pending scripts. */
void
Abort_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
#if !defined(_WIN32) /*[*/
	child_ignore_output();
#endif /*]*/
	abort_script();
}
#endif /*]*/

#if defined(X3270_SCRIPT) /*[*/
/* Accumulate command execution time. */
void
sms_accumulate_time(struct timeval *t0, struct timeval *t1)
{
    if (sms != SN) {
	sms->accumulated = True;
	sms->msec += (t1->tv_sec - t0->tv_sec) * 1000 +
		     (t1->tv_usec - t0->tv_usec + 500) / 1000;
#if defined(DEBUG_ACCUMULATE) /*[*/
	printf("%s: accumulated %lu msec\n", ST_NAME, sms->msec);
#endif /*]*/
    }
}
#endif /*]*/

void
Query_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	static struct {
		char *name;
		const char *(*fn)(void);
	} queries[] = {
		{ "BindPluName", net_query_bind_plu_name },
		{ "ConnectionState", net_query_connection_state },
		{ "Host", net_query_host },
		{ "LuName", net_query_lu_name },
		{ CN, NULL }
	};
	int i;

	switch (*num_params) {
	case 0:
		for (i = 0; queries[i].name != CN; i++) {
			action_output("%s: %s", queries[i].name,
					(*queries[i].fn)());
		}
		break;
	case 1:
		for (i = 0; queries[i].name != CN; i++) {
			if (!strcasecmp(params[0], queries[i].name)) {
				const char *s;

				s = (*queries[i].fn)();
				action_output("%s\n", *s? s: " ");
				return;
			}
		}
		popup_an_error("%s: Unknown parameter",
				action_name(Query_action));
		break;
	default:
		popup_an_error("%s: Requires 0 or 1 arguments",
				action_name(Query_action));
		break;
	}
}

#if defined(X3270_SCRIPT) /*[*/

#if defined(X3270_PLUGIN) /*[*/

/* Plugin facility. */

static void
no_plugin(void)
{
	if (plugin_timeout_id != 0L) {
		RemoveTimeOut(plugin_timeout_id);
		plugin_timeout_id = 0L;
	}

	if (plugin_input_id != 0L) {
		RemoveInput(plugin_input_id);
		plugin_input_id = 0L;
	}

	if (plugin_inpipe != -1) {
		close(plugin_inpipe);
		plugin_inpipe = -1;
	}

	if (plugin_outpipe != -1) {
		close(plugin_outpipe);
		plugin_outpipe = -1;
	}

	plugin_pid = 0;
	plugin_started = False;
	prb_cnt = 0;
	ptq_first = ptq_last = 0;
}

/* Read a response from the plugin process. */
static void
plugin_input(void)
{
	int nr;
	char *nl;

	nr = read(plugin_inpipe, plugin_buf + prb_cnt, PRB_MAX - prb_cnt);
	if (nr < 0) {
		if (plugin_complain)
			popup_an_errno(errno, "Read from plugin command");
		else {
			plugin_start_failed = True;
			(void) snprintf(plugin_start_error, PRB_MAX,
					"Read from plugin command: %s",
					strerror(errno));
		}
		no_plugin();
		return;
	}
	if (nr == 0) {
		if (plugin_complain)
			popup_an_error("Plugin command exited");
		else {
			plugin_start_failed = True;
			(void) snprintf(plugin_start_error, PRB_MAX,
					"Plugin command exited");
		}
		no_plugin();
		return;
	}
	nl = memchr(plugin_buf + prb_cnt, '\n', nr);
	if (nl != NULL) {
		int xtra;

		*nl = '\0';

		trace_dsn("From plugin: %s\n", plugin_buf);

		switch (plugin_tq[ptq_first]) {
		case PLUGIN_INIT:
			if (strcasecmp(plugin_buf, "ok")) {
				if (plugin_complain)
					popup_an_error("Plugin start-up "
							"failed:\n%s",
							plugin_buf);
				else {
					plugin_start_failed = True;
					(void) snprintf(plugin_start_error,
							PRB_MAX,
							"Plugin start-up "
							"failed:\n%s",
							plugin_buf);
				}
				no_plugin();
			}
			plugin_started = True;
			plugin_complain = True;
			break;
		case PLUGIN_AID:
			/* feedback is just for trace/debug purposes */
			break;
		case PLUGIN_CMD:
		default:
			push_macro(plugin_buf, False);
			break;
		}

		ptq_first = (ptq_first + 1) % PLUGIN_QMAX;
		if (ptq_first == ptq_last) {
			RemoveInput(plugin_input_id);
			plugin_input_id = 0L;
			RemoveTimeOut(plugin_timeout_id);
			plugin_timeout_id = 0L;
		}

		xtra = (plugin_buf + prb_cnt + nr - 1) - nl;
		if (xtra > 0)
			(void) memmove(plugin_buf, nl + 1, xtra);
		prb_cnt = xtra;
		return;
	}

	prb_cnt += nr;
	if (prb_cnt >= PRB_MAX) {
		if (plugin_complain)
			popup_an_error("Plugin command buffer overflow");
		else {
			plugin_start_failed = True;
			(void) snprintf(plugin_start_error, PRB_MAX,
					"Plugin command buffer overflow");
		}
		no_plugin();
	}
}

/* Plugin process took too long to answer.  Kill it. */
static void
plugin_timeout(void)
{
	char *s;

	switch (plugin_tq[ptq_first]) {
	case PLUGIN_INIT:
		s = "init";
		break;
	case PLUGIN_AID:
		s = "AID";
		break;
	case PLUGIN_CMD:
	default:
		s = "command";
		break;
	}
	if (plugin_complain)
		popup_an_error("Plugin %s timed out", s);
	else {
		plugin_start_failed = True;
		(void) snprintf(plugin_start_error, PRB_MAX,
				"Plugin %s timed out", s);
	}

	plugin_timeout_id = 0L;
	no_plugin();
}

static void
plugin_start(char *command, char *argv[], Boolean complain)
{
	int inpipe[2];
	int outpipe[2];
	char *s;

	plugin_complain = complain;
	if (pipe(inpipe) < 0) {
		if (complain)
			popup_an_errno(errno, "pipe");
		else {
			(void) snprintf(plugin_start_error, PRB_MAX,
					"pipe: %s", strerror(errno));
			plugin_start_failed = True;
		}
		return;
	}
	if (pipe(outpipe) < 0) {
		if (complain)
			popup_an_errno(errno, "pipe");
		else {
			(void) snprintf(plugin_start_error, PRB_MAX,
					"pipe: %s", strerror(errno));
			plugin_start_failed = True;
		}
		close(inpipe[0]);
		close(inpipe[1]);
		return;
	}
	switch ((plugin_pid = fork())) {
	case -1:
		if (complain)
			popup_an_errno(errno, "fork");
		else {
			(void) snprintf(plugin_start_error, PRB_MAX,
					"fork: %s", strerror(errno));
			plugin_start_failed = True;
		}
		plugin_pid = 0;
		return;
	case 0: /* child */
		(void) dup2(outpipe[0], 0);
		close(outpipe[0]);
		close(outpipe[1]);
		(void) dup2(inpipe[1], 1);
		close(inpipe[1]);
		(void) dup2(1, 2);
		close(inpipe[0]);
		if (execvp(command, argv) < 0) {
			char *buf = xs_buffer("%s: %s\n", command,
					strerror(errno));

			write(2, buf, strlen(buf));
			exit(1);
		}
		break;
	default: /* parent */
		break;
	}

	/* Parent. */
	plugin_inpipe = inpipe[0];
	(void) fcntl(plugin_inpipe, F_SETFD, FD_CLOEXEC);
	close(inpipe[1]);
	plugin_outpipe = outpipe[1];
	(void) fcntl(plugin_outpipe, F_SETFD, FD_CLOEXEC);
	close(outpipe[0]);
	prb_cnt = 0;

	/* Tell the plug-in what we're about. */
	s = xs_buffer("x3270 host %s\n", current_host);
	(void) write(plugin_outpipe, s, strlen(s));
	Free(s);

	/* Wait for the 'ok' from the child. */
	ptq_first = ptq_last = 0;
	plugin_tq[ptq_last] = PLUGIN_INIT;
	ptq_last = (ptq_last + 1) % PLUGIN_QMAX;
	plugin_input_id = AddInput(plugin_inpipe, plugin_input);
	plugin_timeout_id = AddTimeOut(PLUGINSTART_SECS * 1000,
			plugin_timeout);
	plugin_started = False;
}

void
Plugin_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	if (*num_params == 0) {
		popup_an_error("%s: Requires 1 or more arguments",
				action_name(Plugin_action));
		return;
	}

	if (!strcasecmp(params[0], "Start")) {
		char *args[MAX_START_ARGS];
		int i;

		if (*num_params < 2 && !appres.plugin_command) {
			popup_an_error("%s Start: Command name required",
					action_name(Plugin_action));
			return;
		}

		if (plugin_pid) {
			popup_an_error("%s: Already running",
					action_name(Plugin_action));
			return;
		}

		if (!CONNECTED) {
			popup_an_error("%s: Not connected",
					action_name(Plugin_action));
			return;
		}

		if (*num_params >= 2) {
			for (i = 1; i < *num_params && i < MAX_START_ARGS; i++)
				args[i - 1] = params[i];
			args[i - 1] = NULL;
			plugin_start(params[1], args, True);
		} else {
			plugin_start_appres(True);
		}

	} else if (!strcasecmp(params[0], "Stop")) {
		if (*num_params != 1) {
			popup_an_error("%s Stop: Extra argument(s)",
					action_name(Plugin_action));
			return;
		}
		if (plugin_pid) {
			close(plugin_outpipe);
			plugin_outpipe = -1;
			close(plugin_inpipe);
			plugin_inpipe = -1;
			plugin_pid = 0;
		}
	} else if (!strcasecmp(params[0], "Command")) {
		int i;
		int s;

		if (*num_params < 2) {
			popup_an_error("%s Command: additional argument(s) "
					"required",
					action_name(Plugin_action));
			return;
		}
		if (!plugin_pid) {
			if (plugin_start_failed) {
				popup_an_error("%s Command: Start failed:\n%s",
						action_name(Plugin_action),
						plugin_start_error);
				plugin_start_failed = False;
			} else {
				popup_an_error("%s Command: Not running",
						action_name(Plugin_action));
			}
			return;
		}
		if (PLUGIN_BACKLOG >= PLUGIN_QMAX) {
			popup_an_error("%s Command: Plugin queue overflow",
					action_name(Plugin_action));
			return;
		}
		if (write(plugin_outpipe, "command ", 8) < 0) {
			popup_an_errno(errno, "write");
			no_plugin();
			return;
		}
		for (i = 1; i < *num_params; i++) {
			if (i > 1) {
				if (write(plugin_outpipe, " ", 1) < 0) {
					popup_an_errno(errno, "write");
					no_plugin();
					return;
				}
			}
			if (write(plugin_outpipe, params[i],
				     strlen(params[i])) < 0) {
				popup_an_errno(errno, "write");
				no_plugin();
				return;
			}
		}
		if (write(plugin_outpipe, "\n", 1) < 0) {
			popup_an_errno(errno, "write");
			no_plugin();
			return;
		}
		do_read_buffer(NULL, 0, ea_buf, plugin_outpipe);
		plugin_tq[ptq_last] = PLUGIN_CMD;
		ptq_last = (ptq_last + 1) % PLUGIN_QMAX;
		if (plugin_timeout_id != 0L)
			RemoveTimeOut(plugin_timeout_id);
		if (plugin_input_id == 0L)
			plugin_input_id = AddInput(plugin_inpipe,
					plugin_input);
		s = PLUGIN_BACKLOG * PLUGINWAIT_SECS;
		if (!plugin_started && s < PLUGINSTART_SECS)
			s = PLUGINSTART_SECS;
		plugin_timeout_id = AddTimeOut(s * 1000, plugin_timeout);
	} else {
		popup_an_error("%s: First argument must be Start, Stop or "
				"Command", action_name(Plugin_action));
		return;
	}
}

/* Send an AID event to the plugin process. */
void
plugin_aid(unsigned char aid)
{
	char buf[64];
	int s;

	/* No plugin, nothing to do. */
	if (!plugin_pid)
		return;

	/* Make sure we don't overrun the response queue. */
	if (PLUGIN_BACKLOG >= PLUGIN_QMAX) {
		trace_dsn("Plugin queue overflow\n");
		return;
	}

	/* Write the AID and cursor position. */
	snprintf(buf, sizeof(buf), "aid %s\n", see_aid(aid));
	write(plugin_outpipe, buf, strlen(buf));

	/* Write the screen buffer. */
	do_read_buffer(NULL, 0, ea_buf, plugin_outpipe);

	/*
	 * Wait for the response.
	 * If we were already waiting for a response, wait a bit longer.
	 */
	plugin_tq[ptq_last] = PLUGIN_AID;
	ptq_last = (ptq_last + 1) % PLUGIN_QMAX;
	if (plugin_timeout_id != 0L)
		RemoveTimeOut(plugin_timeout_id);
	if (plugin_input_id == 0L)
		plugin_input_id = AddInput(plugin_inpipe, plugin_input);
	s = PLUGIN_BACKLOG * PLUGINWAIT_SECS;
	if (!plugin_started && s < PLUGINSTART_SECS)
		s = PLUGINSTART_SECS;
	plugin_timeout_id = AddTimeOut(s * 1000, plugin_timeout);
}

#else /*][*/
void
Plugin_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
    /* Do nothing. */
}
#endif /*]*/

#if defined(X3270_DISPLAY) || defined(C3270) /*[*/
/*
 * Bell action, used by scripts to ring the console bell and enter a comment
 * into the trace log.
 */
void
Bell_action(Widget w _is_unused, XEvent *event _is_unused, String *params,
    Cardinal *num_params)
{
	ring_bell();
}
#endif /*]*/

#endif /*]*/

void
Source_action(Widget w _is_unused, XEvent *event, String *params,
    Cardinal *num_params)
{
    	int fd;

	action_debug(Source_action, event, params, num_params);
	if (check_usage(Source_action, *num_params, 1, 1) < 0)
		return;
	fd = open(params[0], O_RDONLY);
	if (fd < 0) {
	    	popup_an_errno(errno, "%s", params[0]);
		return;
	}
	push_file(fd);
}
