/*
 * tnmJob.c --
 *
 *	The simple job scheduler used to implement monitoring scripts.
 *	This version is derived from the original job scheduler
 *	written in Tcl by Stefan Schoek (schoek@ibr.cs.tu-bs.de).
 *
 * Copyright (c) 1994-1996 Technical University of Braunschweig.
 * Copyright (c) 1996-1997 University of Twente.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * @(#) $Id: tnmJob.c 860 1997-09-22 22:07:41Z schoenw $
 */

#include "tnmInt.h"
#include "tnmPort.h"

/*
 * Structure used to describe a job.
 */

typedef struct Job {
    Tcl_Obj *cmd;		/* The command to evaluate. */
    Tcl_Obj *newCmd;		/* The new command to replace the current. */
    Tcl_Obj *exitCmd;		/* The command to cleanup exiting jobs. */
    Tcl_Obj *errorCmd;		/* The command to handle errors. */
    int interval;		/* The time interval value in ms. */
    int remtime;		/* The remaining time in ms. */
    int iterations;		/* The number of iterations of this job. */
    int status;			/* The status of this job (see below). */
    int tagsc;
    char **tagsv;
    Tcl_HashTable attributes;	/* The has table of job attributes. */
    Tcl_Command token;		/* The command token used by Tcl. */
    Tcl_Interp *interp;		/* The interpreter which owns this job. */
    struct Job *nextPtr;	/* Next job in our list of jobs. */
} Job;

/*
 * Every Tcl interpreter has an associated JobControl record. It
 * keeps track of the timers and the list of jobs known by the
 * interpreter. The name tnmJobControl is used to get/set the 
 * JobControl record.
 */

static char tnmJobControl[] = "tnmJobControl";

typedef struct JobControl {
    Job *jobList;		/* The list of jobs for this interpreter. */
    Tcl_TimerToken timer;	/* The token for the Tcl timer. */
    Tcl_Time lastTime;		/* The last time stamp. */
} JobControl;

/* 
 * These are all possible status codes of an existing job.
 */

enum status { 
    suspended, waiting, running, expired 
};

static TnmTable statusTable[] =
{
    { suspended,	"suspended" },
    { waiting,		"waiting" },
    { running,		"running" },
    { expired,		"expired" },
    { 0, NULL }
};

/*
 * The pointer to the currently running job.
 */

static Job *currentJob = NULL;

/*
 * Forward declarations for procedures defined later in this file:
 */

static void
AssocDeleteProc	_ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp));

static void
CmdDeleteProc	_ANSI_ARGS_((ClientData clientData));

static void
DestroyProc	_ANSI_ARGS_((char *memPtr));

static void
ScheduleProc	_ANSI_ARGS_((ClientData clientData));

static void 
NextSchedule	_ANSI_ARGS_((Tcl_Interp *interp, JobControl *control));

static void 
AdjustTime	_ANSI_ARGS_((JobControl *control));

static void
Schedule	_ANSI_ARGS_((Tcl_Interp *interp, JobControl *control));

static int
CreateJob	_ANSI_ARGS_((Tcl_Interp *interp, 
			     int objc, Tcl_Obj *CONST objv[]));
static Tcl_Obj*
GetOption	_ANSI_ARGS_((Tcl_Interp *interp, ClientData object, 
			     int option));
static int
SetOption	_ANSI_ARGS_((Tcl_Interp *interp, ClientData object, 
			     int option, Tcl_Obj *objPtr));
static int
JobObjCmd	_ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp, 
			     int objc, Tcl_Obj *CONST objv[]));
static int
FindJobs	_ANSI_ARGS_((Tcl_Interp *interp, JobControl *control,
			     int objc, Tcl_Obj *CONST objv[]));

/*
 * The options used to configure job objects.
 */

enum options { 
    optCommand, optExit, optError, optInterval, optIterations, 
    optStatus, optTags, optTime
};

static TnmTable optionTable[] = {
    { optCommand,	"-command" },
    { optError,		"-error" },
    { optExit,		"-exit" },
    { optInterval,	"-interval" },
    { optIterations,	"-iterations" },
    { optStatus,	"-status" },
    { optTags,		"-tags" },
    { optTime,		"-time" },
    { 0, NULL }
};

static TnmConfig config = {
    optionTable,
    SetOption,
    GetOption
};

/*
 *----------------------------------------------------------------------
 *
 * AssocDeleteProc --
 *
 *	This procedure is called when a Tcl interpreter gets destroyed
 *	so that we can clean up the data associated with this interpreter.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
AssocDeleteProc(clientData, interp)
    ClientData clientData;
    Tcl_Interp *interp;
{
    JobControl *control = (JobControl *) clientData;

    /*
     * Note, we do not care about job objects since Tcl first
     * removes all commands before calling this delete procedure.
     * However, we have to delete the timer to make sure that
     * no further events are processed for this interpreter.
     */

    if (control) {
	if (control->timer) {
	    Tcl_DeleteTimerHandler(control->timer);
	}
	ckfree((char *) control);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * CmdDeleteProc --
 *
 *	This procedure removes the job for the list of active jobs and 
 *	releases all memory associated with a job object.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	A job is destroyed.
 *
 *----------------------------------------------------------------------
 */

static void
CmdDeleteProc(clientData)
    ClientData clientData;
{
    Job **jobPtrPtr;
    Job *jobPtr = (Job *) clientData;
    JobControl *control = (JobControl *)
	Tcl_GetAssocData(jobPtr->interp, tnmJobControl, NULL);

    /*
     * First, update the list of all known jobs.
     */

    jobPtrPtr = &control->jobList;
    while (*jobPtrPtr && (*jobPtrPtr) != jobPtr) {
	jobPtrPtr = &(*jobPtrPtr)->nextPtr;
    }

    if (*jobPtrPtr) {
	(*jobPtrPtr) = jobPtr->nextPtr;
    }

    Tcl_EventuallyFree((ClientData) jobPtr, DestroyProc);
}

/*
 *----------------------------------------------------------------------
 *
 * DestroyProc --
 *
 *	This procedure is invoked by Tcl_EventuallyFree or Tcl_Release
 *	to clean up the internal structure of a job at a safe time
 *	(when no-one is using it anymore).
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Everything associated with the job is freed up.
 *
 *----------------------------------------------------------------------
 */

static void
DestroyProc(memPtr)
    char *memPtr;
{
    Job *jobPtr = (Job *) memPtr;

    TnmAttrClear(&jobPtr->attributes);
    Tcl_DeleteHashTable(&jobPtr->attributes);

    Tcl_DecrRefCount(jobPtr->cmd);
    if (jobPtr->newCmd) {
	Tcl_DecrRefCount(jobPtr->newCmd);
    }
    if (jobPtr->exitCmd) {
	Tcl_DecrRefCount(jobPtr->exitCmd);
    }
    ckfree((char *) jobPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * ScheduleProc --
 *
 *	This procedure is the callback of the Tcl event loop that
 *	invokes the scheduler.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The scheduler gets called.
 *
 *----------------------------------------------------------------------
 */

static void
ScheduleProc(clientData)
    ClientData clientData;
{
    Tcl_Interp *interp = (Tcl_Interp *) clientData;
    JobControl *control = (JobControl *) 
	Tcl_GetAssocData(interp, tnmJobControl, NULL);

    if (control) {
	Schedule(interp, control);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * NextSchedule --
 *
 *	This procedure registers a timer handler that will call us when
 *	the next job should be executed. An internal token is used to 
 *	make sure that we do not have more than one timer event pending
 *	for the job scheduler.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	A new timer handler might be registered.
 *
 *----------------------------------------------------------------------
 */

static void
NextSchedule(interp, control)
    Tcl_Interp *interp;
    JobControl *control;
{
    Job *jobPtr;
    int ms;

    if (control->timer) {
	Tcl_DeleteTimerHandler(control->timer);
	control->timer = NULL;
    }

    /* 
     * Calculate the minimum of the remaining times of all jobs
     * waiting. Tell the event manager to call us again if we found
     * a waiting job with remaining time >= 0. We also wait for 
     * expired jobs so that they will get removed from the job list.
     */

    ms = -1;
    for (jobPtr = control->jobList; jobPtr != NULL; jobPtr = jobPtr->nextPtr) {
        if (jobPtr->status == waiting
	    || jobPtr->status == expired) {
	    if (ms < 0 || jobPtr->remtime < ms) {
		ms = (jobPtr->remtime < 0) ? 0 : jobPtr->remtime;
	    }
	}
    }
    
    if (ms < 0) {
	control->lastTime.sec = 0;
	control->lastTime.usec = 0;
    } else {
        control->timer = Tcl_CreateTimerHandler(ms, ScheduleProc, 
						(ClientData) interp);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * AdjustTime --
 *
 *	This procedure updates the remaining time of all jobs in the 
 *	queue that are not suspended and sets lastTime to the current 
 *	time.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The remaining times of all active jobs is updated.
 *
 *----------------------------------------------------------------------
 */

static void
AdjustTime(control)
    JobControl *control;
{
    int delta;
    Job *jobPtr;
    Tcl_Time currentTime;

    if (control->lastTime.sec == 0 && control->lastTime.usec == 0) {
	TnmGetTime(&control->lastTime);
	return;
    }

    TnmGetTime(&currentTime);

    delta = (currentTime.sec - control->lastTime.sec) * 1000 
	    + (currentTime.usec - control->lastTime.usec) / 1000;

    control->lastTime = currentTime;

    for (jobPtr = control->jobList; jobPtr; jobPtr = jobPtr->nextPtr) {
        if (jobPtr->status != suspended) {
	    jobPtr->remtime -= delta;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Schedule --
 *
 *	This procedure is called to schedule the next job. It first 
 *	checks for jobs that must be processed. It finally tells the 
 *	event mechanism to repeat itself when the next job needs
 *	attention. This function also cleans up the job queue by 
 *	removing all jobs that are waiting in the state expired.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Jobs might be activated and expired jobs are removed.
 *
 *----------------------------------------------------------------------
 */

static void
Schedule(interp, control)
    Tcl_Interp *interp;
    JobControl *control;
{
    Job *jobPtr;

    /*
     * Refresh the remaining time of the active jobs.
     */

    AdjustTime(control);

    /*
     * Execute waiting jobs with remaining time less or equal 0. 
     * Set the job status to expired if the number of iterations
     * reaches zero.
     */

  restart:
    for (jobPtr = control->jobList; jobPtr != NULL; jobPtr = jobPtr->nextPtr) {

	if (jobPtr->newCmd) {
	    Tcl_DecrRefCount(jobPtr->cmd);
	    jobPtr->cmd = jobPtr->newCmd;
	    jobPtr->newCmd = NULL;
	}

	if ((jobPtr->status == waiting) && (jobPtr->remtime <= 0)) {

	    int code;

	    Tcl_Preserve((ClientData) jobPtr);
	    currentJob = jobPtr;
	    jobPtr->status = running;

	    Tcl_AllowExceptions(interp);
	    code = Tcl_GlobalEvalObj(interp, jobPtr->cmd);
	    if (code == TCL_ERROR) {
		if (jobPtr->errorCmd) {
		    Tcl_GlobalEvalObj(interp, jobPtr->errorCmd);
		} else {
		    char *name = Tcl_GetCommandName(interp, jobPtr->token);
		    Tcl_AddErrorInfo(interp, "\n    (script bound to job - ");
		    Tcl_AddErrorInfo(interp, name);
		    Tcl_AddErrorInfo(interp, " deleted)");
		    Tcl_BackgroundError(interp);
		    jobPtr->status = expired;
		}
	    }
    
	    Tcl_ResetResult(interp);
	    if (jobPtr->status == running) {
		jobPtr->status = waiting;
	    }
	    currentJob = NULL;
	    
	    jobPtr->remtime = jobPtr->interval;
	    if (jobPtr->iterations > 0) {
		jobPtr->iterations--; 
		if (jobPtr->iterations == 0) {
		    jobPtr->status = expired;
		}
	    }
	    Tcl_Release((ClientData) jobPtr);
	    goto restart;
	}
    }

    /*
     * Delete all jobs which have reached the status expired.
     * We must restart the loop for every deleted job as the
     * job list is modified by calling Tcl_DeleteCommand().
     */

  repeat:
    for (jobPtr = control->jobList; jobPtr != NULL; jobPtr = jobPtr->nextPtr) {
        if (jobPtr->status == expired) {
	    if (jobPtr->exitCmd) {
		(void) Tcl_GlobalEvalObj(interp, jobPtr->exitCmd);
	    }
	    Tcl_DeleteCommandFromToken(interp, jobPtr->token);
	    goto repeat;
        }
    }
    
    /*
     * Compute and subtract the time needed to execute the jobs
     * and schedule the next pass through the scheduler.
     */

    AdjustTime(control);
    NextSchedule(interp, control);
}

/*
 *----------------------------------------------------------------------
 *
 * CreateJob --
 *
 *	This procedure creates a new job and appends it to our job
 *	list. The scheduler is called to update the timer.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	The job list is updated and the scheduler is started.
 *
 *----------------------------------------------------------------------
 */

static int
CreateJob(interp, objc, objv)
    Tcl_Interp *interp;
    int objc;
    Tcl_Obj *CONST objv[];
{
    static unsigned nextId = 0;
    Job *jobPtr, *p;
    char *name;
    int code;
    JobControl *control = (JobControl *) 
	Tcl_GetAssocData(interp, tnmJobControl, NULL);

    jobPtr = (Job *) ckalloc(sizeof(Job));
    memset((char *) jobPtr, 0, sizeof(Job));
    jobPtr->cmd = Tcl_NewStringObj(NULL, 0);
    Tcl_IncrRefCount(jobPtr->cmd);
    jobPtr->interval = 1000;
    jobPtr->status = waiting;
    jobPtr->interp = interp;
    Tcl_InitHashTable(&(jobPtr->attributes), TCL_STRING_KEYS);

    code = TnmSetConfig(interp, &config, (ClientData) jobPtr, objc, objv);
    if (code != TCL_OK) {
        ckfree((char *) jobPtr);
        return TCL_ERROR;
    }

    /*
     * Put the new job into the job list. We add it at the end
     * to preserve the order in which the jobs were created.
     */

    if (control->jobList == NULL) {
        control->jobList = jobPtr;
    } else {
        for (p = control->jobList; p->nextPtr != NULL; p = p->nextPtr) ;
	p->nextPtr = jobPtr;
    }

    /*
     * Create a new scheduling point for this new job.
     */

    NextSchedule(interp, control);

    /*
     * Create a new Tcl command for this job object.
     */

    name = TnmGetHandle(interp, "job", &nextId);
    jobPtr->token = Tcl_CreateObjCommand(interp, name, JobObjCmd,
					 (ClientData) jobPtr, CmdDeleteProc);
    Tcl_SetResult(interp, name, TCL_STATIC);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * GetOption --
 *
 *	This procedure retrieves the value of a job option.
 *
 * Results:
 *	A pointer to the value formatted as a string.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static Tcl_Obj*
GetOption(interp, object, option)
    Tcl_Interp *interp;
    ClientData object;
    int option;
{
    static char *status;
    Job *jobPtr = (Job *) object;
    JobControl *control = (JobControl *)
	Tcl_GetAssocData(jobPtr->interp, tnmJobControl, NULL);

    static char *freeme = NULL;

    if (freeme) {
	ckfree(freeme);
	freeme = NULL;
    }

    switch ((enum options) option) {
    case optCommand:
 	return jobPtr->newCmd ? jobPtr->newCmd : jobPtr->cmd;
    case optError:
	return jobPtr->errorCmd ? jobPtr->errorCmd : Tcl_NewObj();
    case optExit:
	return jobPtr->exitCmd ? jobPtr->exitCmd : Tcl_NewObj();
    case optInterval:
 	return Tcl_NewIntObj(jobPtr->interval);
    case optIterations:
 	return Tcl_NewIntObj(jobPtr->iterations);
    case optStatus:
	status = TnmGetTableValue(statusTable, jobPtr->status);
 	return Tcl_NewStringObj(status, -1);
    case optTags:
	if (jobPtr->tagsc) {
	    freeme = Tcl_Merge(jobPtr->tagsc, jobPtr->tagsv);
	}
	return Tcl_NewStringObj(freeme, -1);
    case optTime:
	if (control) {
	    AdjustTime(control);
	}
 	return Tcl_NewIntObj(jobPtr->remtime);
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * SetOption --
 *
 *	This procedure modifies a single option of a job object.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
SetOption(interp, object, option, objPtr)
    Tcl_Interp *interp;
    ClientData object;
    int option;
    Tcl_Obj *objPtr;
{
    Job *jobPtr = (Job *) object;
    int num, status, largc;
    char **largv;
    JobControl *control = (JobControl *)
	Tcl_GetAssocData(jobPtr->interp, tnmJobControl, NULL);

    switch ((enum options) option) {
    case optCommand:
	if (jobPtr->newCmd) {
	    Tcl_DecrRefCount(jobPtr->newCmd);
	}
	jobPtr->newCmd = objPtr;
	Tcl_IncrRefCount(jobPtr->newCmd);
	break;
    case optError:
	if (jobPtr->errorCmd) {
	    Tcl_DecrRefCount(jobPtr->errorCmd);
	}
	jobPtr->errorCmd = objPtr;
	Tcl_IncrRefCount(jobPtr->errorCmd);
	break;
    case optExit:
	if (jobPtr->exitCmd) {
	    Tcl_DecrRefCount(jobPtr->exitCmd);
	}
	jobPtr->exitCmd = objPtr;
	Tcl_IncrRefCount(objPtr);
	break;
    case optInterval:
	if (TnmGetPositiveFromObj(interp, objPtr, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	jobPtr->interval = num;
	break;
    case optIterations:
	if (TnmGetUnsignedFromObj(interp, objPtr, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	jobPtr->iterations = num;
	break;
    case optStatus:
 	status = TnmGetTableKeyFromObj(interp, statusTable, objPtr, "status");
	if (status < 0) {
	    return TCL_ERROR;
	}
	jobPtr->status = (status == running) ? waiting : status;

	/*
	 * Compute the current time offsets and create a new 
	 * scheduling point. A suspended job may have resumed 
	 * and we must make sure that our scheduler is running.
	 */
	
	if (control) {
	    AdjustTime(control);
	    NextSchedule(interp, control);
	}
	break;
    case optTags:
	status = Tcl_SplitList(interp, Tcl_GetStringFromObj(objPtr, NULL),
			       &largc, &largv);
	if (status != TCL_OK) {
            return TCL_ERROR;
        }
	if (jobPtr->tagsv) {
	    ckfree((char *) jobPtr->tagsv);
	}
	jobPtr->tagsc = largc;
	jobPtr->tagsv = largv;
	break;
    case optTime:
	break; 
    }

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * JobObjCmd --
 *
 *	This procedure implements the job object command.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

static int
JobObjCmd(clientData, interp, objc, objv)
    ClientData clientData;
    Tcl_Interp *interp;
    int objc;
    Tcl_Obj *CONST objv[];
{
    int result;
    Job *jobPtr = (Job *) clientData;
    JobControl *control = (JobControl *) 
	Tcl_GetAssocData(interp, tnmJobControl, NULL);

    enum commands {
	cmdAttribute, cmdCget, cmdConfigure, cmdDestroy, cmdWait
    } cmd;

    static char *cmdTable[] = {
	"attribute", "cget", "configure", "destroy", "wait", (char *) NULL
    };

    if (objc < 2) {
	Tcl_WrongNumArgs(interp, 1, objv, "option ?args?");
        return TCL_ERROR;
    }

    result = Tcl_GetIndexFromObj(interp, objv[1], cmdTable, 
				 "option", TCL_EXACT, (int *) &cmd);
    if (result != TCL_OK) {
	return result;
    }

    Tcl_Preserve((ClientData) jobPtr);

    switch (cmd) {

    case cmdAttribute:
	if (objc < 2 || objc > 4) {
	    Tcl_WrongNumArgs(interp, 2, objv, "?name ?value??");
	    return TCL_ERROR;
	}
	switch (objc) {
	case 2:
	    TnmAttrList(&jobPtr->attributes, interp);
	    break;
	case 3:
	    result = TnmAttrSet(&jobPtr->attributes, interp, 
				Tcl_GetStringFromObj(objv[2], NULL), NULL);
	    break;
	case 4:
	    TnmAttrSet(&jobPtr->attributes, interp, 
		       Tcl_GetStringFromObj(objv[2], NULL),
		       Tcl_GetStringFromObj(objv[3], NULL));
	    break;
	}
	break;

    case cmdConfigure:
	result = TnmSetConfig(interp, &config, (ClientData) jobPtr, 
			      objc, objv);
	break;

    case cmdCget:
	result = TnmGetConfig(interp, &config, (ClientData) jobPtr, 
			      objc, objv);
	break;

    case cmdDestroy:
        if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
	jobPtr->status = expired;
	break;

    case cmdWait:
	if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
            break;
	}
	if (control) {
	    Job *jPtr;
	repeat:
	    for (jPtr = control->jobList; jPtr; jPtr = jPtr->nextPtr) {
		if (jPtr->status == waiting && jPtr == jobPtr) {
		    Tcl_DoOneEvent(0);
		    goto repeat;
		}
	    }
	}
	break;
    }

    Tcl_Release((ClientData) jobPtr);
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * FindJobs --
 *
 *	This procedure is invoked to process the "find" command
 *	option of the job command.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

static int
FindJobs(interp, control, objc, objv)
    Tcl_Interp *interp;
    JobControl *control;
    int objc;
    Tcl_Obj *CONST objv[];
{
    int i, j, listLen = 0;
    Job *jobPtr;
    Tcl_Obj *listPtr;
    Tcl_Obj **elemPtrs;

    enum selects { 
	selTags 
    };

    static TnmTable selectTable[] = {
        { selTags,	"-tags" },
	{ 0, NULL }
    };

    if (objc % 2) {
	Tcl_WrongNumArgs(interp, 2, objv, "?option value? ?option value? ...");
	return TCL_ERROR;
    }

    for (i = 2; i < objc; i++) {
	char *optionName = Tcl_GetStringFromObj(objv[i++], NULL);
	int option = TnmGetTableKey(selectTable, optionName);
	if (option == -1) {
	    TnmBadOption(interp, optionName, TnmGetTableValues(selectTable));
	    return TCL_ERROR;
	}
	switch ((enum selects) option) {
	case selTags:
	    if (Tcl_ListObjGetElements(interp, objv[i],
				       &listLen, &elemPtrs) != TCL_OK) {
		return TCL_ERROR;
	    }
	    break;
	}
    }

    listPtr = Tcl_GetObjResult(interp);
    for (jobPtr = control->jobList; jobPtr; jobPtr = jobPtr->nextPtr) {
	if (listLen) {
	    int match = 0;
	    for (i = 0; i < listLen && !match; i++) {
		for (j = 0; j < jobPtr->tagsc && !match; j++) {
		    match = Tcl_StringMatch(jobPtr->tagsv[j], 
				    Tcl_GetStringFromObj(elemPtrs[i], NULL));
		}
	    }
	    if (! match) {
		continue;
	    }
	}

    {
	char *cmdName = Tcl_GetCommandName(interp, jobPtr->token);
	Tcl_Obj *elemObjPtr = Tcl_NewStringObj(cmdName, -1);
	Tcl_ListObjAppendElement(interp, listPtr, elemObjPtr);
    }
    }

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Tnm_JobObjCmd --
 *
 *	This procedure is invoked to process the "job" command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

int
Tnm_JobObjCmd(clientData, interp, objc, objv)
    ClientData clientData;
    Tcl_Interp *interp;
    int objc;
    Tcl_Obj *CONST objv[];
{
    Job *jobPtr;
    int result;
    Tcl_Obj *listPtr;
    JobControl *control = (JobControl *) 
	Tcl_GetAssocData(interp, tnmJobControl, NULL);

    enum commands { 
	cmdCreate, cmdCurrent, cmdFind, cmdInfo, cmdSchedule, cmdWait
    } cmd;

    static char *cmdTable[] = {
	"create", "current", "find", "info", "schedule", "wait", (char *) NULL
    };

    if (! control) {
	control = (JobControl *) ckalloc(sizeof(JobControl));
	memset((char *) control, 0, sizeof(JobControl));
	Tcl_SetAssocData(interp, tnmJobControl, AssocDeleteProc, 
			 (ClientData) control);
    }

    if (objc < 2) {
	Tcl_WrongNumArgs(interp, 1, objv, "option ?arg arg ...?");
	return TCL_ERROR;
    }

    result = Tcl_GetIndexFromObj(interp, objv[1], cmdTable, 
				 "option", TCL_EXACT, (int *) &cmd);
    if (result != TCL_OK) {
	return result;
    }

    switch (cmd) {

    case cmdCreate:
	result = CreateJob(interp, objc, objv);
	break;

    case cmdCurrent:
        if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
	if (currentJob && currentJob->interp == interp) {
	    char *name = Tcl_GetCommandName(interp, currentJob->token);
	    Tcl_SetResult(interp, name, TCL_STATIC);
	}
	break;

    case cmdFind:
	result = FindJobs(interp, control, objc, objv);
	break;

    case cmdInfo:
        if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
	listPtr = Tcl_GetObjResult(interp);
	for (jobPtr = control->jobList; jobPtr; jobPtr = jobPtr->nextPtr) {
	    char *cmdName = Tcl_GetCommandName(interp, jobPtr->token);
	    Tcl_Obj *elemObjPtr = Tcl_NewStringObj(cmdName, -1);
	    Tcl_ListObjAppendElement(interp, listPtr, elemObjPtr);
	}
	break;

    case cmdSchedule:
        if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
	Schedule(interp, control);
	break;

    case cmdWait:
        if (objc != 2) {
	    Tcl_WrongNumArgs(interp, 2, objv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
    repeat:
	for (jobPtr = control->jobList; jobPtr; jobPtr = jobPtr->nextPtr) {
	    if (jobPtr->status == waiting) {
		Tcl_DoOneEvent(0);
		goto repeat;
	    }
	}
	break;
    }

    return result;
}
