// 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 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 #include #include #include #include #include #include #include #include #include #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; } // "