Index: /trunk/Ohana/src/opihi/include/pantasks.h
===================================================================
--- /trunk/Ohana/src/opihi/include/pantasks.h	(revision 11054)
+++ /trunk/Ohana/src/opihi/include/pantasks.h	(revision 11055)
@@ -28,4 +28,5 @@
 
 enum {RANGE_ABS, RANGE_DAY, RANGE_WEEK};
+enum {TIMER_ALLJOBS, TIMER_SUCCESS, TIMER_FAILURE};
 
 enum {TASK_NONE, 
@@ -42,4 +43,5 @@
       TASK_OPTIONS, 
       TASK_PERIODS, 
+      TASK_NPENDING, 
       TASK_EXIT, 
       TASK_EXEC
@@ -86,4 +88,7 @@
   int     Njobs;
 
+  int     Npending;  // number of currently pending jobs
+  int     NpendingMax;  // max number of pending jobs allowed
+
   float   poll_period;
   float   exec_period;
@@ -95,4 +100,8 @@
   int Nfailure;
   int Ntimeout;
+
+  double dtimeAve_alljobs, dtimeMin_alljobs, dtimeMax_alljobs;
+  double dtimeAve_success, dtimeMin_success, dtimeMax_success;
+  double dtimeAve_failure, dtimeMin_failure, dtimeMax_failure;
 
   int active;
@@ -140,4 +149,6 @@
   int         stdout_fd;		/* stdout pipe (local only) */
   int         stderr_fd;		/* stderr pipe (local only) */
+
+  double dtime;
 } Job;
 
@@ -167,4 +178,6 @@
 int RemoveTask (Task *task);
 Task *SetNewTask (Task *task);
+void ListTaskStats ();
+void UpdateTaskTimerStats (Task *task, int mode, double dtime);
 
 int NextJobID ();
Index: /trunk/Ohana/src/opihi/include/shell.h
===================================================================
--- /trunk/Ohana/src/opihi/include/shell.h	(revision 11054)
+++ /trunk/Ohana/src/opihi/include/shell.h	(revision 11055)
@@ -102,4 +102,5 @@
 void          ListMacros                PROTO(());
 void          FreeMacro                 PROTO((Macro *macro));
+CommandF     *find_macro_command        PROTO((char *name));
 
 int           exec_loop                 PROTO((Macro *loop));
Index: /trunk/Ohana/src/opihi/pantasks/CheckJobs.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/CheckJobs.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/CheckJobs.c	(revision 11055)
@@ -5,4 +5,5 @@
   FILE *f;
   Job *job;
+  Task *task;
   Macro *macro;
   int i, status;
@@ -13,6 +14,8 @@
   while ((job = NextJob ()) != NULL) {
 
+    task = job[0].task;
+
     /* check poll period (ready to ask for status?) */
-    if (GetTaskTimer(job[0].last) < job[0].task[0].poll_period) continue;
+    if (GetTaskTimer(job[0].last) < task[0].poll_period) continue;
 
     /* check current status */
@@ -20,9 +23,9 @@
     switch (status) {
       case JOB_PENDING:
-	/* if (VerboseMode()) gprint (GP_LOG, "job %s (%d) pending\n", job[0].task[0].name, job[0].JobID); */
+	/* if (VerboseMode()) gprint (GP_LOG, "job %s (%d) pending\n", task[0].name, job[0].JobID); */
 	break;
 
       case JOB_BUSY:
-	/* if (VerboseMode()) gprint (GP_LOG, "job %s (%d) busy\n", job[0].task[0].name, job[0].JobID); */
+	/* if (VerboseMode()) gprint (GP_LOG, "job %s (%d) busy\n", task[0].name, job[0].JobID); */
 	break;
 
@@ -35,5 +38,5 @@
 
 	/* save the stdout and stderr if desired */
-	if (job[0].stdout_dump != NULL) {
+	if ((job[0].stdout_dump != NULL) && strcasecmp(job[0].stdout_dump, "NULL")) {
 	  f = fopen (job[0].stdout_dump, "a");
 	  if (f == NULL) {
@@ -44,5 +47,5 @@
 	  }
 	}
-	if (job[0].stderr_dump != NULL) {
+	if ((job[0].stderr_dump != NULL) && strcasecmp(job[0].stderr_dump, "NULL")) {
 	  f = fopen (job[0].stderr_dump, "a");
 	  if (f == NULL) {
@@ -68,14 +71,19 @@
 	set_int_variable ("options:n", job[0].optc);
 
+	set_variable ("JOB_DTIME", job[0].dtime);
+
 	if (status == JOB_CRASH) {
 	  /* XXX add an Ncrash element? */
-	  job[0].task[0].Nfailure ++;
+	  task[0].Nfailure ++;
+	  UpdateTaskTimerStats (task, TIMER_FAILURE, job[0].dtime);
 
 	  /* run task[0].crash macro, if it exists */
 	  /* perhaps define PushNamedQueueBuffer */
 
-	  if (VerboseMode()) gprint (GP_LOG, "job %s (%d) crash\n", job[0].task[0].name, job[0].JobID);
-	  if (job[0].task[0].crash != NULL) {
-	    exec_loop (job[0].task[0].crash);
+	  set_str_variable ("JOB_STATUS", "CRASH");
+
+	  if (VerboseMode()) gprint (GP_LOG, "job %s (%d) crash\n", task[0].name, job[0].JobID);
+	  if (task[0].crash != NULL) {
+	    exec_loop (task[0].crash);
 	  }
 	}
@@ -83,15 +91,19 @@
 	  /* update the exit status counters */
 	  if (job[0].exit_status) {
-	    job[0].task[0].Nfailure ++;
+	    task[0].Nfailure ++;
+	    UpdateTaskTimerStats (task, TIMER_FAILURE, job[0].dtime);
 	  } else {
-	    job[0].task[0].Nsuccess ++;
-	  }
+	    task[0].Nsuccess ++;
+	    UpdateTaskTimerStats (task, TIMER_SUCCESS, job[0].dtime);
+	  }
+
+	  set_int_variable ("JOB_STATUS", job[0].exit_status);
 
 	  /* run corresponding task[0].exit macro, if it exists */
-	  if (VerboseMode()) gprint (GP_LOG, "job %s (%d) exit\n", job[0].task[0].name, job[0].JobID);
-	  macro = job[0].task[0].defexit;
-	  for (i = 0; i < job[0].task[0].Nexit; i++) {
-	    if (job[0].exit_status == atoi(job[0].task[0].exit[i][0].name)) {
-	      macro = job[0].task[0].exit[i];
+	  if (VerboseMode()) gprint (GP_LOG, "job %s (%d) exit\n", task[0].name, job[0].JobID);
+	  macro = task[0].defexit;
+	  for (i = 0; i < task[0].Nexit; i++) {
+	    if (job[0].exit_status == atoi(task[0].exit[i][0].name)) {
+	      macro = task[0].exit[i];
 	      break;
 	    }
@@ -106,4 +118,7 @@
 	if (queue) InitQueue (queue);
 
+	UpdateTaskTimerStats (task, TIMER_ALLJOBS, job[0].dtime);
+
+	task[0].Npending --;
 	DeleteJob (job);
 	continue;
@@ -120,6 +135,6 @@
      */
     if (job[0].mode == JOB_LOCAL) {
-      if (GetTaskTimer(job[0].start) < job[0].task[0].timeout_period) continue;
-      if (VerboseMode()) gprint (GP_LOG, "timeout on %s\n", job[0].task[0].name);
+      if (GetTaskTimer(job[0].start) < task[0].timeout_period) continue;
+      if (VerboseMode()) gprint (GP_LOG, "timeout on %s\n", task[0].name);
 
       // XXX harvest STDERR and STDOUT from timeout job (should be available...)
@@ -127,5 +142,5 @@
 
       /* update the timeout counter */
-      job[0].task[0].Ntimeout ++;
+      task[0].Ntimeout ++;
 
       if (!KillLocalJob (job)) {
@@ -150,6 +165,6 @@
 
       /* run task[0].timeout macro, if it exists */
-      if (job[0].task[0].timeout != NULL) {
-	exec_loop (job[0].task[0].timeout);
+      if (task[0].timeout != NULL) {
+	exec_loop (task[0].timeout);
       }
       DeleteJob (job);
Index: /trunk/Ohana/src/opihi/pantasks/CheckTasks.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/CheckTasks.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/CheckTasks.c	(revision 11055)
@@ -18,4 +18,5 @@
     if (!CheckTimeRanges (task[0].ranges, task[0].Nranges)) continue;
     if (task[0].Nmax && (task[0].Njobs >= task[0].Nmax)) continue;
+    if (task[0].NpendingMax && (task[0].Npending >= task[0].NpendingMax)) continue;
 
     /* ready to run? : run task.exec macro */
@@ -35,4 +36,5 @@
     gettimeofday (&task[0].last, (void *) NULL);
     task[0].Njobs ++;
+    task[0].Npending ++;
 
     /* increment Nrun for inclusive ranges with Nmax */
Index: /trunk/Ohana/src/opihi/pantasks/ControllerOps.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/ControllerOps.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/ControllerOps.c	(revision 11055)
@@ -95,4 +95,6 @@
   p = memstr (buffer.buffer, "STDERR", buffer.Nbuffer);
   sscanf (p, "%*s %d", &job[0].stderr_size);
+  p = memstr (buffer.buffer, "DTIME",  buffer.Nbuffer);
+  sscanf (p, "%*s %lf", &job[0].dtime);
   FreeIOBuffer (&buffer);
 
Index: /trunk/Ohana/src/opihi/pantasks/LocalJob.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/LocalJob.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/LocalJob.c	(revision 11055)
@@ -97,4 +97,6 @@
 	exit (1);
       }
+      job[0].dtime = GetTaskTimer (job[0].start);
+      break;
   }
   return (FALSE);
Index: /trunk/Ohana/src/opihi/pantasks/TaskOps.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/TaskOps.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/TaskOps.c	(revision 11055)
@@ -52,6 +52,7 @@
 void ListTasks (int verbose) {
 
-  int i, j, valid;
+  int i, j, valid, nameLength, cmdLength;
   char *start, *stop;
+  char format[128];
 
   gprint (GP_LOG, "\n");
@@ -61,6 +62,21 @@
   }
 
+  /* find string lengths */
+  nameLength = cmdLength = 0;
+  for (i = 0; i < Ntasks; i++) {
+    nameLength = MAX (nameLength, strlen(tasks[i][0].name));
+    if (tasks[i][0].argv == NULL) {
+      cmdLength = MAX (nameLength, strlen("(dynamic)"));
+    } else {
+      cmdLength = MAX (nameLength, strlen(tasks[i][0].argv[0]));
+    }
+  }
+
   gprint (GP_LOG, " Task Status\n");
-  gprint (GP_LOG, "  * Name            Njobs  Command\n");
+
+  snprintf (format, 128, "  AV %%-%ds %5s %%-%ds\n", nameLength, "Njobs", cmdLength);
+  gprint (GP_LOG, format, "Name", "Command");
+
+  snprintf (format, 128, "%%-%ds %%5d %%-%ds\n", nameLength, cmdLength);
   for (i = 0; i < Ntasks; i++) {
     valid = CheckTimeRanges (tasks[i][0].ranges, tasks[i][0].Nranges);
@@ -77,7 +93,7 @@
     }
     if (tasks[i][0].argv == NULL) {
-      gprint (GP_LOG, "%-15s %5d  %-20s\n", tasks[i][0].name, tasks[i][0].Njobs, "(dynamic)");
-    } else {
-      gprint (GP_LOG, "%-15s %5d  %-20s\n", tasks[i][0].name, tasks[i][0].Njobs, tasks[i][0].argv[0]);
+      gprint (GP_LOG, format, tasks[i][0].name, tasks[i][0].Njobs, "(dynamic)");
+    } else {
+      gprint (GP_LOG, format, tasks[i][0].name, tasks[i][0].Njobs, tasks[i][0].argv[0]);
     }
     if (verbose) {
@@ -129,4 +145,67 @@
 }
 
+/* list known tasks */
+void ListTaskStats () {
+
+  int i, j, valid, nameLength;
+  char *start, *stop;
+  char format[128];
+
+  gprint (GP_LOG, "\n");
+  if (Ntasks == 0) {
+    gprint (GP_LOG, " no defined tasks\n");
+    return;
+  }
+
+  /* find string lengths */
+  nameLength = 0;
+  for (i = 0; i < Ntasks; i++) {
+    nameLength = MAX (nameLength, strlen(tasks[i][0].name));
+  }
+
+  gprint (GP_LOG, " Task Statistics\n");
+
+  snprintf (format, 128, "     %%-%ds |           alljobs          |           success          |           failure          |\n", nameLength);
+  gprint (GP_LOG, format, "");
+  snprintf (format, 128, "  AV %%-%ds | Njobs   Tmin   Tave   Tmax | Njobs   Tmin   Tave   Tmax | Njobs   Tmin   Tave   Tmax |\n", nameLength);
+  gprint (GP_LOG, format, "Name");
+
+  snprintf (format, 128, "%%-%ds", nameLength);
+  for (i = 0; i < Ntasks; i++) {
+    valid = CheckTimeRanges (tasks[i][0].ranges, tasks[i][0].Nranges);
+    if (tasks[i][0].active) {
+      gprint (GP_LOG, "  +");
+    } else {
+      gprint (GP_LOG, "  -");
+    }
+    if (valid) {
+      gprint (GP_LOG, "+ ");
+    } else {
+      gprint (GP_LOG, "- ");
+    }
+    if (tasks[i][0].argv == NULL) {
+      gprint (GP_LOG, format, tasks[i][0].name);
+    } else {
+      gprint (GP_LOG, format, tasks[i][0].name);
+    }
+    if (tasks[i][0].dtimeMin_alljobs < 0) {
+      gprint (GP_LOG, " | %5d %6s %6.2f %6.2f",     tasks[i][0].Njobs,    "NONE",                       tasks[i][0].dtimeAve_alljobs, tasks[i][0].dtimeMax_alljobs);
+    } else {
+      gprint (GP_LOG, " | %5d %6.2f %6.2f %6.2f",   tasks[i][0].Njobs,    tasks[i][0].dtimeMin_alljobs, tasks[i][0].dtimeAve_alljobs, tasks[i][0].dtimeMax_alljobs);
+    }      
+    if (tasks[i][0].dtimeMin_success < 0) {
+      gprint (GP_LOG, " | %5d %6s %6.2f %6.2f",     tasks[i][0].Nsuccess, "NONE",                       tasks[i][0].dtimeAve_success, tasks[i][0].dtimeMax_success);
+    } else {
+      gprint (GP_LOG, " | %5d %6.2f %6.2f %6.2f",   tasks[i][0].Nsuccess, tasks[i][0].dtimeMin_success, tasks[i][0].dtimeAve_success, tasks[i][0].dtimeMax_success);
+    }
+    if (tasks[i][0].dtimeMin_failure < 0) {
+      gprint (GP_LOG, " | %5d %6s %6.2f %6.2f |\n",   tasks[i][0].Nfailure, "NONE",                       tasks[i][0].dtimeAve_failure, tasks[i][0].dtimeMax_failure);
+    } else {
+      gprint (GP_LOG, " | %5d %6.2f %6.2f %6.2f |\n", tasks[i][0].Nfailure, tasks[i][0].dtimeMin_failure, tasks[i][0].dtimeAve_failure, tasks[i][0].dtimeMax_failure);
+    }
+  }
+  return;
+}
+
 /* show details of a task */
 int ShowTask (char *name) {
@@ -311,8 +390,23 @@
   NewTask[0].Nmax = 0;  /* default value means 'no limit' */
 
+  NewTask[0].NpendingMax = 0;  /* default value means 'no limit' */
+
   NewTask[0].Njobs = 0;
   NewTask[0].Nsuccess = 0;
   NewTask[0].Nfailure = 0;
   NewTask[0].Ntimeout = 0;
+
+  /* jobs timing statistics */
+  NewTask[0].dtimeAve_alljobs =  0.0;
+  NewTask[0].dtimeMin_alljobs = -1.0;
+  NewTask[0].dtimeMax_alljobs =  0.0;
+
+  NewTask[0].dtimeAve_success =  0.0;
+  NewTask[0].dtimeMin_success = -1.0;
+  NewTask[0].dtimeMax_success =  0.0;
+
+  NewTask[0].dtimeAve_failure =  0.0;
+  NewTask[0].dtimeMin_failure = -1.0;
+  NewTask[0].dtimeMax_failure =  0.0;
 
   NewTask[0].active = TRUE;
@@ -433,4 +527,5 @@
   if (!strcasecmp (command, "OPTIONS"))   hash = TASK_OPTIONS;
   if (!strcasecmp (command, "PERIODS"))   hash = TASK_PERIODS;
+  if (!strcasecmp (command, "NPENDING"))  hash = TASK_NPENDING;
   if (!strcasecmp (command, "TASK.EXIT")) hash = TASK_EXIT;
   if (!strcasecmp (command, "TASK.EXEC")) hash = TASK_EXEC;
@@ -461,7 +556,55 @@
 
   Task *task;
+  double isec, fsec;
 
   while ((task = NextTask ()) != NULL) {
     gettimeofday (&task[0].last, (void *) NULL);
+    fsec = modf (task[0].exec_period, &isec);
+    task[0].last.tv_sec -= isec;
+    task[0].last.tv_usec -= 1e6*fsec;
  }
 }
+
+/* must call this after updating the corresponding counter */
+void UpdateTaskTimerStats (Task *task, int mode, double dtime) {
+
+  double total;
+
+  switch (mode) {
+    case TIMER_ALLJOBS:
+      total = task[0].dtimeAve_alljobs * (task[0].Njobs - 1);
+      total += dtime;
+      task[0].dtimeAve_alljobs = total / (float) task[0].Njobs;
+      if (task[0].dtimeMin_alljobs < 0) {
+	task[0].dtimeMin_alljobs = dtime;
+      } else {
+	task[0].dtimeMin_alljobs = MIN (task[0].dtimeMin_alljobs, dtime);
+      }
+      task[0].dtimeMax_alljobs = MAX (task[0].dtimeMax_alljobs, dtime);
+      break;
+    case TIMER_SUCCESS:
+      total = task[0].dtimeAve_success * (task[0].Nsuccess - 1);
+      total += dtime;
+      task[0].dtimeAve_success = total / (float) task[0].Nsuccess;
+      if (task[0].dtimeMin_success < 0) {
+	task[0].dtimeMin_success = dtime;
+      } else {
+	task[0].dtimeMin_success = MIN (task[0].dtimeMin_success, dtime);
+      }
+      task[0].dtimeMax_success = MAX (task[0].dtimeMax_success, dtime);
+      break;
+    case TIMER_FAILURE:
+      total = task[0].dtimeAve_failure * (task[0].Nfailure - 1);
+      total += dtime;
+      task[0].dtimeAve_failure = total / (float) task[0].Nfailure;
+      if (task[0].dtimeMin_failure < 0) {
+	task[0].dtimeMin_failure = dtime;
+      } else {
+	task[0].dtimeMin_failure = MIN (task[0].dtimeMin_failure, dtime);
+      }
+      task[0].dtimeMax_failure = MAX (task[0].dtimeMax_failure, dtime);
+      break;
+    default:
+      abort();
+  }
+}
Index: /trunk/Ohana/src/opihi/pantasks/ipptool2book.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/ipptool2book.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/ipptool2book.c	(revision 11055)
@@ -22,5 +22,5 @@
   Nkeys = 0;
   keys = NULL;
-  if ((N = get_argument (argc, argv, "-id"))) {
+  if ((N = get_argument (argc, argv, "-key"))) {
     remove_argument (N, &argc, argv);
 
@@ -41,5 +41,5 @@
 
   if (argc != 3) {
-    gprint (GP_ERR, "USAGE: ipptool2book (queue) (book) [-uniq] [-id key[:key..]]\n");
+    gprint (GP_ERR, "USAGE: ipptool2book (queue) (book) [-uniq] [-key key[:key..]]\n");
     FREEKEYS;
     return (FALSE);
Index: /trunk/Ohana/src/opihi/pantasks/task.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/task.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/task.c	(revision 11055)
@@ -18,4 +18,10 @@
     remove_argument (N, &argc, argv);
     ListTasks (TRUE);
+    return (TRUE);
+  }
+
+  if ((N = get_argument (argc, argv, "-stats"))) {
+    remove_argument (N, &argc, argv);
+    ListTaskStats ();
     return (TRUE);
   }
@@ -96,4 +102,5 @@
       case TASK_OPTIONS:
       case TASK_PERIODS:
+      case TASK_NPENDING:
       case TASK_ACTIVE:
 	status = command (input, &outline, TRUE);
Index: /trunk/Ohana/src/opihi/pantasks/task_nmax.c
===================================================================
--- /trunk/Ohana/src/opihi/pantasks/task_nmax.c	(revision 11054)
+++ /trunk/Ohana/src/opihi/pantasks/task_nmax.c	(revision 11055)
@@ -20,2 +20,22 @@
   return (FALSE);
 }
+
+int task_npending (int argc, char **argv) {
+
+  Task *task;
+
+  if (argc != 2) goto usage;
+
+  task = GetNewTask ();
+  if (task == NULL) {
+    gprint (GP_ERR, "ERROR: not defining or running a task\n");
+    return (FALSE);
+  }
+
+  task[0].NpendingMax = atoi (argv[1]);
+  return (TRUE);
+
+usage:
+  gprint (GP_ERR, "USAGE: npending N\n");
+  return (FALSE);
+}
