Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Makefile.mingw32
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <filename> [arguments]`

Expand Down
11 changes: 10 additions & 1 deletion src/ps2client.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "utility.h"
#include "ps2link.h"
#include "network.h"
#include "resetcheck.h"

char hostname[256] = { "192.168.0.10" };

Expand All @@ -19,14 +20,15 @@

// 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
#define NO_WAIT 2 // exit right after sending the command

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 },
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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; }
Expand Down
103 changes: 103 additions & 0 deletions src/resetcheck.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include <stdio.h>
#include <stdlib.h>

#ifdef _WIN32
#include <process.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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;
}
8 changes: 8 additions & 0 deletions src/resetcheck.h
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/utility.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <filename> [arguments]\n");
fprintf(stderr, " execee <filename> [arguments]\n");
fprintf(stderr, " poweroff\n");
Expand Down
29 changes: 26 additions & 3 deletions tests/fake_ps2link.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <unistd.h>
#include <time.h>

#define CMD_RESET 0xBABE0201u
#define CMD_EXECEE 0xBABE0203u
#define CMD_DUMPREG 0xBABE020Au
#define REQ_OPEN 0xBABE0111u
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
70 changes: 70 additions & 0 deletions tests/smoke.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
}
4 changes: 4 additions & 0 deletions tests/smoke_windows.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading