The SUTAgent must handle multiple connections and concurrent events. It is expected, however, that often only one or two connections will be established. Threading, therefore, is considered overkill and too big a potential source of bugs. We shall instead use a reactor-based model for concurrency.
The majority of SUTAgent commands are very simple and can easily be executed synchronously, that is, returning a response immediately in the scope of a single event-loop iteration. Therefore, they don't need to keep state, and we can simplify our command-handler code.
There are a few exceptions to this, namely, exec, pull, push, and possibly hash. To service these requests, we will use specific event handlers that override the main command event handler for the duration of the command.
For platform independence, we will use NSPR.
BufferedSocket is just a wrapper around a socket that allows buffering in userspace.
The Reactor is the central event dispatcher. It uses PR_Poll() from NSPR. Since PR_Poll() only listens to events on sockets, we will need to use polling via timeouts to check subprocesses launched via exec rather than pipe handles. Waiting on filesystem events is unsupported on most operating systems anyway.
The Reactor owns EventHandlers and calls handle_event() on them when a socket event is detected or a timeout is reached. The Reactor deletes a handler when it is closed.
An abstract class with three functions, all used by the Reactor:
- getPollDescs(): returns any descriptors for which the handler would like notifications
- handleEvent(): takes a list of events for which the Reactor received a notification in the last iteration. The Handler does application-specific processing here.
- closed(): indicates if a handler is finished, which means it should be deleted by the Reactor (or proxy, see below).
An EventHandler that listens for connections on the given socket and creates CommandEventHandlers, adding them to the Reactor.
A CommandEventHandler represents one session, e.g. the duration of a connection. Because connections can have two states--commands and data--it may act as a proxy to another EventHandler, for potentially long-running commands like 'push' or 'pull' that require some state.
Its handleEvent() implementation either reads a line and passes it to handleLine(), if in command mode, or just passes the event to the data event handler. It also flushes the BufferedSocket output buffer, if needed.
Similarly, its getPollDescs() implementation returns either just a read event for the socket, if in command mode, or collects descriptors from the data event handler. It also adds a write event for the BufferedSocket, if there is buffered, unsent data.
PushFileEventHandler / PullFileEventHandler
Represents a data stream. Used for push and pull commands. Maintains some state, i.e. how much of the file has been received or sent. When finished, closes itself, so that the CommandEventHandler deletes it, returning to regular command processing.
Represents a subprocess. Its getPollDescs() just returns a timeout, since, for platform-independence, we have to poll subprocesses to determine when they are complete. It may also write data to the BufferedSocket. It is expected that this will have to be subclassed for each platform and probably created via a platform-specific factory object. When the process exits, this EventHandler closes, so that the CommandEventHandler can delete it and return to command processing.