9 Commits
Author SHA1 Message Date
MoHadiShibli 017b8e2473 Docs: GoldHEN v2.4b18.10 is the minimum, and a compatibility table
GoldHEN versions before v2.4b18.10 don't run ELF payloads reliably (v2.4b18.8
improved ELF loading, v2.4b18.10 fixed a sceUserServiceInitialize failure in
them) and have no auto-start, and every firmware GoldHEN supports has a
v2.4b18.10 or later. The installation guide now lists each firmware with the
GoldHEN version it needs, marking the ones confirmed on a console (10.01,
13.02, 13.04, 13.52) apart from those that only have GoldHEN and start-up
support so far. The README points to it.
2026-10-07 02:21:39 +03:00
MoHadiShibli 96326c71b0 Docs: tested firmwares, and what to do when updating, restarting or reporting
- Requirements list the firmwares Control4Free is tested on (10.01, 13.02,
  13.04 and 13.52, with GoldHEN v2.4b18.10 and v2.4b18.12); 13.52 needs 1.1.1.
- Updating says you don't need to stop Control4Free before deleting or
  replacing the app. A new "After a restart" section says auto-start brings it
  back by itself and the app doesn't get in the way.
- Uninstalling warns that deleting the app alone leaves Control4Free starting
  with GoldHEN, and how to clean up when the app is already gone.
- Reporting a problem stops the normal version before starting the diagnostic
  one, so the report comes from the diagnostic version, and says how to switch
  back.
- The kernel-log message also names old test builds of the diagnostic app.
2026-10-07 02:03:52 +03:00
MoHadiShibli f57224ee39 Control4Free 1.1.1 2026-10-07 01:39:35 +03:00
MoHadiShibli 9d0c3d6a7d Ship a diagnostic package with every release, and ask for it in bug reports
CI now builds Control4Free-<version>-diag.pkg next to the normal package and
attaches it to each release. The bug report form asks for its screenshots or
report file instead of an optional log, and the troubleshooting guide's
"Reporting a problem" section walks through it: install the diagnostic
package, make the problem happen again, screenshot the 5 pages, then put the
normal package back.
2026-10-07 01:39:35 +03:00
MoHadiShibli b583d149d8 Diagnostic app: never hold the kernel log while Control4Free runs
The app read the kernel log in short windows: when it opened, around a
start, and on Options. The log has one reader, and Control4Free needs it to
sign a controller in. A tester on 13.52 opened the app after a restart and
went back to the home screen while it was reading: the PS4 suspended the app
with the log still open, and no controller could sign in until the app was
closed. Now the app reads the log only while Control4Free is not running,
which is the only time it is useful, and stops as soon as a start succeeds.
2026-10-07 01:39:34 +03:00
MoHadiShibli eceeb7bf46 Diagnostic build: log each connection and each notification
On firmware 13.52 the service now starts and answers the launcher, but a
browser on the network gets its connection closed, and no notification
appears. The diagnostic build now logs, for every connection from another
device, what it asked for, what it was sent, and why it was closed (send or
recv errors with errno, the 10-second timeout with the first byte received),
plus the result of every notification request. The diagnostic screen shows
its build time and, while Control4Free runs, asks for the address to be
opened and page 2 screenshotted. It also reads GoldHEN's SDK version
correctly: GoldHEN returns it through the call's error value.
2026-10-06 02:37:51 +03:00
MoHadiShibli 056a32f6c9 Let Cross retry a failed start instead of asking for a restart
When Control4Free was sent but did not answer, the app locked itself until
the PS4 restarted, so two copies could never run. The payload already takes
care of that: a second copy finds the instance lock and quits at once. So
the app now says to press Cross to try again, and a restart never helped a
payload that cannot start anyway. The lock stays only for the app itself
failing to start ("Reopen the app").
2026-10-06 02:07:00 +03:00
MoHadiShibli 745c16decf Diagnostic build: a package that shows why Control4Free does not start
For consoles it fails on, so a user can send screenshots instead of digging
out log files. `make C4F_DIAG=1` and `make -C launcher C4F_DIAG=1` (or
tools/build-launcher.ps1 -Diag) build Control4Free-<version>-diag.pkg, listed
as "Control4Free (diagnostic)".

Its app shows plain pages in place of the normal screen: a summary with a
verdict (firmware, GoldHEN, sandbox, auto-start files, the service and the
last start), the service log and the previous one, and the kernel log,
filtered and in full. The kernel log is read only in short windows, at
start-up, around a start and on Options, since Control4Free needs it to sign
controllers in. The pages are also saved to
/data/control4free/diag-report.txt, and to a USB stick when one is plugged in.

The payload says on screen that it started, and logs the kernel addresses
the SDK found and the process PayLoader runs it in.
2026-10-06 02:06:53 +03:00
MoHadiShibli 94e4a7ecd7 Build: take the payload's start-up code from a newer SDK, for firmware 13.52
The ps4-payload-sdk v0.9 release has no kernel offsets for firmware 13.52 or
14.00, so on those consoles the payload quit in its start-up code before
main(): PayLoader reported it launched, but no log or service ever appeared.
The SDK added both on master (573b4a0); crt1.o is rebuilt from that commit
with the SDK's own crt/Makefile. It exports the same symbols as v0.9's.
2026-10-06 02:06:36 +03:00
26 changed files with 991 additions and 65 deletions

No files matched your search

+6 -5
View File
@@ -40,10 +40,11 @@ body:
validations:
required: true
- type: textarea
id: log
id: diagnostics
attributes:
label: Log (optional)
label: Diagnostic report
description: |
`/data/control4free/control4free.log` on the PS4, over GoldHEN's FTP server (port 2121). If the problem was before a restart, it's in `control4free.log.previous`.
The log contains your console's network address: remove it if you prefer.
render: text
Install the diagnostic package, `Control4Free-<version>-diag.pkg` from the [latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest), over the app. Open **Control4Free (diagnostic)**, make the problem happen again, then attach a screenshot of each of its 5 pages here (**R1** changes the page). Or attach the report it saves: `control4free-diag.txt` on a USB stick that was plugged in, or `/data/control4free/diag-report.txt` over GoldHEN's FTP server. [Step by step](https://github.com/MoHadiShibli/Control4Free/blob/main/docs/troubleshooting.md#reporting-a-problem).
The report shows your console's local network address. It only works inside your home network, but you can blur it.
validations:
required: true
+6 -5
View File
@@ -1,7 +1,8 @@
name: build
# Builds both toolchain images, the payload, the installable package, and runs
# every host test. On a vX.Y.Z tag it also publishes the release.
# Builds both toolchain images, the payload, the installable package and its
# diagnostic version, and runs every host test. On a vX.Y.Z tag it also
# publishes the release.
on:
push:
branches: [main, next]
@@ -23,10 +24,10 @@ jobs:
docker build -t control4free-build -f docker/Dockerfile docker
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker
- name: Build the payload and the package
- name: Build the payload, the package and the diagnostic package
run: |
docker run --rm --network none -v "$PWD:/src" -w /src \
control4free-launcher-build bash -lc 'make && make -C launcher'
control4free-launcher-build bash -lc 'make && make -C launcher && make C4F_DIAG=1 && make -C launcher C4F_DIAG=1'
- name: Host tests
run: |
@@ -37,7 +38,7 @@ jobs:
run: |
version=$(cat VERSION)
mkdir -p artifacts
cp build/control4free.elf "build/Control4Free-$version.pkg" artifacts/
cp build/control4free.elf "build/Control4Free-$version.pkg" "build/Control4Free-$version-diag.pkg" artifacts/
cd artifacts && sha256sum * > SHA256SUMS && cat SHA256SUMS
- uses: actions/upload-artifact@v4
+13
View File
@@ -3,6 +3,19 @@
Versions follow [Semantic Versioning](https://semver.org): PATCH for fixes, MINOR for features that break
nothing, MAJOR for anything people rely on that changes.
## 1.1.1 — 2026-10-07
Fixes.
- Control4Free starts on firmware 13.52. Before, it stopped before doing anything on that firmware:
GoldHEN said the payload launched, but the app said it never answered. The payload's start-up code now
comes from a newer ps4-payload-sdk that knows 13.52's kernel.
- When a start fails, press **Cross** to try again. The app no longer asks you to restart the PS4: a second
copy of Control4Free quits on its own, so two can't end up running.
- Each release now includes a diagnostic package, `Control4Free-<version>-diag.pkg`. Its app shows what the
PS4 reports (firmware, GoldHEN, the logs, each connection from a phone or PC) on pages you can screenshot
for a bug report. See [Reporting a problem](docs/troubleshooting.md#reporting-a-problem).
## 1.1.0 — 2026-10-06
Rumble and the light bar.
+10
View File
@@ -3,6 +3,7 @@
# Build:
# make # build/control4free.elf
# make C4F_PROBE=1 # plus the rumble/light-bar research probe (dev only)
# make C4F_DIAG=1 # diagnostic build for a console it fails on
# make clean
#
# A build needs the ps4-payload-sdk; docker/Dockerfile has it, and
@@ -29,6 +30,15 @@ ifdef C4F_PROBE
OBJDIR := build/probe
endif
# Diagnostic build: logs what the SDK found in the kernel and the host process,
# and says on screen that it started. The launcher's C4F_DIAG build carries it.
ifdef C4F_DIAG
EXTRA_CFLAGS += -DC4F_DIAG
VERSION := $(VERSION)-diag
ELF := build/$(NAME)-diag.elf
OBJDIR := build/diag
endif
OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o) $(OBJDIR)/client.o $(OBJDIR)/assets.o $(OBJDIR)/qrcodegen.o
CFLAGS += -std=gnu11 -Wall -Wextra -Wpointer-arith -g -O2
+7 -4
View File
@@ -82,8 +82,9 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
## Requirements and compatibility
- A PS4 with GoldHEN. Tested on firmware 10.01 with GoldHEN v2.4b18.10. Auto-start and starting Control4Free
from GoldHEN's menu need v2.4b18.10 or later.
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 13.02, 13.04 and 13.52; the
[compatibility table](docs/installation.md#compatibility) lists every firmware and the GoldHEN version it
needs.
- A phone, tablet or computer with a browser, on the same network as the PS4.
- Turn off other GoldHEN controller plugins in games you play with Control4Free. A plugin that takes over a
signed-in user's controller can stop games from reading the virtual ones.
@@ -104,8 +105,10 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
## Support
Found a bug? [Open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose); I maintain this
project and read every report. If Control4Free is useful to you, you can help keep it going on
Found a bug? Install the diagnostic package from the latest release first, then
[open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) with its screenshots (see
[Reporting a problem](docs/troubleshooting.md#reporting-a-problem)). I maintain this project and read every
report. If Control4Free is useful to you, you can help keep it going on
[Ko-fi](https://ko-fi.com/mohadishibli).
## Credits
+1 -1
View File
@@ -1 +1 @@
1.1.0
1.1.1
+11
View File
@@ -26,6 +26,17 @@ RUN curl -fsSL -o /tmp/sdk.zip \
&& rm /tmp/sdk.zip \
&& chmod -R +x /opt/ps4-payload-sdk/bin
# The payload's start-up code (crt1.o) comes from a later SDK commit than the
# release: v0.9 has no kernel offsets for firmware 13.52 or 14.00, so a payload
# built with it quits before main() on those consoles. Same exports, built with
# the SDK's own crt/Makefile.
ARG SDK_CRT_COMMIT=573b4a0d0d5fa8695cfeee5f5e2f0447c550035d
RUN mkdir /tmp/sdk-src \
&& curl -fsSL "https://github.com/ps4-payload-dev/sdk/archive/${SDK_CRT_COMMIT}.tar.gz" \
| tar xz -C /tmp/sdk-src --strip-components=1 \
&& make -C /tmp/sdk-src/crt LLVM_CONFIG=llvm-config-18 DESTDIR=/opt/ps4-payload-sdk install \
&& rm -rf /tmp/sdk-src
# orbis-clang execs $SCRIPT_DIR/clang, and orbis-ld execs $SCRIPT_DIR/ld.lld.
RUN ln -sf /usr/bin/clang-18 /opt/ps4-payload-sdk/bin/clang \
&& ln -sf /usr/bin/clang++-18 /opt/ps4-payload-sdk/bin/clang++ \
+40 -6
View File
@@ -10,11 +10,32 @@ Control4Free comes in two pieces:
## Requirements
- A PS4 with [GoldHEN](https://github.com/GoldHEN/GoldHEN). Tested on firmware 10.01 with GoldHEN v2.4b18.10.
Auto-start and GoldHEN's Payloader LaunchPad need v2.4b18.10 or later.
- A PS4 with [GoldHEN](https://github.com/GoldHEN/GoldHEN) v2.4b18.10 or later. Older GoldHEN versions don't
run ELF payloads like Control4Free reliably, and have no auto-start. See [Compatibility](#compatibility) for
each firmware.
- **Debug Settings** turned on in GoldHEN, for the Package Installer.
- A phone, tablet or computer with a modern browser, on the same network as the PS4.
## Compatibility
Every firmware GoldHEN supports has a GoldHEN version of v2.4b18.10 or later, so updating GoldHEN is all it
takes. **Tested** means someone confirmed it on a console; the other rows have GoldHEN support and support in
Control4Free's start-up code, but nobody has tried them yet.
| Firmware | GoldHEN | Status |
|---|---|---|
| 10.01 | v2.4b18.10 or later | **Tested** (v2.4b18.10) |
| 13.02, 13.04 | v2.4b18.12 or later | **Tested** (v2.4b18.12) |
| 13.52 | v2.4b18.11 or later | **Tested** (v2.4b18.12), with Control4Free 1.1.1 or later |
| 13.50 | v2.4b18.12 or later | Not tested |
| 13.00 | v2.4b18.10 or later | Not tested |
| 12.00, 12.02, 12.50, 12.52 | v2.4b18.10 or later | Not tested |
| 5.05, 6.71, 6.72, 7.0x, 7.5x, 8.0x, 8.5x, 9.0x, 9.5x, 9.60, 10.00, 10.50, 10.70, 10.71, 11.0x, 11.5x | v2.4b18.10 or later | Not tested |
The older a firmware, the less certain it is that the PS4's virtual-controller API behaves as on the tested
ones. If you try an untested firmware, an issue saying whether it worked helps others, with a
[diagnostic report](troubleshooting.md#reporting-a-problem) if it didn't.
## Install the app
1. Download `Control4Free-<version>.pkg` from the
@@ -34,19 +55,25 @@ Open **Control4Free** and press **Cross**. The app:
1. copies the service to `/data/payloads/control4free.elf`;
2. adds it to GoldHEN's AutoRun list, `/data/GoldHEN/payloads.ini`, so GoldHEN starts it every time it
loads;
3. starts it straight away if GoldHEN's **PayLoader** is on. If it isn't, restart the PS4 and run the
jailbreak: AutoRun starts Control4Free from then on.
3. starts it straight away if GoldHEN's **PayLoader** is on. If it isn't, turn PayLoader on in GoldHEN's
settings and press **Cross** again. AutoRun also starts Control4Free each time GoldHEN loads.
Once it's running, the app shows the address to open, for example `http://192.168.1.20:4264`, and a QR code
for your phone's camera. The PS4 also shows the address in a notification when the service starts.
AutoRun keeps other entries in `payloads.ini` as they are.
## After a restart
Run the GoldHEN jailbreak as usual. With auto-start on, Control4Free starts by itself and shows its address in
a notification, so you don't need to open the app. If you do open it and press **PS** to go back to the home
screen, that's fine too: the app doesn't get in the way of Control4Free.
## The app's buttons
| Button | What it does |
|---|---|
| **Cross** | Starts Control4Free when it isn't running. Until auto-start is on, it sets that up first. |
| **Cross** | Starts Control4Free when it isn't running. Until auto-start is on with this app's version, it sets that up first. |
| **Triangle** | Turns auto-start on or off. After you install a newer package, it updates the auto-start copy. |
| **Square**, then **Cross** | Stops Control4Free and disconnects every controller. |
| **Circle** | Closes the app. Control4Free keeps running without it. |
@@ -54,10 +81,13 @@ AutoRun keeps other entries in `payloads.ini` as they are.
## Updating
1. Install the new package over the old one. Delete the old app first if the installer refuses.
You don't need to stop Control4Free first: deleting or replacing the app leaves the running service and
its auto-start copy alone.
2. Open Control4Free. It says the auto-start copy is older than the one in the app: press **Triangle** to
update it.
3. The old version is still the one running. Press **Square**, then **Cross** to stop it, and **Cross**
again to start the new one (this needs GoldHEN's PayLoader). Or restart the PS4 and run the jailbreak.
again to start the new one (this needs GoldHEN's PayLoader). Or restart the PS4 and run the jailbreak:
auto-start then starts the new one.
Your controller layouts and keys live in each phone's browser, so they survive updates.
@@ -86,6 +116,10 @@ It runs until the PS4 restarts.
2. Delete the app from the Library.
3. Delete `/data/payloads/control4free.elf`, and `/data/control4free/` if you want the logs gone too.
Do step 1 before deleting the app. Deleting the app alone doesn't stop the service, and GoldHEN keeps
starting it after every restart. If the app is already gone, either install it again and start from step 1,
or remove the `control4free.elf` line from `/data/GoldHEN/payloads.ini` by hand and restart the PS4.
## Files on the console
| Path | What it is |
+37 -11
View File
@@ -1,14 +1,16 @@
# Troubleshooting
Find the message you see, or the symptom, below. If nothing here helps,
[open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) with the log described at the
end of this page.
[open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) with a diagnostic report, as
described at the [end of this page](#reporting-a-problem).
## On the controller page
**"Signing a controller in needs the PS4's kernel log, and something else has it."**
A klog viewer is connected to GoldHEN's log server (port 3232). Close it and pick the controller again. Only
signing a controller in needs the log, so this never interrupts play.
A klog viewer is connected to GoldHEN's log server (port 3232). Close it and pick the controller again. A
diagnostic app from a test build before 1.1.1 can also hold the log while it sits in the background: close it
(press **PS**, highlight the app, press **OPTIONS**, choose **Close Application**) and update. Only signing a
controller in needs the log, so this never interrupts play.
**"Another controller is being connected; try again in a moment."**
Controllers are added one at a time. Wait a second and pick yours again. Everyone already playing carries on
@@ -91,8 +93,9 @@ run the jailbreak: with auto-start on, Control4Free starts by itself.
**"Control4Free is not responding."** Wait a few seconds after the PS4 wakes from rest mode, then press
**Cross** again. If it stays stuck, restart the PS4 and run the jailbreak.
**"Sent, but Control4Free never answered."** or **"The transfer to PayLoader broke off."** Restart the PS4
before trying again. The app won't send a second copy until then, so two can't end up running.
**"Sent, but Control4Free did not start."** or **"The transfer to PayLoader broke off."** Press **Cross** to
try again. Two copies can't end up running: a second one quits at once. If it never starts, open an issue
with your firmware and GoldHEN versions.
**"Something answers on port 4264 but not the way this app expects."** or **"No answer the app
understands."** Something else is answering on port 4264, often an older version of Control4Free. Stop it
@@ -106,10 +109,33 @@ the app and open it again; if that doesn't help, restart the PS4 and run the jai
**Control4Free is listed under Games instead of Applications.** That's a package from before 1.0.0. Delete the
app and install the current package.
## The log
## Reporting a problem
The service writes a log to `/data/control4free/control4free.log` on the PS4, and keeps the previous run in
`control4free.log.previous`. Turn on GoldHEN's FTP server and download them from port 2121, or connect to
GoldHEN's log server on port 3232 and look for lines starting with `[c4f]`.
Every release comes with a diagnostic package, `Control4Free-<version>-diag.pkg`. It's the same Control4Free,
except that its app shows on screen what the PS4 reports, so you can send screenshots instead of digging out
log files. Please use it before you open an issue:
The log contains your console's network address. Remove it before posting the log if you prefer.
1. Download `Control4Free-<version>-diag.pkg` from the
[latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest) and install it over the app.
It replaces the app and shows up as **Control4Free (diagnostic)**.
2. Open it. If it says RUNNING, the normal version is still the one running: press **Square**, then **Cross**
to stop it. Then press **Cross** to start the diagnostic version, and wait until the top stops saying
"WORKING" (up to 30 seconds).
3. Make the problem happen again, for example open the page on your phone or pick a controller. Then go back
to the app.
4. Press **R1** to go through its 5 pages, and take a screenshot of each one: press **SHARE**, then
**Triangle**. To copy them to a USB stick, open **Capture Gallery**, press **OPTIONS** and choose **Copy
to USB Storage Device**.
5. [Open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) and attach the
screenshots.
The app also saves the same report as text: `control4free-diag.txt` on a USB stick that was plugged in, and
`/data/control4free/diag-report.txt` on the PS4. You can attach that instead of the screenshots. The report
shows your console's local network address. It only works inside your home network, but you can blur it.
When you're done, install the normal package again over the diagnostic one, open it, press **Square**, then
**Cross** to stop the diagnostic version, and **Cross** to start the normal one. That also puts the normal
one back on auto-start.
The service's own log is `/data/control4free/control4free.log` on the PS4, with the previous run in
`control4free.log.previous`, readable over GoldHEN's FTP server (port 2121). The diagnostic app shows both.
+3
View File
@@ -15,6 +15,9 @@ typedef struct C4fNetClient {
const unsigned char *body;
size_t bodySize, bodySent;
void *user;
#ifdef C4F_DIAG
char peer[16]; /* the other end's address, for the diagnostic log */
#endif
} C4fNetClient;
enum { C4F_NET_OPEN, C4F_NET_MESSAGE_EVENT, C4F_NET_CLOSE,
+15 -4
View File
@@ -4,12 +4,23 @@ endif
SDK := $(OO_PS4_TOOLCHAIN)
VERSION := $(shell cat ../VERSION)
OUT := ../build/launcher
PAYLOAD := ../build/control4free.elf
OBJECTS :=
# Diagnostic build: pages of everything that says why Control4Free does not
# start, in place of the normal screen, with the diagnostic payload inside.
ifdef C4F_DIAG
VERSION := $(VERSION)-diag
OUT := ../build/launcher-diag
PAYLOAD := ../build/control4free-diag.elf
DIAG_FLAGS := -DC4F_DIAG
OBJECTS += $(OUT)/diag.o
endif
PKGDIR := $(OUT)/pkg
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 -DC4F_LAUNCHER_VERSION='"$(VERSION)"'
OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
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 -DC4F_LAUNCHER_VERSION='"$(VERSION)"' $(DIAG_FLAGS)
OBJECTS += $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.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
@@ -34,5 +45,5 @@ $(OUT)/fonts.o: $(OUT)/fonts.c
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
$(OUT)/launcher.elf: $(OBJECTS)
$(LD) -m elf_x86_64 -pie --script $(SDK)/link.x --eh-frame-hdr -o $@ $(OBJECTS) -L$(SDK)/lib $(LIBS) $(SDK)/lib/crt1.o
package: $(OUT)/launcher.elf ../build/control4free.elf
python3 ../tools/package_launcher.py --sdk $(SDK)
package: $(OUT)/launcher.elf $(PAYLOAD)
python3 ../tools/package_launcher.py --sdk $(SDK) $(if $(C4F_DIAG),--diag)
+616
View File
@@ -0,0 +1,616 @@
/* Diagnostic build of the launcher: see diag.h.
*
* Everything is shown on pages a user can screenshot, and also saved to
* /data/control4free/diag-report.txt (and a USB stick when one is plugged in).
* The kernel log has a single reader, and Control4Free needs it to sign a
* controller in. So the app reads it only in short windows, and never while
* Control4Free runs: an app suspended in the background keeps its descriptor
* open, and every controller would then fail to sign in (seen on 13.52). */
#include "diag.h"
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <orbis/libkernel.h>
#include "autorun.h"
#include "draw.h"
#include "sandbox.h"
#include "service.h"
#define C4F_DIAG_DIR "/user/data/control4free"
#define C4F_DIAG_LOG C4F_DIAG_DIR "/control4free.log"
#define C4F_DIAG_REPORT C4F_DIAG_DIR "/diag-report.txt"
#define C4F_DIAG_KLOG C4F_DIAG_DIR "/diag-klog.txt"
#define C4F_DIAG_PAYLOAD "/user/data/payloads/control4free.elf"
#define C4F_DIAG_INI "/user/data/GoldHEN/payloads.ini"
#define C4F_LOG_TAIL (64 * 1024)
#define C4F_KLOG_KEEP 1000
#define C4F_KLOG_WIDTH 192
#define C4F_KLOG_FILE_MAX (4u << 20)
#define C4F_D_BG C4F_RGBA(6, 14, 34, 1)
#define C4F_D_TEXT C4F_RGBA(235, 240, 255, 1)
#define C4F_D_FAINT C4F_RGBA(160, 175, 205, 1)
#define C4F_D_ACCENT C4F_RGBA(90, 160, 255, 1)
#define C4F_D_WARN C4F_RGBA(255, 208, 77, 1)
#define C4F_D_LINE C4F_RGBA(255, 255, 255, .2f)
#define C4F_D_SIZE 20.0f
#define C4F_D_TITLE 28.0f
#define C4F_D_ROW 24
#define C4F_D_LEFT 40
#define C4F_D_TOP 186
#define C4F_D_BOTTOM 1052
static const char *const c4fPageNames[C4F_DIAG_PAGES] = {
"Summary", "Control4Free log", "Previous Control4Free log", "Kernel log (filtered)", "Kernel log (everything)",
};
static pthread_mutex_t c4fLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_t c4fKlogReader;
static int c4fReaderStarted, c4fQuit;
static uint64_t c4fBootMs, c4fCaptureUntil;
static int c4fServiceUp, c4fRefreshed;
/* Pages, rebuilt by the launcher's worker and drawn by its main loop. */
static char *c4fPages[C4F_DIAG_PAGES];
static unsigned c4fGeneration = 1;
static int c4fMaxScroll[C4F_DIAG_PAGES];
/* What start-up found. */
static char c4fFirmware[96], c4fGoldHen[96], c4fSandbox[96], c4fBundled[96];
/* The kernel log: the newest lines, and how reading it went. */
static char c4fKlog[C4F_KLOG_KEEP][C4F_KLOG_WIDTH];
static unsigned c4fKlogTotal, c4fKlogOpens;
static int c4fKlogError;
static unsigned long long c4fKlogBytes;
/* The last start. */
static int c4fStarted, c4fStartResult, c4fLogChanged;
static uint64_t c4fStartedAt, c4fStartTook;
static char c4fStartMessage[192];
static char *c4fLogBefore;
static uint64_t c4fLastReport;
static char c4fReportNote[160];
/* ---- helpers ---- */
typedef struct { char *data; size_t used, size; } C4fText;
static void c4fAdd(C4fText *t, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void c4fAdd(C4fText *t, const char *fmt, ...)
{
for (;;) {
size_t room = t->data ? t->size - t->used : 0;
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(t->data ? t->data + t->used : NULL, room, fmt, ap);
va_end(ap);
if (n < 0) return;
if (t->data && (size_t)n < room) { t->used += (size_t)n; return; }
size_t grown = t->size ? t->size * 2 : 4096;
while (grown < t->used + (size_t)n + 1) grown *= 2;
char *next = realloc(t->data, grown);
if (!next) return;
t->data = next; t->size = grown;
}
}
static char *c4fTake(C4fText *t) { return t->data ? t->data : strdup(""); }
/* The last `limit` bytes of a file, NUL-terminated, NULs shown as spaces;
* NULL with errno when it cannot be read. */
static char *c4fReadTail(const char *path, size_t limit, long long *total)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return NULL;
off_t size = lseek(fd, 0, SEEK_END);
off_t start = size > (off_t)limit ? size - (off_t)limit : 0;
if (size < 0 || lseek(fd, start, SEEK_SET) < 0) { int saved = errno; close(fd); errno = saved; return NULL; }
size_t want = (size_t)(size - start), used = 0;
char *data = malloc(want + 1);
if (!data) { close(fd); errno = ENOMEM; return NULL; }
while (used < want) {
ssize_t n = read(fd, data + used, want - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
used += (size_t)n;
}
close(fd);
for (size_t i = 0; i < used; i++) if (!data[i]) data[i] = ' ';
data[used] = 0;
if (total) *total = (long long)size;
return data;
}
static uint32_t c4fCrc32Update(uint32_t crc, const unsigned char *data, size_t size)
{
crc = ~crc;
for (size_t i = 0; i < size; i++) {
crc ^= data[i];
for (int bit = 0; bit < 8; bit++) crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1)));
}
return ~crc;
}
static uint32_t c4fCrc32(const unsigned char *data, size_t size) { return c4fCrc32Update(0, data, size); }
/* A file's size and CRC-32; -1 with errno when it cannot be read. */
static int c4fCrcFile(const char *path, size_t *size, uint32_t *crc)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
unsigned char buf[16384];
*size = 0; *crc = 0;
for (;;) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 0 && errno == EINTR) continue;
if (n < 0) { int saved = errno; close(fd); errno = saved; return -1; }
if (n == 0) break;
*crc = c4fCrc32Update(*crc, buf, (size_t)n);
*size += (size_t)n;
}
close(fd);
return 0;
}
static const char *c4fLastLine(const char *text, char *out, size_t size)
{
const char *end = text + strlen(text);
while (end > text && (end[-1] == '\n' || end[-1] == '\r' || end[-1] == ' ')) end--;
const char *start = end;
while (start > text && start[-1] != '\n') start--;
snprintf(out, size, "%.*s", (int)(end - start), start);
return out;
}
static int c4fContains(const char *haystack, const char *needle)
{
size_t n = strlen(needle);
for (; *haystack; haystack++) {
size_t i = 0;
while (i < n && haystack[i] && (haystack[i] | 0x20) == (needle[i] | 0x20)) i++;
if (i == n) return 1;
}
return 0;
}
static void c4fWriteAll(const char *path, const char *text, int append)
{
int fd = open(path, O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC), 0666);
if (fd < 0) return;
size_t size = strlen(text);
while (size) {
ssize_t n = write(fd, text, size);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
text += n; size -= (size_t)n;
}
close(fd);
}
/* ---- the kernel log ---- */
/* One line, without terminal colour codes (GoldHEN's lines carry them). */
static void c4fKlogAdd(const char *line)
{
char clean[C4F_KLOG_WIDTH];
size_t used = 0;
for (const char *p = line; *p && used + 1 < sizeof(clean); p++) {
if (*p == '\033') {
if (p[1] == '[') { p += 2; while (*p && !(*p >= '@' && *p <= '~')) p++; if (!*p) break; }
continue;
}
if (*p != '\r') clean[used++] = *p;
}
clean[used] = 0;
pthread_mutex_lock(&c4fLock);
snprintf(c4fKlog[c4fKlogTotal % C4F_KLOG_KEEP], C4F_KLOG_WIDTH, "%s", clean);
c4fKlogTotal++;
pthread_mutex_unlock(&c4fLock);
}
static void *c4fKlogThread(void *unused)
{
(void)unused;
int fd = -1;
size_t used = 0, written = 0;
char line[C4F_KLOG_WIDTH * 2];
for (;;) {
pthread_mutex_lock(&c4fLock);
int quit = c4fQuit, want = !c4fServiceUp && c4fLauncherTimeMs() < c4fCaptureUntil;
pthread_mutex_unlock(&c4fLock);
if (quit) break;
if (want && fd < 0) {
fd = open("/dev/klog", O_RDONLY | O_NONBLOCK);
int error = fd < 0 ? errno : 0;
if (fd >= (int)FD_SETSIZE) { close(fd); fd = -1; error = EMFILE; }
pthread_mutex_lock(&c4fLock);
c4fKlogError = error;
if (fd < 0) c4fCaptureUntil = 0; else c4fKlogOpens++;
pthread_mutex_unlock(&c4fLock);
continue;
}
if (!want && fd >= 0) {
close(fd); fd = -1;
if (used) { line[used] = 0; c4fKlogAdd(line); used = 0; }
}
if (fd < 0) { usleep(100000); continue; }
fd_set set; FD_ZERO(&set); FD_SET(fd, &set);
struct timeval tv = { 0, 100000 };
if (select(fd + 1, &set, NULL, NULL, &tv) <= 0) continue;
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (n <= 0) {
pthread_mutex_lock(&c4fLock);
c4fKlogError = n < 0 ? errno : EPIPE; c4fCaptureUntil = 0;
pthread_mutex_unlock(&c4fLock);
close(fd); fd = -1;
continue;
}
buf[n] = 0;
if (written < C4F_KLOG_FILE_MAX) { c4fWriteAll(C4F_DIAG_KLOG, buf, 1); written += (size_t)n; }
pthread_mutex_lock(&c4fLock);
c4fKlogBytes += (unsigned long long)n;
pthread_mutex_unlock(&c4fLock);
for (ssize_t i = 0; i < n; i++) {
if (buf[i] == '\n' || used + 1 >= sizeof(line)) {
line[used] = 0; c4fKlogAdd(line); used = 0;
if (buf[i] == '\n') continue;
}
line[used++] = buf[i];
}
}
if (fd >= 0) close(fd);
return NULL;
}
void c4fDiagCapture(unsigned milliseconds)
{
pthread_mutex_lock(&c4fLock);
uint64_t until = c4fLauncherTimeMs() + milliseconds;
if (until > c4fCaptureUntil) c4fCaptureUntil = until;
pthread_mutex_unlock(&c4fLock);
}
/* ---- start-up and starts ---- */
void c4fDiagStart(int sandboxResult, int sandboxError)
{
c4fBootMs = c4fLauncherTimeMs();
OrbisKernelSwVersion version;
memset(&version, 0, sizeof(version));
version.Size = sizeof(version);
int ret = sceKernelGetSystemSwVersion(&version);
version.VersionString[sizeof(version.VersionString) - 1] = 0;
snprintf(c4fFirmware, sizeof(c4fFirmware), "\"%s\" = 0x%08x (call returned 0x%08x)",
version.VersionString, version.Version, (uint32_t)ret);
/* GoldHEN returns the version as the call's error value (seen on 13.52:
* 256 = SDK 1.00), so only ENOSYS (78) means there is no GoldHEN call. */
long goldHen = c4fGoldHenCommand(0, NULL);
if (goldHen == -78) snprintf(c4fGoldHen, sizeof(c4fGoldHen), "no answer (errno 78: no GoldHEN SDK call)");
else snprintf(c4fGoldHen, sizeof(c4fGoldHen), "answers, SDK version 0x%lx", goldHen < 0 ? -goldHen : goldHen);
if (sandboxResult == 0) snprintf(c4fSandbox, sizeof(c4fSandbox), "left through GoldHEN");
else snprintf(c4fSandbox, sizeof(c4fSandbox), "still inside, errno %d", sandboxError);
size_t size = 0;
const unsigned char *payload = c4fAutorunBundled(&size);
if (payload) snprintf(c4fBundled, sizeof(c4fBundled), "%zu bytes, crc32 %08x", size, c4fCrc32(payload, size));
else snprintf(c4fBundled, sizeof(c4fBundled), "could not be read");
c4fWriteAll(C4F_DIAG_KLOG, "", 0);
c4fReaderStarted = pthread_create(&c4fKlogReader, NULL, c4fKlogThread, NULL) == 0;
/* The backlog (GoldHEN, AutoRun, earlier runs) is read at the first
* refresh, once it is known whether Control4Free runs. */
}
void c4fDiagStop(void)
{
pthread_mutex_lock(&c4fLock);
c4fQuit = 1;
pthread_mutex_unlock(&c4fLock);
if (c4fReaderStarted) pthread_join(c4fKlogReader, NULL);
}
void c4fDiagBeforeStart(void)
{
free(c4fLogBefore);
c4fLogBefore = c4fReadTail(C4F_DIAG_LOG, C4F_LOG_TAIL, NULL);
c4fStartedAt = c4fLauncherTimeMs();
c4fDiagCapture(30000);
}
void c4fDiagAfterStart(int result, const char *message)
{
char *after = c4fReadTail(C4F_DIAG_LOG, C4F_LOG_TAIL, NULL);
c4fLogChanged = after && (!c4fLogBefore || strcmp(after, c4fLogBefore));
free(after);
c4fStarted = 1;
c4fStartResult = result;
c4fStartTook = c4fLauncherTimeMs() - c4fStartedAt;
snprintf(c4fStartMessage, sizeof(c4fStartMessage), "%s", message);
c4fLastReport = 0; /* save the report straight away */
if (result) c4fDiagCapture(3000);
else { pthread_mutex_lock(&c4fLock); c4fCaptureUntil = 0; pthread_mutex_unlock(&c4fLock); }
}
/* ---- pages ---- */
static void c4fFileSummary(C4fText *t, const char *label, const char *path)
{
long long size = 0;
char *text = c4fReadTail(path, C4F_LOG_TAIL, &size);
if (!text) { c4fAdd(t, " %s: not there (errno %d)\n", label, errno); return; }
char last[200];
const char *firstEnd = strchr(text, '\n');
c4fAdd(t, " %s: %lld bytes\n first: %.*s\n last: %s\n", label, size,
(int)(firstEnd ? firstEnd - text : (long)strlen(text)), text, c4fLastLine(text, last, sizeof(last)));
free(text);
}
static char *c4fSummary(const C4fDiagState *s)
{
C4fText t = {0};
char last[200] = "";
char *log = c4fReadTail(C4F_DIAG_LOG, C4F_LOG_TAIL, NULL);
if (log) c4fLastLine(log, last, sizeof(last));
if (s->running == 1)
c4fAdd(&t, ">> Control4Free is RUNNING (%s). Now open %s on your phone or PC. Whatever happens, come back\n"
">> here and take screenshots of pages 1 and 2: page 2 lists every connection it saw.\n",
s->version, s->address[0] ? s->address : "the PS4's address with :4264");
else if (c4fStarted && c4fContains(c4fStartMessage, "PayLoader did not answer"))
c4fAdd(&t, ">> GoldHEN's PayLoader (port 9090) did not take the payload. Turn PayLoader on in GoldHEN's\n"
">> settings, then press Cross again.\n");
else if (c4fStarted && c4fStartResult == -2 && !c4fLogChanged)
c4fAdd(&t, ">> The payload was handed to PayLoader, but Control4Free's own code never ran (it wrote no log).\n"
">> PayLoader did not run it, or it stopped while starting up on this firmware. The kernel log\n"
">> pages (4 and 5) show what the PS4 said meanwhile.\n");
else if (c4fStarted && c4fLogChanged)
c4fAdd(&t, ">> Control4Free started but does not answer. Its log (page 2) shows how far it got. Last line:\n"
">> %s\n", last);
else if (c4fStarted)
c4fAdd(&t, ">> The start did not work: %s\n", c4fStartMessage);
else
c4fAdd(&t, ">> Not running. Press Cross to start it, wait for the result (up to 30 seconds), then take a\n"
">> screenshot of every page.\n");
free(log);
c4fAdd(&t, "\nConsole\n");
c4fAdd(&t, " system software: %s\n", c4fFirmware);
c4fAdd(&t, " GoldHEN SDK call: %s\n", c4fGoldHen);
c4fAdd(&t, " app sandbox: %s\n", c4fSandbox);
c4fAdd(&t, " network address: %s\n", s->address[0] ? s->address : "(none)");
c4fAdd(&t, "\nThis app\n");
c4fAdd(&t, " version %s, bundled payload %s, open for %llu s\n", C4F_LAUNCHER_VERSION, c4fBundled,
(unsigned long long)((c4fLauncherTimeMs() - c4fBootMs) / 1000));
c4fAdd(&t, "\nService (port 4264)\n");
if (s->running == 1) c4fAdd(&t, " running, version %s, %d controller(s)\n", s->version, s->controllers);
else if (s->running == 0) c4fAdd(&t, " not running (nothing listens)\n");
else c4fAdd(&t, " unclear: %s\n", s->problem[0] ? s->problem : "(no detail)");
c4fAdd(&t, " screen message: %s%s\n", s->message, s->locked ? " [app error: reopen the app]" : "");
c4fAdd(&t, "\nLast start from this app\n");
if (!c4fStarted) c4fAdd(&t, " none yet\n");
else c4fAdd(&t, " result %d after %llu ms; service log %s\n message: %s\n", c4fStartResult,
(unsigned long long)c4fStartTook, c4fLogChanged ? "CHANGED (it ran)" : "unchanged (it did not run)",
c4fStartMessage);
c4fAdd(&t, "\nAuto-start\n");
static const char *const states[] = { "unknown", "off", "on", "on, but an older copy" };
int state = s->autorun + 1;
c4fAdd(&t, " state: %s%s%s\n", state >= 0 && state < 4 ? states[state] : "?", s->autorunNote[0] ? "; " : "",
s->autorunNote);
size_t installedSize = 0;
uint32_t installedCrc = 0;
if (!c4fCrcFile(C4F_DIAG_PAYLOAD, &installedSize, &installedCrc))
c4fAdd(&t, " /data/payloads/control4free.elf: %zu bytes, crc32 %08x\n", installedSize, installedCrc);
else c4fAdd(&t, " /data/payloads/control4free.elf: not there (errno %d)\n", errno);
char *ini = c4fReadTail(C4F_DIAG_INI, 2048, NULL);
if (ini) {
c4fAdd(&t, " /data/GoldHEN/payloads.ini:\n");
for (char *line = strtok(ini, "\n"); line; line = strtok(NULL, "\n")) c4fAdd(&t, " %s\n", line);
free(ini);
} else c4fAdd(&t, " /data/GoldHEN/payloads.ini: not there (errno %d)\n", errno);
c4fAdd(&t, "\nLogs\n");
c4fFileSummary(&t, "control4free.log", C4F_DIAG_LOG);
c4fFileSummary(&t, "control4free.log.previous", C4F_DIAG_LOG ".previous");
pthread_mutex_lock(&c4fLock);
unsigned total = c4fKlogTotal, opens = c4fKlogOpens;
int error = c4fKlogError;
unsigned long long bytes = c4fKlogBytes;
pthread_mutex_unlock(&c4fLock);
c4fAdd(&t, " kernel log: read %u time(s), %llu bytes, %u lines", opens, bytes, total);
if (s->running == 1) c4fAdd(&t, "; not read while Control4Free runs, which needs it to sign controllers in\n");
else if (error == 16) c4fAdd(&t, "; BUSY: a klog viewer is connected to GoldHEN (port 3232). Close it, press Options.\n");
else if (error) c4fAdd(&t, "; last error errno %d\n", error);
else c4fAdd(&t, "\n");
c4fAdd(&t, " report: %s\n", c4fReportNote[0] ? c4fReportNote : "not saved yet");
return c4fTake(&t);
}
static char *c4fLogPage(const char *path)
{
long long size = 0;
char *text = c4fReadTail(path, C4F_LOG_TAIL, &size);
if (!text) {
C4fText t = {0};
c4fAdd(&t, "%s is not there (errno %d).\n", path, errno);
return c4fTake(&t);
}
return text;
}
/* Lines that tend to say why a payload did not run. */
static int c4fInteresting(const char *line)
{
static const char *const words[] = {
"c4f", "control4free", "unsupported", "firmware", "kexec", "payload", "elf", "goldhen", "signal",
"crash", "fault", "panic", "trap", "segv", "party", "rtld", "dlsym", "unable", "fail", "error",
"denied", "9090", "4264",
};
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) if (c4fContains(line, words[i])) return 1;
return 0;
}
static char *c4fKlogPage(int filtered)
{
C4fText t = {0};
pthread_mutex_lock(&c4fLock);
unsigned total = c4fKlogTotal, first = total > C4F_KLOG_KEEP ? total - C4F_KLOG_KEEP : 0;
if (!total) c4fAdd(&t, "Nothing read from the kernel log yet (see the summary page). Options reads it again.\n");
for (unsigned i = first; i < total; i++) {
const char *line = c4fKlog[i % C4F_KLOG_KEEP];
if (!filtered || c4fInteresting(line)) c4fAdd(&t, "%s\n", line);
}
pthread_mutex_unlock(&c4fLock);
return c4fTake(&t);
}
static void c4fSaveReport(void)
{
C4fText t = {0};
pthread_mutex_lock(&c4fLock);
for (int i = 0; i < C4F_DIAG_PAGES; i++)
c4fAdd(&t, "===== %s =====\n%s\n", c4fPageNames[i], c4fPages[i] ? c4fPages[i] : "");
pthread_mutex_unlock(&c4fLock);
char *text = c4fTake(&t);
c4fWriteAll(C4F_DIAG_REPORT, text, 0);
snprintf(c4fReportNote, sizeof(c4fReportNote), "/data/control4free/diag-report.txt");
static const char *const usb[] = { "/mnt/usb0", "/mnt/usb1" };
for (int i = 0; i < 2; i++) {
char path[64];
snprintf(path, sizeof(path), "%s/control4free-diag.txt", usb[i]);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) continue;
close(fd);
c4fWriteAll(path, text, 0);
size_t used = strlen(c4fReportNote);
snprintf(c4fReportNote + used, sizeof(c4fReportNote) - used, " and %s", path);
}
free(text);
}
unsigned c4fDiagRefresh(const C4fDiagState *state)
{
pthread_mutex_lock(&c4fLock);
c4fServiceUp = state->running == 1;
if (c4fServiceUp) c4fCaptureUntil = 0;
pthread_mutex_unlock(&c4fLock);
if (!c4fRefreshed++ && state->running != 1) c4fDiagCapture(4000);
char *pages[C4F_DIAG_PAGES] = {
c4fSummary(state), c4fLogPage(C4F_DIAG_LOG), c4fLogPage(C4F_DIAG_LOG ".previous"),
c4fKlogPage(1), c4fKlogPage(0),
};
int changed = 0;
pthread_mutex_lock(&c4fLock);
for (int i = 0; i < C4F_DIAG_PAGES; i++) {
if (c4fPages[i] && !strcmp(c4fPages[i], pages[i])) { free(pages[i]); continue; }
free(c4fPages[i]);
c4fPages[i] = pages[i];
changed = 1;
}
if (changed) c4fGeneration++;
unsigned generation = c4fGeneration;
pthread_mutex_unlock(&c4fLock);
uint64_t now = c4fLauncherTimeMs();
if (changed && (!c4fLastReport || now - c4fLastReport >= 10000)) { c4fSaveReport(); c4fLastReport = now; }
return generation;
}
/* ---- drawing ---- */
typedef struct { const char *text; int length, highlight; } C4fRow;
/* How many characters of a line fit in the width. */
static int c4fFit(const char *text, int length, float width)
{
char line[400];
int n = length < (int)sizeof(line) - 1 ? length : (int)sizeof(line) - 1;
memcpy(line, text, (size_t)n); line[n] = 0;
float measured = c4fTextWidth(C4F_FONT_REGULAR, C4F_D_SIZE, line);
if (measured <= width) return n;
n = (int)(n * width / measured);
if (n < 1) n = 1;
line[n] = 0;
while (n > 1 && c4fTextWidth(C4F_FONT_REGULAR, C4F_D_SIZE, line) > width) line[--n] = 0;
return n;
}
void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
{
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
const float width = C4F_SCREEN_WIDTH - 2 * C4F_D_LEFT;
int page = s->diagPage >= 0 && s->diagPage < C4F_DIAG_PAGES ? s->diagPage : 0;
char line[320];
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_D_BG);
/* The build time tells one diagnostic build from the next in a screenshot. */
snprintf(line, sizeof(line), "Control4Free DIAGNOSTIC BUILD %s (%s %s) page %d of %d: %s", C4F_LAUNCHER_VERSION,
__DATE__, __TIME__, page + 1, C4F_DIAG_PAGES, c4fPageNames[page]);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_TITLE, C4F_D_LEFT, 48, C4F_D_ACCENT, line);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 84, C4F_D_FAINT,
"L1 / R1: change page Up / Down: scroll Options: read the kernel log again "
"Cross: start Triangle: auto-start on/off Square: stop Circle: close");
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 114, C4F_D_WARN,
"Please take a screenshot of EVERY page (hold SHARE, or press SHARE then Triangle) and send them all.");
if (s->confirmStop) snprintf(line, sizeof(line), "Press Cross to stop Control4Free, or Circle to cancel.");
else snprintf(line, sizeof(line), "%s%s", s->busy ? "WORKING: " : "Now: ", s->message);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 146, C4F_D_TEXT, line);
c4fFillRect(&c, C4F_D_LEFT, 160, (int)width, 1, C4F_D_LINE);
pthread_mutex_lock(&c4fLock);
char *text = strdup(c4fPages[page] ? c4fPages[page] : "(not read yet)");
pthread_mutex_unlock(&c4fLock);
if (!text) return;
/* Wrap every line, then show the window the scroll position picks. */
int count = 0, capacity = 256;
C4fRow *rows = malloc(sizeof(*rows) * (size_t)capacity);
for (char *p = text; rows && *p; ) {
char *end = strchr(p, '\n');
int length = end ? (int)(end - p) : (int)strlen(p);
int highlight = !strncmp(p, ">> ", 3);
if (highlight) { p += 3; length -= 3; }
do {
int fit = length ? c4fFit(p, length, width) : 0;
if (count == capacity) {
C4fRow *grown = realloc(rows, sizeof(*rows) * (size_t)(capacity *= 2));
if (!grown) break;
rows = grown;
}
rows[count++] = (C4fRow){ p, fit, highlight };
p += fit; length -= fit;
} while (length > 0);
p = end ? end + 1 : p + strlen(p);
}
int visible = (C4F_D_BOTTOM - C4F_D_TOP) / C4F_D_ROW;
int maxScroll = count > visible ? count - visible : 0;
c4fMaxScroll[page] = maxScroll;
int first = s->diagScroll[page] >= C4F_DIAG_END ? maxScroll : s->diagScroll[page];
if (first > maxScroll) first = maxScroll;
if (first < 0) first = 0;
for (int i = 0; rows && i < visible && first + i < count; i++) {
const C4fRow *row = &rows[first + i];
c4fTextN(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, C4F_D_TOP + i * C4F_D_ROW,
row->highlight ? C4F_D_WARN : C4F_D_TEXT, row->text, row->length);
}
snprintf(line, sizeof(line), "lines %d-%d of %d", count ? first + 1 : 0, first + visible < count ? first + visible : count, count);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 1072, C4F_D_FAINT, line);
free(rows);
free(text);
}
int c4fDiagMaxScroll(int page)
{
return page >= 0 && page < C4F_DIAG_PAGES ? c4fMaxScroll[page] : 0;
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef C4F_LAUNCHER_DIAG_H
#define C4F_LAUNCHER_DIAG_H
/* Diagnostic build (make -C launcher C4F_DIAG=1): plain pages of everything
* that says why Control4Free does not start on a console, in place of the
* normal screen, so a user can send screenshots instead of log files. */
#include <stdint.h>
#include "screen.h"
#ifdef __cplusplus
extern "C" {
#endif
#define C4F_DIAG_PAGES 5
/* A page's scroll position meaning "show the end". */
#define C4F_DIAG_END 1000000
typedef struct {
int running, controllers, autorun, locked, busy, confirmStop;
const char *version, *problem, *message, *address, *autorunNote;
} C4fDiagState;
/* Records what is known at start-up and reads the kernel log's backlog. */
void c4fDiagStart(int sandboxResult, int sandboxError);
void c4fDiagStop(void);
/* Holds the kernel log open for this long, reading it. */
void c4fDiagCapture(unsigned milliseconds);
/* Around a start: what the service log looked like before, and the result. */
void c4fDiagBeforeStart(void);
void c4fDiagAfterStart(int result, const char *message);
/* Rebuilds the pages; returns their generation, which changes with them. */
unsigned c4fDiagRefresh(const C4fDiagState *state);
void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *screen);
/* The largest useful scroll position of a page, as of its last drawing. */
int c4fDiagMaxScroll(int page);
#ifdef __cplusplus
}
#endif
#endif
+45 -3
View File
@@ -16,6 +16,9 @@
#include "sandbox.h"
#include "screen.h"
#include "service.h"
#ifdef C4F_DIAG
#include "diag.h"
#endif
#define C4F_BUNDLED_PAYLOAD "/app0/assets/control4free.elf"
@@ -54,7 +57,7 @@ static void *c4fWorker(void *)
if (quit) return NULL;
if (command || c4fLauncherTimeMs() >= nextCheck) {
char message[160] = {0}, address[64] = {0};
int result = 0;
[[maybe_unused]] int result = 0; /* read by the diagnostic build */
/* 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;
@@ -66,6 +69,9 @@ static void *c4fWorker(void *)
} else {
c4fLauncherSetHost(NULL);
}
#ifdef C4F_DIAG
if (command == C4F_DO_START || command == C4F_DO_SET_UP) c4fDiagBeforeStart();
#endif
if (command == C4F_DO_START) {
size_t payloadSize;
const unsigned char *payload = c4fAutorunBundled(&payloadSize);
@@ -73,6 +79,9 @@ static void *c4fWorker(void *)
}
if (command == C4F_DO_STOP) result = c4fLauncherStop(message, sizeof(message));
if (command == C4F_DO_SET_UP) result = c4fSetUp(message, sizeof(message));
#ifdef C4F_DIAG
if (command == C4F_DO_START || command == C4F_DO_SET_UP) c4fDiagAfterStart(result, message);
#endif
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));
@@ -80,7 +89,6 @@ static void *c4fWorker(void *)
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 &&
@@ -98,6 +106,15 @@ static void *c4fWorker(void *)
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;
#ifdef C4F_DIAG
C4fLauncherScreen copy = c4fScreen;
pthread_mutex_unlock(&c4fMutex);
C4fDiagState diag = { running, status.controllers, autorun, copy.locked, copy.busy, copy.confirmStop,
status.version, problem, copy.message, copy.address, copy.autorunNote };
unsigned generation = c4fDiagRefresh(&diag);
pthread_mutex_lock(&c4fMutex);
c4fScreen.diagGeneration = generation;
#endif
pthread_mutex_unlock(&c4fMutex);
first = 0; nextCheck = c4fLauncherTimeMs() + 2000;
}
@@ -175,7 +192,12 @@ int main(void)
if (c4fAutorunLoadBundled(C4F_BUNDLED_PAYLOAD)) printf("[c4f-launcher] bundled payload unreadable\n");
/* The sandbox refuses connections to the console itself (EACCES), so the
* app could reach neither Control4Free nor PayLoader from inside it. */
if (c4fSandboxLeave()) printf("[c4f-launcher] could not leave the sandbox, errno %d\n", errno);
int sandbox = c4fSandboxLeave(), sandboxError = errno;
if (sandbox) printf("[c4f-launcher] could not leave the sandbox, errno %d\n", sandboxError);
#ifdef C4F_DIAG
c4fDiagStart(sandbox, sandboxError);
for (int i = 0; i < C4F_DIAG_PAGES; i++) c4fScreen.diagScroll[i] = i ? C4F_DIAG_END : 0;
#endif
pthread_t worker;
int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0;
if (!workerStarted || pad < 0) {
@@ -220,11 +242,28 @@ int main(void)
off ? "Turning auto-start off..." : "Turning auto-start on...");
} else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1;
}
#ifdef C4F_DIAG
/* Pages and scrolling work while busy too. */
int page = c4fScreen.diagPage;
if (pressed & (ORBIS_PAD_BUTTON_R1 | ORBIS_PAD_BUTTON_RIGHT)) c4fScreen.diagPage = (page + 1) % C4F_DIAG_PAGES;
if (pressed & (ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_LEFT)) c4fScreen.diagPage = (page + C4F_DIAG_PAGES - 1) % C4F_DIAG_PAGES;
if (pressed & (ORBIS_PAD_BUTTON_UP | ORBIS_PAD_BUTTON_DOWN)) {
int max = c4fDiagMaxScroll(page), at = c4fScreen.diagScroll[page];
if (at >= C4F_DIAG_END) at = max;
at += pressed & ORBIS_PAD_BUTTON_UP ? -10 : 10;
c4fScreen.diagScroll[page] = at >= max ? C4F_DIAG_END : at < 0 ? 0 : at;
}
if (pressed & ORBIS_PAD_BUTTON_OPTIONS) c4fDiagCapture(4000);
#endif
C4fLauncherScreen snapshot = c4fScreen;
pthread_mutex_unlock(&c4fMutex);
/* Redraw only when something changed, then copy it to each buffer once. */
if (!drawn || memcmp(&snapshot, &drawnState, sizeof(snapshot))) {
#ifdef C4F_DIAG
c4fDrawDiag(canvas, &snapshot);
#else
c4fDrawLauncher(canvas, &snapshot);
#endif
drawnState = snapshot; drawn = 1; shown[0] = shown[1] = 0;
}
if (!shown[buffer]) { memcpy(c4fBuffers[buffer], canvas, frameBytes); shown[buffer] = 1; }
@@ -243,6 +282,9 @@ int main(void)
}
pthread_mutex_lock(&c4fMutex); c4fQuit = 1; pthread_mutex_unlock(&c4fMutex);
if (workerStarted) pthread_join(worker, NULL);
#ifdef C4F_DIAG
c4fDiagStop();
#endif
if (pad >= 0) scePadClose(pad);
sceNetCtlTerm();
c4fCloseVideo();
+16 -5
View File
@@ -30,21 +30,32 @@ static long c4fGoldHen(uint64_t command, void *data)
return failed ? -ret : ret;
}
long c4fGoldHenCommand(unsigned long command, void *data)
{
/* Without GoldHEN the call does not exist: get an error, not SIGSYS. The
* kernel's SIGSYS is FreeBSD's 12; OpenOrbis's header has Linux's 31. */
signal(12, SIG_IGN);
return c4fGoldHen(command, data);
}
int c4fSandboxLeave(void)
{
static C4fJailbreak backup;
if (c4fOut) return 0;
/* Without GoldHEN the call does not exist: get an error, not SIGSYS. The
* kernel's SIGSYS is FreeBSD's 12; OpenOrbis's header has Linux's 31. */
signal(12, SIG_IGN);
memset(&backup, 0, sizeof(backup));
long ret = c4fGoldHen(2, &backup);
long ret = c4fGoldHenCommand(2, &backup);
if (ret != 0) { errno = ret < 0 ? (int)-ret : EPERM; return -1; }
c4fOut = 1;
return 0;
}
#else
/* Host tests: there is no sandbox. */
/* Host tests: there is no sandbox, and no GoldHEN. */
long c4fGoldHenCommand(unsigned long command, void *data)
{
(void)command; (void)data;
return -ENOSYS;
}
int c4fSandboxLeave(void)
{
if (getenv("C4F_TEST_NO_GOLDHEN")) { errno = ENOSYS; return -1; }
+3
View File
@@ -10,6 +10,9 @@ extern "C" {
/* 0 when out of the sandbox (again: no-op); -1 with errno otherwise. */
int c4fSandboxLeave(void);
int c4fSandboxIsOut(void);
/* GoldHEN's SDK call (syscall 500): its result, or -errno when the call
* failed or does not exist. */
long c4fGoldHenCommand(unsigned long command, void *data);
#ifdef __cplusplus
}
+4 -4
View File
@@ -428,10 +428,10 @@ static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSiz
"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";
look.title = "Restart the PS4";
look.subtitle = "Control4Free was sent but never answered. Restart the console before you try again, so "
"two copies never run at once.";
look.pill = look.meta = "App error";
look.title = "Reopen the app";
look.subtitle = "The app could not start properly. Close it with the PS button and open it again. "
"Control4Free itself is not affected.";
} else if (s->busy) {
look = (C4fLook){ "Working", "Please wait", s->message, "Checking", C4F_WARN, C4F_ACCENT, 0 };
} else if (s->running == 1) {
+4
View File
@@ -11,6 +11,10 @@ typedef struct {
int running, busy, controllers, confirmStop, locked;
int autorun; /* C4F_AUTORUN_* from autorun.h */
char address[64], message[160], autorunNote[96];
#ifdef C4F_DIAG
unsigned diagGeneration; /* bumped when a diagnostic page changes */
int diagPage, diagScroll[8];
#endif
} C4fLauncherScreen;
#ifdef __cplusplus
extern "C" {
+5 -3
View File
@@ -242,7 +242,7 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Turn it on in GoldHEN, then press Cross again.", errno); return -1; }
int failed = c4fSendAll(fd, payload, payloadSize, c4fLauncherTimeMs() + 10000);
shutdown(fd, SHUT_WR); close(fd);
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Restart the PS4 before you try again."); return -2; }
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Press Cross to try again."); return -2; }
uint64_t deadline;
deadline = c4fLauncherTimeMs() + 20000;
while (c4fLauncherTimeMs() < deadline) {
@@ -251,8 +251,10 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
}
usleep(250000);
}
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. */
snprintf(message, size, "Sent, but Control4Free did not start (%s). Press Cross to try again.", c4fProblem);
/* Sending again is safe: a copy that did start holds the payload's
* instance lock, so a second one quits at once. */
return -2;
}
int c4fLauncherStop(char *message, size_t size)
+1
View File
@@ -21,6 +21,7 @@ int c4fLauncherProbe(C4fServiceStatus *status);
/* Whether the last probe connected anywhere, so "no answer" can be told apart
* from "cannot reach". */
int c4fLauncherReached(void);
/* 0 running, -1 not sent, -2 sent but Control4Free did not answer. */
int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *message, size_t size);
int c4fLauncherStop(char *message, size_t size);
#ifdef __cplusplus
+5
View File
@@ -102,5 +102,10 @@ void c4fNotify(const char *fmt, ...)
va_start(ap, fmt);
(void)vsnprintf(req.message, sizeof(req.message), fmt, ap);
va_end(ap);
#ifdef C4F_DIAG
int ret = sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
c4fLog("notification = 0x%08x: %s\n", (uint32_t)ret, req.message);
#else
(void)sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
#endif
}
+43
View File
@@ -100,6 +100,45 @@ static void c4fRestoreCredentials(void)
c4fLogCredentials("restored", g_pid);
}
#ifdef C4F_DIAG
#include <signal.h>
#include <string.h>
/* Diagnostic build: what the SDK's start-up code found in the kernel, and
* which process PayLoader runs us in, from GoldHEN's SDK call (syscall 500,
* command 4; struct proc_info of the GoldHEN Plugins SDK). */
typedef struct {
int pid;
char name[40], path[64], titleId[16], contentId[64], version[6];
uint64_t baseAddress;
} __attribute__((packed)) C4fProcInfo;
static void c4fDiagHost(pid_t pid)
{
C4fProcInfo info;
struct sigaction ignore, previous;
c4fLog("diag: built %s %s\n", __DATE__, __TIME__);
c4fLog("diag: kernel image base=0x%lx size=0x%lx allproc=0x%lx prison0=0x%lx rootvnode=0x%lx targetid=0x%lx\n",
(unsigned long)KERNEL_ADDRESS_IMAGE_BASE, (unsigned long)KERNEL_IMAGE_SIZE,
(unsigned long)KERNEL_ADDRESS_ALLPROC, (unsigned long)KERNEL_ADDRESS_PRISON0,
(unsigned long)KERNEL_ADDRESS_ROOTVNODE, (unsigned long)KERNEL_ADDRESS_TARGETID);
c4fLog("diag: proc=0x%lx ucred=0x%lx\n", (unsigned long)kernel_get_proc(pid),
(unsigned long)kernel_get_proc_ucred(pid));
/* Without GoldHEN the call does not exist: get an error, not SIGSYS. */
memset(&info, 0, sizeof(info));
memset(&ignore, 0, sizeof(ignore));
ignore.sa_handler = SIG_IGN;
sigaction(SIGSYS, &ignore, &previous);
int ret = syscall(500, 4, &info);
sigaction(SIGSYS, &previous, NULL);
info.name[sizeof(info.name) - 1] = info.path[sizeof(info.path) - 1] = info.titleId[sizeof(info.titleId) - 1] = 0;
c4fLog("diag: GoldHEN process info = %d: pid=%d name=%s path=%s title=%s\n",
ret, info.pid, info.name, info.path, info.titleId);
}
#endif
static int c4fFinish(int status)
{
c4fRestoreCredentials();
@@ -127,6 +166,10 @@ int main(void)
}
c4fLogOpen();
c4fLog("Control4Free %s: pid=%d firmware=0x%08x\n", C4F_VERSION, pid, kernel_get_fw_version());
#ifdef C4F_DIAG
c4fNotify("Control4Free diagnostic build started (firmware %08x)", kernel_get_fw_version());
c4fDiagHost(pid);
#endif
c4fLogCredentials("on entry", pid);
if (c4fRaiseCredentials(pid) != 0) return c4fFinish(1);
+41 -6
View File
@@ -14,6 +14,16 @@
#include <unistd.h>
#include "c4f_net.h"
#ifdef C4F_DIAG
#include "c4f_log.h"
/* Diagnostic build: each connection from another device, step by step. The
* launcher's status checks over loopback come every 2 seconds and stay out. */
#define C4F_NET_LOG(c, fmt, ...) do { if (strcmp((c)->peer, "127.0.0.1")) \
c4fLog("net: %s " fmt "\n", (c)->peer, __VA_ARGS__); } while (0)
#else
#define C4F_NET_LOG(c, fmt, ...) do { } while (0)
#endif
/* Which interface would carry traffic out, without sending anything: a connected
* UDP socket picks the route, and getsockname then names the local end. */
void c4fNetLocalAddress(char *out, size_t size)
@@ -152,6 +162,7 @@ static void c4fHttpError(C4fNetClient *c, int code, const char *message)
{
char buf[512];
int n = snprintf(buf, sizeof(buf), "HTTP/1.1 %d Error\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n\r\n%s", code, strlen(message), message);
C4F_NET_LOG(c, "answered %d %s", code, message);
c4fQueue(c, buf, (size_t)n); c->closing = 2;
}
@@ -165,6 +176,7 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
if (!end) { if (c->rxUsed == sizeof(c->rx)) c4fHttpError(c, 431, "Headers too large"); return; }
used = (size_t)(end-request) + 4;
request[used] = 0;
C4F_NET_LOG(c, "asked: %.*s", (int)strcspn(request, "\r\n") > 100 ? 100 : (int)strcspn(request, "\r\n"), request);
int statusRequest = !strncmp(request, "GET /api/status HTTP/1.1\r\n", 26);
int stopRequest = !strncmp(request, "POST /api/stop HTTP/1.1\r\n", 25);
/* The two files a phone needs to keep this page on its home screen. */
@@ -231,17 +243,20 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
if (!strstr(connection, "upgrade")) { c4fHttpError(c, 400, "Upgrade required"); return; }
c4fWebSocketAccept(key, accept);
n = snprintf(reply, sizeof(reply), "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", accept);
C4F_NET_LOG(c, "websocket open%s", "");
c4fQueue(c, reply, (size_t)n); c->websocket = 1;
memmove(c->rx, c->rx+used, c->rxUsed-used); c->rxUsed -= used;
net->handler(c, C4F_NET_OPEN, NULL, 0, net->context);
} else if (asset) {
char reply[256];
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: max-age=86400\r\nX-Content-Type-Options: nosniff\r\n\r\n", assetType, assetSize);
C4F_NET_LOG(c, "sending %s, %zu bytes", assetType, assetSize);
c4fQueue(c, reply, (size_t)n);
c->body = asset; c->bodySize = assetSize; c->rxUsed = 0;
} else {
char reply[768];
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; media-src data:; connect-src ws:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
C4F_NET_LOG(c, "sending the page, %zu bytes", c4fPageSize);
c4fQueue(c, reply, (size_t)n);
c->body = c4fPage; c->bodySize = c4fPageSize; c->rxUsed = 0;
}
@@ -343,6 +358,12 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
return -1;
for (index = 0; index < C4F_NET_CLIENTS && net->clients[index].fd >= 0; index++) {}
if (fd >= 0) {
#ifdef C4F_DIAG
char peerName[16] = "?";
inet_ntop(AF_INET, &peer.sin_addr, peerName, sizeof(peerName));
if (index == C4F_NET_CLIENTS || fd >= (int)FD_SETSIZE)
c4fLog("net: %s turned away: fd %d, %s\n", peerName, fd, index == C4F_NET_CLIENTS ? "all slots taken" : "fd too high");
#endif
if (index == C4F_NET_CLIENTS || fd >= (int)FD_SETSIZE || fcntl(fd, F_SETFL, O_NONBLOCK)) close(fd);
else {
int one = 1;
@@ -351,6 +372,10 @@ int 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();
#ifdef C4F_DIAG
memcpy(net->clients[index].peer, peerName, sizeof(peerName));
C4F_NET_LOG(&net->clients[index], "connected: slot %d, fd %d", index, fd);
#endif
}
}
}
@@ -359,8 +384,10 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
if (c->fd < 0) continue;
if (FD_ISSET(c->fd, &rd)) {
ssize_t n = recv(c->fd, c->rx+c->rxUsed, sizeof(c->rx)-c->rxUsed, 0);
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) c->closing = 1;
else if (n > 0) {
if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) {
C4F_NET_LOG(c, "recv = %zd, errno %d: it closed its end", n, n < 0 ? errno : 0);
c->closing = 1;
} else if (n > 0) {
c->rxUsed += (size_t)n;
if (!c->websocket) c4fHttp(net, c);
if (c->websocket) c4fWs(net, c);
@@ -376,15 +403,23 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
flags = MSG_NOSIGNAL;
#endif
ssize_t n = send(c->fd, data, left, flags);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) c->closing = 1;
else if (n > 0) {
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
C4F_NET_LOG(c, "send of %zu bytes failed, errno %d (%s, %zu sent)", left, errno,
body ? "body" : "header", body ? c->bodySent : c->txSent);
c->closing = 1;
} else if (n > 0) {
if (body) {
c->bodySent += (size_t)n;
if (c->bodySent == c->bodySize) { c->body = NULL; c->closing = 1; }
if (c->bodySent == c->bodySize) { C4F_NET_LOG(c, "sent all %zu bytes", c->bodySize); c->body = NULL; c->closing = 1; }
} else { c->txSent += (size_t)n; if (c->txSent == c->txUsed) c->txSent = c->txUsed = 0; }
}
}
if (!c->websocket && c4fTimeMs()-c->openedMs > 10000) c->closing = 1;
if (!c->websocket && c4fTimeMs()-c->openedMs > 10000) {
/* A first byte of 0x16 is a TLS handshake: the browser tried https. */
C4F_NET_LOG(c, "timed out after 10 s with %zu bytes unanswered, first byte 0x%02x", c->rxUsed,
c->rxUsed ? c->rx[0] : 0);
c->closing = 1;
}
if (c->closing == 1 || (c->closing == 2 && c->txSent == c->txUsed)) c4fDrop(net, c);
}
return 0;
+7 -3
View File
@@ -1,16 +1,20 @@
<# Build the payload and installable PS4 launcher locally. Requires Docker. #>
<# Build the payload and installable PS4 launcher locally. Requires Docker.
-Diag builds the diagnostic package instead (Control4Free-<version>-diag.pkg). #>
param([switch]$Diag)
$ErrorActionPreference = 'Stop'
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$flag = if ($Diag) { 'C4F_DIAG=1' } else { '' }
$suffix = if ($Diag) { '-diag' } else { '' }
Push-Location $projectRoot
try {
docker build -t control4free-build -f docker/Dockerfile docker
if ($LASTEXITCODE -ne 0) { throw 'Payload toolchain build failed.' }
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker
if ($LASTEXITCODE -ne 0) { throw 'Launcher toolchain build failed.' }
docker run --rm --network none -v "${projectRoot}:/src" -w /src control4free-launcher-build bash -lc 'make && make -C launcher'
docker run --rm --network none -v "${projectRoot}:/src" -w /src control4free-launcher-build bash -lc "make $flag && make -C launcher $flag"
if ($LASTEXITCODE -ne 0) { throw 'Launcher package build failed.' }
$version = (Get-Content -LiteralPath (Join-Path $projectRoot 'VERSION') -Raw).Trim()
Get-Item -LiteralPath (Join-Path $projectRoot "build/Control4Free-$version.pkg") | Select-Object FullName,Length
Get-Item -LiteralPath (Join-Path $projectRoot "build/Control4Free-$version$suffix.pkg") | Select-Object FullName,Length
} finally {
Pop-Location
}
+1 -1
View File
@@ -66,7 +66,7 @@ int main(int argc, char **argv)
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 Control4Free never answered (127.0.0.1 no reply). Restart the PS4 before you try again.";
message = "Could not initialize launcher. Close with PS and retry.";
} else if (!strcmp(state, "unknown")) {
s.running = -1; s.autorun = C4F_AUTORUN_UNKNOWN;
snprintf(s.autorunNote, sizeof(s.autorunNote), "GoldHEN did not let the app check (errno 78)");
+13 -4
View File
@@ -95,9 +95,17 @@ def render(args, path, width, height):
def main():
global OUT, STAGE, ART, FONTS
parser = argparse.ArgumentParser()
parser.add_argument('--sdk', required=True, type=Path)
sdk = parser.parse_args().sdk
# The diagnostic build (make -C launcher C4F_DIAG=1): its own folder, payload,
# title and file name, so it can never be mistaken for a release.
parser.add_argument('--diag', action='store_true')
args = parser.parse_args()
sdk = args.sdk
suffix = '-diag' if args.diag else ''
OUT = ROOT / f'build/launcher{suffix}'
STAGE, ART, FONTS = OUT / 'pkg', OUT / 'launcher-art', OUT / 'fonts.c'
binary = sdk / 'bin/linux'
# Start clean, so nothing left over from an older build gets packaged.
shutil.rmtree(STAGE, ignore_errors=True)
@@ -106,7 +114,7 @@ def main():
run = lambda *args: subprocess.run([str(arg) for arg in args], cwd=STAGE, check=True)
run(binary / 'create-fself', '-in=' + str(OUT / 'launcher.elf'), '-out=' + str(OUT / 'launcher.oelf'),
'--eboot', 'eboot.bin', '--paid', '0x3800000000000011')
shutil.copyfile(ROOT / 'build/control4free.elf', STAGE / 'assets/control4free.elf')
shutil.copyfile(ROOT / f'build/control4free{suffix}.elf', STAGE / 'assets/control4free.elf')
shutil.copyfile(sdk / 'samples/graphics/sce_sys/about/right.sprx', STAGE / 'sce_sys/about/right.sprx')
# OpenOrbis's stub modules. Its CHANGELOG (v0.5) says homebrew packages
# should carry them in sce_module/, and every sample does.
@@ -136,7 +144,8 @@ def main():
'APP_TYPE': ('Integer', 4, '1'), 'APP_VER': ('Utf8', 8, SFO_VERSION),
'ATTRIBUTE': ('Integer', 4, '32'), 'CATEGORY': ('Utf8', 4, 'gde'),
'CONTENT_ID': ('Utf8', 48, CONTENT_ID), 'DOWNLOAD_DATA_SIZE': ('Integer', 4, '0'),
'SYSTEM_VER': ('Integer', 4, '0'), 'TITLE': ('Utf8', 128, 'Control4Free'),
'SYSTEM_VER': ('Integer', 4, '0'),
'TITLE': ('Utf8', 128, 'Control4Free (diagnostic)' if args.diag else 'Control4Free'),
'TITLE_ID': ('Utf8', 12, TITLE_ID), 'VERSION': ('Utf8', 8, SFO_VERSION),
}
for key, (kind, size, value) in entries.items():
@@ -163,7 +172,7 @@ def main():
shutil.copyfile(project, OUT / 'launcher.gp4')
run(pkgtool, 'pkg_build', project, OUT)
generated = OUT / (CONTENT_ID + '.pkg')
target = ROOT / f'build/Control4Free-{VERSION}.pkg'
target = ROOT / f'build/Control4Free-{VERSION}{suffix}.pkg'
shutil.copyfile(generated, target)
print(f'Built {target} ({target.stat().st_size:,} bytes)', flush=True)