bool parse_actions(SV **spec, int n_spec, struct action *actions, size_t *n_actions, char *aux_buf, size_t *aux_len) {
   bool success;
   size_t action_pos= 0;
   size_t aux_pos= 0;
   int spec_i;

   // Default is one SIGALRM to self
   if (!spec || n_spec == 0) {
      if (*n_actions) {
         actions[0].op= ACT_KILL;
         actions[0].orig_idx= 0;
         actions[0].act.kill.signal= SIGALRM;
         actions[0].act.kill.pid= getpid();
      }
      action_pos++;
   }
   else for (spec_i= 0; spec_i < n_spec; spec_i++) {
      AV *action_spec;
      const char *act_name= NULL;
      STRLEN act_namelen= 0;
      SV **el;
      size_t n_el;
      pid_t common_pid;
      int common_op, common_signal;

      // Get the arrayref for the next action
      if (!(spec[spec_i] && SvROK(spec[spec_i]) && SvTYPE(SvRV(spec[spec_i])) == SVt_PVAV))
         croak("Actions must be arrayrefs");

      // Get the 'command' name of the action
      action_spec= (AV*) SvRV(spec[spec_i]);
      n_el= av_count(action_spec);
      if (n_el < 1 || !(el= av_fetch(action_spec, 0, 0)) || !SvPOK(*el))
         croak("First element of action must be a string");
      act_name= SvPV(*el, act_namelen);

      // Dispatch based on the command
      switch (act_namelen) {
      case 3:
         if (strcmp(act_name, "sig") == 0) {
            if (n_el > 2)
               croak("Too many parameters for 'sig' action");
            common_signal= (n_el == 2 && (el= av_fetch(action_spec, 1, 0)) != NULL && SvOK(*el))?
               parse_signal(*el) : SIGALRM;
            common_pid= getpid();
            goto parse_kill_common;
         }
         if (strcmp(act_name, "run") == 0) {
            common_op= ACT_RUN;
            goto parse_run_common;
         }
      case 4:
         if (strcmp(act_name, "kill") == 0) {
            if (n_el != 3)
               croak("Expected 2 parameters for 'kill' action");
            el= av_fetch(action_spec, 1, 0);
            if (!el || !SvOK(*el))
               croak("Expected Signal as first parameter to 'kill'");
            common_signal= parse_signal(*el);
            el= av_fetch(action_spec, 2, 0);
            if (!el || !SvIOK(*el))
               croak("Expected PID as second parameter to 'kill'");
            common_pid= SvIV(*el);
            goto parse_kill_common;
         }
         if (strcmp(act_name, "exec") == 0) {
            common_op= ACT_EXEC;
            goto parse_run_common;
         }
      case 5:
         if (strcmp(act_name, "sleep") == 0) {
            if (n_el != 2)
               croak("Expected 1 parameter to 'sleep' action");
            el= av_fetch(action_spec, 1, 0);
            if (!el || !SvOK(*el) || !looks_like_number(*el))
               croak("Expected number of seconds in 'sleep' action");
            if (action_pos < *n_actions) {
               actions[action_pos].op= ACT_SLEEP;
               actions[action_pos].orig_idx= spec_i;
               actions[action_pos].act.slp.seconds= SvNV(*el);
            }
            ++action_pos;
            continue;
         }
         if (strcmp(act_name, "close") == 0) {
            common_op= ACT_x_CLOSE;
            goto parse_close_common;
         }
      case 6:
         // The repeat feature opens the possibility of infinite busy-loops,
         // and probably creates more problems than it solves.
         //if (strcmp(act_name, "repeat") == 0) {
         //   int act_count= spec_i;
         //   if (!act_count)
         //      croak("'repeat' cannot be the first action");
         //   if (n_el != 1) { // default is to repeat all, via act_count=i above
         //      if (n_el != 2)
         //         croak("Expected 0 or 1 parameters to 'repeat'");
         //      el= av_fetch(action_spec, 1, 0);
         //      if (!el || !SvOK(*el) || !looks_like_number(*el) || SvIV(*el) <= 0)
         //         croak("Expected positive integer of actions to repeat");
         //      act_count= SvIV(*el);
         //   }
         //   if (action_pos < *n_actions) {
         //      int dest_act_idx, dest_spec_idx= spec_i - act_count;
         //      // Locate the first action record with orig_idx == dest_spec_idx;
         //      for (dest_act_idx= 0; dest_act_idx < spec_i; dest_act_idx++)
         //         if (actions[dest_act_idx].orig_idx == dest_spec_idx)
         //            break;
         //      actions[action_pos].op= ACT_JUMP;
         //      actions[action_pos].orig_idx= spec_i;
         //      actions[action_pos].act.jmp.idx= dest_act_idx;
         //   }
         //   ++action_pos;
         //   continue;
         //}
         if (strcmp(act_name, "shut_r") == 0) {
            common_op= ACT_x_SHUT_R;
            goto parse_close_common;
         }
         if (strcmp(act_name, "shut_w") == 0) {
            common_op= ACT_x_SHUT_W;
            goto parse_close_common;
         }
      case 7:
         if (strcmp(act_name, "shut_rw") == 0) {
            common_op= ACT_x_SHUT_RW;
            goto parse_close_common;
         }
      default:
         croak("Unknown command '%s' in action list", act_name);
      }
      if (0) parse_kill_common: { // arrive from 'kill' and 'sig'
         // common_signal, common_pid will be set.
         // Is there an available action?
         if (action_pos < *n_actions) {
            actions[action_pos].op= ACT_KILL;
            actions[action_pos].orig_idx= spec_i;
            actions[action_pos].act.kill.signal= common_signal;
            actions[action_pos].act.kill.pid= common_pid;
         }
         ++action_pos;
      }
      if (0) parse_run_common: { // arrive from 'run' and 'exec'
         char **argv= NULL, *str;
         STRLEN len;
         int j, argc= n_el-1;
         // common_op will be set.
         if (n_el < 2)
            croak("Expected at least one parameter for '%s'", act_name);
         // Align to pointer boundary within aux_buf
         aux_pos += sizeof(void*) - 1;
         aux_pos &= ~(sizeof(void*) - 1);
         // allocate an array of char* within aux_buf
         // argv remains NULL if there isn't room for it
         if (aux_pos + sizeof(void*) * n_el <= *aux_len)
            argv= (char**)(aux_buf + aux_pos);
         aux_pos += sizeof(void*) * (argc+1);
         // size up each of the strings, and copy them to the buffer if space available
         for (j= 0; j < argc; j++) {
            el= av_fetch(action_spec, j+1, 0);
            if (!el || !*el || !SvOK(*el))
               croak("Found undef element in arguments for '%s'", act_name);
            str= SvPV(*el, len);
            if (argv && aux_pos + len + 1 <= *aux_len) {
               argv[j]= aux_buf + aux_pos;
               memcpy(argv[j], str, len+1);
            }
            aux_pos += len+1;
         }
         // argv lists must end with NULL
         if (argv)
            argv[argc]= NULL;
         // store in an action if space remaining.
         if (action_pos < *n_actions) {
            actions[action_pos].op= common_op;
            actions[action_pos].orig_idx= spec_i;
            actions[action_pos].act.run.argc= argc;
            actions[action_pos].act.run.argv= argv;
         }
         ++action_pos;
      }
      if (0) parse_close_common: { // arrive from 'close', 'shut_r', 'shut_w', 'shut_rw'
         int j;
         const char *str;
         STRLEN len;
         // common_op will be set, and can be ORed with the variant
         if (n_el < 2)
            croak("Expected 1 or more parameters to '%s'", act_name);
         // Each parameter is another action_fd or action_sockname action
         for (j= 1; j < n_el; j++) {
            el= av_fetch(action_spec, j, 0);
            if (!el || !*el || !SvOK(*el))
               croak("'%s' parameter %d is undefined", act_name, j-1);
            // If not a ref...
            if (!SvROK(*el)) {
               // Is it a file descriptor integer?
               if (looks_like_number(*el)) {
                  IV fd= SvIV(*el);
                  if (fd >= 0 && fd < 0x10000) {
                     if (action_pos < *n_actions) {
                        actions[action_pos].op= common_op | ACT_FD_x;
                        actions[action_pos].orig_idx= spec_i;
                        actions[action_pos].act.fd.fd= fd;
                     }
                     ++action_pos;
                     continue;
                  }
               }
               str= SvPV(*el, len);
               // Is the length one of struct sockaddr_in, sockaddr_un, or sockaddr?
               if (len == sizeof(struct sockaddr)
                || len == sizeof(struct sockaddr_in)
                || len == sizeof(struct sockaddr_un)
               ) {
                  if (action_pos < *n_actions) {
                     actions[action_pos].op= common_op | ACT_PNAME_x;
                     actions[action_pos].orig_idx= spec_i;
                     actions[action_pos].act.nam.addr_len= len;
                     actions[action_pos].act.nam.addr= (struct sockaddr*) (aux_buf+aux_pos);
                  }
                  if (aux_pos + len <= *aux_len)
                     memcpy((aux_buf+aux_pos), str, len);
                  aux_pos += len;
                  action_pos++;
                  continue;
               }
               // TODO: parse host:port strings
            }
            else {
               // Try getting a file descriptor from the ref
               int fd= fileno_from_sv(*el);
               if (fd >= 0) {
                  if (action_pos < *n_actions) {
                     actions[action_pos].op= common_op | ACT_FD_x;
                     actions[action_pos].orig_idx= spec_i;
                     actions[action_pos].act.fd.fd= fd;
                  }
                  ++action_pos;
                  continue;
               }
               // TODO: allow notation for socket self-name like { sockname => $x }
               // and maybe allow a full pair of { sockname => $x, peername => $y }
               // or even partial matches like { port => $num }
            }
            str= SvPV(*el, len);
            croak("Invalid parameter to '%s': '%s'; must be integer (fileno), file handle, or socket name like from getpeername", act_name, str);
         }
      }
   }
   success= (action_pos <= *n_actions) && (aux_pos <= *aux_len);
   *n_actions= action_pos;
   *aux_len= aux_pos;
   return success;
}

bool execute_action(struct action *act, bool resume, struct timespec *now_ts, struct socketalarm *parent) {
   int low= act->op & 0xF;
   int high= act->op & ~0xF;
   int how;
   char msgbuf[128];

   switch (high) {
   case ACT_KILL:
      if (kill(act->act.kill.pid, act->act.kill.signal) != 0)
         perror("kill");
      return true; // move to next action
   case ACT_SLEEP: {
      lazy_build_now_ts(now_ts);
      // On initial entry to this action, use current time to calculate the wake time
      if (!resume) {
         double t_seconds;
         t_seconds= (double) now_ts->tv_sec + .000000001 * now_ts->tv_nsec;
         // Add seconds to this time
         t_seconds += act->act.slp.seconds;
         parent->wake_ts.tv_sec= (time_t) t_seconds;
         parent->wake_ts.tv_nsec= (t_seconds - (long) t_seconds) * 1000000000;
         if (!parent->wake_ts.tv_nsec)
            parent->wake_ts.tv_nsec= 1; // because using tv_nsec as a defined-test
         return false; // come back later
      }
      // Else see whether we have reached that time yet
      if (now_ts->tv_sec > parent->wake_ts.tv_sec
         || (now_ts->tv_sec == parent->wake_ts.tv_sec && now_ts->tv_nsec >= parent->wake_ts.tv_nsec))
         return true; // reached end_ts
      return false; // still waiting
   }
   //case ACT_JUMP:
   //   parent->cur_action= act->act.jmp.idx - 1; // parent will ++ after we return true
   //   return true;
   case ACT_FD_x:
      switch (low) {
      case ACT_x_SHUT_R: how= SHUT_RD; if (0)
      case ACT_x_SHUT_W: how= SHUT_WR; if (0)
      case ACT_x_SHUT_RW: how= SHUT_RDWR;
         if (shutdown(act->act.fd.fd, how) < 0) perror("shutdown");
         break;
      default:
         if (close(act->act.fd.fd) < 0) perror("close");
      }
      return true;
   case ACT_PNAME_x:
   case ACT_SNAME_x: {
      struct sockaddr_storage addr;
      socklen_t len;
      int ret, i;
      for (i= 0; i < 1024; i++) {
         len= sizeof(addr);
         ret= high == ACT_PNAME_x? getpeername(i, (struct sockaddr*)&addr, &len)
                                 : getsockname(i, (struct sockaddr*)&addr, &len);
         if (ret == 0 && len == act->act.nam.addr_len
            && memcmp(act->act.nam.addr, &addr, len) == 0
         ) {
            switch (low) {
            case ACT_x_SHUT_R: how= SHUT_RD; if (0)
            case ACT_x_SHUT_W: how= SHUT_WR; if (0)
            case ACT_x_SHUT_RW: how= SHUT_RDWR;
               if (shutdown(i, how) < 0) perror("shutdown");
               break;
            default:
               if (close(i) < 0) perror("close");
            }
         }
      }
      return true;
   }
   case ACT_EXEC: {
      char **argv= act->act.run.argv;
      if (act->op == ACT_RUN) {
         // double-fork, so that parent can reap child, and grandchild gets cleaned up by init()
         pid_t child, gchild;
         if ((child= fork()) < 0) {       // fork failure
            perror("fork");
            return true;
         }
         else if (child > 0) {            // parent - wait for immediate child to return
            int status= -1;
            waitpid(child, &status, 0);
            if (status != 0)
               perror("waitpid");  // not accurate, but probably not going to happen unless child fails to fork
            return true;
         }
         else if ((gchild= fork()) != 0) { // second fork
            if (gchild < 0) perror("fork");
            _exit(gchild < 0? 1 : 0);       // immediately exit
         }
         // else we are the grandchild now
      }
      close(0);
      open("/dev/null", O_RDONLY);
      execvp(argv[0], argv);
      perror("exec"); // if we got here, it failed.  Log the error.
      _exit(1); // make sure we don't continue this process.
   }
   default: {
      int unused= write(2, msgbuf, snprintf(msgbuf, sizeof(msgbuf), "BUG: No such action code %d", act->op));
      (void) unused;
      return true; // pretend success; false would cause it to come back to this action later
   }}
}

const char *act_fd_variant_name(int variant) {
   switch (variant) {
   case ACT_x_CLOSE: return "close";
   case ACT_x_SHUT_R: return "shut_r";
   case ACT_x_SHUT_W: return "shut_w";
   case ACT_x_SHUT_RW: return "shut_rw";
   default: return "BUG";
   }
}

const char *act_fd_variant_description(int variant) {
   switch (variant) {
   case ACT_x_CLOSE: return "close";
   case ACT_x_SHUT_R: return "shutdown SHUT_RD";
   case ACT_x_SHUT_W: return "shutdown SHUT_WR";
   case ACT_x_SHUT_RW: return "shutdown SHUT_RDWR";
   default: return "BUG";
   }
}

static void inflate_action(struct action *act, AV *dest) {
   int low= act->op & 0xF;
   int high= act->op & ~0xF;
   int i;
   switch (high) {
   case ACT_KILL:  av_extend(dest, 2);
      av_push(dest, newSVpvs("kill"));
      av_push(dest, newSViv(act->act.kill.signal));
      av_push(dest, newSViv(act->act.kill.pid));
      return;
   case ACT_SLEEP: av_extend(dest, 1);
      av_push(dest, newSVpvs("sleep"));
      av_push(dest, newSVnv(act->act.slp.seconds));
      return;
   //case ACT_JUMP:  return snprintf(buffer, buflen, "goto %d", (int)act->act.jmp.idx);
   case ACT_FD_x:  av_extend(dest, 1);
      av_push(dest, newSVpv(act_fd_variant_name(low), 0));
      av_push(dest, newSViv(act->act.fd.fd));
      return;
   case ACT_PNAME_x: av_extend(dest, 1);
      av_push(dest, newSVpv(act_fd_variant_name(low), 0));
      av_push(dest, newSVpvn((char*)act->act.nam.addr, act->act.nam.addr_len));
      return;
   case ACT_EXEC: av_extend(dest, act->act.run.argc);
      av_push(dest, newSVpv(act->op == ACT_RUN? "run":"exec", 0));
      for (i= 0; i < act->act.run.argc; i++)
         av_push(dest, newSVpv(act->act.run.argv[i], 0));
      return;
   default:
      croak("BUG: action code %d", act->op);
   }
}

int snprint_action(char *buffer, size_t buflen, struct action *act) {
   int low= act->op & 0xF;
   int high= act->op & ~0xF;
   switch (high) {
   case ACT_KILL:  return snprintf(buffer, buflen, "kill sig=%d pid=%d", (int)act->act.kill.signal, (int) act->act.kill.pid);
   case ACT_SLEEP: return snprintf(buffer, buflen, "sleep %.3lfs", (double)act->act.slp.seconds);
   //case ACT_JUMP:  return snprintf(buffer, buflen, "goto %d", (int)act->act.jmp.idx);
   case ACT_FD_x:  return snprintf(buffer, buflen, "%s %d", act_fd_variant_description(low), act->act.fd.fd);
   case ACT_PNAME_x:
   case ACT_SNAME_x: {
      int pos= snprintf(buffer, buflen, "%s %s ",
         act_fd_variant_description(low),
         high == ACT_PNAME_x? "peername":"sockname"
      );
      return pos + snprint_sockaddr(buffer+pos, buflen > pos? buflen-pos : 0, act->act.nam.addr);
   }
   case ACT_EXEC: {
      int i, pos= snprintf(buffer, buflen, "%sexec(", act->op == ACT_RUN? "fork,fork," : "");
      for (i= 0; i < act->act.run.argc; i++) {
         pos += snprintf(buffer+pos, buflen > pos? buflen-pos : 0, "'%s',", act->act.run.argv[i]);
      }
      // if still in bounds, overwrite final character with ')'
      if (pos < buflen)
         buffer[pos-1]= ')';
      return pos;
   }
   default:
      return snprintf(buffer, buflen, "BUG: action code %d", act->op);
   }
}