Launcher: set up GoldHEN auto-start itself; reach Control4Free from the app

- The first Cross copies the bundled payload to /data/payloads and adds it
  to GoldHEN's AutoRun list, then starts it if PayLoader is on. Triangle
  turns auto-start on or off, or updates an older copy. The app leaves its
  sandbox for this through GoldHEN's SDK call and returns right after.
- The app tries 127.0.0.1 and then the console's own address, for both
  Control4Free and PayLoader, and shows why neither answered.
- The launcher API no longer requires a loopback source address, since
  the app's sandbox may not appear as 127.0.0.1. The custom header and
  the Origin check still keep websites out.
- The screen and messages no longer assume a phone.
This commit is contained in:
MoHadiShibli committed 2026-10-04 21:12:51 +03:00
1 parent b3863fa5f1
commit 0fe839ad2e
15 files changed
+598 -107

No files matched your search

+34 -34
View File
@@ -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
+3
View File
@@ -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`:
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+275
View File
@@ -0,0 +1,275 @@
/* GoldHEN AutoRun for the bundled payload. */
#include "autorun.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* 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 "<path> = 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;
}
+24
View File
@@ -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 <stddef.h>
#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
+21
View File
@@ -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)
+1
View File
@@ -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);
+66 -17
View File
@@ -1,4 +1,5 @@
/* Native PS4 launcher. The controller service runs outside this application. */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -10,46 +11,81 @@
#include <orbis/SystemService.h>
#include <orbis/UserService.h>
#include <orbis/VideoOut.h>
#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;
+52 -15
View File
@@ -6,6 +6,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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";
+2 -1
View File
@@ -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" {
+87 -24
View File
@@ -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;
}
+8 -2
View File
@@ -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);
+8 -5
View File
@@ -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;
}
}
}
+15 -7
View File
@@ -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 <size> <out.rgba>
* launcher_art screen <running|stopped|busy|confirm|locked|unknown> <out.rgba> */
* launcher_art screen <setup|running|stopped|outdated|confirm|busy|locked|unknown> <out.rgba> */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}