diff --git a/Makefile b/Makefile index 8574d1c..2e26592 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,11 @@ @mkdir -p obj $(CC) $(CFLAGS) -c src/ps2netfs.c -o obj/ps2netfs.o + OFILES += obj/resetcheck.o + obj/resetcheck.o: src/resetcheck.c src/resetcheck.h src/ps2link.h src/utility.h + @mkdir -p obj + $(CC) $(CFLAGS) -c src/resetcheck.c -o obj/resetcheck.o + OFILES += obj/utility.o obj/utility.o: src/utility.c src/utility.h @mkdir -p obj diff --git a/Makefile.mingw32 b/Makefile.mingw32 index 76176fe..4f8bb7a 100644 --- a/Makefile.mingw32 +++ b/Makefile.mingw32 @@ -33,6 +33,11 @@ @mkdir -p obj $(CC) $(INC) -c src/ps2netfs.c -o obj/ps2netfs.o + OFILES += obj/resetcheck.o + obj/resetcheck.o: src/resetcheck.c src/resetcheck.h src/ps2link.h src/utility.h + @mkdir -p obj + $(CC) $(INC) -c src/resetcheck.c -o obj/resetcheck.o + OFILES += obj/utility.o obj/utility.o: src/utility.c src/utility.h @mkdir -p obj diff --git a/README.md b/README.md index 1f367b2..912fe35 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,35 @@ Error messages go to stderr; stdout carries only the console output of the ps2. * `reset` -Send a reset request to ps2link. +Send an unacknowledged UDP reset request to ps2link. A zero exit code +only means the host sent a packet; it does **not** prove that the EE +processed the reset. In particular, a level-2 EE exception or wedged +EE command thread can prevent PS2Link from restarting. + +* `resetcheck` + +An opt-in, host-only reset observer compatible with **existing PS2Link +binaries** (no change to the PS2 required): + +```sh +ps2client -h 192.168.31.10 -t 10 resetcheck +``` + +It first requires a successful EE/IOP `host:` handshake. If PS2Link is +unreachable, **it sends no reset** and exits 69. Otherwise it sends +one legacy UDP RESET packet, then looks for a loss and subsequent +recovery of the `host:` round trip. Exit status 0 means that transition +was observed, **not** that RESET was acknowledged. Exit status 124 +means the transition was not observed within the timeout (default +10 seconds, valid range 1–120). A very quick reboot can be missed, +and unrelated network loss/recovery can mimic a reboot; neither +outcome proves whether the reset command executed. This command +does not provide out-of-band recovery for an EE stuck in a hardware +exception. Use a physical restart if the EE is wedged. + +Like `ping`, `resetcheck` temporarily takes over the single `host:` +request connection. Do not run it alongside an active `ps2client` +session servicing application files. * `execiop [arguments]` diff --git a/src/ps2client.c b/src/ps2client.c index 6472bfd..3dd4d2a 100644 --- a/src/ps2client.c +++ b/src/ps2client.c @@ -6,6 +6,7 @@ #include "utility.h" #include "ps2link.h" #include "network.h" +#include "resetcheck.h" char hostname[256] = { "192.168.0.10" }; @@ -19,7 +20,7 @@ // The commands: their minimum number of arguments, what they need from the // connection, and whether they wait for output afterwards. - enum { CMD_RESET, CMD_EXECIOP, CMD_EXECEE, CMD_POWEROFF, CMD_SCRDUMP, CMD_NETDUMP, CMD_DUMPMEM, CMD_STARTVU, + enum { CMD_RESET, CMD_RESETCHECK, CMD_EXECIOP, CMD_EXECEE, CMD_POWEROFF, CMD_SCRDUMP, CMD_NETDUMP, CMD_DUMPMEM, CMD_STARTVU, CMD_STOPVU, CMD_DUMPREG, CMD_GSEXEC, CMD_WRITEMEM, CMD_IOPEXCEP, CMD_LISTEN, CMD_PING, CMD_CONSOLE }; #define NEEDS_HOST 1 // ps2link reads or writes host: files, so wait until it can @@ -27,6 +28,7 @@ struct { char *name; int id; int args; int flags; } commands[] = { { "reset", CMD_RESET, 0, NO_WAIT }, + { "resetcheck", CMD_RESETCHECK, 0, NO_WAIT }, { "execiop", CMD_EXECIOP, 1, NEEDS_HOST }, { "execee", CMD_EXECEE, 1, NEEDS_HOST }, { "poweroff", CMD_POWEROFF, 0, NO_WAIT }, @@ -64,6 +66,7 @@ } int main(int argc, char **argv, char **env) { int loop0 = 0, command = -1, result = 0, what = 0; + const char *executable = argv[0]; unsigned int number0 = 0, number1 = 0; // Turn off stdout buffering. @@ -148,6 +151,12 @@ break; } + // Opt-in host-only reset observer; the legacy reset stays unchanged. + if (commands[command].id == CMD_RESETCHECK) { + if (argc != 0) { fprintf(stderr, "Error: resetcheck takes no arguments.\n"); return PS2CLIENT_EXIT_USAGE; } + return ps2client_resetcheck(executable, hostname, timeout < 0 ? 10 : timeout); + } + // Commands that exit right away need neither console nor request service. // Keep listen's existing host: service; console only receives UDP logs. if (commands[command].flags & NO_WAIT) { timeout = 0; } diff --git a/src/resetcheck.c b/src/resetcheck.c new file mode 100644 index 0000000..d73a942 --- /dev/null +++ b/src/resetcheck.c @@ -0,0 +1,103 @@ +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#endif + +#include "ps2link.h" +#include "resetcheck.h" +#include "utility.h" + +/* Separate processes avoid changing ps2client's legacy network/thread state. */ +static int run_command(const char *executable, const char *hostname, + const char *name, int quiet) +{ + char *const args[] = { + (char *)executable, "-h", (char *)hostname, "-t", "1", + (char *)name, NULL + }; +#ifdef _WIN32 + intptr_t status; + (void)quiet; + status = _spawnvp(_P_WAIT, executable, (const char *const *)args); + return status < 0 ? -1 : (int)status; +#else + pid_t pid = fork(); + int status; + if (pid < 0) return -1; + if (pid == 0) { + if (quiet) { + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + } + execvp(executable, args); + _exit(127); + } + do { + if (waitpid(pid, &status, 0) >= 0) break; + if (errno != EINTR) return -1; + } while (1); + return WIFEXITED(status) ? WEXITSTATUS(status) : -1; +#endif +} + +int ps2client_resetcheck(const char *executable, const char *hostname, + int timeout_seconds) +{ + long long deadline; + int saw_outage = 0; + int status; + + if (timeout_seconds < 1 || timeout_seconds > 120) { + fprintf(stderr, "resetcheck requires a -t timeout between 1 and 120 seconds.\n"); + return PS2CLIENT_EXIT_USAGE; + } + + fprintf(stderr, "resetcheck: checking the EE/IOP round trip before RESET...\n"); + if (run_command(executable, hostname, "ping", 1) != PS2CLIENT_EXIT_OK) { + fprintf(stderr, "resetcheck: PS2Link is not responding; RESET was NOT sent.\n"); + return PS2CLIENT_EXIT_NOCONNECT; + } + + fprintf(stderr, "resetcheck: sending one legacy UDP RESET request...\n"); + status = run_command(executable, hostname, "reset", 1); + if (status != PS2CLIENT_EXIT_OK) { + fprintf(stderr, "resetcheck: failed to send RESET (%d).\n", status); + return PS2CLIENT_EXIT_NOCONNECT; + } + + /* A UDP send has no delivery acknowledgment. Observing a loss and + * restoration of the EE host: round trip is consistent with a restart, + * not proof that the reset command was processed. */ + deadline = time_ms() + (long long)timeout_seconds * 1000; + while (time_ms() < deadline) { + status = run_command(executable, hostname, "ping", 1); + if (status != PS2CLIENT_EXIT_OK) { + if (!saw_outage) { + saw_outage = 1; + fprintf(stderr, "resetcheck: PS2Link round trip went offline.\n"); + } + } else if (saw_outage) { + fprintf(stderr, "resetcheck: PS2Link round trip recovered.\n"); + fprintf(stderr, "resetcheck: recovery observed (not a reset acknowledgment).\n"); + return PS2CLIENT_EXIT_OK; + } + if (time_ms() < deadline) sleep_ms(150); + } + if (!saw_outage) + fprintf(stderr, "resetcheck: no offline transition observed; reset UNCONFIRMED.\n"); + else + fprintf(stderr, "resetcheck: PS2Link did not recover; reset UNCONFIRMED.\n"); + return PS2CLIENT_EXIT_TIMEOUT; +} diff --git a/src/resetcheck.h b/src/resetcheck.h new file mode 100644 index 0000000..6f94ee9 --- /dev/null +++ b/src/resetcheck.h @@ -0,0 +1,8 @@ +#ifndef PS2CLIENT_RESETCHECK_H +#define PS2CLIENT_RESETCHECK_H + +/* Opt-in host-only reset observer, compatible with existing PS2Link. */ +int ps2client_resetcheck(const char *executable, const char *hostname, + int timeout_seconds); + +#endif diff --git a/src/utility.c b/src/utility.c index 90a2d1e..7f62eca 100644 --- a/src/utility.c +++ b/src/utility.c @@ -127,6 +127,7 @@ fprintf(stderr, " number after 'status=' on that line (0 if there is none)\n\n"); fprintf(stderr, " Available commands:\n\n"); fprintf(stderr, " reset\n"); + fprintf(stderr, " resetcheck preflight EE ping, send RESET, observe loss and recovery (-t default 10)\n"); fprintf(stderr, " execiop [arguments]\n"); fprintf(stderr, " execee [arguments]\n"); fprintf(stderr, " poweroff\n"); diff --git a/tests/fake_ps2link.c b/tests/fake_ps2link.c index d0239f9..5d8bd44 100644 --- a/tests/fake_ps2link.c +++ b/tests/fake_ps2link.c @@ -15,6 +15,7 @@ #include #include +#define CMD_RESET 0xBABE0201u #define CMD_EXECEE 0xBABE0203u #define CMD_DUMPREG 0xBABE020Au #define REQ_OPEN 0xBABE0111u @@ -27,9 +28,17 @@ struct fake { struct sockaddr_in client_addr; pthread_mutex_t lock; int ignore_dumpreg; + int reset_outage_ms; + long long unavailable_until_ms; }; static struct fake fake = {.client = -1, .lock = PTHREAD_MUTEX_INITIALIZER}; +static long long now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} static uint32_t get32(const unsigned char *p) { uint32_t n; @@ -239,10 +248,22 @@ static void *run_script(void *context) static void command_packet(unsigned char *packet, size_t length) { uint32_t type; - if (length < 10) return; + if (length < 6) return; type = get32(packet); - if (type == CMD_DUMPREG && length >= 266) { + if (type == CMD_RESET && length == 6) { + if (fake.reset_outage_ms > 0) { + fake.unavailable_until_ms = now_ms() + fake.reset_outage_ms; + pthread_mutex_lock(&fake.lock); + if (fake.client >= 0) { + shutdown(fake.client, SHUT_RDWR); + close(fake.client); + fake.client = -1; + } + pthread_mutex_unlock(&fake.lock); + } + } else if (type == CMD_DUMPREG && length >= 266) { int fd; + if (fake.unavailable_until_ms > now_ms()) return; if (fake.ignore_dumpreg > 0) { fake.ignore_dumpreg--; return; } fd = fake_open((char *)packet + 10, 0x0202); if (fd >= 0) { @@ -303,9 +324,11 @@ int main(int argc, char **argv) if (!strcmp(argv[i], "--bind") && i + 1 < argc) ip = argv[++i]; else if (!strcmp(argv[i], "--ignore-dumpreg") && i + 1 < argc) fake.ignore_dumpreg = atoi(argv[++i]); + else if (!strcmp(argv[i], "--reset-outage-ms") && i + 1 < argc) + fake.reset_outage_ms = atoi(argv[++i]); else { fprintf(stderr, "Unknown option: %s\n", argv[i]); return 2; } } - if (!ip) { fprintf(stderr, "Usage: fake_ps2link --bind IPV4 [--ignore-dumpreg N]\n"); return 2; } + if (!ip) { fprintf(stderr, "Usage: fake_ps2link --bind IPV4 [--ignore-dumpreg N] [--reset-outage-ms N]\n"); return 2; } fake.listener = bind_port(SOCK_STREAM, ip, 0x4711); fake.commands = bind_port(SOCK_DGRAM, ip, 0x4712); if (fake.listener < 0 || fake.commands < 0 || listen(fake.listener, 5) < 0) { diff --git a/tests/smoke.c b/tests/smoke.c index 4a4b630..6a41ff4 100644 --- a/tests/smoke.c +++ b/tests/smoke.c @@ -118,6 +118,59 @@ static void test_reset(void) packet[4] == 0 && packet[5] == 6, "reset uses command UDP without TCP"); } + +static void test_resetcheck_unreachable(void) +{ + struct sockaddr_in address = {0}; + struct timeval timeout = {0, 250000}; + unsigned char packet[32]; + char output[1024]; + char *const args[] = {(char *)client, "-h", "127.0.0.1", + "-t", "1", "resetcheck", NULL}; + int fd = socket(AF_INET, SOCK_DGRAM, 0), rc; + ssize_t n; + if (fd < 0) { check(0, "resetcheck UDP safety socket"); return; } + address.sin_family = AF_INET; + address.sin_port = htons(0x4712); + address.sin_addr.s_addr = htonl(0x7f000001u); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if (bind(fd, (struct sockaddr *)&address, sizeof(address)) < 0) { + check(0, "resetcheck UDP safety bind"); + close(fd); + return; + } + rc = run(args, NULL, output, sizeof(output)); + n = recv(fd, packet, sizeof(packet), 0); + close(fd); + check(rc == 69 && n < 0 && strstr(output, "RESET was NOT sent"), + "resetcheck refuses to send RESET without a healthy EE handshake"); +} +static void test_resetcheck_fake(int outage_ms, const char *timeout, + int expected, const char *message) +{ + char output[2048]; + char *const args[] = {(char *)client, "-h", "127.0.0.1", + "-t", (char *)timeout, "resetcheck", NULL}; + pid_t server = fork(); + int rc; + if (server < 0) { check(0, message); return; } + if (server == 0) { + char outage[24]; + snprintf(outage, sizeof(outage), "%d", outage_ms); + execl("bin/fake_ps2link", "fake_ps2link", "--bind", "127.0.0.1", + "--reset-outage-ms", outage, (char *)NULL); + _exit(127); + } + pause_ms(300); + rc = run(args, NULL, output, sizeof(output)); + kill(server, SIGTERM); + waitpid(server, NULL, 0); + check(rc == expected && + (expected == 0 ? strstr(output, "recovery observed") != NULL : + strstr(output, "UNCONFIRMED") != NULL), + message); +} + int main(int argc, char **argv) { char text[4096]; @@ -138,5 +191,22 @@ int main(int argc, char **argv) check(rc == 69, "legacy listen requires host: service"); test_console(); test_reset(); + { char output[1024]; + char *const invalid_timeout[] = {(char *)client, "-t", "0", + "resetcheck", NULL}; + char *const extra_argument[] = {(char *)client, "resetcheck", + "unexpected", NULL}; + check(run(invalid_timeout, NULL, output, sizeof(output)) == 64, + "resetcheck rejects zero timeout"); + check(run(extra_argument, NULL, output, sizeof(output)) == 64, + "resetcheck rejects extra arguments"); + } + test_resetcheck_unreachable(); + test_resetcheck_fake(1600, "6", 0, + "resetcheck detects simulated loss and recovery"); + test_resetcheck_fake(0, "2", 124, + "resetcheck refuses to claim success without a loss"); + test_resetcheck_fake(15000, "2", 124, + "resetcheck reports a failed recovery"); return failures ? 1 : 0; } diff --git a/tests/smoke_windows.c b/tests/smoke_windows.c index 8445868..d8fa3ce 100644 --- a/tests/smoke_windows.c +++ b/tests/smoke_windows.c @@ -117,6 +117,10 @@ int main(void) SetEnvironmentVariableA("PS2HOSTNAME", NULL); test_console(); test_reset(); + check(run("-h 1.1.l.1 -t 1 resetcheck") == 69, + "resetcheck refuses to send RESET to an unreachable PS2"); + check(run("-h 127.0.0.1 -t 0 resetcheck") == 64, + "resetcheck rejects an invalid observation timeout"); WSACleanup(); return failures ? 1 : 0;