diff --git a/.gitignore b/.gitignore index e9adc2e..b3fbca3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ *.d *.o *.out -bin/ -build/ +bin/* +build/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4df88be..e8dd157 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,10 @@ image: hwrunner:latest variables: GIT_SSL_NO_VERIFY: "true" - EXEC: mush - HW_DIR: hw4 + EXEC: pbx + HW_DIR: hw5 + CPU_LIMIT: 60 + FILE_LIMIT: 1000000 before_script: - make clean all -C ${HW_DIR} stages: @@ -16,8 +18,12 @@ build: run: stage: run script: - - cd ${HW_DIR} && bin/${EXEC} < rsrc/run_test.mush + - ulimit -t ${CPU_LIMIT} + - ulimit -f ${FILE_LIMIT} + - cd ${HW_DIR} && bin/${EXEC} test: stage: test script: - - cd ${HW_DIR} && bin/${EXEC}_tests -S --verbose=0 --timeout 5 + - ulimit -t ${CPU_LIMIT} + - ulimit -f ${FILE_LIMIT} + - cd ${HW_DIR} && bin/${EXEC}_tests -S --verbose=0 -j1 --timeout 50 diff --git a/hw5/Makefile b/hw5/Makefile new file mode 100644 index 0000000..e31789a --- /dev/null +++ b/hw5/Makefile @@ -0,0 +1,81 @@ +CC := gcc +SRCD := src +TSTD := tests +BLDD := build +BIND := bin +INCD := include +LIBD := lib +UTILD := util + +MAIN := $(BLDD)/main.o +LIB := $(LIBD)/pbx.a +LIB_DB := $(LIBD)/pbx.a + +ALL_SRCF := $(shell find $(SRCD) -type f -name *.c) +ALL_OBJF := $(patsubst $(SRCD)/%,$(BLDD)/%,$(ALL_SRCF:.c=.o)) +ALL_FUNCF := $(filter-out $(MAIN), $(ALL_OBJF)) + +TEST_SRC := $(shell find $(TSTD) -type f -name *.c) + +INC := -I $(INCD) + +CFLAGS := -Wall -Werror -Wno-unused-function -Wno-error=switch -MMD +DFLAGS := -g -DDEBUG -DCOLOR +PRINT_STAMENTS := -DERROR -DSUCCESS -DWARN -DINFO + +STD := -std=gnu11 +TEST_LIB := -lcriterion +LIBS := $(LIB) -lpthread +LIBS_DB := $(LIB_DB) -lpthread +EXCLUDES := excludes.h + +CFLAGS += $(STD) -DTEST_CONFIG_C + +EXEC := pbx +TEST_EXEC := $(EXEC)_tests + +.PHONY: clean all setup debug + +all: setup $(BIND)/$(EXEC) $(INCD)/$(EXCLUDES) $(BIND)/$(TEST_EXEC) + +debug: CFLAGS += $(DFLAGS) $(PRINT_STAMENTS) +debug: all + +tester: $(UTILD)/tester + +setup: $(BIND) $(BLDD) +$(BIND): + mkdir -p $(BIND) +$(BLDD): + mkdir -p $(BLDD) + +$(UTILD)/tester: $(UTILD)/tester.c src/globals.c + $(CC) $(DFLAGS) $(INC) $^ -o $@ + +$(BIND)/$(EXEC): $(MAIN) $(ALL_FUNCF) + $(CC) $^ -o $@ $(LIBS) + +$(BIND)/$(TEST_EXEC): $(ALL_FUNCF) $(TEST_SRC) + $(CC) $(CFLAGS) $(INC) $(ALL_FUNCF) $(TEST_SRC) $(TEST_LIB) $(LIBS) -o $@ + +$(BLDD)/%.o: $(SRCD)/%.c + $(CC) $(CFLAGS) $(INC) -c -o $@ $< + +clean: + rm -rf $(BLDD) $(BIND) + +$(INCD)/$(EXCLUDES): $(BIND)/$(EXEC) + rm -f $@ + touch $@ + if nm $(BIND)/$(EXEC) | grep INSTRUCTOR_MAIN > /dev/null; then \ + echo "#define NO_MAIN" >> $@; \ + fi + if nm $(BIND)/$(EXEC) | grep INSTRUCTOR_SERVER > /dev/null; then \ + echo "#define NO_SERVER" >> $@; \ + fi + if nm $(BIND)/$(EXEC) | grep INSTRUCTOR_PBX > /dev/null; then \ + echo "#define NO_PBX" >> $@; \ + fi + +.PRECIOUS: $(BLDD)/*.d +-include $(BLDD)/*.d diff --git a/hw5/demo/pbx b/hw5/demo/pbx new file mode 100755 index 0000000..ef31d4a Binary files /dev/null and b/hw5/demo/pbx differ diff --git a/hw5/hw5.sublime-project b/hw5/hw5.sublime-project new file mode 100644 index 0000000..e7b6e29 --- /dev/null +++ b/hw5/hw5.sublime-project @@ -0,0 +1,46 @@ +{ + "folders": + [ + { + "path":".", + "name":"Project Base" + }, + { + "path": "src", + "name": "C Source", + "follow_symlinks": false, + "file_include_patterns":["*.c"], + }, + { + "path": "include", + "name": "C Headers", + "follow_symlinks": false, + "file_include_patterns":["*.h"], + }, + { + "path": "tests", + "name": "Tests", + } + ], + "settings": + { + }, + "build_systems": + [ + { + "name": "Release (full build)", + "working_dir":"$project_path", + "shell_cmd": "make clean all", + }, + { + "name": "Debug (full build)", + "working_dir":"$project_path", + "shell_cmd": "make clean debug", + }, + { + "name": "Test", + "working_dir":"$project_path", + "shell_cmd": "bin/${project_base_name}_tests}", + } + ] +} diff --git a/hw5/include/debug.h b/hw5/include/debug.h new file mode 100644 index 0000000..e8fc8b6 --- /dev/null +++ b/hw5/include/debug.h @@ -0,0 +1,88 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#include + +#define NL "\n" + +#ifdef COLOR +#define KNRM "\033[0m" +#define KRED "\033[1;31m" +#define KGRN "\033[1;32m" +#define KYEL "\033[1;33m" +#define KBLU "\033[1;34m" +#define KMAG "\033[1;35m" +#define KCYN "\033[1;36m" +#define KWHT "\033[1;37m" +#define KBWN "\033[0;33m" +#else +#define KNRM "" +#define KRED "" +#define KGRN "" +#define KYEL "" +#define KBLU "" +#define KMAG "" +#define KCYN "" +#define KWHT "" +#define KBWN "" +#endif + +#ifdef VERBOSE +#define DEBUG +#define INFO +#define WARN +#define ERROR +#define SUCCESS +#endif + +#ifdef DEBUG +#define debug(S, ...) \ + do { \ + fprintf(stderr, KMAG "DEBUG: %s:%s:%d " KNRM S NL, __FILE__, \ + __extension__ __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + } while (0) +#else +#define debug(S, ...) +#endif + +#ifdef INFO +#define info(S, ...) \ + do { \ + fprintf(stderr, KBLU "INFO: %s:%s:%d " KNRM S NL, __FILE__, \ + __extension__ __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + } while (0) +#else +#define info(S, ...) +#endif + +#ifdef WARN +#define warn(S, ...) \ + do { \ + fprintf(stderr, KYEL "WARN: %s:%s:%d " KNRM S NL, __FILE__, \ + __extension__ __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + } while (0) +#else +#define warn(S, ...) +#endif + +#ifdef SUCCESS +#define success(S, ...) \ + do { \ + fprintf(stderr, KGRN "SUCCESS: %s:%s:%d " KNRM S NL, __FILE__, \ + __extension__ __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + } while (0) +#else +#define success(S, ...) +#endif + +#ifdef ERROR +#define error(S, ...) \ + do { \ + fprintf(stderr, KRED "ERROR: %s:%s:%d " KNRM S NL, __FILE__, \ + __extension__ __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + } while (0) +#else +#define error(S, ...) +#endif + +#endif /* DEBUG_H */ diff --git a/hw5/include/pbx.h b/hw5/include/pbx.h new file mode 100644 index 0000000..8157e73 --- /dev/null +++ b/hw5/include/pbx.h @@ -0,0 +1,52 @@ +/** + * === DO NOT MODIFY THIS FILE === + * If you need some other prototypes or constants in a header, please put them + * in another header file. + * + * When we grade, we will be replacing this file with our own copy. + * You have been warned. + * === DO NOT MODIFY THIS FILE === + */ +#ifndef PBX_H +#define PBX_H + +#include +#include + +#include "tu.h" + +/* + * Structure types representing objects manipulated by the PBX module. + * + * PBX: Represents the current state of a private branch exchange. + * TU: Represents the current state of a telephone unit. + * + * NOTE: These types are "opaque": the actual structure definitions are not + * given here and it is not intended that a client of the PBX module should + * know what they are. The actual structure definitions are local to the + * implementation of the PBX module and are not exported. + */ +typedef struct pbx PBX; + +/* + * Maximum number of extensions supported by a PBX. + */ +#define PBX_MAX_EXTENSIONS FD_SETSIZE + +/* + * End-of-line sequence used in communication with client. + */ +#define EOL "\r\n" + +/* + * Global variable that provides access to the PBX instance. + */ +extern PBX *pbx; + +PBX *pbx_init(); +void pbx_shutdown(PBX *pbx); +int pbx_register(PBX *pbx, TU *tu, int ext); +int pbx_unregister(PBX *pbx, TU *tu); +int pbx_dial(PBX *pbx, TU *tu, int ext); + +#endif diff --git a/hw5/include/server.h b/hw5/include/server.h new file mode 100644 index 0000000..0e26f78 --- /dev/null +++ b/hw5/include/server.h @@ -0,0 +1,49 @@ +/** + * === DO NOT MODIFY THIS FILE === + * If you need some other prototypes or constants in a header, please put them + * in another header file. + * + * When we grade, we will be replacing this file with our own copy. + * You have been warned. + * === DO NOT MODIFY THIS FILE === + */ +#ifndef SERVER_H +#define SERVER_H + +/* + * Definitions of the commands that can be issued by a client. + */ +typedef enum tu_command { + TU_PICKUP_CMD, TU_HANGUP_CMD, TU_DIAL_CMD, TU_CHAT_CMD, + // Below are special values used in grading tests. + TU_NO_CMD = 100, TU_CONNECT_CMD = 101, TU_DISCONNECT_CMD = 102, + TU_AWAIT_CMD = 103, TU_DELAY_CMD = 104, TU_EOF_CMD = 105 +} TU_COMMAND; + +/* + * Array that specifies a printable name for each of the commands that + * can be issued to a TU by a client. These names should be used when + * parsing commands received from a client. They may also be used for + * debugging purposes. + */ +extern char *tu_command_names[]; + +/* + * Thread function for the thread that handles a particular client. + * + * @param Pointer to a variable that holds the file descriptor for + * the client connection. This variable must be freed once the file + * descriptor has been retrieved. + * @return NULL + * + * This function executes a "service loop" that receives messages from + * the client and dispatches to appropriate functions to carry out + * the client's requests. The service loop ends when the network connection + * shuts down and EOF is seen. This could occur either as a result of the + * client explicitly closing the connection, a timeout in the network causing + * the connection to be closed, or the main thread of the server shutting + * down the connection as part of graceful termination. + */ +void *pbx_client_service(void *arg); + +#endif diff --git a/hw5/include/tu.h b/hw5/include/tu.h new file mode 100644 index 0000000..0b2e7f3 --- /dev/null +++ b/hw5/include/tu.h @@ -0,0 +1,52 @@ +/** + * === DO NOT MODIFY THIS FILE === + * If you need some other prototypes or constants in a header, please put them + * in another header file. + * + * When we grade, we will be replacing this file with our own copy. + * You have been warned. + * === DO NOT MODIFY THIS FILE === + */ +#ifndef TU_H +#define TU_H + +/* + * Structure types representing objects manipulated by the TU module. + * + * TU: Represents the current state of a telephone unit. + * + * NOTE: These types are "opaque": the actual structure definitions are not + * given here and it is not intended that a client of the TU module should + * know what they are. The actual structure definitions are local to the + * implementation of the TU module and are not exported. + */ +typedef struct tu TU; + +/* + * The possible states that a TU can be in. + */ +typedef enum tu_state { + TU_ON_HOOK, TU_RINGING, TU_DIAL_TONE, TU_RING_BACK, TU_BUSY_SIGNAL, + TU_CONNECTED, TU_ERROR +} TU_STATE; + +/* + * Array that specifies a printable name for each of the TU states. + * These names should be used when sending state-change notifications to + * the underlying network clients. They may also be used for debugging + * purposes. + */ +extern char *tu_state_names[]; + +TU *tu_init(int fd); +void tu_ref(TU *tu, char *reason); +void tu_unref(TU *tu, char *reason); +int tu_fileno(TU *tu); +int tu_extension(TU *tu); +int tu_set_extension(TU *tu, int ext); +int tu_pickup(TU *tu); +int tu_hangup(TU *tu); +int tu_dial(TU *tu, TU *target); +int tu_chat(TU *tu, char *msg); + +#endif diff --git a/hw5/lib/pbx.a b/hw5/lib/pbx.a new file mode 100644 index 0000000..929ac31 Binary files /dev/null and b/hw5/lib/pbx.a differ diff --git a/hw5/src/globals.c b/hw5/src/globals.c new file mode 100644 index 0000000..3668061 --- /dev/null +++ b/hw5/src/globals.c @@ -0,0 +1,35 @@ +/** + * === DO NOT MODIFY THIS FILE === + * If you need some other prototypes or constants in a header, please put them + * in another header file. + * + * When we grade, we will be replacing this file with our own copy. + * You have been warned. + * === DO NOT MODIFY THIS FILE === + */ + +#include "pbx.h" +#include "server.h" + +char *tu_state_names[] = { + [TU_ON_HOOK] "ON HOOK", + [TU_RINGING] "RINGING", + [TU_DIAL_TONE] "DIAL TONE", + [TU_RING_BACK] "RING BACK", + [TU_BUSY_SIGNAL] "BUSY SIGNAL", + [TU_CONNECTED] "CONNECTED", + [TU_ERROR] "ERROR" +}; + +char *tu_command_names[] = { + [TU_PICKUP_CMD] "pickup", + [TU_HANGUP_CMD] "hangup", + [TU_DIAL_CMD] "dial", + [TU_CHAT_CMD] "chat" +}; + +/* + * Object that maintains the state of the Private Branch Exchange (PBX). + */ +PBX *pbx; + diff --git a/hw5/src/main.c b/hw5/src/main.c new file mode 100644 index 0000000..b32c9ab --- /dev/null +++ b/hw5/src/main.c @@ -0,0 +1,44 @@ +#include +#include + +#include "pbx.h" +#include "server.h" +#include "debug.h" + +static void terminate(int status); + +/* + * "PBX" telephone exchange simulation. + * + * Usage: pbx + */ +int main(int argc, char* argv[]){ + // Option processing should be performed here. + // Option '-p ' is required in order to specify the port number + // on which the server should listen. + + // Perform required initialization of the PBX module. + debug("Initializing PBX..."); + pbx = pbx_init(); + + // TODO: Set up the server socket and enter a loop to accept connections + // on this socket. For each connection, a thread should be started to + // run function pbx_client_service(). In addition, you should install + // a SIGHUP handler, so that receipt of SIGHUP will perform a clean + // shutdown of the server. + + fprintf(stderr, "You have to finish implementing main() " + "before the PBX server will function.\n"); + + terminate(EXIT_FAILURE); +} + +/* + * Function called to cleanly shut down the server. + */ +static void terminate(int status) { + debug("Shutting down PBX..."); + pbx_shutdown(pbx); + debug("PBX server terminating"); + exit(status); +} diff --git a/hw5/src/pbx.c b/hw5/src/pbx.c new file mode 100644 index 0000000..10232b3 --- /dev/null +++ b/hw5/src/pbx.c @@ -0,0 +1,91 @@ +/* + * PBX: simulates a Private Branch Exchange. + */ +#include + +#include "pbx.h" +#include "debug.h" + +/* + * Initialize a new PBX. + * + * @return the newly initialized PBX, or NULL if initialization fails. + */ +#if 0 +PBX *pbx_init() { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Shut down a pbx, shutting down all network connections, waiting for all server + * threads to terminate, and freeing all associated resources. + * If there are any registered extensions, the associated network connections are + * shut down, which will cause the server threads to terminate. + * Once all the server threads have terminated, any remaining resources associated + * with the PBX are freed. The PBX object itself is freed, and should not be used again. + * + * @param pbx The PBX to be shut down. + */ +#if 0 +void pbx_shutdown(PBX *pbx) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Register a telephone unit with a PBX at a specified extension number. + * This amounts to "plugging a telephone unit into the PBX". + * The TU is initialized to the TU_ON_HOOK state. + * The reference count of the TU is increased and the PBX retains this reference + *for as long as the TU remains registered. + * A notification of the assigned extension number is sent to the underlying network + * client. + * + * @param pbx The PBX registry. + * @param tu The TU to be registered. + * @param ext The extension number on which the TU is to be registered. + * @return 0 if registration succeeds, otherwise -1. + */ +#if 0 +int pbx_register(PBX *pbx, TU *tu, int ext) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Unregister a TU from a PBX. + * This amounts to "unplugging a telephone unit from the PBX". + * The TU is disassociated from its extension number. + * Then a hangup operation is performed on the TU to cancel any + * call that might be in progress. + * Finally, the reference held by the PBX to the TU is released. + * + * @param pbx The PBX. + * @param tu The TU to be unregistered. + * @return 0 if unregistration succeeds, otherwise -1. + */ +#if 0 +int pbx_unregister(PBX *pbx, TU *tu) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Use the PBX to initiate a call from a specified TU to a specified extension. + * + * @param pbx The PBX registry. + * @param tu The TU that is initiating the call. + * @param ext The extension number to be called. + * @return 0 if dialing succeeds, otherwise -1. + */ +#if 0 +int pbx_dial(PBX *pbx, TU *tu, int ext) { + // TO BE IMPLEMENTED + abort(); +} +#endif diff --git a/hw5/src/server.c b/hw5/src/server.c new file mode 100644 index 0000000..8dc6dd1 --- /dev/null +++ b/hw5/src/server.c @@ -0,0 +1,21 @@ +/* + * "PBX" server module. + * Manages interaction with a client telephone unit (TU). + */ +#include + +#include "debug.h" +#include "pbx.h" +#include "server.h" + +/* + * Thread function for the thread that handles interaction with a client TU. + * This is called after a network connection has been made via the main server + * thread and a new thread has been created to handle the connection. + */ +#if 0 +void *pbx_client_service(void *arg) { + // TO BE IMPLEMENTED + abort(); +} +#endif diff --git a/hw5/src/tu.c b/hw5/src/tu.c new file mode 100644 index 0000000..580b3cd --- /dev/null +++ b/hw5/src/tu.c @@ -0,0 +1,205 @@ +/* + * TU: simulates a "telephone unit", which interfaces a client with the PBX. + */ +#include + +#include "pbx.h" +#include "debug.h" + +/* + * Initialize a TU + * + * @param fd The file descriptor of the underlying network connection. + * @return The TU, newly initialized and in the TU_ON_HOOK state, if initialization + * was successful, otherwise NULL. + */ +#if 0 +TU *tu_init(int fd) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Increment the reference count on a TU. + * + * @param tu The TU whose reference count is to be incremented + * @param reason A string describing the reason why the count is being incremented + * (for debugging purposes). + */ +#if 0 +void tu_ref(TU *tu, char *reason) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Decrement the reference count on a TU, freeing it if the count becomes 0. + * + * @param tu The TU whose reference count is to be decremented + * @param reason A string describing the reason why the count is being decremented + * (for debugging purposes). + */ +#if 0 +void tu_unref(TU *tu, char *reason) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Get the file descriptor for the network connection underlying a TU. + * This file descriptor should only be used by a server to read input from + * the connection. Output to the connection must only be performed within + * the PBX functions. + * + * @param tu + * @return the underlying file descriptor, if any, otherwise -1. + */ +#if 0 +int tu_fileno(TU *tu) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Get the extension number for a TU. + * This extension number is assigned by the PBX when a TU is registered + * and it is used to identify a particular TU in calls to tu_dial(). + * The value returned might be the same as the value returned by tu_fileno(), + * but is not necessarily so. + * + * @param tu + * @return the extension number, if any, otherwise -1. + */ +#if 0 +int tu_extension(TU *tu) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Set the extension number for a TU. + * A notification is set to the client of the TU. + * This function should be called at most once one any particular TU. + * + * @param tu The TU whose extension is being set. + */ +#if 0 +int tu_set_extension(TU *tu, int ext) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Initiate a call from a specified originating TU to a specified target TU. + * If the originating TU is not in the TU_DIAL_TONE state, then there is no effect. + * If the target TU is the same as the originating TU, then the TU transitions + * to the TU_BUSY_SIGNAL state. + * If the target TU already has a peer, or the target TU is not in the TU_ON_HOOK + * state, then the originating TU transitions to the TU_BUSY_SIGNAL state. + * Otherwise, the originating TU and the target TU are recorded as peers of each other + * (this causes the reference count of each of them to be incremented), + * the target TU transitions to the TU_RINGING state, and the originating TU + * transitions to the TU_RING_BACK state. + * + * In all cases, a notification of the resulting state of the originating TU is sent to + * to the associated network client. If the target TU has changed state, then its client + * is also notified of its new state. + * + * If the caller of this function was unable to determine a target TU to be called, + * it will pass NULL as the target TU. In this case, the originating TU will transition + * to the TU_ERROR state if it was in the TU_DIAL_TONE state, and there will be no + * effect otherwise. This situation is handled here, rather than in the caller, + * because here we have knowledge of the current TU state and we do not want to introduce + * the possibility of transitions to a TU_ERROR state from arbitrary other states, + * especially in states where there could be a peer TU that would have to be dealt with. + * + * @param tu The originating TU. + * @param target The target TU, or NULL if the caller of this function was unable to + * identify a TU to be dialed. + * @return 0 if successful, -1 if any error occurs that results in the originating + * TU transitioning to the TU_ERROR state. + */ +#if 0 +int tu_dial(TU *tu, TU *target) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Take a TU receiver off-hook (i.e. pick up the handset). + * If the TU is in neither the TU_ON_HOOK state nor the TU_RINGING state, + * then there is no effect. + * If the TU is in the TU_ON_HOOK state, it goes to the TU_DIAL_TONE state. + * If the TU was in the TU_RINGING state, it goes to the TU_CONNECTED state, + * reflecting an answered call. In this case, the calling TU simultaneously + * also transitions to the TU_CONNECTED state. + * + * In all cases, a notification of the resulting state of the specified TU is sent to + * to the associated network client. If a peer TU has changed state, then its client + * is also notified of its new state. + * + * @param tu The TU that is to be picked up. + * @return 0 if successful, -1 if any error occurs that results in the originating + * TU transitioning to the TU_ERROR state. + */ +#if 0 +int tu_pickup(TU *tu) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * Hang up a TU (i.e. replace the handset on the switchhook). + * + * If the TU is in the TU_CONNECTED or TU_RINGING state, then it goes to the + * TU_ON_HOOK state. In addition, in this case the peer TU (the one to which + * the call is currently connected) simultaneously transitions to the TU_DIAL_TONE + * state. + * If the TU was in the TU_RING_BACK state, then it goes to the TU_ON_HOOK state. + * In addition, in this case the calling TU (which is in the TU_RINGING state) + * simultaneously transitions to the TU_ON_HOOK state. + * If the TU was in the TU_DIAL_TONE, TU_BUSY_SIGNAL, or TU_ERROR state, + * then it goes to the TU_ON_HOOK state. + * + * In all cases, a notification of the resulting state of the specified TU is sent to + * to the associated network client. If a peer TU has changed state, then its client + * is also notified of its new state. + * + * @param tu The tu that is to be hung up. + * @return 0 if successful, -1 if any error occurs that results in the originating + * TU transitioning to the TU_ERROR state. + */ +#if 0 +int tu_hangup(TU *tu) { + // TO BE IMPLEMENTED + abort(); +} +#endif + +/* + * "Chat" over a connection. + * + * If the state of the TU is not TU_CONNECTED, then nothing is sent and -1 is returned. + * Otherwise, the specified message is sent via the network connection to the peer TU. + * In all cases, the states of the TUs are left unchanged and a notification containing + * the current state is sent to the TU sending the chat. + * + * @param tu The tu sending the chat. + * @param msg The message to be sent. + * @return 0 If the chat was successfully sent, -1 if there is no call in progress + * or some other error occurs. + */ +#if 0 +int tu_chat(TU *tu, char *msg) { + // TO BE IMPLEMENTED + abort(); +} +#endif diff --git a/hw5/tests/__test_includes.h b/hw5/tests/__test_includes.h new file mode 100644 index 0000000..82441e1 --- /dev/null +++ b/hw5/tests/__test_includes.h @@ -0,0 +1,41 @@ +#include "pbx.h" +#include "server.h" + +#define QUOTE1(x) #x +#define QUOTE(x) QUOTE1(x) +#define SCRIPT1(x) x##_script +#define SCRIPT(x) SCRIPT1(x) + +#define SERVER_PORT 9999 +#define SERVER_PORT_STR "9999" +#define SERVER_HOSTNAME "localhost" + +#define NUM_STATES 7 +#define NUM_COMMANDS 5 +#define DELAY_COMMAND (NUM_COMMANDS-1) + +#define ZERO_SEC { 0, 0 } +#define ONE_USEC { 0, 1 } +#define ONE_MSEC { 0, 1000 } +#define TEN_MSEC { 0, 10000 } +#define FTY_MSEC { 0, 50000 } +#define HND_MSEC { 0, 100000 } +#define QTR_SEC { 0, 250000 } +#define ONE_SEC { 1, 0 } + +#define SERVER_STARTUP_SLEEP 1 +#define SERVER_SHUTDOWN_SLEEP 1 + +/* + * Structure describing a single step in a test script. + */ +typedef struct test_step { + int id; // Index of TU performing test, or -1 if end. + TU_COMMAND command; // Command to send. + int id_to_dial; // ID of TU to dial, for TU_DIAL command. + TU_STATE response; // Expected response. + struct timeval timeout; // Limit on time to wait for response (zero for no limit) + // or time to delay. +} TEST_STEP; + +int run_test_script(char *name, TEST_STEP *scr, int port); diff --git a/hw5/tests/basecode_tests.c b/hw5/tests/basecode_tests.c new file mode 100644 index 0000000..b84d74d --- /dev/null +++ b/hw5/tests/basecode_tests.c @@ -0,0 +1,185 @@ +/* + * Important: the Criterion tests in this file have to be run with -j1, + * because they each start a separate server instance and if they are + * run concurrently only one server will be able to bind the server port + * and the others will fail. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "__test_includes.h" + +static int server_pid; + +static void wait_for_server() { + int ret; + int i = 0; + do { + fprintf(stderr, "Waiting for server to start (i = %d)\n", i); + ret = system("netstat -an | grep 'LISTEN[ ]*$' | grep ':"SERVER_PORT_STR"'"); + sleep(SERVER_STARTUP_SLEEP); + } while(++i < 30 && WEXITSTATUS(ret)); +} + +static void wait_for_no_server() { + int ret; + do { + ret = system("netstat -an | grep 'LISTEN[ ]*$' | grep ':"SERVER_PORT_STR"'"); + if(WEXITSTATUS(ret) == 0) { + fprintf(stderr, "Waiting for server port to clear...\n"); + system("killall -s KILL pbx > /dev/null"); + sleep(1); + } else { + break; + } + } while(1); +} + +static void init() { + server_pid = 0; + wait_for_no_server(); + fprintf(stderr, "***Starting server..."); + if((server_pid = fork()) == 0) { + execlp("bin/pbx", "pbx", "-p", SERVER_PORT_STR, NULL); + fprintf(stderr, "Failed to exec server\n"); + abort(); + } + fprintf(stderr, "pid = %d\n", server_pid); + // Wait for server to start before returning + wait_for_server(); +} + +static void fini(int chk) { + int ret; + cr_assert(server_pid != 0, "No server was started!\n"); + fprintf(stderr, "***Sending SIGHUP to server pid %d\n", server_pid); + kill(server_pid, SIGHUP); + sleep(SERVER_SHUTDOWN_SLEEP); + kill(server_pid, SIGKILL); + wait(&ret); + fprintf(stderr, "***Server wait() returned = 0x%x\n", ret); + if(chk) { + if(WIFSIGNALED(ret)) + cr_assert_fail("***Server terminated ungracefully with signal %d\n", WTERMSIG(ret)); + cr_assert_eq(WEXITSTATUS(ret), 0, "Server exit status was not 0"); + } +} + +static void killall() { + system("killall -s KILL pbx /usr/lib/valgrind/memcheck-amd64-linux > /dev/null 2>&1"); +} + + +#define SUITE basecode_suite + +#define TEST_NAME connect_disconnect_test +static TEST_STEP SCRIPT(TEST_NAME)[] = { + // ID, COMMAND, ID_TO_DIAL, RESPONSE, TIMEOUT + { 0, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 0, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { -1, -1, -1, -1, ZERO_SEC } +}; + +Test(SUITE, TEST_NAME, .init = init, .fini = killall, .timeout = 30) +{ + char *name = QUOTE(SUITE)"/"QUOTE(TEST_NAME); + int ret = run_test_script(name, SCRIPT(TEST_NAME), SERVER_PORT); + cr_assert_eq(ret, 0, "expected %d, was %d\n", 0, ret); + fini(0); +} +#undef TEST_NAME + +#define TEST_NAME connect_disconnect2_test +static TEST_STEP SCRIPT(TEST_NAME)[] = { + // ID, COMMAND, ID_TO_DIAL, RESPONSE, TIMEOUT + { 0, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 1, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 0, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { 1, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { -1, -1, -1, -1, ZERO_SEC } +}; + +Test(SUITE, TEST_NAME, .init = init, .fini = killall, .timeout = 30) { + char *name = QUOTE(SUITE)"/"QUOTE(TEST_NAME); + int ret = run_test_script(name, SCRIPT(TEST_NAME), SERVER_PORT); + cr_assert_eq(ret, 0, "expected %d, was %d\n", 0, ret); + fini(0); +} +#undef TEST_NAME + +#define TEST_NAME pickup_hangup_test +static TEST_STEP SCRIPT(TEST_NAME)[] = { + // ID, COMMAND, ID_TO_DIAL, RESPONSE, TIMEOUT + { 0, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 0, TU_PICKUP_CMD, -1, TU_DIAL_TONE, TEN_MSEC }, + { 0, TU_HANGUP_CMD, -1, TU_ON_HOOK, TEN_MSEC }, + { 0, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { -1, -1, -1, -1, ZERO_SEC } +}; + +Test(SUITE, TEST_NAME, .init = init, .fini = killall, .timeout = 30) { + char *name = QUOTE(SUITE)"/"QUOTE(TEST_NAME); + int ret = run_test_script(name, SCRIPT(TEST_NAME), SERVER_PORT); + cr_assert_eq(ret, 0, "expected %d, was %d\n", 0, ret); + fini(0); +} +#undef TEST_NAME + +#define TEST_NAME dial_answer_test +static TEST_STEP SCRIPT(TEST_NAME)[] = { + // ID, COMMAND, ID_TO_DIAL, RESPONSE, TIMEOUT + { 0, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 1, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 0, TU_PICKUP_CMD, -1, TU_DIAL_TONE, TEN_MSEC }, + { 0, TU_DIAL_CMD, 1, TU_RING_BACK, TEN_MSEC }, + { 1, TU_PICKUP_CMD, -1, TU_CONNECTED, FTY_MSEC }, + { 0, TU_HANGUP_CMD, -1, TU_ON_HOOK, FTY_MSEC }, + { 1, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { 0, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { -1, -1, -1, -1, ZERO_SEC } +}; + +Test(SUITE, TEST_NAME, .init = init, .fini = killall, .timeout = 30) { + char *name = QUOTE(SUITE)"/"QUOTE(TEST_NAME); + int ret = run_test_script(name, SCRIPT(TEST_NAME), SERVER_PORT); + cr_assert_eq(ret, 0, "expected %d, was %d\n", 0, ret); + fini(0); +} +#undef TEST_NAME + +#define TEST_NAME dial_disconnect_test +static TEST_STEP SCRIPT(TEST_NAME)[] = { + // ID, COMMAND, ID_TO_DIAL, RESPONSE, TIMEOUT + { 0, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 1, TU_CONNECT_CMD, -1, TU_ON_HOOK, HND_MSEC }, + { 0, TU_PICKUP_CMD, -1, TU_DIAL_TONE, TEN_MSEC }, + { 0, TU_DIAL_CMD, 1, TU_RING_BACK, TEN_MSEC }, + { 1, TU_PICKUP_CMD, -1, TU_CONNECTED, FTY_MSEC }, + { 1, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { 0, TU_HANGUP_CMD, -1, TU_ON_HOOK, FTY_MSEC }, + { 0, TU_DISCONNECT_CMD, -1, -1, TEN_MSEC }, + { -1, -1, -1, -1, ZERO_SEC } +}; + +Test(SUITE, TEST_NAME, .init = init, .fini = killall, .timeout = 30) { + char *name = QUOTE(SUITE)"/"QUOTE(TEST_NAME); + int ret = run_test_script(name, SCRIPT(TEST_NAME), SERVER_PORT); + cr_assert_eq(ret, 0, "expected %d, was %d\n", 0, ret); + fini(0); +} +#undef TEST_NAME diff --git a/hw5/tests/script_tester.c b/hw5/tests/script_tester.c new file mode 100644 index 0000000..7b1f72f --- /dev/null +++ b/hw5/tests/script_tester.c @@ -0,0 +1,642 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pbx.h" +#include "server.h" +#include "__test_includes.h" +#include "debug.h" + +#define NUM_STATES 7 +#define NUM_COMMANDS 5 +#define DELAY_COMMAND (NUM_COMMANDS-1) + +/* + * Table of expected next states. + * Each entry is a bitmap that specifies a set of possible next states, given + * the current state and the last command that was issued. + * + * An issue that this tester has to handle is that commands to the server can + * "cross in transit" asynchronous state-change notifications coming back from the server. + * If we are currently in the TU_ON_HOOK state and we send a TU_PICKUP_CMD, it might + * be that the TU_PICKUP_CMD crosses in transit a TU_RINGING notification being sent + * back to us. What we will see is a next-state notification of TU_RINGING, rather + * than the TU_DIAL_TONE notification that we would otherwise expect. + * + * To handle this, there are two classes of expected states encoded in each entry of + * the table. The "normal case" encodes a TU_STATE s as the bit value 1<id != -1) { + if(ts->id >= MAX_TUS) { + fprintf(stderr, "Script error: TU ID %d too large (>= %d)\n", ts->id, MAX_TUS); + return -1; + } + int cmd = ts->command; + int ext = -1; + TU *tu = &tus[ts->id]; + + // First, deal with performing any explicit action. + switch(ts->command) { + // Meta-commands + case TU_NO_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) TU_NO_CMD\n", timestamp(), TU_ID(tu), ts - scr); + break; + case TU_CONNECT_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) TU_CONNECT_CMD\n", timestamp(), TU_ID(tu), ts - scr); + if(tu->infd) { + fprintf(stderr, "%s: [%ld] Test error: already connected\n", + timestamp(), TU_ID(tu)); + return -1; + } + // Otherwise connect to server and update state. + if(connect_command(tu, port) == -1) + return -1; + break; + case TU_DISCONNECT_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) TU_DISCONNECT_CMD\n", timestamp(), TU_ID(tu), ts - scr); + if(!tu->infd) { + fprintf(stderr, "%s: [%ld] Test error: not connected\n", timestamp(), TU_ID(tu)); + return -1; + } + disconnect_command(tu); + break; + case TU_DELAY_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) TU_DELAY_CMD\n", timestamp(), TU_ID(tu), ts - scr); + // Pause for the specified amount of time. + tms.tv_sec = ts->timeout.tv_sec; + tms.tv_nsec = ts->timeout.tv_usec * 1000l; + nanosleep(&tms, NULL); + break; + case TU_AWAIT_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) TU_AWAIT_CMD\n", timestamp(), TU_ID(tu), ts - scr); + // Process incoming messages until specified state seen + // or timeout occurs. + break; + + // Real commands + case TU_PICKUP_CMD: + case TU_HANGUP_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) %s\n", + timestamp(), TU_ID(tu), ts - scr, tu_command_names[cmd]); + fprintf(tu->out, "%s%s", tu_command_names[cmd], EOL); + fflush(tu->out); + break; + case TU_DIAL_CMD: + ext = tus[ts->id_to_dial].extension; + fprintf(stderr, "%s: [%ld] (step #%ld) %s extension %d (id %d)\n", + timestamp(), TU_ID(tu), ts - scr, tu_command_names[cmd], ext, ts->id_to_dial); + fprintf(tu->out, "%s %d%s", tu_command_names[cmd], ext, EOL); + fflush(tu->out); + break; + case TU_CHAT_CMD: + fprintf(stderr, "%s: [%ld] (step #%ld) %s\n", + timestamp(), TU_ID(tu), ts - scr, tu_command_names[cmd]); + fprintf(tu->out, "%s%s", tu_command_names[cmd], EOL); + fflush(tu->out); + break; + + // Unknown command + default: + fprintf(stderr, "%s: [%ld] (step #%ld) Test error: unknown command (%d)\n", + timestamp(), TU_ID(tu), ts - scr, cmd); + return -1; + } + if(cmd <= TU_CHAT_CMD) { + tu->last_command = cmd; + tu->expected_states = next_states[tu->current_state][cmd]; + } else if(cmd == TU_CONNECT_CMD) { + // This is to get the right set of expected commands on initial connect, + // when no previous command has actually been sent. + tu->last_command = TU_HANGUP_CMD; + tu->expected_states = next_states[tu->current_state][TU_HANGUP_CMD]; + } else if(cmd == TU_DISCONNECT_CMD) { + fprintf(stderr, "%s: [%ld] Disconnected, now expecting EOF\n", timestamp(), TU_ID(tu)); + tu->last_command = cmd; + tu->expected_states = ~0; // We allow anything to drain pending notifications. + } else { + // For pseudo-commands, just recalculate the expected states based on + // the last real command. + tu->expected_states = next_states[tu->current_state][tu->last_command]; + } + + // Next, read responses while keeping track of timeout. + // If expected response seen, go to next step. + // If unexpected response seen, fail. + // If timeout occurs, shutdown the connection so that read will fail. + if(tu->infd && read_responses(tu, ts->response, ts->timeout) == -1) + return -1; + + // Advance script to next test step. + ts++; + } + return 0; +} + +/* + * Connect a specified TU to the server. + * Returns 0 on success, -1 on error. + */ +static int connect_command(TU *tu, int port) { + char *hostname = "localhost"; + struct in_addr sa; + struct hostent *he; + int sfd; + + // Connect to server on specified port and set sfd. + if((he = gethostbyname(hostname)) == NULL) { + herror("gethostbyname"); + return -1; + } + memcpy(&sa, he->h_addr, sizeof(sa)); + if((sfd = connect_to_server(&sa, port)) == -1) { + fprintf(stdout, "%s [%ld]: Failed to connect to server %s:%d\n", + timestamp(), TU_ID(tu), hostname, port); + return -1; + } + struct sockaddr_in s; + socklen_t sl = sizeof(s); + getsockname(sfd, (struct sockaddr *)&s, &sl); + port = s.sin_port; + fprintf(stdout, "%s: [%ld] Connected to server %s:%d\n", + timestamp(), TU_ID(tu), hostname, port); + + // Save file descriptor and set up streams and initial test state. + memset(tu, 0, sizeof(*tu)); + tu->infd = sfd; + tu->outfd = dup(sfd); // So they can be closed independently. + tu->in = fdopen(tu->infd, "r"); + tu->out = fdopen(tu->outfd, "w"); + + // Initial expected state notification is TU_ON_HOOK + tu->expected_states = 1<last_command = TU_HANGUP_CMD; + + return 0; +} + +/* + * Disconnect a specified TU from the server. + */ +static void disconnect_command(TU *tu) { + if(tu->outfd) { + shutdown(tu->outfd, SHUT_WR); // This lets us see if the server notices. + tu->outfd = 0; + } + if(tu->out) { + fclose(tu->out); + tu->out = NULL; + } + // Closing the input should go where we detect EOF. +#if 0 + if(tu->in) { + fclose(tu->in); + tu->in = NULL; + } +#endif +} + +/* There isn't really a maximum message length, but this is just a test driver... */ +#define MAX_MESSAGE_LEN 256 + +static struct timeval current_timeout; + +static TU *tu_to_read; +static void alarm_handler(int sig) { + fprintf(stderr, "%s: [%ld] Timeout (%ld, %ld)\n", timestamp(), TU_ID(tu_to_read), + current_timeout.tv_sec, current_timeout.tv_usec); + shutdown(tu_to_read->infd, SHUT_RD); // Force return from fgets +} + +/* + * Read responses from the server for a specified TU until an expected state is reached. + */ +static int read_responses(TU *tu, TU_STATE exp, struct timeval tv) { + TU_STATE new; + char msg[MAX_MESSAGE_LEN]; + char *arg; + int ret = 0; + fprintf(stderr, "%s: [%ld] Read responses until %s\n", + timestamp(), TU_ID(tu), exp == -1 ? "EOF" : tu_state_names[exp]); + tu_to_read = tu; + struct itimerval itv = {0}; + struct sigaction sa = {0}, oa; + sa.sa_handler = alarm_handler; + sa.sa_flags = SA_RESTART; + itv.it_value = tv; + current_timeout = tv; + sigaction(SIGALRM, &sa, &oa); + setitimer(ITIMER_REAL, &itv, NULL); + memset(&itv, 0, sizeof(itv)); + itv.it_value = tv; + setitimer(ITIMER_REAL, &itv, NULL); + do { + fprintf(stderr, "%s: [%ld] Expecting: %s\n", timestamp(), TU_ID(tu), + unparse_state_set(tu->expected_states)); + + if(fgets(msg, MAX_MESSAGE_LEN, tu->in) == NULL) { + fprintf(stderr, "%s: [%ld] EOF reading message from server\n", timestamp(), TU_ID(tu)); + fclose(tu->in); + tu->infd = 0; + if(tu->resync) { + fprintf(stderr, "%s: [%ld] Premature disconnection during resync\n", + timestamp(), TU_ID(tu)); + ret = -1; + goto disarm; + } else { + if(tu->expected_states == ~0) { + fprintf(stderr, "%s: [%ld] Matched EOF after disconnect\n", + timestamp(), TU_ID(tu)); + goto disarm; + } else { + if(exp == -1) { + fprintf(stderr, "%s: [%ld] Expected EOF correctly seen\n", + timestamp(), TU_ID(tu)); + } else { + fprintf(stderr, "%s: [%ld] EOF seen when it shouldn't have been\n", + timestamp(), TU_ID(tu)); + ret = -1; + } + goto disarm; + } + } + } + trim_eol(msg); + fprintf(stderr, "%s: [%ld] Message from server: %s\n", timestamp(), TU_ID(tu), msg); + new = parse_message(msg, &arg); + if(new > NUM_STATES) { + // Tracing output already produced by parse_message. + ret = -1; + goto disarm; + } + if(new == NUM_STATES) { + // The message is chat. There is no state transition, but we must be + // in the connected state. + if(tu->current_state != TU_CONNECTED) { + fprintf(stderr, "%s: [%ld] Chat received when not in state %s\n", + timestamp(), TU_ID(tu), tu_state_names[tu->current_state]); + ret = -1; + goto disarm; + } + continue; + } + + // Check state transition to see if it is as expected. + if(1<expected_states) { + // OK + tu->resync = 0; + } else if(1<<(new+RESYNC) & tu->expected_states) { + // OK, but set resync because messages crossed in transit. + fprintf(stderr, "%s: [%ld] Resync: state %s, expecting %s\n", + timestamp(), TU_ID(tu), tu_state_names[new], + unparse_state_set(tu->expected_states)); + tu->resync = 1; + } else { + // New state is not one that is expected -- testing fails. + fprintf(stderr, "%s: [%ld] New state %s is not in expected set %s\n", + timestamp(), TU_ID(tu), tu_state_names[new], + unparse_state_set(tu->expected_states)); + ret = -1; + goto disarm; + } + + // Update current state to that specified in message + fprintf(stderr, "%s: [%ld] Change state: %s -> %s\n", + timestamp(), TU_ID(tu), tu_state_names[tu->current_state], tu_state_names[new]); + tu->current_state = new; + if(new == TU_ON_HOOK) { + int ext = atoi(arg); + if(tu->extension != ext) { + tu->extension = ext; + } + } + if(new == TU_CONNECTED) { + int ext = atoi(arg); + tu->peer = ext; + } else { + tu->peer = -1; + } + + if(tu->resync) { + // If resyncing, update expected states based on last command sent, + // unless we are draining to get EOF. + //fprintf(stderr, "%s: [%ld] Resync\n", timestamp(), TU_ID(tu)); + if(tu->expected_states != ~0) + tu->expected_states = next_states[new][tu->last_command]; + } + } while(tu->current_state != exp); + + disarm: + itv = (struct itimerval) {0}; + setitimer(ITIMER_REAL, &itv, NULL); + sigaction(SIGALRM, &oa, NULL); + tu_to_read = NULL; + return ret; +} + +/* + * Parse a message from the PBX, determining the new state. + */ +static TU_STATE parse_message(char *msg, char **arg) { + for(int i = 0; i < NUM_STATES; i++) { + if(strstr(msg, tu_state_names[i]) == msg) { + if(arg) + *arg = msg + strlen(tu_state_names[i]); + return i; + } + } + if(strstr(msg, "CHAT") == msg) { + if(arg) + *arg = msg + strlen("CHAT"); + return NUM_STATES; + } + fprintf(stderr, "%s: Unrecognized message: %s\n", timestamp(), msg); + return NUM_STATES+1; +} + +/* + * Connect to the server at a specified address. + * + * Returns: connection file descriptor in case of success. + * Returns -1 and sets errno in case of error. + */ +static int connect_to_server(struct in_addr *addr, int port) { + struct sockaddr_in sa; + int sfd; + + if((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + return(-1); + } + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + memcpy(&sa.sin_addr.s_addr, addr, sizeof(struct in_addr)); + if(connect(sfd, (struct sockaddr *)(&sa), sizeof(sa)) < 0) { + close(sfd); + return(-1); + } + return sfd; +} + +/* + * Construct a string representation of an expected state bitmap. + */ +static char *unparse_state_set(int set) { + static char buf[100]; + buf[0] = '\0'; + strcat(buf, "{ "); + for(int i = 0; i < NUM_STATES; i++) { + if(set & (1<