mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-07 15:00:35 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
017b8e2473 | ||
|
|
96326c71b0 | ||
|
|
f57224ee39 | ||
|
|
9d0c3d6a7d | ||
|
|
b583d149d8 | ||
|
|
eceeb7bf46 | ||
|
|
056a32f6c9 | ||
|
|
745c16decf | ||
|
|
94e4a7ecd7 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)");
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user