From 332ccf4859af510d788679ca12aecbfbe650ef24 Mon Sep 17 00:00:00 2001 From: mortyzip <338174153+mortyzip@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:53:30 +0100 Subject: [PATCH] Add control page with on/off, QR code and HTTP API (v1.1.0) --- .github/workflows/build.yml | 4 +- .gitignore | 1 + Makefile | 52 ++-- README.md | 43 ++- src/index.html | 518 ++++++++++++++++++++++++++++++++++++ src/ps4.c | 136 ++++++++-- src/ps5.c | 154 +++++++++-- src/web.h | 244 +++++++++++++++++ tools/host_stubs.c | 19 ++ 9 files changed, 1097 insertions(+), 74 deletions(-) create mode 100644 src/index.html create mode 100644 src/web.h create mode 100644 tools/host_stubs.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12e9ae7..af3fa29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install LLVM + - name: Install tools run: | sudo apt-get update - sudo apt-get install -y clang-18 lld-18 llvm-18 + sudo apt-get install -y clang-18 lld-18 llvm-18 xxd echo "LLVM_CONFIG=/usr/lib/llvm-18/bin/llvm-config" >> "$GITHUB_ENV" - name: Download SDKs diff --git a/.gitignore b/.gitignore index 5e4db01..9419646 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.o .DS_Store +build/ diff --git a/Makefile b/Makefile index 22961f9..afd37b5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.0.0.elf -VERSION := 1.0.0 +# Payload Manager reads the version from the filename, e.g. keepawake_ps5_v1.1.0.elf +VERSION := 1.1.0 PS5_PAYLOAD_SDK ?= $(abspath ../ps5-payload-sdk) PS4SDK ?= $(abspath ../ps4-payload-sdk) @@ -19,7 +19,10 @@ LLVM_PFX := $(if $(LLVM_BIN),$(LLVM_BIN)/) PS5_ELF := keepawake_ps5_v$(VERSION).elf PS4_BIN := keepawake_ps4_v$(VERSION).bin -DEFINES := -DKEEPAWAKE_VERSION=\"v$(VERSION)\" +# The control page is embedded into the payloads as a C array. +PAGE_H := build/index_html.h +DEPS := src/web.h $(PAGE_H) +DEFINES := -DKEEPAWAKE_VERSION=\"v$(VERSION)\" -Ibuild # PS5: ELF for the ps5-payload-sdk ELF loader. PS5_CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang @@ -42,32 +45,43 @@ all: ps5 ps4 ps5: $(PS5_ELF) ps4: $(PS4_BIN) -$(PS5_ELF): src/ps5.c - $(PS5_CC) $(PS5_CFLAGS) -o $@ $^ +$(PAGE_H): src/index.html + @mkdir -p build + cd src && xxd -i index.html > ../$@ -$(PS4_BIN): src/ps4.c - $(PS4_CC) $(PS4_CFLAGS) -c -o ps4.o src/ps4.c - $(PS4_CC) --target=x86_64-unknown-freebsd -c -o ps4_crt0.o $(LIBPS4)/crt0.s - $(PS4_LD) -o ps4.elf ps4_crt0.o ps4.o $(PS4_LDFLAGS) - $(PS4_OBJCOPY) -O binary ps4.elf $@ - rm -f ps4.o ps4_crt0.o ps4.elf +$(PS5_ELF): src/ps5.c $(DEPS) + $(PS5_CC) $(PS5_CFLAGS) -o $@ src/ps5.c + +$(PS4_BIN): src/ps4.c $(DEPS) + @mkdir -p build + $(PS4_CC) $(PS4_CFLAGS) -c -o build/ps4.o src/ps4.c + $(PS4_CC) --target=x86_64-unknown-freebsd -c -o build/ps4_crt0.o $(LIBPS4)/crt0.s + $(PS4_LD) -o build/ps4.elf build/ps4_crt0.o build/ps4.o $(PS4_LDFLAGS) + $(PS4_OBJCOPY) -O binary build/ps4.elf $@ + +# Runs the PS5 code on this computer with the console calls stubbed out, for +# working on the control page: make host && ./build/keepawake_host +host: build/keepawake_host + +build/keepawake_host: src/ps5.c tools/host_stubs.c $(DEPS) + cc -Wall -Werror $(DEFINES) -o $@ src/ps5.c tools/host_stubs.c version: @echo $(VERSION) clean: - rm -f keepawake*.elf keepawake*.bin ps4.o ps4_crt0.o ps4.elf + rm -rf build keepawake*.elf keepawake*.bin test-ps5: $(PS5_ELF) - $(PS5_PAYLOAD_SDK)/bin/prospero-deploy -h $(PS5_HOST) -p $(PS5_PORT) $^ + $(PS5_PAYLOAD_SDK)/bin/prospero-deploy -h $(PS5_HOST) -p $(PS5_PORT) $< test-ps4: $(PS4_BIN) - nc -w 1 $(PS4_HOST) $(PS4_PORT) < $^ + nc -w 1 $(PS4_HOST) $(PS4_PORT) < $< -stop-ps5: - nc -z $(PS5_HOST) 9031 +quit-ps5: + curl -fsS -X POST http://$(PS5_HOST):9031/quit -stop-ps4: - nc -z $(PS4_HOST) 9031 +quit-ps4: + curl -fsS -X POST http://$(PS4_HOST):9031/quit -.PHONY: all ps5 ps4 version clean test-ps5 test-ps4 stop-ps5 stop-ps4 +.PHONY: all ps5 ps4 host version clean test-ps5 test-ps4 quit-ps5 quit-ps4 diff --git a/README.md b/README.md index 98017c1..5ea8a02 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,32 @@ Download the payloads from the [Releases](https://github.com/mortyzip/KeepAwake/ | PS5 | `keepawake_ps5_v.elf` | ELF loader, port 9021 | | PS4 | `keepawake_ps4_v.bin` | GoldHEN BinLoader, port 9090 | -- **Enable:** send the payload. You'll see "Keep Awake v enabled". -- **Disable:** send the payload again. You'll see "Keep Awake v disabled". You can also connect to port 9031 on the console, for example with `make stop-ps5` or `nc -z 9031`. +- **Start:** send the payload. The toast shows the control page address, for example "Keep Awake v enabled / Control it at http://192.168.1.50:9031". +- **Turn off and on:** use the control page. Turning it off lets the console go into rest mode, but the payload keeps running, so you can turn it back on from the page without sending the payload again. +- **Close:** use "Close Keep Awake" on the page (tap twice to confirm), or send the payload again. This ends the payload, so it needs sending again to start. Only one copy runs at a time. It uses TCP port 9031 to know whether it's already running. +### Control page + +While it's running, open `http://:9031` in a browser. The page shows: + +- whether Keep Awake is on or off, and for how long, +- which console it's on, +- a button to turn it on or off, and a link to close it, +- a QR code for the page. Open the page on a computer (or the console's browser) and scan the code with your phone's camera to control Keep Awake from your phone. + +The page also has a small HTTP API: + +| Request | Does | +|---|---| +| `GET /status` | Returns JSON: `version`, `console`, `active`, `uptime` and `since` (seconds since start and since it was last turned on or off), `interval`, `ip`, `port` | +| `POST /on` | Turns Keep Awake on, and returns the status | +| `POST /off` | Turns Keep Awake off without closing it, and returns the status | +| `POST /quit` | Closes the payload | + +For example: `curl -X POST http://:9031/off`, or `make quit-ps5 PS5_HOST=`. + ## Building The Makefile expects the SDKs next to this folder (`../ps5-payload-sdk` and `../ps4-payload-sdk`). Set `PS5_PAYLOAD_SDK` or `PS4SDK` to use them from somewhere else. @@ -32,18 +53,30 @@ make test-ps4 PS4_HOST= Both builds need LLVM (`clang`, `ld.lld`, `llvm-objcopy`). The Makefile uses Homebrew's LLVM automatically when it's installed. Otherwise, set `LLVM_CONFIG` to your `llvm-config`. -The version is set by `VERSION` in the [Makefile](Makefile). It goes into the filenames, where Payload Manager reads it, and into the toasts. +The version is set by `VERSION` in the [Makefile](Makefile). It goes into the filenames, where Payload Manager reads it, the toasts, and the control page. + +### Working on the control page + +The page is [src/index.html](src/index.html). The build embeds it into both payloads with `xxd`. To try it without a console, run the PS5 code on your computer with the console calls stubbed out: + +```sh +make host && ./build/keepawake_host +``` + +Then open http://localhost:9031. ## Releasing GitHub Actions builds both payloads on every push to `main`. To publish a release: 1. Bump `VERSION` in the Makefile and commit it to `main`. -2. Tag the commit and push the tag: `git tag v1.0.1 && git push origin v1.0.1`. +2. Tag the commit and push the tag: `git tag v1.1.1 && git push origin v1.1.1`. The workflow builds the payloads and creates the GitHub Release with them attached. It fails if the tag doesn't match `VERSION`. ## Source - [src/ps5.c](src/ps5.c): built with the [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk). -- [src/ps4.c](src/ps4.c): built against libPS4 from the [Scene-Collective ps4-payload-sdk](https://github.com/Scene-Collective/ps4-payload-sdk). libPS4 has no `select()`, so this version checks for a stop request once a second. +- [src/ps4.c](src/ps4.c): built against libPS4 from the [Scene-Collective ps4-payload-sdk](https://github.com/Scene-Collective/ps4-payload-sdk). libPS4 has no `select()`, so this version checks for connections ten times a second. +- [src/web.h](src/web.h): the HTTP server for the control page, shared by both. +- [src/index.html](src/index.html): the control page, including a small QR code encoder (byte mode, error correction level M, up to 106 bytes). diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..26c772b --- /dev/null +++ b/src/index.html @@ -0,0 +1,518 @@ + + + + + + +Keep Awake + + + +
+
+

Keep Awake

+   +
+ +
+
+ +
+

Connecting…

+

Checking the console

+
+
+ +
+
On for
–
+
Console
–
+
Idle timer reset
–
+
+ + + +
+ + + + +
+ + + + + diff --git a/src/ps4.c b/src/ps4.c index a269342..8db2e1e 100644 --- a/src/ps4.c +++ b/src/ps4.c @@ -1,11 +1,11 @@ /* KeepAwake (PS4) - keep the PS4 out of rest mode while this payload is running. Same behaviour as the PS5 build: the idle timer is reset by periodically - calling sceSystemServicePowerTick(), and a local TCP port makes the - payload a toggle (sending it again stops the running instance). + calling sceSystemServicePowerTick(), and a control page is served on TCP + port 9031 (see web.h). libPS4 has no select(), so the control socket is non-blocking and polled - once a second. */ + ten times a second. */ #include "ps4.h" @@ -16,10 +16,17 @@ #define KEEPAWAKE_PORT 9031 #define KEEPAWAKE_TICK_SECONDS 10 +#define KEEPAWAKE_POLL_MS 100 + +#define SCE_NET_SO_RCVTIMEO 0x1006 +#define SCE_NET_EAGAIN 35 +#define SCE_NET_ERROR_EAGAIN 0x80410123 static int (*sceSystemServicePowerTick)(void); +static uint64_t g_started; + static int resolve_power_tick(void) { @@ -34,6 +41,60 @@ resolve_power_tick(void) { } +static int +would_block(int rc) { + return (unsigned int)rc == SCE_NET_ERROR_EAGAIN || sce_net_errno == SCE_NET_EAGAIN; +} + + +#define WEB_CONSOLE "PS4" +#include "web.h" + +static int +web_recv(int fd, void *buf, size_t len) { + int rc = sceNetRecv(fd, buf, len, 0); + if(rc < 0) { + return would_block(rc) ? WEB_AGAIN : WEB_ERROR; + } + return rc; +} + +static int +web_send(int fd, const void *buf, size_t len) { + int rc = sceNetSend(fd, buf, len, 0); + if(rc < 0) { + return would_block(rc) ? WEB_AGAIN : WEB_ERROR; + } + return rc; +} + +static void +web_sleep_ms(int ms) { + sceKernelUsleep(ms * 1000); +} + +static long long +web_uptime(void) { + return (sceKernelGetProcessTime() - g_started) / 1000000; +} + +static void +web_get_ip(char *buf, size_t size) { + SceNetCtlInfo info; + + buf[0] = 0; + memset(&info, 0, sizeof info); + if(sceNetCtlGetInfo(SCE_NET_CTL_INFO_IP_ADDRESS, &info) == 0) { + snprintf(buf, size, "%s", info.ip_address); + } +} + +static void +web_notify(const char *msg) { + printf_notification("%s", msg); +} + + static void make_addr(struct sockaddr_in *addr, unsigned int ip) { memset(addr, 0, sizeof *addr); @@ -44,22 +105,36 @@ make_addr(struct sockaddr_in *addr, unsigned int ip) { } -/* Ask an already running instance to stop by connecting to its port. */ +/* Ask an already running instance to quit. Returns 0 if it confirmed. */ static int -stop_running_instance(void) { +quit_running_instance(void) { struct sockaddr_in addr; + int timeout_us = 3 * 1000 * 1000; + char resp[64]; int fd; - int rc; + int n; - if((fd = sceNetSocket("keepawake_stop", AF_INET, SOCK_STREAM, 0)) < 0) { + if((fd = sceNetSocket("keepawake_quit", AF_INET, SOCK_STREAM, 0)) < 0) { return -1; } + sceNetSetsockopt(fd, SOL_SOCKET, SCE_NET_SO_RCVTIMEO, &timeout_us, sizeof timeout_us); + make_addr(&addr, IP(127, 0, 0, 1)); - rc = sceNetConnect(fd, (struct sockaddr*)&addr, sizeof addr); + if(sceNetConnect(fd, (struct sockaddr*)&addr, sizeof addr) < 0 || + sceNetSend(fd, web_quit_request, sizeof web_quit_request - 1, 0) < 0) { + sceNetSocketClose(fd); + return -1; + } + + n = sceNetRecv(fd, resp, sizeof resp - 1, 0); sceNetSocketClose(fd); - return rc < 0 ? -1 : 0; + if(n <= 0) { + return -1; + } + resp[n] = 0; + return strstr(resp, " 200 ") ? 0 : -1; } @@ -78,7 +153,7 @@ open_control_socket(void) { make_addr(&addr, IN_ADDR_ANY); if(sceNetBind(fd, (struct sockaddr*)&addr, sizeof addr) < 0 || - sceNetListen(fd, 1) < 0) { + sceNetListen(fd, 8) < 0) { sceNetSocketClose(fd); return -1; } @@ -89,8 +164,11 @@ open_control_socket(void) { int _main(struct thread *td) { + char ip[32]; + int yes = 1; int srv; int cli; + int quit; UNUSED(td); @@ -98,6 +176,7 @@ _main(struct thread *td) { initLibc(); initNetwork(); jailbreak(); + sceNetCtlInit(); if(resolve_power_tick() != 0) { printf_notification("Keep Awake failed to start:\n" @@ -106,31 +185,42 @@ _main(struct thread *td) { } if((srv = open_control_socket()) < 0) { - // Port taken: an instance is already running, so toggle it off. - // The running instance shows the "disabled" toast itself. - if(stop_running_instance() == 0) { + // Port taken: an instance is already running, so close it. + // The running instance shows the "closed" toast itself. + if(quit_running_instance() == 0) { return 0; } printf_notification("Keep Awake failed to start:\n" - "port %d unavailable", KEEPAWAKE_PORT); + "port %d is in use", KEEPAWAKE_PORT); return -1; } - printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n" - "Send the payload again to disable"); + g_started = sceKernelGetProcessTime(); + + web_get_ip(ip, sizeof ip); + if(ip[0]) { + printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n" + "Control it at http://%s:%d", ip, KEEPAWAKE_PORT); + } else { + printf_notification("Keep Awake " KEEPAWAKE_VERSION " enabled\n" + "Control page on port %d", KEEPAWAKE_PORT); + } while(1) { - sceSystemServicePowerTick(); + ka_tick(); - for(int i = 0; i < KEEPAWAKE_TICK_SECONDS; i++) { - // Any incoming connection is a request to stop. - if((cli = sceNetAccept(srv, NULL, NULL)) >= 0) { - sceNetSocketClose(cli); - printf_notification("Keep Awake " KEEPAWAKE_VERSION " disabled"); + if((cli = sceNetAccept(srv, NULL, NULL)) >= 0) { + sceNetSetsockopt(cli, SOL_SOCKET, SO_NBIO, &yes, sizeof yes); + quit = web_handle_client(cli); + sceNetSocketClose(cli); + if(quit) { + printf_notification("Keep Awake " KEEPAWAKE_VERSION " closed"); sceNetSocketClose(srv); return 0; } - sceKernelSleep(1); + continue; } + + sceKernelUsleep(KEEPAWAKE_POLL_MS * 1000); } } diff --git a/src/ps5.c b/src/ps5.c index 19acafc..a1edbd6 100644 --- a/src/ps5.c +++ b/src/ps5.c @@ -1,14 +1,17 @@ /* KeepAwake - keep the PS5 out of rest mode while this payload is running. The console's idle timer is reset by periodically calling - sceSystemServicePowerTick(). The payload also listens on a local TCP - port, which serves two purposes: - - it guarantees only one instance runs at a time, and - - sending the payload a second time (or connecting to the port) - stops the running instance, i.e. the payload works as a toggle. */ + sceSystemServicePowerTick(). The payload also serves a control page on + TCP port 9031 (see web.h), which + - shows the status and turns Keep Awake on and off from a browser, + - guarantees only one instance runs at a time, and + - lets a second copy of the payload close the running one. */ #include #include +#include +#include +#include #include #include #include @@ -16,6 +19,7 @@ #include #include #include +#include #include @@ -37,6 +41,18 @@ int sceKernelSendNotificationRequest(int, notify_request_t*, size_t, int); int sceSystemServicePowerTick(void); +static time_t g_started; + + +static time_t +now_seconds(void) { + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec; +} + + static void notify(const char *fmt, ...) { notify_request_t req; @@ -48,30 +64,105 @@ notify(const char *fmt, ...) { va_end(args); printf("[KeepAwake] %s\n", req.message); + fflush(stdout); sceKernelSendNotificationRequest(0, &req, sizeof req, 0); } -/* Ask an already running instance to stop by connecting to its port. */ +#define WEB_CONSOLE "PS5" +#include "web.h" + static int -stop_running_instance(void) { +web_recv(int fd, void *buf, size_t len) { + ssize_t rc = recv(fd, buf, len, 0); + if(rc < 0) { + return (errno == EAGAIN || errno == EWOULDBLOCK) ? WEB_AGAIN : WEB_ERROR; + } + return rc; +} + +static int +web_send(int fd, const void *buf, size_t len) { + ssize_t rc = send(fd, buf, len, 0); + if(rc < 0) { + return (errno == EAGAIN || errno == EWOULDBLOCK) ? WEB_AGAIN : WEB_ERROR; + } + return rc; +} + +static void +web_sleep_ms(int ms) { + usleep(ms * 1000); +} + +static long long +web_uptime(void) { + return now_seconds() - g_started; +} + +static void +web_get_ip(char *buf, size_t size) { + struct ifaddrs *ifs; + struct ifaddrs *it; + + buf[0] = 0; + if(getifaddrs(&ifs) != 0) { + return; + } + + for(it = ifs; it; it = it->ifa_next) { + if(!it->ifa_addr || it->ifa_addr->sa_family != AF_INET || + !(it->ifa_flags & IFF_UP) || (it->ifa_flags & IFF_LOOPBACK)) { + continue; + } + inet_ntop(AF_INET, &((struct sockaddr_in*)it->ifa_addr)->sin_addr, + buf, size); + break; + } + + freeifaddrs(ifs); +} + +static void +web_notify(const char *msg) { + notify("%s", msg); +} + + +/* Ask an already running instance to quit. Returns 0 if it confirmed. */ +static int +quit_running_instance(void) { struct sockaddr_in addr; + struct timeval tv = {3, 0}; + char resp[64]; + ssize_t n; int fd; - int rc; if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { return -1; } + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + bzero(&addr, sizeof addr); addr.sin_family = AF_INET; addr.sin_port = htons(KEEPAWAKE_PORT); addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - rc = connect(fd, (struct sockaddr*)&addr, sizeof addr); + if(connect(fd, (struct sockaddr*)&addr, sizeof addr) != 0 || + send(fd, web_quit_request, sizeof web_quit_request - 1, 0) < 0) { + close(fd); + return -1; + } + + n = recv(fd, resp, sizeof resp - 1, 0); close(fd); - return rc; + if(n <= 0) { + return -1; + } + resp[n] = 0; + return strstr(resp, " 200 ") ? 0 : -1; } @@ -93,7 +184,7 @@ open_control_socket(void) { addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(fd, (struct sockaddr*)&addr, sizeof addr) != 0 || - listen(fd, 1) != 0) { + listen(fd, 8) != 0) { int err = errno; close(fd); errno = err; @@ -107,33 +198,43 @@ open_control_socket(void) { int main(void) { struct timeval tv; + char ip[32]; fd_set fds; int srv; + int cli; int rc; signal(SIGPIPE, SIG_IGN); if((srv = open_control_socket()) < 0) { if(errno == EADDRINUSE) { - // Port taken: an instance is already running, so toggle it off. - // The running instance shows the "disabled" toast itself. - if(stop_running_instance() == 0) { + // Port taken: an instance is already running, so close it. + // The running instance shows the "closed" toast itself. + if(quit_running_instance() == 0) { return 0; } + notify("Keep Awake failed to start:\nport %d is in use", KEEPAWAKE_PORT); + return -1; } notify("Keep Awake failed to start: %s", strerror(errno)); return -1; } - notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n" - "Send the payload again to disable"); + g_started = now_seconds(); + + web_get_ip(ip, sizeof ip); + if(ip[0]) { + notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n" + "Control it at http://%s:%d", ip, KEEPAWAKE_PORT); + } else { + notify("Keep Awake " KEEPAWAKE_VERSION " enabled\n" + "Control page on port %d", KEEPAWAKE_PORT); + } while(1) { - sceSystemServicePowerTick(); - FD_ZERO(&fds); FD_SET(srv, &fds); - tv.tv_sec = KEEPAWAKE_TICK_SECONDS; + tv.tv_sec = ka_tick(); tv.tv_usec = 0; rc = select(srv + 1, &fds, NULL, NULL, &tv); @@ -146,13 +247,16 @@ main(void) { } if(rc > 0 && FD_ISSET(srv, &fds)) { - // Any incoming connection is a request to stop. - int cli = accept(srv, NULL, NULL); - if(cli >= 0) { - close(cli); + if((cli = accept(srv, NULL, NULL)) < 0) { + continue; + } + fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK); + rc = web_handle_client(cli); + close(cli); + if(rc) { + notify("Keep Awake " KEEPAWAKE_VERSION " closed"); + break; } - notify("Keep Awake " KEEPAWAKE_VERSION " disabled"); - break; } } diff --git a/src/web.h b/src/web.h new file mode 100644 index 0000000..dbb6e72 --- /dev/null +++ b/src/web.h @@ -0,0 +1,244 @@ +/* State and tiny HTTP server for the KeepAwake control page, shared by the + PS5 and PS4 builds. + + Keep Awake can be on (resetting the idle timer) or off (paused, payload + still running so it can be turned back on from the page). Quitting ends + the payload. + + Routes: + GET / the control page (src/index.html) + GET /status JSON status + POST /on start keeping the console awake + POST /off stop keeping it awake, but stay running + POST /quit end the payload + + The platform source defines WEB_CONSOLE ("PS5" or "PS4") before including + this file, and implements the functions declared below. Client sockets are + expected to be non-blocking. */ + +#include "index_html.h" + + +#define WEB_AGAIN -1 +#define WEB_ERROR -2 + +#define WEB_TIMEOUT_MS 2000 +#define WEB_POLL_MS 10 + + +/* Bytes read, 0 if the client closed, WEB_AGAIN if no data yet, or WEB_ERROR. */ +static int web_recv(int fd, void *buf, size_t len); + +/* Bytes sent, WEB_AGAIN if the socket is full, or WEB_ERROR. */ +static int web_send(int fd, const void *buf, size_t len); + +static void web_sleep_ms(int ms); + +/* Seconds since the payload started. */ +static long long web_uptime(void); + +/* The console's LAN IP address as text, or an empty string if unknown. */ +static void web_get_ip(char *buf, size_t size); + +/* Show a toast on the console. */ +static void web_notify(const char *msg); + + +static int ka_active = 1; // resetting the idle timer? +static long long ka_changed = 0; // web_uptime() when on/off last changed +static long long ka_next_tick = 0; // web_uptime() when the next tick is due + + +static void +ka_set_active(int active) { + if(active == ka_active) { + return; + } + + ka_active = active; + ka_changed = web_uptime(); + ka_next_tick = 0; + + web_notify(active ? "Keep Awake turned on" : "Keep Awake turned off"); +} + + +static int +web_starts_with(const char *s, size_t len, const char *prefix) { + size_t n = strlen(prefix); + return len >= n && memcmp(s, prefix, n) == 0; +} + + +static int +web_send_all(int fd, const void *buf, size_t len) { + const char *p = buf; + int waited = 0; + int rc; + + while(len > 0) { + rc = web_send(fd, p, len); + if(rc == WEB_AGAIN) { + if((waited += WEB_POLL_MS) > WEB_TIMEOUT_MS) { + return -1; + } + web_sleep_ms(WEB_POLL_MS); + continue; + } + if(rc <= 0) { + return -1; + } + p += rc; + len -= rc; + } + + return 0; +} + + +static void +web_respond(int fd, const char *status, const char *type, + const void *body, size_t len) { + char head[256]; + int n; + + n = snprintf(head, sizeof head, + "HTTP/1.1 %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %d\r\n" + "Cache-Control: no-store\r\n" + "Connection: close\r\n" + "\r\n", status, type, (int)len); + + if(web_send_all(fd, head, n) == 0 && len > 0) { + web_send_all(fd, body, len); + } +} + + +static void +web_respond_json(int fd, const char *status, const char *json) { + web_respond(fd, status, "application/json", json, strlen(json)); +} + + +static void +web_respond_status(int fd) { + char json[320]; + char ip[32]; + + web_get_ip(ip, sizeof ip); + snprintf(json, sizeof json, + "{\"version\":\"%s\",\"console\":\"%s\",\"active\":%s," + "\"uptime\":%lld,\"since\":%lld,\"interval\":%d," + "\"ip\":\"%s\",\"port\":%d}", + KEEPAWAKE_VERSION, WEB_CONSOLE, ka_active ? "true" : "false", + web_uptime(), web_uptime() - ka_changed, KEEPAWAKE_TICK_SECONDS, + ip, KEEPAWAKE_PORT); + web_respond_json(fd, "200 OK", json); +} + + +/* Read the request headers, giving up after WEB_TIMEOUT_MS so a slow or idle + client can't hold up the keep-awake loop. Returns the bytes read. */ +static size_t +web_read_request(int fd, char *buf, size_t size) { + size_t len = 0; + int waited = 0; + int rc; + + while(len < size - 1) { + rc = web_recv(fd, buf + len, size - 1 - len); + if(rc == WEB_AGAIN) { + if((waited += WEB_POLL_MS) > WEB_TIMEOUT_MS) { + break; + } + web_sleep_ms(WEB_POLL_MS); + continue; + } + if(rc <= 0) { + break; + } + len += rc; + buf[len] = 0; + if(strstr(buf, "\r\n\r\n")) { + break; + } + } + + buf[len] = 0; + return len; +} + + +/* Handle one client connection. Returns 1 if the payload should quit. */ +static int +web_handle_client(int fd) { + char req[1024]; + size_t len; + + if(!(len = web_read_request(fd, req, sizeof req))) { + return 0; + } + + if(web_starts_with(req, len, "GET / ") || + web_starts_with(req, len, "GET /index.html ")) { + web_respond(fd, "200 OK", "text/html; charset=utf-8", + index_html, index_html_len); + + } else if(web_starts_with(req, len, "GET /status ")) { + web_respond_status(fd); + + } else if(web_starts_with(req, len, "POST /on ")) { + ka_set_active(1); + web_respond_status(fd); + + } else if(web_starts_with(req, len, "POST /off ")) { + ka_set_active(0); + web_respond_status(fd); + + } else if(web_starts_with(req, len, "POST /quit ")) { + web_respond_json(fd, "200 OK", "{\"quit\":true}"); + return 1; + + } else if(web_starts_with(req, len, "GET /on ") || + web_starts_with(req, len, "GET /off ") || + web_starts_with(req, len, "GET /quit ")) { + // Only POST changes anything, so link previews and prefetching can't. + web_respond_json(fd, "405 Method Not Allowed", + "{\"error\":\"use POST\"}"); + + } else if(web_starts_with(req, len, "GET /favicon.ico ")) { + web_respond(fd, "204 No Content", "text/plain", "", 0); + + } else { + web_respond_json(fd, "404 Not Found", "{\"error\":\"not found\"}"); + } + + return 0; +} + + +/* Call regularly from the main loop: resets the idle timer when it's due. + Returns the seconds until it next needs calling. */ +static int +ka_tick(void) { + long long now = web_uptime(); + + if(!ka_active) { + return KEEPAWAKE_TICK_SECONDS; + } + if(now >= ka_next_tick) { + sceSystemServicePowerTick(); + ka_next_tick = now + KEEPAWAKE_TICK_SECONDS; + } + return (int)(ka_next_tick - now); +} + + +/* The request a new instance sends to make the running one quit. */ +static const char web_quit_request[] = + "POST /quit HTTP/1.0\r\n" + "Host: localhost\r\n" + "Content-Length: 0\r\n" + "\r\n"; diff --git a/tools/host_stubs.c b/tools/host_stubs.c new file mode 100644 index 0000000..a2570ce --- /dev/null +++ b/tools/host_stubs.c @@ -0,0 +1,19 @@ +/* Stand-ins for the PS5 system calls, so src/ps5.c can run on a computer + (see `make host`). */ + +#include +#include + + +int +sceKernelSendNotificationRequest(int device, void *req, size_t size, int blocking) { + return 0; +} + + +int +sceSystemServicePowerTick(void) { + printf("[KeepAwake] power tick\n"); + fflush(stdout); + return 0; +}