735 lines
26 KiB
C
735 lines
26 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// serialLoopPeer.c - the host end of the serial loopback gates.
|
|
//
|
|
// serialLoopPeer tcp-server PORT WALL [BANNER] listen; the emulator connects
|
|
// serialLoopPeer tcp-client PORT WALL [BANNER] connect to the emulator
|
|
// serialLoopPeer pty PATHFILE WALL [BANNER] make a raw pty pair, write the
|
|
// guest end's path to PATHFILE
|
|
//
|
|
// The guest runs SERTEST (examples/sertest): it sends a banner, then echoes
|
|
// every byte it receives. This peer waits for the banner (the guest's
|
|
// transmit works), then sends a probe line and expects it back (its receive
|
|
// works too). WALL is the wall-clock budget in seconds. Prints one LOOP
|
|
// PASS / LOOP FAIL line; exit 0 on pass, 1 on fail, 2 on bad arguments.
|
|
//
|
|
// --close-hold FILE (pty) then has SERTEST close the port: its DTR check,
|
|
// run for the closes alone (a pty has no modem lines), every marker line
|
|
// acknowledged, after which the port stays closed. Prints one CLOSE PASS /
|
|
// CLOSE FAIL line more, and keeps the pty pair open until FILE exists or
|
|
// WALL runs out, so the gate can read the guest end's settings after the
|
|
// close (verify-sdl3-serial.sh).
|
|
//
|
|
// --stream N --rate RATE (TCP, nullmodem transparent:1) then streams N
|
|
// bytes of printable ASCII through SERTEST's echo, at most STREAM_WINDOW
|
|
// bytes ahead of what has come back, and checks every byte returns once, in
|
|
// order. Prints one STREAM PASS / STREAM FAIL line more, with the round
|
|
// trip's bytes a second; PASS needs that to be at least STREAM_MIN_PERCENT
|
|
// of what the line rate RATE (bits a second) carries.
|
|
//
|
|
// --dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which
|
|
// puts the guest's modem lines in the byte stream as they change: 0xFF 0xFF
|
|
// is a data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs
|
|
// SERTEST's DTR check: it sends DTR_CHECK, acknowledges each of SERTEST's
|
|
// marker lines (so SERTEST changes a line only once the marker before has
|
|
// gone out whole), and reads the line states against the markers.
|
|
// jlSerialSetDtr(false) and (true) must reach the line, closing and opening
|
|
// the port again must keep DTR up, and after a hang-up (DTR dropped, then
|
|
// the port closed) DTR must stay down for HANGUP_WATCH_SECONDS. Prints one
|
|
// DTR PASS / DTR FAIL line more.
|
|
//
|
|
// The options may come anywhere among the arguments. The verdict lines
|
|
// begin as shown above (the gates read them); what the guest sent is shown
|
|
// in double quotes, escaped where it is not printable.
|
|
|
|
#define _DEFAULT_SOURCE
|
|
#define _XOPEN_SOURCE 600
|
|
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <poll.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
#include "gateArg.h"
|
|
#include "gateNet.h"
|
|
#include "gateText.h"
|
|
#include "toolBuf.h"
|
|
#include "toolFile.h"
|
|
|
|
#define TOOL_NAME "serialLoopPeer"
|
|
// The arguments that are not options: mode, target, wall, and a banner.
|
|
#define POS_MODE 0
|
|
#define POS_TARGET 1
|
|
#define POS_WALL 2
|
|
#define POS_BANNER 3
|
|
#define POS_MIN 3
|
|
#define POS_MAX 4
|
|
|
|
#define DEFAULT_BANNER "JLSERTEST-READY"
|
|
#define PROBE "PING12345"
|
|
#define PROBE_LINE PROBE "\r\n"
|
|
#define RESEND_SECONDS 3.0
|
|
#define POLL_SECONDS 0.5
|
|
#define CONNECT_SECONDS 2.0
|
|
#define CONNECT_RETRY_SECONDS 0.5
|
|
#define READ_SIZE 256u
|
|
// How much of the stream a line shows (LOOP FAIL a little more).
|
|
#define SHOWN_BYTES 80u
|
|
#define FAIL_EXTRA_BYTES 40u
|
|
#define FAIL_SHOWN_BYTES (SHOWN_BYTES + FAIL_EXTRA_BYTES)
|
|
#define TMP_SUFFIX ".tmp"
|
|
#define OPTION_DASHES "--"
|
|
|
|
// --stream: the window, the chunks, the pass mark, and the bytes it sends
|
|
// (printable ASCII, so never SERTEST's DTR_CHECK).
|
|
#define STREAM_WINDOW 1024u
|
|
#define STREAM_CHUNK 64u
|
|
#define STREAM_POLL_SECONDS 0.01
|
|
#define STREAM_SETTLE_SECONDS 1.0
|
|
#define STREAM_MIN_PERCENT 75
|
|
#define STREAM_FIRST_BYTE 0x20u
|
|
#define STREAM_BYTE_VALUES 95u
|
|
#define STREAM_SHOWN_BYTES 16u
|
|
#define PERCENT 100.0
|
|
// 8N1: a start bit, 8 data bits, a stop bit.
|
|
#define LINE_BITS_PER_BYTE 10
|
|
|
|
// SERTEST's DTR check (examples/sertest/sertest.c).
|
|
#define DTR_CHECK "\x04"
|
|
#define DTR_ACK "\x06"
|
|
#define HANGUP_WATCH_SECONDS 3.0
|
|
#define FAILS_MAX 5u
|
|
|
|
// DOSBox nullmodem transparent:0 framing.
|
|
#define LINE_ESCAPE 0xFFu
|
|
#define LINE_DTR 0x02u
|
|
|
|
// SERTEST's DTR check markers, in the order it sends them.
|
|
typedef enum MarkE {
|
|
MARK_OFF = 0,
|
|
MARK_ON,
|
|
MARK_REOPENED,
|
|
MARK_HANGUP,
|
|
MARK_COUNT
|
|
} MarkE;
|
|
|
|
// DTR as it was at some point: never reported, down, or up.
|
|
typedef enum DtrStateE {
|
|
DTR_UNKNOWN = 0,
|
|
DTR_DOWN,
|
|
DTR_UP
|
|
} DtrStateE;
|
|
|
|
// One DTR state the guest reported, with the count of data bytes before it.
|
|
typedef struct DtrT {
|
|
size_t at;
|
|
bool up;
|
|
} DtrT;
|
|
|
|
// A transparent:0 stream split into data and the guest's DTR states (DtrT
|
|
// records, in the order they came).
|
|
typedef struct LinesT {
|
|
bool escaped;
|
|
size_t dataCount;
|
|
ToolBufT states;
|
|
} LinesT;
|
|
|
|
// The link to the guest: a pty's host end or a TCP connection, how long a
|
|
// read waits, and whether the stream is transparent:0 framed (--dtr-check).
|
|
typedef struct LinkT {
|
|
int fd;
|
|
bool isPty;
|
|
bool framed;
|
|
double pollSeconds;
|
|
LinesT lines;
|
|
} LinkT;
|
|
|
|
// Where in the stream each of SERTEST's markers was, the first count of
|
|
// them found.
|
|
typedef struct MarksT {
|
|
size_t at[MARK_COUNT];
|
|
uint32_t count;
|
|
} MarksT;
|
|
|
|
static void ackMarks(LinkT *link, const ToolBufT *got, size_t base, MarksT *marks);
|
|
static int closeHold(LinkT *link, double wall, double start, ToolBufT *got, const char *holdFile);
|
|
static int connectOne(uint16_t port, double wall, double start);
|
|
static int connectWithin(uint16_t port, double seconds);
|
|
static DtrStateE dtrBefore(const LinesT *lines, size_t position);
|
|
static bool dtrChanges(const LinesT *lines, size_t first, size_t last, bool up);
|
|
static int dtrCheck(LinkT *link, double wall, double start, ToolBufT *got);
|
|
static const DtrT *dtrStates(const LinesT *lines, size_t *count);
|
|
static size_t linesFeed(LinesT *lines, uint8_t *data, size_t len);
|
|
static void linkSend(LinkT *link, const void *data, size_t len);
|
|
static int loop(LinkT *link, double wall, double start, const char *banner, ToolBufT *got);
|
|
static void markFail(const char *label, const MarksT *marks, const ToolBufT *got, size_t base);
|
|
static int openPty(const char *pathFile);
|
|
static void rawMode(int fd);
|
|
static bool receive(LinkT *link, ToolBufT *into);
|
|
static int stream(LinkT *link, double wall, double start, size_t total, long rate);
|
|
static uint8_t streamByte(size_t index);
|
|
static int usage(void);
|
|
|
|
static const char *const kMarks[MARK_COUNT] = {
|
|
"JLDTR-OFF\r\n",
|
|
"JLDTR-ON\r\n",
|
|
"JLDTR-REOPENED\r\n",
|
|
"JLDTR-HANGUP\r\n"
|
|
};
|
|
|
|
|
|
// Acknowledge each of SERTEST's marker lines (after got[base]) as it
|
|
// completes, noting where it is.
|
|
static void ackMarks(LinkT *link, const ToolBufT *got, size_t base, MarksT *marks) {
|
|
const char *mark;
|
|
size_t at;
|
|
|
|
while (marks->count < MARK_COUNT) {
|
|
mark = kMarks[marks->count];
|
|
at = gateTextFind(got->data, got->len, base, mark, strlen(mark));
|
|
if (at == GATE_TEXT_NOT_FOUND) {
|
|
return;
|
|
}
|
|
marks->at[marks->count] = at;
|
|
marks->count++;
|
|
linkSend(link, DTR_ACK, strlen(DTR_ACK));
|
|
}
|
|
}
|
|
|
|
|
|
// After the loopback: SERTEST's DTR check for its closes alone, then the
|
|
// pty held open until holdFile exists.
|
|
static int closeHold(LinkT *link, double wall, double start, ToolBufT *got, const char *holdFile) {
|
|
MarksT marks;
|
|
size_t base;
|
|
int status;
|
|
|
|
base = got->len;
|
|
marks.count = 0u;
|
|
linkSend(link, DTR_CHECK, strlen(DTR_CHECK));
|
|
while (marks.count < MARK_COUNT && gateNetNow() - start < wall) {
|
|
if (!receive(link, got)) {
|
|
break;
|
|
}
|
|
ackMarks(link, got, base, &marks);
|
|
}
|
|
if (marks.count == MARK_COUNT) {
|
|
printf("CLOSE PASS: every marker acknowledged; SERTEST has closed the port\n");
|
|
status = 0;
|
|
} else {
|
|
markFail("CLOSE", &marks, got, base);
|
|
status = 1;
|
|
}
|
|
while (access(holdFile, F_OK) != 0 && gateNetNow() - start < wall) {
|
|
receive(link, NULL);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
|
|
// A connection to the emulator, tried every CONNECT_RETRY_SECONDS until
|
|
// wall seconds from start have gone; -1 when none was made.
|
|
static int connectOne(uint16_t port, double wall, double start) {
|
|
int fd;
|
|
|
|
while (gateNetNow() - start < wall) {
|
|
fd = connectWithin(port, CONNECT_SECONDS);
|
|
if (fd >= 0) {
|
|
return fd;
|
|
}
|
|
gateNetSleep(CONNECT_RETRY_SECONDS);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
// One connection attempt, given seconds to complete; -1 when it fails.
|
|
static int connectWithin(uint16_t port, double seconds) {
|
|
struct sockaddr_in addr;
|
|
socklen_t errLen;
|
|
int fd;
|
|
int flags;
|
|
int err;
|
|
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
addr.sin_addr.s_addr = inet_addr(GATE_NET_HOST);
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
return -1;
|
|
}
|
|
flags = fcntl(fd, F_GETFL);
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
err = 0;
|
|
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
|
err = errno;
|
|
errLen = sizeof(err);
|
|
if (err == EINPROGRESS && gateNetWait(fd, POLLOUT, seconds)) {
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen);
|
|
}
|
|
}
|
|
if (err != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
fcntl(fd, F_SETFL, flags);
|
|
return fd;
|
|
}
|
|
|
|
|
|
// DTR as it was when the data byte at position arrived.
|
|
static DtrStateE dtrBefore(const LinesT *lines, size_t position) {
|
|
const DtrT *states;
|
|
size_t count;
|
|
size_t i;
|
|
DtrStateE state;
|
|
|
|
states = dtrStates(lines, &count);
|
|
state = DTR_UNKNOWN;
|
|
for (i = 0; i < count && states[i].at <= position; i++) {
|
|
state = states[i].up ? DTR_UP : DTR_DOWN;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
|
|
// Whether DTR was reported as up (or down) after data byte first, up to
|
|
// last.
|
|
static bool dtrChanges(const LinesT *lines, size_t first, size_t last, bool up) {
|
|
const DtrT *states;
|
|
size_t count;
|
|
size_t i;
|
|
|
|
states = dtrStates(lines, &count);
|
|
for (i = 0; i < count; i++) {
|
|
if (first < states[i].at && states[i].at <= last && states[i].up == up) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// After the loopback (the stream so far is got, its line states decoded):
|
|
// SERTEST's DTR check, then the watch after its hang-up.
|
|
static int dtrCheck(LinkT *link, double wall, double start, ToolBufT *got) {
|
|
const LinesT *lines;
|
|
const DtrT *states;
|
|
const char *fails[FAILS_MAX];
|
|
MarksT marks;
|
|
size_t base;
|
|
size_t count;
|
|
size_t i;
|
|
uint32_t failCount;
|
|
uint32_t f;
|
|
double hangupAt;
|
|
bool watching;
|
|
bool shown;
|
|
|
|
lines = &link->lines;
|
|
base = got->len;
|
|
marks.count = 0u;
|
|
hangupAt = 0.0;
|
|
watching = false;
|
|
linkSend(link, DTR_CHECK, strlen(DTR_CHECK));
|
|
while (gateNetNow() - start < wall) {
|
|
if (watching && gateNetNow() - hangupAt >= HANGUP_WATCH_SECONDS) {
|
|
break;
|
|
}
|
|
if (!receive(link, got)) {
|
|
break;
|
|
}
|
|
// The last marker starts the watch.
|
|
ackMarks(link, got, base, &marks);
|
|
if (!watching && marks.count == MARK_COUNT) {
|
|
watching = true;
|
|
hangupAt = gateNetNow();
|
|
}
|
|
}
|
|
if (marks.count < MARK_COUNT) {
|
|
markFail("DTR", &marks, got, base);
|
|
return 1;
|
|
}
|
|
if (gateNetNow() - hangupAt < HANGUP_WATCH_SECONDS) {
|
|
printf("DTR FAIL: out of time before the watch after the hang-up ended\n");
|
|
return 1;
|
|
}
|
|
failCount = 0u;
|
|
if (dtrBefore(lines, marks.at[MARK_OFF]) != DTR_DOWN) {
|
|
fails[failCount++] = "jlSerialSetDtr(false) did not drop DTR";
|
|
}
|
|
if (dtrBefore(lines, marks.at[MARK_ON]) != DTR_UP) {
|
|
fails[failCount++] = "jlSerialSetDtr(true) did not raise DTR";
|
|
}
|
|
if (dtrChanges(lines, marks.at[MARK_ON], marks.at[MARK_REOPENED], false)) {
|
|
fails[failCount++] = "closing and opening the port dropped DTR";
|
|
}
|
|
if (dtrBefore(lines, marks.at[MARK_HANGUP]) != DTR_DOWN) {
|
|
fails[failCount++] = "jlSerialSetDtr(false) after the reopen did not drop DTR";
|
|
}
|
|
if (dtrChanges(lines, marks.at[MARK_HANGUP], lines->dataCount, true)) {
|
|
fails[failCount++] = "closing the port after the hang-up raised DTR";
|
|
}
|
|
if (failCount == 0u) {
|
|
printf("DTR PASS: SetDtr(false/true) reached the line, close+open kept DTR up, a hang-up stayed down %.0f s after the close\n", HANGUP_WATCH_SECONDS);
|
|
return 0;
|
|
}
|
|
printf("DTR FAIL: ");
|
|
for (f = 0; f < failCount; f++) {
|
|
printf("%s%s", f == 0u ? "" : "; ", fails[f]);
|
|
}
|
|
// The states reported since the check, each after its count of data
|
|
// bytes.
|
|
printf("; DTR since the check (data byte, state):");
|
|
states = dtrStates(lines, &count);
|
|
shown = false;
|
|
for (i = 0; i < count; i++) {
|
|
if (states[i].at >= base) {
|
|
printf("%s%zu %s", shown ? ", " : " ", states[i].at, states[i].up ? "up" : "down");
|
|
shown = true;
|
|
}
|
|
}
|
|
printf("%s\n", shown ? "" : " none");
|
|
return 1;
|
|
}
|
|
|
|
|
|
// The DTR states the guest has reported, and (in count) how many.
|
|
static const DtrT *dtrStates(const LinesT *lines, size_t *count) {
|
|
*count = lines->states.len / sizeof(DtrT);
|
|
return (const DtrT *)lines->states.data;
|
|
}
|
|
|
|
|
|
// The data bytes of len raw ones (in place, at the start of data), the DTR
|
|
// states among them recorded with the count of data bytes before each. An
|
|
// escape at the end waits for the byte after it.
|
|
static size_t linesFeed(LinesT *lines, uint8_t *data, size_t len) {
|
|
DtrT state;
|
|
size_t in;
|
|
size_t out;
|
|
|
|
out = 0u;
|
|
for (in = 0; in < len; in++) {
|
|
if (!lines->escaped && data[in] == LINE_ESCAPE) {
|
|
lines->escaped = true;
|
|
} else if (lines->escaped && data[in] != LINE_ESCAPE) {
|
|
lines->escaped = false;
|
|
state.at = lines->dataCount;
|
|
state.up = (data[in] & LINE_DTR) != 0u;
|
|
toolBufAdd(&lines->states, &state, sizeof(state));
|
|
} else {
|
|
lines->escaped = false;
|
|
data[out++] = data[in];
|
|
lines->dataCount++;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
// Every byte to the guest; a link that fails ends the peer (exit 1).
|
|
static void linkSend(LinkT *link, const void *data, size_t len) {
|
|
if (!gateNetSendAll(link->fd, data, len)) {
|
|
toolFail("send: %s", strerror(errno));
|
|
}
|
|
}
|
|
|
|
|
|
// Wait for the banner, then send the probe (again every RESEND_SECONDS)
|
|
// until it comes back. got collects the stream.
|
|
static int loop(LinkT *link, double wall, double start, const char *banner, ToolBufT *got) {
|
|
size_t sentAt;
|
|
double lastSend;
|
|
|
|
sentAt = GATE_TEXT_NOT_FOUND;
|
|
lastSend = 0.0;
|
|
while (gateNetNow() - start < wall) {
|
|
if (gateTextFind(got->data, got->len, 0u, banner, strlen(banner)) != GATE_TEXT_NOT_FOUND && (sentAt == GATE_TEXT_NOT_FOUND || gateNetNow() - lastSend > RESEND_SECONDS)) {
|
|
linkSend(link, PROBE_LINE, strlen(PROBE_LINE));
|
|
lastSend = gateNetNow();
|
|
if (sentAt == GATE_TEXT_NOT_FOUND) {
|
|
sentAt = got->len;
|
|
}
|
|
}
|
|
if (!receive(link, got)) {
|
|
break;
|
|
}
|
|
if (sentAt != GATE_TEXT_NOT_FOUND && gateTextFind(got->data, got->len, sentAt, PROBE, strlen(PROBE)) != GATE_TEXT_NOT_FOUND) {
|
|
printf("LOOP PASS: banner ");
|
|
gateNetShowBytes(stdout, banner, strlen(banner));
|
|
printf(" seen, probe echoed; stream=");
|
|
gateNetShowBytes(stdout, got->data, got->len < SHOWN_BYTES ? got->len : SHOWN_BYTES);
|
|
printf("\n");
|
|
return 0;
|
|
}
|
|
}
|
|
printf("LOOP FAIL: stream=");
|
|
gateNetShowBytes(stdout, got->data, got->len < FAIL_SHOWN_BYTES ? got->len : FAIL_SHOWN_BYTES);
|
|
printf("\n");
|
|
return 1;
|
|
}
|
|
|
|
|
|
// "<label> FAIL" for the first marker SERTEST did not send, with the stream
|
|
// since the check began.
|
|
static void markFail(const char *label, const MarksT *marks, const ToolBufT *got, size_t base) {
|
|
const char *mark;
|
|
size_t shown;
|
|
|
|
mark = kMarks[marks->count];
|
|
shown = got->len - base < SHOWN_BYTES ? got->len - base : SHOWN_BYTES;
|
|
printf("%s FAIL: no ", label);
|
|
gateNetShowBytes(stdout, mark, strlen(mark));
|
|
printf(" from SERTEST; stream=");
|
|
gateNetShowBytes(stdout, got->data + base, shown);
|
|
printf("\n");
|
|
}
|
|
|
|
|
|
// A raw pty pair: the host end returned, the guest end's path published in
|
|
// pathFile (whole, by a rename: the gate polls for the file). The guest end
|
|
// stays open here for the peer's life, so the guest's closes never hang the
|
|
// host end up.
|
|
static int openPty(const char *pathFile) {
|
|
const char *guestPath;
|
|
char *tmpPath;
|
|
size_t tmpLen;
|
|
int host;
|
|
int guest;
|
|
|
|
host = posix_openpt(O_RDWR | O_NOCTTY);
|
|
if (host < 0 || grantpt(host) != 0 || unlockpt(host) != 0 || (guestPath = ptsname(host)) == NULL) {
|
|
toolFail("cannot make a pty: %s", strerror(errno));
|
|
}
|
|
guest = open(guestPath, O_RDWR | O_NOCTTY);
|
|
if (guest < 0) {
|
|
toolFail("cannot open %s: %s", guestPath, strerror(errno));
|
|
}
|
|
rawMode(guest);
|
|
rawMode(host);
|
|
guestPath = ttyname(guest);
|
|
tmpLen = strlen(pathFile) + sizeof(TMP_SUFFIX);
|
|
tmpPath = (char *)malloc(tmpLen);
|
|
if (guestPath == NULL || tmpPath == NULL) {
|
|
toolFail("cannot name the pty");
|
|
}
|
|
snprintf(tmpPath, tmpLen, "%s%s", pathFile, TMP_SUFFIX);
|
|
if (!toolWriteFile(tmpPath, (const uint8_t *)guestPath, strlen(guestPath)) || rename(tmpPath, pathFile) != 0) {
|
|
toolFail("cannot write %s", pathFile);
|
|
}
|
|
free(tmpPath);
|
|
return host;
|
|
}
|
|
|
|
|
|
// Raw mode: every POSIX input and local mode off, no output processing, 8
|
|
// bits without parity, reads returning each byte.
|
|
static void rawMode(int fd) {
|
|
struct termios mode;
|
|
|
|
if (tcgetattr(fd, &mode) != 0) {
|
|
toolFail("tcgetattr: %s", strerror(errno));
|
|
}
|
|
mode.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY | IXOFF);
|
|
mode.c_oflag &= ~(tcflag_t)OPOST;
|
|
mode.c_cflag &= ~(tcflag_t)(PARENB | CSIZE);
|
|
mode.c_cflag |= CS8;
|
|
mode.c_lflag &= ~(tcflag_t)(ECHO | ECHOE | ECHOK | ECHONL | ICANON | IEXTEN | ISIG | NOFLSH | TOSTOP);
|
|
mode.c_cc[VMIN] = 1;
|
|
mode.c_cc[VTIME] = 0;
|
|
if (tcsetattr(fd, TCSAFLUSH, &mode) != 0) {
|
|
toolFail("tcsetattr: %s", strerror(errno));
|
|
}
|
|
}
|
|
|
|
|
|
// What the guest sent within the link's poll time, added to into (unless
|
|
// NULL): false at the end of a TCP stream. A pty's host end fails its reads
|
|
// while no guest has the other end open (SERTEST closes and reopens the
|
|
// port), so on a pty a failed read is only "nothing yet".
|
|
static bool receive(LinkT *link, ToolBufT *into) {
|
|
uint8_t raw[READ_SIZE];
|
|
int32_t got;
|
|
size_t len;
|
|
|
|
got = gateNetRecv(link->fd, raw, sizeof(raw), link->pollSeconds);
|
|
if (got <= 0) {
|
|
return got == GATE_NET_IDLE || link->isPty;
|
|
}
|
|
len = (size_t)got;
|
|
if (link->framed) {
|
|
len = linesFeed(&link->lines, raw, len);
|
|
}
|
|
if (into != NULL) {
|
|
toolBufAdd(into, raw, len);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// After the loopback: total bytes through SERTEST's echo, timed. The
|
|
// probe's last echoes go first.
|
|
static int stream(LinkT *link, double wall, double start, size_t total, long rate) {
|
|
ToolBufT got;
|
|
uint8_t chunk[STREAM_CHUNK];
|
|
size_t sent;
|
|
size_t len;
|
|
size_t at;
|
|
size_t i;
|
|
double settle;
|
|
double began;
|
|
double elapsed;
|
|
double perSecond;
|
|
double passMark;
|
|
|
|
settle = gateNetNow() + STREAM_SETTLE_SECONDS;
|
|
while (gateNetNow() < settle) {
|
|
if (!receive(link, NULL)) {
|
|
printf("STREAM FAIL: the connection closed before the stream\n");
|
|
return 1;
|
|
}
|
|
}
|
|
memset(&got, 0, sizeof(got));
|
|
sent = 0u;
|
|
began = gateNetNow();
|
|
while (got.len < total && gateNetNow() - start < wall) {
|
|
if (sent < total && (sent < got.len || sent - got.len < STREAM_WINDOW)) {
|
|
len = total - sent < STREAM_CHUNK ? total - sent : STREAM_CHUNK;
|
|
for (i = 0; i < len; i++) {
|
|
chunk[i] = streamByte(sent + i);
|
|
}
|
|
linkSend(link, chunk, len);
|
|
sent += len;
|
|
}
|
|
if (!receive(link, &got)) {
|
|
break;
|
|
}
|
|
}
|
|
elapsed = gateNetNow() - began;
|
|
perSecond = elapsed > 0.0 ? (double)got.len / elapsed : 0.0;
|
|
passMark = (double)rate / LINE_BITS_PER_BYTE * STREAM_MIN_PERCENT / PERCENT;
|
|
len = got.len < total ? got.len : total;
|
|
for (at = 0; at < len && got.data[at] == streamByte(at); at++) {
|
|
// Up to the first wrong byte.
|
|
}
|
|
if (got.len != total || at < total) {
|
|
printf("STREAM FAIL: %zu of %zu bytes back, the first wrong at %zu; ", got.len, total, at);
|
|
gateNetShowBytes(stdout, got.data + at, got.len - at < STREAM_SHOWN_BYTES ? got.len - at : STREAM_SHOWN_BYTES);
|
|
printf("\n");
|
|
return 1;
|
|
}
|
|
if (perSecond < passMark) {
|
|
printf("STREAM FAIL: %zu bytes back in order, %.0f a second, under %.0f (%d%% of %ld baud)\n", total, perSecond, passMark, STREAM_MIN_PERCENT, rate);
|
|
return 1;
|
|
}
|
|
printf("STREAM PASS: %zu bytes back in order, none lost, %.0f a second (%ld baud carries %ld)\n", total, perSecond, rate, rate / LINE_BITS_PER_BYTE);
|
|
return 0;
|
|
}
|
|
|
|
|
|
static uint8_t streamByte(size_t index) {
|
|
return (uint8_t)(STREAM_FIRST_BYTE + index % STREAM_BYTE_VALUES);
|
|
}
|
|
|
|
|
|
static int usage(void) {
|
|
fprintf(stderr, "usage: %s tcp-server PORT WALL [BANNER] listen; the emulator connects\n %s tcp-client PORT WALL [BANNER] connect to the emulator\n %s pty PATHFILE WALL [BANNER] make a raw pty pair, write the\n guest end's path to PATHFILE\noptions: --close-hold FILE (pty), --stream N --rate RATE (TCP), --dtr-check (TCP)\n", TOOL_NAME, TOOL_NAME, TOOL_NAME);
|
|
return GATE_USAGE_EXIT;
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
LinkT link;
|
|
ToolBufT got;
|
|
const char *pos[POS_MAX];
|
|
const char *holdFile;
|
|
const char *banner;
|
|
long streamBytes;
|
|
long rate;
|
|
uint16_t port;
|
|
double wall;
|
|
double start;
|
|
bool withDtrCheck;
|
|
bool isPty;
|
|
bool isServer;
|
|
int posCount;
|
|
int i;
|
|
int status;
|
|
|
|
gateNetStart(TOOL_NAME);
|
|
holdFile = NULL;
|
|
streamBytes = 0;
|
|
rate = 0;
|
|
withDtrCheck = false;
|
|
posCount = 0;
|
|
for (i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--dtr-check") == 0) {
|
|
withDtrCheck = true;
|
|
} else if (strcmp(argv[i], "--stream") == 0 && i + 1 < argc) {
|
|
streamBytes = gateArgLong("stream length", argv[++i]);
|
|
} else if (strcmp(argv[i], "--rate") == 0 && i + 1 < argc) {
|
|
rate = gateArgLong("rate", argv[++i]);
|
|
} else if (strcmp(argv[i], "--close-hold") == 0 && i + 1 < argc) {
|
|
holdFile = argv[++i];
|
|
} else if (strncmp(argv[i], OPTION_DASHES, strlen(OPTION_DASHES)) == 0 || posCount == POS_MAX) {
|
|
return usage();
|
|
} else {
|
|
pos[posCount++] = argv[i];
|
|
}
|
|
}
|
|
if (posCount < POS_MIN) {
|
|
return usage();
|
|
}
|
|
// --dtr-check and --stream are TCP's, --close-hold the pty's, and a
|
|
// stream needs its line rate.
|
|
isPty = strcmp(pos[POS_MODE], "pty") == 0;
|
|
isServer = strcmp(pos[POS_MODE], "tcp-server") == 0;
|
|
if ((!isPty && !isServer && strcmp(pos[POS_MODE], "tcp-client") != 0) || (withDtrCheck && isPty) || (holdFile != NULL && !isPty) || streamBytes < 0 || (streamBytes > 0 && (isPty || withDtrCheck || rate <= 0)) || (rate != 0 && streamBytes == 0)) {
|
|
return usage();
|
|
}
|
|
port = isPty ? 0u : gateArgPort(pos[POS_TARGET]);
|
|
wall = gateArgDouble("wall", pos[POS_WALL]);
|
|
banner = posCount > POS_BANNER ? pos[POS_BANNER] : DEFAULT_BANNER;
|
|
start = gateNetNow();
|
|
|
|
memset(&link, 0, sizeof(link));
|
|
memset(&got, 0, sizeof(got));
|
|
link.isPty = isPty;
|
|
link.framed = withDtrCheck;
|
|
link.pollSeconds = POLL_SECONDS;
|
|
if (isPty) {
|
|
link.fd = openPty(pos[POS_TARGET]);
|
|
status = loop(&link, wall, start, banner, &got);
|
|
if (status != 0 || holdFile == NULL) {
|
|
return status;
|
|
}
|
|
return closeHold(&link, wall, start, &got, holdFile);
|
|
}
|
|
link.fd = isServer ? gateNetAcceptOne(port, wall) : connectOne(port, wall, start);
|
|
if (link.fd < 0) {
|
|
printf("LOOP FAIL: no connection on port %u\n", (unsigned)port);
|
|
return 1;
|
|
}
|
|
status = loop(&link, wall, start, banner, &got);
|
|
if (status != 0) {
|
|
return status;
|
|
}
|
|
if (streamBytes > 0) {
|
|
link.pollSeconds = STREAM_POLL_SECONDS;
|
|
return stream(&link, wall, start, (size_t)streamBytes, rate);
|
|
}
|
|
if (!withDtrCheck) {
|
|
return status;
|
|
}
|
|
return dtrCheck(&link, wall, start, &got);
|
|
}
|