diff --git a/README.md b/README.md index 2af31c1..f6ca11f 100644 --- a/README.md +++ b/README.md @@ -22,45 +22,45 @@ virtual slots are exposed; the console's device limits apply. plugin that takes over a signed-in user's controller can stop games from reading the virtual controllers. -## Start it with GoldHEN (AutoRun) +## Install -GoldHEN 2.4b18.10 can run payloads on its own, from its **Payloader LaunchPad**, and -automatically each time GoldHEN starts (**AutoRun**). Set this up once and no PC or -PayLoader is needed to start Control4Free. +1. Copy `build/Control4Free-0.2.0.pkg` to a USB drive (or to `/data/pkg/` over + GoldHEN's FTP server) and install it with GoldHEN's Package Installer. +2. Open **Control4Free** from the home screen and press **Cross** once. The app + adds Control4Free to GoldHEN's AutoRun (GoldHEN 2.4b18.10 or later), so GoldHEN + starts it every time it loads. It also starts it right away if GoldHEN's + PayLoader is on; otherwise restart the PS4 and run the jailbreak again. +3. Scan the QR code or enter the address on your phone or PC. Select a controller + there and sign in through the PS4 screen. + +The app stays useful afterwards: it shows whether Control4Free is running, its +address and QR code, and how many controllers are connected. + +- **Cross** starts Control4Free when it is not running (PayLoader must be on). +- **Triangle** turns GoldHEN's auto-start on or off, or updates it after you + install a newer package. +- **Square**, then **Cross**, stops Control4Free and disconnects its controllers. +- **Circle** closes the app. Control4Free keeps running without it. + +### Without the app + +GoldHEN's own **Payloader LaunchPad** (under **Utilities**) does the same job: 1. Put `control4free.elf` in `/data/payloads/` on the PS4, for example through - GoldHEN's FTP server (port 2121). GoldHEN also lists payloads in a `Payloads` - folder on a USB drive, but keep the AutoRun copy on the console. -2. In GoldHEN, open **Payloads > Payloader LaunchPad** and select - `control4free.elf` to start it now. -3. Press **Square** on it to add it to the AutoRun queue. GoldHEN then starts it - every time GoldHEN loads. + GoldHEN's FTP server (port 2121). +2. In the LaunchPad, select `control4free.elf` to start it, or press **Square** on + it to add it to the AutoRun queue. The queue is `/data/GoldHEN/payloads.ini`: -When you update Control4Free, replace `/data/payloads/control4free.elf` as well. + ```ini + [AutoRun] + /user/data/payloads/control4free.elf = 1 + ``` -## Install the launcher app +When you update Control4Free this way, replace `/data/payloads/control4free.elf`. -The launcher is a home-screen app that shows whether Control4Free is running, its -address and a QR code for your phone, and can start or stop it. - -1. Copy `build/Control4Free-0.2.0.pkg` to a USB drive and install it using the - PS4's Package Installer with GoldHEN active. -2. Open **Control4Free** from the home screen. If AutoRun already started it, the - app shows **Running**. Otherwise turn on GoldHEN's PayLoader and press - **Cross**. -3. Scan the QR code or enter the address on your phone. Select a controller there - and sign in through the PS4 screen. -4. Press **Circle** to close the app. Control4Free keeps running on its own. Reopen - the app to see the address again, or press **Square**, then **Cross**, to stop it - and disconnect all its controllers. - -The app does not turn on GoldHEN or PayLoader, and does not start after a reboot by -itself; AutoRun does that. - -**Upgrading from the original test payload:** disconnect controllers and use -**Stop Control4Free** in the phone menu before starting from the launcher. The -old payload has no launcher-management API, so the app refuses to start a second -copy over it. Rebooting the console also clears that old instance. +**Upgrading from the original test payload:** use **Stop Control4Free** in the +controller page's menu, or restart the PS4. That old version has no launcher API, +so the app will not start a second copy over it. ## Use @@ -102,7 +102,7 @@ files varies. Keep the controller page in the foreground while playing. - Explicit disconnect removes the controller immediately. Stopping the payload removes all controllers and restores the saved host credentials. Browser Stop refuses while another browser owns a controller. The launcher's confirmed Stop - can disconnect all controllers through its loopback-only API. + can disconnect all controllers through its launcher API, which websites cannot use. - If the kernel log is not available yet when Control4Free starts (AutoRun can start it early), it connects to it when the first controller is created. - There is no time limit. Rebooting the console ends it; AutoRun, the launcher or diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 05be07e..a7934c0 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -13,6 +13,9 @@ GPL-3.0). The toolchain supplies `sce_sys/about/right.sprx` and its stub `sce_module/libc.prx` and `sce_module/libSceFios2.prx`; LibOrbisPkg produces the package. `docker/Dockerfile.launcher` pins the OpenOrbis image by digest. +- `launcher/autorun.c` calls GoldHEN's SDK command (syscall 500, jailbreak and + unjailbreak) with the `jailbreak_backup` layout from the GoldHEN Plugins SDK + (https://github.com/GoldHEN/GoldHEN_Plugins_SDK), MIT. No SDK code is included. - `vendor/qrcodegen/qrcodegen.c` and `.h` are Project Nayuki's QR generator v1.8.0 (https://github.com/nayuki/QR-Code-generator/tree/v1.8.0/c), MIT. The full license is retained at the top of each file. SHA-256 for `.c`: diff --git a/include/c4f_net.h b/include/c4f_net.h index 6fe34a0..3296abe 100644 --- a/include/c4f_net.h +++ b/include/c4f_net.h @@ -7,7 +7,7 @@ #define C4F_NET_CLIENTS 8 #define C4F_NET_MESSAGE 4096 typedef struct C4fNetClient { - int fd, websocket, closing, fragmented, loopback; + int fd, websocket, closing, fragmented; uint64_t openedMs; unsigned char rx[8192], tx[8192]; size_t rxUsed, txUsed, txSent, messageUsed; diff --git a/launcher/Makefile b/launcher/Makefile index e6146cf..38aca68 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -8,7 +8,7 @@ CC := clang-18 CXX := clang++-18 LD := ld.lld-18 CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o +OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf .PHONY: all package diff --git a/launcher/autorun.c b/launcher/autorun.c new file mode 100644 index 0000000..be43648 --- /dev/null +++ b/launcher/autorun.c @@ -0,0 +1,275 @@ +/* GoldHEN AutoRun for the bundled payload. */ +#include "autorun.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* GoldHEN reads this path from payloads.ini, whatever our test root is. */ +#define C4F_AUTORUN_KEY "/user/data/payloads/control4free.elf" + +static char c4fRoot[256] = "/user/data"; +static unsigned char *c4fBundled; +static size_t c4fBundledSize; + +void c4fAutorunSetRoot(const char *root) { snprintf(c4fRoot, sizeof(c4fRoot), "%s", root); } + +#ifdef __FreeBSD__ +/* GoldHEN's SDK call (syscall 500). Command 2 takes this process out of its + * sandbox, command 3 puts it back; the layout is the GoldHEN Plugins SDK's + * struct jailbreak_backup (MIT). */ +typedef struct { + uint32_t cr_uid, cr_ruid, cr_rgid, cr_groups; + uint64_t cr_paid, cr_caps[2]; + void *cr_prison, *fd_cdir, *fd_jdir, *fd_rdir; +} C4fJailbreak; + +static long c4fGoldHen(uint64_t command, void *data) +{ + long ret = 500; + int failed; + __asm__ volatile("syscall" : "+a"(ret), "=@ccc"(failed) : "D"(command), "S"(data) + : "rcx", "rdx", "r8", "r9", "r10", "r11", "memory"); + return failed ? -ret : ret; +} + +static int c4fEnter(C4fJailbreak *backup) +{ + /* Without GoldHEN the call does not exist: get an error, not SIGSYS. */ + signal(SIGSYS, SIG_IGN); + memset(backup, 0, sizeof(*backup)); + long ret = c4fGoldHen(2, backup); + if (ret != 0) errno = ret < 0 ? (int)-ret : EPERM; + return ret == 0 ? 0 : -1; +} + +static void c4fLeave(C4fJailbreak *backup) { (void)c4fGoldHen(3, backup); } +#else +/* Host tests: no sandbox to leave. */ +typedef struct { int unused; } C4fJailbreak; +static int c4fEnter(C4fJailbreak *backup) +{ + (void)backup; + if (getenv("C4F_TEST_NO_GOLDHEN")) { errno = ENOSYS; return -1; } + return 0; +} +static void c4fLeave(C4fJailbreak *backup) { (void)backup; } +#endif + +static int c4fPath(char *out, size_t size, const char *relative) +{ + return snprintf(out, size, "%s/%s", c4fRoot, relative) < (int)size ? 0 : -1; +} + +/* The whole file, NUL-terminated; NULL (errno set) if missing or too large. */ +static unsigned char *c4fReadFile(const char *path, size_t *size, size_t limit) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return NULL; + unsigned char *data = NULL; + size_t used = 0, capacity = 0; + for (;;) { + if (used + 1 >= capacity) { + size_t grown = capacity ? capacity * 2 : 65536; + unsigned char *next = grown <= limit + 1 ? realloc(data, grown) : NULL; + if (!next) { free(data); close(fd); errno = EFBIG; return NULL; } + data = next; + capacity = grown; + } + ssize_t n = read(fd, data + used, capacity - used - 1); + if (n < 0 && errno == EINTR) continue; + if (n < 0) { int saved = errno; free(data); close(fd); errno = saved; return NULL; } + if (n == 0) break; + used += (size_t)n; + } + close(fd); + data[used] = 0; + *size = used; + return data; +} + +/* Written beside the target first, then renamed over it. */ +static int c4fWriteFile(const char *path, const void *data, size_t size) +{ + char temporary[320]; + if (snprintf(temporary, sizeof(temporary), "%s.tmp", path) >= (int)sizeof(temporary)) { errno = ENAMETOOLONG; return -1; } + int fd = open(temporary, O_WRONLY | O_CREAT | O_TRUNC, 0777); + if (fd < 0) return -1; + const unsigned char *p = data; + while (size) { + ssize_t n = write(fd, p, size); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) { int saved = errno; close(fd); unlink(temporary); errno = saved; return -1; } + p += n; + size -= (size_t)n; + } + if (close(fd) || rename(temporary, path)) { int saved = errno; unlink(temporary); errno = saved; return -1; } + return 0; +} + +int c4fAutorunLoadBundled(const char *path) +{ + size_t size = 0; + unsigned char *data = c4fReadFile(path, &size, 16u << 20); + if (!data) return -1; + if (size < 64 || memcmp(data, "\177ELF", 4)) { free(data); errno = EINVAL; return -1; } + free(c4fBundled); + c4fBundled = data; + c4fBundledSize = size; + return 0; +} + +/* ---- payloads.ini: "[AutoRun]", then " = 1" lines ---- */ + +static void c4fTrim(const char **start, const char **end) +{ + while (*start < *end && (**start == ' ' || **start == '\t' || **start == '\r')) (*start)++; + while (*end > *start && ((*end)[-1] == ' ' || (*end)[-1] == '\t' || (*end)[-1] == '\r')) (*end)--; +} + +static int c4fIsSection(const char *start, const char *end, const char *name) +{ + c4fTrim(&start, &end); + size_t n = strlen(name); + return end - start >= 2 && *start == '[' && end[-1] == ']' && (size_t)(end - start - 2) == n && !memcmp(start + 1, name, n); +} + +/* 1 for our entry; *enabled says whether its value is 1. */ +static int c4fIsOurEntry(const char *start, const char *end, int *enabled) +{ + const char *equals = memchr(start, '=', (size_t)(end - start)); + if (!equals) return 0; + const char *keyStart = start, *keyEnd = equals, *valueStart = equals + 1, *valueEnd = end; + c4fTrim(&keyStart, &keyEnd); + c4fTrim(&valueStart, &valueEnd); + if ((size_t)(keyEnd - keyStart) != strlen(C4F_AUTORUN_KEY) || memcmp(keyStart, C4F_AUTORUN_KEY, strlen(C4F_AUTORUN_KEY))) return 0; + *enabled = valueEnd - valueStart == 1 && *valueStart == '1'; + return 1; +} + +static int c4fIniEnabled(const char *text) +{ + int inAutoRun = 0, enabled = 0; + while (*text) { + const char *end = strchr(text, '\n'); + if (!end) end = text + strlen(text); + const char *start = text, *trimmedEnd = end; + c4fTrim(&start, &trimmedEnd); + if (start < trimmedEnd && *start == '[') inAutoRun = c4fIsSection(text, end, "AutoRun"); + else if (inAutoRun && c4fIsOurEntry(text, end, &enabled) && enabled) return 1; + text = *end ? end + 1 : end; + } + return 0; +} + +/* The same text with our entry present (enable) or gone; other lines stay. */ +static char *c4fIniEdited(const char *text, int enable) +{ + static const char entry[] = C4F_AUTORUN_KEY " = 1\n"; + size_t length = strlen(text); + char *out = malloc(length + sizeof(entry) + 16); + if (!out) return NULL; + size_t used = 0; + int inAutoRun = 0, placed = 0, dummy; +#define C4F_APPEND(data, n) do { memcpy(out + used, (data), (n)); used += (n); } while (0) + while (*text) { + const char *end = strchr(text, '\n'); + if (!end) end = text + strlen(text); + const char *start = text, *trimmedEnd = end; + c4fTrim(&start, &trimmedEnd); + if (start < trimmedEnd && *start == '[') { + if (inAutoRun && enable && !placed) { C4F_APPEND(entry, sizeof(entry) - 1); placed = 1; } + inAutoRun = c4fIsSection(text, end, "AutoRun"); + C4F_APPEND(text, (size_t)(end - text)); C4F_APPEND("\n", 1); + } else if (inAutoRun && c4fIsOurEntry(text, end, &dummy)) { + if (enable && !placed) { C4F_APPEND(entry, sizeof(entry) - 1); placed = 1; } + } else if (end > text || *end) { + C4F_APPEND(text, (size_t)(end - text)); C4F_APPEND("\n", 1); + } + text = *end ? end + 1 : end; + } + if (enable && !placed) { + if (!inAutoRun) C4F_APPEND("[AutoRun]\n", 10); + C4F_APPEND(entry, sizeof(entry) - 1); + } +#undef C4F_APPEND + out[used] = 0; + return out; +} + +/* ---- public ---- */ + +int c4fAutorunCheck(char *problem, size_t size) +{ + C4fJailbreak jailbreak; + char path[300]; + size_t length = 0; + problem[0] = 0; + if (!c4fBundled) { snprintf(problem, size, "the bundled payload could not be read"); return C4F_AUTORUN_UNKNOWN; } + if (c4fEnter(&jailbreak)) { snprintf(problem, size, "GoldHEN did not let the app check (errno %d)", errno); return C4F_AUTORUN_UNKNOWN; } + int state = C4F_AUTORUN_OFF; + char *ini = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini") ? NULL : (char *)c4fReadFile(path, &length, 1u << 20); + if (ini && c4fIniEnabled(ini) && !c4fPath(path, sizeof(path), "payloads/control4free.elf")) { + size_t installedSize = 0; + unsigned char *installed = c4fReadFile(path, &installedSize, 16u << 20); + if (installed) + state = installedSize == c4fBundledSize && !memcmp(installed, c4fBundled, installedSize) ? C4F_AUTORUN_ON : C4F_AUTORUN_OUTDATED; + free(installed); + } + free(ini); + c4fLeave(&jailbreak); + return state; +} + +int c4fAutorunEnable(char *message, size_t size) +{ + C4fJailbreak jailbreak; + char directory[300], path[300]; + size_t length = 0; + if (!c4fBundled) { snprintf(message, size, "The bundled payload could not be read. Reinstall the Control4Free package."); return -1; } + if (c4fEnter(&jailbreak)) { snprintf(message, size, "GoldHEN did not let the app set up auto-start (errno %d).", errno); return -1; } + int failed = c4fPath(directory, sizeof(directory), "payloads") || c4fPath(path, sizeof(path), "payloads/control4free.elf"); + if (!failed && mkdir(directory, 0777) && errno != EEXIST) failed = 1; + if (!failed && c4fWriteFile(path, c4fBundled, c4fBundledSize)) failed = 1; + if (failed) { + snprintf(message, size, "Could not copy Control4Free to /data/payloads (errno %d).", errno); + c4fLeave(&jailbreak); + return -1; + } + failed = c4fPath(directory, sizeof(directory), "GoldHEN") || c4fPath(path, sizeof(path), "GoldHEN/payloads.ini"); + if (!failed && mkdir(directory, 0777) && errno != EEXIST) failed = 1; + char *old = failed ? NULL : (char *)c4fReadFile(path, &length, 1u << 20); + if (!failed && !old && errno != ENOENT) failed = 1; + char *edited = failed ? NULL : c4fIniEdited(old ? old : "", 1); + if (!failed && (!edited || c4fWriteFile(path, edited, strlen(edited)))) failed = 1; + if (failed) snprintf(message, size, "Could not add Control4Free to GoldHEN's AutoRun list (errno %d).", errno); + else snprintf(message, size, "Auto-start is on. GoldHEN starts Control4Free each time it loads."); + free(old); + free(edited); + c4fLeave(&jailbreak); + return failed ? -1 : 0; +} + +int c4fAutorunDisable(char *message, size_t size) +{ + C4fJailbreak jailbreak; + char path[300]; + size_t length = 0; + if (c4fEnter(&jailbreak)) { snprintf(message, size, "GoldHEN did not let the app change auto-start (errno %d).", errno); return -1; } + int failed = c4fPath(path, sizeof(path), "GoldHEN/payloads.ini"); + char *old = failed ? NULL : (char *)c4fReadFile(path, &length, 1u << 20); + char *edited = old ? c4fIniEdited(old, 0) : NULL; + if (!failed && old && (!edited || c4fWriteFile(path, edited, strlen(edited)))) failed = 1; + if (!failed && !old && errno != ENOENT) failed = 1; + if (failed) snprintf(message, size, "Could not change GoldHEN's AutoRun list (errno %d).", errno); + else snprintf(message, size, "Auto-start is off. Start Control4Free here with Cross, or from GoldHEN's Payloader LaunchPad."); + free(old); + free(edited); + c4fLeave(&jailbreak); + return failed ? -1 : 0; +} diff --git a/launcher/autorun.h b/launcher/autorun.h new file mode 100644 index 0000000..7074f25 --- /dev/null +++ b/launcher/autorun.h @@ -0,0 +1,24 @@ +#ifndef C4F_LAUNCHER_AUTORUN_H +#define C4F_LAUNCHER_AUTORUN_H +/* GoldHEN AutoRun for the bundled payload: a copy in /data/payloads and an + * entry in /data/GoldHEN/payloads.ini, the files GoldHEN's Payloader LaunchPad + * uses. The app leaves its sandbox for this through GoldHEN's SDK call. */ +#include +#ifdef __cplusplus +extern "C" { +#endif + +enum { C4F_AUTORUN_UNKNOWN = -1, C4F_AUTORUN_OFF = 0, C4F_AUTORUN_ON = 1, C4F_AUTORUN_OUTDATED = 2 }; + +/* Host tests point the data folder elsewhere; the console uses /user/data. */ +void c4fAutorunSetRoot(const char *root); +/* Reads the bundled payload into memory; 0 on success. */ +int c4fAutorunLoadBundled(const char *path); +int c4fAutorunCheck(char *problem, size_t size); +int c4fAutorunEnable(char *message, size_t size); +int c4fAutorunDisable(char *message, size_t size); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/launcher/draw.c b/launcher/draw.c index ac8f624..99b7187 100644 --- a/launcher/draw.c +++ b/launcher/draw.c @@ -237,6 +237,27 @@ void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, } } +void c4fTriangleOutline(C4fCanvas *c, float cx, float cy, float radius, float width, C4fColor color) +{ + /* An upward equilateral triangle through three corners at `radius`. */ + const float k = 1.7320508f; + float reach = radius + width; + C4fBox b; + if (!c4fClip(c, cx - reach - 1, cy - reach - 1, cx + reach + 1, cy + reach + 1, &b)) return; + for (int py = b.y0; py < b.y1; py++) + for (int px = b.x0; px < b.x1; px++) { + /* Signed distance to an equilateral triangle (Inigo Quilez). */ + float x = fabsf(px + 0.5f - cx), y = -(py + 0.5f - cy) * 1.0f, r = radius * 0.866025f; + x = x - r; + y = y + r / k; + if (x + k * y > 0) { float nx = (x - k * y) / 2, ny = (-k * x - y) / 2; x = nx; y = ny; } + x -= fminf(fmaxf(x, -2 * r), 0); + float d = -hypotf(x, y) * (y < 0 ? -1 : 1); + float cov = c4fClamp01(0.5f - (fabsf(d) - width / 2)); + if (cov > 0) c4fBlend(c, px, py, color, cov); + } +} + /* ---- text ---- */ static C4fFace *c4fFace(int font, float size) diff --git a/launcher/draw.h b/launcher/draw.h index 0a0f826..2801fa9 100644 --- a/launcher/draw.h +++ b/launcher/draw.h @@ -40,6 +40,7 @@ void c4fGlowCircle(C4fCanvas *c, float cx, float cy, float r, float blur, float /* A line with round caps. */ void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float width, C4fColor color); void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color); +void c4fTriangleOutline(C4fCanvas *c, float cx, float cy, float radius, float width, C4fColor color); /* Text on a baseline. Sizes are CSS font-size in pixels. ASCII only. */ float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text); diff --git a/launcher/main.cpp b/launcher/main.cpp index 6bb08bc..f9b953b 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -1,4 +1,5 @@ /* Native PS4 launcher. The controller service runs outside this application. */ +#include #include #include #include @@ -10,46 +11,81 @@ #include #include #include +#include "autorun.h" #include "screen.h" #include "service.h" +#define C4F_BUNDLED_PAYLOAD "/app0/assets/control4free.elf" + +/* Worker commands. */ +enum { C4F_DO_NOTHING, C4F_DO_START, C4F_DO_STOP, C4F_DO_SET_UP, C4F_DO_AUTORUN_ON, C4F_DO_AUTORUN_OFF }; + static pthread_mutex_t c4fMutex = PTHREAD_MUTEX_INITIALIZER; static C4fLauncherScreen c4fScreen; static int c4fCommand, c4fQuit; +/* Set up: auto-start on, then start it now if PayLoader takes it. */ +static int c4fSetUp(char *message, size_t size) +{ + int result = c4fAutorunEnable(message, size); + if (result) return result; + char started[160]; + result = c4fLauncherStart(C4F_BUNDLED_PAYLOAD, started, sizeof(started)); + if (result == 0) snprintf(message, size, "Set up and running. GoldHEN will also start it after each restart."); + else if (result == -2) snprintf(message, size, "%s", started); + else snprintf(message, size, "Auto-start is on: Control4Free starts the next time GoldHEN loads. %s", started); + return result == -2 ? -2 : 0; +} + static void *c4fWorker(void *) { uint64_t nextCheck = 0; - int first = 1; + int first = 1, autorun = C4F_AUTORUN_UNKNOWN; + char autorunNote[96] = {0}, shownProblem[128] = {0}; for (;;) { pthread_mutex_lock(&c4fMutex); int command = c4fCommand, quit = c4fQuit; - c4fCommand = 0; + c4fCommand = C4F_DO_NOTHING; pthread_mutex_unlock(&c4fMutex); if (quit) return NULL; if (command || c4fLauncherTimeMs() >= nextCheck) { char message[160] = {0}, address[64] = {0}; int result = 0; - if (command == 1) result = c4fLauncherStart("/app0/assets/control4free.elf", message, sizeof(message)); - if (command == 2) result = c4fLauncherStop(message, sizeof(message)); - C4fServiceStatus status; - int running = c4fLauncherProbe(&status); + /* The console's own address: shown for the phone or PC, and the + * app's second way to reach Control4Free from its sandbox. */ OrbisNetCtlInfo info; memset(&info, 0, sizeof(info)); if (sceNetCtlGetInfo(ORBIS_NET_CTL_INFO_IP_ADDRESS, &info) == 0 && - info.ip_address[0] && strcmp(info.ip_address, "0.0.0.0")) + info.ip_address[0] && strcmp(info.ip_address, "0.0.0.0")) { snprintf(address, sizeof(address), "http://%.15s:4264", info.ip_address); + c4fLauncherSetHost(info.ip_address); + } else { + c4fLauncherSetHost(NULL); + } + if (command == C4F_DO_START) result = c4fLauncherStart(C4F_BUNDLED_PAYLOAD, message, sizeof(message)); + if (command == C4F_DO_STOP) result = c4fLauncherStop(message, sizeof(message)); + if (command == C4F_DO_SET_UP) result = c4fSetUp(message, sizeof(message)); + if (command == C4F_DO_AUTORUN_ON) result = c4fAutorunEnable(message, sizeof(message)); + if (command == C4F_DO_AUTORUN_OFF) result = c4fAutorunDisable(message, sizeof(message)); + if (first || command >= C4F_DO_SET_UP) autorun = c4fAutorunCheck(autorunNote, sizeof(autorunNote)); + C4fServiceStatus status; + int running = c4fLauncherProbe(&status); + const char *problem = c4fLauncherProblem(); pthread_mutex_lock(&c4fMutex); if (result == -2) c4fScreen.locked = 1; - if (command) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", message); - else if (!c4fScreen.locked && (first || running != c4fScreen.running)) { - snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", - running == 1 ? "Ready. Scan the code or open the address on your phone." : - running == 0 ? "Not running. Press Cross to start it." : - "Something else answers on port 4264. Stop the old copy from its phone page."); + if (command) { + snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", message); + } else if (!c4fScreen.locked && + (first || running != c4fScreen.running || (running < 0 && strcmp(problem, shownProblem)))) { + if (running == 1) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Ready. Open the address on your phone or PC."); + else if (running == 0) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Not running."); + else snprintf(c4fScreen.message, sizeof(c4fScreen.message), "No answer the app understands: %s", problem); + snprintf(shownProblem, sizeof(shownProblem), "%s", problem); } c4fScreen.running = running; c4fScreen.controllers = status.controllers; + c4fScreen.autorun = autorun; + snprintf(c4fScreen.autorunNote, sizeof(c4fScreen.autorunNote), "%s", autorunNote); snprintf(c4fScreen.address, sizeof(c4fScreen.address), "%s", address); /* A queued click while a periodic probe was in flight remains busy. */ if (!c4fCommand) c4fScreen.busy = 0; @@ -108,6 +144,8 @@ int main(void) const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t); setvbuf(stdout, NULL, _IONBF, 0); printf("[c4f-launcher] start " C4F_LAUNCHER_VERSION "\n"); + /* A peer closing mid-send must be an error, not the end of the app. */ + signal(SIGPIPE, SIG_IGN); /* Frames are composed in cached memory: blending reads pixels back, which * is very slow from the write-combined framebuffers. */ uint32_t *canvas = (uint32_t *)malloc(frameBytes); @@ -122,8 +160,10 @@ int main(void) int32_t user = -1, pad = -1; if (scePadInit() == 0 && sceUserServiceGetInitialUser(&user) == 0) pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL); - c4fScreen.running = -1; c4fScreen.busy = 1; + c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN; snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free..."); + /* Read before any sandbox change: /app0 is only visible from inside it. */ + if (c4fAutorunLoadBundled(C4F_BUNDLED_PAYLOAD)) printf("[c4f-launcher] bundled payload unreadable\n"); pthread_t worker; int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0; if (!workerStarted || pad < 0) { @@ -151,12 +191,21 @@ int main(void) else done = 1; } else if (pressed & ORBIS_PAD_BUTTON_CROSS) { if (c4fScreen.confirmStop) { - c4fCommand = 2; c4fScreen.confirmStop = 0; c4fScreen.busy = 1; + c4fCommand = C4F_DO_STOP; c4fScreen.confirmStop = 0; c4fScreen.busy = 1; snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers..."); } else if (!c4fScreen.locked && c4fScreen.running != 1) { - c4fCommand = 1; c4fScreen.busy = 1; - snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Starting Control4Free. Please wait..."); + /* Until auto-start is on, Cross sets it up and starts it. */ + int setUp = c4fScreen.autorun != C4F_AUTORUN_ON; + c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1; + snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", + setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait..."); } + } else if ((pressed & ORBIS_PAD_BUTTON_TRIANGLE) && !c4fScreen.confirmStop && + c4fScreen.autorun != C4F_AUTORUN_UNKNOWN) { + int off = c4fScreen.autorun == C4F_AUTORUN_ON; + c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1; + snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", + off ? "Turning auto-start off..." : "Turning auto-start on..."); } else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1; } C4fLauncherScreen snapshot = c4fScreen; diff --git a/launcher/screen.c b/launcher/screen.c index 9a6d906..bff3f15 100644 --- a/launcher/screen.c +++ b/launcher/screen.c @@ -6,6 +6,7 @@ #include #include #include +#include "autorun.h" #include "draw.h" #include "qrcodegen.h" @@ -40,6 +41,7 @@ #define C4F_CIRCLE_KEY C4F_RGBA(255, 102, 128, 1) #define C4F_CROSS_KEY C4F_RGBA(134, 182, 255, 1) #define C4F_SQUARE_KEY C4F_RGBA(255, 146, 220, 1) +#define C4F_TRIANGLE_KEY C4F_RGBA(63, 224, 184, 1) #define C4F_WHITE C4F_RGBA(255, 255, 255, 1) static uint32_t *c4fBackdrop; @@ -342,7 +344,7 @@ static void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow) } /* Button guide in the PS4's manner, with the page's face-button colours. */ -enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE }; +enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE, C4F_KEY_TRIANGLE }; typedef struct { int key; const char *label; } C4fHint; #define C4F_HINT_RADIUS 22.0f #define C4F_HINT_GAP 14.0f @@ -364,6 +366,8 @@ static void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, c4fSegment(c, cx - 8.5f, cy + 8.5f, cx + 8.5f, cy - 8.5f, 3.6f, C4F_CROSS_KEY); } else if (hints[i].key == C4F_KEY_CIRCLE) { c4fRing(c, cx, cy, 10.5f, 3.6f, C4F_CIRCLE_KEY); + } else if (hints[i].key == C4F_KEY_TRIANGLE) { + c4fTriangleOutline(c, cx, cy + 1.5f, 11.5f, 3.4f, C4F_TRIANGLE_KEY); } else { c4fSquareOutline(c, cx, cy, 8.5f, 3.4f, C4F_SQUARE_KEY); } @@ -406,8 +410,8 @@ typedef struct { static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSize) { C4fLook look = { "Not responding", "Check Control4Free", - "Another program answers on port 4264, or nothing replied in time. Stop the old copy from " - "its phone page, or restart the PS4.", + "Something answers on port 4264 but not the way this app expects. If an older Control4Free " + "is running, stop it on its controller page or restart the PS4.", "Not responding", C4F_BAD, C4F_BAD, 0 }; if (s->locked) { look.pill = look.meta = "Restart needed"; @@ -423,10 +427,15 @@ static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSiz "Open the address or scan the code on your phone or PC. Pick a controller there, then " "choose its user on the PS4.", meta, C4F_OK, C4F_ACCENT, 1 }; - } else if (s->running == 0) { + } else if (s->running == 0 && s->autorun == C4F_AUTORUN_ON) { look = (C4fLook){ "Stopped", "Start Control4Free", - "Press Cross with GoldHEN's PayLoader turned on, or start it from GoldHEN's Payloader " - "LaunchPad. Add it to AutoRun there and it starts with GoldHEN.", + "It starts with GoldHEN after each restart. To start it now, turn on GoldHEN's PayLoader " + "and press Cross.", + "Not running", C4F_FAINT, C4F_FAINT, 0 }; + } else if (s->running == 0) { + look = (C4fLook){ "Stopped", "Set up Control4Free", + "Press Cross once. GoldHEN will start Control4Free each time it loads, and it starts " + "right away if GoldHEN's PayLoader is on.", "Not running", C4F_FAINT, C4F_FAINT, 0 }; } return look; @@ -498,23 +507,51 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s) "The code appears once it runs"); } - /* .panel with the one thing worth knowing. */ + /* .panel: whether GoldHEN starts it by itself. */ + const char *panelTitle, *panelText; + C4fColor panelDot = C4F_FAINT; + if (s->autorun == C4F_AUTORUN_ON) { + panelTitle = "Starts by itself with GoldHEN"; + panelText = "GoldHEN starts Control4Free each time it loads. Closing this app leaves your controllers " + "running. Turn off other controller plugins in your games."; + panelDot = C4F_OK; + } else if (s->autorun == C4F_AUTORUN_OUTDATED) { + panelTitle = "Auto-start uses an older Control4Free"; + panelText = "Press Triangle to switch GoldHEN's auto-start to this version. It takes effect after the " + "next restart."; + panelDot = C4F_WARN; + } else if (s->autorun == C4F_AUTORUN_OFF) { + panelTitle = "Auto-start is off"; + panelText = "Press Triangle and GoldHEN starts Control4Free each time it loads, with no PC needed."; + } else { + panelTitle = "Auto-start status unknown"; + panelText = s->autorunNote[0] ? s->autorunNote : "Checking with GoldHEN..."; + panelDot = s->autorunNote[0] ? C4F_BAD : C4F_WARN; + } float panelY = C4F_PANEL_TOP; c4fPanel(&c, left, panelY, right - left, C4F_PANEL_HEIGHT, C4F_PANEL, C4F_LINE_SOFT); - c4fText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S, panelY + 52, C4F_TEXT, - "Closing this app leaves your controllers running"); + c4fDot(&c, left + 20 * C4F_S + 4 * C4F_S, panelY + 43, panelDot, s->autorun == C4F_AUTORUN_ON); + c4fText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S + 24, panelY + 52, C4F_TEXT, panelTitle); c4fTextWrapped(&c, C4F_FONT_LIGHT, 23, left + 20 * C4F_S, panelY + 94, right - left - 40 * C4F_S, 34, 1, C4F_MUTED, - "Open it again any time to see the address or to stop Control4Free. Turn off other " - "controller plugins in your games."); + panelText); /* .home-foot and the button guide. */ c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION); if (!s->confirmStop && !s->busy) { - C4fHint hints[2]; + C4fHint hints[4]; int n = 0; - if (!s->locked) { - hints[n].key = live ? C4F_KEY_SQUARE : C4F_KEY_CROSS; - hints[n++].label = live ? "Stop" : "Start"; + if (!s->locked && !live) { + hints[n].key = C4F_KEY_CROSS; + hints[n++].label = s->autorun == C4F_AUTORUN_ON ? "Start" : "Set up"; + } + if (s->autorun != C4F_AUTORUN_UNKNOWN) { + hints[n].key = C4F_KEY_TRIANGLE; + hints[n++].label = s->autorun == C4F_AUTORUN_ON ? "Turn off auto-start" : + s->autorun == C4F_AUTORUN_OUTDATED ? "Update auto-start" : "Turn on auto-start"; + } + if (live) { + hints[n].key = C4F_KEY_SQUARE; + hints[n++].label = "Stop"; } hints[n].key = C4F_KEY_CIRCLE; hints[n++].label = "Close"; diff --git a/launcher/screen.h b/launcher/screen.h index e42b048..aaf29e7 100644 --- a/launcher/screen.h +++ b/launcher/screen.h @@ -6,7 +6,8 @@ #define C4F_SCREEN_HEIGHT 1080 typedef struct { int running, busy, controllers, confirmStop, locked; - char address[64], message[160]; + int autorun; /* C4F_AUTORUN_* from autorun.h */ + char address[64], message[160], autorunNote[96]; } C4fLauncherScreen; #ifdef __cplusplus extern "C" { diff --git a/launcher/service.c b/launcher/service.c index f965cb1..fad48d0 100644 --- a/launcher/service.c +++ b/launcher/service.c @@ -21,6 +21,9 @@ #define C4F_PAYLOADER_PORT 9090 #endif +static char c4fHost[16]; +static char c4fProblem[128]; + uint64_t c4fLauncherTimeMs(void) { struct timespec t; @@ -28,6 +31,32 @@ uint64_t c4fLauncherTimeMs(void) return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; } +void c4fLauncherSetHost(const char *ipv4) +{ + struct in_addr parsed; + if (ipv4 && inet_pton(AF_INET, ipv4, &parsed) == 1 && strcmp(ipv4, "127.0.0.1")) + snprintf(c4fHost, sizeof(c4fHost), "%s", ipv4); + else + c4fHost[0] = 0; +} + +const char *c4fLauncherProblem(void) { return c4fProblem; } + +static void c4fNote(const char *address, const char *what) +{ + size_t used = strlen(c4fProblem); + snprintf(c4fProblem + used, sizeof(c4fProblem) - used, "%s%s %s", used ? "; " : "", address, what); +} + +/* Loopback first, then the console's own address. */ +static int c4fTargets(const char *targets[2]) +{ + targets[0] = "127.0.0.1"; + if (!c4fHost[0]) return 1; + targets[1] = c4fHost; + return 2; +} + static int c4fWaitSocket(int fd, int writeReady, uint64_t deadline) { for (;;) { @@ -44,7 +73,7 @@ static int c4fWaitSocket(int fd, int writeReady, uint64_t deadline) } } -static int c4fConnect(int port) +static int c4fConnect(const char *address, int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return -1; @@ -57,7 +86,7 @@ static int c4fConnect(int port) struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + if (inet_pton(AF_INET, address, &addr.sin_addr) != 1) { errno = EINVAL; goto fail; } if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) return fd; if (errno != EINPROGRESS) goto fail; if (c4fWaitSocket(fd, 1, c4fLauncherTimeMs() + 700)) goto fail; @@ -75,7 +104,7 @@ static int c4fSendAll(int fd, const void *data, size_t size, uint64_t deadline) while (size) { if (c4fWaitSocket(fd, 1, deadline)) return -1; int flags = 0; -#ifdef MSG_NOSIGNAL +#if defined(MSG_NOSIGNAL) && defined(__linux__) /* OpenOrbis gives it a Linux value */ flags = MSG_NOSIGNAL; #endif ssize_t n = send(fd, p, size, flags); @@ -86,14 +115,16 @@ static int c4fSendAll(int fd, const void *data, size_t size, uint64_t deadline) return 0; } -static int c4fHttp(const char *method, const char *path, char *body, size_t capacity) +/* One request on a connected socket (closed here); 1 with the body on 200 OK. */ +static int c4fExchange(int fd, const char *address, const char *method, const char *path, char *body, size_t capacity) { - int fd = c4fConnect(C4F_SERVICE_PORT); - if (fd < 0) return errno == ECONNREFUSED ? 0 : -1; - char request[256], response[2048]; - int n = snprintf(request, sizeof(request), "%s %s HTTP/1.1\r\nHost: 127.0.0.1:%d\r\nX-Control4Free-Launcher: 1\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", method, path, C4F_SERVICE_PORT); + char request[256], response[2048], what[48]; + int n = snprintf(request, sizeof(request), "%s %s HTTP/1.1\r\nHost: %s:%d\r\nX-Control4Free-Launcher: 1\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", method, path, address, C4F_SERVICE_PORT); uint64_t deadline = c4fLauncherTimeMs() + 1500; - if (c4fSendAll(fd, request, (size_t)n, deadline)) { close(fd); return -1; } + if (c4fSendAll(fd, request, (size_t)n, deadline)) { + snprintf(what, sizeof(what), "send errno %d", errno); + c4fNote(address, what); close(fd); return -1; + } size_t used = 0; int complete = 0; while (used < sizeof(response) - 1) { @@ -105,15 +136,42 @@ static int c4fHttp(const char *method, const char *path, char *body, size_t capa used += (size_t)count; } close(fd); response[used] = 0; + int code = 0; + if (!used) { c4fNote(address, "no reply"); return -1; } + if (sscanf(response, "HTTP/1.1 %d", &code) != 1) { c4fNote(address, "not HTTP"); return -1; } + if (code != 200) { snprintf(what, sizeof(what), "HTTP %d", code); c4fNote(address, what); return -1; } char *start = strstr(response, "\r\n\r\n"); - if (!complete || strncmp(response, "HTTP/1.1 200 OK\r\n", 17) || !start) return -1; + if (!complete || !start) { c4fNote(address, "reply cut off"); return -1; } start += 4; size_t length = used - (size_t)(start - response); - if (!length || length >= capacity || memchr(start, 0, length)) return -1; + if (!length || length >= capacity || memchr(start, 0, length)) { c4fNote(address, "bad reply"); return -1; } memcpy(body, start, length); body[length] = 0; return 1; } +/* 1: 200 OK with a body. 0: no address accepted a connection and at least one + * refused it, so nothing listens (the service binds every address). -1: any + * other outcome, explained in c4fProblem. */ +static int c4fHttp(const char *method, const char *path, char *body, size_t capacity) +{ + const char *targets[2]; + int count = c4fTargets(targets), refused = 0, connected = 0; + c4fProblem[0] = 0; + for (int i = 0; i < count; i++) { + int fd = c4fConnect(targets[i], C4F_SERVICE_PORT); + if (fd < 0) { + char what[40]; + if (errno == ECONNREFUSED) { refused++; snprintf(what, sizeof(what), "refused"); } + else snprintf(what, sizeof(what), "connect errno %d", errno); + c4fNote(targets[i], what); + continue; + } + connected++; + if (c4fExchange(fd, targets[i], method, path, body, capacity) == 1) return 1; + } + return !connected && refused ? 0 : -1; +} + static int c4fEquals(const char *json, const jsmntok_t *token, const char *value) { return token->end - token->start == (int)strlen(value) && !memcmp(json + token->start, value, strlen(value)); @@ -127,6 +185,7 @@ int c4fLauncherProbe(C4fServiceStatus *status) if (result <= 0) return result; jsmn_init(&parser); int count = jsmn_parse(&parser, body, strlen(body), tokens, 24); + snprintf(c4fProblem, sizeof(c4fProblem), "the status reply is not from Control4Free"); if (count < 1 || tokens[0].type != JSMN_OBJECT || tokens[0].end != (int)strlen(body)) return -1; int app = 0, api = 0, controllers = 0, stopping = 0; for (int i = 1; i + 1 < count; i += 2) { @@ -146,24 +205,28 @@ int c4fLauncherProbe(C4fServiceStatus *status) memcpy(status->version, body + value->start, (size_t)size); } } - return app && api && controllers && stopping ? 1 : -1; + if (!(app && api && controllers && stopping)) return -1; + c4fProblem[0] = 0; + return 1; } int c4fLauncherStart(const char *payload, char *message, size_t size) { C4fServiceStatus status; int probe = c4fLauncherProbe(&status); - if (probe == 1) { snprintf(message, size, "Already running. Scan the code to connect."); return 0; } - if (probe != 0) { snprintf(message, size, "Existing service detected. Stop it from the phone first."); return -1; } + if (probe == 1) { snprintf(message, size, "Already running. Open the address on your phone or PC."); return 0; } + if (probe != 0) { snprintf(message, size, "Another copy may be running (%s). Stop it on its controller page, or restart the PS4.", c4fProblem); return -1; } FILE *file = fopen(payload, "rb"); - if (!file) { snprintf(message, size, "Payload missing. Reinstall the Control4Free package."); return -1; } + if (!file) { snprintf(message, size, "The payload is missing. Reinstall the Control4Free package."); return -1; } unsigned char buffer[16384]; size_t first = fread(buffer, 1, sizeof(buffer), file); if (first < 64 || memcmp(buffer, "\177ELF", 4) || buffer[4] != 2 || buffer[5] != 1) { - fclose(file); snprintf(message, size, "Invalid bundled payload. Reinstall this package."); return -1; + fclose(file); snprintf(message, size, "The bundled payload is damaged. Reinstall the Control4Free package."); return -1; } - int fd = c4fConnect(C4F_PAYLOADER_PORT); - if (fd < 0) { fclose(file); snprintf(message, size, "Turn on GoldHEN's PayLoader, then press Cross again."); return -1; } + const char *targets[2]; + int count = c4fTargets(targets), fd = -1; + for (int i = 0; i < count && fd < 0; i++) fd = c4fConnect(targets[i], C4F_PAYLOADER_PORT); + if (fd < 0) { fclose(file); snprintf(message, size, "PayLoader did not answer. Turn it on in GoldHEN, then press Cross again."); return -1; } uint64_t deadline = c4fLauncherTimeMs() + 10000; int failed = c4fSendAll(fd, buffer, first, deadline); while (!failed) { @@ -172,15 +235,15 @@ int c4fLauncherStart(const char *payload, char *message, size_t size) failed = c4fSendAll(fd, buffer, count, deadline); } fclose(file); shutdown(fd, SHUT_WR); close(fd); - if (failed) { snprintf(message, size, "Transfer interrupted. Restart the PS4 before retrying."); return -2; } + if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Restart the PS4 before you try again."); return -2; } deadline = c4fLauncherTimeMs() + 20000; while (c4fLauncherTimeMs() < deadline) { if (c4fLauncherProbe(&status) == 1 && !status.stopping) { - snprintf(message, size, "Ready. Scan the code or open the address on your phone."); return 0; + snprintf(message, size, "Ready. Open the address on your phone or PC."); return 0; } usleep(250000); } - snprintf(message, size, "Sent, but no reply. Restart the PS4 before retrying."); + snprintf(message, size, "Sent, but Control4Free never answered (%s). Restart the PS4 before you try again.", c4fProblem); return -2; /* Caller locks further sends until restart, avoiding duplicates. */ } @@ -189,14 +252,14 @@ int c4fLauncherStop(char *message, size_t size) C4fServiceStatus status; int probe = c4fLauncherProbe(&status); if (probe == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; } - if (probe != 1) { snprintf(message, size, "Older service detected. Stop it from the phone."); return -1; } + if (probe != 1) { snprintf(message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; } char reply[256]; int ret = c4fHttp("POST", "/api/stop", reply, sizeof(reply)); - if (ret != 1) { snprintf(message, size, "Stop was not confirmed. Check the phone page."); return -1; } + if (ret != 1) { snprintf(message, size, "Stop was not confirmed (%s). Check the controller page.", c4fProblem); return -1; } uint64_t deadline = c4fLauncherTimeMs() + 5000; while (c4fLauncherTimeMs() < deadline) { if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; } usleep(100000); } - snprintf(message, size, "Still stopping. Wait before trying again."); return -1; + snprintf(message, size, "Still stopping. Wait a moment, then try again."); return -1; } diff --git a/launcher/service.h b/launcher/service.h index 1e9bbb4..93c04f8 100644 --- a/launcher/service.h +++ b/launcher/service.h @@ -9,8 +9,14 @@ extern "C" { enum { C4F_SERVICE_UNKNOWN = -1, C4F_SERVICE_DOWN = 0, C4F_SERVICE_RUNNING = 1 }; typedef struct { int controllers, stopping; char version[24]; } C4fServiceStatus; uint64_t c4fLauncherTimeMs(void); -/* DOWN is returned only for a refused local connection. Ambiguous responses - * must not authorize starting another copy in the shared payload host. */ +/* The console's own IPv4 address, when known. The app's sandbox may not reach + * 127.0.0.1, so it is the second way to reach Control4Free and PayLoader. */ +void c4fLauncherSetHost(const char *ipv4); +/* Why the last probe could not confirm the service, for the screen. */ +const char *c4fLauncherProblem(void); +/* DOWN only when nothing accepted a connection and something refused one. + * Ambiguous answers must not allow starting another copy in the shared + * payload host. */ int c4fLauncherProbe(C4fServiceStatus *status); int c4fLauncherStart(const char *payload, char *message, size_t size); int c4fLauncherStop(char *message, size_t size); diff --git a/src/net.c b/src/net.c index 733236d..4354b26 100644 --- a/src/net.c +++ b/src/net.c @@ -169,13 +169,17 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c) } if (statusRequest || stopRequest) { char launcher[8], length[16], transfer[32]; - /* Management is local to the console. Reject browser-origin requests - * and require a custom header, so a website cannot trigger shutdown. */ - if (!c->loopback || origin[0] || upgrade[0] || + /* For the launcher app. A website cannot reach this: it would need the + * custom header, which a browser only sends cross-origin after a CORS + * preflight that is never answered, and every browser request carrying + * an Origin is refused. The source address is not checked, because the + * app's sandbox may not appear as 127.0.0.1; devices on the network can + * already stop Control4Free from the page. */ + if (origin[0] || upgrade[0] || c4fHeader(request, "X-Control4Free-Launcher", launcher, sizeof(launcher)) != 1 || strcmp(launcher, "1") || c4fHeader(request, "Content-Length", length, sizeof(length)) < 0 || (length[0] && strcmp(length, "0")) || c4fHeader(request, "Transfer-Encoding", transfer, sizeof(transfer)) != 0) { - c4fHttpError(c, 403, "Local launcher only"); return; + c4fHttpError(c, 403, "Launcher only"); return; } c->rxUsed = 0; net->handler(c, stopRequest ? C4F_NET_HTTP_STOP : C4F_NET_HTTP_STATUS, NULL, 0, net->context); @@ -296,7 +300,6 @@ void c4fNetPoll(C4fNet *net, int timeoutMs) setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); #endif net->clients[index].fd = fd; net->clients[index].openedMs = c4fTimeMs(); - net->clients[index].loopback = (ntohl(peer.sin_addr.s_addr) >> 24) == 127; } } } diff --git a/tools/launcher_art.c b/tools/launcher_art.c index 4832223..f98e105 100644 --- a/tools/launcher_art.c +++ b/tools/launcher_art.c @@ -1,10 +1,11 @@ /* Renders launcher artwork on the build machine with the launcher's own drawing * code: the package icon, and screen states for review. Writes raw RGBA. * launcher_art icon - * launcher_art screen */ + * launcher_art screen */ #include #include #include +#include "autorun.h" #include "screen.h" static int c4fWrite(const char *path, const uint32_t *pixels, size_t count) @@ -34,17 +35,24 @@ int main(int argc, char **argv) memset(&s, 0, sizeof(s)); snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264"); const char *state = argv[2], *message = ""; - if (!strcmp(state, "running") || !strcmp(state, "confirm")) { + s.autorun = C4F_AUTORUN_ON; + if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated")) { s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm"); - message = "Ready. Scan the code or open the address on your phone."; + if (!strcmp(state, "outdated")) s.autorun = C4F_AUTORUN_OUTDATED; + message = "Ready. Open the address on your phone or PC."; + } else if (!strcmp(state, "setup")) { + s.autorun = C4F_AUTORUN_OFF; message = "Not running."; } else if (!strcmp(state, "stopped")) { - message = "Not running. Press Cross to start it."; + message = "Stopped. Press Cross to start it again."; } else if (!strcmp(state, "busy")) { - s.running = -1; s.busy = 1; message = "Starting Control4Free. Please wait..."; + s.running = -1; s.busy = 1; s.autorun = C4F_AUTORUN_UNKNOWN; message = "Setting up Control4Free. Please wait..."; } else if (!strcmp(state, "locked")) { - s.running = -1; s.locked = 1; message = "Sent, but no reply. Restart the PS4 before retrying."; + s.running = -1; s.locked = 1; + message = "Sent, but Control4Free never answered (127.0.0.1 no reply). Restart the PS4 before you try again."; } else if (!strcmp(state, "unknown")) { - s.running = -1; message = "Existing service detected. Stop it from the phone first."; + s.running = -1; s.autorun = C4F_AUTORUN_UNKNOWN; + snprintf(s.autorunNote, sizeof(s.autorunNote), "GoldHEN did not let the app check (errno 78)"); + message = "No answer the app understands: 127.0.0.1 refused; 192.168.1.20 HTTP 404"; } else { return 2; }