wftk core engine

wftk: wftk core engine

[ download ] [ xml source ] [ discussion ]

This is the core workflow engine for the open-source workflow toolkit. It is a prototype, meaning that it is really bare-bones. The basic mode of operation of this version is as a command-line program which accepts one workflow event command on its command line, or a script file on stdin. Commands are simple: start a process, complete or reject a task, set a value. These commands are intended to be used by a task manager such as the to-do manager to interpret a workflow process definition.

Besides its commands, the engine has two sources of input: the process definition and the datasheet. The procdef stores information about a class of processes; the datasheet stores information about the current instance of that class. Both documents are XML documents, and the core engine interprets these documents using James Clark's expat parser, a stable and fairly simple parser.

As the engine performs the commands given it, it emits commands which will be consumed by the task manager. These commands are on stdout and consists of instructions to active tasks, complete the process, or set the owner of the process. In addition, debugging output may be interspersed on stdout and should be ignored by the task manager.

This initial version of wftk core understands nothing about databases. It knows only how to get its process definitions from the local filesystem, and the same goes for datasheet storage. It is wasteful, as it parses both procdef and datasheet on each call (instead of staying resident as a daemon and caching this information. All in good time.) It relies on an external task manager to maintain all database information about active processes and tasks; indeed, it can't even activate a task alone. It can, however, make changes to data, alert users and others of events, and start programs.

Here's how we do all this stuff: [##itemlist##]


This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2000, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license.
wftk core engine: [##label##]

[##label##]

[Previous: [##prevlabel##]] [Top: [##indexlabel##]] [Next: [##nextlabel##]]

[##body##]
[Previous: [##prevlabel##]] [Top: [##indexlabel##]] [Next: [##nextlabel##]]


This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2000, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license.
The structure of the program file is the same as any single-file C program: #include directives, followed by declarations, followed by the main program. A literate presentation is

So let's go ahead and define our #includes, shall we? #include [[stdio.h> #include [[malloc.h> #include [[stdarg.h> #include [[string.h> #include "xmlparse.h" #include "localdefs.h" void output (char type, const char *format, ...); The localdefs.h file contains whatever local definitions a particular wftk installation needs: particularly the locations of the process definition repository and the datasheet repository (remember that in this version both of these are simply directories in the local filesystem, but that later we'll have much more flexible options for both locations.) Next we define all our data structures and helper functions. XML and expat are rather core to this. Under the rubric of "XML" we'll define our central XML data structures and the functions we'll use to work with them. The use of the expat parser to parse XML text into these structures will be in a separate section because it's not trivial. Next up, let's look at how we will read our command input, and how we'll create our output. Commands will be read from the command line or from a command stream and placed into a linked list of command objects. This will enhance the usability of this code in other projects, I hope. It'll also make it easy to write a different parsing front end for the interpreter. Now we're getting close to the main program: let's define our globals. All the underpinnings out of the way, now we can look at the command interpreter. The interpreter is broken into two parts -- really two interpreters. The entire process is driven by the command stack, but the commands on that stack drive the real work of interpretation, which is doing whatever the procdef tells us to do. Execution state is contained in the datasheet, so that it can be reloaded between runs. (Thus the datasheet represents persistent storage implemented via XML -- lots of buzzwords in that!) And hey! Just as you were losing hope, there's some code in this item after all! Not much, granted, but all it has to do is to read the command from the command line, use that to initialize the command stack, and then call the interpreter on the command stack. int main(int argc, char * argv[]) { interpret (command_stack); xml_free (datasheet); xml_free (procdef); return 0; } Here is as good a place as any to define the text we'll print if the command line doesn't suit us. There's not much that will offend us, I'm sure. printf ("usage: wftk [[command> [[arguments>\n"); printf (" Commands:\n"); printf (" start [[process id> [[procdef>\n"); printf (" activate [[process id> [[locator> [[task id>\n"); printf (" complete [[process id> [[task id>\n"); printf (" reject [[process id> [[task id>\n"); printf (" setvalue [[process id> [[name> [[type> [[file> or value on stdin\n"); printf (" settaskvalue [[process id> [[task id> [[name> [[type> [[file> or value on stdin\n"); printf (" script [[process id> [[file> or command list on stdin\n"); I define three basic datastructures for dealing with XML: the element, the attribute, and the element list. Each of these has a struct definition (_element, _attr, and _list respectively) and a pointer typedef (XML, ATTR, and ELEMENTLIST). My apologies that the names don't match up, but it makes sense to me: the struct names reflect a lower-level appreciation for what the objects are, while the typedefs relect a higher-level view of what they're to be used for. Yeah. Anyway, that's my left-brain rationalization for an essentially right-brained nomenclature. typedef struct _element XML; typedef struct _attr ATTR; typedef struct _list ELEMENTLIST; struct _element { char * name; ATTR * attrs; XML * parent; ELEMENTLIST * children; }; struct _attr { char * name; char * value; ATTR * next; }; struct _list { XML * element; ELEMENTLIST * next; ELEMENTLIST * prev; }; Those datastructures suffice to represent an XML file in memory, semi-efficiently (although hash lookup for element and attribute names will be a nice feature at some later date) and completely. To work with those, I'm building up quite a little menagerie of functions, which are listed (mostly) here and defined further down on this page. None is particularly complex. The only function not on this page is the xml_read() function, which uses expat to parse an XML file. That's on the next page because that makes it more convenient to document the handlers used.

So here's my budding XML manipulation API: Writing the contents of one of our XML structures out into a file is simple. We've got two different variants on this function; one writes the entire element (xml_write) and the other writes just the content of the element (xml_writecontent). void xml_writecontent (FILE * file, XML * xml); void xml_write (FILE * file, XML * xml) { ATTR * attr; ELEMENTLIST * list; First, if the element we're working on is plain text, we just write it out. if (xml->name == NULL) { fprintf (file, "%s", xml->attrs->value); return; } It's a regular element, so we open the element and write the name. fprintf (file, "[[%s", xml->name); attr = xml->attrs; while (attr != NULL) { fprintf (file, " %s=\"%s\"", attr->name, attr->value); attr = attr->next; } If the element has no children (this includes text), then we close the tag as an empty tag, and we're finished. if (xml->children == NULL) { fprintf (file, "/>"); return; } else fprintf (file, ">"); Otherwise we track down the list of children and write each of them, recursively. xml_writecontent (file, xml); And finally, if there were children, then we need to close the tag with the full close. fprintf (file, "[[/%s>", xml->name); } The weakness of this function currently is that in the absence of plain text there will never be a line break. Not good -- but I don't see a good algorithm for doing it better while ruling out the possibility of inserting line breaks where they'll be errors.

Let's go ahead and define our xml_writecontent. void xml_writecontent (FILE * file, XML * xml) { ELEMENTLIST * list; list = xml->children; while (list) { xml_write (file, list->element); list = list->next; } } Prepending to a linked list is, of course, very easy. void xml_prepend (XML * parent, XML * child) { ELEMENTLIST * list; child->parent = parent; list = (ELEMENTLIST *) malloc (sizeof(struct _list)); list->element = child; list->prev = NULL; list->next = parent->children; parent->children = list; } It's appending where we run into problems. void xml_append (XML * parent, XML * child) { ELEMENTLIST * list; ELEMENTLIST * ch; child->parent = parent; list = (ELEMENTLIST *) malloc (sizeof(struct _list)); list->element = child; list->prev = NULL; list->next = NULL; if (parent->children == NULL) { parent->children = list; return; } ch = parent->children; while (ch->next != NULL) ch = ch->next; list->prev = ch; ch->next = list; } XML * xml_loc (XML * start, const char * loc) { char * mark; const char * attrval; char piece[64]; int i; int count; if (!loc) return (start); if (!*loc) return (start); while (start #^7#^7 start->name == NULL) start = xml_next (start); if (!start) return (NULL); while (*loc == ' ') loc++; i = 0; while (*loc #^7#^7 *loc != '.') piece[i++] = *loc++; piece[i] = '\0'; if (*loc) loc++; while (*loc == ' ') loc++; mark = strchr (piece, ']'); if (mark) *mark = '\0'; mark = strchr (piece, '('); if (mark) { *mark++ = '\0'; count = atoi (mark); mark = NULL; } else { count = 0; mark = strchr (piece, '['); if (mark) { *mark++ = '\0'; } } while (start) { if (start->name == NULL) { start = xml_next (start); continue; } if (strcmp (start->name, piece)) { start = xml_next (start); continue; } if (count) { count --; start = xml_next (start); continue; } if (!mark) { if (*loc) return (xml_loc (xml_first (start), loc)); return (start); } attrval = xml_attrval(start, "id"); if (attrval) { if (strcmp (attrval, mark)) { start = xml_next (start); continue; } if (*loc) return (xml_loc (xml_first(start), loc)); return (start); } attrval = xml_attrval(start, "name"); if (attrval) { if (strcmp (attrval, mark)) { start = xml_next (start); continue; } if (*loc) return (xml_loc (xml_first(start), loc)); return (start); } } return (NULL); } Building our locator is recursive. We build our parent's locator, append a dot, and qualify it. void xml_getloc (XML * xml, char *loc, int len) { int s; int count; XML * sib; if (xml->parent != NULL) { xml_getloc (xml->parent, loc, len); } else { *loc = '\0'; } s = strlen (loc); if (s > 0 #^7#^7 s #^lt# len-1) { strcat (loc, "."); s++; } len -= s; loc += s; if (strlen(xml->name) #^lt# len) { strcpy (loc, xml->name); } else { strncpy (loc, xml->name, len-1); loc[len-1] = '\0'; } if (xml->parent == NULL) return; sib = xml_first(xml->parent); count = 0; while (sib != xml #^7#^7 sib != NULL) { if (sib->name != NULL) { if (!strcmp (sib->name, xml->name)) count ++; } sib = xml_next(sib); } if (count > 0 #^7#^7 s > 4) { strcat (loc, "("); sprintf (loc + strlen(loc), "%d", count); strcat (loc, ")"); } } Setting an attribute is a little complicated. If the attribute is already represented in the element's attribute list, then we free the old value, allocate space for a copy of the new value, and copy it. Otherwise we allocate a new attribute holder and copy both name and value into it. void xml_set (XML * xml, const char * name, const char * value) { ATTR * attr; attr = xml->attrs; while (attr) { if (!strcmp (attr->name, name)) break; attr = attr->next; } if (attr) { free ((void *) (attr->value)); attr->value = (char *) malloc (strlen (value) + 1); strcpy (attr->value, value); return; } if (xml->attrs == NULL) { attr = (ATTR *) malloc (sizeof (struct _attr)); xml->attrs = attr; } else { attr = xml->attrs; while (attr->next) attr = attr->next; attr->next = (ATTR *) malloc (sizeof (struct _attr)); attr = attr->next; } attr->next = NULL; attr->name = (char *) malloc (strlen (name) + 1); strcpy (attr->name, name); attr->value = (char *) malloc (strlen (value) + 1); strcpy (attr->value, value); } void xml_setnum (XML * xml, const char *attr, int number) { char buf[sizeof(number) * 3 + 1]; sprintf (buf, "%d", number); xml_set (xml, attr, buf); } Retriving a value, on the other hand, is rather simple. const char * xml_attrval (XML * element,const char * name) { ATTR * attr; attr = element->attrs; while (attr) { if (!strcmp (attr->name, name)) return (attr->value); attr = attr->next; } return (""); } int xml_attrvalnum (XML * element, const char * name) { return (atoi (xml_attrval (element, name))); } Creation of an element is nothing more than allocating the space, then allocating space for a copy of the name and copying it. XML * xml_create (const char * name) { XML * ret; ret = (XML *) malloc (sizeof (struct _element)); ret->name = (char *) malloc (strlen (name) + 1); strcpy (ret->name, name); ret->attrs = NULL; ret->children = NULL; return (ret); } I represent character data (plain old text) as an element with no name. The first (nameless) attribute contains the text. Instead of using xml_create to do this, then using xml_set to set the attribute, I'm defining a special create function for plain text chunks.

And for easy compatibility with expat, there's a version which takes a pointer and length instead of assuming null termination. XML * xml_createtext (const char * value) { XML * ret; ret = (XML *) malloc (sizeof (struct _element)); ret->name = NULL; ret->children = NULL; ret->attrs = (ATTR *) malloc (sizeof (struct _attr)); ret->attrs->name = NULL; ret->attrs->next = NULL; ret->attrs->value = (char *) malloc (strlen (value) + 1); strcpy (ret->attrs->value, value); return (ret); } XML * xml_createtextlen (const char * value, int len) { XML * ret; ret = (XML *) malloc (sizeof (struct _element)); ret->name = NULL; ret->children = NULL; ret->attrs = (ATTR *) malloc (sizeof (struct _attr)); ret->attrs->name = NULL; ret->attrs->next = NULL; ret->attrs->value = (char *) malloc (len + 1); strncpy (ret->attrs->value, value, len); ret->attrs->value[len] = '\0'; return (ret); } To free an XML element, we free its name, each of its attributes (and their names and values), each child (recursively) and the list element which held the child, and finally we can free the XML element itself. void xml_free (XML * xml) { ATTR * attr; ELEMENTLIST * list; if (xml == NULL) return; if (xml->name != NULL) free ((void *) (xml->name)); while (xml->attrs) { attr = xml->attrs; xml->attrs = xml->attrs->next; if (attr->name != NULL) free ((void *) (attr->name)); if (attr->value != NULL) free ((void *) (attr->value)); xml->attrs = attr->next; free ((void *) attr); } while (xml->children) { list = xml->children; xml->children = list->next; if (list->element != NULL) xml_free (list->element); free ((void *) list); } free ((void *) xml); } Deleting a piece out of an XML structure is more than just freeing it; we have to close ranks before and after as well. void xml_delete(XML * piece) { ELEMENTLIST * list; if (!piece) return; if (piece->parent != NULL) { list = piece->parent->children; while (list != NULL && list->element != piece) list = list->next; if (list != NULL) { if (list->next != NULL) list->next->prev = list->prev; if (list->prev != NULL) list->prev->next = list->next; } if (list == piece->parent->children) piece->parent->children = list->next; free ((void *) list); } xml_free (piece); } Finding the first child is, of course, very easy. The last is less so.

I've also tossed in a function xml_firstelem which is just lie xml_first except that it doesn't see plain text elements. XML * xml_first(XML * xml) { if (xml == NULL) return NULL; if (xml->children == NULL) return NULL; return (xml->children->element); } XML * xml_firstelem(XML * xml) { ELEMENTLIST *list; if (xml == NULL) return NULL; list = xml->children; while (list != NULL) { if (list->element->name != NULL) break; list = list->next; } if (list != NULL) return (list->element); return NULL; } XML * xml_last(XML *xml) { ELEMENTLIST *list; list = xml->children; if (list == NULL) return NULL; while (list->next != NULL) list = list->next; return (list->element); } For next and previous, we have to find the current element in its parent's children list, and then we're good to go. Each function comes in two flavors: one sees plain text and the other (e.g. xml_nextelem) doesn't. XML * xml_next(XML * xml) { ELEMENTLIST *list; if (xml == NULL) return (NULL); if (xml->parent == NULL) return (NULL); list = xml->parent->children; while (list != NULL && list->element != xml) list = list->next; if (list == NULL) return (NULL); if (list->next == NULL) return (NULL); return (list->next->element); } XML * xml_nextelem(XML * xml) { ELEMENTLIST *list; if (xml == NULL) return (NULL); if (xml->parent == NULL) return (NULL); list = xml->parent->children; while (list != NULL && list->element != xml) list = list->next; if (list == NULL) return (NULL); while (list->next != NULL) { if (list->next->element->name != NULL) break; list = list->next; } if (list->next == NULL) return (NULL); return (list->next->element); } XML * xml_prev(XML * xml) { ELEMENTLIST *list; if (xml == NULL) return (NULL); if (xml->parent == NULL) return (NULL); list = xml->parent->children; while (list != NULL && list->element != xml) list = list->next; if (list == NULL) return (NULL); if (list->prev == NULL) return (NULL); return (list->prev->element); } XML * xml_prevelem(XML * xml) { ELEMENTLIST *list; if (xml == NULL) return (NULL); if (xml->parent == NULL) return (NULL); list = xml->parent->children; while (list != NULL #^7#^7 list->element != xml) list = list->next; if (list == NULL) return (NULL); while (list->prev != NULL) { if (list->prev->element->name != NULL) break; list = list->prev; } if (list->prev == NULL) return (NULL); return (list->prev->element); } I don't need this just at the moment, so I'll skip it for now. The basic structure of the parser is identical to any expat application. We create the parser and pass in a pointer to the XML we're building up as the user data. We register the handlers for elements and for plain text, and we're not interested in anything else.

Then we simply throw pieces of the input stream at the parser until we're through with it. The handlers do all the work of creating and inserting XML pieces into the growing structure. If we encounter an error, we free all the stuff we've already done; otherwise we return the structure at the conclusion of the parse. XML * xml_read (FILE * file) { XML_Parser parser; char buf[BUFSIZ]; int done; XML * ret; ret = NULL; parser = XML_ParserCreate(NULL); XML_SetUserData (parser, (void *) &ret); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser, charData); done = 0; do { size_t len = fread(buf, 1, sizeof(buf), file); done = len #^lt# sizeof(buf); if (!XML_Parse(parser, buf, len, done)) { output ('E', "XML error: %s at line %d", XML_ErrorString(XML_GetErrorCode(parser)), XML_GetCurrentLineNumber(parser)); xml_free (ret); return NULL; } } while (!done); XML_ParserFree(parser); return (ret); } The startElement handler, then, does a great deal of the work of creating XML data structures. The userData parameter points to the immediate parent of the node being encountered. When we open a new node, we allocate the data structure and copy attributes, append the new node to its parent, then we set userData to point to the new node -- when the element closes, we move userData up the chain back to the parent.

In the case of an empty element, expat fortunately calls first the open handler, then the close handler, so whether we have an explicitly empty element or not doesn't matter.

It's astounding how much simpler this startElement is than the corresponding handler in xmltools! void startElement(void *userData, const char *name, const char **atts) { XML ** parent; XML * element; element = xml_create (name); while (*atts) { xml_set(element, *atts++, *atts++); } parent = (XML **) userData; if (*parent != NULL) xml_append (*parent, element); *parent = element; } At the close of the element, we just jump up the tree to the parent. If there is no parent, then we stay put. Thus if there are for some reason two root elements in the input, the structure won't reflect the input, but the first root element won't get stranded, either. void endElement(void *userData, const char *name) { XML ** element; element = (XML **) userData; if ((*element)->parent != NULL) *element = (*element)->parent; } Character data is even easier. We just create a new text structure and append it onto the parent. End of story. void charData (void *userData, const XML_Char *s, int len) { XML ** parent; parent = (XML **) userData; xml_append (*parent, xml_createtextlen ((char *) s, len)); } The term "command stack" is probably a misnomer. What the structure really is, is a list of things to do to the process. Generally these things to do are task transitions (tasks have been activated or completed) -- so I had thought to call these transitions. But then another command is to start a project, and then yet another is to set a value in a datasheet (which should be done through the workflow engine to allow thread-safe usage, later.)

So I finally decided that commands were what they are. Sorry for the cheesy limit of 5 arguments to a command, but I really don't want to get into a double-malloc situation for a totally throwaway data structure. typedef struct _command COMMAND; struct _command { char * name; int argc; char * argv[5]; COMMAND * next; }; The next member, of course, is to allow commands to be strung together into a linked list. Note that this "command language" is really simple, because the "commands" are really notifications from the task manager that something has happened which the engine needs to act upon. Maybe "messages" would be a better terminology -- but nope, they're commands now. Live with it.

Well, so the other thing we want to define here is a handy little function to tack a command onto a list of commands. COMMAND * command_add (COMMAND * list, char * name, int argc, ...) { COMMAND * cmd; int i; va_list argv; va_start(argv, argc); cmd = (COMMAND *) malloc (sizeof (struct _command)); cmd->name = name; cmd->argc = argc; for (i=0; i < argc; i++) { cmd->argv[i] = va_arg(argv, char *); } va_end(argv); cmd->next = NULL; if (!list) return (cmd); while (list->next != NULL) list = list->next; list->next = cmd; return cmd; } Oh, and we'll want a function to free up lists, too: void command_freelist (COMMAND * list) { COMMAND * cmd; while (list) { cmd = list; list = list->next; free ((void *) cmd); } } And I guess one more function, this one to take a file stream and load up a command list from it. The version I have here is rather crude but we'll presumably improve on it later. void command_load (COMMAND * list, FILE * file) { } Oh. Well, that's even cruder than I had intended, heh, but I'll get to it soon enough. The output stream for this incarnation of wftk-core is simply a list of lines. The first character of each line tells what it does:

  • A: activate task
  • C: complete process
  • O: set owner of process
  • D: debugging output
  • E: error output
The simplest way of doing this, of course, is with a nice little call to vprintf. The output stream is always on stdout. void output (char type, const char *format, ...) { va_list args; va_start (args, format); printf ("%c ", type); vprintf (format, args); va_end (args); printf ("\n"); }
The command stream is simply a list of notifications of events which have occurred. The engine must open the datasheet for the process involved, find the procdef which applies, and then move down the command list, making appropriate changes to the datasheet as defined in the procdef. As this is done, output lines are being written to communicate back to the task manager what things have been changes.

Most particularly, when the procdef tells the engine that it is time to activate a task, the engine must stop and notify the task manager. This is simply because the task manager must assign the task instance a unique identifier. Once this is done, the task manager must call the core engine again to active the task, because the datasheet must reflect the task ID of the new task. (I think.) void interpret (COMMAND * list) { FILE * temp; int datasheet_dirty; char line[1024]; char * mark; XML * holder; datasheet_dirty = 0; while (list) { if (!strcmp (list->name, "start")) { } else if (!strcmp (list->name, "complete")) { } else if (!strcmp (list->name, "reject")) { } else if (!strcmp (list->name, "setvalue")) { } else if (!strcmp (list->name, "script")) { } else { output ('E', "Unknown command %s encountered", list->name); } skip_command: list = list->next; } if (datasheet != NULL && datasheet_dirty) { sprintf (sbuf, "%s%s", DATASHEET_REPOSITORY, process); temp = fopen (sbuf, "w"); if (!temp) { output ('E', "Can't write to datasheet for process %s.", process); } else { xml_write (temp, datasheet); fclose (temp); } } } Starting a process consists of these steps:

  • Finding the process definition
  • Opening a datasheet and marking the current version of the procdef into it
  • Calling the procdef interpreter on the procdef
  • Writing the datasheet out to the appropriate file
Of these steps, the last, writing the datasheet, is taken care of centrally. If the datasheet_dirty flag is set, then the datasheet will be written when the interpreter finishes.

So let's find our process definition. The procdef identifier we're given is a general name; we have to find the current version of that procdef and mark that version as the one we're actually using. Otherwise if changes are made, things are going to get rapidly out of whack.

So in the process definition repository, there is a file procdef_versions.txt for procdef ID 'procdef'. That consists of lines of text, the first tab-delimited field of which is the number of a version. The last line, therefore, identifies the current version.

For version 2a of procdef procdef, the actual definition XML will be in the file procdef_2a.xml. That's what we'll write into our datasheet as our governing procdef.

And yeah, I'm using a goto here. Sue me. if (procdef != NULL) { output ('E', "Start command must be first. Skipping command."); goto skip_command; } if (list->argc < 1) { sprintf (sbuf, "%s%s", DATASHEET_REPOSITORY, process); temp = fopen (sbuf, "r"); if (!temp) { output ('E', "No process '%s' exists.", process); return; } datasheet = xml_read (temp); fclose (temp); sprintf (sbuf, "%s%s", PROCESS_DEFINITION_REPOSITORY, xml_attrval (datasheet, "procdef")); } else { sprintf (sbuf, "%s%s_versions.txt", PROCESS_DEFINITION_REPOSITORY, list->argv[0]); temp = fopen (sbuf, "r"); if (!temp) { output ('E', "No process '%s' defined.", list->argv[0]); return; } *line = '\0'; while (!feof (temp)) { fgets(line, 1023, temp); } fclose (temp); mark = strchr (line, '\t'); if (mark) *mark = '\0'; sprintf (sbuf, "%s%s_%s.xml", PROCESS_DEFINITION_REPOSITORY, list->argv[0], line); } temp = fopen (sbuf, "r"); if (!temp) { output ('E', "Process definition version file '%s' is missing.", sbuf); return; } procdef = xml_read(temp); fclose (temp); if (datasheet == NULL) { datasheet = xml_create("datasheet"); sprintf (sbuf, "%s_%s.xml", list->argv[0], line); xml_set(datasheet, "procdef", sbuf); } datasheet_dirty = 1; output ('N', xml_attrval (procdef, "name")); output ('O', xml_attrval (procdef, "author")); holder = xml_loc (procdef, "workflow.note[description]"); if (holder != NULL) { xml_writecontent (stdout, holder); } printf ("\nEOF\n"); queue_procdef (procdef); process_procdef(); To complete a task, we:

  • Load datasheet and procdef
  • Mark the task as complete
  • Hand off to the procdef interpreter
load_datasheet(); mark = strrchr (list->argv[0], ':'); if (mark) mark++; else (mark = list->argv[0]); sprintf (sbuf, "queue.item[%s]", mark); holder = xml_loc (queue, sbuf); if (holder) xml_set (holder, "block", "resume"); datasheet_dirty = 1; process_procdef();
Rejection of a task is pretty much the same as completion, except that we invoke exception handlers. To set a value, we simply:
  • Load the datasheet
  • Set the value
The script command allows us to use command files (or stdin) to signify several events at once. Note that all commands in a single run must be applied to the same process, and that process is specified on the command line. The commands to be found in a script file lack the process ID, because it's already been specified.

In keeping with the idea that these commands are really state transitions, we will simply tack commands from script files onto the end of the current list. Thus we really don't care that scripts run in order -- all this is conceptually happening simultaneously. temp = NULL; if (list->argc) { output ('D', "Script %s", list->argv[0]); temp = fopen (list->argv[0], "r"); if (!temp) { output ('E', "Unable to open script file '%s'", list->argv[0]); } } else { output ('D', "Script on stdin"); temp = stdin; } if (temp) { output ('D', "This is where the script would be loaded.", list->argv[0]); if (temp != stdin) fclose (temp); } This is a function, to be called at the beginning of every command except for "start". If the datasheet is already loaded, then nothing happens. void load_datasheet () { FILE * temp; if (datasheet) return; sprintf (sbuf, "%s%s", DATASHEET_REPOSITORY, process); temp = fopen (sbuf, "r"); if (!temp) { output ('E', "Can't open datasheet for process %s.", process); return; } datasheet = xml_read (temp); fclose (temp); state = xml_loc (datasheet, "datasheet.state"); idcount = xml_attrvalnum (state, "idcount"); queue = xml_loc (datasheet, "datasheet.state.queue"); sprintf (sbuf, "%s%s", PROCESS_DEFINITION_REPOSITORY, xml_attrval (datasheet, "procdef")); temp = fopen (sbuf, "r"); if (!temp) { output ('E', "Can't open process definition for process %s.", process); xml_delete (datasheet); datasheet = NULL; return; } procdef = xml_read (temp); fclose (temp); } Interpretation of the process definition, as explained above, is driven by the commands passed in at invocation time. The commands which interest us primarily are 'start' and 'complete'; each of these hands off to the procdef interpreter.

The procdef interpreter runs largely recursively, reflecting the nature of the XML data structure. It is called with an XML structure and runs in the context of the currently loaded datasheet.

Note the use of goto to simulate tail recursion. XML * queue_procdef (XML * action) { XML * item; if (action == NULL) return; if (state == NULL) { state = xml_create("state"); xml_append (datasheet, state); } if (queue == NULL) { queue = xml_create("queue"); xml_append (state, queue); } item = xml_create("item"); xml_setnum (item, "id", idcount++); xml_set (item, "type", action->name); xml_getloc (action, sbuf, 1023); xml_set (item, "loc", sbuf); xml_append (queue, item); return (item); } void process_procdef() { XML * item; XML * def; XML * holder; XML * task; XML * data; XML * next; const char * type; int count; int keep; item = xml_first (queue); while (item != NULL) { if (!strcmp("yes", xml_attrval(item, "block"))) { item = xml_next(item); continue; } def = xml_loc (procdef, xml_attrval(item, "loc")); type = xml_attrval (item, "type"); keep = 0; if (!strcmp(type, "workflow") || !strcmp(type, "sequence")) { } else if (!strcmp (type, "parallel")) { } else if (!strcmp (type, "task")) { } else if (!strcmp (type, "data")) { } else if (!strcmp (type, "situation")) { } else if (!strcmp (type, "if") || !strcmp (type, "elseif")) { } else if (!strcmp (type, "alert")) { } else if (!strcmp (type, "start")) { } if (keep) { xml_set (item, "block", "yes"); item = xml_next(item); } else { if (strcmp ("workflow", type)) { sprintf (sbuf, "queue.item[%d]", xml_attrvalnum (item, "parent")); next = xml_loc (queue, sbuf); xml_delete (item); item = next; xml_set (item, "block", "no"); } else { xml_delete (item); item = NULL; } } } sprintf (sbuf, "%d", idcount); xml_set (state, "idcount", sbuf); } The sequence handler takes care of the sequence tag and also the contents of the outer workflow tag (which are executed sequentially). If the "cur" attribute is not yet yet, then this is the first time we've encountered this sequence, and we queue up the first child of the sequence (and note its location with "cur", of course). Otherwise, we find the child located by "cur", find its next sibling, and queue that up.

If something gets queued, then we block. if (!strcmp ("", xml_attrval (item, "cur"))) { next = xml_firstelem (def); } else { next = xml_loc (procdef, xml_attrval (item, "cur")); next = xml_nextelem (next); } if (next) { xml_set (queue_procdef (next), "parent", xml_attrval (item, "id")); xml_getloc (next, sbuf, sizeof(sbuf) - 1); xml_set (item, "cur", sbuf); keep = 1; } else if (!strcmp (type, "workflow")) { output ('F', "Process %s complete.", process); } The parallel item queues up all its children, then blocks. When a child completes, it counts the number of children complete; when its counter decrements to zero, it completes. if (!strcmp ("", xml_attrval (item, "remaining"))) { count = 0; next = xml_firstelem (def); while (next != NULL) { count ++; xml_set (queue_procdef (next), "parent", xml_attrval (item, "id")); next = xml_nextelem (next); } } else { count = xml_attrvalnum (item, "remaining"); count--; } xml_setnum (item, "remaining", count); if (count > 0) keep = 1; Doing a task is nothing more than setting up task data and telling the task manager that the task has been activated. We'll take as the task ID the process ID plus our internal task ID; this will make things easier to handle in the task manager, as it means our task IDs will always be unique (well, assuming the task manager always gives us unique process IDs.) if (strcmp (xml_attrval (item, "block"), "resume")) { sprintf (sbuf, "%s:%s", process, xml_attrval (item, "id")); task = xml_create ("task"); xml_set (task, "id", sbuf); xml_append (datasheet, task); output ('A', "%s-%s-%s", sbuf, xml_attrval (def, "role"), xml_attrval (def, "label")); holder = xml_firstelem (def); while (holder != NULL) { if (!strcmp (holder->name, "data")) { data = xml_create ("data"); xml_set (data, "id", xml_attrval (holder, "name")); xml_set (data, "type", xml_attrval (holder, "type")); xml_append (task, data); } holder = xml_nextelem (holder); } keep = 1; } For the time being, anyway, we're passing alerts back to the task manager for handling. So our output will be an 'L' line containing the recipient, followed by the contents of the alert, terminated by a line containing "EOF" by itself. The task manager can do whatever it wants with this information. output ('L', "%s:%s", xml_attrval(def, "type"), xml_attrval(def, "to")); xml_writecontent (stdout, def); printf ("\nEOF\n"); The state of the current process is kept in global variables. These globals would naturally be put into a struct in a daemon version of the core engine, but for a command-line version, globals make more sense.

Our three chief data structures are the process definition, the datasheet, and the command stack. XML * procdef; XML * datasheet; COMMAND * command_stack; Then we have some special parts of the datasheet which we'll point to separately. XML * state; XML * queue; int idcount; char * process; char sbuf[1024]; About the only thing the initialization code has to do at this point is to read the command off the command line and call command_add to start off the list.

The big switch is horrendous but I'm too rushed to think of an elegant way to do this -- get yer name here, folks! Step right up and tell me how it's done! if (argc #^lt# 3) { } process = argv[2]; switch (argc) { case 3: command_stack = command_add (NULL, argv[1], 0); break; case 4: command_stack = command_add (NULL, argv[1], 1, argv[3]); break; case 5: command_stack = command_add (NULL, argv[1], 2, argv[3], argv[4]); break; case 6: command_stack = command_add (NULL, argv[1], 3, argv[3], argv[4], argv[5]); break; case 7: command_stack = command_add (NULL, argv[1], 4, argv[3], argv[4], argv[5], argv[6]); break; } It's sort of silly to have this little function hanging around on its own page, but it doesn't really fit in anywhere else all that well. It's pretty obvious how this thing works, isn't it? You start at the bottom of the stack, and print location pieces as you move to the top. void print_stack() { FRAME * cur; for (cur = stack.next; cur != NULL; cur = cur->next) { if (cur != stack.next) printf ("."); printf ("%s", cur->name); if (cur->offset_in_parent > 0) { printf ("(%d)", cur->offset_in_parent); } } printf ("\n"); }