From e4c3d4b770df8db4624f3871565b0748df6494cb Mon Sep 17 00:00:00 2001 From: Rick Gaiser Date: Sun, 25 Oct 2026 16:31:15 +0100 Subject: [PATCH 1/2] ps2client: report errors, exit statuses, ping and end marker Fixes: - Report a failed connection instead of waiting for the timeout; connect with a 3 s timeout; do not crash on an unknown hostname. - Fail when the console port is in use (another ps2client) instead of running without console output. - Exit with meaningful statuses: 64 usage, 69 ps2link not answering, 74 ps2link closed the connection, 75 console port in use. - Detect ps2link closing the connection instead of spinning at 100% CPU; ignore SIGPIPE. - Print console packets byte-exact (they are not null-terminated) and receive packets up to 64 KB instead of cutting them off at 1 KB. - Check the length of execee/execiop arguments and the number of arguments of each command; parse 0x numbers. - gsexec sent its size with htonl into a short (always 0). - Bounds checks for request packets and directory descriptors; initialize getstat data when stat fails. - Error messages go to stderr, so stdout is only the ps2 console. Features: - ping: check that ps2link answers through a host: request (EE side, IOP side and host connection), without writing a file. - -e : exit when a console line contains the marker, with the number after "status=" as exit status (124 on idle timeout first). - Commands that use host: files first wait until ps2link answers that check, instead of a fixed 1 s sleep. - dumpreg accepts register type names. --- README.md | 43 ++++++- src/network.c | 102 +++++++++++++++-- src/network.h | 9 +- src/ps2client.c | 168 +++++++++++++++++++++++----- src/ps2link.c | 290 ++++++++++++++++++++++++++++++++++++++---------- src/ps2link.h | 29 ++++- src/utility.c | 119 +++++++++++++------- src/utility.h | 8 +- 8 files changed, 622 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index 72ecf19..078f6d9 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Build command: Basic usage: ```txt -ps2client [-h hostname] [-t timeout] [arguments] +ps2client [-h hostname] [-t timeout] [-e marker] [arguments] fsclient [-h hostname] ``` @@ -54,6 +54,26 @@ The address of the remote ps2. This can be an IP or a hostname. If none is given #### \[-t timeout] An idle timeout period in seconds that the client will wait before exiting. This is useful to allow a script to continue after calling ps2client to send a command to ps2link or ps2netfs. +For `ping`, it is how long to try (default 2 seconds). + +#### \[-e marker] + +Exit as soon as a line of console output contains `marker`. The exit status +is the number after `status=` on that line, or 0 if there is none. A test +program can print for example `@@END@@ status=1` as its last line, so a script +gets the result without waiting for a timeout. + +### EXIT STATUS + +* `0`: done (or the idle timeout passed, without `-e`) +* `64`: usage error +* `69`: ps2link is not answering +* `74`: ps2link closed the connection (reset, reboot or crash of the ps2) +* `75`: the console port is in use, probably by another ps2client +* `124`: with `-e`: the idle timeout passed before the marker +* with `-e`: the status from the marker line + +Error messages go to stderr; stdout carries only the console output of the ps2. ### PS2LINK COMMANDS @@ -95,7 +115,9 @@ Tell the specified vector unit to stop operation. * `dumpreg ` -Dump the registers of a given type into a file. +Dump the registers of a given type into a file. The type is a number or one +of `dma`, `intc`, `timer`, `gs`, `sif`, `fifo`, `gif`, `vif0`, `vif1`, `ipu`, +`all`, `vu0`, `vu1`. * `gsexec ` @@ -113,6 +135,23 @@ I really don't know! OH NOES!! Listen to the ps2link network console. +* `ping` + +Check that ps2link is up: ps2link is asked to dump two registers to a host +file, which the client discards, so this checks the EE side, the IOP side and +the host connection. Exits 0 and prints the round trip time, or exits 69. +Note that ps2link serves one host connection, and a new client replaces the +old one: do not ping while another client is serving a running program. + +Commands that load host files (`execee`, `execiop`, ...) do the same check +before they send the command, instead of waiting a fixed time. + +### TESTS + +`tests/run-tests.sh` runs the client against a fake ps2link +(`tests/fake_ps2link.py`) in unprivileged Linux network namespaces, so no ps2 +is needed. + ## Community Links for discussion and chat are available diff --git a/src/network.c b/src/network.c index 81aa0f6..df340e9 100644 --- a/src/network.c +++ b/src/network.c @@ -1,6 +1,9 @@ #ifndef _WIN32 + #include + #include #include + #include #include #include #include @@ -8,6 +11,8 @@ #include #endif + #include + #include "network.h" /////////////////////// @@ -21,18 +26,46 @@ // Start up winsock. if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) { return -1; } +#else + // A send on a connection the ps2 has closed must fail, not kill us. + signal(SIGPIPE, SIG_IGN); #endif // End function. return 0; } - int network_connect(char *hostname, int port, int type) { int sock = -1; - struct sockaddr_in sockaddr; + static int network_set_blocking(int sock, int blocking) { +#ifdef _WIN32 + u_long mode = blocking ? 0 : 1; + + return ioctlsocket(sock, FIONBIO, &mode) == 0 ? 0 : -1; +#else + int flags = fcntl(sock, F_GETFL, 0); if (flags < 0) { return -1; } + + if (blocking) { flags &= ~O_NONBLOCK; } else { flags |= O_NONBLOCK; } + + return fcntl(sock, F_SETFL, flags); +#endif + } + + int network_connect(char *hostname, int port, int type) { + + // Connect without a timeout. + return network_connect_timeout(hostname, port, type, -1); + + } + + int network_connect_timeout(char *hostname, int port, int type, int timeout_ms) { int sock = -1; + struct sockaddr_in sockaddr; struct hostent *host; + + // Resolve the hostname. + host = gethostbyname(hostname); if (host == NULL || host->h_addr == NULL) { return -1; } // Populate the sockaddr structure. + memset(&sockaddr, 0, sizeof(sockaddr)); sockaddr.sin_family = AF_INET; sockaddr.sin_port = htons(port); - sockaddr.sin_addr = *(struct in_addr *)gethostbyname(hostname)->h_addr; + sockaddr.sin_addr = *(struct in_addr *)host->h_addr; // Open the socket. sock = socket(AF_INET, type, 0); if (sock < 0) { return -1; } @@ -46,8 +79,32 @@ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&nodelay, sizeof(nodelay)); } - // Connect the socket. - if (connect(sock, (struct sockaddr *)&sockaddr, sizeof(struct sockaddr)) < 0) { return -2; } + // Without a timeout, connect the socket and wait as long as it takes. + if (timeout_ms < 0) { + if (connect(sock, (struct sockaddr *)&sockaddr, sizeof(struct sockaddr)) < 0) { network_disconnect(sock); return -2; } + return sock; + } + + // Else, start a non-blocking connect and wait for it until the timeout. + if (network_set_blocking(sock, 0) < 0) { network_disconnect(sock); return -1; } + if (connect(sock, (struct sockaddr *)&sockaddr, sizeof(struct sockaddr)) < 0) { + fd_set wfds, efds; struct timeval tv; int error = 0; socklen_t len = sizeof(error); + +#ifdef _WIN32 + if (WSAGetLastError() != WSAEWOULDBLOCK) { network_disconnect(sock); return -2; } +#else + if (errno != EINPROGRESS) { network_disconnect(sock); return -2; } +#endif + + FD_ZERO(&wfds); FD_SET(sock, &wfds); + FD_ZERO(&efds); FD_SET(sock, &efds); + tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; + + // Timed out, or the connect failed. + if (select(sock + 1, NULL, &wfds, &efds, &tv) <= 0) { network_disconnect(sock); return -2; } + if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&error, &len) < 0 || error != 0) { network_disconnect(sock); return -2; } + } + if (network_set_blocking(sock, 1) < 0) { network_disconnect(sock); return -1; } // Return the socket. return sock; @@ -58,6 +115,7 @@ struct sockaddr_in sockaddr; // Populate the sockaddr structure. + memset(&sockaddr, 0, sizeof(sockaddr)); sockaddr.sin_family = AF_INET; sockaddr.sin_port = htons(port); sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); @@ -66,7 +124,7 @@ sock = socket(AF_INET, type, 0); if (sock < 0) { return -1; } // Bind the socket. - if (bind(sock, (struct sockaddr *)&sockaddr, sizeof(struct sockaddr)) < 0) { return -1; } + if (bind(sock, (struct sockaddr *)&sockaddr, sizeof(struct sockaddr)) < 0) { network_disconnect(sock); return -2; } // Return the socket. return sock; @@ -76,7 +134,19 @@ int network_send(int sock, void *buffer, int size) { int total = 0; // Keep sending data until it has all been sent. - while (total < size) { total += send(sock, &((char *)buffer)[total], size - total, 0); } + while (total < size) { + int sent = send(sock, &((char *)buffer)[total], size - total, 0); + + // The connection is gone. + if (sent <= 0) { +#ifndef _WIN32 + if (sent < 0 && errno == EINTR) { continue; } +#endif + return -1; + } + + total += sent; + } // Return the total bytes sent. return total; @@ -93,10 +163,10 @@ tv.tv_sec = timeout; tv.tv_usec = 0; // No timeout was specified, so wait forever. - if (timeout < 0) { return select(FD_SETSIZE, &nfds, NULL, NULL, NULL); } + if (timeout < 0) { return select(sock + 1, &nfds, NULL, NULL, NULL); } // A timeout was specified, so wait until the time has elapsed. - else { return select(FD_SETSIZE, &nfds, NULL, NULL, &tv); } + else { return select(sock + 1, &nfds, NULL, NULL, &tv); } // End function. return 0; @@ -113,7 +183,19 @@ int network_receive_all(int sock, void *buffer, int size) { int total = 0; // Receive the data from the socket. - while (total < size) { total += recvfrom(sock, &((char *)buffer)[total], size - total, 0, NULL, NULL); } + while (total < size) { + int received = recvfrom(sock, &((char *)buffer)[total], size - total, 0, NULL, NULL); + + // The connection was closed (0) or failed (<0). + if (received <= 0) { +#ifndef _WIN32 + if (received < 0 && errno == EINTR) { continue; } +#endif + return -1; + } + + total += received; + } // Return the total bytes received. return total; diff --git a/src/network.h b/src/network.h index a1fe603..576fe59 100644 --- a/src/network.h +++ b/src/network.h @@ -3,6 +3,7 @@ #ifdef _WIN32 #include + #include #include #else #include @@ -16,9 +17,12 @@ int network_connect(char *hostname, int port, int type); - int network_listen(int port, int type); + // Like network_connect, but give up after timeout_ms (-1: no timeout). + // Returns -1 if the hostname does not resolve, -2 if the connect fails. + int network_connect_timeout(char *hostname, int port, int type, int timeout_ms); - int network_accept(int sock); + // Returns -2 if the port is in use. + int network_listen(int port, int type); int network_send(int sock, void *buffer, int size); @@ -26,6 +30,7 @@ int network_receive(int sock, void *buffer, int size); + // Returns -1 if the connection was closed or failed before size bytes came in. int network_receive_all(int sock, void *buffer, int size); int network_disconnect(int sock); diff --git a/src/ps2client.c b/src/ps2client.c index 6facf84..8a8bd22 100644 --- a/src/ps2client.c +++ b/src/ps2client.c @@ -11,7 +11,59 @@ int timeout = -1; - int main(int argc, char **argv, char **env) { int loop0 = 0; + char *marker = NULL; + + // How long to wait for ps2link to accept the connection and to answer. + #define CONNECT_TIMEOUT_MS 3000 + #define HANDSHAKE_TIMEOUT_MS 3000 + + // 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, + CMD_STOPVU, CMD_DUMPREG, CMD_GSEXEC, CMD_WRITEMEM, CMD_IOPEXCEP, CMD_LISTEN, CMD_PING }; + + #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 }, + { "execiop", CMD_EXECIOP, 1, NEEDS_HOST }, + { "execee", CMD_EXECEE, 1, NEEDS_HOST }, + { "poweroff", CMD_POWEROFF, 0, NO_WAIT }, + { "scrdump", CMD_SCRDUMP, 0, NO_WAIT }, + { "netdump", CMD_NETDUMP, 0, NO_WAIT }, + { "dumpmem", CMD_DUMPMEM, 3, NEEDS_HOST }, + { "startvu", CMD_STARTVU, 1, NO_WAIT }, + { "stopvu", CMD_STOPVU, 1, NO_WAIT }, + { "dumpreg", CMD_DUMPREG, 2, NEEDS_HOST }, + { "gsexec", CMD_GSEXEC, 2, NEEDS_HOST }, + { "writemem", CMD_WRITEMEM, 3, NEEDS_HOST }, + { "iopexcep", CMD_IOPEXCEP, 0, NO_WAIT }, + { "listen", CMD_LISTEN, 0, 0 }, + { "ping", CMD_PING, 0, NEEDS_HOST }, + }; + + // Register dump types, by name. + char *regtypes[] = { "dma", "intc", "timer", "gs", "sif", "fifo", "gif", "vif0", "vif1", "ipu", "all", "vu0", "vu1" }; + + // Parse a number, decimal or 0x hexadecimal. Returns -1 if it is not one. + int parse_number(char *string, unsigned int *value) { char *end; + + *value = strtoul(string, &end, 0); + return (*string == 0 || *end != 0) ? -1 : 0; + + } + + int parse_regtype(char *string) { unsigned int loop0, value; + + for (loop0=0; loop0 256) { fprintf(stderr, "Error: The filename and arguments are too long (%d bytes, at most 256 with a null after each).\n", length); return PS2CLIENT_EXIT_USAGE; } + break; + } + case CMD_STARTVU: case CMD_STOPVU: case CMD_GSEXEC: + if (parse_number(argv[0], &number0) < 0) { fprintf(stderr, "Error: Bad number. (%s)\n", argv[0]); return PS2CLIENT_EXIT_USAGE; } + break; + } + + // Commands that exit right away need neither the console nor the request + // connection. ping needs only the request connection. + if (commands[command].flags & NO_WAIT) { timeout = 0; } + if (commands[command].flags & NEEDS_HOST) { what |= PS2LINK_CONNECT_REQUEST; } + if (!(commands[command].flags & NO_WAIT) && commands[command].id != CMD_PING) { what |= PS2LINK_CONNECT_CONSOLE | PS2LINK_CONNECT_REQUEST; } // Startup network. - if (network_startup() < 0) { printf("Error: Could not startup network.\n"); return 1; } + if (network_startup() < 0) { fprintf(stderr, "Error: Could not startup network.\n"); return PS2CLIENT_EXIT_NOCONNECT; } + + // ping: is ps2link up and serving host: files? -t is how long to try. + if (commands[command].id == CMD_PING) { int limit = timeout > 0 ? timeout * 1000 : 2000; long long start = time_ms(); + + if ((result = ps2link_connect(hostname, what, limit)) != 0) { return result; } + result = ps2link_handshake(limit - (int)(time_ms() - start)); + ps2link_disconnect(); + if (result < 0) { fprintf(stderr, "ps2link at %s is not answering.\n", hostname); return PS2CLIENT_EXIT_NOCONNECT; } + printf("ps2link at %s answered in %d ms.\n", hostname, result); + return PS2CLIENT_EXIT_OK; + + } // Connect to the ps2link server. - if (ps2link_connect(hostname) < 0) { printf("Error: Could not connect to the ps2link server. (%s)\n", hostname); return -1; } + ps2link_set_marker(marker); + if ((result = ps2link_connect(hostname, what, CONNECT_TIMEOUT_MS)) != 0) { return result; } + + // Wait until ps2link can serve the host: files the command needs. + if ((what & PS2LINK_CONNECT_REQUEST) && ps2link_handshake(HANDSHAKE_TIMEOUT_MS) < 0) { + fprintf(stderr, "Error: ps2link at %s accepted the connection but does not answer.\n", hostname); + ps2link_disconnect(); + return PS2CLIENT_EXIT_NOCONNECT; + } // Perform the requested command. - if (strcmp(argv[-1], "reset") == 0) { ps2link_command_reset(); timeout = 0; } else - if (strcmp(argv[-1], "execiop") == 0) { ps2link_command_execiop(argc, argv); } else - if (strcmp(argv[-1], "execee") == 0) { ps2link_command_execee(argc, argv); } else - if (strcmp(argv[-1], "poweroff") == 0) { ps2link_command_poweroff(); timeout = 0; } else - if (strcmp(argv[-1], "scrdump") == 0) { ps2link_command_scrdump(); timeout = 0; } else - if (strcmp(argv[-1], "netdump") == 0) { ps2link_command_netdump(); timeout = 0; } else - if (strcmp(argv[-1], "dumpmem") == 0) { ps2link_command_dumpmem(atoi(argv[0]), atoi(argv[1]), argv[2]); } else - if (strcmp(argv[-1], "startvu") == 0) { ps2link_command_startvu(atoi(argv[0])); timeout = 0; } else - if (strcmp(argv[-1], "stopvu") == 0) { ps2link_command_stopvu(atoi(argv[0])); timeout = 0; } else - if (strcmp(argv[-1], "dumpreg") == 0) { ps2link_command_dumpreg(atoi(argv[0]), argv[1]); } else - if (strcmp(argv[-1], "gsexec") == 0) { ps2link_command_gsexec(atoi(argv[0]), argv[1]); } else - if (strcmp(argv[-1], "writemem") == 0) { ps2link_command_writemem(atoi(argv[0]), atoi(argv[1]), argv[2]); } else - if (strcmp(argv[-1], "iopexcep") == 0) { ps2link_command_iopexcep(); timeout = 0; } else - if (strcmp(argv[-1], "listen") == 0) { } else - - // An unknown command was requested. - { printf("Error: Unknown command requested. (%s)\n", argv[-1]); print_usage(); return -1; } + switch (commands[command].id) { + case CMD_RESET: result = ps2link_command_reset(); break; + case CMD_EXECIOP: result = ps2link_command_execiop(argc, argv); break; + case CMD_EXECEE: result = ps2link_command_execee(argc, argv); break; + case CMD_POWEROFF: result = ps2link_command_poweroff(); break; + case CMD_SCRDUMP: result = ps2link_command_scrdump(); break; + case CMD_NETDUMP: result = ps2link_command_netdump(); break; + case CMD_DUMPMEM: result = ps2link_command_dumpmem(number0, number1, argv[2]); break; + case CMD_STARTVU: result = ps2link_command_startvu(number0); break; + case CMD_STOPVU: result = ps2link_command_stopvu(number0); break; + case CMD_DUMPREG: result = ps2link_command_dumpreg(number0, argv[1]); break; + case CMD_GSEXEC: result = ps2link_command_gsexec(number0, argv[1]); break; + case CMD_WRITEMEM: result = ps2link_command_writemem(number0, number1, argv[2]); break; + case CMD_IOPEXCEP: result = ps2link_command_iopexcep(); break; + case CMD_LISTEN: result = 0; break; + } + if (result < 0) { fprintf(stderr, "Error: Could not send the command.\n"); ps2link_disconnect(); return PS2CLIENT_EXIT_NOCONNECT; } // Enter the main loop. - ps2link_mainloop(timeout); + result = ps2link_mainloop(timeout); // Disconnect from the ps2link server. ps2link_disconnect(); // End program. - return 0; + return result; } diff --git a/src/ps2link.c b/src/ps2link.c index 06d08fd..540bf7c 100644 --- a/src/ps2link.c +++ b/src/ps2link.c @@ -13,8 +13,6 @@ #include #else #include - #define sleep(x) Sleep(x * 1000) - #define pause() while (1) { Sleep(600000); } #endif #include "network.h" @@ -28,7 +26,29 @@ pthread_t console_thread_id; pthread_t request_thread_id; - int ps2link_counter = 0; + // State shared between the threads and the main loop. + pthread_mutex_t ps2link_lock = PTHREAD_MUTEX_INITIALIZER; + long long ps2link_activity = 0; // time_ms() of the last console output or request + int ps2link_lost = 0; // ps2link closed the request connection + int ps2link_ping_answered = 0; // the handshake round trip has completed + int ps2link_marker_seen = 0; + int ps2link_marker_status = 0; + + // The console line being scanned for the marker. + char *ps2link_marker = NULL; + char ps2link_line[4096]; + int ps2link_line_length = 0; + + // The handshake has ps2link dump a few registers to this host: file, which + // the request thread sends to the null device. + #define PS2LINK_PING_PATHNAME "ps2client.ping" +#ifdef _WIN32 + #define PS2LINK_NULL_DEVICE "NUL" +#else + #define PS2LINK_NULL_DEVICE "/dev/null" +#endif + #define PS2LINK_PING_REGS 1 // INTC: two registers, read without side effects + int ps2link_ping_fd = -1; // ps2link_dd is now an array of structs struct { @@ -39,75 +59,193 @@ { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL } }; + static void ps2link_set_activity(void) { + pthread_mutex_lock(&ps2link_lock); + ps2link_activity = time_ms(); + pthread_mutex_unlock(&ps2link_lock); + } + + static int ps2link_get_flag(int *flag) { int value; + pthread_mutex_lock(&ps2link_lock); + value = *flag; + pthread_mutex_unlock(&ps2link_lock); + return value; + } + + static void ps2link_set_flag(int *flag) { + pthread_mutex_lock(&ps2link_lock); + *flag = 1; + pthread_mutex_unlock(&ps2link_lock); + } + + // Check a complete console line for the marker. Call with ps2link_lock held. + static void ps2link_check_line(void) { char *found, *status; + + ps2link_line[ps2link_line_length] = 0; + if (ps2link_marker_seen || (found = strstr(ps2link_line, ps2link_marker)) == NULL) { return; } + + // The status is the number after "status=", if there is one. + status = strstr(found, "status="); + ps2link_marker_status = status ? (int)strtol(status + 7, NULL, 10) : 0; + ps2link_marker_seen = 1; + + } + + // Scan console output for the marker, line by line. + static void ps2link_scan_console(char *buffer, int size) { int loop0; + + if (ps2link_marker == NULL) { return; } + + pthread_mutex_lock(&ps2link_lock); + for (loop0=0; loop0 0) { pthread_create(&console_thread_id, NULL, ps2link_thread_console, NULL); } + int ps2link_connect(char *hostname, int what, int timeout_ms) { - // Connect to the request port. - request_socket = network_connect(hostname, 0x4711, SOCK_STREAM); - - // Disable Nagle algorithm: without TCP_NODELAY, the two-part response - // (small header then data) interacts with PS2's delayed-ACK and causes - // ~200 ms stall per read() syscall, making fread() ~400x slower than read(). - if (request_socket > 0) { - int one = 1; - setsockopt(request_socket, IPPROTO_TCP, TCP_NODELAY, (const char *)&one, sizeof(one)); + // Listen on the console port. Only one client can, and a second one + // would silently get no output, so this is an error. + if (what & PS2LINK_CONNECT_CONSOLE) { + console_socket = network_listen(0x4712, SOCK_DGRAM); + if (console_socket < 0) { + fprintf(stderr, "Error: Could not listen on the console port (UDP 0x4712). Is another ps2client running?\n"); + return PS2CLIENT_EXIT_PORTBUSY; + } } - // Create the request thread. - if (request_socket > 0) { pthread_create(&request_thread_id, NULL, ps2link_thread_request, NULL); } + // Connect to the request port. + if (what & PS2LINK_CONNECT_REQUEST) { + request_socket = network_connect_timeout(hostname, 0x4711, SOCK_STREAM, timeout_ms); + if (request_socket < 0) { + fprintf(stderr, "Error: Could not connect to the ps2link server. (%s)\n", hostname); + return PS2CLIENT_EXIT_NOCONNECT; + } + + // Disable Nagle algorithm: without TCP_NODELAY, the two-part response + // (small header then data) interacts with PS2's delayed-ACK and causes + // ~200 ms stall per read() syscall, making fread() ~400x slower than read(). + int one = 1; + setsockopt(request_socket, IPPROTO_TCP, TCP_NODELAY, (const char *)&one, sizeof(one)); + } // Connect to the command port. command_socket = network_connect(hostname, 0x4712, SOCK_DGRAM); + if (command_socket < 0) { + fprintf(stderr, "Error: Could not open the command port. (%s)\n", hostname); + return PS2CLIENT_EXIT_NOCONNECT; + } - // Delay for a moment to let ps2link finish setup. - sleep(1); + // Create the threads. + ps2link_set_activity(); + if (console_socket >= 0) { pthread_create(&console_thread_id, NULL, ps2link_thread_console, NULL); } + if (request_socket >= 0) { pthread_create(&request_thread_id, NULL, ps2link_thread_request, NULL); } // End function. return 0; } - int ps2link_mainloop(int timeout) { + int ps2link_handshake(int timeout_ms) { + long long start = time_ms(), next = start; + + // Repeat the command if it gets no answer, but slowly: when the EE side of + // ps2link is not running (e.g. right after a program exits), the IOP side + // cannot pass commands on, and a flood of them can keep the ps2 from + // coming back. + while (!ps2link_get_flag(&ps2link_ping_answered)) { + long long now = time_ms(); + if (now - start >= timeout_ms || ps2link_get_flag(&ps2link_lost)) { return -1; } + if (now >= next) { ps2link_command_dumpreg(PS2LINK_PING_REGS, PS2LINK_PING_PATHNAME); next = now + 1000; } + sleep_ms(5); + } + + // Return the round trip time. + return (int)(time_ms() - start); + + } + + int ps2link_mainloop(int timeout) { int status = PS2CLIENT_EXIT_OK; // Disconnect from the command port. - if (network_disconnect(command_socket) < 0) { return -1; } + if (command_socket >= 0) { network_disconnect(command_socket); command_socket = -1; } // If no timeout was given, timeout immediately. - if (timeout == 0) { return 0; } - - // If timeout was never, wait forever. - if (timeout < 0) { pause(); } - - // Increment the timeout counter until timeout is reached. - while (ps2link_counter++ < timeout) { sleep(1); }; + if (timeout == 0) { return PS2CLIENT_EXIT_OK; } + + // Wait for the marker, a lost connection, or the idle timeout. + ps2link_set_activity(); + for (;;) { long long activity; int lost; + + pthread_mutex_lock(&ps2link_lock); + if (ps2link_marker_seen) { status = ps2link_marker_status & 0xff; pthread_mutex_unlock(&ps2link_lock); break; } + activity = ps2link_activity; lost = ps2link_lost; + pthread_mutex_unlock(&ps2link_lock); + + // ps2link closed the connection (the ps2 was reset, or the program + // rebooted it). Give the last console output a moment to come in. + if (lost) { + sleep_ms(500); + pthread_mutex_lock(&ps2link_lock); + if (ps2link_marker != NULL && !ps2link_marker_seen && ps2link_line_length > 0) { ps2link_check_line(); } + status = ps2link_marker_seen ? (ps2link_marker_status & 0xff) : PS2CLIENT_EXIT_CONNLOST; + pthread_mutex_unlock(&ps2link_lock); + if (status == PS2CLIENT_EXIT_CONNLOST) { fprintf(stderr, "ps2client: ps2link closed the connection.\n"); } + break; + } + + // Idle timeout. + if (timeout > 0 && time_ms() - activity >= (long long)timeout * 1000) { + pthread_mutex_lock(&ps2link_lock); + if (ps2link_marker != NULL && !ps2link_marker_seen && ps2link_line_length > 0) { ps2link_check_line(); } + if (ps2link_marker_seen) { status = ps2link_marker_status & 0xff; } + else if (ps2link_marker != NULL) { status = PS2CLIENT_EXIT_TIMEOUT; } + pthread_mutex_unlock(&ps2link_lock); + if (status == PS2CLIENT_EXIT_TIMEOUT) { fprintf(stderr, "ps2client: no output for %d seconds and no marker; giving up.\n", timeout); } + break; + } + + sleep_ms(20); + } // End function. - return 0; + return status; } int ps2link_disconnect(void) { // Kill created threads. - pthread_cancel(request_thread_id); - pthread_cancel(console_thread_id); - - // Disconnect from the command port. - if (network_disconnect(command_socket) < 0) { return -1; } + if (request_socket >= 0) { pthread_cancel(request_thread_id); } + if (console_socket >= 0) { pthread_cancel(console_thread_id); } - // Disconnect from the request port. - if (network_disconnect(request_socket) < 0) { return -1; } - - // Disconnect from console port. - if (network_disconnect(console_socket) < 0) { return -1; } + // Disconnect from the command, request and console ports. + if (command_socket >= 0) { network_disconnect(command_socket); command_socket = -1; } + if (request_socket >= 0) { network_disconnect(request_socket); request_socket = -1; } + if (console_socket >= 0) { network_disconnect(console_socket); console_socket = -1; } // End function. return 0; @@ -137,7 +275,7 @@ command.number = htonl(PS2LINK_COMMAND_EXECIOP); command.length = htons(sizeof(command)); command.argc = htonl(argc); - fix_argv(command.argv, argv); + if (fix_argv(command.argv, sizeof(command.argv), argv) < 0) { return -1; } // Send the command packet. return network_send(command_socket, &command, sizeof(command)); @@ -151,7 +289,7 @@ command.number = htonl(PS2LINK_COMMAND_EXECEE); command.length = htons(sizeof(command)); command.argc = htonl(argc); - fix_argv(command.argv, argv); + if (fix_argv(command.argv, sizeof(command.argv), argv) < 0) { return -1; } // Send the command packet. return network_send(command_socket, &command, sizeof(command)); @@ -255,7 +393,7 @@ // Build the command packet.. command.number = htonl(PS2LINK_COMMAND_GSEXEC); command.length = htons(sizeof(command)); - command.size = htonl(size); + command.size = htons(size); if (pathname) { snprintf(command.pathname, sizeof(command.pathname), "%s", pathname); } // Send the command packet. @@ -306,6 +444,12 @@ fix_pathname(request->pathname); request->flags = fix_flags(ntohl(request->flags)); + // The handshake's register dump goes nowhere. + if (strcmp(request->pathname, PS2LINK_PING_PATHNAME) == 0) { + result = ps2link_ping_fd = open(PS2LINK_NULL_DEVICE, O_WRONLY); + return ps2link_response_open(result); + } + if(((stat(request->pathname, &stats) == 0) && (!S_ISDIR(stats.st_mode))) || (request->flags & O_CREAT)) { // Perform the request. @@ -328,6 +472,12 @@ // Perform the request. result = close(ntohl(request->fd)); + // The handshake's register dump is complete. + if (ps2link_ping_fd >= 0 && (int)ntohl(request->fd) == ps2link_ping_fd) { + ps2link_ping_fd = -1; + ps2link_set_flag(&ps2link_ping_answered); + } + // Send the response. return ps2link_response_close(result); @@ -342,6 +492,7 @@ // Allocate the bigbuffer. char *bigbuffer = malloc(ntohl(request->size)); + if (bigbuffer == NULL) { ps2link_response_read(-1, -1); return -1; } // Perform the request. result = size = read(ntohl(request->fd), bigbuffer, ntohl(request->size)); @@ -383,9 +534,10 @@ // Allocate the bigbuffer. char *bigbuffer = malloc(ntohl(request->size)); + if (bigbuffer == NULL) { return -1; } // Read the request data. - network_receive_all(request_socket, bigbuffer, ntohl(request->size)); + if (network_receive_all(request_socket, bigbuffer, ntohl(request->size)) < 0) { free(bigbuffer); return -1; } // Perform the request. result = write(ntohl(request->fd), bigbuffer, ntohl(request->size)); @@ -400,7 +552,7 @@ } else { // Read the request data. - network_receive_all(request_socket, buffer, ntohl(request->size)); + if (network_receive_all(request_socket, buffer, ntohl(request->size)) < 0) { return -1; } // Perform the request. result = write(ntohl(request->fd), buffer, ntohl(request->size)); @@ -457,6 +609,9 @@ struct { unsigned int number; unsigned short length; int dd; } PACKED *request = packet; int result = -1; + // Check the directory descriptor. + if (ntohl(request->dd) >= 10 || ps2link_dd[ntohl(request->dd)].dir == NULL) { return ps2link_response_closedir(-1); } + // Perform the request. result = closedir(ps2link_dd[ntohl(request->dd)].dir); @@ -481,6 +636,9 @@ unsigned int mode; unsigned char ctime[8]; unsigned char atime[8]; unsigned char mtime[8]; char tname[512]; + // Check the directory descriptor. + if (ntohl(request->dd) >= 10 || ps2link_dd[ntohl(request->dd)].dir == NULL) { return ps2link_response_readdir(-1, 0, 0, 0, NULL, NULL, NULL, 0, NULL); } + dir = ps2link_dd[ntohl(request->dd)].dir; // Perform the request. @@ -594,12 +752,13 @@ int ps2link_request_getstat(void *packet) { struct stat stats; struct tm *loctime; int ret; unsigned int mode = 0; - unsigned char ctime[8]; unsigned char atime[8]; unsigned char mtime[8]; + unsigned char ctime[8] = { 0 }; unsigned char atime[8] = { 0 }; unsigned char mtime[8] = { 0 }; // Fix the arguments. fix_pathname(request->name); // Fetch the entry's statistics. + memset(&stats, 0, sizeof(stats)); ret = stat(request->name, &stats); if (ret == 0) { @@ -823,29 +982,31 @@ int ps2link_response_getstat(int result, unsigned int mode, unsigned int attr, u ////////////////////////////// void *ps2link_thread_console(void *userdata) { - char buffer[1024]; + // As big as a UDP packet can be: recv cuts off what does not fit. + static char buffer[65536]; (void)userdata; // If the socket isn't open, this thread isn't needed. if (console_socket < 0) { return NULL; } // Loop forever... - for (;;) { + for (;;) { int size; // Wait for network activity. network_wait(console_socket, -1); // Receive the console buffer. - network_receive(console_socket, buffer, sizeof(buffer)); + size = network_receive(console_socket, buffer, sizeof(buffer)); + if (size < 0) { sleep_ms(10); continue; } - // Print out the console buffer. - printf("%s", buffer); + // Print out exactly what came in: a packet is not null-terminated. + fwrite(buffer, 1, size, stdout); - // Clear the console buffer. - memset(buffer, 0, sizeof(buffer)); + // Look for the marker. + ps2link_scan_console(buffer, size); // Reset the timeout counter. - ps2link_counter = 0; + ps2link_set_activity(); } @@ -862,16 +1023,22 @@ int ps2link_response_getstat(int result, unsigned int mode, unsigned int attr, u if (request_socket < 0) { return NULL; } // Loop forever... - for (;;) { + for (;;) { int length; // Wait for network activity. network_wait(request_socket, -1); - // Read in the request packet header. - network_receive_all(request_socket, &packet, 6); + // Read in the request packet header. Failing means ps2link closed the + // connection (reset or reboot) or the connection broke. + if (network_receive_all(request_socket, &packet, 6) < 0) { break; } // Read in the rest of the packet. - network_receive_all(request_socket, packet.buffer, ntohs(packet.length) - 6); + length = ntohs(packet.length); + if (length < 6 || length - 6 > (int)sizeof(packet.buffer)) { + fprintf(stderr, "Error: Bad request packet from ps2link (0x%08X, length %d).\n", ntohl(packet.number), length); + break; + } + if (network_receive_all(request_socket, packet.buffer, length - 6) < 0) { break; } // Perform the requested action. if (ntohl(packet.number) == PS2LINK_REQUEST_OPEN) { ps2link_request_open(&packet); } else @@ -888,10 +1055,13 @@ int ps2link_response_getstat(int result, unsigned int mode, unsigned int attr, u if (ntohl(packet.number) == PS2LINK_REQUEST_GETSTAT) { ps2link_request_getstat(&packet); } // Reset the timeout counter. - ps2link_counter = 0; + ps2link_set_activity(); } + // Tell the main loop the connection is gone. + ps2link_set_flag(&ps2link_lost); + // End function. return NULL; diff --git a/src/ps2link.h b/src/ps2link.h index 8c860dd..5c14371 100644 --- a/src/ps2link.h +++ b/src/ps2link.h @@ -5,8 +5,30 @@ // PS2LINK FUNCTIONS // /////////////////////// - int ps2link_connect(char *hostname); - + // Exit statuses of ps2client (sysexits.h values where one fits). + #define PS2CLIENT_EXIT_OK 0 + #define PS2CLIENT_EXIT_USAGE 64 + #define PS2CLIENT_EXIT_NOCONNECT 69 // ps2link is not answering + #define PS2CLIENT_EXIT_CONNLOST 74 // ps2link closed the connection + #define PS2CLIENT_EXIT_PORTBUSY 75 // the console port is in use + #define PS2CLIENT_EXIT_TIMEOUT 124 // idle timeout before the -e marker + + // What ps2link_connect sets up. + #define PS2LINK_CONNECT_CONSOLE 1 // receive the console output + #define PS2LINK_CONNECT_REQUEST 2 // serve host: file requests + + // Exit the main loop when a console line contains marker (NULL: never). + void ps2link_set_marker(char *marker); + + // Returns 0, or PS2CLIENT_EXIT_PORTBUSY / PS2CLIENT_EXIT_NOCONNECT. + int ps2link_connect(char *hostname, int what, int timeout_ms); + + // Wait until ps2link answers a command through its host: requests, which + // shows that the EE side, the IOP side and the request connection all work. + // Returns the round trip time in ms, or -1 on timeout. + int ps2link_handshake(int timeout_ms); + + // Returns the exit status for ps2client. int ps2link_mainloop(int timeout); int ps2link_disconnect(void); @@ -31,6 +53,7 @@ int ps2link_command_reset(void); + // Returns -1 if the arguments do not fit in the command packet. int ps2link_command_execiop(int argc, char **argv); int ps2link_command_execee(int argc, char **argv); @@ -94,6 +117,8 @@ int ps2link_request_rmdir(void *packet); + int ps2link_request_getstat(void *packet); + //////////////////////////////// // PS2LINK RESPONSE FUNCTIONS // //////////////////////////////// diff --git a/src/utility.c b/src/utility.c index 76eccdd..f765c91 100644 --- a/src/utility.c +++ b/src/utility.c @@ -2,6 +2,12 @@ #include #include #include + #include +#ifdef _WIN32 + #include +#else + #include +#endif #include "utility.h" @@ -57,64 +63,97 @@ } - int fix_argv(char *destination, char **argv) { int loop0 = 0; + int fix_argv(char *destination, int size, char **argv) { int loop0 = 0, used = 0; // For each argv... - for (loop0=0; argv[loop0]; loop0++) { + for (loop0=0; argv[loop0]; loop0++) { int length = strlen(argv[loop0]) + 1; - // Copy the argv to the destination. - memcpy(destination, argv[loop0], strlen(argv[loop0])); + // The arguments, each with its null, must fit in the destination. + if (used + length > size) { return -1; } - // Increment the destination pointer. - destination += strlen(argv[loop0]); + // Copy the argv and its null to the destination. + memcpy(&destination[used], argv[loop0], length); - // Null-terminate the argv. - *destination = 0; - - // Increment the destination pointer. - destination += 1; + // Increment the destination offset. + used += length; } + // Clear the rest of the destination. + memset(&destination[used], 0, size - used); + // End function. return 0; } + long long time_ms(void) { +#ifdef _WIN32 + return (long long)GetTickCount64(); +#else + struct timespec ts; + + // A monotonic clock, so time changes do not affect timeouts. + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +#endif + } + + void sleep_ms(int ms) { +#ifdef _WIN32 + Sleep(ms); +#else + struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L }; + + nanosleep(&ts, NULL); +#endif + } + int print_usage(void) { // Print out the ps2client usage string. - printf("\n"); - printf(" Usage: ps2client [-h hostname] [-t timeout] [arguments]\n\n"); - printf(" Available commands:\n\n"); - printf(" reset\n"); - printf(" execiop [arguments]\n"); - printf(" execee [arguments]\n"); - printf(" poweroff\n"); - printf(" scrdump\n"); - printf(" netdump\n"); - printf(" dumpmem \n"); - printf(" startvu \n"); - printf(" stopvu \n"); - printf(" dumpreg \n"); - printf(" gsexec \n"); - printf(" writemem \n"); - printf(" iopexcep\n"); - printf(" listen\n\n"); + fprintf(stderr, "\n"); + fprintf(stderr, " Usage: ps2client [-h hostname] [-t timeout] [-e marker] [arguments]\n\n"); + fprintf(stderr, " Options:\n\n"); + fprintf(stderr, " -h hostname address of the ps2 (default: $PS2HOSTNAME, else 192.168.0.10)\n"); + fprintf(stderr, " -t timeout exit after seconds without console output or requests\n"); + fprintf(stderr, " (ping: give up after seconds, default 2)\n"); + fprintf(stderr, " -e marker exit when a console line contains ; the exit status is the\n"); + 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, " execiop [arguments]\n"); + fprintf(stderr, " execee [arguments]\n"); + fprintf(stderr, " poweroff\n"); + fprintf(stderr, " scrdump\n"); + fprintf(stderr, " netdump\n"); + fprintf(stderr, " dumpmem \n"); + fprintf(stderr, " startvu \n"); + fprintf(stderr, " stopvu \n"); + fprintf(stderr, " dumpreg (type: number, or dma intc timer gs sif fifo gif\n"); + fprintf(stderr, " vif0 vif1 ipu all vu0 vu1)\n"); + fprintf(stderr, " gsexec \n"); + fprintf(stderr, " writemem \n"); + fprintf(stderr, " iopexcep\n"); + fprintf(stderr, " listen\n"); + fprintf(stderr, " ping\n\n"); + fprintf(stderr, " Exit status: 0 ok, 64 usage error, 69 ps2link not answering, 74 ps2link closed\n"); + fprintf(stderr, " the connection, 75 console port in use (another ps2client running?), 124 timeout\n"); + fprintf(stderr, " before the -e marker. With -e: the status from the marker line.\n\n"); // Print out the fsclient usage string. - printf(" Usage: fsclient [-h hostname] [arguments]\n\n"); - printf(" Available commands:\n\n"); - printf(" copyfrom \n"); - printf(" copyto \n"); - printf(" delete \n"); - printf(" devlist\n"); - printf(" dir \n"); - printf(" mkdir \n"); - printf(" mount \n"); - printf(" rmdir \n"); - printf(" sync \n"); - printf(" umount \n\n"); + fprintf(stderr, " Usage: fsclient [-h hostname] [arguments]\n\n"); + fprintf(stderr, " Available commands:\n\n"); + fprintf(stderr, " copyfrom \n"); + fprintf(stderr, " copyto \n"); + fprintf(stderr, " delete \n"); + fprintf(stderr, " devlist\n"); + fprintf(stderr, " dir \n"); + fprintf(stderr, " mkdir \n"); + fprintf(stderr, " mount \n"); + fprintf(stderr, " rmdir \n"); + fprintf(stderr, " sync \n"); + fprintf(stderr, " umount \n\n"); // End function. return 0; diff --git a/src/utility.h b/src/utility.h index e731143..0e095b0 100644 --- a/src/utility.h +++ b/src/utility.h @@ -11,7 +11,13 @@ int fix_pathname(char *pathname); - int fix_argv(char *destination, char **argv); + // Pack argv as null-terminated strings into destination; -1 if they do not fit. + int fix_argv(char *destination, int size, char **argv); + + // Monotonic time in milliseconds. + long long time_ms(void); + + void sleep_ms(int ms); int print_usage(void); From a51890ba6747c4e787ea5939cedec9480fc15162 Mon Sep 17 00:00:00 2001 From: Rick Gaiser Date: Sun, 25 Oct 2026 16:31:15 +0100 Subject: [PATCH 2/2] ps2client: tests against a fake ps2link tests/run-tests.sh runs ps2client against tests/fake_ps2link.py in unprivileged Linux network namespaces, so no ps2 is needed. It covers the error paths, exit statuses, ping, the end marker, console packets, host: file reads and the handshake retry. --- tests/fake_ps2link.py | 187 ++++++++++++++++++++++++++++++++++++++++++ tests/run-tests.sh | 163 ++++++++++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100755 tests/fake_ps2link.py create mode 100755 tests/run-tests.sh diff --git a/tests/fake_ps2link.py b/tests/fake_ps2link.py new file mode 100755 index 0000000..803f804 --- /dev/null +++ b/tests/fake_ps2link.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""A fake ps2link for testing ps2client without a PS2.""" + +# It speaks the ps2link protocol: it accepts the request connection on TCP +# 0x4711, takes commands on UDP 0x4712 and sends console output to UDP 0x4712 +# of the client. Run it on another address than the client (see run-tests.sh), +# since both use UDP port 0x4712. +# +# execee loads its file through host: requests like ps2link does, then runs it +# as a script, one command per line: +# +# print console output, a newline is added +# raw one console packet of n bytes of , no newline +# sleep wait +# close close the request connection (ps2link went away) +# readback read a host: file and print its size and checksum +# +# dumpreg writes 8 bytes to the given host: file, like ps2link's INTC dump. + +import argparse +import select +import socket +import struct +import threading +import time +import zlib + +PORT_REQUEST = 0x4711 +PORT_COMMAND = 0x4712 +PORT_CONSOLE = 0x4712 + +CMD_EXECEE = 0xBABE0203 +CMD_DUMPREG = 0xBABE020A +REQ_OPEN, REQ_CLOSE, REQ_READ, REQ_WRITE = 0xBABE0111, 0xBABE0121, 0xBABE0131, 0xBABE0141 + +PS2_O_RDONLY, PS2_O_WRONLY, PS2_O_CREAT = 0x0001, 0x0002, 0x0200 + + +class Fake: + """The state of the fake ps2link and the host: requests it makes.""" + + def __init__(self, ignore_dumpreg): + """Start without a request connection.""" + self.conn = None + self.client_ip = None + self.lock = threading.Lock() + self.ignore_dumpreg = ignore_dumpreg + self.console = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def recv_all(self, n): + data = b"" + while len(data) < n: + chunk = self.conn.recv(n - len(data)) + if not chunk: + raise ConnectionError("client closed") + data += chunk + return data + + def request(self, number, payload, data=b""): + # The length covers the header, not the data of a write. + self.conn.sendall(struct.pack(">IH", number, 6 + len(payload)) + payload + data) + + def response(self, size): + return self.recv_all(size) + + def open(self, path, flags): + with self.lock: + self.request(REQ_OPEN, struct.pack(">i256s", flags, path.encode())) + _, _, fd = struct.unpack(">IHi", self.response(10)) + return fd + + def close(self, fd): + with self.lock: + self.request(REQ_CLOSE, struct.pack(">i", fd)) + self.response(10) + + def read(self, fd, size): + with self.lock: + self.request(REQ_READ, struct.pack(">ii", fd, size)) + _, _, _, n = struct.unpack(">IHii", self.response(14)) + return self.recv_all(n) if n > 0 else b"" + + def write(self, fd, data): + with self.lock: + self.request(REQ_WRITE, struct.pack(">ii", fd, len(data)), data) + self.response(10) + + def load(self, path): + fd = self.open(path, PS2_O_RDONLY) + if fd < 0: + return None + data = b"" + while True: + # Big reads, like the IOP does for ELF loading. + chunk = self.read(fd, 128 * 1024) + if not chunk: + break + data += chunk + self.close(fd) + return data + + def say(self, data): + self.console.sendto(data, (self.client_ip, PORT_CONSOLE)) + + def run_script(self, text): + for line in text.splitlines(): + cmd, _, arg = line.partition(" ") + if cmd == "print": + self.say(arg.encode() + b"\n") + elif cmd == "raw": + n, ch = arg.split() + self.say(ch.encode() * int(n)) + elif cmd == "sleep": + time.sleep(float(arg)) + elif cmd == "close": + self.conn.shutdown(socket.SHUT_RDWR) + self.conn.close() + return + elif cmd == "readback": + data = self.load(arg) + self.say(b"readback %d %08x\n" % (len(data), zlib.crc32(data))) + + def command(self, packet): + number, _ = struct.unpack(">IH", packet[:6]) + if number == CMD_DUMPREG: + if self.ignore_dumpreg > 0: + self.ignore_dumpreg -= 1 + return + _, path = struct.unpack(">i256s", packet[6:6 + 260]) + fd = self.open(path.split(b"\0")[0].decode(), PS2_O_WRONLY | PS2_O_CREAT) + if fd >= 0: + self.write(fd, b"\0" * 8) + self.close(fd) + elif number == CMD_EXECEE: + argc, argv = struct.unpack(">i256s", packet[6:6 + 260]) + args = argv.split(b"\0")[:argc] + path = args[0].decode().removeprefix("host:") + script = self.load(path) + if script is None: + self.say(b"fake: could not load %s\n" % path.encode()) + return + self.say(b"fake: args " + b" ".join(args[1:]) + b"\n") + threading.Thread(target=self.run_script, args=(script.decode(),), daemon=True).start() + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--bind", required=True, help="the address to serve on") + ap.add_argument("--ignore-dumpreg", type=int, default=0, help="ignore the first N dumpreg commands (not ready yet)") + opts = ap.parse_args() + fake = Fake(opts.ignore_dumpreg) + + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((opts.bind, PORT_REQUEST)) + server.listen(5) + commands = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + commands.bind((opts.bind, PORT_COMMAND)) + + def accept(): + conn, addr = server.accept() + # Like ps2link: a new client replaces the old one. + if fake.conn: + fake.conn.close() + conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + fake.conn, fake.client_ip = conn, addr[0] + + while True: + # A client connects before it sends a command, so take pending + # connections first: the command is for the newest one. + ready, _, _ = select.select([server, commands], [], []) + if server in ready: + accept() + continue + packet, _ = commands.recvfrom(2048) + # Without a request connection the host: open fails and the command + # does nothing, as on ps2link. + if fake.conn is None: + continue + try: + fake.command(packet) + except (OSError, ConnectionError): + pass + + +if __name__ == "__main__": + main() diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..9d03f48 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Test ps2client against tests/fake_ps2link.py (Linux only). +# +# ps2client and ps2link both use UDP port 0x4712, so the fake runs in its own +# network namespace (10.0.0.2), linked by a veth pair to the client's +# (10.0.0.1). Both are unprivileged user namespaces; no root needed. +# +# tests/run-tests.sh [path/to/ps2client] + +set -u +HERE=$(cd "$(dirname "$0")" && pwd) +CLIENT=$(realpath "${1:-$HERE/../bin/ps2client}") + +if [ -z "${PS2CLIENT_TEST_NS:-}" ]; then + exec unshare -rn env PS2CLIENT_TEST_NS=1 "$0" "$CLIENT" +fi + +ip link set lo up + +WORK=$(mktemp -d) +trap 'stop_fake; rm -rf "$WORK"' EXIT +cd "$WORK" +FAKE_PID= + +start_fake() { + # The link goes away with the namespace of the previous fake. + ip link del veth0 2>/dev/null + ip link add veth0 type veth peer name veth1 + ip addr add 10.0.0.1/24 dev veth0 + ip link set veth0 up + # shellcheck disable=SC2016 # $0 and $@ expand in the inner shell + unshare -n sh -c ' + while ! ip link show veth1 >/dev/null 2>&1; do sleep 0.02; done + ip link set lo up; ip addr add 10.0.0.2/24 dev veth1; ip link set veth1 up + exec python3 -I "$0" --bind 10.0.0.2 "$@"' "$HERE/fake_ps2link.py" "$@" & + FAKE_PID=$! + # Move the link only once the child has its own namespace. + while [ "$(readlink /proc/$FAKE_PID/ns/net)" = "$(readlink /proc/self/ns/net)" ]; do sleep 0.01; done + ip link set veth1 netns "$FAKE_PID" + # Wait until it accepts connections. A plain connect, so that no command + # reaches the fake before the test does. + for _ in $(seq 100); do + (exec 3<>/dev/tcp/10.0.0.2/18193) 2>/dev/null && return 0 + sleep 0.05 + done + echo "fake ps2link did not start" >&2 + exit 1 +} + +stop_fake() { + [ -n "$FAKE_PID" ] && kill "$FAKE_PID" 2>/dev/null && wait "$FAKE_PID" 2>/dev/null + FAKE_PID= +} + +PASS=0 FAIL=0 +# Monotonic milliseconds; the wall clock can be adjusted during a run. +now_ms() { awk '{ printf "%d", $1 * 1000 }' /proc/uptime; } +# check : runs the client, keeps +# stdout/stderr in $OUT/$ERR and its run time (ms) in $MS. +check() { + local name=$1 want=$2 start; shift 2 + start=$(now_ms) + "$CLIENT" "$@" > out.txt 2> err.txt + local rc=$? + MS=$(( $(now_ms) - start )) + OUT=$(cat out.txt) ERR=$(cat err.txt) + if [ "$rc" = "$want" ]; then + PASS=$((PASS + 1)); echo "PASS $name (status $rc, ${MS} ms)" + else + FAIL=$((FAIL + 1)); echo "FAIL $name: status $rc, want $want"; echo " stdout: $OUT"; echo " stderr: $ERR" + fi +} +expect() { # expect + local name=$1; shift + if "$@"; then PASS=$((PASS + 1)); echo "PASS $name"; else FAIL=$((FAIL + 1)); echo "FAIL $name"; echo " stdout: $OUT"; echo " stderr: $ERR"; fi +} + +H=(-h 10.0.0.2) + +# --- without ps2link +check "ping, connection refused" 69 -h 127.0.0.1 -t 1 ping +expect "ping fails fast when refused" test "$MS" -lt 500 +check "execee, nothing listening" 69 "${H[@]}" -t 5 execee host:x.elf +expect "error on stderr, stdout empty" test -z "$OUT" -a -n "$ERR" + +# --- usage errors +check "unknown command" 64 "${H[@]}" frobnicate +check "missing arguments" 64 "${H[@]}" dumpmem 0x100000 +check "bad register type" 64 "${H[@]}" dumpreg eeregs x.bin +check "-e without marker" 64 "${H[@]}" -e +check "arguments too long" 64 "${H[@]}" execee host:x.elf "$(printf 'a%.0s' $(seq 300))" + +start_fake + +check "ping" 0 "${H[@]}" -t 2 ping +expect "ping reports round trip" grep -q "answered in" out.txt +check "ping, no answer" 69 -h 10.0.0.99 -t 1 ping +expect "ping gives up after -t" test "$MS" -ge 990 -a "$MS" -lt 2500 + +# --- the marker ends the run with its status +printf 'print hello\nprint @@END@@ status=3\nsleep 5\nprint late\n' > status.elf +check "marker status" 3 "${H[@]}" -t 10 -e @@END@@ execee host:status.elf one two +expect "output before marker" grep -q "^hello$" out.txt +expect "arguments passed" grep -q "^fake: args one two$" out.txt +expect "exits at the marker" test "$MS" -lt 3000 +expect "nothing after the marker" bash -c '! grep -q late out.txt' + +printf 'print @@END@@\n' > nostatus.elf +check "marker without status" 0 "${H[@]}" -t 10 -e @@END@@ execee host:nostatus.elf + +# --- console packets are not null-terminated +printf 'raw 1024 A\nraw 10 B\nraw 1500 C\nprint\nprint @@END@@ status=0\n' > raw.elf +check "console packets" 0 "${H[@]}" -t 10 -e @@END@@ execee host:raw.elf +expect "exact console bytes, packets over 1 KB too" test "$(grep -c '^A\{1024\}B\{10\}C\{1500\}$' out.txt)" = 1 + +# --- ps2link going away +printf 'print bye\nclose\n' > close.elf +check "connection closed" 74 "${H[@]}" -t 10 -e @@END@@ execee host:close.elf +expect "closed: fast" test "$MS" -lt 3000 +expect "closed: console kept" grep -q "^bye$" out.txt +check "connection closed, no marker option" 74 "${H[@]}" -t 10 execee host:close.elf + +# --- output is written as it comes in, so a killed client loses nothing +printf 'print early\nsleep 10\n' > early.elf +"$CLIENT" "${H[@]}" -t 20 execee host:early.elf > early.txt 2>&1 & +CPID=$! +sleep 1.5 +kill -TERM $CPID; wait $CPID 2>/dev/null +OUT=$(cat early.txt) ERR= +expect "unbuffered output survives kill" grep -q "^early$" early.txt + +# --- idle timeout +printf 'print quiet\nsleep 10\n' > quiet.elf +check "idle timeout before marker" 124 "${H[@]}" -t 1 -e @@END@@ execee host:quiet.elf +check "idle timeout without -e" 0 "${H[@]}" -t 1 execee host:quiet.elf +expect "idle timeout waits" test "$MS" -ge 990 + +# --- host: file serving with big reads +head -c 300000 /dev/urandom > blob.bin +printf 'readback blob.bin\nprint @@END@@ status=0\n' > readback.elf +check "big host: read" 0 "${H[@]}" -t 10 -e @@END@@ execee host:readback.elf +expect "readback size and crc" grep -q "^readback 300000 $(python3 -c 'import sys,zlib;print("%08x"%zlib.crc32(open(sys.argv[1],"rb").read()))' blob.bin)$" out.txt + +# --- second client +"$CLIENT" "${H[@]}" -t 5 listen > /dev/null 2>&1 & +LISTENER=$! +sleep 0.5 +check "console port in use" 75 "${H[@]}" -t 5 -e @@END@@ execee host:nostatus.elf +expect "port in use: clear message" grep -q "another ps2client" err.txt +kill $LISTENER; wait $LISTENER 2>/dev/null + +# --- reset needs neither console nor request connection +check "reset" 0 "${H[@]}" reset + +# --- ps2link not ready right after accepting +stop_fake +start_fake --ignore-dumpreg 1 +check "handshake retries" 0 "${H[@]}" -t 10 -e @@END@@ execee host:nostatus.elf +expect "retried after 1 s" test "$MS" -ge 990 +stop_fake + +echo "passed $PASS, failed $FAIL" +[ "$FAIL" = 0 ]