text:
/**********************************************************************
 * pltcl.c		- PostgreSQL support for Tcl as
 *				  procedural language (PL)
 *
 *	  $PostgreSQL: pgsql/src/pl/tcl/pltcl.c,v 1.109 2007/02/01 19:10:30 momjian Exp $
 *
 **********************************************************************/

#include "postgres.h"

#include <tcl.h>

#include <unistd.h>
#include <fcntl.h>

/* Hack to deal with Tcl 8.4 const-ification without losing compatibility */
#ifndef CONST84
#define CONST84
#endif

#include "access/heapam.h"
#include "catalog/pg_language.h"
#include "catalog/pg_proc.h"
#include "commands/trigger.h"
#include "executor/spi.h"
#include "fmgr.h"
#include "nodes/makefuncs.h"
#include "parser/parse_type.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/syscache.h"
#include "utils/typcache.h"

#define HAVE_TCL_VERSION(maj,min) \
	((TCL_MAJOR_VERSION > maj) || \
	 (TCL_MAJOR_VERSION == maj && TCL_MINOR_VERSION >= min))

/* In Tcl >= 8.0, really not supposed to touch interp->result directly */
#if !HAVE_TCL_VERSION(8,0)
#define Tcl_GetStringResult(interp)  ((interp)->result)
#endif

#if defined(UNICODE_CONVERSION) && HAVE_TCL_VERSION(8,1)

#include "mb/pg_wchar.h"

static unsigned char *
utf_u2e(unsigned char *src)
{
	return pg_do_encoding_conversion(src, strlen(src), PG_UTF8, GetDatabaseEncoding());
}

static unsigned char *
utf_e2u(unsigned char *src)
{
	return pg_do_encoding_conversion(src, strlen(src), GetDatabaseEncoding(), PG_UTF8);
}

#define PLTCL_UTF
#define UTF_BEGIN	 do { \
					unsigned char *_pltcl_utf_src; \
					unsigned char *_pltcl_utf_dst
#define UTF_END		 if (_pltcl_utf_src!=_pltcl_utf_dst) \
					pfree(_pltcl_utf_dst); } while (0)
#define UTF_U2E(x)	 (_pltcl_utf_dst=utf_u2e(_pltcl_utf_src=(x)))
#define UTF_E2U(x)	 (_pltcl_utf_dst=utf_e2u(_pltcl_utf_src=(x)))
#else							/* !PLTCL_UTF */

#define  UTF_BEGIN
#define  UTF_END
#define  UTF_U2E(x)  (x)
#define  UTF_E2U(x)  (x)
#endif   /* PLTCL_UTF */

PG_MODULE_MAGIC;

/**********************************************************************
 * The information we cache about loaded procedures
 **********************************************************************/
typedef struct pltcl_proc_desc
{
	char	   *proname;
	TransactionId fn_xmin;
	ItemPointerData fn_tid;
	bool		fn_readonly;
	bool		lanpltrusted;
	FmgrInfo	result_in_func;
	Oid			result_typioparam;
	int			nargs;
	FmgrInfo	arg_out_func[FUNC_MAX_ARGS];
	bool		arg_is_rowtype[FUNC_MAX_ARGS];
} pltcl_proc_desc;

/**********************************************************************
 * The information we cache about prepared and saved plans
 **********************************************************************/
typedef struct pltcl_query_desc
{
	char		qname[20];
	void	   *plan;
	int			nargs;
	Oid		   *argtypes;
	FmgrInfo   *arginfuncs;
	Oid		   *argtypioparams;
} pltcl_query_desc;

/**********************************************************************
 * Global data
 **********************************************************************/
static bool pltcl_pm_init_done = false;
static bool pltcl_be_init_done = false;
static Tcl_Interp *pltcl_hold_interp = NULL;
static Tcl_Interp *pltcl_norm_interp = NULL;
static Tcl_Interp *pltcl_safe_interp = NULL;
static Tcl_HashTable *pltcl_proc_hash = NULL;
static Tcl_HashTable *pltcl_norm_query_hash = NULL;
static Tcl_HashTable *pltcl_safe_query_hash = NULL;

/* these are saved and restored by pltcl_call_handler */
static FunctionCallInfo pltcl_current_fcinfo = NULL;
static pltcl_proc_desc *pltcl_current_prodesc = NULL;

/**********************************************************************
 * Forward declarations
 **********************************************************************/
Datum		pltcl_call_handler(PG_FUNCTION_ARGS);
Datum		pltclu_call_handler(PG_FUNCTION_ARGS);
void		_PG_init(void);

static void pltcl_init_all(void);
static void pltcl_init_interp(Tcl_Interp *interp);
static void pltcl_init_load_unknown(Tcl_Interp *interp);

static Datum pltcl_func_handler(PG_FUNCTION_ARGS);

static HeapTuple pltcl_trigger_handler(PG_FUNCTION_ARGS);

static void throw_tcl_error(Tcl_Interp *interp);

static pltcl_proc_desc *compile_pltcl_function(Oid fn_oid, Oid tgreloid);

static int pltcl_elog(ClientData cdata, Tcl_Interp *interp,
		   int argc, CONST84 char *argv[]);
static int pltcl_quote(ClientData cdata, Tcl_Interp *interp,
			int argc, CONST84 char *argv[]);
static int pltcl_argisnull(ClientData cdata, Tcl_Interp *interp,
				int argc, CONST84 char *argv[]);
static int pltcl_returnnull(ClientData cdata, Tcl_Interp *interp,
				 int argc, CONST84 char *argv[]);

static int pltcl_SPI_execute(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[]);
static int pltcl_process_SPI_result(Tcl_Interp *interp,
						 CONST84 char *arrayname,
						 CONST84 char *loop_body,
						 int spi_rc,
						 SPITupleTable *tuptable,
						 int ntuples);
static int pltcl_SPI_prepare(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[]);
static int pltcl_SPI_execute_plan(ClientData cdata, Tcl_Interp *interp,
					   int argc, CONST84 char *argv[]);
static int pltcl_SPI_lastoid(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[]);

static void pltcl_set_tuple_values(Tcl_Interp *interp, CONST84 char *arrayname,
					   int tupno, HeapTuple tuple, TupleDesc tupdesc);
static void pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc,
						   Tcl_DString *retval);

/*
 * This routine is a crock, and so is everyplace that calls it.  The problem
 * is that the cached form of pltcl functions/queries is allocated permanently
 * (mostly via malloc()) and never released until backend exit.  Subsidiary
 * data structures such as fmgr info records therefore must live forever
 * as well.  A better implementation would store all this stuff in a per-
 * function memory context that could be reclaimed at need.  In the meantime,
 * fmgr_info_cxt must be called specifying TopMemoryContext so that whatever
 * it might allocate, and whatever the eventual function might allocate using
 * fn_mcxt, will live forever too.
 */
static void
perm_fmgr_info(Oid functionId, FmgrInfo *finfo)
{
	fmgr_info_cxt(functionId, finfo, TopMemoryContext);
}

/*
 * _PG_init()			- library load-time initialization
 *
 * DO NOT make this static nor change its name!
 */
void
_PG_init(void)
{
	/* Be sure we do initialization only once (should be redundant now) */
	if (pltcl_pm_init_done)
		return;

#ifdef WIN32
	/* Required on win32 to prevent error loading init.tcl */
	Tcl_FindExecutable("");
#endif

	/************************************************************
	 * Create the dummy hold interpreter to prevent close of
	 * stdout and stderr on DeleteInterp
	 ************************************************************/
	if ((pltcl_hold_interp = Tcl_CreateInterp()) == NULL)
		elog(ERROR, "could not create \"hold\" interpreter");

	/************************************************************
	 * Create the two interpreters
	 ************************************************************/
	if ((pltcl_norm_interp =
		 Tcl_CreateSlave(pltcl_hold_interp, "norm", 0)) == NULL)
		elog(ERROR, "could not create \"normal\" interpreter");
	pltcl_init_interp(pltcl_norm_interp);

	if ((pltcl_safe_interp =
		 Tcl_CreateSlave(pltcl_hold_interp, "safe", 1)) == NULL)
		elog(ERROR, "could not create \"safe\" interpreter");
	pltcl_init_interp(pltcl_safe_interp);

	/************************************************************
	 * Initialize the proc and query hash tables
	 ************************************************************/
	pltcl_proc_hash = (Tcl_HashTable *) malloc(sizeof(Tcl_HashTable));
	pltcl_norm_query_hash = (Tcl_HashTable *) malloc(sizeof(Tcl_HashTable));
	pltcl_safe_query_hash = (Tcl_HashTable *) malloc(sizeof(Tcl_HashTable));
	Tcl_InitHashTable(pltcl_proc_hash, TCL_STRING_KEYS);
	Tcl_InitHashTable(pltcl_norm_query_hash, TCL_STRING_KEYS);
	Tcl_InitHashTable(pltcl_safe_query_hash, TCL_STRING_KEYS);

	pltcl_pm_init_done = true;
}

/**********************************************************************
 * pltcl_init_all()		- Initialize all
 *
 * This does initialization that can't be done in the postmaster, and
 * hence is not safe to do at library load time.
 **********************************************************************/
static void
pltcl_init_all(void)
{
	/************************************************************
	 * Try to load the unknown procedure from pltcl_modules
	 ************************************************************/
	if (!pltcl_be_init_done)
	{
		if (SPI_connect() != SPI_OK_CONNECT)
			elog(ERROR, "SPI_connect failed");
		pltcl_init_load_unknown(pltcl_norm_interp);
		pltcl_init_load_unknown(pltcl_safe_interp);
		if (SPI_finish() != SPI_OK_FINISH)
			elog(ERROR, "SPI_finish failed");
		pltcl_be_init_done = true;
	}
}

/**********************************************************************
 * pltcl_init_interp() - initialize a Tcl interpreter
 **********************************************************************/
static void
pltcl_init_interp(Tcl_Interp *interp)
{
	/************************************************************
	 * Install the commands for SPI support in the interpreter
	 ************************************************************/
	Tcl_CreateCommand(interp, "elog",
					  pltcl_elog, NULL, NULL);
	Tcl_CreateCommand(interp, "quote",
					  pltcl_quote, NULL, NULL);
	Tcl_CreateCommand(interp, "argisnull",
					  pltcl_argisnull, NULL, NULL);
	Tcl_CreateCommand(interp, "return_null",
					  pltcl_returnnull, NULL, NULL);

	Tcl_CreateCommand(interp, "spi_exec",
					  pltcl_SPI_execute, NULL, NULL);
	Tcl_CreateCommand(interp, "spi_prepare",
					  pltcl_SPI_prepare, NULL, NULL);
	Tcl_CreateCommand(interp, "spi_execp",
					  pltcl_SPI_execute_plan, NULL, NULL);
	Tcl_CreateCommand(interp, "spi_lastoid",
					  pltcl_SPI_lastoid, NULL, NULL);
}

/**********************************************************************
 * pltcl_init_load_unknown()	- Load the unknown procedure from
 *				  table pltcl_modules (if it exists)
 **********************************************************************/
static void
pltcl_init_load_unknown(Tcl_Interp *interp)
{
	int			spi_rc;
	int			tcl_rc;
	Tcl_DString unknown_src;
	char	   *part;
	int			i;
	int			fno;

	/************************************************************
	 * Check if table pltcl_modules exists
	 ************************************************************/
	spi_rc = SPI_execute("select 1 from pg_catalog.pg_class "
						 "where relname = 'pltcl_modules'",
						 false, 1);
	SPI_freetuptable(SPI_tuptable);
	if (spi_rc != SPI_OK_SELECT)
		elog(ERROR, "select from pg_class failed");
	if (SPI_processed == 0)
		return;

	/************************************************************
	 * Read all the row's from it where modname = 'unknown' in
	 * the order of modseq
	 ************************************************************/
	Tcl_DStringInit(&unknown_src);

	spi_rc = SPI_execute("select modseq, modsrc from pltcl_modules "
						 "where modname = 'unknown' "
						 "order by modseq",
						 false, 0);
	if (spi_rc != SPI_OK_SELECT)
		elog(ERROR, "select from pltcl_modules failed");

	/************************************************************
	 * If there's nothing, module unknown doesn't exist
	 ************************************************************/
	if (SPI_processed == 0)
	{
		Tcl_DStringFree(&unknown_src);
		SPI_freetuptable(SPI_tuptable);
		elog(WARNING, "module \"unknown\" not found in pltcl_modules");
		return;
	}

	/************************************************************
	 * There is a module named unknown. Resemble the
	 * source from the modsrc attributes and evaluate
	 * it in the Tcl interpreter
	 ************************************************************/
	fno = SPI_fnumber(SPI_tuptable->tupdesc, "modsrc");

	for (i = 0; i < SPI_processed; i++)
	{
		part = SPI_getvalue(SPI_tuptable->vals[i],
							SPI_tuptable->tupdesc, fno);
		if (part != NULL)
		{
			UTF_BEGIN;
			Tcl_DStringAppend(&unknown_src, UTF_E2U(part), -1);
			UTF_END;
			pfree(part);
		}
	}
	tcl_rc = Tcl_GlobalEval(interp, Tcl_DStringValue(&unknown_src));
	Tcl_DStringFree(&unknown_src);
	SPI_freetuptable(SPI_tuptable);
}

/**********************************************************************
 * pltcl_call_handler		- This is the only visible function
 *				  of the PL interpreter. The PostgreSQL
 *				  function manager and trigger manager
 *				  call this function for execution of
 *				  PL/Tcl procedures.
 **********************************************************************/
PG_FUNCTION_INFO_V1(pltcl_call_handler);

/* keep non-static */
Datum
pltcl_call_handler(PG_FUNCTION_ARGS)
{
	Datum		retval;
	FunctionCallInfo save_fcinfo;
	pltcl_proc_desc *save_prodesc;

	/*
	 * Initialize interpreters if first time through
	 */
	pltcl_init_all();

	/*
	 * Ensure that static pointers are saved/restored properly
	 */
	save_fcinfo = pltcl_current_fcinfo;
	save_prodesc = pltcl_current_prodesc;

	PG_TRY();
	{
		/*
		 * Determine if called as function or trigger and call appropriate
		 * subhandler
		 */
		if (CALLED_AS_TRIGGER(fcinfo))
		{
			pltcl_current_fcinfo = NULL;
			retval = PointerGetDatum(pltcl_trigger_handler(fcinfo));
		}
		else
		{
			pltcl_current_fcinfo = fcinfo;
			retval = pltcl_func_handler(fcinfo);
		}
	}
	PG_CATCH();
	{
		pltcl_current_fcinfo = save_fcinfo;
		pltcl_current_prodesc = save_prodesc;
		PG_RE_THROW();
	}
	PG_END_TRY();

	pltcl_current_fcinfo = save_fcinfo;
	pltcl_current_prodesc = save_prodesc;

	return retval;
}

/*
 * Alternate handler for unsafe functions
 */
PG_FUNCTION_INFO_V1(pltclu_call_handler);

/* keep non-static */
Datum
pltclu_call_handler(PG_FUNCTION_ARGS)
{
	return pltcl_call_handler(fcinfo);
}

/**********************************************************************
 * pltcl_func_handler()		- Handler for regular function calls
 **********************************************************************/
static Datum
pltcl_func_handler(PG_FUNCTION_ARGS)
{
	pltcl_proc_desc *prodesc;
	Tcl_Interp *volatile interp;
	Tcl_DString tcl_cmd;
	Tcl_DString list_tmp;
	int			i;
	int			tcl_rc;
	Datum		retval;

	/* Connect to SPI manager */
	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "could not connect to SPI manager");

	/* Find or compile the function */
	prodesc = compile_pltcl_function(fcinfo->flinfo->fn_oid, InvalidOid);

	pltcl_current_prodesc = prodesc;

	if (prodesc->lanpltrusted)
		interp = pltcl_safe_interp;
	else
		interp = pltcl_norm_interp;

	/************************************************************
	 * Create the tcl command to call the internal
	 * proc in the Tcl interpreter
	 ************************************************************/
	Tcl_DStringInit(&tcl_cmd);
	Tcl_DStringInit(&list_tmp);
	Tcl_DStringAppendElement(&tcl_cmd, prodesc->proname);

	/************************************************************
	 * Add all call arguments to the command
	 ************************************************************/
	PG_TRY();
	{
		for (i = 0; i < prodesc->nargs; i++)
		{
			if (prodesc->arg_is_rowtype[i])
			{
				/**************************************************
				 * For tuple values, add a list for 'array set ...'
				 **************************************************/
				if (fcinfo->argnull[i])
					Tcl_DStringAppendElement(&tcl_cmd, "");
				else
				{
					HeapTupleHeader td;
					Oid			tupType;
					int32		tupTypmod;
					TupleDesc	tupdesc;
					HeapTupleData tmptup;

					td = DatumGetHeapTupleHeader(fcinfo->arg[i]);
					/* Extract rowtype info and find a tupdesc */
					tupType = HeapTupleHeaderGetTypeId(td);
					tupTypmod = HeapTupleHeaderGetTypMod(td);
					tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
					/* Build a temporary HeapTuple control structure */
					tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
					tmptup.t_data = td;

					Tcl_DStringSetLength(&list_tmp, 0);
					pltcl_build_tuple_argument(&tmptup, tupdesc, &list_tmp);
					Tcl_DStringAppendElement(&tcl_cmd,
											 Tcl_DStringValue(&list_tmp));
					ReleaseTupleDesc(tupdesc);
				}
			}
			else
			{
				/**************************************************
				 * Single values are added as string element
				 * of their external representation
				 **************************************************/
				if (fcinfo->argnull[i])
					Tcl_DStringAppendElement(&tcl_cmd, "");
				else
				{
					char	   *tmp;

					tmp = OutputFunctionCall(&prodesc->arg_out_func[i],
											 fcinfo->arg[i]);
					UTF_BEGIN;
					Tcl_DStringAppendElement(&tcl_cmd, UTF_E2U(tmp));
					UTF_END;
					pfree(tmp);
				}
			}
		}
	}
	PG_CATCH();
	{
		Tcl_DStringFree(&tcl_cmd);
		Tcl_DStringFree(&list_tmp);
		PG_RE_THROW();
	}
	PG_END_TRY();
	Tcl_DStringFree(&list_tmp);

	/************************************************************
	 * Call the Tcl function
	 *
	 * We assume no PG error can be thrown directly from this call.
	 ************************************************************/
	tcl_rc = Tcl_GlobalEval(interp, Tcl_DStringValue(&tcl_cmd));
	Tcl_DStringFree(&tcl_cmd);

	/************************************************************
	 * Check for errors reported by Tcl.
	 ************************************************************/
	if (tcl_rc != TCL_OK)
		throw_tcl_error(interp);

	/************************************************************
	 * Disconnect from SPI manager and then create the return
	 * value datum (if the input function does a palloc for it
	 * this must not be allocated in the SPI memory context
	 * because SPI_finish would free it).  But don't try to call
	 * the result_in_func if we've been told to return a NULL;
	 * the Tcl result may not be a valid value of the result type
	 * in that case.
	 ************************************************************/
	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish() failed");

	if (fcinfo->isnull)
		retval = InputFunctionCall(&prodesc->result_in_func,
								   NULL,
								   prodesc->result_typioparam,
								   -1);
	else
	{
		UTF_BEGIN;
		retval = InputFunctionCall(&prodesc->result_in_func,
								   UTF_U2E((char *) Tcl_GetStringResult(interp)),
								   prodesc->result_typioparam,
								   -1);
		UTF_END;
	}

	return retval;
}

/**********************************************************************
 * pltcl_trigger_handler()	- Handler for trigger calls
 **********************************************************************/
static HeapTuple
pltcl_trigger_handler(PG_FUNCTION_ARGS)
{
	pltcl_proc_desc *prodesc;
	Tcl_Interp *volatile interp;
	TriggerData *trigdata = (TriggerData *) fcinfo->context;
	char	   *stroid;
	TupleDesc	tupdesc;
	volatile HeapTuple rettup;
	Tcl_DString tcl_cmd;
	Tcl_DString tcl_trigtup;
	Tcl_DString tcl_newtup;
	int			tcl_rc;
	int			i;
	int		   *modattrs;
	Datum	   *modvalues;
	char	   *modnulls;
	int			ret_numvals;
	CONST84 char *result;
	CONST84 char **ret_values;

	/* Connect to SPI manager */
	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "could not connect to SPI manager");

	/* Find or compile the function */
	prodesc = compile_pltcl_function(fcinfo->flinfo->fn_oid,
									 RelationGetRelid(trigdata->tg_relation));

	pltcl_current_prodesc = prodesc;

	if (prodesc->lanpltrusted)
		interp = pltcl_safe_interp;
	else
		interp = pltcl_norm_interp;

	tupdesc = trigdata->tg_relation->rd_att;

	/************************************************************
	 * Create the tcl command to call the internal
	 * proc in the interpreter
	 ************************************************************/
	Tcl_DStringInit(&tcl_cmd);
	Tcl_DStringInit(&tcl_trigtup);
	Tcl_DStringInit(&tcl_newtup);
	PG_TRY();
	{
		/* The procedure name */
		Tcl_DStringAppendElement(&tcl_cmd, prodesc->proname);

		/* The trigger name for argument TG_name */
		Tcl_DStringAppendElement(&tcl_cmd, trigdata->tg_trigger->tgname);

		/* The oid of the trigger relation for argument TG_relid */
		stroid = DatumGetCString(DirectFunctionCall1(oidout,
							ObjectIdGetDatum(trigdata->tg_relation->rd_id)));
		Tcl_DStringAppendElement(&tcl_cmd, stroid);
		pfree(stroid);

		/* The name of the table the trigger is acting on: TG_table_name */
		stroid = SPI_getrelname(trigdata->tg_relation);
		Tcl_DStringAppendElement(&tcl_cmd, stroid);
		pfree(stroid);

		/* The schema of the table the trigger is acting on: TG_table_schema */
		stroid = SPI_getnspname(trigdata->tg_relation);
		Tcl_DStringAppendElement(&tcl_cmd, stroid);
		pfree(stroid);

		/* A list of attribute names for argument TG_relatts */
		Tcl_DStringAppendElement(&tcl_trigtup, "");
		for (i = 0; i < tupdesc->natts; i++)
		{
			if (tupdesc->attrs[i]->attisdropped)
				Tcl_DStringAppendElement(&tcl_trigtup, "");
			else
				Tcl_DStringAppendElement(&tcl_trigtup,
										 NameStr(tupdesc->attrs[i]->attname));
		}
		Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
		Tcl_DStringFree(&tcl_trigtup);
		Tcl_DStringInit(&tcl_trigtup);

		/* The when part of the event for TG_when */
		if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
			Tcl_DStringAppendElement(&tcl_cmd, "BEFORE");
		else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
			Tcl_DStringAppendElement(&tcl_cmd, "AFTER");
		else
			elog(ERROR, "unrecognized WHEN tg_event: %u", trigdata->tg_event);

		/* The level part of the event for TG_level */
		if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
		{
			Tcl_DStringAppendElement(&tcl_cmd, "ROW");

			/* Build the data list for the trigtuple */
			pltcl_build_tuple_argument(trigdata->tg_trigtuple,
									   tupdesc, &tcl_trigtup);

			/*
			 * Now the command part of the event for TG_op and data for NEW
			 * and OLD
			 */
			if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
			{
				Tcl_DStringAppendElement(&tcl_cmd, "INSERT");

				Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));
				Tcl_DStringAppendElement(&tcl_cmd, "");

				rettup = trigdata->tg_trigtuple;
			}
			else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
			{
				Tcl_DStringAppendElement(&tcl_cmd, "DELETE");

				Tcl_DStringAppendElement(&tcl_cmd, "");
				Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));

				rettup = trigdata->tg_trigtuple;
			}
			else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
			{
				Tcl_DStringAppendElement(&tcl_cmd, "UPDATE");

				pltcl_build_tuple_argument(trigdata->tg_newtuple,
										   tupdesc, &tcl_newtup);

				Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_newtup));
				Tcl_DStringAppendElement(&tcl_cmd, Tcl_DStringValue(&tcl_trigtup));

				rettup = trigdata->tg_newtuple;
			}
			else
				elog(ERROR, "unrecognized OP tg_event: %u", trigdata->tg_event);
		}
		else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
		{
			Tcl_DStringAppendElement(&tcl_cmd, "STATEMENT");

			if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
				Tcl_DStringAppendElement(&tcl_cmd, "INSERT");
			else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
				Tcl_DStringAppendElement(&tcl_cmd, "DELETE");
			else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
				Tcl_DStringAppendElement(&tcl_cmd, "UPDATE");
			else
				elog(ERROR, "unrecognized OP tg_event: %u", trigdata->tg_event);

			Tcl_DStringAppendElement(&tcl_cmd, "");
			Tcl_DStringAppendElement(&tcl_cmd, "");

			rettup = (HeapTuple) NULL;
		}
		else
			elog(ERROR, "unrecognized LEVEL tg_event: %u", trigdata->tg_event);

		/* Finally append the arguments from CREATE TRIGGER */
		for (i = 0; i < trigdata->tg_trigger->tgnargs; i++)
			Tcl_DStringAppendElement(&tcl_cmd, trigdata->tg_trigger->tgargs[i]);

	}
	PG_CATCH();
	{
		Tcl_DStringFree(&tcl_cmd);
		Tcl_DStringFree(&tcl_trigtup);
		Tcl_DStringFree(&tcl_newtup);
		PG_RE_THROW();
	}
	PG_END_TRY();
	Tcl_DStringFree(&tcl_trigtup);
	Tcl_DStringFree(&tcl_newtup);

	/************************************************************
	 * Call the Tcl function
	 *
	 * We assume no PG error can be thrown directly from this call.
	 ************************************************************/
	tcl_rc = Tcl_GlobalEval(interp, Tcl_DStringValue(&tcl_cmd));
	Tcl_DStringFree(&tcl_cmd);

	/************************************************************
	 * Check for errors reported by Tcl.
	 ************************************************************/
	if (tcl_rc != TCL_OK)
		throw_tcl_error(interp);

	/************************************************************
	 * The return value from the procedure might be one of
	 * the magic strings OK or SKIP or a list from array get.
	 * We can check for OK or SKIP without worrying about encoding.
	 ************************************************************/
	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish() failed");

	result = Tcl_GetStringResult(interp);

	if (strcmp(result, "OK") == 0)
		return rettup;
	if (strcmp(result, "SKIP") == 0)
		return (HeapTuple) NULL;

	/************************************************************
	 * Convert the result value from the Tcl interpreter
	 * and setup structures for SPI_modifytuple();
	 ************************************************************/
	if (Tcl_SplitList(interp, result,
					  &ret_numvals, &ret_values) != TCL_OK)
	{
		UTF_BEGIN;
		elog(ERROR, "could not split return value from trigger: %s",
			 UTF_U2E(Tcl_GetStringResult(interp)));
		UTF_END;
	}

	/* Use a TRY to ensure ret_values will get freed */
	PG_TRY();
	{
		if (ret_numvals % 2 != 0)
			elog(ERROR, "invalid return list from trigger - must have even # of elements");

		modattrs = (int *) palloc(tupdesc->natts * sizeof(int));
		modvalues = (Datum *) palloc(tupdesc->natts * sizeof(Datum));
		for (i = 0; i < tupdesc->natts; i++)
		{
			modattrs[i] = i + 1;
			modvalues[i] = (Datum) NULL;
		}

		modnulls = palloc(tupdesc->natts);
		memset(modnulls, 'n', tupdesc->natts);

		for (i = 0; i < ret_numvals; i += 2)
		{
			CONST84 char *ret_name = ret_values[i];
			CONST84 char *ret_value = ret_values[i + 1];
			int			attnum;
			HeapTuple	typeTup;
			Oid			typinput;
			Oid			typioparam;
			FmgrInfo	finfo;

			/************************************************************
			 * Ignore ".tupno" pseudo elements (see pltcl_set_tuple_values)
			 ************************************************************/
			if (strcmp(ret_name, ".tupno") == 0)
				continue;

			/************************************************************
			 * Get the attribute number
			 ************************************************************/
			attnum = SPI_fnumber(tupdesc, ret_name);
			if (attnum == SPI_ERROR_NOATTRIBUTE)
				elog(ERROR, "invalid attribute \"%s\"", ret_name);
			if (attnum <= 0)
				elog(ERROR, "cannot set system attribute \"%s\"", ret_name);

			/************************************************************
			 * Ignore dropped columns
			 ************************************************************/
			if (tupdesc->attrs[attnum - 1]->attisdropped)
				continue;

			/************************************************************
			 * Lookup the attribute type in the syscache
			 * for the input function
			 ************************************************************/
			typeTup = SearchSysCache(TYPEOID,
					  ObjectIdGetDatum(tupdesc->attrs[attnum - 1]->atttypid),
									 0, 0, 0);
			if (!HeapTupleIsValid(typeTup))
				elog(ERROR, "cache lookup failed for type %u",
					 tupdesc->attrs[attnum - 1]->atttypid);
			typinput = ((Form_pg_type) GETSTRUCT(typeTup))->typinput;
			typioparam = getTypeIOParam(typeTup);
			ReleaseSysCache(typeTup);

			/************************************************************
			 * Set the attribute to NOT NULL and convert the contents
			 ************************************************************/
			modnulls[attnum - 1] = ' ';
			fmgr_info(typinput, &finfo);
			UTF_BEGIN;
			modvalues[attnum - 1] = InputFunctionCall(&finfo,
												 (char *) UTF_U2E(ret_value),
													  typioparam,
									  tupdesc->attrs[attnum - 1]->atttypmod);
			UTF_END;
		}

		rettup = SPI_modifytuple(trigdata->tg_relation, rettup, tupdesc->natts,
								 modattrs, modvalues, modnulls);

		pfree(modattrs);
		pfree(modvalues);
		pfree(modnulls);

		if (rettup == NULL)
			elog(ERROR, "SPI_modifytuple() failed - RC = %d", SPI_result);

	}
	PG_CATCH();
	{
		ckfree((char *) ret_values);
		PG_RE_THROW();
	}
	PG_END_TRY();
	ckfree((char *) ret_values);

	return rettup;
}

/**********************************************************************
 * throw_tcl_error	- ereport an error returned from the Tcl interpreter
 **********************************************************************/
static void
throw_tcl_error(Tcl_Interp *interp)
{
	/*
	 * Caution is needed here because Tcl_GetVar could overwrite the
	 * interpreter result (even though it's not really supposed to),
	 * and we can't control the order of evaluation of ereport arguments.
	 * Hence, make real sure we have our own copy of the result string
	 * before invoking Tcl_GetVar.
	 */
	char	   *emsg;
	char	   *econtext;

	UTF_BEGIN;
	emsg = pstrdup(UTF_U2E(Tcl_GetStringResult(interp)));
	UTF_END;
	UTF_BEGIN;
	econtext = UTF_U2E((char *) Tcl_GetVar(interp, "errorInfo",
										   TCL_GLOBAL_ONLY));
	ereport(ERROR,
			(errmsg("%s", emsg),
			 errcontext("%s", econtext)));
	UTF_END;
}

/**********************************************************************
 * compile_pltcl_function	- compile (or hopefully just look up) function
 *
 * tgreloid is the OID of the relation when compiling a trigger, or zero
 * (InvalidOid) when compiling a plain function.
 **********************************************************************/
static pltcl_proc_desc *
compile_pltcl_function(Oid fn_oid, Oid tgreloid)
{
	bool		is_trigger = OidIsValid(tgreloid);
	HeapTuple	procTup;
	Form_pg_proc procStruct;
	char		internal_proname[128];
	Tcl_HashEntry *hashent;
	pltcl_proc_desc *prodesc = NULL;
	Tcl_Interp *interp;
	int			i;
	int			hashnew;
	int			tcl_rc;

	/* We'll need the pg_proc tuple in any case... */
	procTup = SearchSysCache(PROCOID,
							 ObjectIdGetDatum(fn_oid),
							 0, 0, 0);
	if (!HeapTupleIsValid(procTup))
		elog(ERROR, "cache lookup failed for function %u", fn_oid);
	procStruct = (Form_pg_proc) GETSTRUCT(procTup);

	/************************************************************
	 * Build our internal proc name from the functions Oid
	 ************************************************************/
	if (!is_trigger)
		snprintf(internal_proname, sizeof(internal_proname),
				 "__PLTcl_proc_%u", fn_oid);
	else
		snprintf(internal_proname, sizeof(internal_proname),
				 "__PLTcl_proc_%u_trigger_%u", fn_oid, tgreloid);

	/************************************************************
	 * Lookup the internal proc name in the hashtable
	 ************************************************************/
	hashent = Tcl_FindHashEntry(pltcl_proc_hash, internal_proname);

	/************************************************************
	 * If it's present, must check whether it's still up to date.
	 * This is needed because CREATE OR REPLACE FUNCTION can modify the
	 * function's pg_proc entry without changing its OID.
	 ************************************************************/
	if (hashent != NULL)
	{
		bool		uptodate;

		prodesc = (pltcl_proc_desc *) Tcl_GetHashValue(hashent);

		uptodate = (prodesc->fn_xmin == HeapTupleHeaderGetXmin(procTup->t_data) &&
				ItemPointerEquals(&prodesc->fn_tid, &procTup->t_self));

		if (!uptodate)
		{
			Tcl_DeleteHashEntry(hashent);
			hashent = NULL;
		}
	}

	/************************************************************
	 * If we haven't found it in the hashtable, we analyze
	 * the functions arguments and returntype and store
	 * the in-/out-functions in the prodesc block and create
	 * a new hashtable entry for it.
	 *
	 * Then we load the procedure into the Tcl interpreter.
	 ************************************************************/
	if (hashent == NULL)
	{
		HeapTuple	langTup;
		HeapTuple	typeTup;
		Form_pg_language langStruct;
		Form_pg_type typeStruct;
		Tcl_DString proc_internal_def;
		Tcl_DString proc_internal_body;
		char		proc_internal_args[33 * FUNC_MAX_ARGS];
		Datum		prosrcdatum;
		bool		isnull;
		char	   *proc_source;
		char		buf[32];

		/************************************************************
		 * Allocate a new procedure description block
		 ************************************************************/
		prodesc = (pltcl_proc_desc *) malloc(sizeof(pltcl_proc_desc));
		if (prodesc == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_OUT_OF_MEMORY),
					 errmsg("out of memory")));
		MemSet(prodesc, 0, sizeof(pltcl_proc_desc));
		prodesc->proname = strdup(internal_proname);
		prodesc->fn_xmin = HeapTupleHeaderGetXmin(procTup->t_data);
		prodesc->fn_tid = procTup->t_self;

		/* Remember if function is STABLE/IMMUTABLE */
		prodesc->fn_readonly =
			(procStruct->provolatile != PROVOLATILE_VOLATILE);

		/************************************************************
		 * Lookup the pg_language tuple by Oid
		 ************************************************************/
		langTup = SearchSysCache(LANGOID,
								 ObjectIdGetDatum(procStruct->prolang),
								 0, 0, 0);
		if (!HeapTupleIsValid(langTup))
		{
			free(prodesc->proname);
			free(prodesc);
			elog(ERROR, "cache lookup failed for language %u",
				 procStruct->prolang);
		}
		langStruct = (Form_pg_language) GETSTRUCT(langTup);
		prodesc->lanpltrusted = langStruct->lanpltrusted;
		ReleaseSysCache(langTup);

		if (prodesc->lanpltrusted)
			interp = pltcl_safe_interp;
		else
			interp = pltcl_norm_interp;

		/************************************************************
		 * Get the required information for input conversion of the
		 * return value.
		 ************************************************************/
		if (!is_trigger)
		{
			typeTup = SearchSysCache(TYPEOID,
									 ObjectIdGetDatum(procStruct->prorettype),
									 0, 0, 0);
			if (!HeapTupleIsValid(typeTup))
			{
				free(prodesc->proname);
				free(prodesc);
				elog(ERROR, "cache lookup failed for type %u",
					 procStruct->prorettype);
			}
			typeStruct = (Form_pg_type) GETSTRUCT(typeTup);

			/* Disallow pseudotype result, except VOID */
			if (typeStruct->typtype == 'p')
			{
				if (procStruct->prorettype == VOIDOID)
					 /* okay */ ;
				else if (procStruct->prorettype == TRIGGEROID)
				{
					free(prodesc->proname);
					free(prodesc);
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("trigger functions may only be called as triggers")));
				}
				else
				{
					free(prodesc->proname);
					free(prodesc);
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("pltcl functions cannot return type %s",
									format_type_be(procStruct->prorettype))));
				}
			}

			if (typeStruct->typtype == 'c')
			{
				free(prodesc->proname);
				free(prodesc);
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("pltcl functions cannot return tuples yet")));
			}

			perm_fmgr_info(typeStruct->typinput, &(prodesc->result_in_func));
			prodesc->result_typioparam = getTypeIOParam(typeTup);

			ReleaseSysCache(typeTup);
		}

		/************************************************************
		 * Get the required information for output conversion
		 * of all procedure arguments
		 ************************************************************/
		if (!is_trigger)
		{
			prodesc->nargs = procStruct->pronargs;
			proc_internal_args[0] = '\0';
			for (i = 0; i < prodesc->nargs; i++)
			{
				typeTup = SearchSysCache(TYPEOID,
						 ObjectIdGetDatum(procStruct->proargtypes.values[i]),
										 0, 0, 0);
				if (!HeapTupleIsValid(typeTup))
				{
					free(prodesc->proname);
					free(prodesc);
					elog(ERROR, "cache lookup failed for type %u",
						 procStruct->proargtypes.values[i]);
				}
				typeStruct = (Form_pg_type) GETSTRUCT(typeTup);

				/* Disallow pseudotype argument */
				if (typeStruct->typtype == 'p')
				{
					free(prodesc->proname);
					free(prodesc);
					ereport(ERROR,
							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
							 errmsg("pltcl functions cannot take type %s",
						format_type_be(procStruct->proargtypes.values[i]))));
				}

				if (typeStruct->typtype == 'c')
				{
					prodesc->arg_is_rowtype[i] = true;
					snprintf(buf, sizeof(buf), "__PLTcl_Tup_%d", i + 1);
				}
				else
				{
					prodesc->arg_is_rowtype[i] = false;
					perm_fmgr_info(typeStruct->typoutput,
								   &(prodesc->arg_out_func[i]));
					snprintf(buf, sizeof(buf), "%d", i + 1);
				}

				if (i > 0)
					strcat(proc_internal_args, " ");
				strcat(proc_internal_args, buf);

				ReleaseSysCache(typeTup);
			}
		}
		else
		{
			/* trigger procedure has fixed args */
			strcpy(proc_internal_args,
				   "TG_name TG_relid TG_table_name TG_table_schema TG_relatts TG_when TG_level TG_op __PLTcl_Tup_NEW __PLTcl_Tup_OLD args");
		}

		/************************************************************
		 * Create the tcl command to define the internal
		 * procedure
		 ************************************************************/
		Tcl_DStringInit(&proc_internal_def);
		Tcl_DStringInit(&proc_internal_body);
		Tcl_DStringAppendElement(&proc_internal_def, "proc");
		Tcl_DStringAppendElement(&proc_internal_def, internal_proname);
		Tcl_DStringAppendElement(&proc_internal_def, proc_internal_args);

		/************************************************************
		 * prefix procedure body with
		 * upvar #0 <internal_procname> GD
		 * and with appropriate setting of arguments
		 ************************************************************/
		Tcl_DStringAppend(&proc_internal_body, "upvar #0 ", -1);
		Tcl_DStringAppend(&proc_internal_body, internal_proname, -1);
		Tcl_DStringAppend(&proc_internal_body, " GD\n", -1);
		if (!is_trigger)
		{
			for (i = 0; i < prodesc->nargs; i++)
			{
				if (prodesc->arg_is_rowtype[i])
				{
					snprintf(buf, sizeof(buf),
							 "array set %d $__PLTcl_Tup_%d\n",
							 i + 1, i + 1);
					Tcl_DStringAppend(&proc_internal_body, buf, -1);
				}
			}
		}
		else
		{
			Tcl_DStringAppend(&proc_internal_body,
							  "array set NEW $__PLTcl_Tup_NEW\n", -1);
			Tcl_DStringAppend(&proc_internal_body,
							  "array set OLD $__PLTcl_Tup_OLD\n", -1);

			Tcl_DStringAppend(&proc_internal_body,
							  "set i 0\n"
							  "set v 0\n"
							  "foreach v $args {\n"
							  "  incr i\n"
							  "  set $i $v\n"
							  "}\n"
							  "unset i v\n\n", -1);
		}

		/************************************************************
		 * Add user's function definition to proc body
		 ************************************************************/
		prosrcdatum = SysCacheGetAttr(PROCOID, procTup,
									  Anum_pg_proc_prosrc, &isnull);
		if (isnull)
			elog(ERROR, "null prosrc");
		proc_source = DatumGetCString(DirectFunctionCall1(textout,
														  prosrcdatum));
		UTF_BEGIN;
		Tcl_DStringAppend(&proc_internal_body, UTF_E2U(proc_source), -1);
		UTF_END;
		pfree(proc_source);
		Tcl_DStringAppendElement(&proc_internal_def,
								 Tcl_DStringValue(&proc_internal_body));
		Tcl_DStringFree(&proc_internal_body);

		/************************************************************
		 * Create the procedure in the interpreter
		 ************************************************************/
		tcl_rc = Tcl_GlobalEval(interp,
								Tcl_DStringValue(&proc_internal_def));
		Tcl_DStringFree(&proc_internal_def);
		if (tcl_rc != TCL_OK)
		{
			free(prodesc->proname);
			free(prodesc);
			UTF_BEGIN;
			elog(ERROR, "could not create internal procedure \"%s\": %s",
				 internal_proname, UTF_U2E(Tcl_GetStringResult(interp)));
			UTF_END;
		}

		/************************************************************
		 * Add the proc description block to the hashtable
		 ************************************************************/
		hashent = Tcl_CreateHashEntry(pltcl_proc_hash,
									  prodesc->proname, &hashnew);
		Tcl_SetHashValue(hashent, (ClientData) prodesc);
	}

	ReleaseSysCache(procTup);

	return prodesc;
}

/**********************************************************************
 * pltcl_elog()		- elog() support for PLTcl
 **********************************************************************/
static int
pltcl_elog(ClientData cdata, Tcl_Interp *interp,
		   int argc, CONST84 char *argv[])
{
	volatile int level;
	MemoryContext oldcontext;

	if (argc != 3)
	{
		Tcl_SetResult(interp, "syntax error - 'elog level msg'", TCL_STATIC);
		return TCL_ERROR;
	}

	if (strcmp(argv[1], "DEBUG") == 0)
		level = DEBUG2;
	else if (strcmp(argv[1], "LOG") == 0)
		level = LOG;
	else if (strcmp(argv[1], "INFO") == 0)
		level = INFO;
	else if (strcmp(argv[1], "NOTICE") == 0)
		level = NOTICE;
	else if (strcmp(argv[1], "WARNING") == 0)
		level = WARNING;
	else if (strcmp(argv[1], "ERROR") == 0)
		level = ERROR;
	else if (strcmp(argv[1], "FATAL") == 0)
		level = FATAL;
	else
	{
		Tcl_AppendResult(interp, "Unknown elog level '", argv[1],
						 "'", NULL);
		return TCL_ERROR;
	}

	if (level == ERROR)
	{
		/*
		 * We just pass the error back to Tcl.  If it's not caught,
		 * it'll eventually get converted to a PG error when we reach
		 * the call handler.
		 */
		Tcl_SetResult(interp, (char *) argv[2], TCL_VOLATILE);
		return TCL_ERROR;
	}

	/*
	 * For non-error messages, just pass 'em to elog().  We do not expect
	 * that this will fail, but just on the off chance it does, report the
	 * error back to Tcl.  Note we are assuming that elog() can't have any
	 * internal failures that are so bad as to require a transaction abort.
	 *
	 * This path is also used for FATAL errors, which aren't going to come
	 * back to us at all.
	 */
	oldcontext = CurrentMemoryContext;
	PG_TRY();
	{
		UTF_BEGIN;
		elog(level, "%s", UTF_U2E(argv[2]));
		UTF_END;
	}
	PG_CATCH();
	{
		ErrorData  *edata;

		/* Must reset elog.c's state */
		MemoryContextSwitchTo(oldcontext);
		edata = CopyErrorData();
		FlushErrorState();

		/* Pass the error message to Tcl */
		UTF_BEGIN;
		Tcl_SetResult(interp, UTF_E2U(edata->message), TCL_VOLATILE);
		UTF_END;
		FreeErrorData(edata);

		return TCL_ERROR;
	}
	PG_END_TRY();

	return TCL_OK;
}

/**********************************************************************
 * pltcl_quote()	- quote literal strings that are to
 *			  be used in SPI_execute query strings
 **********************************************************************/
static int
pltcl_quote(ClientData cdata, Tcl_Interp *interp,
			int argc, CONST84 char *argv[])
{
	char	   *tmp;
	const char *cp1;
	char	   *cp2;

	/************************************************************
	 * Check call syntax
	 ************************************************************/
	if (argc != 2)
	{
		Tcl_SetResult(interp, "syntax error - 'quote string'", TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Allocate space for the maximum the string can
	 * grow to and initialize pointers
	 ************************************************************/
	tmp = palloc(strlen(argv[1]) * 2 + 1);
	cp1 = argv[1];
	cp2 = tmp;

	/************************************************************
	 * Walk through string and double every quote and backslash
	 ************************************************************/
	while (*cp1)
	{
		if (*cp1 == '\'')
			*cp2++ = '\'';
		else
		{
			if (*cp1 == '\\')
				*cp2++ = '\\';
		}
		*cp2++ = *cp1++;
	}

	/************************************************************
	 * Terminate the string and set it as result
	 ************************************************************/
	*cp2 = '\0';
	Tcl_SetResult(interp, tmp, TCL_VOLATILE);
	pfree(tmp);
	return TCL_OK;
}

/**********************************************************************
 * pltcl_argisnull()	- determine if a specific argument is NULL
 **********************************************************************/
static int
pltcl_argisnull(ClientData cdata, Tcl_Interp *interp,
				int argc, CONST84 char *argv[])
{
	int			argno;
	FunctionCallInfo fcinfo = pltcl_current_fcinfo;

	/************************************************************
	 * Check call syntax
	 ************************************************************/
	if (argc != 2)
	{
		Tcl_SetResult(interp, "syntax error - 'argisnull argno'",
					  TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Check that we're called as a normal function
	 ************************************************************/
	if (fcinfo == NULL)
	{
		Tcl_SetResult(interp, "argisnull cannot be used in triggers",
					  TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Get the argument number
	 ************************************************************/
	if (Tcl_GetInt(interp, argv[1], &argno) != TCL_OK)
		return TCL_ERROR;

	/************************************************************
	 * Check that the argno is valid
	 ************************************************************/
	argno--;
	if (argno < 0 || argno >= fcinfo->nargs)
	{
		Tcl_SetResult(interp, "argno out of range", TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Get the requested NULL state
	 ************************************************************/
	if (PG_ARGISNULL(argno))
		Tcl_SetResult(interp, "1", TCL_STATIC);
	else
		Tcl_SetResult(interp, "0", TCL_STATIC);

	return TCL_OK;
}

/**********************************************************************
 * pltcl_returnnull()	- Cause a NULL return from a function
 **********************************************************************/
static int
pltcl_returnnull(ClientData cdata, Tcl_Interp *interp,
				 int argc, CONST84 char *argv[])
{
	FunctionCallInfo fcinfo = pltcl_current_fcinfo;

	/************************************************************
	 * Check call syntax
	 ************************************************************/
	if (argc != 1)
	{
		Tcl_SetResult(interp, "syntax error - 'return_null'", TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Check that we're called as a normal function
	 ************************************************************/
	if (fcinfo == NULL)
	{
		Tcl_SetResult(interp, "return_null cannot be used in triggers",
					  TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Set the NULL return flag and cause Tcl to return from the
	 * procedure.
	 ************************************************************/
	fcinfo->isnull = true;

	return TCL_RETURN;
}

/*----------
 * Support for running SPI operations inside subtransactions
 *
 * Intended usage pattern is:
 *
 *	MemoryContext oldcontext = CurrentMemoryContext;
 *	ResourceOwner oldowner = CurrentResourceOwner;
 *
 *	...
 *	pltcl_subtrans_begin(oldcontext, oldowner);
 *	PG_TRY();
 *	{
 *		do something risky;
 *		pltcl_subtrans_commit(oldcontext, oldowner);
 *	}
 *	PG_CATCH();
 *	{
 *		pltcl_subtrans_abort(interp, oldcontext, oldowner);
 *		return TCL_ERROR;
 *	}
 *	PG_END_TRY();
 *	return TCL_OK;
 *----------
 */
static void
pltcl_subtrans_begin(MemoryContext oldcontext, ResourceOwner oldowner)
{
	BeginInternalSubTransaction(NULL);

	/* Want to run inside function's memory context */
	MemoryContextSwitchTo(oldcontext);
}

static void
pltcl_subtrans_commit(MemoryContext oldcontext, ResourceOwner oldowner)
{
	/* Commit the inner transaction, return to outer xact context */
	ReleaseCurrentSubTransaction();
	MemoryContextSwitchTo(oldcontext);
	CurrentResourceOwner = oldowner;

	/*
	 * AtEOSubXact_SPI() should not have popped any SPI context, but just in
	 * case it did, make sure we remain connected.
	 */
	SPI_restore_connection();
}

static void
pltcl_subtrans_abort(Tcl_Interp *interp,
					 MemoryContext oldcontext, ResourceOwner oldowner)
{
	ErrorData  *edata;

	/* Save error info */
	MemoryContextSwitchTo(oldcontext);
	edata = CopyErrorData();
	FlushErrorState();

	/* Abort the inner transaction */
	RollbackAndReleaseCurrentSubTransaction();
	MemoryContextSwitchTo(oldcontext);
	CurrentResourceOwner = oldowner;

	/*
	 * If AtEOSubXact_SPI() popped any SPI context of the subxact, it will
	 * have left us in a disconnected state.  We need this hack to return to
	 * connected state.
	 */
	SPI_restore_connection();

	/* Pass the error message to Tcl */
	UTF_BEGIN;
	Tcl_SetResult(interp, UTF_E2U(edata->message), TCL_VOLATILE);
	UTF_END;
	FreeErrorData(edata);
}

/**********************************************************************
 * pltcl_SPI_execute()		- The builtin SPI_execute command
 *				  for the Tcl interpreter
 **********************************************************************/
static int
pltcl_SPI_execute(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[])
{
	int			my_rc;
	int			spi_rc;
	int			query_idx;
	int			i;
	int			count = 0;
	CONST84 char *volatile arrayname = NULL;
	CONST84 char *volatile loop_body = NULL;
	MemoryContext oldcontext = CurrentMemoryContext;
	ResourceOwner oldowner = CurrentResourceOwner;

	char	   *usage = "syntax error - 'SPI_exec "
	"?-count n? "
	"?-array name? query ?loop body?";

	/************************************************************
	 * Check the call syntax and get the options
	 ************************************************************/
	if (argc < 2)
	{
		Tcl_SetResult(interp, usage, TCL_STATIC);
		return TCL_ERROR;
	}

	i = 1;
	while (i < argc)
	{
		if (strcmp(argv[i], "-array") == 0)
		{
			if (++i >= argc)
			{
				Tcl_SetResult(interp, usage, TCL_STATIC);
				return TCL_ERROR;
			}
			arrayname = argv[i++];
			continue;
		}

		if (strcmp(argv[i], "-count") == 0)
		{
			if (++i >= argc)
			{
				Tcl_SetResult(interp, usage, TCL_STATIC);
				return TCL_ERROR;
			}
			if (Tcl_GetInt(interp, argv[i++], &count) != TCL_OK)
				return TCL_ERROR;
			continue;
		}

		break;
	}

	query_idx = i;
	if (query_idx >= argc || query_idx + 2 < argc)
	{
		Tcl_SetResult(interp, usage, TCL_STATIC);
		return TCL_ERROR;
	}
	if (query_idx + 1 < argc)
		loop_body = argv[query_idx + 1];

	/************************************************************
	 * Execute the query inside a sub-transaction, so we can cope with
	 * errors sanely
	 ************************************************************/

	pltcl_subtrans_begin(oldcontext, oldowner);

	PG_TRY();
	{
		UTF_BEGIN;
		spi_rc = SPI_execute(UTF_U2E(argv[query_idx]),
							 pltcl_current_prodesc->fn_readonly, count);
		UTF_END;

		my_rc = pltcl_process_SPI_result(interp,
										 arrayname,
										 loop_body,
										 spi_rc,
										 SPI_tuptable,
										 SPI_processed);

		pltcl_subtrans_commit(oldcontext, oldowner);
	}
	PG_CATCH();
	{
		pltcl_subtrans_abort(interp, oldcontext, oldowner);
		return TCL_ERROR;
	}
	PG_END_TRY();

	return my_rc;
}

/*
 * Process the result from SPI_execute or SPI_execute_plan
 *
 * Shared code between pltcl_SPI_execute and pltcl_SPI_execute_plan
 */
static int
pltcl_process_SPI_result(Tcl_Interp *interp,
						 CONST84 char *arrayname,
						 CONST84 char *loop_body,
						 int spi_rc,
						 SPITupleTable *tuptable,
						 int ntuples)
{
	int			my_rc = TCL_OK;
	char		buf[64];
	int			i;
	int			loop_rc;
	HeapTuple  *tuples;
	TupleDesc	tupdesc;

	switch (spi_rc)
	{
		case SPI_OK_SELINTO:
		case SPI_OK_INSERT:
		case SPI_OK_DELETE:
		case SPI_OK_UPDATE:
			snprintf(buf, sizeof(buf), "%d", ntuples);
			Tcl_SetResult(interp, buf, TCL_VOLATILE);
			break;

		case SPI_OK_UTILITY:
			if (tuptable == NULL)
			{
				Tcl_SetResult(interp, "0", TCL_STATIC);
				break;
			}
			/* FALL THRU for utility returning tuples */

		case SPI_OK_SELECT:
		case SPI_OK_INSERT_RETURNING:
		case SPI_OK_DELETE_RETURNING:
		case SPI_OK_UPDATE_RETURNING:

			/*
			 * Process the tuples we got
			 */
			tuples = tuptable->vals;
			tupdesc = tuptable->tupdesc;

			if (loop_body == NULL)
			{
				/*
				 * If there is no loop body given, just set the variables from
				 * the first tuple (if any)
				 */
				if (ntuples > 0)
					pltcl_set_tuple_values(interp, arrayname, 0,
										   tuples[0], tupdesc);
			}
			else
			{
				/*
				 * There is a loop body - process all tuples and evaluate the
				 * body on each
				 */
				for (i = 0; i < ntuples; i++)
				{
					pltcl_set_tuple_values(interp, arrayname, i,
										   tuples[i], tupdesc);

					loop_rc = Tcl_Eval(interp, loop_body);

					if (loop_rc == TCL_OK)
						continue;
					if (loop_rc == TCL_CONTINUE)
						continue;
					if (loop_rc == TCL_RETURN)
					{
						my_rc = TCL_RETURN;
						break;
					}
					if (loop_rc == TCL_BREAK)
						break;
					my_rc = TCL_ERROR;
					break;
				}
			}

			if (my_rc == TCL_OK)
			{
				snprintf(buf, sizeof(buf), "%d", ntuples);
				Tcl_SetResult(interp, buf, TCL_VOLATILE);
			}
			break;

		default:
			Tcl_AppendResult(interp, "pltcl: SPI_execute failed: ",
							 SPI_result_code_string(spi_rc), NULL);
			my_rc = TCL_ERROR;
			break;
	}

	SPI_freetuptable(tuptable);

	return my_rc;
}

/**********************************************************************
 * pltcl_SPI_prepare()		- Builtin support for prepared plans
 *				  The Tcl command SPI_prepare
 *				  always saves the plan using
 *				  SPI_saveplan and returns a key for
 *				  access. There is no chance to prepare
 *				  and not save the plan currently.
 **********************************************************************/
static int
pltcl_SPI_prepare(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[])
{
	int			nargs;
	CONST84 char **args;
	pltcl_query_desc *qdesc;
	void	   *plan;
	int			i;
	Tcl_HashEntry *hashent;
	int			hashnew;
	Tcl_HashTable *query_hash;
	MemoryContext oldcontext = CurrentMemoryContext;
	ResourceOwner oldowner = CurrentResourceOwner;

	/************************************************************
	 * Check the call syntax
	 ************************************************************/
	if (argc != 3)
	{
		Tcl_SetResult(interp, "syntax error - 'SPI_prepare query argtypes'",
					  TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Split the argument type list
	 ************************************************************/
	if (Tcl_SplitList(interp, argv[2], &nargs, &args) != TCL_OK)
		return TCL_ERROR;

	/************************************************************
	 * Allocate the new querydesc structure
	 ************************************************************/
	qdesc = (pltcl_query_desc *) malloc(sizeof(pltcl_query_desc));
	snprintf(qdesc->qname, sizeof(qdesc->qname), "%lx", (long) qdesc);
	qdesc->nargs = nargs;
	qdesc->argtypes = (Oid *) malloc(nargs * sizeof(Oid));
	qdesc->arginfuncs = (FmgrInfo *) malloc(nargs * sizeof(FmgrInfo));
	qdesc->argtypioparams = (Oid *) malloc(nargs * sizeof(Oid));

	/************************************************************
	 * Execute the prepare inside a sub-transaction, so we can cope with
	 * errors sanely
	 ************************************************************/

	pltcl_subtrans_begin(oldcontext, oldowner);

	PG_TRY();
	{
		/************************************************************
		 * Lookup the argument types by name in the system cache
		 * and remember the required information for input conversion
		 ************************************************************/
		for (i = 0; i < nargs; i++)
		{
			List	   *names;
			HeapTuple	typeTup;

			/* Parse possibly-qualified type name and look it up in pg_type */
			names = stringToQualifiedNameList(args[i],
											  "pltcl_SPI_prepare");
			typeTup = typenameType(NULL, makeTypeNameFromNameList(names));
			qdesc->argtypes[i] = HeapTupleGetOid(typeTup);
			perm_fmgr_info(((Form_pg_type) GETSTRUCT(typeTup))->typinput,
						   &(qdesc->arginfuncs[i]));
			qdesc->argtypioparams[i] = getTypeIOParam(typeTup);
			ReleaseSysCache(typeTup);
		}

		/************************************************************
		 * Prepare the plan and check for errors
		 ************************************************************/
		UTF_BEGIN;
		plan = SPI_prepare(UTF_U2E(argv[1]), nargs, qdesc->argtypes);
		UTF_END;

		if (plan == NULL)
			elog(ERROR, "SPI_prepare() failed");

		/************************************************************
		 * Save the plan into permanent memory (right now it's in the
		 * SPI procCxt, which will go away at function end).
		 ************************************************************/
		qdesc->plan = SPI_saveplan(plan);
		if (qdesc->plan == NULL)
			elog(ERROR, "SPI_saveplan() failed");

		/* Release the procCxt copy to avoid within-function memory leak */
		SPI_freeplan(plan);

		pltcl_subtrans_commit(oldcontext, oldowner);
	}
	PG_CATCH();
	{
		pltcl_subtrans_abort(interp, oldcontext, oldowner);

		free(qdesc->argtypes);
		free(qdesc->arginfuncs);
		free(qdesc->argtypioparams);
		free(qdesc);
		ckfree((char *) args);

		return TCL_ERROR;
	}
	PG_END_TRY();

	/************************************************************
	 * Insert a hashtable entry for the plan and return
	 * the key to the caller
	 ************************************************************/
	if (interp == pltcl_norm_interp)
		query_hash = pltcl_norm_query_hash;
	else
		query_hash = pltcl_safe_query_hash;

	hashent = Tcl_CreateHashEntry(query_hash, qdesc->qname, &hashnew);
	Tcl_SetHashValue(hashent, (ClientData) qdesc);

	ckfree((char *) args);

	/* qname is ASCII, so no need for encoding conversion */
	Tcl_SetResult(interp, qdesc->qname, TCL_VOLATILE);
	return TCL_OK;
}

/**********************************************************************
 * pltcl_SPI_execute_plan()		- Execute a prepared plan
 **********************************************************************/
static int
pltcl_SPI_execute_plan(ClientData cdata, Tcl_Interp *interp,
					   int argc, CONST84 char *argv[])
{
	int			my_rc;
	int			spi_rc;
	int			i;
	int			j;
	Tcl_HashEntry *hashent;
	pltcl_query_desc *qdesc;
	const char *volatile nulls = NULL;
	CONST84 char *volatile arrayname = NULL;
	CONST84 char *volatile loop_body = NULL;
	int			count = 0;
	int			callnargs;
	CONST84 char **callargs = NULL;
	Datum	   *argvalues;
	MemoryContext oldcontext = CurrentMemoryContext;
	ResourceOwner oldowner = CurrentResourceOwner;
	Tcl_HashTable *query_hash;

	char	   *usage = "syntax error - 'SPI_execp "
	"?-nulls string? ?-count n? "
	"?-array name? query ?args? ?loop body?";

	/************************************************************
	 * Get the options and check syntax
	 ************************************************************/
	i = 1;
	while (i < argc)
	{
		if (strcmp(argv[i], "-array") == 0)
		{
			if (++i >= argc)
			{
				Tcl_SetResult(interp, usage, TCL_STATIC);
				return TCL_ERROR;
			}
			arrayname = argv[i++];
			continue;
		}
		if (strcmp(argv[i], "-nulls") == 0)
		{
			if (++i >= argc)
			{
				Tcl_SetResult(interp, usage, TCL_STATIC);
				return TCL_ERROR;
			}
			nulls = argv[i++];
			continue;
		}
		if (strcmp(argv[i], "-count") == 0)
		{
			if (++i >= argc)
			{
				Tcl_SetResult(interp, usage, TCL_STATIC);
				return TCL_ERROR;
			}
			if (Tcl_GetInt(interp, argv[i++], &count) != TCL_OK)
				return TCL_ERROR;
			continue;
		}

		break;
	}

	/************************************************************
	 * Get the prepared plan descriptor by its key
	 ************************************************************/
	if (i >= argc)
	{
		Tcl_SetResult(interp, usage, TCL_STATIC);
		return TCL_ERROR;
	}

	if (interp == pltcl_norm_interp)
		query_hash = pltcl_norm_query_hash;
	else
		query_hash = pltcl_safe_query_hash;

	hashent = Tcl_FindHashEntry(query_hash, argv[i]);
	if (hashent == NULL)
	{
		Tcl_AppendResult(interp, "invalid queryid '", argv[i], "'", NULL);
		return TCL_ERROR;
	}
	qdesc = (pltcl_query_desc *) Tcl_GetHashValue(hashent);
	i++;

	/************************************************************
	 * If a nulls string is given, check for correct length
	 ************************************************************/
	if (nulls != NULL)
	{
		if (strlen(nulls) != qdesc->nargs)
		{
			Tcl_SetResult(interp,
					   "length of nulls string doesn't match # of arguments",
						  TCL_STATIC);
			return TCL_ERROR;
		}
	}

	/************************************************************
	 * If there was a argtype list on preparation, we need
	 * an argument value list now
	 ************************************************************/
	if (qdesc->nargs > 0)
	{
		if (i >= argc)
		{
			Tcl_SetResult(interp, "missing argument list", TCL_STATIC);
			return TCL_ERROR;
		}

		/************************************************************
		 * Split the argument values
		 ************************************************************/
		if (Tcl_SplitList(interp, argv[i++], &callnargs, &callargs) != TCL_OK)
			return TCL_ERROR;

		/************************************************************
		 * Check that the # of arguments matches
		 ************************************************************/
		if (callnargs != qdesc->nargs)
		{
			Tcl_SetResult(interp,
			   "argument list length doesn't match # of arguments for query",
						  TCL_STATIC);
			ckfree((char *) callargs);
			return TCL_ERROR;
		}
	}
	else
		callnargs = 0;

	/************************************************************
	 * Get loop body if present
	 ************************************************************/
	if (i < argc)
		loop_body = argv[i++];

	if (i != argc)
	{
		Tcl_SetResult(interp, usage, TCL_STATIC);
		return TCL_ERROR;
	}

	/************************************************************
	 * Execute the plan inside a sub-transaction, so we can cope with
	 * errors sanely
	 ************************************************************/

	pltcl_subtrans_begin(oldcontext, oldowner);

	PG_TRY();
	{
		/************************************************************
		 * Setup the value array for SPI_execute_plan() using
		 * the type specific input functions
		 ************************************************************/
		argvalues = (Datum *) palloc(callnargs * sizeof(Datum));

		for (j = 0; j < callnargs; j++)
		{
			if (nulls && nulls[j] == 'n')
			{
				argvalues[j] = InputFunctionCall(&qdesc->arginfuncs[j],
												 NULL,
												 qdesc->argtypioparams[j],
												 -1);
			}
			else
			{
				UTF_BEGIN;
				argvalues[j] = InputFunctionCall(&qdesc->arginfuncs[j],
											   (char *) UTF_U2E(callargs[j]),
												 qdesc->argtypioparams[j],
												 -1);
				UTF_END;
			}
		}

		if (callargs)
			ckfree((char *) callargs);
		callargs = NULL;

		/************************************************************
		 * Execute the plan
		 ************************************************************/
		spi_rc = SPI_execute_plan(qdesc->plan, argvalues, nulls,
								  pltcl_current_prodesc->fn_readonly, count);

		my_rc = pltcl_process_SPI_result(interp,
										 arrayname,
										 loop_body,
										 spi_rc,
										 SPI_tuptable,
										 SPI_processed);

		pltcl_subtrans_commit(oldcontext, oldowner);
	}
	PG_CATCH();
	{
		pltcl_subtrans_abort(interp, oldcontext, oldowner);

		if (callargs)
			ckfree((char *) callargs);

		return TCL_ERROR;
	}
	PG_END_TRY();

	return my_rc;
}

/**********************************************************************
 * pltcl_SPI_lastoid()	- return the last oid. To
 *		  be used after insert queries
 **********************************************************************/
static int
pltcl_SPI_lastoid(ClientData cdata, Tcl_Interp *interp,
				  int argc, CONST84 char *argv[])
{
	char		buf[64];

	snprintf(buf, sizeof(buf), "%u", SPI_lastoid);
	Tcl_SetResult(interp, buf, TCL_VOLATILE);
	return TCL_OK;
}

/**********************************************************************
 * pltcl_set_tuple_values() - Set variables for all attributes
 *				  of a given tuple
 **********************************************************************/
static void
pltcl_set_tuple_values(Tcl_Interp *interp, CONST84 char *arrayname,
					   int tupno, HeapTuple tuple, TupleDesc tupdesc)
{
	int			i;
	char	   *outputstr;
	char		buf[64];
	Datum		attr;
	bool		isnull;

	CONST84 char *attname;
	HeapTuple	typeTup;
	Oid			typoutput;

	CONST84 char **arrptr;
	CONST84 char **nameptr;
	CONST84 char *nullname = NULL;

	/************************************************************
	 * Prepare pointers for Tcl_SetVar2() below and in array
	 * mode set the .tupno element
	 ************************************************************/
	if (arrayname == NULL)
	{
		arrptr = &attname;
		nameptr = &nullname;
	}
	else
	{
		arrptr = &arrayname;
		nameptr = &attname;
		snprintf(buf, sizeof(buf), "%d", tupno);
		Tcl_SetVar2(interp, arrayname, ".tupno", buf, 0);
	}

	for (i = 0; i < tupdesc->natts; i++)
	{
		/* ignore dropped attributes */
		if (tupdesc->attrs[i]->attisdropped)
			continue;

		/************************************************************
		 * Get the attribute name
		 ************************************************************/
		attname = NameStr(tupdesc->attrs[i]->attname);

		/************************************************************
		 * Get the attributes value
		 ************************************************************/
		attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);

		/************************************************************
		 * Lookup the attribute type in the syscache
		 * for the output function
		 ************************************************************/
		typeTup = SearchSysCache(TYPEOID,
							   ObjectIdGetDatum(tupdesc->attrs[i]->atttypid),
								 0, 0, 0);
		if (!HeapTupleIsValid(typeTup))
			elog(ERROR, "cache lookup failed for type %u",
				 tupdesc->attrs[i]->atttypid);

		typoutput = ((Form_pg_type) GETSTRUCT(typeTup))->typoutput;
		ReleaseSysCache(typeTup);

		/************************************************************
		 * If there is a value, set the variable
		 * If not, unset it
		 *
		 * Hmmm - Null attributes will cause functions to
		 *		  crash if they don't expect them - need something
		 *		  smarter here.
		 ************************************************************/
		if (!isnull && OidIsValid(typoutput))
		{
			outputstr = OidOutputFunctionCall(typoutput, attr);
			UTF_BEGIN;
			Tcl_SetVar2(interp, *arrptr, *nameptr, UTF_E2U(outputstr), 0);
			UTF_END;
			pfree(outputstr);
		}
		else
			Tcl_UnsetVar2(interp, *arrptr, *nameptr, 0);
	}
}

/**********************************************************************
 * pltcl_build_tuple_argument() - Build a string usable for 'array set'
 *				  from all attributes of a given tuple
 **********************************************************************/
static void
pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc,
						   Tcl_DString *retval)
{
	int			i;
	char	   *outputstr;
	Datum		attr;
	bool		isnull;

	char	   *attname;
	HeapTuple	typeTup;
	Oid			typoutput;

	for (i = 0; i < tupdesc->natts; i++)
	{
		/* ignore dropped attributes */
		if (tupdesc->attrs[i]->attisdropped)
			continue;

		/************************************************************
		 * Get the attribute name
		 ************************************************************/
		attname = NameStr(tupdesc->attrs[i]->attname);

		/************************************************************
		 * Get the attributes value
		 ************************************************************/
		attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);

		/************************************************************
		 * Lookup the attribute type in the syscache
		 * for the output function
		 ************************************************************/
		typeTup = SearchSysCache(TYPEOID,
							   ObjectIdGetDatum(tupdesc->attrs[i]->atttypid),
								 0, 0, 0);
		if (!HeapTupleIsValid(typeTup))
			elog(ERROR, "cache lookup failed for type %u",
				 tupdesc->attrs[i]->atttypid);

		typoutput = ((Form_pg_type) GETSTRUCT(typeTup))->typoutput;
		ReleaseSysCache(typeTup);

		/************************************************************
		 * If there is a value, append the attribute name and the
		 * value to the list
		 *
		 * Hmmm - Null attributes will cause functions to
		 *		  crash if they don't expect them - need something
		 *		  smarter here.
		 ************************************************************/
		if (!isnull && OidIsValid(typoutput))
		{
			outputstr = OidOutputFunctionCall(typoutput, attr);
			Tcl_DStringAppendElement(retval, attname);
			UTF_BEGIN;
			Tcl_DStringAppendElement(retval, UTF_E2U(outputstr));
			UTF_END;
			pfree(outputstr);
		}
	}
}


text:
import React, { RefObject } from 'react';
import cx from 'classnames';

interface HoverMenuProps {
  position: 'left' | 'top';
  innerRef: RefObject<HTMLDivElement>;
  children: React.ReactNode;
}

export default class HoverMenu extends React.PureComponent<HoverMenuProps> {
  static defaultProps = {
    position: 'left',
    innerRef: null,
    children: null,
  };

  render() {
    const { innerRef, position, children } = this.props;
    return (
      <div
        ref={innerRef}
        className={cx(
          'hover-menu',
          position === 'left' && 'hover-menu--left',
          position === 'top' && 'hover-menu--top',
        )}
      >
        {children}
      </div>
    );
  }
}

redpajama_set_name: RedPajamaGithub


text:
    Menu

      Enquire about your certificate order


text:
import { Sync } from "./sync"

export const sync = new Sync()


text:
package com.gemstone.gemfire.distributed;

import static org.junit.Assert.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;

import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.server.CacheServer;
import com.gemstone.gemfire.distributed.ServerLauncher.Builder;
import com.gemstone.gemfire.distributed.ServerLauncher.Command;
import com.gemstone.gemfire.distributed.internal.DistributionConfig;
import com.gemstone.gemfire.distributed.support.DistributedSystemAdapter;
import com.gemstone.gemfire.internal.i18n.LocalizedStrings;
import com.gemstone.gemfire.internal.shared.NativeCalls;
import com.gemstone.gemfire.internal.util.IOUtils;

import edu.umd.cs.mtc.MultithreadedTestCase;
import edu.umd.cs.mtc.TestFramework;

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * The ServerLauncherJUnitTest class is a test suite of unit tests testing the contract, functionality and invariants
 * of the ServerLauncher class.
 *
 * @author John Blum
 * @author Kirk Lund
 * @see com.gemstone.gemfire.distributed.CommonLauncherTestSuite
 * @see com.gemstone.gemfire.distributed.ServerLauncher
 * @see com.gemstone.gemfire.distributed.ServerLauncher.Builder
 * @see com.gemstone.gemfire.distributed.ServerLauncher.Command
 * @see org.junit.Assert
 * @see org.junit.Test
 * @since 7.0
 */
public class ServerLauncherJUnitTest extends CommonLauncherTestSuite {

  private static final String GEMFIRE_PROPERTIES_FILE_NAME = "gemfire.properties";
  private static final String TEMPORARY_FILE_NAME = "beforeServerLauncherJUnitTest_" + GEMFIRE_PROPERTIES_FILE_NAME;

  private Mockery mockContext;

  @BeforeClass
  public static void testSuiteSetup() {
    if (!NativeCalls.getInstance().getOSType().isWindows()) {
      File file = new File(GEMFIRE_PROPERTIES_FILE_NAME);
      if (file.exists()) {
        File dest = new File(TEMPORARY_FILE_NAME);
        assertTrue(file.renameTo(dest));
      }
    }
  }

  @AfterClass
  public static void testSuiteTearDown() {
    if (!NativeCalls.getInstance().getOSType().isWindows()) {
      File file = new File(TEMPORARY_FILE_NAME);
      if (file.exists()) {
        File dest = new File(GEMFIRE_PROPERTIES_FILE_NAME);
        assertTrue(file.renameTo(dest));
      }
    }
  }

  @Before
  public void setup() {
    mockContext = new Mockery() {{
      setImposteriser(ClassImposteriser.INSTANCE);
    }};
  }

  @After
  public void tearDown() {
    mockContext.assertIsSatisfied();
    mockContext = null;
  }

  @Test
  public void testParseArguments() throws Exception {
    final Builder builder = new Builder();

    builder.parseArguments("start", "serverOne", "--assign-buckets", "--disable-default-server", "--debug", "--force",
      "--rebalance", "--redirect-output", "--dir=" + ServerLauncher.DEFAULT_WORKING_DIRECTORY, "--pid=1234",
        "--server-bind-address=" + InetAddress.getLocalHost().getHostAddress(), "--server-port=11235");

    assertEquals(Command.START, builder.getCommand());
    assertEquals("serverOne", builder.getMemberName());
    assertTrue(builder.getAssignBuckets());
    assertTrue(builder.getDisableDefaultServer());
    assertTrue(builder.getDebug());
    assertTrue(builder.getForce());
    assertFalse(Boolean.TRUE.equals(builder.getHelp()));
    assertTrue(builder.getRebalance());
    assertTrue(builder.getRedirectOutput());
    assertEquals(ServerLauncher.DEFAULT_WORKING_DIRECTORY, builder.getWorkingDirectory());
    assertEquals(1234, builder.getPid().intValue());
    assertEquals(InetAddress.getLocalHost(), builder.getServerBindAddress());
    assertEquals(11235, builder.getServerPort().intValue());
  }

  @Test
  public void testParseCommand() {
    final Builder builder = new Builder();

    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());

    builder.parseCommand((String[]) null);

    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());

    builder.parseCommand(); // empty String array

    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());

    builder.parseCommand(Command.START.getName());

    assertEquals(Command.START, builder.getCommand());

    builder.parseCommand("Status");

    assertEquals(Command.STATUS, builder.getCommand());

    builder.parseCommand("sToP");

    assertEquals(Command.STOP, builder.getCommand());

    builder.parseCommand("--opt", "START", "-o", Command.STATUS.getName());

    assertEquals(Command.START, builder.getCommand());

    builder.setCommand(null);
    builder.parseCommand("badCommandName", "--start", "stat");

    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());
  }

  @Test
  public void testParseMemberName() {
    final Builder builder = new Builder();

    assertNull(builder.getMemberName());

    builder.parseMemberName((String[]) null);

    assertNull(builder.getMemberName());

    builder.parseMemberName(); // empty String array

    assertNull(builder.getMemberName());

    builder.parseMemberName(Command.START.getName(), "--opt", "-o");

    assertNull(builder.getMemberName());

    builder.parseMemberName("memberOne");

    assertEquals("memberOne", builder.getMemberName());
  }

  @Test
  public void testSetAndGetCommand() {
    final Builder builder = new Builder();

    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());
    assertSame(builder, builder.setCommand(Command.STATUS));
    assertEquals(Command.STATUS, builder.getCommand());
    assertSame(builder, builder.setCommand(null));
    assertEquals(Builder.DEFAULT_COMMAND, builder.getCommand());
  }

  @Test
  public void testSetAndGetMemberName() {
    final Builder builder = new Builder();

    assertNull(builder.getMemberName());
    assertSame(builder, builder.setMemberName("serverOne"));
    assertEquals("serverOne", builder.getMemberName());
    assertSame(builder, builder.setMemberName(null));
    assertNull(builder.getMemberName());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetMemberNameToBlankString() {
    try {
      new Builder().setMemberName("  ");
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_MEMBER_NAME_ERROR_MESSAGE.toLocalizedString("Server"),
        expected.getMessage());
      throw expected;
    }
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetMemberNameToEmptyString() {
    try {
      new Builder().setMemberName("");
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_MEMBER_NAME_ERROR_MESSAGE.toLocalizedString("Server"),
        expected.getMessage());
      throw expected;
    }
  }

  @Test
  public void testSetAndGetPid() {
    final Builder builder = new Builder();

    assertNull(builder.getPid());
    assertSame(builder, builder.setPid(0));
    assertEquals(0, builder.getPid().intValue());
    assertSame(builder, builder.setPid(1));
    assertEquals(1, builder.getPid().intValue());
    assertSame(builder, builder.setPid(1024));
    assertEquals(1024, builder.getPid().intValue());
    assertSame(builder, builder.setPid(12345));
    assertEquals(12345, builder.getPid().intValue());
    assertSame(builder, builder.setPid(null));
    assertNull(builder.getPid());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetPidToInvalidValue() {
    try {
      new Builder().setPid(-1);
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_PID_ERROR_MESSAGE.toLocalizedString(), expected.getMessage());
      throw expected;
    }
  }

  @Test
  public void testSetAndGetServerBindAddress() throws Exception {
    final Builder builder = new Builder();

    assertNull(builder.getServerBindAddress());
    assertSame(builder, builder.setServerBindAddress(null));
    assertNull(builder.getServerBindAddress());
    assertSame(builder, builder.setServerBindAddress(""));
    assertNull(builder.getServerBindAddress());
    assertSame(builder, builder.setServerBindAddress("  "));
    assertNull(builder.getServerBindAddress());
    assertSame(builder, builder.setServerBindAddress(InetAddress.getLocalHost().getCanonicalHostName()));
    assertEquals(InetAddress.getLocalHost(), builder.getServerBindAddress());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetServerBindAddressToUnknownHost() {
    try {
      new Builder().setServerBindAddress("badHostName.badCompany.com");
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_UNKNOWN_HOST_ERROR_MESSAGE.toLocalizedString("Server"), expected.getMessage());
      assertTrue(expected.getCause() instanceof UnknownHostException);
      throw expected;
    }
  }

  @Test
  public void testSetAndGetServerPort() {
    final Builder builder = new Builder();

    assertEquals(ServerLauncher.DEFAULT_SERVER_PORT, builder.getServerPort());
    assertSame(builder, builder.setServerPort(0));
    assertEquals(0, builder.getServerPort().intValue());
    assertSame(builder, builder.setServerPort(1));
    assertEquals(1, builder.getServerPort().intValue());
    assertSame(builder, builder.setServerPort(80));
    assertEquals(80, builder.getServerPort().intValue());
    assertSame(builder, builder.setServerPort(1024));
    assertEquals(1024, builder.getServerPort().intValue());
    assertSame(builder, builder.setServerPort(65535));
    assertEquals(65535, builder.getServerPort().intValue());
    assertSame(builder, builder.setServerPort(null));
    assertEquals(ServerLauncher.DEFAULT_SERVER_PORT, builder.getServerPort());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetServerPortToOverflow() {
    try {
      new Builder().setServerPort(65536);
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_INVALID_PORT_ERROR_MESSAGE.toLocalizedString("Server"),
        expected.getMessage());
      throw expected;
    }
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetServerPortToUnderflow() {
    try {
      new Builder().setServerPort(-1);
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_INVALID_PORT_ERROR_MESSAGE.toLocalizedString("Server"),
        expected.getMessage());
      throw expected;
    }
  }

  @Test
  public void testSetAndGetWorkingDirectory() {
    final Builder builder = new Builder();

    assertEquals(ServerLauncher.DEFAULT_WORKING_DIRECTORY, builder.getWorkingDirectory());
    assertSame(builder, builder.setWorkingDirectory(System.getProperty("java.io.tmpdir")));
    assertEquals(IOUtils.tryGetCanonicalPathElseGetAbsolutePath(new File(System.getProperty("java.io.tmpdir"))),
      builder.getWorkingDirectory());
    assertSame(builder, builder.setWorkingDirectory("  "));
    assertEquals(ServerLauncher.DEFAULT_WORKING_DIRECTORY, builder.getWorkingDirectory());
    assertSame(builder, builder.setWorkingDirectory(""));
    assertEquals(ServerLauncher.DEFAULT_WORKING_DIRECTORY, builder.getWorkingDirectory());
    assertSame(builder, builder.setWorkingDirectory(null));
    assertEquals(ServerLauncher.DEFAULT_WORKING_DIRECTORY, builder.getWorkingDirectory());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetWorkingDirectoryToNonExistingDirectory() {
    try {
      new Builder().setWorkingDirectory("/path/to/non_existing/directory");
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_WORKING_DIRECTORY_NOT_FOUND_ERROR_MESSAGE
        .toLocalizedString("Server"), expected.getMessage());
      assertTrue(expected.getCause() instanceof FileNotFoundException);
      assertEquals("/path/to/non_existing/directory", expected.getCause().getMessage());
      throw expected;
    }
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSetWorkingDirectoryToFile() throws IOException {
    File tmpFile = File.createTempFile("tmp", "file");

    assertNotNull(tmpFile);
    assertTrue(tmpFile.isFile());

    tmpFile.deleteOnExit();

    try {
      new Builder().setWorkingDirectory(tmpFile.getAbsolutePath());
    }
    catch (IllegalArgumentException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_WORKING_DIRECTORY_NOT_FOUND_ERROR_MESSAGE
        .toLocalizedString("Server"), expected.getMessage());
      assertTrue(expected.getCause() instanceof FileNotFoundException);
      assertEquals(tmpFile.getAbsolutePath(), expected.getCause().getMessage());
      throw expected;
    }
  }

  @Test
  public void testBuild() throws Exception {
    ServerLauncher launcher = new Builder()
      .setCommand(Command.STOP)
      .setAssignBuckets(true)
      .setForce(true)
      .setMemberName("serverOne")
      .setRebalance(true)
      .setServerBindAddress(InetAddress.getLocalHost().getHostAddress())
      .setServerPort(11235)
      .setWorkingDirectory(System.getProperty("java.io.tmpdir"))
      .build();

    assertNotNull(launcher);
    assertTrue(launcher.isAssignBuckets());
    assertFalse(launcher.isDisableDefaultServer());
    assertTrue(launcher.isForcing());
    assertFalse(launcher.isHelping());
    assertTrue(launcher.isRebalancing());
    assertFalse(launcher.isRunning());
    assertEquals(Command.STOP, launcher.getCommand());
    assertEquals("serverOne", launcher.getMemberName());
    assertEquals(InetAddress.getLocalHost(), launcher.getServerBindAddress());
    assertEquals(11235, launcher.getServerPort().intValue());
    assertEquals(IOUtils.tryGetCanonicalPathElseGetAbsolutePath(new File(System.getProperty("java.io.tmpdir"))),
      launcher.getWorkingDirectory());
  }

  @Test
  public void testBuildWithMemberNameSetInApiPropertiesOnStart() {
    ServerLauncher launcher = new Builder()
      .setCommand(ServerLauncher.Command.START)
      .setMemberName(null)
      .set(DistributionConfig.NAME_NAME, "serverABC")
      .build();

    assertNotNull(launcher);
    assertEquals(ServerLauncher.Command.START, launcher.getCommand());
    assertNull(launcher.getMemberName());
    assertEquals("serverABC", launcher.getProperties().getProperty(DistributionConfig.NAME_NAME));
  }

  @Test
  public void testBuildWithMemberNameSetInGemFirePropertiesOnStart() {
    // TODO fix this test on Windows; File renameTo and delete in finally fail on Windows
    if (NativeCalls.getInstance().getOSType().isWindows()) {
      return;
    }

    Properties gemfireProperties = new Properties();

    gemfireProperties.setProperty(DistributionConfig.NAME_NAME, "server123");

    File gemfirePropertiesFile = writeGemFirePropertiesToFile(gemfireProperties, "gemfire.properties",
      String.format("Test gemfire.properties file for %1$s.%2$s.", getClass().getSimpleName(),
        "testBuildWithMemberNameSetInGemFirePropertiesOnStart"));

    assertNotNull(gemfirePropertiesFile);
    assertTrue(gemfirePropertiesFile.isFile());

    try {
      ServerLauncher launcher = new Builder()
        .setCommand(ServerLauncher.Command.START)
        .setMemberName(null)
        .build();

      assertNotNull(launcher);
      assertEquals(ServerLauncher.Command.START, launcher.getCommand());
      assertNull(launcher.getMemberName());
    }
    finally {
      assertTrue(gemfirePropertiesFile.delete());
      assertFalse(gemfirePropertiesFile.isFile());
    }
  }

  @Test
  public void testBuildWithMemberNameSetInSystemPropertiesOnStart() {
    try {
      System.setProperty(DistributionConfig.GEMFIRE_PREFIX + DistributionConfig.NAME_NAME, "serverXYZ");

      ServerLauncher launcher = new Builder()
        .setCommand(ServerLauncher.Command.START)
        .setMemberName(null)
        .build();

      assertNotNull(launcher);
      assertEquals(ServerLauncher.Command.START, launcher.getCommand());
      assertNull(launcher.getMemberName());
    }
    finally {
      System.clearProperty(DistributionConfig.GEMFIRE_PREFIX + DistributionConfig.NAME_NAME);
    }
  }

  @Test(expected = IllegalStateException.class)
  public void testBuildNoMemberNameOnStart() {
    try {
      new Builder().setCommand(Command.START).build();
    }
    catch (IllegalStateException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_MEMBER_NAME_VALIDATION_ERROR_MESSAGE.toLocalizedString("Server"),
        expected.getMessage());
      throw expected;
    }
  }

  @Test(expected = IllegalStateException.class)
  public void testBuildWithInvalidWorkingDirectoryOnStart() {
    try {
      new Builder().setCommand(Command.START)
        .setMemberName("serverOne")
        .setWorkingDirectory(System.getProperty("java.io.tmpdir"))
        .build();
    }
    catch (IllegalStateException expected) {
      assertEquals(LocalizedStrings.Launcher_Builder_WORKING_DIRECTORY_OPTION_NOT_VALID_ERROR_MESSAGE
        .toLocalizedString("Server"), expected.getMessage());
      throw expected;
    }
  }

  @Test
  public void testIsServing() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final CacheServer mockCacheServer = mockContext.mock(CacheServer.class, "CacheServer");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.singletonList(mockCacheServer)));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertTrue(serverLauncher.isServing(mockCache));
  }

  @Test
  public void testIsServingWhenNoCacheServersExist() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.emptyList()));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertFalse(serverLauncher.isServing(mockCache));
  }

  @Test
  public void testIsWaiting() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final DistributedSystem mockDistributedSystem = mockContext.mock(DistributedSystem.class, "DistributedSystem");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getDistributedSystem();
      will(returnValue(mockDistributedSystem));
      oneOf(mockDistributedSystem).isConnected();
      will(returnValue(true));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());

    serverLauncher.running.set(true);

    assertTrue(serverLauncher.isRunning());
    assertTrue(serverLauncher.isWaiting(mockCache));
  }

  @Test
  public void testIsWaitingWhenNotConnected() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final DistributedSystem mockDistributedSystem = mockContext.mock(DistributedSystem.class, "DistributedSystem");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getDistributedSystem();
      will(returnValue(mockDistributedSystem));
      oneOf(mockDistributedSystem).isConnected();
      will(returnValue(false));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());

    serverLauncher.running.set(true);

    assertTrue(serverLauncher.isRunning());
    assertFalse(serverLauncher.isWaiting(mockCache));
  }

  @Test
  public void testIsWaitingWhenNotRunning() {
    ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());

    serverLauncher.running.set(false);

    assertFalse(serverLauncher.isRunning());
    assertFalse(serverLauncher.isWaiting(null));
  }

  @Test
  public void testWaitOnServer() throws Throwable {
    TestFramework.runOnce(new ServerWaitMultiThreadedTestCase());
  }

  @Test
  public void testIsDefaultServerEnabled() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.emptyList()));
    }});

    ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertFalse(serverLauncher.isDisableDefaultServer());
    assertTrue(serverLauncher.isDefaultServerEnabled(mockCache));
  }

  @Test
  public void testIsDefaultServerEnabledWhenCacheServersExist() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final CacheServer mockCacheServer = mockContext.mock(CacheServer.class, "CacheServer");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.singletonList(mockCacheServer)));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").setDisableDefaultServer(false).build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertFalse(serverLauncher.isDisableDefaultServer());
    assertFalse(serverLauncher.isDefaultServerEnabled(mockCache));
  }
  @Test
  public void testIsDefaultServerEnabledWhenNoCacheServersExistAndDefaultServerDisabled() {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.emptyList()));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").setDisableDefaultServer(true).build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertTrue(serverLauncher.isDisableDefaultServer());
    assertFalse(serverLauncher.isDefaultServerEnabled(mockCache));
  }

  @Test
  public void testStartCacheServer() throws IOException {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final CacheServer mockCacheServer = mockContext.mock(CacheServer.class, "CacheServer");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.emptyList()));
      oneOf(mockCache).addCacheServer();
      will(returnValue(mockCacheServer));
      oneOf(mockCacheServer).setBindAddress(with(aNull(String.class)));
      oneOf(mockCacheServer).setPort(with(equal(11235)));
      oneOf(mockCacheServer).start();
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne")
      .setServerBindAddress(null)
      .setServerPort(11235)
      .setDisableDefaultServer(false)
      .build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertFalse(serverLauncher.isDisableDefaultServer());

    serverLauncher.startCacheServer(mockCache);
  }

  @Test
  public void testStartCacheServerWhenDefaultServerDisabled() throws IOException {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.emptyList()));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").setDisableDefaultServer(true).build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertTrue(serverLauncher.isDisableDefaultServer());

    serverLauncher.startCacheServer(mockCache);
  }

  @Test
  public void testStartCacheServerWithExistingCacheServer() throws IOException {
    final Cache mockCache = mockContext.mock(Cache.class, "Cache");
    final CacheServer mockCacheServer = mockContext.mock(CacheServer.class, "CacheServer");

    mockContext.checking(new Expectations() {{
      oneOf(mockCache).getCacheServers();
      will(returnValue(Collections.singletonList(mockCacheServer)));
    }});

    final ServerLauncher serverLauncher = new Builder().setMemberName("serverOne").setDisableDefaultServer(false).build();

    assertNotNull(serverLauncher);
    assertEquals("serverOne", serverLauncher.getMemberName());
    assertFalse(serverLauncher.isDisableDefaultServer());

    serverLauncher.startCacheServer(mockCache);
  }

  public static void main(final String... args) {
    System.err.printf("Thread (%1$s) is daemon (%2$s)%n", Thread.currentThread().getName(),
      Thread.currentThread().isDaemon());
    new Builder(args).setCommand(Command.START).build().run();
  }

  private final class ServerWaitMultiThreadedTestCase extends MultithreadedTestCase {

    private final AtomicBoolean connectionStateHolder = new AtomicBoolean(true);

    private ServerLauncher serverLauncher;

    @Override
    public void initialize() {
      super.initialize();

      final Cache mockCache = mockContext.mock(Cache.class, "Cache");

      final DistributedSystem mockDistributedSystem = new DistributedSystemAdapter() {
        @Override public boolean isConnected() {
          return connectionStateHolder.get();
        }
      };

      mockContext.checking(new Expectations() {{
        allowing(mockCache).getDistributedSystem();
        will(returnValue(mockDistributedSystem));
        allowing(mockCache).getCacheServers();
        will(returnValue(Collections.emptyList()));
        oneOf(mockCache).close();
      }});

      this.serverLauncher = new Builder().setMemberName("dataMember").setDisableDefaultServer(true)
        .setCache(mockCache).build();

      assertNotNull(this.serverLauncher);
      assertEquals("dataMember", this.serverLauncher.getMemberName());
      assertTrue(this.serverLauncher.isDisableDefaultServer());
      assertTrue(connectionStateHolder.get());
    }

    public void thread1() {
      assertTick(0);

      Thread.currentThread().setName("GemFire Data Member 'main' Thread");
      this.serverLauncher.running.set(true);

      assertTrue(this.serverLauncher.isRunning());
      assertFalse(this.serverLauncher.isServing(this.serverLauncher.getCache()));
      assertTrue(this.serverLauncher.isWaiting(this.serverLauncher.getCache()));

      this.serverLauncher.waitOnServer();

      assertTick(1); // NOTE the tick does not advance when the other Thread terminates
    }

    public void thread2() {
      waitForTick(1);

      Thread.currentThread().setName("GemFire 'shutdown' Thread");

      assertTrue(this.serverLauncher.isRunning());

      this.connectionStateHolder.set(false);
    }

    @Override
    public void finish() {
      super.finish();
      assertFalse(this.serverLauncher.isRunning());
    }
  }

}