XPAServer: The XPA Server-side Programming Interface

Summary

A description of the XPA server-side programming interface.

Introduction to XPA Server Programming

Creating an XPA server is easy: you generally only need to call the XPANew() subroutine to define a named XPA access point and set up the send and receive callback routines. You then enter an event loop such as XPAMainLoop() to field XPA requests.

  #include <xpa.h>

  XPA XPANew(char *class, char *name, char *help,
      int (*send_callback)(), void *send_data, char *send_mode,
      int (*rec_callback)(),  void *rec_data,  char *rec_mode);

  XPA XPACmdNew(char *class, char *name);

  XPACmd XPACmdAdd(XPA xpa,
         char *name, char *help,
         int (*send_callback)(), void *send_data, char *send_mode,
         int (*rec_callback)(),  void *rec_data,  char *rec_mode);

  void XPACmdDel(XPA xpa, XPACmd cmd);

  XPA XPAInfoNew(char *class, char *name,
      int (*info_callback)(), void *info_data, char *info_mode);

  int XPAFree(XPA xpa);

  void XPAMainLoop(void);

  int XPAPoll(int msec, int maxreq);

  void XPAAtExit(void);

  void XPACleanup(void);

Introduction

To use the XPA application programming interface, a software developer generally will include the xpa.h definitions file:
  #include <xpa.h>
in the software module that defines or accesses an XPA access point, and then will link against the libxpa.a library:
  gcc -o foo foo.c libxpa.a
XPA has been compiled using both C and C++ compilers.

A server program generally defines an XPA access point by calling the XPANew() routine and specifies "send" and/or "receive" callback procedures to be executed by the program when an external process either sends data or commands to this access point or requests data or information from this access point. A program also can define several sub-commands for a single access point by calling XPACmdNew() and XPACmdAdd() instead. Having defined one or more public access points in this way, an XPA server program enters its usual event loop (or uses the standard XPA event loop).

XPANew: create a new XPA access point

  #include <xpa.h>

  XPA XPANew(char *class, char *name, char *help,
	     int (*send_callback)(),
	     void *send_data, char *send_mode,
	     int (*rec_callback)(),
	     void *rec_data,  char *rec_mode);

Create a new XPA public access point with the class:name identifier template and enter this access point into the XPA name server, so that it can be accessed by external processes. XPANew() returns an XPA struct. Note that the length of the class and name designations must be less than or equal to 1024 characters each.

The XPA name server daemon, xpans, will be started automatically if it is not running already (assuming it can be found in the path). The program's ip address and listening port are specified by the environment variable XPA_NSINET, which takes the form :. If no such environment variable exists, then xpans is started on the current machine listening on port 14285. It also uses 14286 as a known port for its public access point (so that routines do not have to go to the name server to find the name server ip and port!) As of XPA 2.1.1, version information is exchanged between the xpans process and the new access point. If the access point uses an XPA major/minor version newer than xpans, a warning is issued by both processes, since mixing of new servers and old xpa programs (xpaset, xpaget, xpans, etc.) is not likely to work. You can turn off the warning message by setting the XPA_VERSIONCHECK environment variable to "false".

The help string is meant to be returned by a request from xpaget:

  xpaget class:name -help

A send_callback and/or a receive_callback can be specified; at least one of them must be specified.

A send_callback can be specified that will be executed in response to an external request from the xpaget program, the XPAGet() routine, or XPAGetFd() routine. This callback is used to send data to the requesting client.

The calling sequence for send_callback() is:

  int send_callback(void *send_data, void *call_data,
    char *paramlist, char **buf, int *len)
  {
    XPA xpa = (XPA)call_data;
    ...
    return(stat);
  }

The send_mode string is of the form: "key1=value1,key2=value2,..." The following keywords are recognized:

  key   	value		default		explanation
  ------	--------	--------	-----------
  acl		true/false	true		enable access control
  freebuf	true/false	true		free buf after callback completes

The call_data should be recast to the XPA struct as shown. In addition, client-specific data can be passed to the callback in send_data.

The paramlist will be supplied by the client as qualifying parameters for the callback. There are two ways in which the send_callback() routine can send data back to the client:

1. The send_callback() routine can fill in a buffer and pass back a pointer to this buffer. An integer len also is returned to specify the number of bytes of data in buf. XPA will send this buffer to the client after the callback is complete.

2. The send_callback can send data directly to the client by writing to the fd pointed by the macro:

  xpa_datafd(xpa)

Note that this fd is of the kind returned by socket() or open().

If a buf has been allocated by a standard malloc routine, filled, and returned to XPA, then freebuf generally is set so that the buffer will be freed automatically when the callback is completed and data has been sent to the client. If a static buf is returned, freebuf should be set to false to avoid a system error when freeing static storage. Note that default value for freebuf implies that the callback will allocate a buffer rather than use static storage.

On the other hand, if buf is dynamically allocated using a method other than a standard malloc/calloc/realloc routine (e.g. using Perl's memory allocation and garbage collection scheme), then it is necessary to tell XPA how to free the allocated buffer. To do this, use the XPASetFree() routine within your callback:

  void XPASetFree(XPA xpa, void (*myfree)(void *), void *myfree_ptr);
The first argument is the usual XPA handle. The second argument is the special routine to call to free your allocated memory. The third argument is an optional pointer. If not NULL, the specified free routine is called with that pointer as its sole argument. If NULL, the free routine is called with the standard buf pointer as its sole argument. This is useful in cases where there is a mapping between the buffer pointer and the actual allocated memory location, and the special routine is expecting to be passed the former.

If, while the callback performs its processing, an error occurs that should be communicated to the client, then the routine XPAError should be called:

  XPAError(XPA xpa, char *s);

where s is an arbitrary error message. The returned error message string will be of the form:

  XPA$ERROR   [error] (class:name ip:port)

If the callback wants to send a specific acknowledgment message back to the client, the routine XPAMessage can be called:

  XPAMessage(XPA xpa, char *s);

where s is an arbitrary error message. The returned error message string will be of the form:

  XPA$MESSAGE [message] (class:name ip:port)

Otherwise, a standard acknowledgment is sent back to the client after the callback is completed.

The callback routine should return 0 if no error occurs, or -1 to signal an error.

A receive_callback can be specified that will be executed in response to an external request from the xpaset program, or the XPASet (or XPASetFd()) routine. This callback is used to process data received from an external process.

The calling sequence for receive_callback is:

  int receive_callback(void *receive_data, void *call_data,
    char *paramlist, char *buf, int len)
  {
    XPA xpa = (XPA)call_data;
    ...
    return(stat);
  }

The mode string is of the form: "key1=value1,key2=value2,..." The following keywords are recognized:

  key   	value		default		explanation
  ------	--------	--------	-----------
  acl		true/false	true		enable access control
  buf		true/false	true		server expects data bytes from client
  fillbuf	true/false	true		read data into buf before executing callback
  freebuf	true/false	true		free buf after callback completes

The call_data should be recast to the XPA struct as shown. In addition, client-specific data can be passed to the callback in receive_data.

The paramlist will be supplied by the client. In addition, if the receive_mode keywords buf and fillbuf are true, then on entry into the receive_callback() routine, buf will contain the data sent by the client. If buf is true but fillbuf is false, it becomes the callback's responsibility to retrieve the data from the client, using the data fd pointed to by the macro xpa_datafd(xpa). If freebuf is true, then buf will be freed when the callback is complete.

If, while the callback is performing its processing, an error occurs that should be communicated to the client, then the routine XPAError can be called:

  XPAError(XPA xpa, char *s);

where s is an arbitrary error message.

The callback routine should return 0 if no error occurs, or -1 to signal an error.

XPACmdNew: create a new XPA public access point for commands

  #include <xpa.h>

  XPA XPACmdNew(char *class, char *name);

Create a new XPA public access point for commands that will share a common identifier class:name. Enter this access point into the XPA name server, so that it can be accessed by external processes. XPACmdNew() returns an XPA struct.

It often is more convenient to have one public access point that can manage a number of commands, rather than having individual access points for each command. For example, it is easier to command the ds9 image display using:

  echo "colormap I8"   | xpaset ds9
  echo "scale log"     | xpaset ds9
  echo "file foo.fits" | xpaset ds9

then to use:

  echo "I8"       | xpaset ds9_colormap
  echo "log"      | xpaset ds9_scale
  echo "foo.fits" | xpaset ds9_file

In the first case, the commands remain the same regardless of the target XPA name. In the second case, the command names must change for each instance of ds9. That is, if a second instance of ds9 called DS9 were running, it would be commanded either as:

  echo "colormap I8"   | xpaset DS9
  echo "scale log"     | xpaset DS9
  echo "file foo.fits" | xpaset DS9

or as:

  echo "I8"       | xpaset DS9_colormap
  echo "log"      | xpaset DS9_scale
  echo "foo.fits" | xpaset DS9_file

Thus, in cases where a program is going to manage many commands, it generally is easier to define them as commands associated with the XPACmdNew() routine, rather than as separate access points using XPANew().

When XPACmdNew() is called, only the class:name identifier is specified. Each sub-command is subsequently defined using the XPACmdAdd() routine.

XPACmdAdd: add a command to an XPA command public access point

  #include <xpa.h>

  XPACmd XPACmdAdd(XPA xpa, char *name, char *help,
	           int (*send_callback)(),
		   void *send_data, char *send_mode,
         	   int (*rec_callback)(),
		   void *rec_data,  char *rec_mode);

Add a command to an XPA command access point. The XPA argument specifies the XPA struct returned by a call to XPANewCmd(). The name argument is the name of the command. The other arguments function identically to the arguments in the XPANew() command, i.e., the send_callback and rec_callback routines have identical calling sequences to their XPANew() counterparts, with the exceptions noted below.

When help is requested for a command access point using:

  xpaget -h class:name

all of the command help strings are listed. To get help for a given command, use:

  xpaget -h class:name cmd

Also, the acl keyword in the send_mode and receive_mode strings is global to the access point, not local to the command. Thus, the value for the acl mode should be the same in all send_mode (or receive_mode) strings for each command in a command access point. (The acl for send_mode need not be the same as the acl for receive_mode, though).

XPACmdDel: remove a command from an XPA command public access point

  #include <xpa.h>

  void XPACmdDel(XPA xpa, XPACmd cmd);

This routine removes a command from the list of available commands in a given XPA. That command will no longer be available for processing.

XPAInfoNew: define an XPA info public access point

  #include <xpa.h>

  XPA XPAInfoNew(char *class, char *name,
	         int (*info_callback)(),
		 void *info_data, char *info_mode);

[NB: this is an experimental interface, new to XPA 2.0, whose value and best use is evolving.]

A program can register interest in receiving a short message about a particular topic from any other process that cares to send such a message. Neither has to be an XPA server. For example, if a user starts to work with a new image file called new.fits, she might wish to alert interested programs about this new file by sending a short message using xpainfo:

  xpainfo IMAGEFILE /data/new.fits

In this example, each process that has used the XPAInfoNew() call to register interest in messages associated with the identifier IMAGEFILE will have its info_callback() executed with the following calling sequence:

  int info_cb(void *info_data, void *call_data, char *paramlist)
  {
    XPA xpa = (XPA)call_data;
  }

The arguments passed to this routine are equivalent to those sent in the send_callback() routine. The main difference is that there is no buf sent to the info callback: this mechanism is meant for short announcement of messages of interest to many clients.

The mode string is of the form: "key1=value1,key2=value2,..." The following keywords are recognized:

  key   	value		default		explanation
  ------	--------	--------	-----------
  acl		true/false	true		enable access control

Because no buf is passed to this callback, the usual buf-related keywords are not applicable here.

The information sent in the parameter list is arbitrary. However, we envision sending information such as file names or XPA access points from which to collect more data. Note that the xpainfo program and the XPAInfo() routine that cause the info_callback to execute do not wait for the callback to complete before returning.

XPAFree: remove an XPA public access point


  #include <xpa.h>

  int XPAFree(XPA xpa);

Remove the specified XPA public access point from the name server and free all associated storage. Note that removal from the name server happens automatically when the process terminates, so this call is not generally needed. It is used when public access points are being defined temporarily and then destroyed when no longer needed. For example, ds9 temporarily creates a public access point when it loads a new image for display and destroys it when the image is unloaded.

XPAMainLoop: optional main loop for XPA

  #include <xpa.h>

  void XPAMainLoop();

Once XPA access points have been defined, a program must enter an event loop to watch for requests from external programs. This can be done in a variety of ways, depending on whether the event loop is processing events other than XPA events. In cases where there are no non-XPA events to be processed, the program can simply call the XPAMainLoop() event loop. This loop is implemented essentially as follows (error checking is simplified in this example):

  FD_ZERO(&readfds);
  while( XPAAddSelect(NULL, &readfds) ){
    if( sgot = select(swidth, &readfds, NULL, NULL, NULL) >0 )
      XPAProcessSelect(&readfds, 0);
    else
      break;
    FD_ZERO(&readfds);
  }

The XPAAddSelect() routine sets up the select() readfds variable so that select() will wait for I/O on all the active XPA channels. It returns the number of XPAs that are active; the loop will end when there are no active XPAs. The standard select() routine is called to wait for an external I/O request. Since no timeout struct is passed in argument 5, the select() call hangs until there is an external request. When an external I/O request is made, the XPAProcessSelect() routine is executed to process the pending requests. In this routine, the maxreq value determines how many requests will be processed: if maxreq <=0, then all currently pending requests will be processed. Otherwise, up to maxreq requests will be processed. (The most usual values for maxreq is 0 to process all requests.)

If a program has its own Unix select() loop, then XPA access points can be added to it by using a variation of the standard XPAMainLoop:

  XPAAddSelect(xpa, &readfds);
  [app-specific ...]
  if( select(width, &readfds, ...) ){
    XPAProcessSelect(&readfds, maxreq);
    [app-specific ...]
    FD_ZERO(&readfds);
  }

XPAAddSelect() is called before select() to add the access points. If the first argument is NULL, then all active XPA access points are added. Otherwise only the specified access point is added. After select() is called, the XPAProcessSelect() routine can be called to process XPA requests. Once again, the maxreq value determines how many requests will be processed: if maxreq <=0, then all currently pending requests will be processed. Otherwise, up to maxreq requests will be processed.

XPA access points can be added to Xt event loops (using XtAppMainLoop()) and Tcl/Tk event loops (using vwait and the Tk loop). When using XPA with these event loops, you only need to call:

int XPAXtAddInput(XtAppContext app, XPA xpa)
or
  int XPATclAddInput(XPA xpa)
respectively before entering the loop.

XPAPoll: execute existing XPA requests

  #include <xpa.h>

  int XPAPoll(int msec, int maxreq);

It is sometimes desirable to implement a polling loop, i.e., where one checks for and processes XPA requests without blocking. For this situation, use the XPAPoll() routine:

  XPAPoll(int msec, int maxreq);

The XPAPoll() routine will perform XPAAddSelect() and select(), but with a timeout specified in millisecs by the msec argument. If one or more XPA requests are made before the timeout expires, the XPAProcessSelect() routine is called to process those requests. The maxreq value determines how many requests will be processed: if maxreq < 0, then no events are processed, but instead, the return value indicates the number of events that are pending. If maxreq == 0, then all currently pending requests will be processed. Otherwise, up to maxreq requests will be processed. (The most usual values for maxreq are 0 to process all requests and 1 to process one request).

XPAAtExit: install exit handler


  #include <xpa.h>

  void XPAAtExit(void);

XPAAtExit() will install an exit handler using atexit() to run XPAFree on all XPA access points. This might be useful in cases where Unix sockets are being used: if an explicit call to XPAFree() is not made by the program, the Unix socket file will not be deleted immediately without an atexit handler. (NB: this call should not be made in a Tcl/Tk application. Accessing the Tcl native file system after Tcl has shut down all file systems causes the Tcl/Tl program to crash).

XPACleanup: release reserved XPA memory


  #include <xpa.h>

  void XPACleanup(void);

When XPA is initialized, it allocates a small amount of memory for the access control list, temp directory path, and reserved commands. This memory is found by valgrind to be "still reachable", meaning that "your program didn't free some memory it could have". Calling the XPACleanup() routine before exiting the program will free this memory and make valgrind happy.

XPA Server Callback Macros

  #include <xpa.h>

  xpa_class, xpa_name, xpa_method, xpa_cmdfd, xpa_datafd,
  xpa_sendian, xpa_cendian

Server routines have access to information about the XPA being called via the following macros (each of which takes the xpa handle as an argument):

  macro		 	explanation
  ------		-----------
  xpa_class		class of this xpa
  xpa_name		name of this xpa
  xpa_method		method string (inet or local connect info)
  xpa_cmdfd		fd of command socket
  xpa_datafd		fd of data socket
  xpa_sendian		endian-ness of server ("little" or "big")
  xpa_cendian		endian-ness of client ("little" or "big"

The argument to these macros is the call_data pointer that is passed to the server procedure. This pointer should be type case to XPA in the server routine:

  XPA xpa = (XPA)call_data;

The most important of these macros is xpa_datafd(). A server routine that sets "fillbuf=false" in receive_mode or send_mode can use this macro to perform I/O directly to/from the client, rather than using buf.

The xpa_cendian and xpa_sendian macros can be used together to determine if the data transferred from the client is byte swapped with respect to the server. Values for these macros are: "little", "big", or "?". In order to do a proper conversion, you still need to know the format of the data (i.e., byte swapping is dependent on the size of the data element being converted).

XPA Race Conditions

Potential XPA race conditions and how to avoid them.

Currently, there is only one known circumstance in which XPA can get (temporarily) deadlocked in a race condition: if two or more XPA servers send messages to one another using an XPA client routine such as XPASet(), they can deadlock while each waits for the other server to respond. (This can happen if the servers call XPAPoll() with a time limit, and send messages in between the polling call.) The reason this happens is that both client routines send a string to the other server to establish the handshake and then wait for the server response. Since each client is waiting for a response, neither is able to enter its event-handling loop and respond to the other's request. This deadlock will continue until one of the timeout periods expire, at which point an error condition will be triggered and the timed-out server will return to its event loop.

Starting with version 2.1.6, this rare race condition can be avoided by setting the XPA_IOCALLSXPA environment variable for servers that will make client calls. Setting this variable causes all XPA socket IO calls to process outstanding XPA requests whenever the primary socket is not ready for IO. This means that a server making a client call will (recursively) process incoming server requests while waiting for client completion. It also means that a server callback routine can handle incoming XPA messages if it makes its own XPA call. The semi-public routine oldvalue=XPAIOCallsXPA(newvalue) can be used to turn this behavior off and on temporarily. Passing a 0 will turn off IO processing, 1 will turn it back on. The old value is returned by the call.

By default, the XPA_IOCALLSXPA option is turned off, because we judge that the added code complication and overhead involved will not be justified by the amount of its use. Moreover, processing XPA requests within socket IO can lead to non-intuitive results, since incoming server requests will not necessarily be processed to completion in the order in which they are received.

Aside from setting XPA_IOCALLSXPA, the simplest way to avoid this race condition is to multi-process: when you want to send a client message, simply start a separate process to call the client routine, so that the server is not stopped. It probably is fastest and easiest to use fork() and then have the child call the client routine and exit. But you also can use either the system() or popen() routine to start one of the command line programs and do the same thing. Alternatively, you can use XPA's internal launch() routine instead of system(). Based on fork() and exec(), this routine is more secure than system() because it does not call /bin/sh.

Starting with version 2.1.5, you also can send an XPAInfo() message with the mode string "ack=false". This will cause the client to send a message to the server and then exit without waiting for any return message from the server. This UDP-like behavior will avoid the server deadlock when sending short XPAInfo messages.

Go to XPA Help Index

Last updated: September 10, 2003