/*
 * Copyright (c) 1993-2024 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.
 */

/*
 *      task.c
 *              This module handles macro and script processing
 */

#include "globals.h"

#if !defined(_WIN32) /*[*/
#include <sys/wait.h>
#include <signal.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#else /*][*/
#include "wincmn.h"
#endif /*]*/
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <assert.h>

#include "3270ds.h"
#include "appres.h"
#include "ctlr.h"
#include "resources.h"
#include "toggles.h"

#include "actions.h"
#include "base64.h"
#include "bind-opt.h"
#include "child.h"
#include "childscript.h"
#include "copyright.h"
#include "ctlrc.h"
#include "unicodec.h"
#include "ft.h"
#include "host.h"
#include "idle.h"
#include "kybd.h"
#include "menubar.h"
#include "names.h"
#include "nvt.h"
#include "opts.h"
#include "peerscript.h"
#include "popups.h"
#include "pr3287_session.h"
#include "product.h"
#include "s3270_proto.h"
#include "screen.h"
#include "source.h"
#include "split_host.h"
#include "stdinscript.h"
#include "stringscript.h"
#include "task.h"
#include "telnet.h"
#include "toupper.h"
#include "trace.h"
#include "txa.h"
#include "utf8.h"
#include "utils.h"
#include "varbuf.h"
#include "vstatus.h"
#include "xio.h"

#include "w3misc.h"

#define NVT_SAVE_SIZE	4096

/* Maximum size of a macro. */
#define MSC_BUF	1024

/* IA test for UTF-8 overrides. */
#define IA_UTF8(ia)	((ia) == IA_HTTPD)

/* Base and variable wait times for a wrong cookie, in ms. */
#define WRONG_COOKIE_BASE	1000
#define WRONG_COOKIE_VAR	1000

/* Globals */
struct macro_def *macro_defs = NULL;
char *security_cookie;

/* Statics */

typedef struct task {
    /* Common fields. */
    struct task *next;		/**< next task on the stack */
    struct _taskq *taskq;	/**< root of this stack */
    enum task_type {
	ST_MACRO,		/**< macro */
	ST_CB,			/**< meta (script, string, httpd, login macro, idle) */
#define NUM_ST (ST_CB + 1)
    } type;
    enum task_state {
	TS_IDLE,		/**< no command active */
	TS_RUNNING,		/**< command executing */
	TS_NEED_RUN,	/* need run callback */
	/* --- all states after this are blocked --- */
#define MIN_WAITING_STATE TS_KBWAIT
	TS_KBWAIT,	/* command awaiting keyboard unlock */
	TS_CONNECT_WAIT,/* command awaiting connection to complete */
	TS_TIME_WAIT,   /* command awaiting simple timeout */
	TS_WAIT_NVT,	/* awaiting completion of Wait(NVTMode) */
	TS_WAIT_3270,	/* awaiting completion of Wait(3270Mode) */
	TS_WAIT_OUTPUT,	/* awaiting completion of Wait(Output) */
	TS_SWAIT_OUTPUT,/* awaiting completion of Snap(Wait) */
	TS_WAIT_DISC,	/* awaiting completion of Wait(Disconnect) */
	TS_WAIT_IFIELD,	/* awaiting completion of Wait(InputField) */
	TS_WAIT_UNLOCK,	/* awaiting completion of Wait(Unlock) */
	TS_EXPECTING,	/* awaiting completion of Expect() */
	TS_PASSTHRU,	/* awaiting completion of a pass-through action */
	TS_XWAIT,	/* extended wait */
	TS_WAIT_CURSOR_AT, /* awaiting cursor at a specific location */
	TS_WAIT_STRING_AT, /* awaiting string string a specific location */
	TS_WAIT_IFIELD_AT, /* awaiting an input field at a specific location */
    } state;
    bool success;
    bool accumulated;	/* accumulated time flag */
    bool is_ui;		/* generated by the user interface */
    struct timeval t0;	/* start time */
    unsigned long child_msec;	/* child time */
    ioid_t expect_id;	/* timeout ID for Expect() */
    ioid_t wait_id;	/* timeout ID for Wait() */
    int passthru_index;	/* UI passthru command index */
    int depth;		/* depth on stack */
    bool fatal;		/* tear everything down after completion */
    bool is_ft;		/* is waiting for a file transfer */

    void *wait_context;	/* opaque context for waiting */
    xcontinue_fn *xcontinue_fn; /* continue function */

    struct {
	int baddr;	/* location for wait operations */
	char *string;	/* string to wait for */
	bool force_utf8;/* true if string is UTF-8 */
    } match;

    /* Expect() fields. */
    struct {
	char   *text;	/* text to match */
	size_t	len;	/* length of match */
    } expect;

    /* Macro fields. */
    struct {
	char   *msc;	/* if string based, input buffer */
	const char *dptr; /*                data pointer */
	cmd_t **cmds;	/* if split, commands to run, null terminated */
	cmd_t **cmd_next; /*          next command to run */
#	define LAST_BUF 64
	char	last[LAST_BUF]; /* last command */
    } macro;

    /* cb fields. */
    struct task_cbx {	/* ST_CB context: */
	const tcb_t *cb;	/*  callback block */
	task_cbh handle;	/*  handle */
    } cbx;

} task_t;
static task_t *current_task = NULL;	/* the current task */
static int passthru_index = 0;
static peer_listen_t global_peer_listen = NULL;

/* List of active task stacks. */
typedef struct _taskq {
    llist_t llist;	/* linkage */
    char *name;		/* simple name */
    char *unique_name;	/* unique name */
    const tcb_t *cb;	/* callback block */
    task_t *top;	/* top of stack */
    int depth;		/* depth of the stack, for debug display */
    unsigned short index;	/* index, for debug display */
    bool deleted;	/* delete flag */
    bool output_wait_needed; /* should Wait(Output) block? */
} taskq_t;
static llist_t taskq = LLIST_INIT(taskq);
static unsigned short taskq_index = 1;

ioid_t async_fail_id = NULL_IOID;
static char *async_fail_msg;

typedef struct _owait {
    struct _owait *next;	/* linkage */
    const tcb_t *cb;		/* callback block */
} owait_t;
static owait_t *owait_list = NULL;

static const char *task_state_name[] = {
    "IDLE",
    "RUNNING",
    "NEED_RUN",
    "KBWAIT",
    "CONNECT_WAIT",
    "TIME_WAIT",
    "WAIT_NVT",
    "WAIT_3270",
    "WAIT_OUTPUT",
    "SWAIT_OUTPUT",
    "WAIT_DISC",
    "WAIT_IFIELD",
    "WAIT_UNLOCK",
    "EXPECTING",
    "PASSTHRU",
    "XWAIT",
    "WAIT_CURSOR_AT",
    "WAIT_STRING_AT",
    "WAIT_IFIELD_AT",
};

static struct macro_def *macro_last = (struct macro_def *) NULL;
static unsigned char *nvt_save_buf;
static size_t   nvt_save_cnt = 0;
static int      nvt_save_ix = 0;
static const char *st_name[NUM_ST] = {
    "Macro",		/* MACRO */
    "Callback"		/* CB */
};
static const char *stsname(task_t *s);
#define TASK_NAME_FMT	"%s[#%u.%d]"
#define TASK_sNAME(s)	stsname(s), (s)->taskq->index, (s)->depth
#define TASK_NAME	TASK_sNAME(current_task)

#if !defined(_WIN32) /*[*/
static void cleanup_socket(bool b);
#endif /*]*/
static void call_run(task_t *s);
static void task_done(bool success);
static void task_pop(void);
static void wait_timed_out(ioid_t id);
static task_t *task_redirect_to(void);
static bool expect_matches(task_t *task);

/* Macro that defines that the keyboard is locked due to user input. */
#define KBWAIT_MASK	(KL_OIA_LOCKED|KL_OIA_TWAIT|KL_DEFERRED_UNLOCK|KL_ENTER_INHIBIT|KL_AWAITING_FIRST|KL_FT|KL_BID)
#define _KBWAIT(k)	((k) & KBWAIT_MASK)
#define KBWAIT	_KBWAIT(kybdlock)
#define _CKBWAIT(k)	(toggled(AID_WAIT) && _KBWAIT(k))
#define CKBWAIT	_CKBWAIT(kybdlock)

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

/* An input request. */
typedef struct {
    llist_t llist;		/* linkage */
    continue_fn *continue_fn;	/* continue function */
    abort_fn *abort_fn;		/* abort function */
    void *handle;		/* to pass to continue function */
} input_request_t;
static llist_t input_requestq = LLIST_INIT(input_requestq);

/* Per-type input request state, kept per (interactive) callback. */
typedef struct {
    llist_t llist;		/* linkage */
    const char *name;		/* name */
    void *state;		/* state */
    ir_state_abort_cb abort;	/* abort callback */
} ir_state_t;

/* Wait() keywords. */
static struct {
    const char *keyword;
    int min_args;
    int max_args;
    enum task_state next_state;
} wait_keywords[] = {
    { Kw3270Mode,      0, 0, TS_WAIT_3270 },
    { Kw3270,          0, 0, TS_WAIT_3270 },
    { KwNvtMode,       0, 0, TS_WAIT_NVT },
    { KwAnsi,          0, 0, TS_WAIT_NVT },
    { KwDisconnect,    0, 0, TS_WAIT_DISC },
    { KwInputField,    0, 0, TS_WAIT_IFIELD },
    { KwOutput,        0, 0, TS_WAIT_OUTPUT },
    { KwUnlock,        0, 0, TS_WAIT_UNLOCK },
    { KwSeconds,       0, 0, TS_TIME_WAIT },
    { KwCursorAt,      1, 2, TS_WAIT_CURSOR_AT },
    { KwStringAt,      2, 3, TS_WAIT_STRING_AT },
    { KwInputFieldAt,  1, 2, TS_WAIT_IFIELD_AT },
    { NULL, 0, 0 }
};

static action_t Abort_action;
static action_t Ascii_action;
static action_t Ascii1_action;
static action_t AsciiField_action;
static action_t CloseScript_action;
static action_t Ebcdic_action;
static action_t Ebcdic1_action;
static action_t EbcdicField_action;
static action_t Execute_action;
static action_t Expect_action;
static action_t KeyboardDisable_action;
static action_t Macro_action;
static action_t NvtText_action;
static action_t Pause_action;
static action_t ReadBuffer_action;
static action_t Snap_action;
static action_t Wait_action;
static action_t Capabilities_action;
static action_t Cookie_action;
static action_t ResumeInput_action;
static action_t RequestInput_action;

static action_t Bell_action;
static action_t Printer_action;

static unsigned char calc_cs(unsigned char cs);

/**
 * Expand the name of a task.
 *
 * @param[in] s		Task
 *
 * @return Its name
 */
static const char *
stsname(task_t *s)
{
    if (s->type == ST_CB) {
	return txAsprintf("CB(%s)", s->cbx.cb->shortname);
    } else {
	return st_name[(int)s->type];
    }
}

static void
trace_task_output(task_t *s, const char *fmt, ...)
{
    va_list args;
    char *msgbuf;
    char *st;
    char *m;
    char c;

    if (!toggled(TRACING)) {
	return;
    }

    va_start(args, fmt);
    msgbuf = Vasprintf(fmt, args);
    va_end(args);

    m = msgbuf;
    st = msgbuf;
    while ((c = *st++)) {
	if (c == '\n') {
	    vtrace("Output for " TASK_NAME_FMT ": '%.*s'\n", TASK_sNAME(s),
		    (int)((st - 1) - m), m);
	    m = st;
	    continue;
	}
    }
    Free(msgbuf);
}

/* Parse the macros resource into the macro list */
void
macros_init(void)
{
    char *s = NULL;
    char *name, *action;
    struct macro_def *m;
    int ns;
    int ix = 1;
    static char *last_macros_resource = NULL;
    const char *macros_resource = NULL;

    /* 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);
	if (s != NULL) {
	    macros_resource = txAsprintf("%s.%s", ResMacros, rname);
	}
	Free(rname);
    }
    if (s == NULL) {
	if (appres.macros == NULL) {
	    return;
	}
	s = NewString(appres.macros);
	macros_resource = ResMacros;
    } else {
	s = NewString(s);
    }

    /* See if this is a repeat call. */
    if (last_macros_resource != NULL &&
	    !strcmp(last_macros_resource, macros_resource)) {
	Free(s);
	return;
    }
    Replace(last_macros_resource, NewString(macros_resource));

    /* Free the previous macro definitions. */
    while (macro_defs) {
	m = macro_defs->next;
	Free(macro_defs);
	macro_defs = m;
    }
    macro_defs = NULL;

    while ((ns = split_dresource(&s, &name, &action)) == 1) {
	char *action_error;

	m = (struct macro_def *)Malloc(sizeof(*m));
	if (!split_hier(name, &m->name, &m->parents)) {
	    Free(m);
	    continue;
	}
	if (!validate_command(action, (int)(action - name), &action_error)) {
	    popup_an_error("Error in %s line %d:\n%s", macros_resource, ix,
		    action_error);
	    Free(action_error);
	    ns = 0;
	    break;
	}
	m->action = action;
	if (macro_last) {
	    macro_last->next = m;
	} else {
	    macro_defs = m;
	}
	m->next = NULL;
	macro_last = m;
	ix++;
    }
    if (ns < 0) {
	popup_an_error("Format error in %s line %d", macros_resource, ix);
    }
}

/**
 * Upcall for toggling the script listener on and off.
 *
 * @param[in] name	Name of toggle
 * @param[in] value	Toggle value
 * @param[in] flags	Set() flags
 * @param[in] ia	Cause
 *
 * @returns toggle_upcall_ret_t
 */
static toggle_upcall_ret_t
scriptport_toggle_upcall(const char *name, const char *value, unsigned flags, ia_t ia)
{
    struct sockaddr *sa;
    socklen_t sa_len;

    if (global_peer_listen != NULL) {
	peer_shutdown(global_peer_listen);
	global_peer_listen = NULL;
    }
    if (value == NULL || !*value) {
	Replace(appres.script_port, NULL);
	return TU_SUCCESS;
    }

    if (!parse_bind_opt(value, &sa, &sa_len)) {
	popup_an_error("Invalid %s: %s", name, value);
	return TU_FAILURE;
    }
    Replace(appres.script_port, NewString(canonical_bind_opt(sa)));
    global_peer_listen = peer_init(sa, sa_len, PLM_MULTI);
    Free(sa);
    return TU_SUCCESS;
}

static bool
Info_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnInfo, ia, argc, argv);
    if (check_argc(AnInfo, argc, 1, 1) < 0) {
	return false;
    }
    popup_an_info("%s", argv[0]);
    return true;
}

static bool
ignore_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(Anignore, ia, argc, argv);
    return true;
}

/* Concatenate a set of arguments together. */
static char *
argcat(unsigned argc, const char **argv)
{
    varbuf_t r;
    char *sep = "";
    unsigned i;

    vb_init(&r);
    for (i = 0; i < argc; i++) {
	vb_appendf(&r, "%s%s", sep, argv[i]);
	sep = " ";
    }
    return vb_consume(&r);
}

static bool
Echo_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnEcho, ia, argc, argv);
    if (argc == 0) {
	action_output(" ");
    } else {
	char *c = argcat(argc, argv);

	action_output("%s", c);
	Free(c);
    }
    return true;
}

/* Timeout function for async failures. */
static void
async_fail(ioid_t id)
{
    char *msg = async_fail_msg;

    async_fail_msg = NULL;
    async_fail_id = NULL_IOID;
    popup_an_error("%s", msg);
    Free(msg);
}

static bool
Fail_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnFail, ia, argc, argv);
    if (argc == 0) {
	popup_an_error("Failed");
    } else {
	if (!strcasecmp(argv[0], KwDashAsync)) {
	    if (async_fail_id != NULL_IOID) {
		popup_an_error(AnFail "(): async already pending");
		return false;
	    }
	    if (argc > 1) {
		async_fail_msg = argcat(argc - 1, argv +1);
	    } else {
		async_fail_msg = NewString("Failed");
	    }
	    async_fail_id = AddTimeOut(0, async_fail);
	    return true;
	} else {
	    char *c = argcat(argc, argv);

	    popup_an_error("%s", c);
	    Free(c);
	}
    }
    return false;
}

/**
 * Task module registration.
 */
void
task_register(void)
{
    static action_table_t task_actions[] = {
	{ AnAbort,		Abort_action, ACTION_KE },
	{ AnAnsiText,		NvtText_action, 0 },
	{ AnAscii,		Ascii_action, 0 },
	{ AnAscii1,		Ascii1_action, 0 },
	{ AnAsciiField,		AsciiField_action, 0 },
	{ AnBell,		Bell_action, 0 },
	{ AnCapabilities,	Capabilities_action, ACTION_HIDDEN },
	{ AnCloseScript,	CloseScript_action, 0 },
	{ AnCookie,		Cookie_action, 0 },
	{ AnEbcdic,		Ebcdic_action, 0 },
	{ AnEbcdic1,		Ebcdic1_action, 0 },
	{ AnEbcdicField,	EbcdicField_action, 0 },
	{ AnEcho,		Echo_action, 0 },
	{ AnExecute,		Execute_action, ACTION_KE },
	{ AnExpect,		Expect_action, 0 },
	{ AnFail,		Fail_action, 0 },
	{ Anignore,		ignore_action, ACTION_KE },
	{ AnInfo,		Info_action, 0 },
	{ AnKeyboardDisable,	KeyboardDisable_action, 0 },
	{ AnMacro,		Macro_action, ACTION_KE },
	{ AnNvtText,		NvtText_action, 0 },
	{ AnPause,		Pause_action, 0 },
	{ AnPrompt,		Prompt_action, 0 },
	{ AnReadBuffer,		ReadBuffer_action, 0 },
	{ RESUME_INPUT,		ResumeInput_action, ACTION_HIDDEN },
	{ AnRequestInput,	RequestInput_action, ACTION_HIDDEN },
	{ AnScript,		Script_action, ACTION_KE },
	{ AnSnap,		Snap_action, 0 },
	{ AnSource,		Source_action, ACTION_KE },
	{ AnWait,		Wait_action, ACTION_KE }
    };
    static action_table_t task_dactions[] = {
	{ AnPrinter,		Printer_action, ACTION_KE },
    };
    static toggle_register_t toggles[] = {
	{ AID_WAIT,	NULL,	0 }
    };
    static xres_t task_xresources[] = {
	{ ResMacros,		V_WILD },
    };

    /* Register actions.*/
    register_actions(task_actions, array_count(task_actions));
    if (product_has_display()) {
	register_actions(task_dactions, array_count(task_dactions));
    }

    /* Register toggles. */
    register_toggles(toggles, array_count(toggles));

    /* Register extended toggle. */
    register_extended_toggle(ResScriptPort, scriptport_toggle_upcall, NULL,
	   canonical_bind_opt_res, (void **)&appres.script_port, XRM_STRING);

    /* Register resources. */
    register_xresources(task_xresources, array_count(task_xresources));

    /* This doesn't go here, but it needs to happen once. */
    nvt_save_buf = (unsigned char *)Malloc(NVT_SAVE_SIZE);
}

/**
 * Set the state of a task.
 *
 * @param[in,out] s	task to change
 * @param[in] state	new state
 * @param[in] why	reason for change
 */
static void
task_set_state(task_t *s, enum task_state state, const char *why)
{
    if (s->state != state) {
	vtrace(TASK_NAME_FMT " %s -> %s (%s)\n", TASK_sNAME(s),
		task_state_name[s->state],
		task_state_name[state],
		why);
	s->state = state;
    }
}

/**
 * Set match parameters.
 *
 * @param[in] s		task to modify
 * @param[in] baddr	buffer address, or -1
 * @param[in] string	string to match, or NULL
 * @param[in] force_utf8 true if string is encoded in UTF-8
 */
static void
task_set_match(task_t *s, int baddr, const char *string, bool force_utf8)
{
    vtrace(TASK_NAME_FMT " wait @%d%s\n", TASK_sNAME(s),
	    baddr, (string != NULL)? txAsprintf(" '%s'", string): "");
    s->match.baddr = baddr;
    s->match.string = NewString(string);
    s->match.force_utf8 = force_utf8;
}

/* Allocate a new task. */
static task_t *
new_task(enum task_type type, taskq_t *q)
{
    task_t *s;

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

    s->taskq = q;
    s->next = q->top;
    s->depth = ++q->depth;
    s->type = type;
    s->state = TS_IDLE;
    s->success = true;
    s->expect_id = NULL_IOID;
    s->wait_id = NULL_IOID;
    gettimeofday(&s->t0, NULL);
    s->child_msec = 0L;
    s->fatal = false;
    s->is_ft = false;
    s->match.baddr = -1;
    s->match.string = NULL;
    s->match.force_utf8 = false;

    return s;
}

/*
 * Set the menubar and status line to indicate a script running.
 */
static void
task_status_set(void)
{
    taskq_t *q;
    bool any = false;

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;

	for (s = q->top; s != NULL; s = s->next) {
	    if (!s->is_ui) {
		any = true;
		break;
	    }
	}
	if (any) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    menubar_as_set(any);
    vstatus_script(any);
}

/*
 * Push a task onto a particular task stack.
 * Returns the task.
 */
static task_t *
task_push_onto(taskq_t *q, enum task_type type, bool is_ui)
{
    task_t *s;

    s = new_task(type, q);
    s->is_ui = is_ui;
    q->top = s;
    if (current_task != NULL && q == current_task->taskq) {
	current_task = s;
    }

    /* Enable the abort button on the menu and the status indication. */
    task_status_set();

    return s;
}

/**
 * Free a task structure.
 *
 * @param[in] t		Task
 */
static void
free_task(task_t *t)
{
    /* Cancel any pending timeouts. */
    if (t->expect_id != NULL_IOID) {
	RemoveTimeOut(t->expect_id);
    }
    if (t->wait_id != NULL_IOID) {
	RemoveTimeOut(t->wait_id);
    }

    /* Free auxiliary buffers. */
    Replace(t->macro.msc, NULL);
    Replace(t->expect.text, NULL);
    if (t->macro.cmds != NULL) {
	int i, j;
	cmd_t *c;

	for (i = 0; (c = t->macro.cmds[i]) != NULL; i++) {
	    Free((char *)c->action);
	    c->action = NULL;
	    for (j = 0; c->args[j] != NULL; j++) {
		Free((char *)c->args[j]);
	    }
	    Free((void *)c->args);
	    Free(c);
	}
	Replace(t->macro.cmds, NULL);
	t->macro.cmd_next = NULL;
    }
    Replace(t->match.string, NULL);
    
    /* Free the structure. */
    Free(t);
}

/* Pop a task off the stack. */
static void
task_pop(void)
{
    task_t *s;
    unsigned long msec;
    struct timeval t1;

    vtrace(TASK_NAME_FMT " complete, %s\n", TASK_NAME,
	    current_task->success? "success": "failure");

    /*
     * If this is a callback or macro, propagate the state.
     * We also propagate status up to plain macros, so a failed script,
     * e.g., will cause the macro to fail.
     */
    if (current_task->next != NULL) {
	current_task->next->success = current_task->success;
    }

    s = current_task;

    /* Accumulate time. */
    gettimeofday(&t1, NULL);
    msec = (t1.tv_sec - s->t0.tv_sec) * 1000 +
	   (t1.tv_usec - s->t0.tv_usec + 500) / 1000;
    if (s->next) {
	s->next->child_msec = msec;
    }

    /* Dequeue. */
    if (s->next == NULL) {
	taskq_t *q = current_task->taskq;

	assert(q != NULL);
	vtrace("CB(%s)[#%u] complete\n", q->name, q->index);

	/* Do not delete the taskq yet -- someone might be walking it. */
	q->top = NULL;
	q->deleted = true;
	q->depth = 0;
	current_task = NULL;
    } else {
	s->taskq->top = current_task = s->next;
	s->taskq->depth--;
    }

    /* Release the memory. */
    free_task(s);

    /* Re-evaluate the OIA and menus. */
    task_status_set();

    /*
     * In the old code, there was a test here that if the new top task is
     * not blocked and the keyboard is now locked, block it now. That won't
     * work, because the global keyboard lock may be unrelated to this context.
     * But is it still necessary in some cases?
     */

    if (current_task != NULL &&
	    current_task->state == TS_IDLE &&
	    current_task->type == ST_CB &&
	    (current_task->cbx.cb->flags & CB_NEEDS_RUN)) {
	/* The parent needs to be informed. */
	task_set_state(current_task, TS_RUNNING, "child popped");
    }
}

/*
 * 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)
{
    /* TODO: When this function returns early, it should check for the cookie resource expecting stdin/stdout. */
    if (appres.script_port) {
	struct sockaddr *sa;
	socklen_t sa_len;

	if (!parse_bind_opt(appres.script_port, &sa, &sa_len)) {
	    popup_an_error("Invalid script port value '%s', "
		    "ignoring", appres.script_port);
	    return;
	}
#if !defined(_WIN32) /*[*/
	if (appres.socket) {
	    xs_warning("-scriptport overrides -socket");
	}
#endif /*]*/

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

	/* Do the actual initialization. */
	global_peer_listen = peer_init(sa, sa_len,
		appres.script_port_once? PLM_ONCE: PLM_MULTI);
	Free(sa);

	return;
    }

#if !defined(_WIN32) /*[*/
    if (appres.socket && !appres.script_port) {
	struct sockaddr_un *ssun;

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

	/* Create the listening socket. */
	ssun = (struct sockaddr_un *)Malloc(sizeof(struct sockaddr_un));
	memset(ssun, '\0', sizeof(*ssun));
	ssun->sun_family = AF_UNIX;
	snprintf(ssun->sun_path, sizeof(ssun->sun_path),
		"/tmp/x3sck.%u", (unsigned)getpid());
	unlink(ssun->sun_path);
	peer_init((struct sockaddr *)ssun, sizeof(*ssun), PLM_MULTI);
	Free(ssun);
	register_schange(ST_EXITING, cleanup_socket);
	return;
    }
#endif /*]*/

    if (appres.httpd_port) {
	appres.scripted = false;
    }

    if ((!appres.scripted || appres.scripting.callback != NULL)
	    && !appres.scripted_always) {
	return;
    }

    /* Set up to receive script commands from stdin. */
    stdin_init();
}

#if !defined(_WIN32) /*[*/
/* Clean up the Unix-domain socket. */
static void
cleanup_socket(bool b _is_unused)
{
    unlink(txAsprintf("/tmp/x3sck.%u", getpid()));
}
#endif /*]*/

/**
 * Look up an action.
 *
 * @param[in] action	Action name
 * @param[out] errorp	Returned error text
 *
 * @return action structure, or null
 */
static action_elt_t *
lookup_action(const char *action, char **errorp)
{
    action_elt_t *e;
    action_elt_t *any = NULL;
    action_elt_t *exact = NULL;

    /* Search the action list. */
    FOREACH_LLIST(&actions_list, e, action_elt_t *) {
	if (!strcasecmp(action, e->t.name)) {
	    exact = any = e;
	    break;
	}
    } FOREACH_LLIST_END(&actions_list, e, action_elt_t *);
    if (exact == NULL) {
	FOREACH_LLIST(&actions_list, e, action_elt_t *) {
	    if (!strncasecmp(action, e->t.name, strlen(action))) {
		if (any != NULL) {
		    *errorp = Asprintf("Ambiguous action name: %s", action);
		    return NULL;
		}
		any = e;
	    }
	} FOREACH_LLIST_END(&actions_list, e, action_elt_t *);
    }

    if (any == NULL) {
	*errorp = Asprintf("Unknown action: %s", action);
    }

    return any;
}

/**
 * Split a command into an action and arguments.
 *
 * @param[in] s		string to parse
 * @param[in] offset	offset into string for error message
 * @param[out] np	returned pointer to additional commands
 * @param[out] entryp	returned action entry
 * @param[out] argsp	returned arguments
 * @param[out] errorp	returned error text (if false returned)
 *
 * @returns true for success, false for failure
 */
static bool
parse_command(const char *s, int offset, const char **np,
	action_elt_t **entryp, char ***argsp, char **errorp)
{
#   define MAX_ANAME	64
    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_LPAREN_COMMA,/* saw left paren and comma */
	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_BSL2,	/* paren: after second 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_BSL2,	/* space: after second backslash in quoted parameter */
	ME_S_PARMx	/* space: saw whitespace after parameter */
    } state = ME_GND;
    char c;
    char aname[MAX_ANAME+1];
    int nx = 0;
    unsigned param_count = 0;	/* parameter count */
    unsigned vbcount = 0;	/* allocated parameter count */
    varbuf_t *r = NULL;		/* accumulated parameters */
    int failreason = 0;
    unsigned i;
    bool rc = false;	/* failure return code */
    const char *s_orig = s;
    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",
	/*6*/ "Syntax error: unclosed \""
    };
#define fail(n) { failreason = n; goto failure; }

    *np = NULL;
    *entryp = NULL;
    *argsp = NULL;
    *errorp = NULL;

    while ((c = *s++)) {

	if ((param_count + 1) > vbcount) {
	    /* Allocate a varbuf for the next parameter. */
	    r = (varbuf_t *)Realloc(r, (param_count + 1) * sizeof(varbuf_t));
	    vb_init(&r[param_count]);
	    vbcount = param_count + 1;
	}

	switch (state) {
	case ME_GND:
	    if (isspace((unsigned char)c)) {
		continue;
	    } else if (isalnum((unsigned char)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((unsigned char)c)) {
		aname[nx] = '\0';
		if (c == '(') {
		    nx = 0;
		    state = ME_LPAREN;
		} else {
		    state = ME_FUNCTIONx;
		}
	    } else if (isalnum((unsigned char)c) || c == '_' || c == '-') {
		if (nx < MAX_ANAME) {
		    aname[nx++] = c;
		}
	    } else {
		fail(2);
	    }
	    break;
	case ME_FUNCTIONx:	/* space after function name */
	    if (isspace((unsigned char)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;
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_LPAREN:
	case ME_LPAREN_COMMA:
	    if (isspace((unsigned char)c)) {
		continue;
	    } else if (c == '"') {
		state = ME_P_QPARM;
	    } else if (c == ',') {
		param_count++;
		state = ME_LPAREN_COMMA;
	    } else if (c == ')') {
		goto success;
	    } else {
		state = ME_P_PARM;
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_P_PARM:
	    if (isspace((unsigned char)c)) {
		param_count++;
		state = ME_P_PARMx;
	    } else if (c == ')') {
		param_count++;
		goto success;
	    } else if (c == ',') {
		param_count++;
		state = ME_LPAREN_COMMA;
	    } else {
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_P_BSL:
	    if (c != '"') {
		vb_append(&r[param_count], "\\", 1);
	    }
	    if (c == '\\') {
		state = ME_P_BSL2;
	    } else {
		vb_append(&r[param_count], &c, 1);
		state = ME_P_QPARM;
	    }
	    break;
	case ME_P_BSL2:
	    if (c == '"') {
		param_count++;
		state = ME_P_PARMx;
	    } else {
		vb_append(&r[param_count], "\\", 1);
		if (c != '\\') {
		    vb_append(&r[param_count], &c, 1);
		    state = ME_P_QPARM;
		}
	    }
	    break;
	case ME_P_QPARM:
	    if (c == '"') {
		param_count++;
		state = ME_P_PARMx;
	    } else if (c == '\\') {
		state = ME_P_BSL;
	    } else {
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_P_PARMx:
	    if (isspace((unsigned char)c)) {
		continue;
	    } else if (c == ',') {
		state = ME_LPAREN_COMMA;
	    } else if (c == ')') {
		goto success;
	    } else {
		fail(3);
	    }
	    break;
	case ME_S_PARM:
	    if (isspace((unsigned char)c)) {
		param_count++;
		state = ME_S_PARMx;
	    } else {
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_S_BSL:
	    if (c != '"') {
		vb_append(&r[param_count], "\\", 1);
	    }
	    if (c == '\\') {
		state = ME_S_BSL2;
	    } else {
		vb_append(&r[param_count], &c, 1);
		state = ME_S_QPARM;
	    }
	    break;
	case ME_S_BSL2:
	    if (c == '"') {
		param_count++;
		state = ME_S_PARMx;
	    } else {
		vb_append(&r[param_count], "\\", 1);
		if (c != '\\') {
		    vb_append(&r[param_count], &c, 1);
		    state = ME_S_QPARM;
		}
	    }
	    break;
	case ME_S_QPARM:
	    if (c == '"') {
		param_count++;
		state = ME_S_PARMx;
	    } else if (c == '\\') {
		state = ME_S_BSL;
	    } else {
		vb_append(&r[param_count], &c, 1);
	    }
	    break;
	case ME_S_PARMx:
	    if (isspace((unsigned char)c)) {
		continue;
	    } else if (c == '"') {
		state = ME_S_QPARM;
	    } else {
		vb_append(&r[param_count], &c, 1);
		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;
	}
	rc = true;
	goto silent_failure;
    case ME_S_PARMx:	/* space after space-style parameter */
	break;
    case ME_S_PARM:	/* mid space-style parameter */
	param_count++;
	break;
    case ME_S_QPARM:	/* inside quoted parameter */
    case ME_P_QPARM:
    case ME_S_BSL:	/* backslash inside quoted parameter */
    case ME_P_BSL:
    case ME_S_BSL2:	/* second backslash inside quoted parameter */
    case ME_P_BSL2:
	fail(6);
    default:
	fail(5);
    }

success:
    if (state == ME_LPAREN_COMMA) {
	param_count++;
    }

    if (c) {
	/* Skip trailing white space. */
	while (*s && isspace((unsigned char)*s)) {
	    s++;
	}
	if (*s) {
	    if (np) {
		/* Something follows. */
		*np = s;
	    } else {
		fail(4);
	    }
	} else if (np) {
	    /* Nothing follows the whitespace. */
	    *np = s;
	}
    } else if (np) {
	/* Nothing follows. */
	*np = s-1;
    }

    /* Look up the action. */
    *entryp = lookup_action(aname, errorp);
    if (*entryp == NULL) {
	goto silent_failure;
    }

    /* Return the arguments. */
    *argsp = (char **)Malloc((param_count + 1) * sizeof(const char *));
    for (i = 0; i < param_count; i++) {
	(*argsp)[i] = vb_consume(&r[i]);
    }
    (*argsp)[i] = NULL;
    Free(r);

    return true;

failure:
    *errorp = Asprintf("%s at column %d", fail_text[failreason-1],
	    (int)(s - s_orig) + offset);
silent_failure:
    if (vbcount) {
	for (i = 0; i < vbcount; i++) {
	    vb_free(&r[i]);
	}
	Free(r);
    }
    return rc;

#undef fail
}

/**
 * Interpret and execute a script or macro command.
 *
 * @param[in] cause	Origin of action
 * @param[in] entry	Action to execute
 * @param[in] args	Arguments
 * @param[out] last	Returned action and paramters, canonicalized
 * @param[in] last_len	Length of the last
 * @param[in] cbx	Context
 *
 * @return success or failure
 */
static bool
execute_command_backend(enum iaction cause, action_elt_t *entry,
	const char **args, char *last, size_t last_len, struct task_cbx *cbx)
{
    bool stat = true;
    int i;
    varbuf_t r;
    char *s;

    /* Check for restrictions. */
    if (entry->t.ia_restrict != IA_NONE && cause != entry->t.ia_restrict) {
	popup_an_error("Action %s is invalid in this context",
		entry->t.name);
	return false;
    }

    /* Check for cookies. */
    if (security_cookie != NULL &&
	    cbx != NULL &&
	    (cbx->cb->flags & CB_NEEDCOOKIE) &&
	    cbx->cb->getxflags != NULL && !((cbx->cb->getxflags)(cbx->handle) & XF_HAVECOOKIE) &&
	    strcasecmp(entry->t.name, AnCookie)) {
	popup_an_error("Security cookie not supplied");
	return false;
    }

    /* Record the action. */
    vb_init(&r);
    vb_appendf(&r, "%s(", entry->t.name);
    for (i = 0; args[i] != NULL; i++) {
	vb_appendf(&r, "%s%s", i? ",": "", qscatv(args[i]));
    }
    vb_appends(&r, ")");
    strncpy(last, (s = vb_consume(&r)), last_len - 1);
    last[last_len - 1] = '\0';
    Free(s);

    /* Run the action. */
    for (i = 0; args[i] != NULL; i++) {
    }
    (void) run_action_entry(entry, cause, i, (const char **)args);

    /* Refresh the screen, in case the action changed it. */
    screen_disp(false);

    /* If it produced an error message, it failed. */
    if (!current_task->success) {
	stat = false;
    }

    /* Check for trace file rollover. */
    trace_rollover_check();

    return stat;
}

/**
 * Interpret and execute a script or macro command.
 *
 * @param[in] cause	Origin of action
 * @param[in] cmd	Action and arguments
 * @param[out] last	Returned action and paramters, canonicalized
 * @param[in] last_len	Length of the last
 * @param[in] cbx	Context
 *
 * @return success or failure
 */
static bool
execute_command_split(enum iaction cause, cmd_t *cmd, char *last,
	size_t last_len, struct task_cbx *cbx)
{
    action_elt_t *entry;
    char *error;

    entry = lookup_action(cmd->action, &error);
    if (entry == NULL) {
	popup_an_error("%s", error);
	Free(error);
	return false;
    }
    return execute_command_backend(cause, entry, cmd->args, last, last_len, cbx);
}

/**
 * Interpret and execute a script or macro command.
 *
 * @param[in] cause	Origin of action
 * @param[in] s		Buffer containing action and paramters
 * @param[out] np	Returned pointer to next action
 * @param[out] last	Returned action and paramters, canonicalized
 * @param[in] last_len	Length of the last
 * @param[in] cbx	Context
 *
 * @return success or failure
 */
static bool
execute_command(enum iaction cause, const char *s, const char **np, char *last,
	size_t last_len, struct task_cbx *cbx)
{
    bool stat;
    action_elt_t *entry;
    char **args;
    char *error;
    int i;

    /* Parse the command. */
    stat = parse_command(s, 0, np, &entry, &args, &error);
    if (!stat) {
	popup_an_error("%s", error);
	Free(error);
	return stat;
    }

    if (entry == NULL) {
	/* A comment. */
	return true;
    }

    /* Run it. */
    stat = execute_command_backend(cause, entry, (const char **)args, last,
	    last_len, cbx);

    /* Free the arguments. */
    for (i = 0; args[i] != NULL; i++) {
	Free(args[i]);
    }
    Replace(args, NULL);
    return stat;
}

/**
 * Validate that a macro contains valid syntax and defined actions.
 *
 * @param[in] command	Command to check
 * @param[in] offset	Offset for error message
 * @param[out] error	Returned error message
 * 
 * @return true for success, false for failure
 */
bool
validate_command(const char *command, int offset, char **error)
{
    action_elt_t *entry;
    const char *np;
    char **args = NULL;
    int i;

    np = command;
    while (*np) {
	if (!parse_command(np, (int)(np - command) + offset, &np, &entry,
		    &args, error)) {
	    return false;
	}
	for (i = 0; args[i] != NULL; i++) {
	    Free(args[i]);
	}
	Free(args);
    }
    return true;
}

/**
 * Tests a task for interactivity.
 *
 * @param[in] t		Task to inspect
 * @returns true if interactive.
 */
static bool
is_interactive(task_t *t)
{
    return t != NULL &&
	t->cbx.cb != NULL &&
	t->cbx.cb->getflags != NULL &&
	((*t->cbx.cb->getflags)(t->cbx.handle) & CBF_INTERACTIVE) != 0;
}

/**
 * Test a specific task for non-blocking Connect() / Open() / Transfer().
 *
 * @param[in] t		Task to inspect
 * @returns true if non-blocking.
 */
static bool is_nonblocking_connect(task_t *t)
{
    return t != NULL &&
	t->cbx.cb != NULL &&
	t->cbx.cb->getflags != NULL &&
	((*t->cbx.cb->getflags)(t->cbx.handle) & CBF_CONNECT_FT_NONBLOCK) != 0;
}

/* Run the macro at the top of the stack. */
static void
run_macro(void)
{
    task_t *s = current_task;
    const char *a = s->macro.dptr;
    const char *nextm = NULL;
    bool es;
    bool fatal = false;

    vtrace(TASK_NAME_FMT " running\n", TASK_NAME);

    /*
     * Keep executing commands off the line until one pauses or
     * we run out of commands.
     */
    while (((a != NULL && *a) ||
	    (s->macro.cmd_next != NULL && *s->macro.cmd_next != NULL)) &&
	   !fatal) {
	enum iaction ia;
	bool was_ckbwait = CKBWAIT;
	unsigned int old_kybdlock = kybdlock;
	struct task_cbx *cbx = NULL;

	/*
	 * Check for command failure.
	 */
	if (!s->success) {
	    vtrace(TASK_NAME_FMT " failed\n", TASK_NAME);

	    /* Propagate it. */
	    if (s->next != NULL) {
		s->next->success = false;
	    }
	    break;
	}

	task_set_state(s, TS_RUNNING, "executing");
	vtrace(TASK_NAME_FMT " '%s'\n", TASK_NAME,
		scatv((a != NULL)? a: (*s->macro.cmd_next)->action));
	s->success = true;

	if (s->type == ST_MACRO &&
		s->next != NULL &&
		s->next->type == ST_CB) {
	    ia = s->next->cbx.cb->ia;
	    cbx = &s->next->cbx;
	} else {
	    ia = IA_MACRO;
	}

	if (s->macro.cmd_next != NULL) {
	    es = execute_command_split(ia, *s->macro.cmd_next, s->macro.last,
		    LAST_BUF, cbx);
	    s->macro.cmd_next++;
	} else {
	    es = execute_command(ia, a, &nextm, s->macro.last, LAST_BUF, cbx);
	    s->macro.dptr = nextm;
	}

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

	/* Macro could not execute.  Abort it. */
	if (!es) {
	    vtrace(TASK_NAME_FMT " error\n", TASK_NAME);

	    /* Propagate it. */
	    s->success = false;
	    if (s->next != NULL) {
		s->next->success = false;
	    }

	    break;
	}

	/*
	 * Check for keyboard lock.
	 * Minor hack: If interactive (x3270> prompt), don't change the task
	 * state just for file transfers. This allows the Transfer() action to
	 * return immediately instead of waiting for the transfer to complete.
	 */
	if (!was_ckbwait &&
		CKBWAIT &&
		!(((old_kybdlock ^ kybdlock) & KL_FT) && is_nonblocking_connect(s->next))) {
	    task_set_state(s, TS_KBWAIT, "keyboard locked");
	    if ((old_kybdlock ^ kybdlock) & KL_FT) {
		vtrace(TASK_NAME_FMT " setting is_ft\n", TASK_sNAME(s));
		s->is_ft = true;
	    }
	}

	/* Macro paused, implicitly or explicitly.  Suspend it. */
	if (s->state >= (int)MIN_WAITING_STATE) {
	    s->macro.dptr = nextm;
	    return;
	}

	/* Macro ran. */
	a = nextm;

	fatal = s->fatal;
    }

    /* Finished with this macro. */
    task_pop();

    if (fatal) {
	current_task = NULL;
	abort_script();
    }
}

/* Push a macro (macro, command or keymap action) on the stack. */
static task_t *
push_xmacro_onto(taskq_t *q, enum task_type type, const char *st, size_t len,
	bool is_ui)
{
    task_t *s;

    s = task_push_onto(q, type, is_ui);
    s->macro.msc = Malloc(len + 1);
    memcpy(s->macro.msc, st, len);
    s->macro.msc[len] = '\0';
    s->macro.dptr = s->macro.msc;
    s->fatal = false;
    task_set_state(s, TS_RUNNING, "fresh push");
    return s;
}

/* Push a macro (macro, command or keymap action) on the stack. */
static task_t *
push_xmacro_onto_split(taskq_t *q, enum task_type type, cmd_t **cmds,
	bool is_ui)
{
    task_t *s;

    s = task_push_onto(q, type, is_ui);
    s->macro.cmds = cmds;
    s->macro.cmd_next = cmds;
    s->fatal = false;
    task_set_state(s, TS_RUNNING, "fresh push");
    return s;
}

/* Push a macro (macro, command or keymap action) on the stack. */
static void
push_xmacro(enum task_type type, const char *st, size_t len, bool is_ui)
{
    assert(current_task != NULL);
    current_task = push_xmacro_onto(current_task->taskq, type, st, len, is_ui);
}

/* Push a macro on the stack. */
void
push_stack_macro(char *s)
{
    push_xmacro(ST_MACRO, s, strlen(s), false);
}

/**
 * Find a callback block in the owait list.
 *
 * @param[in] cb	Callback block
 *
 * @return True if the block is in the list.
 */
static bool
find_owait(const tcb_t *cb)
{
    owait_t *o;

    for (o = owait_list; o != NULL; o = o->next) {
	if (o->cb == cb) {
	    return true;
	}
    }
    return false;
}

/**
 * Push a callback on the stack.
 * @param[in] buf	Macro to push, or NULL
 * @param[in] len	Length of macro
 * @param[in] cmds	Split-out commands, or NULL
 * @param[in] cb	Callback block
 * @param[in] handle	Callback handle
 *
 * @return Name of the new cb.
 */
static char *
push_cb_backend(const char *buf, size_t len, cmd_t **cmds, const tcb_t *cb,
	task_cbh handle)
{
    task_t *s;
    taskq_t *q = NULL;
    char *name = NULL;
    bool is_ui = (cb->flags & CB_UI) != 0;

    /* We performed some new action, so we're not idle. */
    reset_idle_timer();

    if (cb->flags & CB_NEW_TASKQ) {
	/* Allocate a new taskq. */
	q = (taskq_t *)Calloc(sizeof(taskq_t) + strlen(cb->shortname) + 1, 1);
	llist_init(&q->llist);
	q->name = (char *)(q + 1);
	strcpy(q->name, cb->shortname);
	q->cb = cb;
	q->top = NULL;
	q->index = taskq_index++;
	q->deleted = false;
	q->output_wait_needed = find_owait(cb);
	LLIST_APPEND(&q->llist, taskq);
	name = q->unique_name = Asprintf("CB(%s)[#%u]", q->name, q->index);
	vtrace("%s started%s\n", name, q->output_wait_needed? " (owait)": "");
    } else {
	q = current_task->taskq;
    }

    /* Push a callback. */
    s = task_push_onto(q, ST_CB, is_ui);
    s->cbx.cb = cb;
    s->cbx.handle = handle;
    if (name == NULL) {
	name = txAsprintf(TASK_NAME_FMT, TASK_sNAME(s));
    }

    /* Push the command as a macro on top of the callback. */
    if (buf) {
	task_set_state(s, TS_RUNNING, "child task to be pushed next");
	push_xmacro_onto(q, ST_MACRO, buf, len, is_ui);
    } else if (cmds != NULL) {
	task_set_state(s, TS_RUNNING, "child task to be pushed next");
	push_xmacro_onto_split(q, ST_MACRO, cmds, is_ui);
    } else if (cb->flags & CB_NEEDS_RUN) {
	/* Must call the run callback to get a child. */
	task_set_state(s, TS_NEED_RUN, "need to call run callback");
    } else {
	/* Children will be generated asynchronously. */
	task_set_state(s, TS_IDLE, "async CB");
    }

    /* Enable the abort button on the menu and the status indication. */
    if (!is_ui) {
	task_status_set();
    }

    /* Return the name. */
    return name;
}

/**
 * Push a callback on the stack.
 * @param[in] buf	Macro to push, or NULL
 * @param[in] len	Length of buf
 * @param[in] cb	Callback block
 * @param[in] handle	Callback handle
 *
 * @return Name of the new cb.
 */
char *
push_cb(const char *buf, size_t len, const tcb_t *cb, task_cbh handle)
{
    return push_cb_backend(buf, len, NULL, cb, handle);
}

/**
 * Push a callback on the stack.
 * @param[in] cmds	Actions to execute
 * @param[in] cb	Callback block
 * @param[in] handle	Callback handle
 *
 * @return Name of the new cb.
 */
char *
push_cb_split(cmd_t **cmds, const tcb_t *cb, task_cbh handle)
{
    return push_cb_backend(NULL, 0, cmds, cb, handle);
}

/**
 * Find the task associated with a handle.
 *
 * @param[in] handle	Handle to search for
 *
 * @return task, or NULL if not found
 */
static task_t *
task_find_cb(task_cbh handle)
{
    taskq_t *q;

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;

	for (s = q->top; s != NULL; s = s->next) {
	    if (s->type == ST_CB && s->cbx.handle == handle) {
		return s;
	    }
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    return NULL;
}

/* Set a CB task to NEED_RUN state. */
void
task_activate(task_cbh handle)
{
    task_t *s;

    s = task_find_cb(handle);
    if (s != NULL) {
	task_set_state(s, TS_NEED_RUN, "asked explicitly");
    }
}

/* Set a pending string. */
void
ps_set(char *s, bool is_hex, bool force_utf8)
{
    push_string(s, is_hex, false, force_utf8);
}

/* Callback for macros menu. */
void
macro_command(struct macro_def *m)
{
    push_macro(m->action);
}

/* Pass result text up to a script. */
static void
task_result(task_t *s, const char *msg, bool success)
{
    size_t sl = strlen(msg);
    char *text = NewString(msg);

    if (s->type != ST_CB) {
	Free(text);
	return;
    }

    /* Remove trailing spaces and newlines. */
    while (sl && (text[sl - 1] == ' ' || text[sl - 1] == '\n')) {
	sl--;
    }
    trace_task_output(s, "%.*s\n", (int)sl, text);
    (*s->cbx.cb->data)(s->cbx.handle, text, sl, success);

    Free(text);
}

/* Handle an error generated during the execution of a task. */
void
task_error(const char *msg)
{
    task_t *s;

    /* Print the error message. */
    s = task_redirect_to();
    if (s != NULL) {
	task_result(s, msg, false);
	s->success = false;
	current_task->success = false;
    } else {
	fprintf(stderr, "%s\n", msg);
	fflush(stderr);
    }
}

/*
 * Generate a response to a task.
 *
 * If the parameter is an empty string, generates nothing, but if it is a
 * newline, generates an empty line.
 */
void
task_info(const char *fmt, ...)
{
    char *nl;
    char *msgbuf;
    char *msg;
    va_list args;
    task_t *s;

    va_start(args, fmt);
    msgbuf = Vasprintf(fmt, args);
    va_end(args);

    msg = msgbuf;
    do {
	size_t nc;

	nl = strchr(msg, '\n');
	if (nl != NULL) {
	    nc = nl - msg;
	} else {
	    nc = strlen(msg);
	}
	if (nc || (nl != NULL)) {
	    if ((s = task_redirect_to()) != NULL) {
		assert(s->type == ST_CB);
		trace_task_output(current_task, "%.*s\n", nc, msg);
		(*s->cbx.cb->data)(s->cbx.handle, msg, nc, true);
	    } else {
		fprintf(stderr, "%.*s\n", (int)nc, msg);
	    }
	}
	msg = nl + 1;
    } while (nl);

    Free(msgbuf);
}

/**
 * Abort a blocked task because the host disconnected.
 *
 * @param[in] s		task to abort
 */
static void
task_disconnect_abort(task_t *s)
{
    task_t *t = s;

    vtrace("Canceling " TASK_NAME_FMT "\n", TASK_sNAME(s));

    while (t != NULL && t->type != ST_CB) {
	t = t->next;
    }
    if (t != NULL) {
	task_result(t,
		(s->state == TS_CONNECT_WAIT)?
		    "Connection failed": "Host disconnected",
		false);
	t->success = false;
	current_task->success = false;
    }
}

/**
 * Pop up an error, redirected towards a particular task.
 */
static void
popup_an_error_to(task_t *t, pae_t type, const char *fmt, ...)
{
    va_list ap;

    assert(current_task == NULL);
    current_task = t;
    va_start(ap, fmt);
    popup_a_vxerror(type, fmt, ap);
    va_end(ap);
    current_task = NULL;
}

/**
 * Pop up a connection-related error (a disconnect).
 */
void
connect_error(const char *fmt, ...)
{
    va_list ap;
    char *msg;

    /* Expand the message. */
    va_start(ap, fmt);
    msg = Vasprintf(fmt, ap);
    va_end(ap);

    if (!host_retry_mode && current_task == NULL) {
	taskq_t *q;
	task_t *s;
	bool found = false;
	
	/* Asynchronous. Look for a task in CONNECT_WAIT state. */
	FOREACH_LLIST(&taskq, q, taskq_t *) {
	    for (s = q->top; s != NULL; s = s->next) {
		if (s->state == TS_CONNECT_WAIT) {
		    found = true;
		    break;
		}
	    }
	    if (found) {
		break;
	    }
	} FOREACH_LLIST_END(&taskq, q, taskq_t *);

	if (found) {

	    /* Send it the error message. */
	    popup_an_error_to(s, ET_CONNECT, "%s", msg);

	    /* Let it complete with the error. */
	    s->wait_id = NULL_IOID;
	    s->success = false;
	    task_set_state(s, TS_RUNNING, "connection failed");
	    Free(msg);

	    host_disconnect(true);

	    return;
	}
    }

    /* Let the GUI handle it. */
    popup_an_xerror(ET_CONNECT, "%s", msg);
    Free(msg);

    /* Propagate elsewhere. */
    host_disconnect(true);
}

/**
 * Pop up a connection-related error (a disconnect), given an errno.
 */
void
connect_errno(int e, const char *fmt, ...)
{
    va_list ap;
    char *msg;

    /* Expand the message. */
    va_start(ap, fmt);
    msg = Vasprintf(fmt, ap);
    va_end(ap);
    connect_error("%s: %s", msg, strerror(e));
    Free(msg);
}

/**
 * Complete a file transfer.
 */
void
task_ft_complete(const char *msg, bool is_error)
{
    taskq_t *q;
    task_t *s;
    bool found = false;

    /* Look for a task in the right state. */
    FOREACH_LLIST(&taskq, q, taskq_t *) {
	for (s = q->top; s != NULL; s = s->next) {
	    if (s->is_ft) {
		found = true;
		break;
	    }
	}
	if (found) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    if (found) {
	task_t *parent = s->next;

	/* XXX: Calling another task's data method in this task's context could be dangerous. */
	if (parent != NULL && parent->type == ST_CB) {
	    const char *fragment = msg;
	    char *nl;

	    while ((nl = strchr(fragment, '\n')) != NULL) {
		if (nl > fragment) {
		    (*parent->cbx.cb->data)(parent->cbx.handle, fragment, nl - fragment, !is_error);
		}
		fragment = nl + 1;
	    }
	    if (*fragment) {
		(*parent->cbx.cb->data)(parent->cbx.handle, fragment, strlen(fragment), !is_error);
	    }

	    if (is_error) {
		s->success = false;
	    }
	}
    }
}

/**
 * Grabs a string from an offset on the screen.
 * Returns the string.
 *
 * N.B.: The length parameter is not the number of buffer positions to scan,
 * it is the size of the string to return. This is because the string to wait
 * for is multibyte, and we don't know how many buffer positions that might
 * represent.
 *
 * @param[in] baddr	buffer address
 * @param[in] len	length of match string
 * @param[in] buf	display buffer
 * @param[in] force_utf8 true to force string to UTF-8 encoding
 */
static char *
grab_string(int baddr, size_t len, struct ea *buf, bool force_utf8)
{
    int i;
    bool is_zero = false;
    varbuf_t r;
    char *ret;

    vb_init(&r);

    is_zero = FA_IS_ZERO(get_field_attribute(baddr));

    for (i = 0; vb_len(&r) < len; i++) {
	char mb[16];
	ucs4_t uc;
	size_t j;
	size_t xlen;
	struct ea *ea = &buf[(baddr + i) % (ROWS * COLS)];

	if (ea->fa) {
	    is_zero = FA_IS_ZERO(ea->fa);
	    vb_appends(&r, " ");
	} else if (is_zero) {
	    vb_appends(&r, " ");
	} else if (IS_RIGHT(ctlr_dbcs_state(baddr + i))) {
	    continue;
	} else {
	    if (is_nvt(ea, false, &uc)) {
		/* NVT-mode text. */
		if (uc >= UPRIV2_Aunderbar && uc <= UPRIV2_Zunderbar) {
		    uc -= UPRIV2;
		}
		if (toggled(MONOCASE)) {
		    uc = u_toupper(uc);
		}
		xlen = unicode_to_multibyte_f(uc, mb, sizeof(mb), force_utf8);
		for (j = 0; j < xlen - 1; j++) {
		    vb_appendf(&r, "%c", mb[j]);
		}
	    } else {
		/* 3270-mode text. */
		if (IS_LEFT(ctlr_dbcs_state(baddr + i))) {
		    xlen = ebcdic_to_multibyte_f((ea->ec << 8) |
			    buf[(baddr + i + 1) % (ROWS * COLS)].ec,
			    mb, sizeof(mb), force_utf8);
		    for (j = 0; j < xlen - 1; j++) {
			vb_appendf(&r, "%c", mb[j]);
		    }
		} else {
		    xlen = ebcdic_to_multibyte_fx(ea->ec, ea->cs, mb,
			    sizeof(mb),
			    EUO_BLANK_UNDEF |
			     (toggled(MONOCASE)? EUO_TOUPPER: 0),
			    &uc, force_utf8);
		    for (j = 0; j < xlen - 1; j++) {
			vb_appendf(&r, "%c", mb[j]);
		    }
		}
	    }
	}
    }

    ret = NewString(vb_buf(&r));
    if (ret == NULL) {
	ret = NewString("");
    }
    vb_free(&r);
    return ret;
}

/**
 * Run one task queue.
 *
 * @return True if one or more tasks were processed.
 */
static bool
run_taskq(void)
{
    bool any = false;

    while (true) {
	bool need_run = false;

	if (current_task == NULL) {
	    return any;
	}

	switch (current_task->state) {

	case TS_IDLE:
	    return any;		/* nothing to do */

	case TS_NEED_RUN:
	    need_run = true;
	    /* fall through... */
	case TS_RUNNING:
	    break;		/* let it proceed */

	case TS_KBWAIT:
	    if (CKBWAIT) {
		return any;
	    } else {
		if (current_task->is_ft) {
		    vtrace(TASK_NAME_FMT " clearing is_ft\n", TASK_NAME);
		}
		current_task->is_ft = false;
	    }
	    break;

	case TS_WAIT_NVT:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (IN_NVT) {
		task_set_state(current_task, TS_WAIT_IFIELD,
			"need ifield after NVT?");
		continue;
	    }
	    return any;

	case TS_WAIT_3270:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (IN_3270 | IN_SSCP) {
		task_set_state(current_task, TS_WAIT_IFIELD,
			"need ifield after 3270");
		continue;
	    }
	    return any;

	case TS_WAIT_UNLOCK:
	    if (KBWAIT) {
		return any;
	    }
	    break;

	case TS_WAIT_IFIELD:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (!CAN_PROCEED) {
		return any;
	    }
	    /* fall through... */
	case TS_CONNECT_WAIT:
	    if (!PCONNECTED) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (HALF_CONNECTED ||
		(CONNECTED && (kybdlock & KL_AWAITING_FIRST)) ||
		cstate == RECONNECTING) {
		return any;
	    }
	    break;

	case TS_TIME_WAIT:
	    return any;

	case TS_WAIT_OUTPUT:
	case TS_SWAIT_OUTPUT:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    return any;

	case TS_WAIT_DISC:
	    if (!CONNECTED || cstate == RECONNECTING) {
		break;
	    } else {
		return any;
	    }

	case TS_EXPECTING:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (expect_matches(current_task)) {
		any = true;
		break;
	    }
	    return any;

	case TS_PASSTHRU:
	    return any;

	case TS_XWAIT:
	    return any;

	case TS_WAIT_CURSOR_AT:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (cursor_addr == current_task->match.baddr) {
		any = true;
		break;
	    }
	    return any;
	case TS_WAIT_STRING_AT:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (current_task->match.baddr < ROWS * COLS) {
		char *current_string = grab_string(current_task->match.baddr,
			strlen(current_task->match.string), ea_buf,
			current_task->match.force_utf8);

		if (!strcmp(current_string, current_task->match.string)) {
		    Free(current_string);
		    any = true;
		    break;
		}
		Free(current_string);
	    }
	    return any;
	case TS_WAIT_IFIELD_AT:
	    if (!PCONNECTED || cstate == RECONNECTING) {
		task_disconnect_abort(current_task);
		any = true;
		break;
	    }
	    if (current_task->match.baddr < ROWS * COLS) {
		int fa_addr = find_field_attribute(current_task->match.baddr);

		if (fa_addr >= 0 && !FA_IS_PROTECTED(ea_buf[fa_addr].fa)) {
		    any = true;
		    break;
		}
	    }
	    return any;
	}

	/* Restart the task. */

	any = true;

	task_set_state(current_task, TS_IDLE, "about to resume");

	if (current_task->wait_id != NULL_IOID) {
	    RemoveTimeOut(current_task->wait_id);
	    current_task->wait_id = NULL_IOID;
	}

	switch (current_task->type) {
	case ST_MACRO:
	    run_macro();
	    break;
	case ST_CB:
	    if (need_run) {
		call_run(current_task);
	    } else {
		task_done(current_task->success);
	    }
	    break;
	}
    }

    return any;
}

/**
 * Run pending tasks.
 */
bool
run_tasks(void)
{
    taskq_t *q;
    bool any = false;

    /* There is no running task unless we are inside this function. */
    assert(current_task == NULL);

restart:
    /* Walk each queue, and run the tasks on it. */
    FOREACH_LLIST(&taskq, q, taskq_t *) {
	if (q->top != NULL) {
	    current_task = q->top;
	    any |= run_taskq();
	}
	if (q->deleted) {
	    llist_unlink(&q->llist);
	    Free(q->unique_name);
	    Free(q);
	    goto restart;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    /* Now there is no active task. */
    current_task = NULL;
    task_status_set();
    return any;
}

/* Set and propagate the output_wait_needed flag. */
static void
set_output_needed(bool needed)
{
    owait_t *o;

    if (needed) {
	const tcb_t *cb = current_task->taskq->cb;

	/* Change the flag on the current taskq. */
	current_task->taskq->output_wait_needed = needed;

	/* Track the callback block, to clear later. */
	for (o = owait_list; o != NULL; o = o->next) {
	    if (o->cb == cb) {
		break;
	    }
	}
	if (!o) {
	    o = (owait_t *)Malloc(sizeof(owait_t));
	    o->next = owait_list;
	    o->cb = cb;
	    owait_list = o;
	}
    } else {
	taskq_t *q;

	/* Clear it everywhere. */
	FOREACH_LLIST(&taskq, q, taskq_t *) {
	    q->output_wait_needed = false;
	} FOREACH_LLIST_END(&taskq, q, taskq_t *);

	/* No need to propagate it any more. */
	while (owait_list) {
	    owait_t *next = owait_list->next;

	    Free(owait_list);
	    owait_list = next;
	}
    }
}

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

/*
 * Dump a range of screen locations.
 * Returns true if anything was dumped.
 */
static bool
dump_range(int first, int len, bool in_ascii, struct ea *buf,
    int rel_rows _is_unused, int rel_cols, bool force_utf8)
{
    int i;
    bool any = false;
    bool is_zero = false;
    varbuf_t r;

    vb_init(&r);

    /*
     * 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 task_host_output,
     * which is called from the write logic in ctlr.c.
     *
     * Any of the following actions will enable Wait(Output):
     * - Ascii
     * - Ascii1
     * - Ebcdic
     * - Ebcdic1
     * - ReadBuffer
     */     
    if (current_task != NULL && buf == ea_buf) {
	set_output_needed(true);
    }

    is_zero = FA_IS_ZERO(get_field_attribute(first));

    for (i = 0; i < len; i++) {
	if (i && !((first + i) % rel_cols)) {
	    action_output("%s", vb_buf(&r));
	    vb_reset(&r);
	    any = false;
	}
	if (in_ascii) {
	    char mb[16];
	    ucs4_t uc;
	    size_t j;
	    size_t xlen;
	    enum dbcs_state d = ctlr_dbcs_state(first + i);

	    if (buf[first + i].fa) {
		is_zero = FA_IS_ZERO(buf[first + i].fa);
		vb_appends(&r, " ");
	    } else if (is_zero) {
		vb_appends(&r, " ");
	    } else if (IS_RIGHT(d)) {
		if (d == DBCS_RIGHT_WRAP) {
		    vb_appends(&r, " ");
		} else {
		    continue;
		}
	    } else {
		if (is_nvt(&buf[first + i], false, &uc)) {
		    /* NVT-mode text. */
		    if (uc >= UPRIV2_Aunderbar && uc <= UPRIV2_Zunderbar) {
			uc -= UPRIV2;
		    }
		    if (toggled(MONOCASE)) {
			uc = u_toupper(uc);
		    }
		    xlen = unicode_to_multibyte_f(uc, mb, sizeof(mb),
			    force_utf8);
		    for (j = 0; j < xlen - 1; j++) {
			vb_appendf(&r, "%c", mb[j]);
		    }
		} else {
		    /* 3270-mode text. */
		    if (IS_LEFT(d)) {
			xlen = ebcdic_to_multibyte_f((buf[first + i].ec << 8) |
				buf[first + i + 1].ec,
				mb, sizeof(mb), force_utf8);
			for (j = 0; j < xlen - 1; j++) {
			    vb_appendf(&r, "%c", mb[j]);
			}
		    } else {
			xlen = ebcdic_to_multibyte_fx(buf[first + i].ec,
				buf[first + i].cs, mb, sizeof(mb),
				EUO_BLANK_UNDEF |
				 (toggled(MONOCASE)? EUO_TOUPPER: 0),
				&uc, force_utf8);
			for (j = 0; j < xlen - 1; j++) {
			    vb_appendf(&r, "%c", mb[j]);
			}
		    }
		}
	    }
	} else {
	    ebc_t ebc = 0;

	    if (buf[first + i].ucs4) {
		/* NVT-mode text. */
		if (IS_RIGHT(ctlr_dbcs_state(first + i))) {
		    continue;
		}
		if (buf[first + i].cs != CS_LINEDRAW) {
		    /* Try to translate to EBCDIC. */
		    ebc = unicode_to_ebcdic(buf[first + i].ucs4);
		}
	    } else {
		/* 3270-mode text. */
		ebc = buf[first + i].ec;
	    }
	    vb_appendf(&r, "%s%02x", any ? " " : "", ebc);
	}
	any = true;
    }
    if (any) {
	action_output("%s", vb_buf(&r));
    }
    vb_free(&r);
    return any;
}

static bool
dump_fixed(const char **params, unsigned count, int origin, const char *name,
	bool in_ascii, struct ea *buf, int rel_rows, int rel_cols,
	int caddr, bool force_utf8)
{
    int row, col, len, rows = 0, cols = 0;
    bool any = false;

    switch (count) {
    case 0:	/* everything */
	row = origin;
	col = origin;
	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 false;
    }

    if (row < 0) {
	if (-row > rel_rows) {
	    popup_an_error("%s(): Invalid row", name);
	    return false;
	}
	row += rel_rows;
    } else {
	row -= origin;
    }
    if (col < 0) {
	if (-col > rel_cols) {
	    popup_an_error("%s(): Invalid column", name);
	    return false;
	}
	col += rel_cols;
    } else {
	col -= origin;
    }

    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 false;
    }
    if (count < 4) {
	any |= dump_range((row * rel_cols) + col, len, in_ascii, buf, rel_rows,
		rel_cols, force_utf8);
    } else {
	int i;

	for (i = 0; i < rows; i++) {
	    any |= dump_range(((row+i) * rel_cols) + col, cols, in_ascii, buf,
		    rel_rows, rel_cols, force_utf8);
	}
    }
    if (!any) {
	action_output("%s", "\n");
    }
    return true;
}

static bool
dump_field(unsigned count, const char *name, bool in_ascii, bool force_utf8)
{
    int faddr;
    int start, baddr;
    int len = 0;

    if (count != 0) {
	popup_an_error("%s() requires 0 arguments", name);
	return false;
    }
    if (!formatted) {
	popup_an_error("%s(): Screen is not formatted", name);
	return false;
    }
    faddr = find_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, force_utf8);
    return true;
}

static bool
Ascii_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnAscii, ia, argc, argv);
    return dump_fixed(argv, argc, 0, AnAscii, true, ea_buf, ROWS, COLS,
	    cursor_addr, IA_UTF8(ia));
}

static bool
Ascii1_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnAscii1, ia, argc, argv);
    return dump_fixed(argv, argc, 1, AnAscii1, true, ea_buf, ROWS, COLS,
	    cursor_addr, IA_UTF8(ia));
}

static bool
AsciiField_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnAsciiField, ia, argc, argv);
    return dump_field(argc, AnAsciiField, true, IA_UTF8(ia));
}

static bool
Ebcdic_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnEbcdic, ia, argc, argv);
    return dump_fixed(argv, argc, 0, AnEbcdic, false, ea_buf, ROWS, COLS,
	    cursor_addr, IA_UTF8(ia));
}

static bool
Ebcdic1_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnEbcdic1, ia, argc, argv);
    return dump_fixed(argv, argc, 1, AnEbcdic1, false, ea_buf, ROWS, COLS,
	    cursor_addr, IA_UTF8(ia));
}

static bool
EbcdicField_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnEbcdicField, ia, argc, argv);
    return dump_field(argc, AnEbcdicField, false, IA_UTF8(ia));
}

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 bool
do_read_buffer(const char **params, unsigned num_params, struct ea *buf,
	bool force_utf8)
{
    int	baddr;
    unsigned char current_fg = 0x00;
    unsigned char current_bg = 0x00;
    unsigned char current_gr = 0x00;
    unsigned char current_cs = 0x00;
    unsigned char current_ic = 0x00;
    enum { RB_ASCII, RB_EBCDIC, RB_UNICODE } mode = RB_ASCII;
    varbuf_t r;
    bool field = false;
    int field_baddr = 0;
    bool any = false;

    if (num_params > 0) {
	unsigned i;

	for (i = 0; i < num_params; i++) {
	    if (!strncasecmp(params[i], KwAscii, strlen(params[i]))) {
		mode = RB_ASCII;
	    } else if (!strncasecmp(params[i], KwEbcdic, strlen(params[i]))) {
		mode = RB_EBCDIC;
	    } else if (!strncasecmp(params[i], KwUnicode, strlen(params[i]))) {
		mode = RB_UNICODE;
	    } else if (!strncasecmp(params[i], KwField, strlen(params[i]))) {
		field = true;
	    } else {
		return action_args_are(AnReadBuffer, KwAscii, KwEbcdic,
			KwUnicode, KwField, NULL);
		return false;
	    }
	}
    }

    /*
     * 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 task_host_output,
     * which is called from the write logic in ctlr.c.
     *
     * Any of the following actions will enable Wait(Output):
     * - Ascii
     * - Ebcdic
     * - ReadBuffer
     */     
    if (current_task != NULL && buf == ea_buf) {
	set_output_needed(true);
    }

    if (field) {
	if (!formatted) {
	    popup_an_error(AnReadBuffer "(): no field");
	    return false;
	}
	baddr = find_field_attribute(cursor_addr);
	assert(baddr >= 0);
	field_baddr = baddr;
	action_output("Start1: %d %d", (baddr / COLS) + 1, (baddr % COLS) + 1);
	action_output("StartOffset: %d", baddr);
	action_output("Cursor1: %d %d", (cursor_addr / COLS) + 1,
		(cursor_addr % COLS) + 1);
	action_output("CursorOffset: %d", cursor_addr);
    } else {
	baddr = 0;
    }

    vb_init(&r);
    for (;;) {
	if (!field && !(baddr % COLS)) {
	    if (baddr) {
		action_output("%s", vb_buf(&r) + 1);
	    }
	    vb_reset(&r);
	}
	if (buf[baddr].fa) {
	    if (field && any) {
		break;
	    }
	    vb_appendf(&r, " SF(%02x=%02x", XA_3270, buf[baddr].fa);
	    if (buf[baddr].fg) {
		vb_appendf(&r, ",%02x=%02x", XA_FOREGROUND, buf[baddr].fg);
	    }
	    if (buf[baddr].bg) {
		vb_appendf(&r, ",%02x=%02x", XA_BACKGROUND, buf[baddr].bg);
	    }
	    if (buf[baddr].gr) {
		vb_appendf(&r, ",%02x=%02x", XA_HIGHLIGHTING,
			buf[baddr].gr | 0xf0);
	    }
	    if (buf[baddr].ic) {
		vb_appendf(&r, ",%02x=%02x", XA_INPUT_CONTROL, buf[baddr].ic);
	    }
	    if (buf[baddr].cs & CS_MASK) {
		vb_appendf(&r, ",%02x=%02x", XA_CHARSET,
			calc_cs(buf[baddr].cs));
	    }
	    vb_appends(&r, ")");
	} else {
	    bool any_sa = false;
	    unsigned char xcs;
#           define SA_SEP (any_sa? ",": " SA(")

	    if (buf[baddr].fg != current_fg) {
		vb_appendf(&r, "%s%02x=%02x", SA_SEP, XA_FOREGROUND,
			buf[baddr].fg);
		current_fg = buf[baddr].fg;
		any_sa = true;
	    }
	    if (buf[baddr].bg != current_bg) {
		vb_appendf(&r, "%s%02x=%02x", SA_SEP, XA_BACKGROUND,
			buf[baddr].bg);
		current_bg = buf[baddr].bg;
		any_sa = true;
	    }
	    if (buf[baddr].gr != current_gr) {
		vb_appendf(&r, "%s%02x=%02x", SA_SEP, XA_HIGHLIGHTING,
			buf[baddr].gr | 0xf0);
		current_gr = buf[baddr].gr;
		any_sa = true;
	    }
	    if (buf[baddr].ic != current_ic) {
		vb_appendf(&r, "%s%02x=%02x", SA_SEP, XA_INPUT_CONTROL,
			buf[baddr].ic);
		current_gr = buf[baddr].gr;
		any_sa = true;
	    }
	    xcs = buf[baddr].cs & CS_MASK;
	    if (xcs == CS_LINEDRAW) {
		/* Treat LINEDRAW and BASE as equivalent. */
		xcs = CS_BASE;
	    }
	    if (xcs != (current_cs & CS_MASK)) {
		vb_appendf(&r, "%s%02x=%02x", SA_SEP, XA_CHARSET, calc_cs(xcs));
		current_cs = xcs;
		any_sa = true;
	    }
	    if (any_sa) {
		vb_appends(&r, ")");
	    }
	    if (mode == RB_EBCDIC) {
		/*
		 * When dumping the buffer in EBCDIC mode, we implicitly
		 * ignore NVT-node text -- because the host never sent us
		 * anything in EBCDIC.
		 */
		if (buf[baddr].cs & CS_GE) {
		    vb_appendf(&r, " GE(%02x)", buf[baddr].ec);
		} else {
		    vb_appendf(&r, " %02x", buf[baddr].ec);
		}
	    } else if (mode == RB_ASCII) {
		bool done = false;
		char mb[16];
		size_t j;
		ucs4_t uc;
		size_t len;

		if (IS_LEFT(ctlr_dbcs_state(baddr))) {
		    if (buf[baddr].ucs4) {
			/* NVT-mode text. */
			len = unicode_to_multibyte_f(buf[baddr].ucs4, mb,
				sizeof(mb), force_utf8);
		    } else {
			/* 3270-mode text. */
			len = ebcdic_to_multibyte_f((buf[baddr].ec << 8) | buf[baddr + 1].ec, mb,
				sizeof(mb), force_utf8);
		    }
		    vb_appends(&r, " ");
		    for (j = 0; j < len-1; j++) {
			vb_appendf(&r, "%02x", mb[j] & 0xff);
		    }
		    done = true;
		} else if (IS_RIGHT(ctlr_dbcs_state(baddr))) {
		    vb_appends(&r, " -");
		    done = true;
		}

		if (is_nvt(&buf[baddr], false, &uc)) {
		    /* NVT-mode text. */
		    len = unicode_to_multibyte_f(uc, mb, sizeof(mb),
			    force_utf8);
		} else {
		    /* 3270-mode text. */
		    switch (buf[baddr].ec) {
		    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:
			ebcdic_to_multibyte_fx(buf[baddr].ec, buf[baddr].cs,
				mb, sizeof(mb), EUO_NONE, &uc, force_utf8);
			break;
		    }
		}

		if (!done) {
		    vb_appends(&r, " ");
		    if (mb[0] == '\0') {
			vb_appends(&r, "00");
		    } else {
			for (j = 0; mb[j]; j++) {
			    vb_appendf(&r, "%02x", mb[j] & 0xff);
			}
		    }
		}
	    } else {
		/* Unicode. */
		ucs4_t uc;

		if (IS_RIGHT(ctlr_dbcs_state(baddr))) {
		    vb_appends(&r, " -");
		} else {
		    if (IS_LEFT(ctlr_dbcs_state(baddr))) {
			if ((uc = buf[baddr].ucs4) == 0) {
			    uc = ebcdic_to_unicode(
				    (buf[baddr].ec << 8) | buf[baddr + 1].ec,
				    buf[baddr].cs, 0);
			}
		    } else {
			if (!is_nvt(&buf[baddr], false, &uc)) {
			    /* 3270-mode text. */
			    switch (buf[baddr].ec) {
			    case EBC_null:
				uc = 0;
				break;
			    case EBC_so:
				uc = 0x0e;
				break;
			    case EBC_si:
				uc = 0x0f;
				break;
			    default:
				uc = ebcdic_to_unicode(buf[baddr].ec,
					buf[baddr].cs, 0);
				break;
			    }
			}
		    }
		    vb_appendf(&r, " %04x", uc);
		}
	    }
	}
	INC_BA(baddr);
	if ((!field || !formatted) && baddr == 0) {
	    break;
	}
	if (field && baddr == field_baddr) {
	    break;
	}
	any = true;
    }
    action_output("%s%s", field? "Contents: ": "", vb_buf(&r) + 1);
    vb_free(&r);
    return true;
}

/*
 * ReadBuffer action.
 */
static bool
ReadBuffer_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnReadBuffer, ia, argc, argv);
    return do_read_buffer(argv, argc, ea_buf, IA_UTF8(ia));
}

/*
 * The script 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 NVT character mode
 *     L connected in NVT line mode
 *     P 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 = NULL;
    char em_mode;
    char *r;

    kb_stat = task_kbwait_state()? 'L': 'U';
    fmt_stat = formatted? 'F': 'U';

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

	prot_stat = FA_IS_PROTECTED(fa)? 'P': 'U';
    }

    if (cstate > RECONNECTING) {
	connect_stat = Asprintf("C(%s)", current_host);
    } else {
	connect_stat = NewString("N");
    }

    if (PCONNECTED) {
	if (IN_NVT) {
	    em_mode = linemode? 'L': 'C';
	} else {
	    em_mode = IN_3270? 'I': 'P';
	}
    } else {
	em_mode = 'N';
    }

    r = Asprintf("%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,
	    screen_window_number());

    Free(connect_stat);
    return r;
}

/* Return a status string for error reporting purposes. */
char *
task_status_string(void)
{
    return txAsprintf("%s 0.000", txdFree(status_string()));
}

/* Call a run callback. */
static void
call_run(task_t *s)
{
    bool success;

    vtrace("Running " TASK_NAME_FMT "\n", TASK_NAME);
    if ((*current_task->cbx.cb->run)(current_task->cbx.handle, &success)) {
	/* CB is complete. */
	vtrace(TASK_NAME_FMT " is complete, %s\n", TASK_NAME,
		success? "success": "failure");
	current_task->success = success;
	if (current_task->next) {
	    current_task->next->success = success;
	}
	task_pop();
    }
}

/**
 * A child task is done. Tell its parent CB.
 *
 * @param[in] success	True if action succeeded
 */
static void
task_done(bool success)
{
    struct task_cbx cbx = current_task->cbx;

    assert(current_task->type == ST_CB);

    vtrace(TASK_NAME_FMT " child task done, %s\n", TASK_NAME,
	    success? "success": "failure");

    /* Tell the callback its child is done. */
    if ((*cbx.cb->done)(cbx.handle, success, false)) {
	/* CB is complete. */
	task_pop();
    } else if (*cbx.cb->run != NULL) {
	call_run(current_task);
    }
}

/**
 * Generate a prompt, given a cb handle.
 *
 * @param[in] handle	handle
 *
 * @return prompt
 */
char *
task_cb_prompt(task_cbh handle)
{
    task_t *s;
    char *st;
    char *t;

    s = task_find_cb(handle);
    
    if (!s) {
	return "???";
    }

    st = status_string();
    t = txAsprintf("%s %ld.%03ld", st,
	    s->child_msec / 1000L,
	    s->child_msec % 1000L);
    Free(st);
    return t;
}

const char *
task_cb_name(task_cbh handle)
{
    task_t *s = task_find_cb(handle);

    return (s != NULL)? txAsprintf(TASK_NAME_FMT, TASK_sNAME(s)): "???";
}

/**
 * Return the child execution time for a cb.
 *
 * @param[in] handle	handle
 *
 * @return Execution time in msec.
 */
unsigned long
task_cb_msec(task_cbh handle)
{
    task_t *s = task_find_cb(handle);
    
    if (!s) {
	return 0;
    }
    return s->child_msec;
}

/* 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)
{
    set_output_needed(true);
    Replace(snap_status, status_string());

    Replace(snap_buf, (struct ea *)Malloc(ROWS*COLS*sizeof(struct ea)));
    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
 */
static bool
Snap_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnSnap, ia, argc, argv);

    if (current_task == NULL || current_task->state != TS_RUNNING) {
	popup_an_error(AnSnap "() can only be called from scripts or macros");
	return false;
    }

    if (argc == 0) {
	snap_save();
	return true;
    }

    /* Handle 'Snap Wait' separately. */
    if (!strcasecmp(argv[0], AnWait)) {
	long tmo = -1;
	char *ptr;
	unsigned maxp = 0;

	if (argc > 1 &&
	    (tmo = strtol(argv[1], &ptr, 10)) >= 0 &&
	    ptr != argv[0] &&
	    *ptr == '\0') {
	    maxp = 3;
	} else {
	    tmo = -1;
	    maxp = 2;
	}
	if (argc > maxp) {
	    popup_an_error("Too many arguments to " AnSnap "(" AnWait ")");
	    return false;
	}
	if (argc < maxp) {
	    popup_an_error("Too few arguments to " AnSnap "(" AnWait ")");
	    return false;
	}
	if (strcasecmp(argv[argc - 1], KwOutput)) {
	    popup_an_error("Unknown parameter to " AnSnap "(" AnWait ")");
	    return false;
	}

	/* Must be connected. */
	if (!(CONNECTED || HALF_CONNECTED)) {
	    popup_an_error(AnSnap "(): Not connected");
	    return false;
	}

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

	/* Set the new state. */
	task_set_state(current_task, TS_SWAIT_OUTPUT,
		AnWait "(" KwOutput ")");

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

    if (!strcasecmp(argv[0], KwSave)) {
	if (argc != 1) {
	    popup_an_error(AnSnap "(): Extra argument(s)");
	    return false;
	}
	snap_save();
    } else if (!strcasecmp(argv[0], KwSnapStatus)) {
	if (argc != 1) {
	    popup_an_error(AnSnap "(): Extra argument(s)");
	    return false;
	}
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	action_output("%s", snap_status);
    } else if (!strcasecmp(argv[0], KwRows)) {
	if (argc != 1) {
	    popup_an_error(AnSnap "(): Extra argument(s)");
	    return false;
	}
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	action_output("%d", snap_rows);
    } else if (!strcasecmp(argv[0], KwCols)) {
	if (argc != 1) {
	    popup_an_error(AnSnap "(): Extra argument(s)");
	    return false;
	}
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	action_output("%d", snap_cols);
    } else if (!strcasecmp(argv[0], AnAscii)) {
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	return dump_fixed(argv + 1, argc - 1, 0, AnAscii, true, snap_buf,
		snap_rows, snap_cols, snap_caddr, IA_UTF8(ia));
    } else if (!strcasecmp(argv[0], AnAscii1)) {
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	return dump_fixed(argv + 1, argc - 1, 1, AnAscii1, true, snap_buf,
		snap_rows, snap_cols, snap_caddr, IA_UTF8(ia));
    } else if (!strcasecmp(argv[0], AnEbcdic)) {
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	return dump_fixed(argv + 1, argc - 1, 0, AnEbcdic, false, snap_buf,
		snap_rows, snap_cols, snap_caddr, IA_UTF8(ia));
    } else if (!strcasecmp(argv[0], AnEbcdic1)) {
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	return dump_fixed(argv + 1, argc - 1, 1, AnEbcdic1, false, snap_buf,
		snap_rows, snap_cols, snap_caddr, IA_UTF8(ia));
    } else if (!strcasecmp(argv[0], AnReadBuffer)) {
	if (snap_status == NULL) {
	    popup_an_error(AnSnap "(): No saved state");
	    return false;
	}
	return do_read_buffer(argv + 1, argc - 1, snap_buf, IA_UTF8(ia));
    } else {
	return action_args_are(AnSnap, KwSave, KwSnapStatus, KwRows, KwCols,
		AnWait, AnAscii, AnAscii1, AnEbcdic, AnEbcdic1, AnReadBuffer,
		NULL);
	return false;
    }
    return true;
}

/* Parse row and column or offset arguments. */
static bool
parse_rco(const char *action, const char *keyword, unsigned argc,
	const char **argv, int *baddr)
{
    char *next;
    unsigned long offset;
    long row, column;

    if (argc == 1) {
	/* Offset. */
	offset = strtoul(argv[0], &next, 10);
	if (!argv[0][0] || *next != '\0' ||
		offset >= (unsigned long)(ROWS * COLS)) {
	    popup_an_error("%s(%s): Invalid offset '%s'", action, keyword,
		    argv[0]);
	    return false;
	}
	*baddr = (int)offset;
	return true;
    }

    /* Row and column. */
    row = strtol(argv[0], &next, 10);
    if (!argv[0][0] || *next != '\0' || row >= (long)ROWS) {
	popup_an_error("%s(%s): Invalid row '%s'", action, keyword, argv[0]);
	return false;
    }
    if (row < 0) {
	if (-row > (long)ROWS) {
	    popup_an_error("%s(%s): Invalid row '%s'", action, keyword, argv[0]);
	    return false;
	}
	row += ROWS + 1;
    }
    column = strtol(argv[1], &next, 10);
    if (!argv[1][0] || *next != '\0' || column >= (long)COLS) {
	popup_an_error("%s(%s): Invalid column '%s'", action, keyword, argv[1]);
	return false;
    }
    if (column < 0) {
	if (-column > (long)COLS) {
	    popup_an_error("%s(%s): Invalid column '%s'", action, keyword, argv[0]);
	    return false;
	}
	column += COLS + 1;
    }
    *baddr = (int)(((row - 1) * COLS) + (column - 1));
    return true;
}

/* Find the keyword for a wait state. */
static const char *
find_wait_kw(enum task_state state)
{
    int i;

    for (i = 0; wait_keywords[i].keyword != NULL; i++) {
	if (wait_keywords[i].next_state == state) {
	    return wait_keywords[i].keyword;
	}
    }
    return "Unknown";
}

/*
 * Wait for various conditions.
 */
static bool
Wait_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    enum task_state next_state = TS_WAIT_IFIELD;
    float tmo = -1.0;
    char *ptr;
    unsigned np;
    const char **pr;
    int i;
    int match_baddr = -1;
    const char *match_string = NULL;
    char *next_why;
#define CONNECTED_CHECK do { \
    if (next_state != TS_TIME_WAIT && !(CONNECTED || HALF_CONNECTED)) { \
	popup_an_error(AnWait "(): Not connected"); \
	return false; \
    } \
} while(false)

    action_debug(AnWait, ia, argc, argv);

    if (current_task == NULL || current_task->state != TS_RUNNING) {
	popup_an_error(AnWait "() can only be called from scripts or macros");
	return false;
    }

    /* Pick off the timeout parameter first. */
    if (argc > 0 &&
	(tmo = strtof(argv[0], &ptr)) >= 0.0 &&
	ptr != argv[0] &&
	*ptr == '\0') {
	np = argc - 1;
	pr = argv + 1;
    } else {
	tmo = -1.0;
	np = argc;
	pr = argv;
    }

    /* Do the initial keyword match and argument count check. */
    if (np > 0) {
	for (i = 0; wait_keywords[i].keyword != NULL; i++) {
	    if (!strcasecmp(pr[0], wait_keywords[i].keyword)) {
		if (check_argc(txAsprintf(AnWait "(%s)", wait_keywords[i].keyword),
			    np - 1, wait_keywords[i].min_args,
			    wait_keywords[i].max_args) < 0) {
		    return false;
		}
		next_state = wait_keywords[i].next_state;
		break;
	    }
	}
	if (wait_keywords[i].keyword == NULL) {
	    return action_args_are(AnWait, KwInputField, KwNvtMode, Kw3270Mode,
		    KwOutput, KwSeconds, KwDisconnect, KwUnlock, KwCursorAt,
		    KwStringAt, KwInputFieldAt, NULL);
	}
    }

    /* Do keyword-specific actions. */
    switch (next_state) {
    case TS_WAIT_3270:
	CONNECTED_CHECK;
	if (IN_3270) {
	    return true;
	}
	break;
    case TS_WAIT_NVT:
	CONNECTED_CHECK;
	if (IN_NVT) {
	    return true;
	}
	break;
    case TS_WAIT_DISC:
	if (!CONNECTED) {
	    return true;
	}
	break;
    case TS_WAIT_IFIELD:
	CONNECTED_CHECK;
	if (CAN_PROCEED) {
	    return true;
	}
	break;
    case TS_WAIT_OUTPUT:
	CONNECTED_CHECK;
	if (!current_task->taskq->output_wait_needed) {
	    return true;
	}
	break;
    case TS_WAIT_UNLOCK:
	CONNECTED_CHECK;
	if (!KBWAIT) {
	    return true;
	}
	break;
    case TS_TIME_WAIT:
	break;
    case TS_WAIT_CURSOR_AT:
	if (!parse_rco(AnWait, KwCursorAt, np - 1, pr + 1, &match_baddr)) {
	    return false;
	}
	CONNECTED_CHECK;
	if (cursor_addr == match_baddr) {
	    return true;
	}
	break;
    case TS_WAIT_STRING_AT:
	CONNECTED_CHECK;
	if (!parse_rco(AnWait, KwStringAt, np - 2, pr + 1, &match_baddr)) {
	    return false;
	}
	CONNECTED_CHECK;
	match_string = pr[np - 1];
	if (match_baddr < ROWS * COLS) {
	    char *current_string = grab_string(match_baddr,
		    strlen(match_string), ea_buf, ia == IA_HTTPD);

	    if (!strcmp(current_string, match_string)) {
		Free(current_string);
		return true;
	    }
	    Free(current_string);
	}
	break;
    case TS_WAIT_IFIELD_AT:
	if (!parse_rco(AnWait, KwInputFieldAt, np - 1, pr + 1, &match_baddr)) {
	    return false;
	}
	CONNECTED_CHECK;
	if (match_baddr < ROWS * COLS) {
	    int fa_addr = find_field_attribute(match_baddr);

	    if (fa_addr >= 0 && !FA_IS_PROTECTED(ea_buf[fa_addr].fa)) {
		return true;
	    }
	}
	break;
    default:
	break;
    }

    /* Wait for whatever it is to happen. */
    next_why = txAsprintf(AnWait "(%s%s)",
	    tmo >= 0.0 ? txAsprintf("%g,", tmo) : "",
	    find_wait_kw(next_state));
    task_set_state(current_task, next_state, next_why);
    if (match_baddr >= 0) {
	task_set_match(current_task, match_baddr, match_string,
		ia == IA_HTTPD);
    }

    /* Set up a timeout, if they want one. */
    if (tmo >= 0.0) {
	unsigned long tmo_msec = (unsigned long)(tmo * 1000);

	if (tmo_msec == 0) {
	    tmo_msec = 1;
	}
	current_task->wait_id = AddTimeOut(tmo_msec, wait_timed_out);
    }

    return true;
}

/* Timeout for Pause action. */
static void
pause_timed_out(ioid_t id)
{
    taskq_t *q;
    task_t *s;
    bool found = false;

    assert(current_task == NULL);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	for (s = q->top; s != NULL; s = s->next) {
	    if (s->wait_id == id) {
		found = true;
		break;
	    }
	}
	if (found) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    if (!found) {
	vtrace("pause_timed_out: no match\n");
	return;
    }

    /* If they just wanted a delay, succeed. */
    s->success = true;
    task_set_state(s, TS_RUNNING, AnPause "() completed");
    s->wait_id = NULL_IOID;
}

/*
 * Pause for unlockDelayMs milliseconds.
 */
static bool
Pause_action(ia_t ia _is_unused, unsigned argc, const char **argv)
{
    action_debug(AnPause, ia, argc, argv);
    if (check_argc(AnPause, argc, 0, 0) < 0) {
	return false;
    }
    if (appres.unlock_delay_ms == 0) {
	return true;
    }

    task_set_state(current_task, TS_TIME_WAIT, AnPause "()");
    current_task->wait_id = AddTimeOut(appres.unlock_delay_ms,
	    pause_timed_out);
    return true;
}

/*
 * Callback from Connect() and Reconnect() actions, to block a task.
 */
void
task_connect_wait(void)
{
    if (current_task != NULL &&
	(int)current_task->state >= (int)TS_RUNNING &&
	current_task->state != TS_WAIT_IFIELD &&
	(HALF_CONNECTED || (CONNECTED && (kybdlock & KL_AWAITING_FIRST)))) {

	task_set_state(current_task, TS_CONNECT_WAIT, AnConnect "() or Reconnect()");
    }
}

/*
 * Callback from ctlr.c, to indicate that the host has changed the screen.
 */
void
task_host_output(void)
{
    taskq_t *q;

    set_output_needed(false);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;

	for (s = q->top; s != NULL; s = s->next) {
	    switch (s->state) {
	    case TS_SWAIT_OUTPUT:
		snap_save();
		/* fall through... */
	    case TS_WAIT_OUTPUT:
		task_set_state(s, TS_RUNNING, "host changed screen");
		break;
	    default:
		break;
	    }
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);
}

/*
 * If error pop-ups and action output should be redirected, return the task to
 * redirect to.
 *
 * This is effective only for synchronous errors.
 */
static task_t *
task_redirect_to(void)
{
    task_t *s;

    for (s = current_task; s != NULL; s = s->next) {
	if (s->type == ST_CB && s->state == TS_RUNNING) {
	    return s;
	}
    }
    return NULL;
}

/* Return whether error pop-ups and action output should be short-circuited. */
bool
task_redirect(void)
{
    return task_redirect_to() != NULL;
}

/* Return whether any tasks are active. */
bool
task_active(void)
{
    return current_task != NULL;
}

/* Translate an expect string (uses C escape syntax). */
static void
expand_expect(task_t *task, const 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";

    task->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;
	    case 't':
		*t++ = '\t';
		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((unsigned char)c)) {
		n = (n * 16) + (int)(strchr(hexes,
			    tolower((unsigned char)c)) - hexes);
		nd++;
	    } else {
		if (nd) {
		    *t++ = n;
		} else {
		    *t++ = 'x';
		}
		*t++ = c;
		state = XS_BASE;
	    }
	    break;
	}
    }
    task->expect.len = t - task->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 NULL;
}

/* Check for a match against an expect string. */
static bool
expect_matches(task_t *task)
{
    size_t ix, i;
    unsigned char buf[NVT_SAVE_SIZE];
    char *t;

    ix = (nvt_save_ix + NVT_SAVE_SIZE - nvt_save_cnt) % NVT_SAVE_SIZE;
    for (i = 0; i < nvt_save_cnt; i++) {
	buf[i] = nvt_save_buf[(ix + i) % NVT_SAVE_SIZE];
    }
    t = memstr((char *)buf, task->expect.text, (int)nvt_save_cnt,
	    (int)task->expect.len);
    if (t != NULL) {
	nvt_save_cnt -= ((unsigned char *)t - buf) + task->expect.len;
	Replace(task->expect.text, NULL);
	return true;
    } else {
	return false;
    }
}

/* Store an NVT character for use by the Expect action. */
void
task_store(unsigned char c)
{
    /* Save the character in the buffer. */
    nvt_save_buf[nvt_save_ix++] = c;
    nvt_save_ix %= NVT_SAVE_SIZE;
    if (nvt_save_cnt < NVT_SAVE_SIZE) {
	nvt_save_cnt++;
    }
}

/* Dump whatever NVT data has been sent by the host since last called. */
static bool
NvtText_action(ia_t ia, unsigned argc, const char **argv)
{
    size_t i;
    size_t ix;
    unsigned char c;
    varbuf_t r;

    action_debug(AnNvtText, ia, argc, argv);
    if (check_argc(AnNvtText, argc, 0, 0) < 0) {
	return false;
    }

    if (!nvt_save_cnt) {
	action_output("%s", "\n");
	return true;
    }

    ix = (nvt_save_ix + NVT_SAVE_SIZE - nvt_save_cnt) % NVT_SAVE_SIZE;
    vb_init(&r);
    for (i = 0; i < nvt_save_cnt; i++) {
	c = nvt_save_buf[(ix + i) % NVT_SAVE_SIZE];
	if (!(c & ~0x1f)) switch (c) {
	    case '\n':
		vb_appends(&r, "\\n");
		break;
	    case '\r':
		vb_appends(&r, "\\r");
		break;
	    case '\b':
		vb_appends(&r, "\\b");
		break;
	    default:
		vb_appendf(&r, "\\%03o", c);
		break;
	} else if (c == '\\') {
	    vb_appends(&r, "\\\\");
	} else {
	    vb_append(&r, (char *)&c, 1);
	}
    }
    action_output("%s", vb_buf(&r));
    vb_free(&r);
    nvt_save_cnt = 0;
    nvt_save_ix = 0;
    return true;
}

/* Stop listening to stdin. */
static bool
CloseScript_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnCloseScript, ia, argc, argv);
    if (check_argc(AnCloseScript, argc, 0, 1) < 0) {
	return false;
    }

    if (current_task->type == ST_MACRO &&
	    current_task->next->type == ST_CB &&
	    current_task->next->cbx.cb->closescript != NULL) {
	(*current_task->next->cbx.cb->closescript)(current_task->next->cbx.handle);
	return true;
    } else {
	popup_an_error(AnCloseScript "() not supported for this type of "
		"script");
	return false;
    }
}

/* Execute an arbitrary shell command. */
static bool
Execute_action(ia_t ia, unsigned argc, const char **argv)
{
    const char **nargv = NULL;
    int nargc = 0;

    action_debug(AnExecute, ia, argc, argv);
    if (check_argc(AnExecute, argc, 1, 1) < 0) {
	return false;
    }

    /* Package it up for Script(). */
    array_add(&nargv, nargc++, "-NoLock");
#if !defined(_WIN32) /*[*/
    array_add(&nargv, nargc++, "/bin/sh");
    array_add(&nargv, nargc++, "-c");
#else /*][*/
    array_add(&nargv, nargc++, "cmd");
    array_add(&nargv, nargc++, "/c");
#endif /*]*/
    array_add(&nargv, nargc++, argv[0]);
    array_add(&nargv, nargc, NULL);
    return Script_action(ia, nargc, nargv);
}

/* Timeout for Expect action. */
static void
expect_timed_out(ioid_t id)
{
    taskq_t *q;
    task_t *s;
    bool found = false;

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	for (s = q->top; s != NULL; s = s->next) {
	    if (s->expect_id == id) {
		found = true;
		break;
	    }
	}
	if (found) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    if (!found) {
	vtrace("expect_timed_out: no match\n");
	return;
    }

    Replace(s->expect.text, NULL);

    current_task = s;
    popup_an_error(AnExpect "(): Timed out");
    current_task = NULL;

    s->expect_id = NULL_IOID;
    task_set_state(s, TS_RUNNING, AnExpect "() timed out");
    s->success = false;
}

/* Timeout for Wait action. */
static void
wait_timed_out(ioid_t id)
{
    taskq_t *q;
    task_t *s;
    bool found = false;
    const char *wait_kw;

    assert(current_task == NULL);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	for (s = q->top; s != NULL; s = s->next) {
	    if (s->wait_id == id) {
		found = true;
		break;
	    }
	}
	if (found) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    if (!found) {
	vtrace("wait_timed_out: no match\n");
	return;
    }

    wait_kw = find_wait_kw(s->state);

    /* If they just wanted a delay, succeed. */
    if (s->state == TS_TIME_WAIT) {
	s->success = true;
	task_set_state(s, TS_RUNNING, txAsprintf(AnWait "(%s) timed out", wait_kw));
	s->wait_id = NULL_IOID;
	return;
    }

    /* Pop up the error message. */
    popup_an_error_to(s, ET_OTHER, AnWait "(%s): Timed out", wait_kw);

    /* Forget the ID. */
    s->success = false;
    task_set_state(s, TS_RUNNING, txAsprintf(AnWait "(%s) timed out", wait_kw));
    s->wait_id = NULL_IOID;
}

/* Wait for a string from the host (NVT mode only). */
static bool
Expect_action(ia_t ia, unsigned argc, const char **argv)
{
    int tmo;

    action_debug(AnExpect, ia, argc, argv);
    if (check_argc(AnExpect, argc, 1, 2) < 0) {
	return false;
    }

    /* Verify the environment and parameters. */
    if (!IN_NVT) {
	popup_an_error(AnExpect "() is valid only when connected in NVT mode");
	return false;
    }
    if (argc == 2) {
	tmo = atoi(argv[1]);
	if (tmo < 1 || tmo > 600) {
	    popup_an_error(AnExpect "(): Invalid timeout: %s", argv[1]);
	    return false;
	}
    } else {
	tmo = 30;
    }

    /* See if the text is there already; if not, wait for it. */
    expand_expect(current_task, argv[0]);
    if (!expect_matches(current_task)) {
	current_task->expect_id = AddTimeOut(tmo * 1000, expect_timed_out);
	task_set_state(current_task, TS_EXPECTING, AnExpect "()");
    }
    /* else allow task to proceed */
    return true;
}

/* Keyboard disable action, enables or disables the keyboard explicitly. */
static bool
KeyboardDisable_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnKeyboardDisable, ia, argc, argv);
    if (check_argc(AnKeyboardDisable, argc, 0, 1) < 0) {
	return false;
    }

    if (argc == 0) {
	disable_keyboard(DISABLE, EXPLICIT, AnKeyboardDisable "() action");
    } else {
	if (!strcasecmp(argv[0], ResTrue)) {
	    disable_keyboard(DISABLE, EXPLICIT, AnKeyboardDisable "() action");
	} else if (!strcasecmp(argv[0], ResFalse)) {
	    disable_keyboard(ENABLE, EXPLICIT, AnKeyboardDisable "() action");
	} else if (!strcasecmp(argv[0], KwForceEnable)) {
	    force_enable_keyboard();
	} else {
	    return action_args_are(AnKeyboardDisable, ResTrue, ResFalse,
		    KwForceEnable, NULL);
	}
    }
    return true;
}

/* "Macro" action, explicitly invokes a named macro. */
static bool
Macro_action(ia_t ia, unsigned argc, const char **argv)
{
    struct macro_def *m;

    action_debug(AnMacro, ia, argc, argv);
    if (check_argc(AnMacro, argc, 1, 1) < 0) {
	return false;
    }
    for (m = macro_defs; m != NULL; m = m->next) {
	if (!strcmp(m->name, argv[0])) {
	    push_stack_macro(m->action);
	    return true;
	}
    }
    popup_an_error(AnMacro "(): No such macro: '%s'", argv[0]);
    return false;
}

/* "Printer" action, starts or stops a printer session. */
static bool
Printer_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnPrinter, ia, argc, argv);
    if (check_argc(AnPrinter, argc, 1, 2) < 0) {
	return false;
    }
    if (!strcasecmp(argv[0], KwStart)) {
	pr3287_session_start((argc > 1)? argv[1] : NULL);
    } else if (!strcasecmp(argv[0], KwStop)) {
	if (argc != 1) {
	    popup_an_error(AnPrinter "(): Extra argument(s)");
	    return false;
	}
	pr3287_session_stop();
    } else {
	return action_args_are(AnPrinter, KwStart, KwStop, NULL);
    }
    return true;
}

/*
 * Abort a queue.
 */
static void
abortq(taskq_t *q)
{
    task_t *s;
    task_t *next;

    for (s = q->top; s != NULL; s = next) {
	next = s->next;

	/* Don't abort a peer script. */
	if (s->type == ST_CB && (s->cbx.cb->flags & CB_PEER)) {
	    vtrace("Abort skipping peer\n");
	    continue;
	}

	/* Abort the cb. */
	if (s->type == ST_CB) {
	    vtrace("Canceling " TASK_NAME_FMT "\n", TASK_sNAME(s));
	    task_result(s, "Canceled", false);
	    (*s->cbx.cb->done)(s->cbx.handle, true, true);
	}

	/* Free the task -- this is not a pop */
	vtrace("Freeing " TASK_NAME_FMT "\n", TASK_sNAME(s));
	free_task(s);

	/* Take it out of the taskq. */
	q->top = next;
	q->depth--;
    }

    /* Mark the taskq as deleted. */
    if (q->depth == 0) {
	q->deleted = true;
    }
}

/*
 * Abort all scripts using a particular CB.
 */
void
abort_script_by_cb(const char *cb_name)
{
    taskq_t *q;

#if !defined(_WIN32) /*[*/
    /* child_ignore_output(); */ /* Needed? */
#endif /*]*/

    vtrace("Canceling all pending scripts for %s\n", cb_name);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	if (!strcmp(cb_name, q->cb->shortname)) {
	    abortq(q);
	}

    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    /* Re-evaluate the OIA and menus. */
    task_status_set();
}

/*
 * Abort all scripts using a particular CB.
 */
void
abort_queue(const char *unique_name)
{
    taskq_t *q;

#if !defined(_WIN32) /*[*/
    /* child_ignore_output(); */ /* Needed? */
#endif /*]*/

    vtrace("Canceling all pending scripts for %s\n", unique_name);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	if (!strcmp(unique_name, q->unique_name)) {
	    abortq(q);
	}

    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    /* Re-evaluate the OIA and menus. */
    task_status_set();
}

/* Abort all running scripts. */
void
abort_script(void)
{
    taskq_t *q;

#if !defined(_WIN32) /*[*/
    child_ignore_output();
#endif /*]*/

    vtrace("Canceling all pending scripts\n");

    /*
     * - Call the kill callbacks for every cb.
     * - Free every task (not popping, just freeing).
     * - Mark every taskq as deleted.
     */
    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;
	task_t *next;

	for (s = q->top; s != NULL; s = next) {
	    next = s->next;

	    /* Don't abort a peer script. */
	    if (s->type == ST_CB && (s->cbx.cb->flags & CB_PEER)) {
		vtrace("Abort skipping peer\n");
		continue;
	    }

	    /* Abort the cb. */
	    if (s->type == ST_CB) {
		vtrace("Canceling " TASK_NAME_FMT "\n", TASK_sNAME(s));
		task_result(s, "Canceled", false);
		(*s->cbx.cb->done)(s->cbx.handle, true, true);
	    }

	    /* Free the task -- this is not a pop */
	    vtrace("Freeing " TASK_NAME_FMT "\n", TASK_sNAME(s));
	    free_task(s);

	    /* Take it out of the taskq. */
	    q->top = next;
	    q->depth--;
	}

	/* Mark the taskq as deleted. */
	if (q->depth == 0) {
	    q->deleted = true;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    /* Re-evaluate the OIA and menus. */
    task_status_set();
}

/* "Abort" action, stops pending scripts. */
static bool
Abort_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnAbort, ia, argc, argv);
    if (check_argc(AnAbort, argc, 0, 0) < 0) {
	return false;
    }

    /* Set the bomb. */
    if (current_task != NULL && current_task->type == ST_MACRO) {
	current_task->fatal = true;
	if (current_task->next && current_task->next->type == ST_CB) {
	    current_task->next->fatal = true;
	}
    }
    return true;
}

/*
 * Bell action, used by scripts to ring the console bell and enter a comment
 * into the trace log.
 */
static bool
Bell_action(ia_t ia, unsigned argc, const char **argv)
{
    action_debug(AnBell, ia, argc, argv);
    if (check_argc(AnBell, argc, 0, 0) < 0) {
	return false;
    }
    if (product_has_display()) {
	ring_bell();
    } else {
	action_output("(ding)");
    }

    return true;
}

/* Tasks action, dumps out the current task state. */
char *
task_get_tasks(void)
{
    varbuf_t r;
    taskq_t *q;

    vb_init(&r);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	int i;
	vb_appendf(&r, "CB(%s) #%u\n", q->name, q->index);

	/* Walk the list backwards. */
	for (i = 0; i < q->depth; i++) {
	    task_t *s;
	    int j;
	    const char *last = NULL;

	    for (s = q->top, j = 0; j < q->depth - i - 1; s = s->next, j++) {
	    }

	    if (s->type == ST_MACRO && s->macro.last[0]) {
		last = s->macro.last;
	    } else if (s->type == ST_CB && s->cbx.cb->command != NULL) {
		last = (*s->cbx.cb->command)(s->cbx.handle);
	    }

	    vb_appendf(&r, "%*s" TASK_NAME_FMT " %s%s%s\n",
		    s->depth + 1, "",
		    TASK_sNAME(s),
		    task_state_name[s->state],
		    last? " => ": "",
		    last? last: "");
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    return vb_consume(&r);
}

/* Capabilities action, sets flags in the current CB. */
static bool
Capabilities_action(ia_t ia, unsigned argc, const char **argv)
{
    unsigned i;
    int j;
    task_t *redirect;
    unsigned flags = 0;
    static struct {
	unsigned flag;
	const char *name;
    } fname[] = {
	{ CBF_INTERACTIVE, KwInteractive },
	{ CBF_PWINPUT, KwPwInput },
	{ CBF_ERRD, KwErrd },
	{ 0, NULL }
    };

    action_debug(AnCapabilities, ia, argc, argv);

    redirect = task_redirect_to();

    if (argc == 0) {
	if (redirect == NULL || redirect->cbx.cb->getflags == NULL) {
	    return true;
	}

	flags = (*redirect->cbx.cb->getflags)(redirect->cbx.handle);
	for (j = 0; fname[j].name != NULL; j++) {
	    if (flags & fname[j].flag) {
		action_output("%s", fname[j].name);
	    }
	}
	return true;
    }

    if (redirect == NULL || redirect->cbx.cb->setflags == NULL) {
	popup_an_error(AnCapabilities "(): cannot set on this task type");
	return false;
    }

    for (i = 0; i < argc; i++) {
	for (j = 0; fname[j].name != NULL; j++) {
	    if (!strcasecmp(argv[i], fname[j].name)) {
		flags |= fname[j].flag;
		break;
	    }
	}
	if (fname[j].name == NULL) {
	    popup_an_error(AnCapabilities "(): Unknown flag '%s'", argv[i]);
	    return false;
	}
    }

    if (flags) {
	(*redirect->cbx.cb->setflags)(redirect->cbx.handle, flags);
    }

    return true;
}

/* Timeout for displaying the error message for a wrong cookie. */
static void
wrong_cookie_timeout(ioid_t id)
{
    taskq_t *q;
    task_t *s;
    bool found = false;

    assert(current_task == NULL);

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	for (s = q->top; s != NULL; s = s->next) {
	    if (s->wait_id == id) {
		found = true;
		break;
	    }
	}
	if (found) {
	    break;
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);

    if (!found) {
	vtrace("cookie_timed_out: no match\n");
	return;
    }

    /* Fail it. */
    task_result(s->next, AnCookie "(): Incorrect cookie", false);
    s->success = false;
    task_set_state(s, TS_RUNNING, AnCookie "() completed");
    s->wait_id = NULL_IOID;
}

/* Cookie  action, provides the security cookie. */
static bool
Cookie_action(ia_t ia, unsigned argc, const char **argv)
{
    task_t *redirect;

    action_debug(AnCookie, ia, argc, argv);
    if (check_argc(AnCookie, argc, 1, 1) < 0) {
	return false;
    }

    if (security_cookie == NULL) {
	/* Trying to set the cookie when none is needed is a no-op. */
	return true;
    }

    redirect = task_redirect_to();
    if (redirect == NULL || !(redirect->cbx.cb->flags & CB_NEEDCOOKIE) || redirect->cbx.cb->setxflags == NULL) {
	/* Trying to set the cookie when none is needed is a no-op. */
	return true;
    }

    if (strcmp(argv[0], security_cookie)) {
	unsigned long wait_ms = WRONG_COOKIE_BASE + (random() % WRONG_COOKIE_VAR);

	/* popup_an_error(AnCookie "(): Incorrect cookie"); */
	task_set_state(current_task, TS_TIME_WAIT, AnCookie "()");
	current_task->wait_id = AddTimeOut(wait_ms, wrong_cookie_timeout);
	(*redirect->cbx.cb->setxflags)(redirect->cbx.handle,
		(*redirect->cbx.cb->getxflags)(redirect->cbx.handle) & ~XF_HAVECOOKIE);
	return true;
    }

    /* Set the have-the-cookie flag. */
    (*redirect->cbx.cb->setxflags)(redirect->cbx.handle, XF_HAVECOOKIE);

    return true;
}

/*
 * ResumeInput action, resumes an action-suspended action.
 *
 * ResumeInput(text)
 * ResumeInput(-Abort)
 */
static bool
ResumeInput_action(ia_t ia, unsigned argc, const char **argv)
{
    input_request_t *ir;
    void *irhandle;
    task_t *redirect = task_redirect_to();
    char *text;
    int ret;

    action_debug(RESUME_INPUT, ia, argc, argv);
    if (check_argc(RESUME_INPUT, argc, 1, 1) < 0) {
	return false;
    }

    /* Check for a pending request. */
    if (redirect == NULL ||
	redirect->cbx.cb->irv == NULL ||
	(irhandle =
	 (*redirect->cbx.cb->irv->getir)(redirect->cbx.handle)) == NULL) {
	popup_an_error(RESUME_INPUT ": No pending input request");
	return false;
    }

    if (!strcasecmp(argv[0], "-Abort")) {
	/* Tell the CB to forget about it. */
	(*redirect->cbx.cb->irv->setir)(redirect->cbx.handle, NULL);

	/* Forget about it. */
	task_abort_input_request_irhandle(irhandle);

	popup_an_error("Action canceled");
	return false;
    }

    /* Decode the response text. */
    text = base64_decode(argv[0]);
    if (text == NULL) {
	(*redirect->cbx.cb->irv->setir)(redirect->cbx.handle, NULL);
	task_abort_input_request_irhandle(irhandle);
	popup_an_error(RESUME_INPUT ": Invalid base64 text");
	return false;
    }

    /* Tell the CB to forget about the input request. */
    (*redirect->cbx.cb->irv->setir)(redirect->cbx.handle, NULL);

    /* Continue and free the input request. */
    ir = (input_request_t *)irhandle;
    llist_unlink(&ir->llist);
    ret = (*ir->continue_fn)(ir->handle, text);
    Free(text);
    Free(ir);
    return ret;
}

/**
 * Test the current task for interactivity.
 *
 * @returns true if interactive.
 */
bool
task_is_interactive(void)
{
    return is_interactive(task_redirect_to());
}

/**
 * Test the current task for non-blocking Connect() / Open() / Transfer().
 *
 * @returns true if non-blocking.
 */
bool
task_nonblocking_connect(void)
{
    return is_nonblocking_connect(task_redirect_to());
}

/**
 * Check if the current running task contains the specified CB.
 *
 * @param[in] cb	CB
 *
 * @returns true if CB is active
 */
bool
task_running_cb_contains(tcb_t *cb)
{
    task_t *t;

    for (t = current_task; t != NULL; t = t->next) {
	if (t->type == ST_CB && t->cbx.cb == cb) {
	    return true;
	}
    }
    return false;
}

/**
 * Request input.
 *
 * @param[in] action		Action name
 * @param[in] no_echo		True to use no-echo mode
 *
 * @returns true if input can be provided
 */
bool
task_can_request_input(const char *action, bool no_echo)
{
    task_t *redirect = task_redirect_to();
    unsigned flags;

    if (redirect == NULL ||
	    redirect->cbx.cb->getflags == NULL ||
	    !((flags = (*redirect->cbx.cb->getflags)(redirect->cbx.handle)) &
	     CBF_INTERACTIVE)) {
	popup_an_error("%s: not an interactive session", action);
	return false;
    }

    if (no_echo && !(flags & CBF_PWINPUT)) {
	popup_an_error("%s: session does not support password input", action);
	return false;
    }

    return true;
}

/**
 * Request input.
 *
 * @param[in] action		Action name
 * @param[in] prompt		Prompt string
 * @param[in] continue_fn	Continue function
 * @param[in] abort_fn		Abort function
 * @param[in] handle		Handle to pass to continue functon
 * @param[in] no_echo		True to use no-echo mode
 *
 * @returns true if input requested successfully
 */
bool
task_request_input(const char *action, const char *prompt,
	continue_fn *continue_fn, abort_fn *abort_fn, void *handle,
	bool no_echo)
{
    task_t *redirect = task_redirect_to();
    unsigned flags;
    input_request_t *ir;
    char *encoded;

    if (redirect == NULL ||
	    redirect->cbx.cb->getflags == NULL ||
	    !((flags = (*redirect->cbx.cb->getflags)(redirect->cbx.handle)) &
	     CBF_INTERACTIVE)) {
	popup_an_error("%s: not an interactive session", action);
	return false;
    }

    if (no_echo && !(flags & CBF_PWINPUT)) {
	popup_an_error("%s: session does not support password input", action);
	return false;
    }

    /* Track this request. */
    ir = (input_request_t *)Malloc(sizeof(input_request_t));
    llist_init(&ir->llist);
    ir->continue_fn = continue_fn;
    ir->abort_fn = abort_fn;
    ir->handle = handle;
    LLIST_APPEND(&ir->llist, input_requestq);

    /* Tell the parent. */
    (*redirect->cbx.cb->irv->setir)(redirect->cbx.handle, ir);

    /* Tell them we want input. */
    encoded = txdFree(base64_encode(prompt));
    (*redirect->cbx.cb->reqinput)(redirect->cbx.handle, encoded,
	    strlen(encoded), !no_echo);
    return true;
}

/**
 * Abort an input request for a given handle.
 */
void
task_abort_input_request_irhandle(void *irhandle)
{
    input_request_t *ir = irhandle;

    /* Abort and forget. */
    llist_unlink(&ir->llist);
    if (ir->abort_fn != NULL) {
	(*ir->abort_fn)(ir->handle);
    }
    Free(ir);
}

/**
 * Set input-specific request context.
 *
 * @param[in] name	Input request type name
 * @param[in] state	Context to store
 * @param[in] abort	Abort callback
 */
void
task_set_ir_state(const char *name, void *state, ir_state_abort_cb abort)
{
    task_t *redirect = task_redirect_to();

    if (redirect != NULL && redirect->cbx.cb->irv != NULL) {
	(*redirect->cbx.cb->irv->setir_state)(redirect->cbx.handle, name,
		state, abort);
    }
}

/**
 * Get input-specific request context.
 *
 * @param[in] name	Input request type name
 * @returns context, or NULL
 */
void *
task_get_ir_state(const char *name)
{
    task_t *redirect = task_redirect_to();

    if (redirect != NULL && redirect->cbx.cb->irv != NULL) {
	return (*redirect->cbx.cb->irv->getir_state)(redirect->cbx.handle,
		name);
    } else {
	return NULL;
    }
}

typedef struct {
    char *previous;
} sample_per_type_t;

/* Continue the sample RequestInput action. */
static bool
sample_continue_input(void *handle, const char *text)
{
    sample_per_type_t *state = (sample_per_type_t *)handle;

    vtrace("Continuing RequestInput\n");
    action_output("You said '%s'", text);

    /* Remember for next time. */
    state = (sample_per_type_t *)handle;
    if (state != NULL) {
	Replace(state->previous, NewString(text));
    }

    return true;
}

/* Abort the sample RequestInput action. */
static void
sample_abort_input(void *handle)
{
    sample_per_type_t *state = (sample_per_type_t *)handle;

    vtrace("Canceling RequestInput\n");
    if (state != NULL) {
	Replace(state->previous, NewString("[canceled]"));
    }
}

/* Abort sample input request state. */
static void
sample_abort_session(void *handle)
{
    sample_per_type_t *state = (sample_per_type_t *)handle;

    vtrace("Canceling input request session\n");
    if (state != NULL) {
	Replace(state->previous, NULL);
	Free(state);
    }
}

/*
 * RequestInput action, dummy test of interactive input.
 *
 * RequestInput()
 */
static bool
RequestInput_action(ia_t ia, unsigned argc, const char **argv)
{
    sample_per_type_t *state;
    bool no_echo = false;

    action_debug(AnRequestInput, ia, argc, argv);
    if (check_argc(AnRequestInput, argc, 0, 1) < 0) {
	return false;
    }

    if (argc > 0) {
	if (!strcasecmp(argv[0], KwDashNoEcho)) {
	    no_echo = true;
	} else {
	    popup_an_error(AnRequestInput "(): unknown keyword '%s'", argv[0]);
	    return false;
	}
    }

    if (!task_can_request_input(AnRequestInput, no_echo)) {
	return false;
    }

    state = (sample_per_type_t *)task_get_ir_state(AnRequestInput);
    if (state == NULL) {
	/* Set up some state. */
	state = (sample_per_type_t *)Malloc(sizeof(*state));
	state->previous = NULL;
	task_set_ir_state(AnRequestInput, state, sample_abort_session);
    } else if (state->previous != NULL) {
	action_output("Your last answer was '%s'", state->previous);
    }

    task_request_input(AnRequestInput, "Input: ",
	    sample_continue_input, sample_abort_input, state, no_echo);
    return false;
}

/**
 * Initialize input request state.
 *
 * @param[in,out] ir_state	Input request state.
 */
void
task_cb_init_ir_state(task_cb_ir_state_t *ir_state)
{
    llist_init(ir_state);
}

/**
 * Set input request state.
 *
 * @param[in,out] ir_state	Input request state
 * @param[in] name		Name
 * @param[in] state		State value
 * @param[in] abort		Abort callback
 */
void
task_cb_set_ir_state(task_cb_ir_state_t *ir_state, const char *name,
	void *state, ir_state_abort_cb abort)
{
    ir_state_t *irs;

    FOREACH_LLIST(ir_state, irs, ir_state_t *) {
	if (!strcmp(irs->name, name)) {
	    irs->state = state;
	    irs->abort = abort;
	    return;
	}
    } FOREACH_LLIST_END(ir_state, irs, ir_state_t);

    irs = (ir_state_t *)Calloc(1, sizeof(ir_state_t));
    llist_init(&irs->llist);
    irs->name = name;
    irs->state = state;
    irs->abort = abort;
    LLIST_APPEND(&irs->llist, *ir_state);
}

/**
 * Get input request state.
 *
 * @param[in] ir_state	Input request state
 * @param[in] name	Name
 * @returns state, or NULL if name not found
 */
void *
task_cb_get_ir_state(task_cb_ir_state_t *ir_state, const char *name)
{
    ir_state_t *irs;

    FOREACH_LLIST(ir_state, irs, ir_state_t *) {
	if (!strcmp(irs->name, name)) {
	    return irs->state;
	}
    } FOREACH_LLIST_END(ir_state, irs, ir_state_t);
    return NULL;
}

/**
 * Abort all input request state.
 *
 * @param[in,out] ir_state	Input request state
 */
void
task_cb_abort_ir_state(task_cb_ir_state_t *ir_state)
{
    ir_state_t *irs;

    FOREACH_LLIST(ir_state, irs, ir_state_t *) {
	if (irs->abort != NULL) {
	    (*irs->abort)(irs->state);
	}
	llist_unlink(&irs->llist);
	Free(irs);
    } FOREACH_LLIST_END(ir_state, irs, ir_state_t);
}

/**
 * Indicate whether an input field can proceed.
 *
 * @return True if safe to proceed.
 */
bool
task_ifield_can_proceed(void)
{
    return CAN_PROCEED;
}

/**
 * Indicate whether it is safe for a task to go into KBWAIT state.
 *
 * @return True if safe for KBWAIT.
 */
bool task_can_kbwait(void)
{
    return (kybdlock & KBWAIT_MASK) && !(kybdlock & ~KBWAIT_MASK);
}

/**
 * Set the current task waiting for the keyboard to unlock.
 */
void
task_kbwait(void)
{
    task_set_state(current_task, TS_KBWAIT, "explicit request");
}

/* Set a task to WAIT_PASSTHRU state. */
const char *
task_set_passthru(task_cbh **ret_cbh)
{
    *ret_cbh = NULL;

    if (current_task != NULL && current_task->state == TS_RUNNING) {

	/* Look for a calling UI context. */
	task_t *t;
	for (t = current_task->next; t != NULL; t = t->next) {
	    if (t->is_ui && t->cbx.handle != NULL) {
		*ret_cbh = t->cbx.handle;
		break;
	    }
	}

	task_set_state(current_task, TS_PASSTHRU, "passthru processing");
	current_task->passthru_index = ++passthru_index;
	return txAsprintf("emu-%d", passthru_index);
    } else {
	return NULL;
    }
}

/* Complete a passthru. */
void
task_passthru_done(const char *tag, bool success, const char *result)
{
    taskq_t *q;

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;

	for (s = q->top; s != NULL; s = s->next) {
	    if (s->state == TS_PASSTHRU &&
		    !strncmp(tag, "emu-", 4) &&
		    s->passthru_index == atoi(tag + 4)) {
		task_set_state(s, TS_RUNNING, "passthru done");
		s->success = success;
		if (result && s->next != NULL && s->next->type == ST_CB) {
		    /* Pass the result back. */
		    task_result(s->next, result, success);
		}
		return;
	    }
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);
}

/* Continue a task that is blocked by task_xwait(). */
void
task_resume_xwait(void *context, bool cancel, const char *why)
{
    taskq_t *q;

    FOREACH_LLIST(&taskq, q, taskq_t *) {
	task_t *s;

	for (s = q->top; s != NULL; s = s->next) {
	    if (s->state == TS_XWAIT && s->wait_context == context) {
		task_set_state(s, TS_RUNNING,
			txAsprintf("extended wait done%s: %s",
			    cancel? " - cancel": "", why));
		s->wait_context = NULL;
		(*s->xcontinue_fn)(context, cancel);

		/*
		 * This code used to set s->success to false if this was a
		 * cancel, but there is no value in it, and also nothing
		 * no error is popped up.
		 */
		return;
	    }
	}
    } FOREACH_LLIST_END(&taskq, q, taskq_t *);
}

/* Block a task until task_resume_xwait() is called. */
void
task_xwait(void *context, xcontinue_fn *continue_fn, const char *why)
{
    assert(current_task != NULL);
    current_task->wait_context = context;
    current_task->xcontinue_fn = continue_fn;
    task_set_state(current_task, TS_XWAIT, txAsprintf("extended wait: %s", why));
}

/* Return the current KBWAIT status. */
bool
task_kbwait_state(void)
{
    return kybdlock != 0;
}
