diff src/lwwire.c @ 0:bef2801ac83e

Initial checkin with reference implementation of core protocol Initial checkin. Has the initial version of the protocol documentation along with a reference implementation of the core protocol.
author William Astle <lost@l-w.ca>
date Sun, 08 May 2016 12:56:39 -0600
parents
children 2f2cbd2d2561
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lwwire.c	Sun May 08 12:56:39 2016 -0600
@@ -0,0 +1,669 @@
+/*
+This program implements the lwwire protocol. It expects STDIN and STDOUT
+to be connected to an appropriately configured communication channel.
+
+The following timeouts are specified by the protocol and are listed here
+for convenience:
+
+Between bytes in a request: 10 milliseconds
+Between bytes reading a response (client): 10ms <= timeout <= 1000ms
+
+Server receiving bad request: >= 1100 milliseconds
+
+Implementation notes:
+
+This implementation uses low level I/O calls (read()/write()) on STDIN
+and STDOUT because we MUST have fully unbuffered I/O for the protocol
+to function properly and we really do not want the stdio overhead for
+that.
+
+The complexity of the lwwire_readdata() and lwwire_writedata() functions
+is required to handle some possible corner cases that would otherwise
+completely bollix everything up.
+
+Command line options:
+
+drive=N,PATH
+
+Specify that drive #N should reference the file at PATH. Note that the
+file at PATH will be created if it doesn't exist, but only if it is actually
+accessed. N is a decimal number from 0 to 255. If N is prefixed with "C",
+the drive is treated as read-only.
+
+
+By default, no drives are associated with files. Also, it is unspecified
+what happens if multiple protocol instances access the same drive.
+
+*/
+
+// for nanosleep
+#define _POSIX_C_SOURCE 199309L
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/select.h>
+#include <time.h>
+#include <unistd.h>
+
+#define LWERR_NONE 0
+#define LWERR_CHECKSUM 0xF3
+#define LWERR_READ 0xF4
+#define LWERR_WRITE 0xF5
+#define LWERR_NOTREADY 0xF6
+
+struct lwwire_driveinfo
+{
+	char *path;
+	FILE *fp;
+	int isconst;
+};
+
+struct lwwire_driveinfo drivedata[256];
+
+void lwwire_protoerror(void);
+int lwwire_readdata(void *, int, int);
+int lwwire_writedata(void *, int);
+void lwwire_write(void *, int);
+int lwwire_read(void *, int);
+int lwwire_read2(void *, int, int);
+void lwwire_reset(void);
+int lwwire_fetch_sector(int dn, int lsn, void *);
+int lwwire_save_sector(int dn, int lsn, void *);
+int nonblock(int);
+
+void lwwire_proto_read(void);
+void lwwire_proto_write(void);
+void lwwire_proto_readex(void);
+void lwwire_proto_requestextension(void);
+void lwwire_proto_disableextension(void);
+void lwwire_proto_extensionop(void);
+
+int main(int argc, char **argv)
+{
+	unsigned char buf[32];
+	time_t curtime;
+	struct tm *tmval;
+	int rv;
+	int i;
+		
+	// make stdin and stdout non-blocking
+	if (nonblock(0) < 0)
+	{
+		fprintf(stderr, "Cannot make stdin non-blocking: %s\n", strerror(errno));
+		exit(1);
+	}
+	if (nonblock(1) < 0)
+	{
+		fprintf(stderr, "Cannot make stdout non-blocking: %s\n", strerror(errno));
+		exit(1);
+	}
+
+	memset(&drivedata, 0, sizeof(drivedata));
+
+	for (i = 1; i < argc; i++)
+	{
+		if (strncmp("drive=", argv[i], 6) == 0)
+		{
+			int dn=0;
+			int isconst = 0;
+			char *ptr;
+			ptr = argv[i] + 6;
+			if (*ptr == 'C')
+			{
+				isconst = 1;
+				ptr++;
+			}
+			while (*ptr >= '0' && *ptr <= '9')
+			{
+				dn = dn * 10 + (*ptr - '0');
+				ptr++;
+			}
+			if (*ptr != ',' || dn > 255)
+			{
+				fprintf(stderr, "Ignoring invalid drive specification: %s\n", argv[i]);
+				continue;
+			}
+			ptr++;
+			drivedata[dn].path = ptr;
+			drivedata[dn].isconst = isconst;
+		}
+	}
+
+	fprintf(stderr, "Running with the following disk images:\n");
+	for (i = 0; i < 256; i++)
+	{
+		if (drivedata[i].path)
+		{
+			fprintf(stderr, "[%d%s] %s\n", i, drivedata[i].isconst ? "C" : "", drivedata[i].path);
+		}
+	}
+
+	// main loop reading operations and dispatching
+	for (;;)
+	{
+		rv = lwwire_readdata(buf, 1, 0);
+		if (rv < 0)
+		{
+			fprintf(stderr, "Error or timeout reading operation code.\n");
+			lwwire_protoerror();
+			continue;
+		}
+		if (rv == 0)
+		{
+			fprintf(stderr, "EOF on comm channel. Exiting.\n");
+			exit(0);
+		}
+		fprintf(stderr, "Handling opcode %02X\n", buf[0]);
+		
+		// we have an opcode here
+		switch (buf[0])
+		{
+		case 0x00: // NOOP
+			break;
+
+		case 0x23: // TIME
+			curtime = time(NULL);
+			tmval = localtime(&curtime);
+			buf[0] = tmval -> tm_year;
+			buf[1] = tmval -> tm_mon;
+			buf[2] = tmval -> tm_mday;
+			buf[3] = tmval -> tm_hour;
+			buf[4] = tmval -> tm_min;
+			buf[5] = tmval -> tm_sec;
+			buf[6] = tmval -> tm_wday;
+			lwwire_write(buf, 7);
+			break;
+		
+		case 0x46: // PRINTFLUSH
+			// no printer is supported by this implemention so NO-OP
+			break;
+		
+		case 0x47: // GETSTAT (useless dw3 operation)
+		case 0x53: // SETSTAT (useless dw3 operation)
+			// burn two bytes from the client and do nothing
+			lwwire_read(buf, 2);
+			break;
+		
+		case 0x49: // INIT (old style INIT call)
+		case 0x54: // TERM (old DW3 op treated same as INIT)
+		case 0xF8: // RESET3 (junk on the line during reset)
+		case 0xFE: // RESET1 (junk on the line during reset)
+		case 0xFF: // RESET2 (junk on the line during reset)
+			lwwire_reset();
+			break;
+		
+		case 0x50: // PRINT
+			// burn a byte because we don't support any printers
+			lwwire_read(buf, 1);
+			break;
+		
+		case 0x52: // READ
+		case 0x72: // REREAD (same semantics as READ)
+			fprintf(stderr, "DWPROTO: read()\n");
+			lwwire_proto_read();
+			break;
+		
+		case 0x57: // WRITE
+		case 0x77: // REWRITE (same semantics as WRITE)
+			fprintf(stderr, "DWPROTO: write()\n");
+			lwwire_proto_write();
+			break;
+		
+		case 0x5A: // DWINIT (new style init)
+			lwwire_reset();
+			if (lwwire_read(buf, 1) < 0)
+				break;
+			fprintf(stderr, "DWINIT: client drive code %02X\n", buf[0]);
+			// tell the client we speak lwwire protocol
+			buf[0] = 0x80;
+			lwwire_write(buf, 1);
+			break;
+		
+		case 0xD2: // READEX (improved reading operation)
+		case 0xF2: // REREADEX (same semantics as READEX)
+			fprintf(stderr, "DWPROTO: readex()\n");
+			lwwire_proto_readex();
+			break;
+		
+		case 0xF0: // REQUESTEXTENSION
+			lwwire_proto_requestextension();
+			break;
+		
+		case 0xF1: // DISABLEEXTENSION
+			lwwire_proto_disableextension();
+			break;
+		
+		case 0xF3: // EXTENSIONOP
+			lwwire_proto_extensionop();
+			break;
+		
+		default:
+			fprintf(stderr, "Unrecognized operation code %02X. Doing error state.\n", buf[0]);
+			lwwire_protoerror();
+			break;
+		}
+	}
+}
+
+// protocol handling functions
+void lwwire_proto_read(void)
+{
+	unsigned char buf[259];
+	int ec;
+	int lsn;
+	int i;
+	
+	if (lwwire_read(buf, 4) < 0)
+		return;
+	
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	
+	ec = lwwire_fetch_sector(buf[0], lsn, buf + 1);
+	buf[0] = ec;
+	lwwire_write(buf, 1);
+	if (ec)
+		return;
+	// all this futzing around here is probably a long enough
+	// delay but testing on real hardware is needed here
+	ec = 0;
+	for (i = 1; i < 257; i++)
+		ec += buf[i];
+	buf[257] = (ec >> 8) & 0xff;
+	buf[258] = ec & 0xff;
+	lwwire_write(buf + 1, 258);
+}
+
+void lwwire_proto_write(void)
+{
+	unsigned char buf[262];
+	int lsn;
+	int ec;
+	int i;	
+	if (lwwire_read(buf, 262) < 0)
+		return;
+	
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	for (ec = 0, i = 4; i < 260; i++)
+		ec += buf[i];
+	if (ec != ((buf[260] << 8) | buf[261]))
+	{
+		buf[0] = LWERR_CHECKSUM;
+	}
+	else
+	{
+		ec = lwwire_save_sector(buf[0], lsn, buf + 4);
+		buf[0] = ec;
+	}
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_readex(void)
+{
+	unsigned char buf[256];
+	int lsn;
+	int ec;
+	int i;
+	int csum;
+	if (lwwire_read(buf, 4) < 0)
+		return;
+	lsn = (buf[1] << 16) | (buf[2] << 8) | buf[3];
+	ec = lwwire_fetch_sector(buf[0], lsn, buf);
+	if (ec)
+		memset(buf, 0, 256);
+	for (i = 0, csum = 0; i < 256; i++)
+		csum += buf[i];
+	lwwire_write(buf, 256);
+	if ((i = lwwire_read2(buf, 2, 5)) < 0)
+	{
+		fprintf(stderr, "Error reading protocol bytes: %d, %s\n", i, strerror(errno));
+		return;
+	}
+	i = (buf[0] << 8) | buf[1];
+	if (i != csum)
+		ec = LWERR_CHECKSUM;
+	buf[0] = ec;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_requestextension(void)
+{
+	unsigned char buf[1];
+	
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// NAK the request
+	buf[1] = 0x55;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_disableextension(void)
+{
+	unsigned char buf[1];
+	
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// ACK disabling any unsupported extensions
+	buf[1] = 0x42;
+	lwwire_write(buf, 1);
+}
+
+void lwwire_proto_extensionop(void)
+{
+	unsigned char buf[1];
+	if (lwwire_read(buf, 1) < 0)
+		return;
+	// we don't currently support any extensions so treat as unknown
+	lwwire_protoerror();
+}
+
+// Various infrastructure things follow here.
+int nonblock(int fd)
+{
+	int flags;
+	
+	flags = fcntl(fd, F_GETFL);
+	if (flags < 0)
+		return -1;
+	flags |= O_NONBLOCK;
+	return fcntl(fd, F_SETFL, flags);
+}
+
+/*
+Read len bytes from the input. If no bytes are available after
+10 ms, return error.
+
+This *may* allow a timeout longer than 10ms. However, it will
+eventually time out. In the worse case, it is more permissive
+than the specification. It will not time out before 10ms elapses.
+
+If "itimeout" is 0, then it will wait forever for the first
+byte. Otherwise, it will time out even on the first one.
+
+*/
+int lwwire_readdata(void *buf, int len, int itimeout)
+{
+	int toread = len;
+	int rv;
+	fd_set fdset;
+	struct timeval timeout;
+	
+	if (itimeout == 0)
+	{
+		for (;;)
+		{
+			// now wait for the descriptor to be readable
+			FD_ZERO(&fdset);
+			FD_SET(0, &fdset);
+		
+			rv = select(1, &fdset, NULL, NULL, NULL);
+			if (rv < 0)
+			{
+				// this is a last ditch effort to not break completely
+				// in the face of a signal; it should occur only rarely
+				// and it is not clear what the correct behaviour should
+				// be.
+				if (errno == EINTR)
+					continue;
+				return -1;
+			}
+			// if we actually have something to read, move on
+			if (rv > 0)
+				break;
+		}
+	}
+	while (toread > 0)
+	{
+		rv = read(0, buf, toread);
+		if (rv == toread)
+			break;
+		if (rv == 0)
+		{
+			// flag EOF so the caller knows to bail
+			return 0;
+		}
+		if (rv < 0)
+		{
+			if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
+			{
+				return -1;
+			}
+			rv = 0;
+		}
+		// now rv is the number of bytes read
+		buf += rv;
+		toread -= rv;
+		
+		// now wait for the descriptor to be readable
+		FD_ZERO(&fdset);
+		FD_SET(0, &fdset);
+		timeout.tv_sec = 0;
+		timeout.tv_usec = 10000 * itimeout;
+		
+		rv = select(1, &fdset, NULL, NULL, &timeout);
+		if (rv < 0)
+		{
+			// this is a last ditch effort to not break completely
+			// in the face of a signal; it should occur only rarely
+			// and it is not clear what the correct behaviour should
+			// be.
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		// timeout condition
+		if (rv == 0)
+		{
+			errno = ETIMEDOUT;
+			return -1;
+		}
+		// anything else here means we have more bytes to read
+	}
+	fprintf(stderr, "Protocol bytes read (%d):", len);
+	for (rv = 0; rv < len; rv++)
+		fprintf(stderr, " %02X ", ((char *)(buf))[rv] & 0xff);
+	fprintf(stderr, "\n");
+	return len;
+}
+
+/*
+Write data to the output. This will time out after 10 seconds. The timeout
+is only there in case the underlying communication channel goes out to lunch.
+
+It returns -1 on error or len on success.
+
+The timeout requires the file descriptor to be non-blocking.
+
+*/
+int lwwire_writedata(void *buf, int len)
+{
+	int towrite = len;
+	int rv;
+	fd_set fdset;
+	struct timeval timeout;
+	
+	while (towrite > 0)
+	{
+		rv = write(0, buf, towrite);
+		if (rv == towrite)
+			break;
+		if (rv < 0)
+		{
+			if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
+			{
+				return -1;
+			}
+			rv = 0;
+		}
+		// now rv is the number of bytes read
+		buf += rv;
+		towrite -= rv;
+		
+		// now wait for the descriptor to be writable
+		FD_ZERO(&fdset);
+		FD_SET(1, &fdset);
+		timeout.tv_sec = 10;
+		timeout.tv_usec = 0;
+		
+		rv = select(2, NULL, &fdset, NULL, &timeout);
+		if (rv < 0)
+		{
+			// this is a last ditch effort to not break completely
+			// in the face of a signal; it should occur only rarely
+			// and it is not clear what the correct behaviour should
+			// be.
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		// timeout condition
+		if (rv == 0)
+		{
+			errno = ETIMEDOUT;
+			return -1;
+		}
+		// anything else here means we have more bytes to write
+	}
+	fprintf(stderr, "Protocol bytes written (%d):", len);
+	for (rv = 0; rv < len; rv++)
+		fprintf(stderr, " %02X ", ((char *)(buf))[rv] & 0xff);
+	fprintf(stderr, "\n");
+	return len;
+}
+
+// like lwwire_writedata() except it bails the program on error.
+void lwwire_write(void *buf, int len)
+{
+	if (lwwire_writedata(buf, len) < 0)
+	{
+		fprintf(stderr, "Error writing %d bytes to client: %s\n", len, strerror(errno));
+		exit(1);
+	}
+}
+
+// like lwwire_readdata() except it bails on EOF and bounces to
+// error state on errors, and always does the timeout for initial
+// bytes. It returns -1 on a timeout. It does not return on EOF.
+// It does not return on random errors.
+int lwwire_read2(void *buf, int len, int toscale)
+{
+	int rv;
+	rv = lwwire_readdata(buf, len, toscale);
+	if (rv < 0)
+	{
+		if (errno == ETIMEDOUT)
+		{
+			lwwire_protoerror();
+			return -1;
+		}
+		fprintf(stderr, "Error reading %d bytes from client: %s\n", len, strerror(errno));
+		exit(1);
+	}
+	if (rv == 0)
+	{
+		fprintf(stderr, "EOF reading %d bytes from client.", len);
+		exit(0);
+	}
+	return 0;
+}
+
+int lwwire_read(void *buf, int len)
+{
+	return lwwire_read2(buf, len, 1);
+}
+
+/*
+Handle a protocol error by maintaining radio silence for
+at least 1100 ms. The pause must be *at least* 1100ms so
+it's no problem if the messing about takes longer. Do a
+state reset after the error.
+*/
+void lwwire_protoerror(void)
+{
+	struct timespec sltime;
+	struct timespec rtime;
+	
+	sltime.tv_sec = 1;
+	sltime.tv_nsec = 100000000;
+	
+	while (nanosleep(&sltime, &rtime) < 0)
+	{
+		// anything other than EINTR indicates something seriously messed up
+		if (errno != EINTR)
+			break;
+		sltime = rtime;
+	}
+	lwwire_reset();
+}
+
+/* fetch a file pointer for the specified drive number */
+FILE *lwwire_fetch_drive_fp(int dn)
+{
+	if (drivedata[dn].path == NULL)
+		return NULL;
+	if (drivedata[dn].fp)
+	{
+		if (ferror(drivedata[dn].fp))
+			fclose(drivedata[dn].fp);
+		else
+			return drivedata[dn].fp;
+	}
+	
+	drivedata[dn].fp = fopen(drivedata[dn].path, "r+");
+	if (!drivedata[dn].fp)
+	{
+		if (errno == ENOENT && !drivedata[dn].isconst)
+		{
+			drivedata[dn].fp = fopen(drivedata[dn].path, "w+");
+		}
+	}
+	return drivedata[dn].fp;
+}
+
+/* read a sector from a disk image */
+int lwwire_fetch_sector(int dn, int lsn, void *buf)
+{
+	FILE *fp;
+	int rc;
+		
+	fp = lwwire_fetch_drive_fp(dn);
+	if (!fp)
+		return LWERR_NOTREADY;
+	
+	if (fseek(fp, lsn * 256, SEEK_SET) < 0)
+		return LWERR_READ;
+	rc = fread(buf, 1, 256, fp);
+	if (rc < 256)
+	{
+		memset(buf + rc, 0, 256 - rc);
+	}
+	return 0;
+}
+
+int lwwire_save_sector(int dn, int lsn, void *buf)
+{
+	FILE *fp;
+	int rc;
+
+	fp = lwwire_fetch_drive_fp(dn);
+	if (!fp)
+		return LWERR_NOTREADY;
+	if (drivedata[dn].isconst)
+		return LWERR_WRITE;
+	if (fseek(fp, lsn * 256, SEEK_SET) < 0)
+		return LWERR_WRITE;
+	rc = fwrite(buf, 1, 256, fp);
+	if (rc < 256)
+		return LWERR_WRITE;
+	return 0;
+}	
+
+
+/*
+Reset the protocol state to "base protocol" mode.
+*/
+void lwwire_reset(void)
+{
+}