joeylib2/tests/gate/x68kSerialPeer.c

222 lines
7.8 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// x68kSerialPeer.c - the host end of the X68000 serial gate.
//
// x68kSerialPeer [--port N] [--probe TEXT] [--timeout SECONDS]
// [--until TEXT] [--listen-only]
//
// MAME's null_modem socket form CONNECTS outward, so this side listens (on
// 127.0.0.1:N, default 6800). Once the guest's SERIAL.X is up it echoes
// every byte it receives, so sending a probe string (default JOEYLIB-X68K)
// and reading it back proves both directions of the link.
//
// The guest takes a while to appear: Human68k boots, then COMMAND.X runs
// AUTOEXEC.BAT, then SERIAL.X initialises. Rather than guess, this
// retransmits the probe every RESEND_SECONDS until the echo comes back or
// the deadline (--timeout, default 420 seconds) passes.
//
// --listen-only never transmits: it isolates the guest's transmit path
// (keyboard -> jlSerialWrite) from the echo. It passes once --until's text
// has arrived, or at the deadline when any bytes did.
//
// Each option takes its value as the next argument or after "=" (--port=N);
// -h or --help prints the usage. Prints an X68SER line for each step and
// each chunk received (bytes in double quotes, escaped where they are not
// printable), then one X68SER PASS, COLLECTED, or FAIL line; exit 0 on
// pass, 1 on fail, 2 on bad arguments.
#define _DEFAULT_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "gateArg.h"
#include "gateNet.h"
#include "gateText.h"
#include "toolBuf.h"
#define TOOL_NAME "x68kSerialPeer"
#define HELP_EXIT 0
#define DEFAULT_PORT 6800u
#define DEFAULT_PROBE "JOEYLIB-X68K"
#define DEFAULT_TIMEOUT 420.0
// The probe's resend interval, and how long one read waits.
#define RESEND_SECONDS 5.0
#define READ_SECONDS 2.0
#define READ_SIZE 256u
#define SHOWN_BYTES 64u
// What the command line asked for.
typedef struct ArgsT {
uint16_t port;
const char *probe;
double timeout;
const char *until;
bool listenOnly;
} ArgsT;
static void argsRead(ArgsT *args, int argc, char **argv);
static const char *optionValue(int argc, char **argv, int *i, const char *name);
static int peer(const ArgsT *args);
static void usage(FILE *out, int status) __attribute__((noreturn));
static void argsRead(ArgsT *args, int argc, char **argv) {
const char *value;
int i;
args->port = DEFAULT_PORT;
args->probe = DEFAULT_PROBE;
args->timeout = DEFAULT_TIMEOUT;
args->until = "";
args->listenOnly = false;
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
usage(stdout, HELP_EXIT);
}
if (strcmp(argv[i], "--listen-only") == 0) {
args->listenOnly = true;
} else if ((value = optionValue(argc, argv, &i, "--port")) != NULL) {
args->port = gateArgPort(value);
} else if ((value = optionValue(argc, argv, &i, "--probe")) != NULL) {
args->probe = value;
} else if ((value = optionValue(argc, argv, &i, "--timeout")) != NULL) {
args->timeout = gateArgDouble("timeout", value);
} else if ((value = optionValue(argc, argv, &i, "--until")) != NULL) {
args->until = value;
} else {
usage(stderr, GATE_USAGE_EXIT);
}
}
}
// The value of option name when argv[*i] is it: "--name VALUE" (the next
// argument, taken) or "--name=VALUE". NULL when argv[*i] is not name; a
// name without its value is a usage error.
static const char *optionValue(int argc, char **argv, int *i, const char *name) {
const char *arg;
size_t len;
arg = argv[*i];
len = strlen(name);
if (strncmp(arg, name, len) != 0) {
return NULL;
}
if (arg[len] == '=') {
return arg + len + 1u;
}
if (arg[len] != '\0') {
return NULL;
}
if (*i + 1 >= argc) {
usage(stderr, GATE_USAGE_EXIT);
}
*i += 1;
return argv[*i];
}
// Listen, then send the probe until it comes back (or, listen-only, collect
// what the guest sends) until the deadline.
static int peer(const ArgsT *args) {
struct sockaddr_in from;
ToolBufT received;
uint8_t chunk[READ_SIZE];
size_t probeLen;
size_t untilLen;
size_t sentTotal;
double deadline;
double lastSend;
bool sentOnce;
int32_t got;
int listener;
int conn;
probeLen = strlen(args->probe);
untilLen = strlen(args->until);
deadline = gateNetNow() + args->timeout;
listener = gateNetListen(args->port, GATE_NET_ONE_PEER);
printf("X68SER listening on %s:%u\n", GATE_NET_HOST, (unsigned)args->port);
conn = gateNetAccept(listener, args->timeout, &from);
close(listener);
if (conn < 0) {
printf("X68SER FAIL: MAME never connected\n");
return 1;
}
printf("X68SER connected from %s:%u\n", inet_ntoa(from.sin_addr), (unsigned)ntohs(from.sin_port));
memset(&received, 0, sizeof(received));
sentTotal = 0u;
lastSend = 0.0;
sentOnce = false;
while (gateNetNow() < deadline) {
// Resend: the guest is not listening until SERIAL.X has booted and
// opened the port, and nothing here can see when that happens.
if (!args->listenOnly && (!sentOnce || gateNetNow() - lastSend > RESEND_SECONDS)) {
if (!gateNetSendAll(conn, args->probe, probeLen)) {
printf("X68SER FAIL: send error: %s\n", strerror(errno));
return 1;
}
sentTotal += probeLen;
lastSend = gateNetNow();
sentOnce = true;
}
got = gateNetRecv(conn, chunk, sizeof(chunk), READ_SECONDS);
if (got == GATE_NET_IDLE) {
continue;
}
if (got == GATE_NET_CLOSED) {
if (errno != 0) {
printf("X68SER FAIL: recv error: %s\n", strerror(errno));
} else {
printf("X68SER FAIL: link closed by MAME\n");
}
return 1;
}
toolBufAdd(&received, chunk, (size_t)got);
printf("X68SER rx %d bytes: ", got);
gateNetShowBytes(stdout, chunk, (size_t)got);
printf("\n");
if (args->listenOnly && untilLen > 0u && gateTextFind(received.data, received.len, 0u, args->until, untilLen) != GATE_TEXT_NOT_FOUND) {
printf("X68SER PASS: guest transmitted %zu bytes unprompted: ", received.len);
gateNetShowBytes(stdout, received.data, received.len);
printf("\n");
return 0;
}
if (gateTextFind(received.data, received.len, 0u, args->probe, probeLen) != GATE_TEXT_NOT_FOUND) {
printf("X68SER PASS: probe echoed back after %.1fs (sent %zu bytes, received %zu)\n", args->timeout - (deadline - gateNetNow()), sentTotal, received.len);
return 0;
}
}
if (args->listenOnly && received.len > 0u) {
printf("X68SER COLLECTED %zu bytes: ", received.len);
gateNetShowBytes(stdout, received.data, received.len);
printf("\n");
return 0;
}
printf("X68SER FAIL: timeout. sent=%zu received=%zu bytes: ", sentTotal, received.len);
gateNetShowBytes(stdout, received.data, received.len < SHOWN_BYTES ? received.len : SHOWN_BYTES);
printf("\n");
return 1;
}
static void usage(FILE *out, int status) {
fprintf(out, "usage: %s [--port N] [--probe TEXT] [--timeout SECONDS] [--until TEXT] [--listen-only]\n", TOOL_NAME);
exit(status);
}
int main(int argc, char **argv) {
ArgsT args;
gateNetStart(TOOL_NAME);
argsRead(&args, argc, argv);
return peer(&args);
}