From 130215c21ce4aa300abc9a276e2ffb35c8f43803 Mon Sep 17 00:00:00 2001 From: gbc dev Date: Fri, 17 Jul 2026 19:46:58 +0200 Subject: gb: auto-reconnect a dead --serial-sock link instead of going silent Root-cause work for the 'idle link-socket drop' (see gbos docs/link-drop-investigation.md): when the hub restarts or the bridge dies, the link fd went dead forever - and before SIGPIPE was ignored, the next console-echo write() would kill the whole emulator silently. Now, in --serial-sock mode, EOF on read or EPIPE/ECONNRESET/ENOTCONN/ EBADF on either direction marks the link dead, closes the fd, and retries connect() every 500ms from the serial poll path (the guest polls constantly, so no extra plumbing). RX stays pending while down (serial_no_eof semantics), so the guest just sees a quiet link that comes back. Logs 'link socket lost / reconnected' either way. Verified: SIGKILL the hub, restart it - every GB reconnects within ~1s and keeps its lease. --- src/gb.c | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- src/gb.h | 6 ++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/gb.c b/src/gb.c index 2aba2a1..aed51b7 100644 --- a/src/gb.c +++ b/src/gb.c @@ -6,6 +6,59 @@ #include #include #include +#include +#include +#include +#include +#include + +// ---------- link socket resilience (--serial-sock) ---------- +// The link/network bridge socket can die under us (hub restarted, peer +// killed). A dead link must not be fatal or silent: mark it down, then retry +// a background reconnect at most every 500ms from the serial poll path. RX +// stays "pending" while down (serial_no_eof), so the guest just sees a quiet +// link that comes back. + +static double mono_sec(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1e9; +} + +static void serial_mark_dead(GB *gb) { + if (!gb->serial_sock_path || gb->serial_sock_dead) return; + fprintf(stderr, "[sl0pboy] link socket lost (%s); reconnecting...\n", + gb->serial_sock_path); + if (gb->serial_in_fd >= 0) close(gb->serial_in_fd); + gb->serial_in_fd = gb->serial_out_fd = -1; + gb->serial_sock_dead = true; + gb->serial_retry_at = mono_sec() + 0.5; +} + +// One reconnect attempt (rate-limited). Returns true if the link is up. +static bool serial_check(GB *gb) { + if (!gb->serial_sock_dead) return gb->serial_in_fd >= 0; + double now = mono_sec(); + if (now < gb->serial_retry_at) return false; + gb->serial_retry_at = now + 0.5; + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return false; + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, gb->serial_sock_path, sizeof(a.sun_path) - 1); + if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { + close(fd); + return false; + } + int fl = fcntl(fd, F_GETFL, 0); + if (fl != -1) fcntl(fd, F_SETFL, fl | O_NONBLOCK); + gb->serial_in_fd = gb->serial_out_fd = fd; + gb->serial_sock_dead = false; + fprintf(stderr, "[sl0pboy] link socket reconnected (%s)\n", + gb->serial_sock_path); + return true; +} void gb_request_interrupt(GB *gb, u8 flag) { gb->iff |= flag; @@ -129,9 +182,13 @@ void gb_write(GB *gb, u16 addr, u8 val) { // internal clock (GB is master): transmit gb->sb. Completes now. u8 outb = gb->sb; if (gb->serial_log) { fputc(outb, stderr); fflush(stderr); } + if (gb->serial_sock_path && gb->serial_sock_dead) serial_check(gb); if (gb->serial_out_fd >= 0) { unsigned char c = outb; - (void)!write(gb->serial_out_fd, &c, 1); + ssize_t w = write(gb->serial_out_fd, &c, 1); + if (w < 0 && (errno == EPIPE || errno == ECONNRESET || + errno == EBADF || errno == ENOTCONN)) + serial_mark_dead(gb); } gb->sb = 0xFF; // nothing shifted in on a pure TX gb->sc &= ~0x80; @@ -141,6 +198,7 @@ void gb_write(GB *gb, u16 addr, u8 val) { // Complete only when an input byte is available - and do NOT echo // it to stdout - so a program can poll for input without emitting // junk. Stays pending (bit7 set) while stdin has nothing. + if (gb->serial_sock_path && gb->serial_sock_dead) serial_check(gb); if (gb->serial_in_fd >= 0) { unsigned char c; ssize_t n = read(gb->serial_in_fd, &c, 1); @@ -149,13 +207,18 @@ void gb_write(GB *gb, u16 addr, u8 val) { gb->sc &= ~0x80; gb_request_interrupt(gb, INT_SERIAL); } else if (n == 0) { - if (!gb->serial_no_eof) { + if (gb->serial_sock_path) { + serial_mark_dead(gb); // peer closed: reconnect, stay pending + } else if (!gb->serial_no_eof) { gb->sb = 0x04; // EOF -> EOT (Ctrl-D) gb->sc &= ~0x80; gb_request_interrupt(gb, INT_SERIAL); } // else: stay pending (OSK provides input) + } else if (errno == ECONNRESET || errno == EBADF || + errno == ENOTCONN) { + serial_mark_dead(gb); } - // n < 0 (EAGAIN on a live tty): stay pending, poll again + // other n < 0 (EAGAIN on a live link): stay pending, poll again } } return; diff --git a/src/gb.h b/src/gb.h index 7162b4d..33746b4 100644 --- a/src/gb.h +++ b/src/gb.h @@ -117,6 +117,12 @@ struct GB { bool serial_log; // print serial output to stderr (blargg tests) int serial_out_fd; // if >=0, transmitted serial bytes written here (stdout) int serial_in_fd; // if >=0, received serial bytes read here (stdin), else 0xFF + // Link-over-unix-socket resilience: when serial_sock_path is set, a dead + // socket (EOF / EPIPE / ECONNRESET) is closed and reconnected in the + // background instead of going silently dead forever. See serial_check(). + const char *serial_sock_path; // --serial-sock path (NULL = stdio serial) + bool serial_sock_dead; // link socket lost; reconnect pending + double serial_retry_at; // CLOCK_MONOTONIC time of next reconnect try bool poweroff; // set by the $ED opcode: request a clean emulator exit // Boot ROM (BIOS). When boot_rom is non-NULL, gb_reset starts execution -- cgit v1.3.1-sl0p