mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-27 13:01:30 +01:00
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:
1 parent
b3863fa5f1
commit
0fe839ad2e
15 files changed
+598
-107
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user