diff --git a/Makefile b/Makefile index 4f1a19e..6e523e1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/launcher/Makefile b/launcher/Makefile index ceedc8f..fadbf3e 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -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) diff --git a/launcher/diag.c b/launcher/diag.c new file mode 100644 index 0000000..22a7b48 --- /dev/null +++ b/launcher/diag.c @@ -0,0 +1,602 @@ +/* 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 is read only in short windows (start-up, and around a start) + * because it has a single reader, and Control4Free needs it to sign a + * controller in. */ +#include "diag.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; + +/* 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 = 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); + long goldHen = c4fGoldHenCommand(0, NULL); + if (goldHen >= 0) snprintf(c4fGoldHen, sizeof(c4fGoldHen), "answers, SDK version 0x%lx", goldHen); + else snprintf(c4fGoldHen, sizeof(c4fGoldHen), "no answer, errno %ld (78 = no GoldHEN SDK call)", -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; + c4fDiagCapture(4000); /* the backlog: GoldHEN, AutoRun and earlier 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 */ + c4fDiagCapture(3000); +} + +/* ---- 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 try it from your phone. If a controller fails, come back\n" + ">> here and take screenshots of every page again.\n", s->version); + 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 (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) +{ + 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); + + snprintf(line, sizeof(line), "Control4Free DIAGNOSTIC BUILD %s page %d of %d: %s", C4F_LAUNCHER_VERSION, + 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; +} diff --git a/launcher/diag.h b/launcher/diag.h new file mode 100644 index 0000000..5736e61 --- /dev/null +++ b/launcher/diag.h @@ -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 +#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 diff --git a/launcher/main.cpp b/launcher/main.cpp index d026e6f..8d6b214 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -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" @@ -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)); @@ -98,6 +107,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 +193,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 +243,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 +283,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(); diff --git a/launcher/sandbox.c b/launcher/sandbox.c index 8d7a714..06fdfc7 100644 --- a/launcher/sandbox.c +++ b/launcher/sandbox.c @@ -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; } diff --git a/launcher/sandbox.h b/launcher/sandbox.h index 6e99b9f..30de733 100644 --- a/launcher/sandbox.h +++ b/launcher/sandbox.h @@ -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 } diff --git a/launcher/screen.h b/launcher/screen.h index 548016d..a6bbaf0 100644 --- a/launcher/screen.h +++ b/launcher/screen.h @@ -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" { diff --git a/src/main.c b/src/main.c index b9c0b26..d685943 100644 --- a/src/main.c +++ b/src/main.c @@ -100,6 +100,44 @@ static void c4fRestoreCredentials(void) c4fLogCredentials("restored", g_pid); } +#ifdef C4F_DIAG +#include +#include + +/* 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: 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 +165,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); diff --git a/tools/build-launcher.ps1 b/tools/build-launcher.ps1 index e91770e..ff96ea7 100644 --- a/tools/build-launcher.ps1 +++ b/tools/build-launcher.ps1 @@ -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--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 } diff --git a/tools/package_launcher.py b/tools/package_launcher.py index f691ff5..e6c36dc 100644 --- a/tools/package_launcher.py +++ b/tools/package_launcher.py @@ -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)